Freelancer tax software

TaxTidy

Turn Tax Chaos Into Calm

TaxTidy automatically collects, categorizes, and stores freelancers’ tax documents from invoices, bank feeds, and receipt photos, producing IRS-ready, annotated tax packets. Freelance creatives and solo consultants (25–45) using mobile-first workflows rely on it to extract tax data, match expenses, and cut tax-prep time by roughly 60%, saving hours each quarter.

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

TaxTidy

Product Details

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

Vision & Mission

Vision
Empower freelancers worldwide to reclaim time and eliminate tax stress by turning chaotic receipts into instant, audit-ready records.
Long Term Goal
Within 4 years, serve 100,000 freelancers, automate 250,000 IRS-ready filings annually, save 10 million freelance hours, and cut missed deductions by 20%.
Impact
TaxTidy reduces tax-prep time by 60% for freelance creatives and solo consultants (25–45), saving an average eight hours per quarter, decreasing missed deductions by 20%, cutting accountant revisions and document errors by 35%, and enabling IRS-ready filings 30% faster.

Problem & Solution

Problem Statement
Mobile-first freelance creatives and solo consultants struggle to assemble scattered invoices, receipt photos, and bank feeds into IRS-ready, categorized tax packets; generic accounting tools require manual reconciliation and don’t produce audit-ready bundles.
Solution Overview
TaxTidy automatically ingests invoices, bank feeds, and receipt photos, uses OCR and smart-matching to categorize expenses, then compiles annotated, IRS-ready tax packets per fiscal period—eliminating manual reconciliation and saving freelancers hours during tax season.

Details & Audience

Description
TaxTidy automatically collects, categorizes, and stores freelancers’ tax documents from invoices, bank feeds, and receipt photos. Freelance creatives and solo consultants (25–45) juggling irregular income and using mobile-first workflows rely on it. It reduces tax-time stress by extracting tax-relevant data, matching expenses to categories, and producing audit-ready records that save hours per quarter. Its IRS-ready bundle creator compiles annotated, downloadable tax packets per fiscal period.
Target Audience
Freelance creatives and solo consultants (25–45) juggling irregular income needing tax clarity, preferring mobile-first workflows.
Inspiration
At midnight I watched a freelance photographer crouched over her kitchen counter, phone glowing, an email inbox overflowing, bank CSVs open on her laptop, and crumpled receipt photos scattered like confetti. She mumbled missed deductions while manually matching lines. That frantic, needle-in-a-haystack moment—inspired TaxTidy: automatic capture, smart matching, and one IRS-ready packet to restore calm.

User Personas

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

A

Automation Avery

- Age 33, independent product designer/developer hybrid - Based in Austin; works remotely across time zones - Tech stack: Notion, Zapier, Slack, Stripe, Wise - Income $120k–$180k, variable by project

Background

Built past side projects that automated invoicing and client onboarding. Burned time reconciling edge cases, pushing them to demand programmable bookkeeping with minimal manual touch.

Needs & Pain Points

Needs

1. Reliable Zapier triggers for receipts and matches 2. API access for custom categorization rules 3. Exception inbox with bulk resolve actions

Pain Points

1. Integrations break silently after provider changes 2. Duplicate entries across overlapping automations 3. Manual fixes derail streamlined flows

Psychographics

- Optimizes everything; hates repetitive click-work - Values open integrations and data portability - Trusts tools proven by community setups - Prefers silent automation over frequent notifications

Channels

1. Reddit r/Zapier 2. Slack communities 3. YouTube tutorials 4. Twitter/X threads 5. Product Hunt launches

P

Privacy Piper

- Age 38, cybersecurity consultant serving SaaS startups - Based in Denver; travels for short client engagements - Prefers iPhone + Mac; uses Proton Mail, 1Password - Income $150k–$220k, high sensitivity to risk

Background

Previously suffered a client data scare due to a leaky app. Since then, built strict operational hygiene with encrypted storage and compartmentalized accounts, demanding similar rigor from finance tools.

Needs & Pain Points

Needs

1. End-to-end encryption for documents at rest 2. Granular data retention and delete controls 3. Offline, encrypted export of tax packets

Pain Points

1. Opaque vendor security practices and policies 2. Forced data retention beyond engagement 3. Limited control over third-party processors

Psychographics

- Privacy-first; defaults to least data shared - Skeptical of third-party data processors - Values transparency, audits, and certifications - Prefers local exports and offline redundancies

Channels

1. Hacker News security 2. LinkedIn groups 3. Reddit r/privacy 4. Substack newsletters 5. YouTube reviews

M

Multicurrency Milo

- Age 29, videographer/editor for global clients - Currently in Lisbon; tax home in California - Paid via Wise, PayPal, USD/EUR cards - Income $70k–$110k, volatile, multi-currency

Background

Learned the hard way when mismatched exchange rates skewed deductions. Adopted strict habit of documenting currency, location, and project per receipt to avoid headaches at filing.

Needs & Pain Points

Needs

1. Automatic FX conversion with IRS-approved rates 2. Geotagged receipts and travel day classification 3. State-source tagging for income and expenses

Pain Points

1. Bank feeds mix native and converted amounts 2. Per-diem rules are confusing abroad 3. Clients reimburse late across currencies

Psychographics

- Pragmatic about rules; hates surprises later - Loves travel, accepts administrative overhead - Values accurate conversions over pretty dashboards - Motivated by clean, audit-proof paper trails

Channels

1. Reddit r/digitalnomad 2. YouTube travel 3. Instagram stories 4. Wise community 5. Facebook expat groups

P

Paperwork Parker

- Age 35, motion graphics contractor for agencies - Brooklyn-based, hybrid studio and on-site shoots - Manages subcontractors; collects their W-9s and 1099s - Income $90k–$140k, project-based with retainers

Background

Once delayed payment for weeks due to a missing COI. Built checklists to chase paperwork, but tracking across email threads and folders still fails at scale.

Needs & Pain Points

Needs

1. W-9/COI request and tracking workflow 2. Per-client policy templates on expenses 3. Attach proofs to invoices automatically

Pain Points

1. Payment holds due to missing documents 2. Scattered paperwork across email and drives 3. Re-entering policy details per client

Psychographics

- Compliance-minded but time-starved multitasker - Craves checklists with visible completion - Values tidy, client-ready attachments - Motivated by faster payment releases

Channels

1. LinkedIn messages 2. Slack agency 3. Gmail add-ons 4. Vimeo communities 5. Reddit r/freelance

D

Deadline Dana

- Age 27, social media strategist, gig-heavy - Los Angeles based; works late-night, mobile-first - Disorganized inbox with multiple cards/accounts - Income $55k–$85k, uneven monthly cash flow

Background

Filed an extension last year after a shoebox of receipts meltdown. Swore off shame spirals and now looks for tools that forgive procrastination with smart bulk fixes.

Needs & Pain Points

Needs

1. Bulk categorize with high-confidence suggestions 2. Triage view grouping biggest deduction wins 3. Deadline countdown with auto-packet assembly

Pain Points

1. Paralyzing backlog of uncategorized transactions 2. Missed deductions from scattered receipts 3. Stress from looming tax deadlines

Psychographics

- Avoids admin until urgency spikes - Responds to gamified progress nudges - Values clear, friendly language, not jargon - Motivated by deadlines and visible wins

Channels

1. TikTok tips 2. Instagram reels 3. YouTube explainers 4. Gmail promotions 5. Reddit r/tax

D

Delegator Dylan

- Age 41, fractional CTO consulting for startups - Seattle-based; manages two assistants remotely - Uses Slack, Asana, QuickBooks Online - Income $180k–$260k, multiple entities

Background

Scaled from solo to small team after landing retainer clients. Past mishaps with shared logins led to stricter permissions and documented workflows.

Needs & Pain Points

Needs

1. Role-based permissions and approval flows 2. Assignable tasks with due dates 3. Detailed activity logs and change history

Pain Points

1. Shared credentials causing accidental changes 2. Bottlenecks from unclear ownership of tasks 3. No visibility into who edited what

Psychographics

- Delegates ops; guards financial oversight - Values accountability with minimal friction - Trusts audit trails over verbal assurances - Prefers tools that grow with headcount

Channels

1. LinkedIn posts 2. Slack founder 3. Twitter/X DMs 4. YouTube demos 5. Substack operations

Product Features

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

Login Freeze

Hit pause on risk with a one-tap freeze that blocks all new sign-ins and device enrollments while keeping your current devices signed in. Perfect for misplaced phones or sketchy Wi‑Fi moments—unfreeze anytime from any verified device in your Safety Dashboard.

Requirements

One-Tap Freeze Toggle
"As a freelancer using TaxTidy, I want a single button to freeze or unfreeze my account so that I can quickly protect access without losing my current sessions."
Description

Add a prominent one-tap Freeze/Unfreeze control in the Safety Dashboard (mobile and web). Activating freeze sets an account-level "frozen" flag in the identity service and propagates within seconds to all authentication endpoints. Current signed-in sessions on verified devices remain active; new sign-ins and device enrollments are blocked until unfreeze. The UI displays real-time status, last changed timestamp, and a contextual help sheet. The control must be idempotent, responsive (<5s end-to-end), accessible, and resilient to intermittent connectivity (queued request with clear state messaging). Integrates with auth service, session manager, notification service, and audit logging.

Acceptance Criteria
Freeze Propagation and Blocking Behavior
Given the user is signed in on a verified device and the account is Active When the user taps Freeze and confirms Then the identity service sets account.frozen=true within 1 second of tap time And all authentication endpoints enforce the frozen state within 5 seconds of tap time And all new sign-in attempts return standardized error code AUTH_ACCOUNT_FROZEN And all new device enrollment attempts are blocked with error code AUTH_ACCOUNT_FROZEN And the Safety Dashboard displays status "Frozen" within 5 seconds
Existing Verified Sessions Remain Active
Given the user has active sessions on verified devices A and B and freezes the account When performing authenticated in-app actions (e.g., viewing documents, syncing) Then those sessions continue without forced logout and receive success responses for at least 30 minutes post-freeze And access token refresh/renewal on those verified sessions succeeds while the account is frozen And any sign-in attempt on a new device during this period is blocked with AUTH_ACCOUNT_FROZEN
Unfreeze Restores Normal Authentication
Given the account is Frozen When the user taps Unfreeze from a verified device Then the identity service sets account.frozen=false within 1 second And authentication endpoints allow new sign-ins and device enrollments within 5 seconds of tap time And the Safety Dashboard displays status "Active" within 5 seconds And sign-in attempts no longer receive AUTH_ACCOUNT_FROZEN
Idempotency and Concurrency Safety
Given the user double-taps the Freeze control within 500 ms When the backend receives duplicate requests with the same requestId Then exactly one state transition is applied and duplicates return a 200 OK no-op And only one audit log entry and one notification set are created for the transition Given two verified devices issue conflicting toggles within 2 seconds When the backend processes both requests Then the final state reflects the latest server-received timestamp (last write wins) And both UIs refresh to the final state and last changed timestamp within 5 seconds
UI Status, Prominence, Timestamp, and Help
Given the Safety Dashboard is opened on mobile and web When the account is Active or Frozen Then the Freeze/Unfreeze control is visible above the fold on standard viewports (mobile ≥375x667, web ≥1366x768) And the control shows the current state label ("Active"/"Frozen") and icon with contrast ratio ≥4.5:1 And the "Last changed" timestamp is displayed in the user's locale/time zone and updates within 2 seconds after a toggle And tapping the help icon opens a contextual help sheet within 500 ms with accurate description and a link to the security FAQ And the control exposes accessible name "Account Freeze", role "switch", and is operable via keyboard and screen reader (WCAG 2.1 AA)
Offline and Intermittent Connectivity Resilience
Given the device is offline or experiencing up to 30% packet loss When the user taps Freeze/Unfreeze Then the app queues a single toggle request and shows a Pending state message ("Request queued—will apply when online") within 300 ms And retries are attempted with exponential backoff for at least 2 minutes And upon success, the UI transitions to the correct final state within 2 seconds and the pending message clears And if not successful within 2 minutes, the user sees an actionable error with Retry and Cancel; no server-side state change occurs; audit has no new entry
Notifications and Audit Logging on State Change
Given a successful state change (Active->Frozen or Frozen->Active) When the identity service commits the change Then a push notification and an email are sent to the account owner within 60 seconds including action, actor device name, and timestamp And an audit log entry is created with userId, action, previousState, newState, actorDeviceId, actorIp, source (mobile/web), requestId, and timestamp (UTC) And duplicate or retried requests do not create duplicate user-visible notifications or duplicate audit entries
Frozen-State Auth Enforcement
"As a security-conscious user, I want all new sign-ins blocked while frozen so that unauthorized access is prevented even if someone has my credentials."
Description

Enforce the frozen state across all authentication vectors: email/password, passkeys/biometrics, magic links, OAuth, and API tokens. When frozen, deny creation of new sessions, prevent password resets from resulting in sign-in, and block device enrollment/remember-me tokens. Existing valid sessions can refresh tokens on verified devices but cannot enroll new devices. Provide standardized error codes/messages to clients and show user-facing guidance with an Unfreeze CTA for verified devices. Ensure coverage at edge locations and rate-limit repeated attempts. Includes unit/integration tests and feature flags for staged rollout.

Acceptance Criteria
Block New Session Creation Across All Auth Vectors During Freeze
- Given account state = Frozen and auth method = Email/Password on any device When user submits valid credentials Then no new session is created and response http_status=423 and error.code="AUTH_FROZEN" and error.detail="NEW_SESSION_BLOCKED". - Given account state = Frozen and auth method = Passkey/WebAuthn on any device When user completes assertion Then no new session is created and response http_status=423 with error.code="AUTH_FROZEN" and error.detail="NEW_SESSION_BLOCKED". - Given account state = Frozen and auth method = Magic Link When link is opened Then no new session is created; client shows blocked screen; API returns 423 AUTH_FROZEN NEW_SESSION_BLOCKED. - Given account state = Frozen and auth method = OAuth (e.g., Google/Apple) When OAuth callback is received Then do not finalize TaxTidy session; respond 423 with error.code="AUTH_FROZEN" and error.detail="NEW_SESSION_BLOCKED" and include guidance URL. - Given feature flag "login_freeze_enforcement" is enabled in env ∈ {staging, production} When any of the above flows occur Then the rules apply; When the flag is disabled Then flows behave as unfrozen for staged rollout/A-B testing.
Allow Token Refresh on Verified Devices; Deny on Unverified During Freeze
- Given account = Frozen and device is Verified with an existing valid session When client refreshes the access token Then refresh succeeds (200), session remains bound to the device, and no new device enrollment or remember-me token is created. - Given account = Frozen and device is Unverified or new When client attempts token refresh Then response is 423 with error.code="AUTH_FROZEN" and error.detail="REFRESH_NOT_VERIFIED" and no session mutation occurs. - Given account = Frozen When client attempts to create or extend a "remember me" long-lived token Then response is 423 with error.code="AUTH_FROZEN" and error.detail="REMEMBER_ME_BLOCKED". - Given account transitions to Unfrozen When the same device refreshes Then standard refresh behavior resumes (200) with normal token TTLs.
Password Reset Executes Without Sign-In While Frozen
- Given account = Frozen When user completes password reset via email Then password is updated (200) and no session cookie or access/refresh token is issued. - Given account = Frozen and frontend attempts auto-login after reset Then request is blocked with 423 and error.code="AUTH_FROZEN" and error.detail="NEW_SESSION_BLOCKED". - Given account = Frozen When reset completes Then UI shows guidance text and an Unfreeze CTA only on verified devices; on unverified devices, CTA is hidden/disabled and guidance to use a verified device is shown. - Given audit logging is enabled When reset completes in frozen state Then an event "password_reset_completed" with property frozen=true is recorded with user_id, device_id, and request_id.
Device Enrollment and Remember-Me Tokens Blocked Under Freeze
- Given account = Frozen When client calls any device enrollment endpoint (e.g., WebAuthn registration, TOTP binding, push enrollment) Then operation is rejected with 423, error.code="AUTH_FROZEN", error.detail="DEVICE_ENROLL_BLOCKED", and no device records are created. - Given account = Frozen When client requests creation of a remember-me cookie/token Then reject with 423, error.code="AUTH_FROZEN", error.detail="REMEMBER_ME_BLOCKED", and no token is persisted. - Given account = Frozen When an existing session attempts to add any new second factor Then operation is rejected with the same 423 AUTH_FROZEN DEVICE_ENROLL_BLOCKED response. - Given account transitions to Unfrozen When the same enrollment request is retried Then operation proceeds per normal policy and is auditable.
API Token Issuance and Exchange Rules During Freeze
- Given account = Frozen When user attempts to create a new Personal API Token (PAT) or perform OAuth client credential issuance Then server returns 423 with error.code="AUTH_FROZEN" and error.detail="API_TOKEN_ISSUE_BLOCKED" and no token is stored. - Given account = Frozen and an existing, unexpired API access token bound to a previously verified session is presented When calling APIs Then requests are authorized until token expiry with no privilege escalation. - Given account = Frozen When a refresh or token exchange is attempted from an unverified device or without verified-session context Then reject with 423 and error.code="AUTH_FROZEN" and error.detail="API_TOKEN_EXCHANGE_BLOCKED". - Given account transitions to Unfrozen When token issuance or exchange is attempted Then standard policies apply and succeed per permissions.
Standardized Errors and User-Facing Guidance with Unfreeze CTA
- Given any blocked action due to frozen state When response is returned Then http_status=423 and body contains error.code="AUTH_FROZEN", error.detail ∈ {"NEW_SESSION_BLOCKED","REFRESH_NOT_VERIFIED","DEVICE_ENROLL_BLOCKED","REMEMBER_ME_BLOCKED","API_TOKEN_ISSUE_BLOCKED","API_TOKEN_EXCHANGE_BLOCKED"}, support_url, and a stable error_id for tracing. - Given UI receives AUTH_FROZEN on a verified device When rendering Then show banner "Account is frozen" with Unfreeze CTA linking to Safety Dashboard; CTA successfully opens unfreeze flow. - Given UI receives AUTH_FROZEN on an unverified device When rendering Then show guidance to use a verified device; do not render an active Unfreeze CTA. - Given locales ∈ {en, es, fr} When messages are displayed Then localized strings are shown and pass snapshot QA.
Edge Propagation and Rate Limiting for Frozen State
- Given account is toggled to Frozen in the primary region When validating from ≥3 edge PoPs (e.g., us-east, us-west, eu-central) Then ≥95% of requests observe frozen enforcement within 10s of toggle and ≥99% within 30s. - Given repeated blocked attempts by the same user+IP While frozen Then limit to ≤5 attempts per minute per auth vector and ≤20 per hour overall; excess returns 429 with error.code="RATE_LIMITED" and error.detail="FROZEN_ATTEMPT_RATE_LIMIT". - Given 1000 concurrent blocked attempts from distributed IPs When monitored Then auth endpoint p95 latency remains <400ms and zero sessions/devices are created. - Given feature flag rollout percentage is adjusted When set to X% Then only users in the enabled cohort receive frozen enforcement and logs reflect cohort membership.
Verified Devices Registry
"As a user who often switches phones and laptops, I want to see and manage my verified devices so that I can control which devices can unfreeze my account."
Description

Create and maintain a registry of verified devices authorized to unfreeze the account. A device becomes verified after successful strong auth (e.g., 2FA) and device attestation/fingerprint. Store device ID, platform, app version, last seen, IP city, and friendly name. Display the list in the Safety Dashboard with actions to rename and revoke. Unfreeze is permitted only from devices present in this registry. During freeze, device enrollment is disabled. Provide secure storage of device secrets, handle device loss/replacement flows, and sync across platforms.

Acceptance Criteria
Verify Device After Strong Auth and Attestation
Given a user signs in on a new device and completes strong authentication and device attestation, when verification completes, then the device is added to the Verified Devices Registry with fields device_id, platform, app_version, last_seen (UTC), ip_city, and a default friendly_name. Given strong authentication or device attestation fails, when verification is attempted, then the device is not added to the registry and the user is shown an actionable error. Given a device is successfully verified, when the registry is queried, then the record includes a server timestamp and the verifying actor_id for auditing.
Restrict Unfreeze to Verified Devices
Given an account is frozen, when an unfreeze is attempted from a device present in the Verified Devices Registry, then the unfreeze succeeds and the attempt is logged with device_id and actor_id. Given an account is frozen, when an unfreeze is attempted from a device not present in the registry, then the unfreeze is blocked with error code "unverified_device" and the attempt is logged. Given an account is frozen, when an unfreeze is attempted from a revoked device, then the unfreeze is blocked and the attempt is logged.
Safety Dashboard Device List and Actions
Given a user opens the Safety Dashboard on a verified device, when the device list loads, then each entry shows platform, app_version, last_seen (UTC or relative), ip_city, friendly_name, and a truncated device_id. Given the device list contains N verified devices, when the dashboard loads over a normal network, then N entries render within 2 seconds with no missing fields. Given a user renames a device, when they save the change, then friendly_name updates in the registry and is visible on all signed-in devices within 60 seconds. Given a user revokes a device, when they confirm revocation, then the device is removed from the registry, can no longer initiate unfreeze, and a revocation event is logged with device_id and actor_id.
Block Device Enrollment During Freeze
Given an account is frozen, when a sign-in or verification is initiated from a new device, then device enrollment is blocked with error code "enrollment_disabled_during_freeze" and no registry record is created. Given an account is frozen, when existing verified devices access the app, then they remain signed in and can use the Safety Dashboard but cannot enroll new devices. Given an account transitions from frozen to unfrozen, when a new device completes strong auth and attestation, then the device can be added to the registry.
Secure Device Secret Storage
Given the app runs on iOS, when a device secret is created, then it is stored in the iOS Keychain with hardware-backed protection when available and is non-exportable. Given the app runs on Android 9+, when a device secret is created, then it is stored in Android Keystore with hardware-backed protection (e.g., StrongBox when available) and is non-exportable. Given client-server communication occurs, when device credentials are transmitted, then private key material never leaves the device and all transport is protected by TLS 1.2+. Given the server persists device credentials, when data is stored at rest, then only public keys and/or opaque attestation artifacts are stored; no raw device secret material is retrievable via any API.
Device Loss and Replacement Flow
Given a device is lost, when the user opens the Safety Dashboard from another verified device and revokes the lost device, then the lost device is removed from the registry, cannot unfreeze, and will be denied Safety actions on next contact. Given no verified devices remain on the account, when the user attempts to unfreeze or enroll a new device, then they are blocked and shown an entry point to the account recovery flow. Given a replacement device, when the user completes strong auth and device attestation and the account is unfrozen (or unfreezes from another verified device), then the replacement device is added to the registry.
Cross-Platform Registry Sync and Data Accuracy
Given a rename or revoke occurs on one platform, when viewed from another signed-in platform, then the change is reflected within 60 seconds. Given a device becomes active, when it contacts the service, then last_seen (UTC) and ip_city update within 5 seconds and display in the Safety Dashboard. Given concurrent friendly_name edits from two platforms, when updates are processed, then last-write-wins based on server timestamp and both edits are logged for audit. Given a device is removed from the registry, when viewing the list on any platform, then the device no longer appears and cannot initiate unfreeze.
Secure Recovery Unfreeze
"As a user who lost all my devices, I want a secure recovery way to unfreeze my account so that I can regain access without compromising my security."
Description

Provide a fallback unfreeze path when no verified device is available. Options include backup recovery codes, a time-delayed unfreeze (e.g., 24-hour hold with continuous notifications and cancel capability), and support-assisted verification with ID checks. All recovery attempts require additional risk checks (device reputation, IP velocity, geolocation mismatch) and are fully logged. The flow clearly communicates timelines and allows users to cancel pending recovery. Minimizes lockouts while maintaining strong security posture.

Acceptance Criteria
Backup Recovery Codes Unfreeze
Given no verified device is available and the user selects "Use backup recovery code" When a valid, unused 12-character recovery code is entered Then the account is unfrozen immediately, existing sessions remain signed in, and new sign-ins are permitted. Given an invalid or already-used code is entered 3 times within 15 minutes When the threshold is reached Then the recovery-code path is locked for 15 minutes and notifications are sent to all verified contact methods. Given a successful unfreeze via recovery code Then the used code is invalidated, remaining code count is displayed, and code regeneration is blocked until at least one device is re-verified. Given the recovery code input endpoint Then rate limiting of 5 attempts/hour per IP and 10 attempts/day per account is enforced and inputs are masked client-side and server-side validated.
Time-Delayed Unfreeze with Notifications
Given the user initiates a time-delayed unfreeze When they confirm the 24-hour hold Then a hold starts immediately and notifications (email + push/SMS where available) are sent to all verified contact methods containing a cancel link and expected completion time. Given a time-delayed unfreeze is active Then the Safety Dashboard displays a real-time countdown and status; reminder notifications are sent at T-23h and T-1h. Given any cancel action is taken from a verified link or the Safety Dashboard When processed Then the pending unfreeze is canceled immediately and confirmation notifications are sent; unfreeze will not occur unless re-initiated. Given the hold elapses with no cancellation Then the unfreeze is executed automatically and a completion notification is sent to all verified contacts. Given a time-delayed unfreeze is active Then only one pending request is allowed; starting a new request cancels and supersedes the prior request and resets the timer. Given a time-delayed unfreeze is within 5 minutes of completion Then a final risk check is performed; if high risk is detected the unfreeze is paused and the user is instructed to proceed via support-assisted verification.
Support-Assisted Verification Unfreeze
Given the user selects support-assisted verification When they pass document ID + liveness verification at or above the configured threshold (e.g., score ≥ 0.85) and confirm via one-time code sent to a verified contact method Then support may approve the unfreeze and the system records approver ID and timestamp. Given identity attributes (name/DOB) fail matching rules beyond tolerance Then the request is rejected and the user is prompted to retry or select another path. Given any high-severity risk signal is present Then approval requires two-person control (agent + supervisor) before unfreeze can complete. Given a support-assisted session completes Then session artifacts (decision, evidence hashes) are encrypted at rest, retained per policy (12 months), and a summary receipt is sent to the user.
Risk Checks on All Recovery Attempts
Given any recovery attempt (codes, time-delayed, or support-assisted) is initiated Then a risk score is computed using device reputation, IP velocity, and geolocation mismatch before any state change. Given the risk score exceeds the high-risk threshold (e.g., >80/100) Then the attempt is blocked and the user is routed to support-assisted verification; a notification is sent to verified contacts. Given IP velocity exceeds 3 distinct countries or ASNs within 24 hours OR TOR/VPN is detected Then the time-delayed path is denied and only support-assisted path is offered. Given geolocation mismatch exceeds 500 km within 10 minutes from last trusted event AND the device fingerprint is new Then an out-of-band OTP to a verified contact is required to proceed on the chosen path. Given a risk decision is made Then the inputs, feature values, and thresholds used are captured in the audit log for explainability.
Audit Logging of Recovery Events
Given any recovery event occurs (initiation, step, decision, cancellation, completion) Then an immutable log entry is written within 2 seconds including: timestamp, user ID, event type, actor (user/system/support), IP, device fingerprint, geolocation estimate, risk score, channel, and outcome. Given logs contain sensitive attributes Then values are tokenized/minimized and access is restricted to admins with explicit scope; all access is audited. Given audit logs are maintained Then daily integrity checks run and any tamper signal generates an alert within 5 minutes. Given retention policy applies Then recovery event logs are retained for 24 months and purged upon account deletion per policy.
Cancel Pending Recovery Request
Given a time-delayed unfreeze is pending When the user clicks a cancel link from a verified contact or cancels via the Safety Dashboard Then the request is canceled immediately, cannot complete automatically, and a confirmation notification is sent. Given a cancel link is issued Then it is single-use and expires upon use or when the request completes; replay attempts are rejected and logged. Given multiple cancel attempts occur concurrently Then the system guarantees at-most-once state transition and records the final state as Canceled. Given a cancellation completes Then the Dashboard reflects the canceled status within 5 seconds and the user is presented with next-step options (e.g., start new request, use recovery code, contact support).
Security Notifications & Alerts
"As a user, I want immediate notifications about freeze changes and blocked sign-ins so that I can react quickly to suspicious activity."
Description

Send real-time alerts for freeze/unfreeze actions, blocked sign-in attempts, and recovery requests via push and email (optional SMS for high-risk events). Notifications include timestamp, attempting device type, IP-based location, and actionable guidance. Deep links are protected with short-lived, single-use nonces and device checks to prevent phishing. Users can configure notification preferences in Settings. Integrates with the notification service, templates localized for top locales, and suppresses noise via aggregation rules.

Acceptance Criteria
Freeze/Unfreeze Action Alerts Delivered
Given a verified user freezes their account from any device When the freeze action is confirmed Then a push notification and an email are sent within 10 seconds And both messages include: action type "Freeze", ISO-8601 UTC timestamp, initiating device type/OS, and IP-based city/region/country And both messages include a primary CTA linking to the Safety Dashboard via a protected deep link Given a verified user unfreezes their account from any device When the unfreeze action is confirmed Then a push notification and an email are sent within 10 seconds And both messages include: action type "Unfreeze", ISO-8601 UTC timestamp, initiating device type/OS, and IP-based city/region/country And both messages include guidance on re-enabling freeze if this was not intended
Blocked Sign-in Attempt Alerts During Freeze
Given the account is in Login Freeze state When a new device attempts to sign in Then send push and email alerts within 10 seconds And do not allow the sign-in And include attempting device type/OS, IP-based city/region/country, and attempt timestamp (UTC) And include actionable guidance to keep the freeze active and review devices in the Safety Dashboard
High-Risk Event SMS Escalation
Given the user has opted into SMS and verified their phone number When a recovery request is initiated OR a blocked sign-in occurs from a country not seen for the account in the past 30 days Then send an SMS in addition to push and email And the SMS includes the action type, country/city, and UTC timestamp, and a short Safety Dashboard link And enforce rate limiting to a maximum of 1 SMS per event type per 5 minutes per user And the SMS includes STOP and HELP instructions
Deep Link Nonce and Device Check Security
Given a notification deep link is generated When the link is created Then it contains a cryptographically random nonce valid for 10 minutes and marked single-use server-side And the target action executes only if device attestation passes and the request originates from a verified session And on nonce reuse, expiry, or failed device attestation, the server denies the action, returns HTTP 401/410 as appropriate, and redirects to a neutral help page And all link uses are logged with user ID, device ID, IP, and outcome
Notification Preferences Enforcement
Given a user updates notification preferences in Settings for events (Freeze/Unfreeze, Blocked Sign-in, Recovery Requests) When they toggle push, email, or SMS and save Then the change persists and is applied to subsequent notifications within 60 seconds And SMS cannot be enabled unless the phone number is verified via OTP And critical security emails (recovery requests) cannot be fully disabled; attempting to disable shows an inline explanation and keeps email enabled for that event And subsequent notifications honor the user's saved preferences, except for mandatory critical emails
Localized Templates for Top Locales
Given a user has set a preferred language that is within the configured top locales When a notification is sent Then the localized template for that locale is used And dynamic fields are localized (date/time format, translated location names where available, number formatting, and correctly capitalized device OS names) And if the locale is unsupported, fall back to English (en-US) And templates pass automated placeholder validation (no missing required variables) and include the required support link and legal footer
Aggregation and Noise Suppression
Given 3 or more similar events (e.g., blocked sign-in attempts) from the same device fingerprint and IP occur within 120 seconds When generating notifications Then send a single aggregated alert within 10 seconds summarizing total count and first/last timestamps and source IP/location And suppress additional alerts for that event type and source for 120 seconds And if a new event of higher severity occurs (e.g., new country or new device), bypass suppression and send an immediate distinct alert And aggregation does not merge events from different IPs or device fingerprints
Audit Logging & Export
"As a privacy- and security-minded user, I want a detailed audit history of freeze-related activity so that I can verify what happened and share evidence if needed."
Description

Record an immutable audit trail for all freeze/unfreeze events, blocked authentication attempts, device verification changes, and recovery actions. Each event includes user ID, device ID, IP, user agent, geo, initiator, and correlation IDs. Expose a user-facing view in the Safety Dashboard with searchable filters and export to CSV/JSON. Enforce retention (24 months by default) and secure storage with write-once semantics. Provide admin-only analytics access for fraud monitoring and incident response.

Acceptance Criteria
Capture of Freeze/Unfreeze, Blocked Auth, Device Verification, and Recovery Events
Given a verified device toggles Login Freeze on or off, When the action is confirmed, Then an audit event with event_type in {freeze_enabled, freeze_disabled} is appended including user_id, device_id, ip, user_agent, geo, initiator=user, correlation_id, request_id, and timestamp. Given Login Freeze is active, When a new sign-in is attempted from any unrecognized device or location, Then an audit event with event_type=auth_blocked, reason=login_freeze is appended with all required fields and no session is created. Given Login Freeze is active, When a new device enrollment is attempted, Then an audit event with event_type=enrollment_blocked, reason=login_freeze is appended with all required fields. Given a device verification state changes (verify or revoke) by user or system, When the change is applied, Then an audit event with event_type=device_verification_changed is appended including old_state and new_state. Given an account recovery action occurs (e.g., backup_code_used, email_link, trusted_contact), When the action completes, Then an audit event with event_type=recovery_action and subtype is appended with all required fields. Given any audit event is emitted, When the operation completes, Then the event is durable and queryable in the Safety Dashboard within 5 seconds of the triggering action.
Event Schema Completeness and Correlation Integrity
Given any audit event, Then it contains non-null fields: event_id (UUID/ULID), event_type (one of the allowed set), timestamp (UTC ISO 8601 with milliseconds), user_id (UUID), ip (valid IPv4 or IPv6), user_agent (string), initiator in {user, system, admin}, correlation_id (UUID), request_id (UUID), and includes geo.country (ISO 3166-1 alpha-2) when resolvable; device_id is present when known, otherwise null. Given geo lookup fails, Then geo fields are null and geo_lookup_status="unavailable" is recorded. Given multiple events are emitted for the same user action (e.g., request received, action applied), When they are written, Then correlation_id is identical across those events and request_id is unique per inbound request attempt. Given events are listed for a user, When sorted by timestamp desc, Then ordering is stable and consistent; ties are broken by event_id ascending. Given an event contains any field outside the schema or with invalid format, Then the event is rejected from persistence and a schema_validation_failed error is logged (without losing the triggering product action).
Safety Dashboard Log View with Search and Filters
Given a signed-in user opens the Safety Dashboard Audit Log, When the page loads, Then only that user's audit events within the retention window are listed sorted by timestamp desc. Given the user applies filters for date_range, event_type, device_id, ip, and initiator, When the query is submitted, Then the results reflect all filters and the total count updates accordingly. Given the user enters a free-text search on ip or device label, When submitted, Then results include events whose ip or device label contains the search term (case-insensitive). Given results exceed one page, When the user paginates next/previous, Then the correct next/previous slice is returned without duplicates or gaps under the current filters. Given no events match the filters, Then an empty state is displayed and export actions are disabled.
User-Initiated Export to CSV and JSON
Given any set of filters is applied in the Safety Dashboard, When the user selects Export CSV, Then a UTF-8 CSV (RFC 4180 compliant) is downloaded containing only matching events and headers: event_id,event_type,timestamp,user_id,device_id,ip,user_agent,geo_country,geo_region,geo_city,initiator,correlation_id,request_id,result,reason,metadata. Given any set of filters is applied, When the user selects Export JSON, Then a UTF-8 JSON array is downloaded with one object per event matching the on-screen schema and filters. Given an export is initiated, When processing starts, Then an audit event export_initiated is appended with format and filter summary; When the export completes or fails, Then an export_completed or export_failed event is appended with row_count or error_code. Given the result set contains more than 10,000 events, When the user exports, Then the export streams all matching events without truncation and the row_count in export_completed equals the number of matched events.
Data Retention Enforcement at 24 Months
Given events exist older than 24 months, When the scheduled retention job runs, Then all events strictly older than 24 months from current UTC time are permanently purged from storage and no longer retrievable via UI, API, or export. Given an event timestamp is exactly at the 24-month boundary, Then it is retained until it becomes strictly older than 24 months. Given a retention purge occurs, When it completes, Then a retention_purge admin audit event is appended containing cutoff_timestamp and count_purged. Given a user requests a date range extending beyond the retention window, Then only events within the last 24 months are returned and the UI indicates the retention limit.
Write-Once Immutable Storage Enforcement
Given any persisted audit event within its retention period, When an internal or external client attempts to update or delete the record, Then the operation is rejected (HTTP 403 or equivalent) and the stored record remains unchanged. Given storage is configured for write-once semantics, When a tester inspects the underlying object/record metadata, Then an active immutability/retention lock is present through the event's retention expiry. Given an admin UI or API is used, Then no edit/delete controls are exposed for audit events; any attempted mutation calls are logged as immutability_violation without altering data. Given an event reaches the end of its retention period, When the retention job issues a delete, Then storage allows deletion consistent with the expired lock and the record is removed.
Admin-Only Fraud Analytics Access
Given a non-admin authenticated user attempts to access analytics endpoints or pages, When the request is made, Then access is denied with HTTP 403 and an access_denied audit event is appended with user_id and attempted_resource. Given an admin user accesses analytics, When a date range and filters (event_type, initiator, ip_subnet, country) are applied, Then aggregate metrics (counts and time series) for freeze/unfreeze, auth_blocked, device_verification_changed, and recovery_action are returned accurately for the selected scope. Given an admin generates or downloads an analytics report, When the action completes, Then an admin_analytics_report event is appended with query_filters and result_row_count; report content excludes unnecessary PII beyond user_id, device_id, ip, and country. Given analytics access is enabled, When any admin loads the page, Then only users with the admin role can view cross-account aggregates; all analytics access is itself recorded in the audit log.

QR Handoff

Sign in on desktop by scanning a secure QR with your passkey-enabled phone—no passwords, no email links, no phishing. Your phone’s biometric approves the session, and a hardware-bound, short‑lived token starts your desktop session instantly.

Requirements

Desktop QR Challenge Generation
"As a freelancer accessing TaxTidy on my laptop, I want a secure QR code that represents a one-time login request so that I can start sign-in from my phone without typing a password."
Description

Generate and display a single-use, short-lived QR code on the desktop login screen that encapsulates an opaque challenge ID bound to the current browser session. The backend issues the challenge with TTL (e.g., 60 seconds), one-time use, and anti-replay properties, storing minimal state needed for verification. The QR payload must not contain PII or secrets—only the challenge reference and environment metadata. The UI shows a countdown, auto-regenerates on expiry, and supports manual cancel/refresh. Integrates with TaxTidy’s auth service and WebSocket/long-poll channel for real-time handoff, works across modern browsers, and adheres to accessibility and responsive design standards.

Acceptance Criteria
Session‑Bound QR Challenge Display
Given an unauthenticated user opens the TaxTidy desktop login page When the page requests a QR challenge from the Auth Service Then the UI renders a QR representing the issued challenge within 1 second of the API response And the challenge is bound server-side to the current browser session identifier And a visible countdown starts at the configured TTL (60 seconds by default) And any previously active challenge for this session is invalidated
QR Payload and Server State Constraints
Given the displayed QR is decoded Then the payload includes only: opaque challengeId, environment metadata (env, region), client/app version, issuedAt And the payload contains no PII, user identifiers, or secrets And the challengeId has at least 128 bits of entropy and is unguessable And the payload size is <= 1024 bytes and uses URL-safe characters And server-side state for the challenge is limited to: challengeId, sessionBinding hash, issuedAt, expiresAt, status, environment metadata; no PII is stored
TTL Expiry, Countdown, and Auto‑Regeneration
Given a displayed challenge with a 60-second TTL When the countdown reaches 0 or the server-side TTL elapses Then the challenge becomes invalid and scanning it returns an expired result And the desktop UI automatically requests and displays a new challenge within 1 second And the countdown resets to the configured TTL And if the tab regains focus after being backgrounded, the UI reconciles with server state and regenerates if expired
Manual Cancel and Refresh Controls
Given the QR challenge is displayed When the user clicks Refresh Then the current challenge is invalidated server-side and a new challenge is displayed within 1 second with a reset countdown When the user clicks Cancel Then the current challenge is invalidated server-side And the QR is removed, the countdown stops, and the handoff channel is closed And the UI provides a clear action to resume QR login
One‑Time Use and Anti‑Replay Enforcement
Given a valid, unconsumed challenge When it is approved on a passkey-enabled phone Then the backend marks it consumed and restricts redemption to the originating desktop session And any subsequent redemption attempts for the same challenge return HTTP 409 (AlreadyUsed) And any redemption attempt from a different desktop session returns HTTP 403 (SessionMismatch) And any redemption attempt after expiry returns HTTP 410 (Expired)
Real‑Time Handoff Channel Behavior
Given the desktop login page has an active challenge When establishing the handoff channel Then a WebSocket connection is established within 2 seconds, or long-polling fallback engages within 2 seconds if WebSocket fails And heartbeat/retry keeps at most one active connection per session until cancel, expiry, or consumption When the phone approves the challenge Then the desktop receives the approval event within 2 seconds and proceeds to session start flow And the channel closes within 2 seconds after completion or cancel
Accessibility, Responsiveness, and Cross‑Browser Support
Given the desktop login page is loaded Then all interactive elements (QR area, Refresh, Cancel) are reachable via keyboard with visible focus and logical order And QR area, countdown, and controls have accessible names/labels; countdown updates via ARIA live polite no more than once per second And color contrast meets WCAG 2.2 AA; no flashing content is present And layout adapts from 320px to 1920px widths; QR renders at least 180px on narrow viewports and remains crisp on high-DPI And the feature works on the latest two versions of Chrome, Firefox, Safari, and Edge on desktop
Mobile Passkey Approval Flow
"As a user with a passkey on my phone, I want to approve the desktop login with my biometric so that I can sign in quickly and securely without passwords or email links."
Description

Upon scanning the desktop QR, the TaxTidy mobile app or mobile web captures the challenge ID via deep link/App Link and initiates a WebAuthn (FIDO2) authentication using the device’s platform authenticator (Face ID/Touch ID/biometric/PIN). The UI displays clear origin and session details (domain, browser, device, approximate location, time) before prompting biometric approval. On success, the mobile client submits the signed assertion to the backend for verification against the registered credential (hardware-bound key), ensuring rpId binding to the TaxTidy domain. Supports iOS and Android, native app and PWA, with graceful handling if the app is not installed.

Acceptance Criteria
QR Deep Link Capture and Fallback
- Given a desktop QR encodes a valid challengeId and App/Universal Link, when the user scans it with the phone camera or TaxTidy app, then the TaxTidy mobile client launches and captures the challengeId via deep/App Link without manual copy-paste. - Given the TaxTidy app is not installed, when the QR is scanned with the system camera, then the App/Universal Link opens the mobile web PWA approval route with the challengeId prefilled. - Given the App/Universal Link is correctly configured, when opened on iOS and Android major supported versions, then the native app (if installed) is opened directly without an interstitial browser prompt. - Given a tampered or malformed deep/App Link is opened, when validation runs, then the client rejects it, shows “Invalid or expired code,” and does not proceed to authentication. - Given a QR challenge older than 120 seconds, when the link is opened, then the client displays “Code expired” and does not initiate WebAuthn.
Session Details Preview Prior to Approval
- Given the mobile client has resolved the challenge context, when the approval screen is presented, then it displays the requesting domain (login.taxtidy.com), browser (e.g., Chrome/Safari and version), device name/type (from desktop client hint), approximate city/region, and current time before any approve action is enabled. - Given the detected origin domain is not within the allowed rpId scope (taxtidy.com), when rendering the screen, then the approve action is disabled and a prominent warning is shown. - Given approximate location cannot be determined, when rendering the screen, then the location field reads “Location unavailable” without blocking approval. - Given accessibility services (VoiceOver/TalkBack) are active, when navigating the approval screen, then all elements have accessible labels and meet WCAG AA contrast (>= 4.5:1). - Given the screen is shown, when the user does not act for 120 seconds, then the approval session times out and returns to the start state with a clear message.
WebAuthn Platform Authenticator Invocation
- Given a registered platform credential exists on the device, when the user taps Approve, then the client invokes WebAuthn get() with rpId "taxtidy.com", the provided challenge, userVerification="required", and allowCredentials including the user’s credential ID(s). - Given biometrics are available, when WebAuthn is invoked, then the system biometric prompt is shown; if the user cancels or fails verification, no assertion is sent and the flow returns to the approval screen with a non-success state. - Given biometrics are unavailable or disabled, when WebAuthn is invoked, then a device PIN/unlock fallback is offered and userVerification remains required. - Given the authenticator returns an assertion, when inspected client-side, then authenticatorData flags include UV=1 and UP=1, and the client proceeds to submit over a secure channel. - From tap on Approve to system prompt display, the latency is ≤ 500 ms on reference devices (p50) and ≤ 1,500 ms (p95).
Assertion Submission and Backend Verification
- Given a signed WebAuthn assertion is produced, when it is submitted to the backend over TLS 1.2+ with certificate pinning enabled in app, then the server verifies rpIdHash matches taxtidy.com, clientData.origin equals the approved origin (https://login.taxtidy.com or the PWA origin), the challenge matches and is unused, and the signature verifies against the stored public key. - Given verification succeeds, when the desktop session polls with the challengeId, then a short-lived, one-time token (TTL ≤ 30s) is minted and consumed to start the desktop session; the mobile client displays “Approved” within 2 seconds of server confirmation. - Given the assertion is replayed or the challenge was already consumed, when verification runs, then the server rejects it and no desktop session is started; both mobile and desktop show a clear failure state. - Given the credential is revoked or not found, when verification runs, then the server returns a specific error code that the client maps to “Credential not available,” offering account recovery guidance. - Given signCount or backup flags indicate possible key cloning, when verification runs, then the server flags the event, denies the login, and records a security event with the challengeId.
Cross-Platform Native and PWA Coverage
- Given iOS 15+ and Android 10+ devices, when the flow is executed in the native TaxTidy app, then all approval, WebAuthn, and verification steps complete successfully per this specification. - Given the user runs the flow in a mobile PWA (Safari/Chrome), when invoking WebAuthn, then platform authenticators function with the same rpId constraints and the approval completes successfully. - Given Digital Asset Links (Android) and Universal Links (iOS) are configured, when the link is tapped, then routing opens the native app if installed, else falls back to the PWA without user intervention. - Given the device/browser lacks WebAuthn platform authenticator support, when the flow is initiated, then the user is informed of unsupported device and offered alternatives (install app or use a supported browser) without attempting approval. - Given localization defaults to en-US, when system locale is en-US, then all texts on approval and error screens match provided copies; placeholders are not shown.
Failure, Cancellation, and Timeout Handling
- Given network loss occurs at any step, when detected, then the client shows a retry option, prevents duplicate submissions, and resumes safely upon connectivity without auto-approving. - Given the user denies the biometric/PIN prompt, when the system returns control, then the desktop shows “Approval denied,” and the mobile remains on the approval screen with an option to retry. - Given the desktop session is closed or QR invalidated, when the mobile checks session state, then it shows “Session closed” and disables the Approve action. - Given the QR challenge expires during the flow, when the user attempts to approve, then the server rejects with an expiry code and the client prompts to rescan a new code. - All error toasts/dialogs use plain language, include a short reason and retry guidance, and are logged with a non-PII event id tied to the challengeId.
Performance and Security Constraints
- Median time from QR scan to desktop session start is ≤ 5 seconds on 4G/LTE reference devices; p95 ≤ 10 seconds. - QR challenges are single-use with a validity window of 120 seconds; attempts outside the window fail server-side and client-side messaging reflects expiry. - All auth-related API requests enforce TLS 1.2+ and HSTS; content is served over HTTPS with a strict CSP that blocks mixed content and unauthorized origins. - No biometric data leaves the device; only standard WebAuthn assertion fields are transmitted and stored; logs exclude biometric or raw location data. - Security events (replay, rpId mismatch, suspected cloning) are recorded with timestamps and challengeId for auditability without storing sensitive payloads.
Token Minting and Session Handoff
"As a user who approved login on my phone, I want my desktop session to start instantly so that I can continue working without extra steps."
Description

After verifying the WebAuthn assertion, the backend mints a short-lived, one-time handoff token bound to the desktop challenge, browser session identifiers, and CSRF context. The desktop client receives the token via an established real-time channel (WebSocket/SSE/long-poll) and immediately exchanges it for a full authenticated session. The token is redeemable once, has a strict TTL (e.g., 30 seconds), and enforces additional bindings (e.g., IP/subnet heuristic, user agent hash) to mitigate interception. Successful redemption invalidates all outstanding challenges. Session cookies are set with Secure, HttpOnly, SameSite flags and aligned with existing TaxTidy session management.

Acceptance Criteria
Mint one-time handoff token after WebAuthn verification
Given a valid WebAuthn assertion for user U against desktop challenge C When the backend verifies the assertion Then it mints exactly one handoff token T with at least 128 bits of randomness And marks T as single-use And associates T to user U and challenge C And records issuedAt for T And no token is minted if assertion verification fails And T is never logged or exposed in analytics/telemetry
Enforce 30s TTL on handoff token
Given a handoff token T was issued at time t0 with a 30-second TTL When T is redeemed at time t ≤ t0 + 30s Then redemption succeeds with HTTP 200 When T is redeemed at time t > t0 + 30s Then redemption fails with HTTP 401 InvalidOrExpiredToken And T remains unusable thereafter And expiresAt = t0 + 30s is persisted and auditable
Bind token to desktop challenge, browser session, and CSRF context
Given token T was minted for challenge C, desktop sessionId S, and CSRF token X When a redemption request includes C, S, and X that exactly match the mint bindings Then redemption succeeds with HTTP 200 When any of C, S, or X do not match the mint bindings Then redemption fails with HTTP 401 BindingMismatch And T remains redeemable until TTL expiry (failed attempts do not consume T)
Deliver token over established real-time channel and exchange for session
Given desktop client D has an established real-time channel (WebSocket/SSE/long-poll) for challenge C And mobile approval mints token T When the server publishes T Then only D’s channel bound to C receives T And no other subscribed client receives T When D submits T to the redeem endpoint over HTTPS POST Then the server issues a full authenticated session and returns HTTP 200 And T is not included in URLs, query strings, or referrers
Enforce IP/subnet heuristic and user-agent binding
Given token T was minted with observed IP/subnet N and user-agent hash H for desktop D When redemption originates from a client whose IP/subnet matches N per the configured heuristic and whose user-agent hash equals H Then redemption succeeds with HTTP 200 When either the IP/subnet or user-agent hash does not match Then redemption fails with HTTP 401 BindingMismatch
Successful redemption invalidates all outstanding challenges
Given user U has outstanding desktop challenges C1..Cn And token T1 for C1 is redeemed successfully When any other challenge Ci attempts to mint or redeem a token Then the server rejects with HTTP 409 ChallengeInvalidated And all pending challenge records for U are marked invalidated and cannot be reused
Set secure, aligned session cookies on redemption
Given token T is redeemed successfully When the server establishes the authenticated session Then session cookies match TaxTidy’s standard login configuration (name, domain, path, expiry, rotation policy) And each cookie is set with Secure, HttpOnly, and SameSite=Lax (or stricter per policy) flags And no session identifiers are placed in localStorage or sessionStorage And cookies are transmitted only over HTTPS
Anti‑Phishing Domain and Device Binding
"As a security-conscious user, I want clear confirmation of the site and device I’m granting access to so that I don’t accidentally approve a fraudulent login."
Description

Enforce strict origin and domain validation throughout the flow: QR payloads are opaque and environment-scoped; the mobile client verifies that the relying party ID matches the official TaxTidy domain; deep links use verified App Links/Universal Links; and the approval screen presents human-readable origin and device details. Reject mismatched domains, stale or cross-environment challenges, and tampered payloads. Implement replay protection, strict TLS requirements, and telemetry to detect anomalous approvals.

Acceptance Criteria
RPID Match and TLS Enforcement on QR Approval
Given a QR challenge generated from https://app.taxtidy.com in the prod environment When the mobile app scans and resolves the challenge Then it must verify the relying party ID equals taxtidy.com And the TLS connection must use TLS 1.2 or higher with a valid certificate chain and hostname match And certificate pinning (SPKI) must succeed against the current allowed pin set And only upon successful biometric authentication may the approval be sent And the desktop session must start only after all previous checks pass
Reject Mismatched or Look‑Alike Domains
Given a QR challenge originating from a domain that is not an allowed FQDN of taxtidy.com When the mobile app evaluates the relying party ID or origin Then the approval must be blocked And the user must see a high‑risk warning naming the untrusted domain And no session token is issued And a Security.AnomalousApprovalAttempt event with reason=DomainMismatch is recorded with device, origin, environment, and timestamp
Challenge Expiration, One‑Time Use, and Device‑Bound Token
Given a QR challenge with a server‑issued nonce and expiry of 60 seconds When the challenge is older than 60 seconds or already marked used Then the approval must be rejected with error=ChallengeExpiredOrReplayed And no session token is issued When a valid challenge is approved Then the resulting session token must be single‑use, valid for at most 120 seconds to complete desktop session bootstrap, and bound to the approving phone device key and the requesting desktop fingerprint And any reuse from a different desktop or after first redemption must fail with HTTP 401 and reason=TokenBoundOrReplayed
Environment‑Scoped QR Payloads
Given a mobile app configured for the prod environment When it scans a QR payload whose embedded environment is not prod Then the approval must be rejected with reason=EnvironmentMismatch And the user must see a message indicating the environment mismatch And a Security.CrossEnvironmentAttempt event is logged When environments match Then processing may continue to origin and signature validation
Verified App/Universal Link Enforcement
Given a deep link URL for QR approval And the device has verified App Links/Universal Links set up for taxtidy.com When the link is invoked Then the OS must route directly into the TaxTidy app without showing an in‑app webview And if verification fails, the app must not process the deep link and must show instructions to open the app manually And the approval flow must require an in‑app confirmation screen before any network approval is sent
Tamper‑Proof Opaque QR Payload
Given a QR payload that has been altered, truncated, or fails signature verification When the mobile app attempts to decode and verify it Then the payload must be treated as opaque and invalid; no fields are trusted And the approval must be blocked with reason=PayloadTampered And the event is logged with signatureFailure=true and no sensitive data persisted
Human‑Readable Origin and Device Confirmation Screen
Given an approval confirmation screen prior to sending approval When the challenge originated from https://app.taxtidy.com on a desktop device Then the screen must display at least: full origin FQDN, environment label (e.g., Production), desktop OS and browser name, and the requesting device nickname if available And the displayed values must match the server‑attested values for the specific challenge And the user must be able to cancel; cancellation must abort the flow and log reason=UserDeclined
Pairing Code Fallback (Camera Alternative)
"As a user whose camera can’t scan QR, I want a short code fallback so that I can still sign in without passwords or email links."
Description

Provide a passkey-backed fallback when scanning is unavailable: the desktop displays a short alphanumeric pairing code bound to the same one-time challenge, and the mobile app offers an “Enter Code” flow. After code entry and biometric approval via WebAuthn, the same token handoff occurs. The pairing code has a short TTL (e.g., 60 seconds), is single-use, rate-limited, and does not introduce passwords or email links. All anti-replay and domain-binding protections apply.

Acceptance Criteria
Code Fallback Entry and Passwordless UX Integrity
Given the user is on the desktop QR Handoff screen When the user selects "Use pairing code" on desktop or taps "Enter Code" in the mobile app Then the desktop displays a 6-character uppercase alphanumeric pairing code with a visible 60-second countdown bound to the current one-time challenge, and the mobile app shows a code entry screen And no password field or "email me a link" option is presented anywhere in this flow
Successful Pairing via Code and Biometric Approval
Given a valid, unexpired pairing code bound to the user’s desktop one-time challenge When the user enters the code in the mobile app and completes WebAuthn with a platform authenticator (biometric or device PIN) for the TaxTidy relying party Then the assertion verifies, a hardware-bound single-use desktop session token is minted, the desktop session becomes authenticated within 3 seconds, and the pairing code is immediately invalidated
Code Expiration at 60 Seconds
Given a pairing code older than 60 seconds or marked expired When it is submitted in the mobile app Then the backend rejects with a "Code expired" error, no session/token is issued, and the desktop prompts to generate a new code; the attempt is logged as expired
Single-Use Code and Anti-Replay Enforcement
Given a pairing code that has already been used successfully or explicitly invalidated When any client attempts to submit the same code again (even within the original TTL) Then the request is rejected with an "Invalid or used code" error, no token is issued, and the event is logged as a replay attempt
Rate Limiting on Code Entry Attempts
Given repeated invalid code submissions for the same desktop challenge or from the same device When more than 5 invalid attempts occur within 60 seconds Then further submissions are blocked for 60 seconds with a generic "Too many attempts" error, no information is leaked about code validity, and counters reset after the cooldown
Domain Binding and Origin Verification
Given the pairing code challenge is bound to the desktop origin (e.g., app.taxtidy.com) and the TaxTidy relying party ID When the WebAuthn assertion or code submission originates from a mismatched origin/RP or the desktop origin has changed Then verification fails, no session/token is issued, both clients show an "Origin mismatch" error, and the attempt is logged with reason rpId/origin mismatch
Token Handoff Scope and Lifetime
Given a successful WebAuthn verification for a pairing code When the desktop session token is created and delivered Then the token is single-use, scoped to the exact desktop browser session/tab that initiated the challenge, and expires if not consumed within 120 seconds; if the desktop tab closes before handoff, the token is destroyed and no session is created
Audit Logging and User Notifications
"As a user, I want visibility into where and when my account was accessed so that I can detect and respond to suspicious activity."
Description

Record structured audit events for each stage (QR issued, scanned, passkey verified, token minted, session started) with timestamps, user/account ID, device and client metadata, and outcome codes. Expose a user-facing security log within account settings and send optional push notification on successful desktop sign-in with a one-tap session review/revoke link. Ensure logs are immutable, privacy-conscious (hashed/trimmed IPs), retained per policy, and exportable to SIEM for security monitoring.

Acceptance Criteria
Audit Events for QR Handoff Lifecycle
Given a QR handoff is initiated When a QR code is issued Then an audit record is created with fields: event_type=qr_issued, timestamp (UTC ISO-8601), user_id (or null pre-auth), account_id (if available), correlation_id, client_metadata (user_agent, platform, app_version), device_metadata (device_type, os_version), ip_hash, outcome_code=success And When the QR is scanned from a device Then an audit record event_type=qr_scanned is created with the same correlation_id and scanning device/client metadata And When passkey authentication completes Then an audit record event_type=passkey_verified is created with outcome_code in {success, failure} and failure_reason if failure And When a desktop token is minted Then an audit record event_type=token_minted is created with token_ttl_seconds, key_id (not token), outcome_code And When the desktop session starts Then an audit record event_type=session_started is created with desktop client metadata and the same correlation_id And For any timeout/cancel/error Then an audit record is created with appropriate event_type and outcome_code=failure with a standardized failure_reason from an enumerated set And All records are durable with <=200 ms p50 and <=1 s p99 write latency and include a monotonically increasing sequence_number per correlation_id
User Security Log UI in Account Settings
Given an authenticated user navigates to Account Settings > Security Log When the log page loads Then the user sees only their events (scoped by user_id), sorted by most recent, with columns: timestamp (localized), event_type, device/platform, outcome_code, location (coarse), and correlation_id And When the user filters by date range, event_type, or outcome_code Then results update within 500 ms and reflect the filter And When the user expands a correlation_id Then the full QR handoff chain (qr_issued -> qr_scanned -> passkey_verified -> token_minted -> session_started) is shown in order with metadata And New events appear in the UI within 10 seconds of occurrence And The user can export visible results as CSV and JSON; export contains exactly the fields displayed plus correlation_id and sequence_number And Authorization prevents access to other users’ logs; direct API calls with foreign user_id return 403 and are audited And The page meets WCAG 2.1 AA accessibility criteria
Push Notification on Successful Desktop Sign-In
Given the user has at least one registered mobile device with push enabled and notifications opted-in When a desktop session transitions to session_started Then a push notification is sent within 5 seconds including app name, desktop client label, timestamp, and a single deep link CTA: Review/Revoke Session And The notification payload contains no secrets, tokens, or PII beyond device label and time And When the user taps the CTA and completes biometric on the phone Then the app opens a session detail view for that correlation_id showing device, location, and time, and offers a Revoke Session action And When the user taps Revoke Session Then the desktop session is terminated within 10 seconds, the user is signed out on desktop, and an audit event session_revoked_by_user is recorded And If push delivery fails or the user disabled notifications Then no notification is sent and no error is surfaced to the user; the sign-in event still appears in the security log
Immutability and Tamper Evidence of Audit Logs
Given an audit record has been written When any client or admin API attempts to update or delete it Then the operation is rejected with 403, no mutation occurs, and an audit event audit_mutation_blocked is recorded And Audit storage is append-only with cryptographic hash chaining; each record contains previous_hash and record_hash enabling verification And A daily verification job computes and stores a signed digest; verification failures raise alerts and are recorded as audit_integrity_alert events And Only a dedicated service account can write audit records; roles and permissions are enforced and covered by automated tests
Privacy-First Logging (Hashed/Trimmed IPs and PII Minimization)
Given an audit event is stored Then raw IP addresses are not stored; instead, ip_truncated retains IPv4 /24 or IPv6 /64 and ip_hash is a salted SHA-256 of the full IP; the salt rotates at a configurable interval And No passkey material, private keys, bearer tokens, or full device identifiers are stored; only key_id and device category are allowed And Free-text fields are disallowed; metadata is restricted to a whitelisted schema; attempts to write extra fields are rejected and logged And A privacy test suite validates that representative events contain no PII beyond user_id/account_id and masked IP; schema linting runs in CI
Retention Policy Enforcement
Given retention_days is configured to X in the environment When an audit record becomes older than X days Then it is purged within 24 hours by a scheduled job, and an audit event audit_retention_purge is recorded with a count of records purged And Purged records are not visible in the user-facing security log or API And Retention jobs are idempotent and do not purge records newer than X days And Changing retention_days takes effect within the next purge cycle And A discovery endpoint returns the current retention_days
SIEM Export and Streaming
Given a SIEM destination is configured (syslog over TLS or HTTPS webhook) When audit events are generated Then they are streamed within 60 seconds in JSON using the documented schema, including correlation_id, sequence_number, event_type, timestamp, outcome_code, and metadata And Delivery is at-least-once with a deduplication_id to support consumer-side dedupe And On transport failure, events are retried with exponential backoff up to a configurable maximum; backlog is buffered to a configurable size and oldest events are dropped with alerts if the buffer is exhausted And An on-demand export API supports time-bounded fetch with a continuation cursor; the export matches exactly what is stored and validates against the schema And A health endpoint exposes delivery lag and last success timestamp; SIEM connection failures generate audit_siem_delivery_alert events
Rate Limiting and Abuse Prevention
"As a platform operator, I want safeguards against brute-force and replay attacks so that the QR Handoff feature remains secure and reliable at scale."
Description

Implement adaptive rate limits for QR issuance, pairing code attempts, and WebAuthn assertions per IP, device, and account. Automatically invalidate outstanding challenges after successful login or after multiple failures. Detect and block replayed assertions, duplicated tokens, and anomalous patterns; require additional verification (e.g., challenge refresh) under suspected abuse. Instrument metrics, alerts, and dashboards; ensure no PII appears in QR payloads; and document incident response runbooks.

Acceptance Criteria
Adaptive Rate Limiting: QR Issuance
- Given an entity (IP, device fingerprint, or account) requests QR handoff challenges, When more than 3 issuances occur within 60 seconds for the same entity, Then the 4th+ requests return HTTP 429 with a Retry-After header and reason=qr_issuance_rate_limited logged. - Given an entity has been rate limited, When no requests occur for 15 minutes, Then the quota resets and the next issuance succeeds with HTTP 200. - Given an entity receives 2 consecutive 429s within 5 minutes, When additional issuance requests are made, Then exponential backoff is enforced (min blocks of 30s, then 60s, then 120s) and each block window is logged. - Given normal behavior (<=2 issuances per 60 seconds per entity), Then no throttling occurs and no false-positive alerts are triggered. - Rate limits are enforced independently per IP, per device, and per account; the most restrictive limit applies, and all decisions are auditable via telemetry without PII.
Adaptive Rate Limiting: Pairing Code Attempts
- Given a pending QR handoff challenge, When pairing code submissions exceed 5 attempts per challenge or 10 attempts per IP within 5 minutes, Then further attempts return HTTP 429 and the challenge is invalidated with error=challenge_locked. - Given pairing attempts for an entity have an invalid rate >50% over the last 20 attempts within 10 minutes, Then subsequent attempts require a new challenge (HTTP 423 with action=refresh_challenge) for 10 minutes. - Given a pairing attempt is successful, Then any additional submissions for that challenge return HTTP 410 error=challenge_consumed and do not count toward rate limits. - All events record entity type (IP/device/account), outcome, and reason; logs and metrics are redacted and contain no PII.
WebAuthn Assertion Integrity and Rate Limiting
- Given WebAuthn get() operations are initiated, When more than 3 assertions are attempted per device or account within 60 seconds, Then subsequent attempts return HTTP 429 with Retry-After and metric=webauthn_rate_limited. - Given a challenge_id and clientData.challenge, Then only the first valid assertion with a unique jti and increasing signCount is accepted; any duplicate assertion (same jti/signature) within 10 minutes returns HTTP 409 error=replay_detected. - Given an assertion presents a non-incrementing signCount for a registered credential, Then reject with HTTP 401 error=signcount_regression and flag the credential for review in security telemetry. - Given an assertion references a consumed or expired challenge, Then return HTTP 410 error=challenge_consumed. - All rejections increment replay/duplication metrics and emit security audit logs without PII.
Challenge Lifecycle and Automatic Invalidation
- Given a QR handoff challenge is issued, Then its TTL is <=60 seconds and auto-expires; expired challenges cannot be used. - Given a desktop session is approved via phone biometric, Then all outstanding QR challenges for that account and initiating session are immediately marked consumed and cannot be reused. - Given 5 consecutive failed pairing or assertion attempts occur against a challenge, Then the challenge is invalidated and further requests return HTTP 410 error=challenge_locked until a new challenge is issued. - Given a challenge is invalidated by any reason, Then it is removed from the active store within 5 seconds and excluded from any API responses. - All state transitions (issued, consumed, expired, locked) are timestamped and exported to telemetry.
Replay and Duplicate Token Protection
- Given a token/assertion is issued for QR handoff, Then it includes a cryptographically random jti/nonce (>=128 bits) and is single-use; acceptance of one assertion invalidates reuse. - Given a second request presents a previously seen jti/nonce or identical assertion signature within 10 minutes, Then reject with HTTP 409 error=replay_detected and increment replay metrics. - Given tokens are issued, Then duplicated token IDs within any 24h window are impossible by construction (collision tests in CI) and any detection triggers a sev-1 alert. - Given a replay is detected from an IP/device, Then the entity is temporarily blocked for 10 minutes (HTTP 423) and requires challenge refresh to proceed.
Anomaly Detection and Step-up Verification
- Given anomaly signals such as >100 pairing failures from one IP in 5 minutes, geolocation delta between desktop and phone >1000 km within 2 minutes, or a 10x spike vs 24h baseline per entity, Then mark the entity suspected_abuse for 30 minutes. - When suspected_abuse is active for an entity, Then new QR issuances and pairing attempts require step-up verification: enforce immediate challenge refresh and proof-of-presence re-scan; affected requests return HTTP 423 with action=refresh_challenge and reason=suspected_abuse. - Given anomaly signals subside for 30 minutes, Then automatically clear suspected_abuse and resume normal limits. - All anomaly detections and clearances emit alerts and appear on the security dashboard with timestamps and entity aggregation, without PII.
Security Telemetry, Alerts, Dashboards, and Runbooks
- Metrics emitted at 1-minute resolution include: QR issuances, pairing attempts, WebAuthn assertions, 2xx/4xx/5xx, 429 counts per entity, challenge invalidations by reason, replay detections, suspected_abuse activations, and P95/P99 latencies; dashboards show 24h and 7d views segmented by IP/device/account. - Alerts fire within 2 minutes when: replay_detected >5/min for 5 minutes; 429_rate >20% for any entity for 10 minutes; challenge_locked >50/min globally; or metrics ingestion delay >5 minutes; alerts route to on-call with severity and runbook links. - Incident response runbooks for rate-limit abuse, replay attacks, token duplication, and QR payload violations exist at docs/runbooks/qr-handoff/; each runbook includes roles, triage steps, containment actions (IP/device block, credential revocation, key rotation), customer comms templates, and RTO/RPO; a tabletop exercise is completed within the last 90 days with link to notes. - On-call can execute documented steps to invalidate all active challenges and tighten limits within 5 minutes; scripts or automation referenced in runbooks are present and tested in staging. - All logs and dashboards exclude PII by policy and automated checks; any PII detection in telemetry triggers a sev-1 alert and incident response per runbook.
QR Payload Contains No PII
- Given a generated QR payload, Then it contains only: challenge_id (opaque >=128-bit random), issuer URL, and expiry timestamp; it contains no email, name, phone, account ID, IP address, or device identifiers. - Given the QR payload is produced, Then total size <=800 bytes, it is signed or encrypted (JWS/JWE), and validated server-side before use; schema versioning is enforced. - Given CI runs, Then unit tests and schema checks fail if any PII field is added to the payload; static scans verify no PII-bearing properties are serialized. - Given a QR is scanned, Then server validates payload schema and rejects malformed or PII-bearing payloads with HTTP 400 error=invalid_payload and logs a security event without PII.

Two-Key Recovery

Recover access with confidence using two approvals instead of one—any combination of a second device, a trusted contact, or an offline recovery code. Built-in time locks and cancel windows prevent rushed mistakes while keeping you in control during emergencies.

Requirements

Two-Key Recovery Orchestration Engine
"As a locked-out TaxTidy user, I want the system to accept any two valid approvals and unlock my account only when both are verified so that my access is restored securely without relying on a single point of failure."
Description

Implement a backend state machine that coordinates recovery requests requiring any two approvals from supported methods (secondary device approval, trusted contact approval, or offline recovery code). The engine must validate method availability, track partial approvals, enforce method diversity rules (e.g., cannot count the same code twice), handle expirations, and resolve to success only when two valid approvals are received within the allowed window. Include rate limiting, brute-force protection for codes, replay prevention, and comprehensive error states. Provide APIs for initiating recovery, submitting approvals, querying status, canceling requests, and finalizing account unlock. All events must be idempotent and recorded with timestamps for auditability.

Acceptance Criteria
Initiate Recovery Request and Validate Method Availability
Given a user account with at least two configured recovery methods (secondary device, trusted contact, offline recovery code) When the client POSTs /recovery/initiate with explicit methods or mode=auto Then the engine validates method availability (active, not revoked, not exhausted) and responds 201 with recoveryRequestId, status="pending", availableMethods[], timeLockStart, timeLockEnd (if configured), and expiryTimestamp And if fewer than two methods are available, the engine responds 422 with errorCode="INSUFFICIENT_METHODS" and does not create a request And GET /recovery/{id} immediately reflects the same metadata and status
Submit Mixed-Method Approvals and Enforce Diversity Rules
Given an active recovery request within its allowed window When the first approval is verified via method A (secondary device approval, trusted contact approval, or offline recovery code) Then the engine records approval #1 with method=A, actor fingerprint, and timestamp, increments approvalsCount to 1, and keeps status="pending" When a subsequent approval attempts to reuse the same factor (same device fingerprint, same trusted contact identity, or same offline code instance) Then the engine rejects it with 409 and errorCode="DUPLICATE_FACTOR" without changing approvalsCount When a second approval via a different method B is verified within the window Then approvalsCount becomes 2 and status transitions to "ready_to_finalize"
Handle Expiration, Time Lock, and Cancel Windows
Given a recovery request with timeLockDuration=30m and expiryDuration=24h When approvals are submitted during the time lock window Then the engine stores them but prevents finalization When POST /recovery/finalize is called before the time lock ends Then the engine responds 423 (Locked) with errorCode="TIME_LOCK_ACTIVE" and no state change When currentTime > expiryTimestamp Then the engine auto-transitions the request to status="expired" and any further approvals or finalize calls return 410 (Gone) with errorCode="REQUEST_EXPIRED" When the user calls POST /recovery/cancel before finalization Then the engine sets status="canceled", invalidates pending approval tokens, and any subsequent approve/finalize attempts return 409 with errorCode="REQUEST_CANCELED"
Rate Limiting and Brute-Force Protections for Offline Codes
Given POST /recovery/approve with method=offline_code for a specific recoveryRequestId When an incorrect code is submitted Then the engine responds 401 (Unauthorized), increments failedAttempts for the request+method, and returns remainingAttempts And after 5 failed attempts for that request, further code attempts return 429 (Too Many Requests) with retryAfter And the account-level throttle enforces a maximum of 10 failed offline-code attempts per hour across requests, returning 429 when exceeded And the IP throttle enforces a maximum of 3 attempts per minute per IP for offline codes, returning 429 when exceeded When a correct offline code is submitted Then the engine verifies it in constant-time, consumes the code (one-time use), records approval, and subsequent reuse of the same code returns 409 with errorCode="REPLAY_DETECTED"
Idempotency and Replay Prevention for All Endpoints
Given any POST endpoint (/recovery/initiate, /recovery/approve, /recovery/cancel, /recovery/finalize) with Idempotency-Key header When the same request payload is retried with the same Idempotency-Key within 24h Then the engine returns the original response (status, body) and performs no additional state transitions or duplicate audit entries When an approval token (trusted contact link or secondary device token) is submitted more than once Then only the first valid submission changes state; subsequent submissions return 409 with errorCode="REPLAY_DETECTED" and no state change And offline codes, once accepted, are marked consumed and cannot be used again across any request
Audit Logging with Timestamps and State Transitions
Given any event (initiate, approval accepted/rejected, cancel, expire, finalize) When the event occurs Then the engine writes an immutable audit entry containing eventId, recoveryRequestId, actorType (user/device/contact/system), method (if applicable), previousState, newState, outcome (success/failure with errorCode), and timestamp in UTC ISO-8601 with millisecond precision And GET /recovery/{id}/events returns a complete, strictly ordered list of all events for that request with no duplicates And idempotent retries return the same eventId and do not create additional entries
Finalize Account Unlock after Two Valid Approvals
Given a recovery request with approvalsCount=2, status="ready_to_finalize", within expiry, not canceled, and time lock elapsed When the client POSTs /recovery/finalize with recoveryRequestId Then the engine validates preconditions, unlocks the account, rotates/invalidates all recovery tokens tied to the request, sets status="completed", establishes a fresh authenticated session (if configured), and returns 200 with confirmationId When any precondition is not met (time lock active, not enough approvals, expired, canceled) Then the engine returns a specific error (423 TIME_LOCK_ACTIVE, 409 NOT_READY, 410 REQUEST_EXPIRED, or 409 REQUEST_CANCELED) without unlocking the account
Recovery Method Enrollment & Management
"As a security-conscious freelancer, I want to enroll and manage multiple recovery methods so that I can regain access even if one method becomes unavailable."
Description

Provide a guided, mobile-first enrollment flow to add, verify, and manage recovery methods: link a secondary device, nominate and verify trusted contacts, and generate offline recovery codes. Users must be able to view active methods, set a minimum required mix (e.g., at least one person-based method), revoke or replace methods, and re-verify as needed. Store method metadata securely (e.g., keys and codes encrypted at rest, one-time codes hashed). Enforce setup checks (e.g., confirm two distinct methods before enabling Two-Key Recovery) and provide a summary confirmation. Include guardrails such as periodic revalidation prompts and alerts when recovery posture weakens (e.g., too few methods).

Acceptance Criteria
Enroll Secondary Device via Mobile Flow
Given a logged-in user on a mobile device, When they select “Link secondary device,” Then the app displays both a QR code and a 6-digit pairing code that expire in 10 minutes. Given a new device scans the QR or enters the pairing code within the validity window, When pairing succeeds, Then the secondary device appears in Active Methods with device name, platform, last seen timestamp, and status = Verified. Given an invalid or expired pairing attempt, When submission occurs, Then the system returns a non-revealing error and no device is added to Active Methods. Given a successful pairing, When the user signs out and back in, Then the linked device remains visible and verified. Given the user views audit events, When pairing completes, Then an event “Secondary device added” with timestamp and device fingerprint is recorded.
Nominate and Verify Trusted Contacts
Given a logged-in user, When they add a trusted contact by entering name and a unique email or phone, Then the system sends a verification invite and shows the method as Pending with a sent timestamp. Given a pending contact, When the contact completes identity verification via the invite and submits the OTP within 10 minutes, Then the method status updates to Verified and the verification timestamp is stored. Given an invite that is not acted on within 7 days, When the user views the method, Then status is Expired and a “Resend invite” action is available. Given a pending or verified contact, When the user selects Cancel/Remove and confirms, Then the method is revoked immediately and cannot be used for recovery approvals. Given a verified contact, When the user triggers Re-verify, Then a fresh OTP is required and success updates the last verified date; failures after 3 attempts lock re-verification for 15 minutes.
Generate Offline Recovery Codes
Given a logged-in user, When they generate offline recovery codes, Then exactly 10 unique one-time codes are created and displayed once with an option to download/print. Given code generation completes, When data is stored, Then only salted hashes of codes are persisted; plaintext codes are not retrievable after leaving the screen. Given an existing code set, When the user regenerates codes, Then the prior set is invalidated immediately and marked Revoked with an audit event. Given an offline code, When it is redeemed during recovery, Then it becomes unusable for any future attempts and is marked Used with timestamp and device/contact that co-approved. Given code generation, When the user has not confirmed secure storage via checkbox, Then the “Finish” action is disabled.
Manage Recovery Methods (View, Revoke, Replace, Re-verify)
Given the user opens Active Methods, When methods are listed, Then each entry shows type (Secondary Device/Trusted Contact/Offline Codes), status (Verified/Pending/Unverified), last verified date, and risk indicator. Given a verified method, When the user selects Re-verify and completes the flow, Then status remains Verified and last verified date updates; on failure, status becomes Unverified and the method is excluded from recovery approvals. Given a verified method, When the user selects Revoke and confirms, Then the method is immediately removed from Active Methods and cannot be used; an audit log entry is created. Given a revoked or failing method, When the user selects Replace, Then the flow adds a new method of the same type and upon success optionally offers to remove the old method if still present. Given network loss during any management action, When connectivity resumes within 60 seconds, Then the UI restores the last known state and prompts the user to retry or cancel without duplicating methods.
Enforce Minimum Mix and Distinct Methods Before Enabling Two-Key Recovery
Given a default policy, When the user attempts to enable Two-Key Recovery, Then the system requires at least 2 distinct methods, including at least 1 person-based or device-based method; offline codes alone are insufficient. Given the user-customized minimum mix (e.g., 3 methods with at least 1 trusted contact), When methods do not meet the policy, Then enabling is blocked and a checklist of unmet requirements is shown. Given requirements are met, When the user proceeds to enable, Then a summary confirmation displays the active methods, their statuses, and the user’s minimum mix; enabling requires explicit confirmation. Given two methods on the same physical device or identity, When counted toward distinctness, Then the system counts them as 1 and presents a warning to add a truly distinct method. Given enabling is completed, When the user returns later, Then the enabled state persists and is reflected in settings with the policy shown.
Security of Stored Recovery Method Metadata
Given recovery method data is persisted, When inspected via approved security tests, Then device keys and contact tokens are encrypted at rest and decryption keys are managed by a KMS; offline codes are stored only as salted hashes. Given any API that returns recovery method data, When invoked, Then no endpoint returns plaintext secrets or full offline codes; only masked values and metadata are returned. Given read/write operations on recovery methods, When they occur, Then audit logs capture user ID, action, method type, and timestamp; logs are immutable and queryable by security. Given key rotation policy, When rotation occurs, Then encrypted data remains accessible post-rotation and a rotation event is logged without exposing secrets. Given a penetration test, When attempting to retrieve plaintext codes after generation, Then tests confirm it is not possible via UI, API, or storage access without KMS keys and authorization.
Periodic Revalidation and Recovery Posture Alerts
Given verified person-based and device methods, When 90 days elapse since last verification, Then the user receives in-app and email/push prompts to re-verify, with a single snooze option up to 7 days. Given a method exceeds 14 days past due for revalidation, When the user views Active Methods, Then the method is marked Unverified and excluded from the minimum mix count until re-verified. Given the total verified methods drop below the user’s minimum mix (e.g., revocation, expiry), When the condition is detected, Then an alert is sent within 5 minutes and a red posture indicator appears in settings until resolved. Given posture changes (weakened or restored), When they occur, Then an event is added to the security/activity feed with timestamp and details. Given posture is below minimum, When the user attempts to enable Two-Key Recovery or initiate a recovery that requires the policy, Then the action is blocked with guidance on how to restore posture.
Trusted Contact Verification & Approvals
"As a TaxTidy user, I want a trusted contact to securely approve my recovery request so that I’m not locked out when I lose my primary device."
Description

Enable users to nominate trusted contacts by email/phone, collect consent, and verify identity with a secure out-of-band invitation. Provide a lightweight approver portal or in-app experience for contacts to review a recovery request, see non-sensitive context (e.g., user name, request timestamp), and approve or decline with optional reason. Include revocation, replacement, and notification when a contact’s details change. Enforce contact eligibility (minimum age, maximum number, region restrictions) and limit concurrent requests. Capture approver device fingerprint, IP, and timestamp in the audit trail. Prevent approver lock-in by allowing contacts to opt out at any time.

Acceptance Criteria
Nominate Trusted Contact and Send Out-of-Band Invitation
Given an authenticated user with Two-Key Recovery enabled When the user submits a trusted contact with a valid email or phone Then the system validates format and deduplicates against existing contacts And if eligible, creates the contact in Pending status and sends a single-use, out-of-band invite link via the provided channel within 10 seconds And the invite link expires after 24 hours or upon first successful use, whichever comes first And the user interface reflects Pending status within 2 seconds with resend and revoke options And if delivery fails, the system surfaces a retry option and logs a delivery error with reason code
Trusted Contact Identity Verification and Consent Capture
Given a contact opens a valid invite link on a supported device When the contact verifies ownership via a one-time code sent to the same channel (email/SMS) Then the code must be 6 digits, time-bound to 10 minutes, with a maximum of 5 attempts before lockout for 30 minutes And upon successful verification, the contact is shown non-sensitive context (user display name, invite timestamp) and a clear consent screen And when consent is granted, the system records consent timestamp, versioned consent text ID, IP, device fingerprint, and user agent, and sets status to Active And if consent is declined or link expired, status becomes Declined/Expired respectively and the user is notified
Approver Reviews Recovery Request and Submits Decision
Given an Active trusted contact receives a recovery request notification When the contact opens the approver portal or in-app view Then the view displays non-sensitive context (requesting user display name, request ID, request timestamp) and a remaining time indicator And the contact can Approve or Decline and optionally add a free-text reason up to 250 characters And on decision, the system records decision, reason (if any), timestamp, IP, device fingerprint, and user agent, and returns a confirmation within 2 seconds And the requesting user receives a real-time notification of the decision; late decisions after expiration are rejected with an explanatory message
Revocation and Replacement of Trusted Contacts
Given a user views their trusted contacts list When the user selects Revoke on an Active or Pending contact Then the system requires a confirmation step and, upon confirmation, sets the contact to Revoked, invalidates any outstanding invites or approval ability, and logs the action And the revoked contact is notified of revocation via original channel without exposing sensitive user data And when the user adds a replacement contact, eligibility is re-checked and the total active contacts cannot exceed the configured maximum (e.g., 5) And any in-progress recovery requests referencing the revoked contact are updated to remove that approver and inform the requester
Notifications for Contact Detail Changes and Opt-Out
Given a trusted contact changes their email or phone via the approver portal When the change is submitted Then the system requires re-verification of the new channel before it becomes active, sets contact status to Verification Required, and notifies the user of the pending change And if the contact opts out, the system immediately disables their approver capability, sets status to Opted Out, notifies the user, and updates audit logs And attempts to use an opted-out contact for approval are blocked with a specific error code and guidance
Eligibility Rules and Concurrent Request Limits Enforcement
Given a user attempts to add a trusted contact When the contact provides age and region details and passes basic format checks Then the system enforces minimum age (e.g., 18+), allowed regions list, and maximum number of active contacts (e.g., 5); ineligible contacts are rejected with precise error messages And duplicate contacts by channel identifier (email/phone) are prevented And when a recovery is initiated, the system allows at most 1 active recovery request per user; additional requests are blocked until completion or cancellation And all rejections are logged with rule ID and timestamp
Audit Trail Records for Approver Actions
Given any approver action occurs (invite sent, consent given/declined, approval/decline decision, revocation, opt-out, detail change) When the event is processed Then an immutable audit entry is written containing event type, actor role, correlation/request ID, timestamp (UTC), IP, device fingerprint, user agent, and outcome/reason And personally identifiable information is minimized and stored per data retention policy, with redaction applied in user-visible logs And authorized users can retrieve a chronological audit view within 2 seconds for the last 90 days
Secondary Device Linking & Push Approvals
"As a user with multiple devices, I want to approve recovery from my spare phone so that I can quickly regain access without waiting on others."
Description

Support linking a second mobile device to the account using a secure pairing flow (QR code plus short-lived token). Deliver push notifications for recovery approvals to any linked device, with biometric or device PIN confirmation. Provide offline fallback via time-based challenge code displayed on the second device that the server verifies. Allow users to view and revoke linked devices, with automatic invalidation on device reset or app reinstallation. Implement device binding, rotating device keys, and mutual TLS where applicable. Ensure cross-platform support (iOS/Android) and background notification reliability.

Acceptance Criteria
Secure QR Pairing with Short-Lived Token
Given the user initiates "Link a second device" from the primary device When the primary device displays a QR containing an ephemeral session ID and the server issues a pairing token with TTL 120 seconds And the secondary device scans the QR and performs a challenge–response proving possession of a freshly generated hardware-backed device key over mutually authenticated TLS And the user confirms pairing with biometric or device PIN on both devices Then the server binds the secondary device to the account, persists device metadata (model, OS, key fingerprint), and returns success within 3 seconds And pairing is rejected if the token is expired, replayed, or invalid, or if scan attempts exceed 5 per minute (respond 429) And all pairing attempts (success/failure) are audit-logged with timestamps and device identifiers
Push Approval with Biometric Confirmation
Given an account recovery that requires second-device approval is initiated When push notifications are dispatched to all online linked devices via APNs/FCM Then at least one linked device receives the notification within 10 seconds at the 95th percentile and within 30 seconds at the 99th percentile And opening the notification requires biometric or device PIN before displaying requester device, approximate location, time, and action summary And an approval produces a signed assertion using the device key; the server verifies signature, device binding, and request freshness before granting And a rejection cancels the request across devices; if no response is received within 5 minutes, the request auto-expires And all outcomes (approved, rejected, expired) are audit-logged
Offline Time-Based Challenge Code Fallback
Given push delivery is unavailable When the user unlocks the linked secondary device with biometric or device PIN Then the app displays a time-based challenge code derived offline from the device secret that changes every 30 seconds with 1-step clock skew tolerance And the server accepts a code only once within its validity window and only if it matches the pending recovery request And after 3 consecutive invalid submissions, further attempts are blocked for 60 seconds and logged And the code display works without network connectivity on the secondary device
Linked Device Management and Revocation
Given the user opens Linked Devices in settings Then the list shows for each device: nickname, platform, last seen timestamp, and key fingerprint suffix And selecting a device allows rename or revoke; revoke requires biometric or device PIN and confirmation When a device is revoked Then its push tokens are invalidated, its device key is blacklisted, and future approvals from that device are rejected with reason "revoked" And both the account owner and the revoked device receive notifications of revocation And the UI reflects the change within 5 seconds and all actions are audit-logged
Auto-Invalidation on Device Reset or App Reinstall
Given a linked device is factory reset or the app is reinstalled causing loss of binding keys When the device attempts any approval or attestation Then the server detects key mismatch and automatically invalidates the prior binding And the invalidated binding cannot approve; attempts return 401 with reason "binding_invalid" And remaining linked devices and the account email receive a security alert within 1 minute And the device must complete the pairing flow again to relink
Device Key Rotation and Mutual TLS Enforcement
Given a device binding is older than 90 days or a risk event triggers rotation When key rotation is initiated Then the device generates a new key pair and proves continuity by signing with the old key; the server atomically updates the binding And the old key remains valid for at most 24 hours for in-flight requests, after which it is revoked And all pairing and approval API calls use mutual TLS with a device-bound client certificate where the platform supports hardware-backed keys; otherwise TLS with certificate pinning is enforced And approval requests from capable devices without mutual TLS are rejected
Background Notification Reliability and Cross-Platform Support
Given a device is linked and the app is backgrounded or terminated When a recovery approval request is issued Then APNs/FCM notifications are delivered with p95 latency ≤ 10 seconds and p99 ≤ 30 seconds, with ≥ 99.5% delivery success over 24 hours And tapping the notification deep-links to the approval screen; the action proceeds after biometric or device PIN without requiring a full app relogin And Android uses high-priority FCM with proper notification channel and foreground service when needed; iOS uses actionable notification categories And undelivered notifications are retried up to 3 times with exponential backoff (5s, 20s, 60s) And delivery, open rate, and time-to-approve metrics are captured per platform and exposed to monitoring
Offline Recovery Codes (One-Time)
"As a traveling freelancer, I want one-time offline recovery codes so that I can regain access even without internet or a second device."
Description

Generate a set of single-use, high-entropy recovery codes during setup. Display codes in a secure, printable format with copy/download disabled by default and clear storage guidance. Support verifying one code during setup to ensure the user understands usage. Allow regeneration (which invalidates all previous codes), show remaining count, and block using the same code twice. Hash and salt codes server-side; never store plaintext. Enforce rate limits and lockouts on repeated invalid attempts. Provide i18n-aware instructions and visual cues for offline safekeeping.

Acceptance Criteria
Setup: Generate One-Time High-Entropy Recovery Codes
Given the user is at the Two-Key Recovery setup step When they choose to generate offline recovery codes Then the system creates exactly 10 unique codes using a cryptographically secure random generator with ≥80 bits of entropy per code And each code has a minimum length of 16 characters And the generated codes are marked as single-use in the system And the user is advanced to the display step upon successful generation
Display: Secure, Printable Codes with Storage Guidance
Given a newly generated set of offline recovery codes When the codes are displayed on screen Then the UI renders a printer-friendly layout suitable for paper storage And clipboard copy and file download actions are disabled by default And clear, prominently placed guidance explains safe offline storage (e.g., print and store securely, do not save online) And a Print action is available and produces a print preview containing the codes and instructions And no third-party resources are loaded that could exfiltrate codes during display
Onboarding: Verify One Code to Confirm Understanding
Given the user is still in setup after viewing their codes When the user enters one of the displayed codes into a verification input Then the system confirms the code matches the generated set And the verification does not consume or invalidate the code And the user cannot complete setup until a correct code is verified And if the user enters a non-matching code, a clear error is shown and completion is blocked
Maintenance: Regenerate Codes and Invalidate Previous Set
Given the user has an existing set of offline recovery codes When the user chooses to regenerate codes and confirms the action Then a brand-new set is generated and displayed And all previously issued codes are immediately and permanently invalidated And the remaining-count indicator resets to the new set size And attempting to use any prior code thereafter results in a generic invalid-code error
Usage: Consume Code, Update Remaining Count, and Block Reuse
Given the user is in a recovery flow that accepts an offline recovery code as one approval factor When the user submits a valid, unused code from their set Then the system accepts the code exactly once and marks it as consumed And the remaining code count is decremented by one and reflected in the user’s security settings after sign-in And submitting the same code again results in a generic invalid/used error and does not change the count And the remaining count never displays a negative value
Security: Server Storage and Abuse Protection
Given offline recovery codes are generated Then the server stores only salted, slow-hash digests of the codes (e.g., Argon2/bcrypt) and never plaintext And no API, log, or admin view returns plaintext codes And all code submissions are transmitted over TLS Given an attacker attempts repeated invalid codes When more than 5 invalid attempts occur for an account within 10 minutes Then further attempts are blocked for 15 minutes for that account And error messages remain generic and do not reveal validity or remaining counts And rate limits are enforced per account and per IP
Internationalization: i18n Instructions and Visual Cues
Given the app locale is set to a supported language When viewing the codes display, verification, or recovery submission screens Then all instructional text, button labels, and error messages are shown in the selected locale And text expansion up to 30% in length does not truncate or overflow on a 360×640 mobile viewport And any date/time (e.g., lockout timers) follows locale formatting rules And visual cues for offline safekeeping (icons/labels) are culturally neutral and localized with accessible text
Time Lock & Cancel Window Controls
"As a cautious user, I want a built-in delay and the ability to cancel a recovery so that rushed or fraudulent requests don’t unlock my account."
Description

Introduce configurable time delays for recovery completion (e.g., 1–24 hours) after the first approval, plus a user-visible cancel window to abort suspicious requests. Send immediate notifications when a recovery is initiated, upon each approval, and before finalization to enable timely cancellation. Allow product-defined defaults with user-tunable options within safe bounds. Handle edge cases such as cancel after second approval, expired windows, and overlapping requests. Persist countdown state across sessions and ensure consistency under retries. Clearly communicate expected timelines and remaining time in the UI.

Acceptance Criteria
Initiation and Approval Notifications with Time Lock Start
Given a Two-Key Recovery request is initiated and the first approval is recorded When the first approval is accepted Then the system starts a countdown equal to the configured time lock duration (in hours) And sends immediate notifications for (a) recovery initiation and (b) the first approval to the account email and all registered push devices And for any subsequent approval, the system sends an immediate notification to the same channels And the audit log records each notification event with timestamp, channel, and approval source
Configurable Time Lock Within Safe Bounds
Given the user opens Recovery Settings When selecting a time lock value Then the UI only permits selection from 1–24 hours in 1-hour increments And the default selection equals the product-defined default value And if an out-of-bounds value is submitted via API, the request is rejected with HTTP 400 and an error code TIMELOCK_OUT_OF_RANGE And the saved value persists and is applied to the next recovery flow
Cancel Window Available Until Finalization
Given a recovery is in countdown prior to finalization When the user clicks Cancel via in-app control or secure notification link Then the recovery state transitions to CANCELED within 5 seconds And cancellation notifications are sent immediately to account email and all registered push devices And any finalize operation on the canceled request returns HTTP 409 with state=CANCELED And the cancel action remains available up to (but not including) the finalization moment
Cancel After Second Approval Prevents Finalization
Given both required approvals are recorded and the time lock has not expired When the user cancels the recovery Then the recovery state becomes CANCELED and will not auto-finalize on countdown expiry And all participants see 'Canceled' status with timestamp in the activity view And a new recovery may be initiated immediately after cancellation without conflict
Pre-Finalization Notice and Auto-Finalize on Expiry
Given required approvals are satisfied and a time lock is active When the countdown reaches 10 minutes remaining Then the system sends a pre-finalization notification to account email and all registered push devices And when the countdown expires with no cancellation Then the system finalizes the recovery within 60 seconds and sends a finalization notification And the audit log records pre-finalization and finalization timestamps
Overlapping Recovery Requests Are Blocked
Given there is an active recovery request in PENDING or COUNTDOWN state When the user attempts to initiate another recovery Then the API responds with HTTP 409 CONFLICT and returns the active request ID and remaining time And the UI displays the active request details and countdown instead of starting a new one And no new notifications are sent for the blocked attempt
Countdown Persistence and Consistency Across Sessions
Given a recovery countdown is in progress When the user refreshes, switches devices, or reconnects after network loss Then the remaining time shown differs by no more than ±2 seconds from server-calculated time And the countdown resumes from server state without restarting or skipping And repeated delivery retries do not create duplicate approvals or state transitions And the UI shows both a live countdown and the absolute finalize-by timestamp in the user’s locale and timezone
Recovery Notifications & Audit Logging
"As a user, I want clear notifications and an auditable record of my recovery so that I can verify what happened and spot any suspicious activity."
Description

Implement multi-channel notifications (push, email, optional SMS) for key recovery events: initiation, partial approval, second approval, time-lock start, upcoming finalization, completion, and cancellation. Provide an in-app timeline with immutable, tamper-evident audit logs capturing who approved, methods used (generic, non-secret), timestamps, IP/device info, and request outcomes. Offer export of logs as a signed JSON bundle for compliance or support review. Ensure deliverability monitoring, unsubscribe controls for non-security emails, and localization. Integrate with existing TaxTidy notification infrastructure and respect user communication preferences while always sending mandatory security alerts.

Acceptance Criteria
Event Notification Triggers & Channel Routing
Given a user with Two-Key Recovery enabled and saved notification preferences And a recovery request is initiated for their account When the recovery request is initiated Then a "Recovery Initiated" notification is sent via push and email within 10 seconds And an SMS is sent only if the user has explicitly opted in to SMS for security alerts When a first approval is recorded Then a "Partial Approval" notification is sent via the same channels as above When the second approval is recorded Then a "Second Approval" notification is sent via the same channels as above When the policy time lock begins Then a "Time-Lock Started" notification is sent via the same channels as above When the recovery is within the configured upcoming finalization window (default 24 hours) Then an "Upcoming Finalization" notification is sent once per recovery request When the recovery completes Then a "Recovery Completed" notification is sent via the same channels as above When the recovery is canceled Then a "Recovery Canceled" notification is sent via the same channels as above And each notification payload includes: request_id, event_type, occurred_at (ISO 8601 with timezone), masked account identifier, and support contact link And each notification is tagged with a delivery tracking ID that is recorded in the audit log And notifications are localized to the user's preferred locale with fallback to en-US
Mandatory Security Alerts vs Preferences
Given a user has opted out of marketing and non-security emails And the user has not opted in to SMS When a security-critical recovery event occurs (initiation, partial approval, second approval, time-lock start, upcoming finalization, completion, cancellation) Then push and email security alerts are sent regardless of general unsubscribe status And SMS is not sent unless the user opted in And non-security messages remain unsubscribed And each email includes a visible "Manage notifications" link and does not include an unsubscribe for mandatory alerts And preference changes take effect within 2 seconds and are respected on subsequent sends
Tamper-Evident Audit Log Entry Creation
Given any Two-Key Recovery event occurs When the system writes the audit log entry Then the entry includes: request_id, event_type, actor_type (owner/approver/system), actor_id, actor_display_name, approval_method (generic: second_device/trusted_contact/offline_code), outcome (success/failure/canceled), ip_address, device_info (model/OS/app_version), occurred_at (ISO 8601), delivery_tracking_ids, and locale And secrets or full contact details are not stored (no codes, no phone numbers or emails beyond masked) And a SHA-256 hash of the entry is computed and chained to the previous entry's hash (prev_hash), producing entry_hash And the log store enforces append-only semantics; update/delete operations are rejected And verification of the chain over any queried range returns "intact" for unmodified logs and "tampered" if a link is broken And log write fails atomically if hashing or persistence fails, with a retry policy up to 3 attempts
In-App Recovery Timeline Display & Filters
Given a signed audit log exists for the user's recovery requests When the user opens Settings > Security > Recovery Timeline Then the timeline lists entries in reverse chronological order with relative times and absolute timestamps localized to the user's locale And filters by event_type and request_id are available and return results within 500 ms for up to 2,000 entries And a "Tamper-evident: Verified" indicator is shown when the hash chain validates; otherwise a prominent warning is displayed And tapping an entry reveals details: approver display name, method (generic), IP, device info, outcome, and delivery tracking IDs And no edit controls are present; entries are read-only And the screen meets accessibility requirements (screen-reader labels, focus order, and contrast AA)
Signed JSON Audit Export for Compliance
Given a user or authorized support agent requests an audit export and selects a date range When the export is generated Then a JSON bundle is produced with fields: metadata (generated_at, range_start, range_end, requester_id, key_id, algorithm), entries[], and integrity {chain_root_hash, signature} And the bundle is signed with Ed25519 using the platform's export key; the detached signature and public key ID are included And verification using the published public key validates the signature and the hash chain And the export file is named "taxtidy_recovery_audit_{YYYYMMDD}_{request_id}.json" and available for download for 15 minutes, single-use And an "Audit Export Generated" event is appended to the log referencing the export request_id And PII is minimized and masked consistent with in-app display
Deliverability Monitoring, Retry, and Fallback
Given notifications are sent through the existing TaxTidy notification infrastructure When a notification send attempt is made Then a delivery status (queued/sent/deferred/bounced/complained) is captured per channel and stored with tracking IDs And email sends are retried up to 3 times over 30 minutes on transient failures; push is retried per platform guidelines; SMS per carrier rules And if email permanently bounces or is on the complaint list, a push/in-app banner is attempted as a fallback for security alerts And if the 15-minute rolling failure rate for any channel exceeds 5% across recovery notifications, an on-call alert is created And deliverability metrics are visible in the monitoring system with per-event-type breakdowns
Localization of Notifications and Timeline
Given the app supports multiple locales When recovery notifications are sent or the timeline is rendered Then all strings use localized templates with placeholders correctly substituted And date/time, numbers, and directionality are formatted per locale; RTL languages render correctly And if a translation is missing, content falls back to en-US without exposing placeholder keys And notification templates pass pseudolocalization and length expansion tests without truncation in the top supported locales

Risk Guard

Get adaptive protection on every sign-in. If something looks unusual (new country, odd time, unknown network), Risk Guard steps up the challenge—requesting a device-to-device approval or a recovery code—while keeping trusted scenarios effortless.

Requirements

Anomalous Sign‑in Signal Detection
"As a TaxTidy user, I want suspicious logins to be detected based on unusual patterns so that my account is protected without extra friction during normal usage."
Description

Collect and evaluate contextual sign‑in signals to identify unusual behavior, including geolocation variance (country/region), autonomous system/network reputation, device fingerprint changes, time‑of‑day deviations from the user’s historical pattern, impossible travel, TOR/proxy/VPN indicators, and OS/browser integrity checks. Integrate lightweight mobile and web SDKs to transmit device and network attributes with strict data minimization and consent. Normalize and enrich signals server‑side for consistency and feed them into the risk engine in real time. Provide configurable signal toggles and thresholds, plus safe defaults tuned for freelancers’ mobile‑first usage patterns. Ensure privacy controls, regional data residency adherence, and limited retention windows. Expose signal summaries and reason codes to downstream components.

Acceptance Criteria
Geolocation Variance and Impossible Travel Flagging
Given a user has a sign-in history with last_successful_sign_in containing geolocation and timestamp and an impossible_travel_speed_kmh threshold (default 900; configurable 400–1200) When a new sign-in occurs and country_or_region differs from the last_successful_sign_in OR computed travel speed between the last two sign-ins exceeds the configured threshold Then add reason_codes including "geo_variance" and/or "impossible_travel", include distance_km, time_delta_s, speed_kmh, prior_country_region, current_country_region, anomaly=true, and publish to the risk engine within 300 ms p95 and 800 ms p99 And When region changes within the same country and distance_km < 100 within 24h, Then do not flag unless an override is configured
Network Reputation and Anonymity Service Detection
Given TOR/VPN/proxy indicator lists and ASN reputation scores are refreshed at least every 24h with a cached fallback up to 7 days When a sign-in contains ip_address and asn Then set network_indicators { tor:boolean, vpn_proxy:boolean, asn_reputation_score:0–100 }, add reason_codes "network_anonymity" when tor=true or vpn_proxy=true and "low_asn_reputation" when asn_reputation_score <= threshold (default 25), and publish within 300 ms p95 And When feeds are unavailable, Then use last cached data and set detection_status="stale" without blocking processing
Device Fingerprint Drift Handling
Given a privacy-preserving device_fingerprint derived from a whitelisted set of attributes hashed client-side and a drift_threshold (default 0.35 Jaccard distance; configurable 0.2–0.6) When a new sign-in includes device_fingerprint that differs from the most recent trusted device for the user Then compute fingerprint_distance and set device_status to one of { "trusted_match" (<0.1), "minor_drift" (>=0.1 and < drift_threshold), "new_device" (>= drift_threshold) }, append reason_codes "device_change" for minor_drift or "new_device" for new_device, include changed_components[], and publish within 300 ms p95 And Given a user-approved trusted devices list, When a trusted_match occurs, Then suppress device_change reason codes
Time-of-Day Deviation Detection
Given a per-user baseline built from the last 60 days or at least 20 successful sign-ins (whichever is greater) and a deviation threshold of 2 standard deviations from mean hour-of-day, with default allowed window 08:00–22:00 local time When a sign-in occurs outside the baseline confidence interval Then add reason_codes "temporal_anomaly", include baseline_hours, local_hour, deviation_score (z-score), and publish within 300 ms p95 And When insufficient history (<10 sign-ins) exists, Then use global default window 07:00–23:00 and only flag if outside this window
Configurable Signal Toggles and Thresholds with Safe Defaults
Given an admin configuration exposing per-signal enable/disable and threshold controls with safe defaults tuned for mobile-first usage When an admin disables a signal Then cease evaluating and emitting that signal and its reason_codes within 1 minute, and record an audit entry with actor, old_value, new_value, timestamp And When a threshold is updated Then validate against allowed ranges, apply to new evaluations within 1 minute, version the change (config_version), and include config_version in downstream events
Privacy, Consent, Data Minimization, Residency, and Retention
Given SDKs enforce explicit user consent and use a whitelist of minimal attributes (no raw PII) When consent is not granted or is revoked Then SDK transmits no signals; server rejects unauthenticated signal posts and emits consent_missing audit events And When consent is granted Then SDK transmits only whitelisted fields with per-field toggles honored, includes consent_version, and encrypts in transit And Given regional data residency per account When signals are stored Then data at rest remains in the configured region with no cross-region replication; access is geo-fenced and audited And Given retention default 30 days (configurable 7–90) When records reach retention end Then hard-delete within 24 hours and record deletion audits
Real-time Normalization, Enrichment, and Downstream Exposure
Given incoming device and network signals from SDKs and server When processing occurs Then normalize and enrich (IP->geo country/region/city, ASN, user-agent parsing, distance_km, speed_kmh) and emit a unified SignalEvent v1 with event_id, pseudonymous user_id, timestamps, risk_score 0–100, reason_codes[], and signal_summaries{} And Then deliver to the risk engine and downstream consumers (decision API and event bus) within 300 ms p95 and 800 ms p99 with at-least-once delivery, idempotency via event_id, and versioned schema evolution And When a consumer requests explanations via API Then return human-readable summaries and machine-readable reason_codes for contributing signals within 200 ms p95
Adaptive Risk Scoring Engine
"As a security‑conscious user, I want my sign‑ins evaluated in real time so that extra challenges only appear when they’re truly needed."
Description

Compute a per‑attempt risk score using weighted signals and per‑user behavioral baselines to differentiate trusted from risky scenarios. Support configurable policies and dynamic thresholds with environment‑specific defaults (web vs. mobile). Enforce low‑latency evaluation (p99 ≤ 200 ms) and include risk reason codes for transparency and debugging. Continuously adapt baselines using recent sign‑ins while guarding against poisoning (require confirmed successes). Provide simulation and A/B evaluation modes to tune weights before enforcement. Emit metrics for score distributions, false positive/negative rates, and business impact.

Acceptance Criteria
Compute Weighted Risk Score on Each Sign-In
- Given a sign-in attempt with signals {geo_distance_km, device_fingerprint_match, ip_reputation, login_hour_anomaly, network_asn_change, failed_attempt_streak}, When the engine evaluates the attempt, Then it produces a deterministic risk_score in [0,100] with tolerance ±0.1 for identical inputs. - Given any missing signal, When evaluated, Then configured default/neutral values are applied and a risk_score in [0,100] is still returned. - Given a calibration fixture, When evaluated, Then the returned risk_score matches the expected value within ±0.5. - Given identical inputs and policy version across environments, When evaluated, Then the computed risk_score is identical on web and mobile.
Adaptive Per-User Baselines with Poisoning Protection
- Given a user with ≥5 confirmed successful sign-ins in the past 30 days, When a new confirmed success occurs, Then baseline features (country set, device fingerprint set, login-hour distribution, ASN set) update within 5 minutes. - Given failed or unconfirmed/challenged attempts, When recorded, Then they do not modify the user's baseline. - Given ≥10 unconfirmed anomalous attempts within 24 hours, When evaluated, Then the baseline remains unchanged and a 'baseline_poisoning_blocked' audit event is emitted. - Given a user with insufficient confirmed history (<3), When evaluated, Then the engine falls back to segment/global baselines and includes reason_code 'baseline_fallback'.
Environment-Specific Policies and Dynamic Thresholds
- Given configured thresholds web.allow<40, web.challenge≥40<70, web.deny≥70 and mobile.allow<50, mobile.challenge≥50<80, mobile.deny≥80, When evaluating a web attempt with score 65, Then outcome is 'challenge'; When evaluating a mobile attempt with score 65, Then outcome is 'allow'. - Given a policy change via the config API, When updated, Then new thresholds are applied within 60 seconds and evaluations reference the new policy_version. - Given a config outage, When evaluating, Then environment-specific default thresholds are used and 'policy_fallback_used' metric is incremented.
Low-Latency Risk Evaluation at Scale
- Given load of 2,000 QPS mixed web/mobile and 95th-percentile payload size, When running steady-state for 15 minutes (warm), Then end-to-end evaluation latency p99 ≤ 200 ms and p50 ≤ 60 ms measured at the decision API boundary. - Given cold start of a new instance, When receiving the first 500 requests, Then p99 ≤ 300 ms and stabilizes to ≤ 200 ms within 2 minutes. - Given dependency cache miss rate ≤1%, When evaluating, Then no synchronous external call causes p99 to exceed 200 ms.
Risk Reason Codes and Debug Trace
- Given any evaluation, When returning a decision, Then the response and audit log include an ordered list of up to 5 reason_codes with per-signal contributions (weight and normalized impact %) summing within ±1% of the risk_score. - Given the same inputs and policy_version, When evaluated twice, Then reason_codes order and contribution values are identical. - Given raw features containing PII, When generating reason details, Then outputs exclude PII and include only hashed/categorical values. - Given debug_mode enabled for an authorized admin request, When evaluated, Then correlation_id and feature vector snapshot are recorded to debug logs with retention and access tags.
Simulation and A/B Evaluation Modes
- Given simulation mode enabled for 100% of traffic, When evaluating, Then the engine computes risk_score and proposed outcome but does not enforce; user flow remains unchanged; simulation_decision and policy_version are logged. - Given A/B mode with 20% users in variant B assigned by stable user hash, When evaluating, Then both variant scores are computed; control outcome is enforced; score_diff and decision_diff are logged for analysis. - Given simulation or A/B mode enabled, When evaluating, Then added latency overhead is ≤ 10 ms p99.
Metrics Emission for Score Distributions and Impact
- Given production traffic, When running, Then the engine emits per-environment metrics every 60 seconds: risk_score histogram (0–100 in 5-point buckets), decision rates (allow/challenge/deny), and top 10 reason_codes. - Given labeled outcomes from post-auth confirmations and fraud adjudications, When processed daily, Then false positive rate and false negative rate are computed and published with 95% confidence intervals. - Given step-up challenges, When monitored, Then metrics include challenge pass rate, abandonment rate, and estimated conversion delta tagged by policy_version. - Given a metrics pipeline outage, When occurring, Then local buffering stores ≥15 minutes of metrics and drops oldest first with 'metrics_dropped' counter.
Step‑up Challenge Orchestration
"As a user, I want the system to request the right verification step when something seems off so that I can quickly prove it’s me without hassle."
Description

Select and execute the appropriate challenge based on risk score, user preferences, and policy, preferring device‑to‑device approval and falling back to recovery code when necessary. Implement a resilient state machine with clear timeouts, retries, idempotency keys, and race‑condition handling across devices. Provide UX hooks for web and mobile to display challenge status and contextual details. Support seamless allow for low‑risk, trusted contexts and automatic escalation if the first challenge type fails. Log outcomes and propagate decision artifacts to session creation. Ensure accessibility, localization, and consistent behavior across platforms.

Acceptance Criteria
Low-Risk Trusted Context: Seamless Allow
Given a sign-in request has risk_score < 30 and the device, network, and login time match the user’s trusted context When Risk Guard evaluates the sign-in Then the system returns allow without presenting any step-up challenge And the decision reason includes "trusted_context_low_risk" And session creation begins within 500 ms of policy evaluation completion And the outcome is logged with risk_score, trust_signals, and allow=true
Policy- and Preference-Driven Challenge Selection
Given risk_score is between 30 and 69 or unusual_signal=true and user_preference.prefer_device_approval=true and policy.allows_d2d=true When step-up is required Then orchestrator selects challenge_type=device_to_device and fallback=recovery_code And emits challenge_id and idempotency_key unique per auth_attempt And records selected_challenge and rationale in the audit log Given policy.allows_d2d=false or user has no registered trusted devices When step-up is required Then orchestrator selects challenge_type=recovery_code as primary And exposes selection metadata to clients via the challenge descriptor
Device-to-Device Approval Success Path
Given a pending device_to_device challenge is dispatched to at least one registered trusted device When the user approves on a trusted device within 120 seconds and the approval payload includes a valid nonce bound to the idempotency_key Then orchestrator marks status=approved and proceeds to session creation And all other pending device approvals are cancelled with status=closed And web/mobile clients receive a status update within 2 seconds of approval And the audit log captures device_id (hashed), approval_timestamp, and approver_device_trust_level
Automatic Escalation to Recovery Code on D2D Failure
Given a device_to_device challenge is active When no approved response is received within 120 seconds or all targeted devices respond denied or undeliverable Then orchestrator escalates to challenge_type=recovery_code without requiring re-entry of username/password And clients render recovery code entry with masked contact hints only And a single valid 8-character recovery code is accepted within 5 minutes with max_attempts=5 and min_attempt_interval=5 seconds And upon correct code, session is created and the original D2D challenge is closed And wrong-code attempts are logged with redacted values and a 10-minute lockout is enforced after max attempts
Resilient Orchestration: Timeouts, Retries, Idempotency, Race Handling
Given any challenge initiation request includes idempotency_key scoped to user+client+auth_attempt for 10 minutes When duplicate initiation requests arrive concurrently from multiple tabs/devices Then only one challenge is created and subsequent requests receive the existing challenge descriptor And state machine transitions are restricted to: pending -> approved|denied|expired|escalated -> closed And network/transient failures trigger exponential backoff retries up to 3 attempts without creating duplicate challenges And timers are durable so service restarts do not lose countdowns; expired challenges transition deterministically to expired And if conflicting terminal events arrive, the earliest valid terminal event wins and late events are ignored and logged as late
UX Hooks and Status Streaming Across Web and Mobile (Accessibility & Localization)
Given a step-up challenge exists When web or mobile clients subscribe via SDK Then clients receive real-time status updates (pending, approved, denied, expired, escalated) via SSE/WebSocket/push within 2 seconds of server state change And UX hooks expose: challenge_type, reason_localized, target_device_count, time_remaining, and retry_allowed And UI strings localize to at least en and es with fallback to en; locale sourced from profile or Accept-Language And interactive flows meet WCAG 2.1 AA (labels, focus order, aria roles, contrast >= 4.5:1) on web and native accessibility APIs on mobile And behavior and error codes are consistent across platforms for the same states
Audit Logging and Decision Artifact Propagation to Session
Given a step-up flow completes with status approved or denied When the session is created (approved) or blocked (denied) Then decision artifacts are attached to the session/claims: risk_score, reason_codes, challenge_type, challenge_result, approver_device_id_hashed, timestamps, policy_version And an immutable audit record is persisted with correlation_id and structured JSON fields, redacting PII And analytics counters increment for challenge_selected, approved, denied, expired, escalated by challenge_type And logs are exportable to SIEM with outcome, latency_ms, and error_code where applicable
Trusted Contexts & Allowlist Management
"As a frequent traveler freelancer, I want my usual phone and home network to be recognized so that I’m not prompted unnecessarily during normal use."
Description

Recognize and manage trusted devices, networks, and typical locations to minimize friction in safe scenarios. Auto‑register a device or network as trusted after a successful, verified sign‑in, with configurable decay/expiration and revocation controls. Provide user‑facing settings to view and revoke trusted contexts and admin policies to disable or scope trust on sensitive actions. Persist trust tokens securely with binding to device keys and integrity checks to prevent cloning. Respect privacy by limiting precision of stored location data and supporting data deletion requests. Surface trust status to the orchestration layer to bypass step‑ups when appropriate.

Acceptance Criteria
Auto-Register Trusted Device After Verified Sign-In
Given a user signs in on an untrusted device and completes step-up verification, When the session is established, Then a device-bound trust token is issued with unique ID, issued_at, last_seen, and expires_at. Given the same user signs in again from the same device within the validity window, When risk evaluation runs, Then the orchestration layer receives trusted_device=true and no step-up is required unless a stricter admin policy applies. Given the user revokes this device from settings, When the device attempts the next sign-in, Then the token is invalidated server-side and a step-up is required.
Auto-Register Trusted Network After Verified Sign-In
Given a user signs in from a new network and completes step-up verification, When trust registration runs, Then a network trust entry is created using a privacy-preserving identifier (e.g., hashed SSID+BSSID or public IP CIDR) with issued_at and expires_at. Given the user signs in again from the same network within validity, When risk evaluation runs, Then the orchestration layer receives trusted_network=true and step-up is bypassed unless policy forbids. Given the network trust expires or is revoked, When the user next signs in from that network, Then a step-up is required and a new trust entry is created only after successful re-verification.
Trust Expiration and Decay Enforcement
Given default device trust TTL is 90 days and network trust TTL is 30 days, When a token reaches expires_at, Then it is rejected and cannot bypass step-ups. Given a decay threshold of 15 days before expiration, When the user signs in within the decay window, Then the system prompts for lightweight re-verification and extends expires_at upon success. Given an admin updates TTL/decay settings, When the next sign-in occurs, Then the new policy is applied to new and existing tokens per configured migration rule (shorten immediately; extend only after re-verification).
User View and Revoke Trusted Contexts
Given a signed-in user opens Settings > Security > Trusted Contexts, When the list loads, Then it displays all trusted devices, networks, and locations with name/label, type, last_seen, and expires_at. Given the user selects Revoke on any entry, When they confirm, Then the entry is removed within 5 seconds and corresponding tokens are invalidated across all services. Given the user selects Revoke All, When they confirm, Then all trusted contexts are deleted and the next sign-in requires step-up regardless of device or network.
Admin Policy to Scope/Disable Trust on Sensitive Actions
Given an admin sets a policy that trust does not bypass step-ups for sensitive actions (e.g., bank linking, tax packet export, password change), When a user initiates such an action from a trusted context, Then a fresh step-up is required. Given an admin scopes trust to specific countries or IP ranges, When a sign-in occurs from outside the allowed scope, Then trust is ignored and a step-up is required even if device/network is trusted. Given an admin disables network trust globally, When a user signs in from a previously trusted network, Then the orchestration layer receives trusted_network=false and a step-up is required.
Trust Token Security and Anti-Cloning
Given trust tokens are bound to device hardware keys, When a token is exported and reused on a different device, Then validation fails due to key mismatch and the session requires step-up. Given a trust token is tampered with, When presented, Then the server rejects it with invalid_token and logs an integrity violation event. Given tokens are stored at rest, When a security audit runs, Then tokens are encrypted via platform keystore/AES-256 and contain no precise location or PII beyond coarse labels.
Privacy: Location Precision and Data Deletion
Given location trust uses coarse precision, When a trusted location is stored, Then only city-level or 3-digit ZIP and country are retained; precise lat/long are not stored. Given a user submits a data deletion request, When the request is processed, Then all trusted context records and tokens for that user are purged within 30 days and cannot be used to bypass step-ups thereafter. Given the user requests a data export, When the export is generated, Then trusted context fields reflect coarse location precision and exclude raw network identifiers (e.g., raw MAC, full IP).
Device‑to‑Device Approval Flow
"As a TaxTidy user with my phone nearby, I want to approve a new login from my phone so that I can sign in securely without typing codes."
Description

Deliver push‑based, out‑of‑band approvals to the user’s registered mobile device for high‑risk sign‑ins, using cryptographic challenge‑response bound to device keys. Display clear context (requesting device, city/country, approximate time, network) and allow approve/deny with biometric or PIN on the approving device. Support APNs/FCM delivery with secure payloads, rate limiting, replay protection, and expiration windows. Handle multi‑device accounts, notification fallbacks, and deep links back to the pending session. Ensure no sensitive PII is exposed in notifications and that the flow works reliably under low connectivity. Provide analytics on delivery, open, and completion rates.

Acceptance Criteria
High-Risk Sign-In Triggers Device-to-Device Approval
- Given a sign-in attempt is flagged high risk (e.g., new country, unknown network, atypical time), when credentials are validated, then the server generates a single-use challenge bound to the user’s registered device public key(s) and queues APNs/FCM pushes within 5 seconds. - Then the pending session remains in “Awaiting approval” and no session token is issued until a valid device signature over the challenge is verified by the server. - Then at least one registered device receives the push notification in ≥95% of attempts under normal network conditions.
Approval Prompt Shows Context and Biometric/PIN Authorization
- Given the approver opens the push, when the app displays the request, then it shows: requesting device, city and country, approximate local time (±5 minutes), and network type (Wi‑Fi/Cellular) without exposing PII. - When the user taps Approve or Deny, then the action is gated by biometric or app PIN; on Approve the app signs the challenge with the device private key and sends it within 3 seconds. - Then the originating session reflects the approval/denial within 5 seconds and records requestId, deviceId, and timestamp.
Multi-Device Account Handling and Single-Result Resolution
- Given multiple registered devices, when a high-risk sign-in occurs, then push requests are sent to all eligible (non-revoked, active) devices concurrently. - Then exactly one approval completes the sign-in; all other pending approvals for the same requestId auto-expire and show “Resolved on other device”. - When any device denies, then the sign-in is immediately rejected and all other pending approvals are canceled. - Then the originating session displays which device approved/denied by friendly device name only (no PII).
Delivery Resilience, Low Connectivity, and Fallbacks
- Given push delivery fails or is not opened within 30 seconds, when the approver launches or foregrounds the app, then it securely pulls pending approvals and presents the request. - Given bandwidth ≤256 kbps and up to 3% packet loss, when the user approves, then the client retries with exponential backoff up to 3 times and completes end-to-end within 90 seconds in ≥90% of such cases. - When the push is tapped, then a deep link opens the exact approval screen in-app; on completion, the originating session updates via SSE or polling within 5 seconds. - If no registered device is reachable within 2 minutes, then the user is offered a recovery code flow and the original approval request is invalidated.
Expiration Window, Replay Protection, and Rate Limiting
- Given an approval challenge is created, when not completed, then it expires after 120 seconds; post-expiration responses are rejected with a specific error and do not create a session. - Then each challenge includes a cryptographic nonce and server-signed payload; any replayed signature or device key mismatch is detected, rejected, and audit logged. - Then a maximum of 3 active approval challenges per account and 5 per IP per 10 minutes is enforced; excess attempts are throttled with appropriate user messaging.
Privacy-Safe Notifications and Encrypted Payloads
- Given a push is delivered, then the notification banner/lock-screen text contains no email, full name, IP address, or exact location; only a generic prompt (e.g., “Approve sign-in request”) and app name. - When the app processes the notification, then the payload is encrypted end-to-end for the device and decrypted only in-app; no sensitive fields are stored in plaintext on disk. - Then server and client logs redact PII and persist only requestId, coarse location (city, country), and deviceId per retention policy.
Delivery, Open, and Completion Analytics
- Given an approval flow starts, when push is sent/opened/approved/denied/expired, then analytics events are emitted with consistent requestId, timestamps, and deviceId across steps. - Then dashboards report delivery rate, open rate, completion rate, median approval latency, and failure reasons with data freshness ≤15 minutes and ingestion success ≥99%. - Then each request is traceable end-to-end via logs/traces linked by requestId for audits without exposing PII.
Recovery Code Verification & Backup Codes
"As a user who lost access to my primary device, I want a secure recovery code option so that I can still verify a risky sign‑in and access my account."
Description

Provide a secure recovery code path when device‑to‑device approval is unavailable by issuing single‑use codes and an optional set of backup codes at enrollment. Hash and salt stored codes server‑side, display codes only once, and support regeneration with invalidation and confirmation. Enforce input throttling, lockouts on repeated failures, and comprehensive auditing. Ensure an accessible, localized UI for code entry on web and mobile. Integrate with orchestration to serve as a fallback based on policy and user context. Offer user education and reminders to store codes safely.

Acceptance Criteria
Policy-Based Fallback to Recovery Codes on Risky Sign-In
Given a sign-in is flagged as requiring step-up and device-to-device approval is unavailable When the Risk Guard policy allows recovery-code fallback for the user's context (e.g., country, network, device) Then the orchestrator routes the session to the recovery code flow on web and mobile And the UI clearly communicates the recovery step without exposing sensitive risk details And the option is withheld when policy forbids recovery codes for the detected context And an event "recovery_fallback_initiated" is audited with requestId, userId, riskLevel, policyId, ip, country, and device metadata
Single-Use Recovery Code Generation and Verification
Given the recovery code fallback is initiated and single-use codes are enabled by policy When the user requests a single-use code Then the system generates a high-entropy code (>=128-bit equivalent), displays it once, and starts a TTL per policy And only a salted hash with a unique per-code salt and expiration is stored server-side; plaintext is never persisted or logged And when the user submits a valid single-use code within TTL, authentication succeeds and the code is immediately invalidated And when the code is invalid, expired, or already used, authentication fails with a generic error that does not reveal which condition occurred And all outcomes are audited without logging plaintext or partial code values
Backup Codes Issuance at Enrollment (Display Once, Secure Storage)
Given a user enables recovery during enrollment When enrollment completes Then the system generates N backup codes (configurable, default 10), each with at least 64-bit entropy and an unambiguous character set And codes are displayed exactly once with options to copy, download (text/PDF), or print And the UI provides education on safe storage (e.g., password manager or printed copy kept securely) And the user must acknowledge "I stored my codes" before finishing enrollment And only salted hashes with unique per-code salts are stored server-side; plaintext codes are never retrievable later And no plaintext or partial codes appear in logs, analytics, or telemetry
Backup Codes Use, Depletion, and Regeneration
Given a user has backup codes provisioned When the user enters a valid unused backup code during recovery Then authentication succeeds, the code is marked as used, and it cannot be reused And the remaining backup code count is displayed non-intrusively after success And when remaining codes are at or below the configured threshold, the user is prompted to regenerate after sign-in And when the user opts to regenerate, the system requires re-auth or a trusted factor confirmation And regeneration invalidates all previous unused codes, generates a new set, displays them once with safe-storage education, and requires acknowledgment And notifications (email/in-app) confirm regeneration and prior invalidation without including plaintext codes And all actions and outcomes are audited
Input Throttling and Lockouts for Recovery/Backup Codes
Given the recovery/backup code entry form When the user submits invalid codes repeatedly Then the service enforces the configured attempt rate limit and incremental backoff without revealing code validity And after exceeding the lockout threshold within the configured window, the account is locked for recovery-code use for the configured duration, returning a clear but generic lockout message/state And locked-out attempts are blocked, return an appropriate status (e.g., 429/locked), and are audited And upon a successful verification, any attempt counters and backoff timers for that flow are reset
Accessible, Localized Code Entry UI (Web and Mobile)
Given the recovery code entry screen is rendered on web or mobile in any supported locale When used with assistive technologies and varied device settings Then controls expose correct labels/roles, focus order is logical, errors are announced, and contrast meets WCAG 2.1 AA And input fields support paste, auto-advance between groups (if grouped), numeric keypad hints on mobile, and disable autocorrect/autoformatting And all user-facing text (including errors, success, timeouts, and prompts) is localized with correct pluralization and RTL support; locale respects user preference or device settings with a manual override And dark mode and high-contrast modes render correctly without loss of information
Comprehensive Auditing and Redaction
Given any recovery-code-related event occurs (initiation, issuance, display, verification success/failure, regeneration, throttling, lockout) When the event is recorded Then the audit entry includes timestamp, requestId, userId, event type, outcome, ip, country, device metadata, risk level, and policyId And plaintext codes or fragments are never stored or logged; only salted hashes or opaque identifiers are recorded where needed And audit records are immutable/tamper-evident, retained per policy, and queryable by authorized roles
Security Audit Logging & User Alerts
"As a user, I want a clear record and alerts for unusual sign‑in activity so that I can spot suspicious behavior and secure my account quickly."
Description

Capture immutable logs of sign‑in attempts, signals observed, risk scores, challenges issued, user responses, and final outcomes for compliance and forensics. Provide a user‑facing security activity feed and optional alerts (email/push) on high‑risk events or new trusted context registrations. Support export to SIEM via webhook/stream and define retention policies aligned with privacy regulations. Include time zone‑aware timestamps, device identifiers, and reason codes without leaking sensitive PII. Offer admin filters for investigation and self‑service incident response actions (revoke sessions, remove trusted devices). Monitor for anomalous spikes and trigger internal alerts.

Acceptance Criteria
Immutable Audit Log Coverage and Retention
- Given any sign-in attempt is processed, When the event is finalized, Then an audit entry is appended capturing: event_id, request_id, event_type, outcome, event_time (ISO 8601 with timezone offset), user_ref (non-PII internal ID), device_id (stable hashed), network_identifier (masked IP / ASN), geo (country/region only), risk_signals[], risk_score (0–100), challenge_type, challenge_status, user_response, reason_codes[]. - Given the audit store, When attempting to update or delete an existing entry, Then the operation is prevented, a tamper-attempt event is logged, and cryptographic chain/hash verification remains valid. - Given a tenant retention policy of X months (3–84 configurable, default 24), When any log entry exceeds X months, Then it is purged or irreversibly anonymized within 72 hours, with a purge-audit record retained; backup copies are purged within 7 days. - Given a legal hold is enabled for a tenant, When entries pass the retention threshold, Then they are preserved until the hold is removed, and this exception is auditable. - Given PII minimization rules, When logs are written, Then full IPs are masked (IPv4 /24, IPv6 /64), no raw email/phone are stored, and device fingerprints are hashed with rotation every 90 days while preserving linkability within the rotation window.
User Security Activity Feed (Time Zone Aware)
- Given a signed-in user opens Security Activity, When the feed loads, Then it displays the last 90 days of their security events with timestamps converted to the user’s selected time zone and relative time labels. - Given feed entries, When rendered, Then sensitive data is masked (no full IP, no raw email/phone) and each entry shows: event type, coarse location (country/region), device label, challenge/outcome, and reason codes. - Given the audit log, When the same date range and user_ref are queried, Then the count and details shown in the feed match the underlying logs (modulo masking). - Given many events, When the user scrolls or paginates, Then the feed provides pagination/infinite scroll with a stable sort (event_time desc) and returns the first page within 2 seconds for up to 500 events. - Given user filters by risk level or event type, When applied, Then only matching items appear and the filter state persists for the session.
High-Risk Event Alerts (Email/Push)
- Given alert preferences are enabled, When a high-risk sign-in occurs (risk_score ≥ configured threshold) or a new trusted context is registered (device/network), Then an email and/or push alert is sent within 2 minutes including event time (user’s TZ), device label, coarse location, and a secure link to review activity. - Given alert content, When delivered, Then no sensitive PII is included (no full IP, no exact address), and links use short-lived, single-use tokens that expire in 15 minutes. - Given potential alert floods, When multiple high-risk events occur within 10 minutes, Then alerts are deduplicated and rate-limited to a maximum of 3 per hour per user per channel. - Given user preferences, When the user toggles alert channels (email/push) or thresholds, Then changes take effect for subsequent events and are recorded in the audit log with reason codes. - Given an alert is sent, When the user takes an action (review, revoke sessions, remove device) via the link, Then the action executes successfully or returns a clear error, and the outcome is logged.
SIEM Export via Webhook/Stream
- Given a tenant configures a SIEM destination, When export is enabled, Then all new audit events are delivered as JSON with a documented schema, excluding sensitive PII fields per policy. - Given HTTPS webhook delivery, When the SIEM endpoint responds non-2xx, Then retries occur with exponential backoff for up to 6 attempts over 30 minutes; after exhaustion, events are placed in a tenant-scoped dead-letter queue for later reprocessing. - Given delivery, When events are posted, Then each payload includes an HMAC-SHA256 signature header and an idempotency key so the receiver can verify integrity and deduplicate. - Given a streaming destination, When a managed topic/stream is provisioned, Then events are published at-least-once with ordering per user_ref and partition key, and consumer offsets can be monitored. - Given export failures, When an operator views the export status, Then they can see success/failure rates, last error, and re-drive dead-lettered events, with all actions audited.
Admin Investigation Filters
- Given an admin with Security Auditor role opens Audit Explorer, When they query, Then they can filter by date/time range (TZ-aware), user_ref, event_type, outcome, risk_score range, challenge_type/status, reason_codes, device_id, country/region, and masked IP/ASN. - Given filtered results, When exported, Then CSV and JSON exports include the same fields as the UI (with masking rules) and are available for download within 60 seconds for up to 100k rows. - Given access control, When a non-privileged user attempts to access Audit Explorer, Then access is denied and the attempt is logged. - Given investigators need traceability, When a request_id is entered, Then all correlated events across services are returned in descending time order. - Given UI actions, When a filter preset is saved or loaded, Then it persists per admin and its creation/modification is audited.
Self-Service Incident Response Actions
- Given an admin reviewing an event, When they select Revoke all sessions for this user, Then all active sessions for that user are invalidated within 60 seconds and the action is recorded with actor, time, scope, and reason code. - Given a trusted device is identified as compromised, When Remove trusted device is executed (admin or user self-service), Then the device trust token is revoked immediately, future sign-ins from that device require re-challenge, and the removal is logged. - Given a network is suspected, When Block network (by ASN or masked CIDR) is applied, Then subsequent sign-ins from that network are treated as high-risk and challenged per policy; the block rule and its TTL are auditable and reversible. - Given an event is incorrectly flagged, When Mark as false positive is applied with a reason, Then risk models are updated or excluded for that context, and the decision is logged without deleting the original event. - Given any incident action, When executed, Then the affected user is notified (per preferences) and given guidance to secure their account.
Anomalous Spike Monitoring and Internal Alerts
- Given baseline metrics (rolling 7-day by hour), When high-risk event rate exceeds 3x baseline and at least 100 events occur within 15 minutes, Then an internal alert is created with summary, affected tenants, top reason_codes, and links to Audit Explorer. - Given sign-in failures surge, When failed attempts exceed 3x baseline within 15 minutes, Then an internal alert is triggered and runbooks are referenced in the alert payload. - Given noise control, When duplicate conditions are detected, Then alerts are deduplicated with a 30-minute suppression window and all suppressions are logged. - Given maintenance windows, When a window is active, Then anomaly alerts for covered services are suppressed with a recorded justification. - Given an internal alert is created, When delivered, Then it reaches on-call channels (Pager/SMS/Email) within 5 minutes and is stored in the audit log without PII.

Scoped Sessions

Set per-device permissions and session lifetimes. Let a VA’s tablet upload receipts and categorize transactions without exposing balances or tax totals. Tokens are encrypted, short-lived, and auto-expire based on the scope you choose.

Requirements

Device Pairing & Owner Approval
"As an account owner, I want to register and approve a VA’s device with limited permissions and a defined session lifetime so that they can upload and categorize without seeing balances or tax totals."
Description

Enable secure per-device registration via QR code or magic link, requiring the account owner’s approval before activation. Upon approval, the owner assigns a device name, selects a permission scope (e.g., upload receipts, categorize transactions), and sets a session lifetime. The device is fingerprinted and associated with the account, preventing access to balances or tax totals unless explicitly allowed. This integrates with TaxTidy’s mobile-first workflows so VAs can capture receipts and categorize expenses without exposure to sensitive financial summaries, ensuring least-privilege access from the outset.

Acceptance Criteria
QR Pairing With Owner Approval and Scoped Token Issuance
Given the owner opens the Device Pairing screen and a QR code is displayed And a VA device scans the QR and submits a pairing request When the owner approves and sets device name, permission scope, and session lifetime Then the VA device receives a scoped token and is marked Active And the device appears under the owner's Devices list with the chosen name, scope, and lifetime And if the owner denies, no token is issued and the device shows "Pairing denied"
Magic Link Pairing Alternative and One-Time Use
Given the owner requests a magic link for device pairing When the owner opens the magic link within 15 minutes And approves the pending device by setting device name, permission scope, and session lifetime Then the device receives a scoped token and is marked Active And the magic link becomes invalid immediately after use or after 15 minutes, whichever occurs first And attempts to reuse or use an expired link return "Link expired" and no token is issued
Permission Scope Enforcement (Least-Privilege)
Given a device with scope "Upload Receipts" is active When the device uploads a receipt photo Then the upload succeeds and the receipt is stored and associated with the account And requests to balances, tax totals, exports, and reports return 403 Forbidden Given a device with scope "Categorize Transactions" is active When the device requests the uncategorized transactions list and submits category assignments Then it can view transaction line-items and persist category changes And requests to balances, tax totals, and tax packet downloads return 403 Forbidden
Session Lifetime Configuration and Auto-Expiry
Given the owner sets session lifetime to 8 hours at approval When 8 hours elapse from token issuance Then subsequent device API calls return 401 Unauthorized and the UI prompts to re-pair Given the owner sets session lifetime to 24 hours or 7 days When the configured lifetime elapses Then the same expiry behavior occurs And the expiry event is recorded with device name and timestamp in the audit log
Device Fingerprinting and Binding
Given a device is approved and its device fingerprint hash is stored When the token is presented from a device with a different fingerprint hash Then the request is rejected with 401 "Device mismatch" And an owner in-app notification is generated And the event is recorded in the audit log with fingerprint mismatch details Given the token is presented from the approved device fingerprint Then requests within scope succeed And the Devices list shows device name, scope, last seen timestamp, and fingerprint summary
Revocation and Immediate Cutoff
Given a device is active When the owner revokes the device from the Devices list Then the token is invalidated within 60 seconds And subsequent requests from that device return 401 Unauthorized And the device status changes to Revoked in the Devices list And the revocation event is logged with actor, timestamp, and device fingerprint
Time-Limited, Single-Use QR and Anti-Replay
Given a pairing QR code is generated When 5 minutes elapse or the QR has been used once Then further attempts to use that QR return "QR expired or already used" and are rejected And replayed requests using captured QR payloads are rejected with 400 and logged as replay attempts
Scope Templates & Custom Permissions Matrix
"As an account owner, I want ready-made scope templates and fine-grained permission toggles so that I can quickly grant the exact access a helper needs without risking overexposure of my finances."
Description

Provide prebuilt scope templates (e.g., VA – Capture & Categorize, Accountant – Review Only) and a granular permissions matrix to tailor access per device. Owners can toggle capabilities such as upload receipts, create/edit categories, approve matches, edit transaction details, export data, and view balances/tax totals. Selected permissions are encoded into the session scope and enforced client- and server-side across TaxTidy’s apps and APIs, ensuring consistent least-privilege behavior and faster setup using templates.

Acceptance Criteria
Apply VA – Capture & Categorize Template to a Device Session
Given I am the workspace Owner And a device named "VA Tablet" is registered without an active scoped session When I select the "VA – Capture & Categorize" template and issue a session token for that device Then the saved scope includes only these allowed permissions: upload_receipts, categorize_transactions, create_edit_categories And the saved scope explicitly denies: approve_matches, edit_transaction_details, export_data, view_balances, view_tax_totals And the device receives a token bound to that scope
Apply Accountant – Review Only Template to a Device Session
Given I am the workspace Owner And a device named "Accountant Laptop" is registered without an active scoped session When I select the "Accountant – Review Only" template and issue a session token for that device Then the saved scope allows: view_balances, view_tax_totals, export_data And the saved scope denies: upload_receipts, create_edit_categories, categorize_transactions, edit_transaction_details, approve_matches And the device receives a token bound to that scope
Customize Permissions Matrix After Template Selection
Given I have applied a template to a pending device session When I toggle ON: approve_matches And toggle OFF: create_edit_categories And I save the scope Then the final saved scope contains approve_matches and omits create_edit_categories And the UI labels the selection as "Template (modified)" And the stored scope JSON exactly matches the toggled permissions And no other devices’ scopes are altered
Scope Encoded, Encrypted, and Immutable in Session Token
Given a session token is issued with a selected scope Then the token payload includes an explicit permissions array and an exp (expiry) timestamp And the token is cryptographically signed and encrypted per platform policy When the token is tampered (e.g., permissions field modified) Then the server rejects it with 401 Unauthorized and error code TOKEN_TAMPERED And permissions cannot be elevated without issuing a new token; updating settings does not alter an already issued token
Client-Side Enforcement of Scoped Capabilities
Given a device is authenticated with a VA-scoped token When the mobile app and web app load Then controls for export_data, view_balances, view_tax_totals, approve_matches, edit_transaction_details are hidden or disabled And attempting to open restricted screens via deep link shows an access denied message And no sensitive values (balances, tax totals) are fetched or rendered in memory for the restricted scope
Server-Side Authorization Across APIs
Given a VA-scoped token When calling POST /api/receipts with a valid image Then the response is 201 Created When calling GET /api/balances or GET /api/tax-totals Then the response is 403 Forbidden with error code SCOPE_DENIED When calling POST /api/transactions/{id}/approve-match Then the response is 403 Forbidden with error code SCOPE_DENIED And with an Accountant-scoped token, GET /api/balances returns 200 OK and POST /api/receipts returns 403 Forbidden
Per-Device Session Lifetime and Auto-Expiry by Scope
Given I set a 24-hour lifetime for a device session and issue the token Then the token’s exp is within 24 hours of issuance (±5 minutes skew) And after the exp time, all API requests using the token return 401 Unauthorized with error code TOKEN_EXPIRED And the client app signs out and removes the token within 60 seconds of encountering TOKEN_EXPIRED And changing templates or permissions later does not change the exp of the already issued token
Short‑lived Token Lifecycle with Device Binding & Secure Storage
"As a security-conscious user, I want tokens that are short‑lived, device‑bound, and securely stored so that even if a device is compromised, my sensitive tax data remains protected."
Description

Issue scoped, short‑lived access tokens (e.g., 5–30 minutes) with rotating refresh tokens that are cryptographically bound to the device fingerprint. Store tokens in the device’s secure enclave/Keychain/Keystore and encrypt all token data at rest and in transit. On rotation or scope change, invalidate prior tokens and block reuse. Tokens carry scope claims used by TaxTidy services to authorize actions like receipt upload and categorization while denying balance/tax total reads. Provide graceful handling for expired tokens and require seamless re-auth, preserving queued, non-sensitive actions until a valid token is obtained.

Acceptance Criteria
Short-lived access token issuance with scope claims
Given a user selects a device scope permitting receipt:upload and transaction:categorize only When the user authenticates on a new device Then the system issues an access token with an expiry between 5 and 30 minutes and containing only the selected scope claims And the token contains no balance:read or taxes:read claims And the token's exp claim matches the configured TTL And the token is rejected after expiry with at most 10 seconds clock-skew tolerance And a refresh token is issued alongside the access token
Device-bound rotating refresh tokens
Given an access/refresh token pair bound to device fingerprint F_A on Device A When the refresh endpoint is called from Device A before access token expiry Then a new access token and a new refresh token are returned, both bound to F_A And the prior refresh token is invalidated and cannot be reused And calling the same prior refresh token from Device B (fingerprint F_B ≠ F_A) returns 401 invalid_device and no tokens are issued And an audit event records device_id, scope, rotation_time, and outcome
Secure token storage and transport encryption
Given the mobile app stores tokens on iOS and Android When inspecting platform storage usage Then access and refresh tokens are stored only in iOS Keychain/Secure Enclave and Android Keystore/StrongBox (or OS-provided secure storage) And tokens are never persisted in plaintext in preferences, files, logs, WebView, or crash reports And all token transport uses TLS 1.2 or higher with certificate validation And security tooling finds zero occurrences of tokens in logs or non-secure storage during testing
Token invalidation on rotation and scope change
Given a valid session on a device When a refresh occurs Then the previous access token and previous refresh token become unusable within 1 second And any API call using a superseded token returns 401 invalid_token Given the session scope is reduced (e.g., removing balance:read) When the change is saved Then all existing tokens for that device are invalidated and reissued with the reduced scope And attempts to reuse invalidated tokens are blocked and rate-limited And an audit event records reason=rotation or reason=scope_change
Scope-based authorization enforcement
Given a VA device session with scopes [receipt:upload, transaction:categorize] When calling POST /receipts and POST /transactions/{id}/categorize Then the APIs return 2xx success When calling GET /balances and GET /tax/summary Then the APIs return 403 insufficient_scope And token introspection shows no balance:read or taxes:read scopes And the UI hides or disables balance and tax total views for that session
Graceful expiry with seamless re-auth and queued actions
Given the access token expires during queued receipt uploads or categorization When the app attempts the next queued action Then it first performs a silent refresh using the device-bound refresh token And if silent refresh succeeds, queued actions retry and complete without user-visible errors And if refresh fails due to expiry or revocation, the user is prompted to re-authenticate And queued non-sensitive actions remain queued until a valid token is obtained And actions requiring disallowed scopes are never executed And error messages do not expose token contents and provide a retry option
Per-device session lifetime configuration and enforcement
Given an admin configures a VA tablet device scope with access token TTL=10 minutes and refresh token TTL per policy When tokens are issued for that device Then access tokens expire within 10 minutes ±10 seconds and refresh tokens follow the configured policy And changing the configured TTL to 5 minutes applies only to newly issued tokens And existing tokens retain their original TTL until invalidated And attempting to set an access token TTL outside 5–30 minutes is rejected with a validation error And attempts to use tokens after expiry return 401 token_expired
Auto‑Expiry, Idle Timeout, and Scope‑Based Lifetimes
"As an account owner, I want sessions to auto‑expire and time out based on the assigned scope so that limited-access devices don’t retain ongoing privileges beyond what I intend."
Description

Allow owners to set absolute session lifetimes and idle timeouts per scope (e.g., VA sessions expire nightly with 15‑minute idle logout). Enforce expiry server‑side with background cleanup and client‑side with proactive logout, ensuring devices lose access promptly when time limits are reached. Sensitive scopes cannot bypass expiry, and attempts to access balances or tax totals from restricted scopes are consistently denied. Integrates with notification hooks to warn before expiration, reducing disruption to receipt capture flows.

Acceptance Criteria
VA Session Idle Timeout—Proactive Client Logout at 15 Minutes
Given a VA-scoped session with idleTimeoutMinutes=15 on a registered tablet When there is no user interaction (tap, scroll, keystroke, receipt capture start/finish, or foreground network call) for 14 minutes and 30 seconds Then the client displays a non-blocking 30-second countdown warning indicating imminent logout due to inactivity And when no interaction occurs during the countdown and the idle threshold reaches 15 minutes Then the client logs out the user within 5 seconds of the threshold and clears session tokens from secure storage And the next authenticated API call returns 401 with error_code="SESSION_IDLE_EXPIRED" And the refresh token is invalidated and cannot be used to obtain a new access token And any in-progress receipt capture is saved locally and recoverable upon next sign-in without data loss
Absolute Lifetime Expiry—Nightly Cutoff for VA Scope
Given the owner configures VA scope with absoluteExpiryPolicy="nightly" cutoff at 23:59:00 in the workspace time zone And a VA-scoped session is created before the cutoff When server time reaches 23:59:00 Then the session becomes invalid server-side immediately And the client, if foregrounded, transitions to logged out state within 5 seconds; if backgrounded, enforces logout within 60 seconds of next app foreground And all subsequent API requests using the expired token receive 401 with error_code="SESSION_ABSOLUTE_EXPIRED" And the session cannot be extended by activity, refresh, or client clock changes
Server-Side Enforcement Across All Endpoints and Channels
Given any access token whose scope has reached idle or absolute expiry When requests are made to REST APIs, WebSocket channels, or background sync endpoints using that token Then the server rejects the request with 401 and an appropriate error_code in {"SESSION_IDLE_EXPIRED","SESSION_ABSOLUTE_EXPIRED"} And no partial data is returned in the response body And active WebSocket connections are closed with close_code=4001 and reason="Session expired" And attempts to use the refresh endpoint with the expired session return 401 with error_code="REFRESH_EXPIRED" And an audit log event is recorded with user_id, device_id, scope, cause in {"idle","absolute"}
Scope-Based Access Denial—Balances and Tax Totals Hidden from VA
Given a VA-scoped session limited to receipt upload and transaction categorization When the user attempts to access balances or tax totals via UI navigation or API (e.g., GET /balances, GET /tax/totals) Then the UI hides navigation to these views and blocks deep links with an access denied message that contains no monetary values And API calls return 403 with error_code="SCOPE_FORBIDDEN" and scope="va" with no sensitive fields present in the payload And denial behavior is consistent regardless of token freshness or device state And expiry policies for this scope cannot be disabled or extended via client actions
Pre-Expiry Notification Hooks and In-App Warnings
Given notification hooks are enabled for the workspace And a VA-scoped session is approaching idle expiry (T-30s) or absolute expiry (T-5m) When the threshold is reached Then TaxTidy posts a single callback to the configured endpoint with payload {user_id, device_id, scope, type in ["idle","absolute"], expires_at ISO8601 in workspace time zone} And the client displays a non-blocking banner with remaining time and a "Stay Signed In" action that resets the idle timer upon interaction And no duplicate notifications are sent for the same expiry event And using the "Stay Signed In" action does not extend the absolute lifetime
Background Cleanup and Offline Devices
Given a background cleanup job runs server-side every 60 seconds When sessions cross their expiry thresholds Then expired tokens are marked invalid within the next cleanup cycle And any active WebSocket for those tokens is closed within 10 seconds And if a device was offline during expiry, its first authenticated request after reconnect is rejected with 401 and the appropriate expiry error_code And the client enforces logout within 5 seconds after receiving the rejection
Timezone and Clock Skew Handling
Given the workspace time zone is set and server time is authoritative And a device clock is skewed by up to ±2 minutes from server time When absolute cutoff or idle thresholds are evaluated Then expiry is determined using server time in the workspace time zone And user-facing timestamps in warnings and messages reflect the workspace time zone And device clock skew does not allow access past server-determined expiry
Session Management Dashboard
"As an account owner, I want a clear dashboard to view and manage device sessions so that I can revoke or adjust access instantly if something looks off."
Description

Provide an Owner-only dashboard listing all active and recent sessions by device, user, scope, last seen, IP/location, and expiration time. Offer actions to revoke, pause, or extend sessions within policy, rename devices, and clone scope settings. Include quick-start creation using templates and deep links/QR codes for new device pairing. Integrate with TaxTidy’s security settings so owners maintain continuous visibility and control over scoped access without contacting support.

Acceptance Criteria
Owner-Only Access Control for Session Management Dashboard
Given an account owner is authenticated When they navigate to Security > Sessions Then the Session Management Dashboard is accessible and loads within 2 seconds Given any non-owner role (e.g., VA, collaborator) is authenticated When they attempt to access Security > Sessions via UI or direct URL Then access is denied with HTTP 403 and no session data is returned Given an unauthenticated user When they attempt to access the dashboard URL Then they are redirected to sign-in without exposing dashboard content Given the owner has at least 1 active session When the dashboard loads Then the owner identity is shown and no cross-tenant data leaks occur
Accurate Listing of Active and Recent Sessions with Metadata
Given the owner has multiple sessions across devices When the dashboard loads Then each session row displays device name, user/role, scope name(s), status (Active/Paused/Expired/Revoked), last seen timestamp, originating IP, geolocation (city/region/country when resolvable), and expiration time Given a session performs an authenticated API call When 10 seconds elapse Then the session's Last Seen updates within the dashboard Given a session reaches its expiration time When 10 seconds elapse Then its status changes to Expired, it disappears from Active, and appears under Recent with an Expired reason Given there are more than 25 sessions total When the dashboard loads Then pagination is available with default page size 25 and server-side sorting by Last Seen, Expiration, Device Name, and Scope Given the owner enters a filter (device name contains, scope equals, status equals) When they apply the filter Then only matching sessions are displayed with counts updated
Revoking, Pausing, and Extending Sessions with Policy Enforcement
Given an active session is selected When the owner clicks Revoke and confirms Then the token becomes invalid within 5 seconds; subsequent API calls from that session return HTTP 401; the session moves to Recent with reason Revoked Given an active session is selected When the owner clicks Pause Then the session status changes to Paused within 5 seconds; API calls from that session return HTTP 401; expiration time continues to count down while paused Given a paused session is selected When the owner clicks Resume Then the session returns to Active within 5 seconds Given an active session is selected and policy allows extension up to a maximum lifetime When the owner clicks Extend and selects a new expiration within policy Then the expiration updates and an audit entry is created; attempts to exceed policy are blocked with a clear error message Given any action (Revoke, Pause, Resume, Extend) is executed When it completes Then an audit log entry records actor, timestamp, IP, session ID, and old/new values
Renaming Devices and Propagating Labels
Given a session is selected When the owner edits the device name Then input is validated (2–50 characters; letters, numbers, spaces, hyphen, underscore; no leading/trailing spaces) Given a valid new device name is submitted When saved Then the new name appears on the session row, on the session detail view, and in any related security views within 5 seconds Given the owner attempts to set a device name already used on another device within the same account When saved Then the system prevents duplicates and displays a helpful error Given a device rename is saved When viewed in the audit trail Then an entry shows old name, new name, actor, timestamp, and IP
Cloning Scope Settings from Existing Session
Given an existing session with a defined scope is selected When the owner chooses Clone Scope Then a new scope configuration is pre-populated with identical permissions and default lifetime Given the cloned configuration exceeds current policy limits When the owner attempts to proceed Then the system highlights violations and blocks saving until resolved Given the owner reviews the cloned configuration When they confirm Then a new saved template is created with a unique name and associated policy constraints; an audit entry is recorded Given the owner uses Clone Scope to initiate a new session pairing When they proceed Then the pairing flow starts with the cloned scope pre-selected
Quick-Start Device Pairing via Templates and QR/Deep Links
Given the owner opens Quick-Start creation When they select a template and lifetime Then the system generates a one-time deep link and QR code valid for 15 minutes and single-use Given the QR/deep link is scanned on a mobile device When the link is opened Then the device is shown a scope summary and pairing request screen within 3 seconds Given a pairing request is initiated When it appears in the owner dashboard as Pending Then the owner can Approve or Deny; on Approve, the session is created with the selected scope and expiration; on Deny, the link is invalidated Given a QR/deep link expires or is consumed When a user attempts to reuse it Then the system rejects with an invalid/expired link message and no session is created Given a session is created via Quick-Start When viewed in the dashboard Then it appears under Active with correct device name (or placeholder if unknown), scope, last seen, IP/location, and expiration populated
Security Integration, Audit Trail, and Notifications
Given any dashboard action occurs (create, revoke, pause, resume, extend, rename, clone, approve/deny pairing) When it completes Then an immutable audit entry is stored with actor, target session/template ID, action, timestamp (UTC), originating IP, and old/new values Given the owner opens Security Settings > Audit When they filter by action type or date range Then matching entries are returned within 2 seconds and export to CSV is available Given a new device is paired or a session is extended beyond the default template lifetime When the action completes Then the owner receives a real-time notification (in-app) and an email within 1 minute Given audit retention is configured to 1 year When entries exceed 1 year Then they are no longer visible in the dashboard export but a retention policy note is displayed
Audit Logging & Exportable Access Trails
"As an owner, I want detailed audit logs of what each scoped device did so that I can trace changes and maintain compliance without exposing extra personal data."
Description

Record a tamper-evident audit trail for scoped sessions, capturing authentication events, device identifiers, IPs, and authorized actions (e.g., receipt uploaded, transaction categorized, match approved) with timestamps and resource references. Surface per-session timelines in the dashboard and enable CSV/JSON export for accountant review or compliance. Apply data minimization and redaction for PII while maintaining sufficient detail to attribute actions to specific devices and scopes within TaxTidy.

Acceptance Criteria
Scoped Session Authentication Events Are Logged
Given an authenticated account owner creates a scoped session for a VA’s device When the session token is issued Then an audit entry is appended with fields: session_id, device_id, ip_truncated, scope, token_ttl_seconds, issued_at_utc, entry_hash, prev_entry_hash And only a non-reversible token_fingerprint is stored (no raw token) And ip_truncated masks IPv4 to /24 or IPv6 to /64 And issued_at_utc is recorded in ISO 8601 UTC with millisecond precision
Authorized Actions Capture Resource References
Given a scoped session performs an authorized action (receipt uploaded, transaction categorized, match approved) When the action is committed Then an audit entry records: session_id, action_type, resource_type, resource_id, scope, occurred_at_utc, entry_hash, prev_entry_hash And no financial balances, tax totals, or receipt image content are stored in the audit payload And the action is attributable to the specific device_id and scope used
Per-Session Timeline Visible in Dashboard
Given an account owner opens a specific session’s details in the dashboard When the session timeline loads Then events are displayed in chronological order with occurred_at in the user’s local timezone And the view supports filtering by date range and action_type and searching by resource_id And each event links to the referenced resource when the user has permission; otherwise the link is disabled And access is restricted to account owners/admins; unauthorized users receive a 403
CSV and JSON Export of Access Trails
Given the user selects a date range and one or more sessions and chooses CSV or JSON When Export is requested Then the file downloads within 5 seconds for exports up to 50,000 events And CSV includes headers: session_id,device_id,ip_truncated,scope,action_type,resource_type,resource_id,occurred_at_utc,entry_hash,prev_entry_hash,redaction_flags And JSON is an array of objects with the same fields and stable field ordering And redaction_flags indicate which fields were minimized or redacted
Tamper-Evident Chain Verification
Given the system maintains a hash-chained, append-only audit log When integrity verification is run over a selected range Then verification passes if no entries have been altered or removed And any modification causes verification to fail and surfaces an integrity_error with the first offending entry_id And exports include the chain head hash to enable offline verification
Data Minimization and PII Redaction Enforcement
Given an audit event contains potential PII (e.g., full IP, device nickname, user email) When the event is persisted to the audit log Then IP addresses are stored in truncated form (IPv4 /24, IPv6 /64) And device nicknames are hashed with a salt; user emails are omitted And no raw receipt text or images are stored; only resource references (IDs) And entries retain device_id and scope to ensure attribution without exposing PII
Anomalous Activity Alerts & Auto‑Protection
"As an account owner, I want to be alerted to unusual session activity and have risky sessions auto‑paused so that I can stop unauthorized access without constantly monitoring the app."
Description

Continuously monitor scoped sessions for anomalies such as new geolocations, excessive API calls, scope escalation attempts, or access outside allowed hours. Trigger push/email alerts to the owner, auto‑pause suspicious sessions, and require re‑authentication before resuming. Integrate with the session dashboard for one‑click remediation and provide tunable thresholds per scope to balance security with minimal interruption to routine receipt capture and categorization.

Acceptance Criteria
New Geolocation Anomaly Detection and Alert
Given a scoped session with a geolocation anomaly threshold set to 50 km and auto-pause enabled When the session makes an API request from a location ≥50 km from its last validated location Then push and email alerts are sent to the owner within 15 seconds, the session state becomes "Paused — Geolocation anomaly", subsequent API calls return HTTP 423, and an audit record includes timestamp, IP, country, city, lat/long, device ID, and scope
Excessive API Call Rate Auto‑Pause
Given a scope configured with an API rate threshold of 120 requests per 60 seconds and auto-pause on breach When the session exceeds 120 requests in 60 seconds for two consecutive windows Then the session is auto-paused within 1 second of the second breach, push and email alerts are sent within 15 seconds with the observed rate metrics, subsequent API calls return HTTP 423, and an audit record captures per-window counts and endpoints hit
Scope Escalation Attempt Block and Owner Notification
Given a session token scoped to receipt:write and transactions:categorize without balance:read When the client requests a new token with expanded scope or calls an endpoint requiring balance:read Then the request is denied with HTTP 403, no expanded token is issued, the current session is auto-paused, push and email alerts are sent within 15 seconds, and no sensitive data is returned; an audit record captures requested scopes and endpoint
Access Outside Allowed Hours Enforcement
Given a scope with allowed hours set to 07:00–22:00 owner local time and action alert+pause When a request from that session occurs at 23:30 local time Then the session is auto-paused immediately, push and email alerts are sent within 15 seconds indicating the local time breach, subsequent API calls return HTTP 423, and the dashboard displays the pause reason and next allowed window
Re‑authentication Required to Resume Paused Session
Given a session paused due to any anomaly When the owner initiates Resume from the session dashboard and completes MFA successfully within 5 minutes Then the session is unpaused within 5 seconds, a fresh token with the same scope is issued, any prior tokens for that device are revoked, normal API access resumes, and an audit record links the anomaly to the re-auth event; if MFA fails or times out, the session remains paused
Session Dashboard One‑Click Remediation Actions
Given a paused or flagged session visible in the session dashboard When the owner clicks Revoke or Resume after MFA Then Revoke invalidates all tokens for that device/scope within 2 seconds and marks the session Terminated; Resume after MFA triggers the MFA flow and unpauses upon success, updating the UI state in under 2 seconds; all actions are recorded with actor, timestamp, and outcome
Per‑Scope Tunable Thresholds Apply Without Downtime
Given a scope named "VA Tablet" with thresholds geo=50 km, rate=120/60s, hours=07:00–22:00 When the owner updates any threshold via scope settings Then inputs are validated with inline errors for invalid values, accepted changes persist and propagate to active sessions within 30 seconds without dropping valid requests, subsequent anomaly detection uses the new thresholds, and all changes are audit logged with before/after values and actor

Travel Mode

Pre‑approve travel windows and destinations so legitimate movement doesn’t trigger lockouts. While traveling, new‑device enrollments are blocked by default, risk checks adjust to your itinerary, and printable backup codes keep you covered offline.

Requirements

Travel Window & Destination Pre-Approval
"As a traveling freelancer, I want to pre-approve my trip dates and destinations so that my legitimate logins don’t trigger lockouts while I’m on the road."
Description

Enable users to define one or more travel windows with start/end dates, time zones, and approved destinations (city, region, country). During active windows, authentication and session risk policies reference the itinerary to prevent false-positive lockouts from geo-velocity and unfamiliar location checks. Provide a calendar-based mobile-first UI, validation against conflicting windows, and server-side APIs to store encrypted itineraries. Integrate with the risk engine and auth gateway to tag sessions with Travel Mode context, adjust anomaly thresholds, and log all decisions for audit. Support editing, canceling, and quick activation within 24 hours of departure, with push/email confirmations.

Acceptance Criteria
Mobile Calendar UI: Create Travel Window
Given an authenticated user on the mobile Travel Mode screen When they tap "Add Travel Window", select start and end dates via the calendar picker, choose a time zone (IANA), add one or more destinations (city, region, or country), and tap Save Then the itinerary is validated (all required fields present; end >= start), saved server-side, and appears in the Travel Windows list with normalized time zone and destination labels And the API responds 201 with payload containing windowId, startAt/endAt (ISO 8601 UTC), timeZone, destinations[], and status="scheduled" And push and email confirmations are delivered within 60 seconds containing a summary and a manage link
Conflict Validation for Overlapping Travel Windows
Given at least one existing scheduled or active travel window When the user creates or edits a window whose time range overlaps an existing window after normalizing to UTC Then the action is rejected, the UI displays "Conflicts with existing travel window", and no changes are persisted And the API returns 409 with conflictWindowIds[] and the attempted payload echoed for correction And when the new window starts exactly when an existing window ends (new.startAt == existing.endAt), Then it is accepted as non-overlapping
Risk Engine Adjustments During Active Travel Window
Given an active travel window and a login/session initiated from within an approved destination and declared time zone When geo-velocity and unfamiliar-location checks are evaluated Then the session/auth context is tagged travelMode=true with windowId, and no lockout/MFA escalation is triggered solely due to travel within the approved window/destinations And a decision log entry is written with fields: sessionId, userId, windowId, decision, reasonCodes[], riskScoreBefore, riskScoreAfter, timestamp And when the session originates outside any approved destination during the active window, Then standard risk policy applies (no relaxation) and a reasonCode "outside_approved_destination" is logged
Block New-Device Enrollment While Traveling
Given an active travel window When the user attempts to enroll a new device or browser Then the enrollment is blocked by default with guidance to use pre-generated printable backup codes, and the API responds 403 with errorCode="travel_enrollment_blocked" And an audit event "device_enrollment_blocked_travel" is recorded with userId, windowId, timestamp, and device fingerprint And when the user provides a valid printable backup code, Then a one-time override allows enrollment to proceed, the event is logged as "device_enrollment_override_backup_code", and the session is marked with travelMode=true
Edit, Cancel, and Quick Activate within 24 Hours
Given a scheduled travel window starting within the next 24 hours When the user taps "Quick Activate" Then the window status becomes active immediately, the risk engine context updates within 60 seconds, and push/email confirmations are sent Given a scheduled or active window When the user edits dates, time zone, or destinations and saves Then conflict and field validations run; the update persists; the API responds 200 with windowId and revision; and confirmations are sent Given a scheduled window When the user cancels it Then it is removed from the schedule, the API responds 200, and an audit event "travel_window_canceled" is recorded
Server APIs: Encrypted Itinerary Storage and Audit Logging
Given a valid authenticated API client for the user When the client creates, updates, retrieves, or deletes a travel window via server APIs Then itinerary data (dates, time zone, destinations) is stored encrypted at rest and is never logged in plaintext; API requests are authorized to the resource owner only And every create/update/delete action emits an audit log with requestId, actorId, action, windowId, timestamp, and before/after summaries (sensitive fields masked) And retrieval APIs return only the caller's windows; cross-user access attempts return 403 and are logged
Itinerary-Aware Risk Scoring
"As a user, I want risk checks to adapt to my itinerary so that I can sign in smoothly from expected locations without reducing overall account security."
Description

Modify the risk engine to consume active itinerary data and location signals (GPS, IP geo, device locale) to dynamically adjust risk scoring. Lower weight of location novelty and geovelocity within approved corridors; maintain high sensitivity for impossible travel outside corridors and known bad IPs. Provide policy controls per environment (mobile/web) and configurable thresholds by security tier. Emit structured risk reasons, expose them in admin audit logs, and surface user-facing explanations for challenged logins. Ensure compatibility with VPN detection and roaming cellular networks, and add unit/chaos tests for common travel patterns.

Acceptance Criteria
Approved Corridor Attenuation
Given a user has an active travel itinerary with an approved corridor and time window And baseline weights for location_novelty and geovelocity are configured And an authentication attempt occurs from a location inside the corridor during the window When the risk engine evaluates the attempt Then the applied weight for location_novelty equals baseline_weight * configured_corridor_attenuation_factor And the applied weight for geovelocity equals baseline_weight * configured_corridor_attenuation_factor And the overall risk score reflects these attenuations And no "impossible_travel" reason is emitted
Impossible Travel Outside Corridor
Given two successful authentications within a 30-minute interval from locations requiring travel speed above the configured impossible_travel_speed_threshold And neither location is within any active approved travel corridor/window for the user When the risk engine evaluates the second authentication Then it emits a structured reason with reason_code "impossible_travel" and severity "high" And the overall risk score meets or exceeds the configured challenge_threshold for the environment And the session is marked "challenge_required"
Known Bad IP Overrides
Given an authentication attempt originates from an IP present in the known_bad_ips list or threat intelligence feed When the risk engine evaluates the attempt Then it emits a structured reason with reason_code "known_bad_ip" And the overall risk score meets or exceeds the configured block_or_challenge threshold regardless of travel corridor or window And corridor-based attenuation does not reduce the severity below the configured minimum for known_bad_ip
Environment- and Tier-Specific Thresholds
Given distinct risk thresholds are configured per environment (mobile, web) and per security tier (standard, elevated, high) And a user is assigned to a security tier When the same authentication signal set is evaluated on mobile and on web Then the computed overall risk scores are compared against their respective environment thresholds And the decision outcomes match the configured policy for each environment/tier combination And changes to thresholds via admin settings take effect within 1 minute and are auditable
Structured Risk Reasons and Admin Audit Logs
Given an authentication attempt is evaluated When the risk engine produces a decision Then it emits structured risk reasons as JSON containing at minimum: reason_code, signal, weight_applied, baseline_weight, confidence, environment, security_tier, itinerary_context (active, corridor_match), vpn_detected (true/false), ip_reputation, and correlation_id And the admin audit log records the full decision with timestamp, user_id, device_id, ip, geo, reasons[], score, and decision And audit log entries are searchable by correlation_id and user_id within 5 seconds of the event
User-Facing Challenge Explanations
Given a user’s login is challenged due to risk related to travel signals When the challenge screen is presented Then the user-facing explanation states the high-level reason (e.g., "location mismatch" or "impossible travel") and references the active Travel Mode itinerary if applicable And wording avoids sensitive data exposure (no exact GPS coordinates or IP shown) And an accessible "What’s this?" link provides steps to resolve (e.g., verify device, use backup codes) And the copy is localized according to the user’s locale
VPN/Roaming Compatibility and Travel Pattern Tests
Given the user connects via a consumer VPN that exits in a different country while device GPS is inside an approved corridor When the risk engine evaluates the attempt Then vpn_detected is true and reasons include "vpn" And corridor matching prefers trusted on-device location signals per policy, attenuating location_novelty as configured And connections from datacenter IP ranges are evaluated with elevated sensitivity per policy Given a user on a roaming cellular network causes transient IP geolocation hops within the corridor When multiple authentications occur within 2 hours Then geovelocity is not falsely triggered if GPS and device locale are consistent with the itinerary And unit and chaos tests simulate at least: airport Wi‑Fi handoffs, cross‑time‑zone flights, overnight train within corridor, VPN on/off toggles, and roaming IP changes, with all expected outcomes passing
New-Device Enrollment Lock (Travel)
"As a traveler, I want new-device enrollment blocked by default so that stolen credentials can’t be used to add a fresh device while I’m away."
Description

When Travel Mode is active, block new-device enrollments by default across mobile and web. Require one of: pre-generated backup code, previously-verified device approval, or support-verified override to allow enrollment. Present clear UX messaging and fallback paths; log and notify the user on each blocked attempt. Provide admin controls to whitelist specific device IDs for the travel window. Ensure consistent enforcement at auth gateway, device management service, and recovery flows.

Acceptance Criteria
Default Block of New-Device Enrollments During Active Travel Mode
Given a user with Travel Mode set to Active And the user attempts to enroll a new device from mobile or web When the enrollment request reaches the auth gateway, device management service, or any recovery flow Then the enrollment is denied with HTTP 403 and error code TRAVEL_ENROLL_BLOCKED And no new device record is created And the denial is consistently enforced across auth gateway, device management service, and recovery flows (password reset, account recovery) And an audit event travel_enroll_blocked is recorded with user_id, request_channel, device_fingerprint_hash, ip, timestamp, and reason And the UI displays a blocking message with links to allowed fallback methods
Enrollment via Backup Code During Travel
Given a user with Travel Mode set to Active And the user has at least one unused pre-generated backup code When the user enters a valid backup code during new-device enrollment Then the system validates the code (case-insensitive, exact match, unused) And enrollment proceeds and succeeds And the used backup code is immediately marked consumed and cannot be reused And an audit event travel_enroll_allowed_backup_code is recorded with user_id, device_fingerprint_hash, ip, timestamp, code_suffix And invalid backup code attempts are rate-limited to 5 per hour per user and return HTTP 401 with error code INVALID_BACKUP_CODE
Enrollment via Previously-Verified Device Approval During Travel
Given a user with Travel Mode set to Active And the user has at least one previously-verified device online When the user initiates new-device enrollment and requests approval from a verified device Then a one-time approval prompt is delivered to verified devices within 10 seconds And upon explicit approve on a verified device within 5 minutes, a single-use token is minted and sent to the enrollment flow And enrollment succeeds only if the token matches the pending request and is unused and unexpired And an audit event travel_enroll_allowed_device_approval is recorded with user_id, approver_device_id, new_device_fingerprint_hash, ip, timestamp And no enrollment occurs if no verified device approves within 5 minutes (error code DEVICE_APPROVAL_TIMEOUT)
Support-Verified Override for Enrollment During Travel
Given a user with Travel Mode set to Active And a support agent with override permission has verified the user per policy When the agent issues a support override token scoped to the user and a single new device Then the token has a maximum validity of 15 minutes or first use, whichever comes first And enrollment succeeds only when the presented token matches the issued token and device fingerprint hash And all override actions are logged with ticket_id, agent_id, user_id, device_fingerprint_hash, ip, timestamp And misuse is prevented by requiring agent MFA and reason code And expired or mismatched tokens return HTTP 401 with error code OVERRIDE_INVALID_OR_EXPIRED
User Messaging and Fallback Options on Blocked Enrollment
Given a user with Travel Mode set to Active When a new-device enrollment is blocked Then the UI displays the title "Travel Mode is on: new-device enrollments are blocked" And shows three actionable options: "Enter Backup Code", "Approve from Verified Device", and "Contact Support" And if no backup codes exist, the backup code option is disabled with guidance to generate codes before travel And accessibility is met (WCAG AA: focus order, labels, and screen-reader text) And the error code TRAVEL_ENROLL_BLOCKED is visible for support reference
Audit Logging and User Notifications for Blocked Attempts
Given a user with Travel Mode set to Active When any new-device enrollment attempt is blocked Then an audit log entry is created within 2 seconds containing user_id, ip, user_agent_hash, device_fingerprint_hash, channel, timestamp, reason=travel_mode_block And the user receives a notification within 60 seconds via email (and push if enabled) including timestamp, location hint (city/region), device label (derived), and a link to manage Travel Mode And notifications exclude full device fingerprints and backup code data And repeated attempts from the same IP within 10 minutes are bundled into a single digest notification
Admin Whitelist of Device IDs Within Travel Window
Given Travel Mode is Active and an admin has access to Travel Mode controls When the admin whitelists a specific device_id for a defined travel window (start/end timestamps) Then the whitelist propagates to auth gateway and device management within 60 seconds And enrollment from a device presenting the matching device_id during the window is allowed without backup code or approvals And attempts outside the window or with a non-matching device_id are blocked with TRAVEL_ENROLL_BLOCKED And creation, modification, and revocation of whitelist entries are audit-logged with admin_id, user_id, device_id, timestamps, and reason And revoking the whitelist immediately (<=60 seconds) re-enables blocks for that device
Offline Backup Codes Pack
"As a user preparing to travel, I want printable backup codes so that I can access my account even if I’m offline or can’t receive MFA prompts."
Description

Allow users to generate and download/print a one-time-use backup code pack prior to travel. Provide 10 single-use codes plus one emergency longer code, formatted for easy printing and safe storage, with optional PDF and Apple/Google Wallet passes. Enforce rate limits, code hashing at rest, and display last-used metadata. Allow regeneration that invalidates previous packs. Support code entry in low-bandwidth and offline-aware screens, with accessible formatting and localizations.

Acceptance Criteria
Generate One-Time Backup Code Pack Before Travel
Given a logged-in user with no active backup code pack When the user selects "Generate Backup Codes" from the Travel Mode setup Then the system creates a pack containing exactly 10 single-use codes and 1 emergency code And the emergency code length is greater than each single-use code And the UI labels the pack with creation date/time and the user’s timezone And codes are presented in an accessible, print-friendly layout (monospaced font, grouped characters, clear headings) And all text, dates, and labels are localized to the user’s selected language
Export Codes to PDF and Apple/Google Wallet
Given a generated backup code pack is visible to the user When the user selects "Download PDF" Then a PDF is produced containing all 10 single-use codes and the emergency code, the pack creation timestamp, and Travel Mode label And the PDF prints cleanly on one page for Letter and A4 with adequate margins and high contrast And the PDF uses accessible fonts and preserves code grouping for readability And PDF text (not images) is selectable and copyable And labels, dates, and messages in the PDF reflect the user’s locale When the user selects "Add to Apple Wallet" on iOS Then a valid Wallet pass is created and installable, displaying the codes and pack metadata And pass labels and dates are localized When the user selects "Add to Google Wallet" on Android Then an equivalent pass is created and installable with the same content and localization
Rate Limiting on Pack Generation
Given a configurable generation limit window (N) is in effect And the user has generated a pack within the last N period When the user attempts to generate a new pack within the same N period Then the request is blocked with HTTP 429 (or equivalent) and a clear message showing remaining wait time And no new codes are created And an audit event is recorded with user ID, timestamp, and reason "rate_limited" When the N window has elapsed Then the user can successfully generate a new pack
Codes Stored as One-Way Hashes at Rest
Given a backup code pack has been generated When codes are persisted to storage Then only salted one-way hashes of each code are stored at rest; plaintext codes are not persisted beyond initial render And verification uses constant-time comparison against stored hashes And database snapshots/backups contain only hashed values And after the generation view is closed or refreshed, plaintext codes cannot be retrieved by the client
Display Last-Used Metadata for Codes
Given a user has an active backup code pack And one or more codes have been used for authentication When the user opens the Backup Codes screen Then each code displays its status (Unused/Used) And used codes display the last-used timestamp in the user’s timezone and approximate device or location metadata as available And the emergency code displays status and metadata in the same format And the screen shows the count of remaining single-use codes
Regenerate Pack and Invalidate Previous Codes
Given a user has an active backup code pack When the user selects "Regenerate Pack" and confirms the action Then a new pack with new codes is created and displayed And all codes from the previous pack are immediately invalidated And attempting to use any previous code is rejected with a clear error message and audit log entry And the UI marks the prior pack as "Revoked" with a revocation timestamp
Offline-Aware, Low-Bandwidth Code Entry Screen
Given a user is on a mobile device with poor connectivity (offline or 2G-equivalent) When the user opens the backup code entry screen Then the screen loads with minimal assets, displays an offline/low-bandwidth notice, and remains fully operable with keyboard and screen readers And labels and messages are localized to the user’s language When the user submits a code while offline Then the app queues the submission and prompts to retry when a connection is available without losing the entered code When connectivity is available Then the code is validated and the user receives clear success/failure feedback within the defined timeout
Auto Start/Stop + Extension Controls
"As a user, I want Travel Mode to start and stop automatically with easy extensions so that I don’t have to babysit my security settings while traveling."
Description

Implement automatic activation of Travel Mode at the start of the configured window and automatic deactivation at the end. Provide reminders 24 hours before start and 12 hours before end, with one-tap options to start now, pause, or extend the window. Handle early returns and delays by allowing extension by date/time with validation. Sync state across devices in near real-time and ensure deactivation resets policies to default. Record state changes in audit logs and send confirmation notifications.

Acceptance Criteria
Auto Activation at Scheduled Start
Given a user has configured a Travel Mode window with start time Tstart and end time Tend in their profile timezone, When the current time reaches Tstart, Then Travel Mode transitions to Active within 60 seconds without user interaction, And Travel Mode policies are applied immediately (e.g., new-device enrollments blocked, risk checks switched to Travel profile).
Auto Deactivation at Scheduled End
Given Travel Mode is Active with a configured end time Tend, When the current time reaches Tend, Then Travel Mode transitions to Inactive within 60 seconds, And all policies reset to the account’s default profile (e.g., new-device enrollments unblocked, standard risk checks restored).
24-Hour Pre-Start Reminder with Start Now
Given a scheduled Travel Mode window with start time Tstart ≥ 24 hours from now and notifications enabled, When the current time is Tstart minus 24 hours (±2 minutes), Then a reminder is delivered containing a Start Now action, And when the user taps Start Now, Then Travel Mode activates immediately, the original Tend remains unchanged, And the scheduled auto-start at Tstart is canceled to prevent duplicate activation.
12-Hour Pre-End Reminder with Extend
Given a Travel Mode window with end time Tend and notifications enabled, When the current time is Tend minus 12 hours (±2 minutes), Then a reminder is delivered containing an Extend action, And when the user selects Extend and chooses a valid new end time Tnew > Tend, Then the window’s end time updates to Tnew and the updated schedule is displayed to the user.
Manual Controls During Active Window (Pause/Resume, End Now)
Given Travel Mode is Active, When the user taps Pause, Then the state changes to Paused within 30 seconds and default policies are applied while paused, And the scheduled end time remains unchanged; When the user taps Resume, Then the state changes back to Active within 30 seconds and Travel Mode policies reapply; When the user taps End Now, Then Travel Mode transitions to Inactive within 30 seconds, scheduled auto-deactivation for this window is canceled, and default policies are restored.
Validated Extension by Date/Time
Given a user chooses to extend an existing Travel Mode window, When the user submits a new end date/time Tnew, Then the system validates that Tnew is (a) later than now, (b) later than the current end time, and (c) expressed and stored in the correct timezone context shown to the user; When validation fails, Then the extension is rejected with a specific error message; When validation succeeds, Then the end time updates to Tnew and the change is reflected immediately in the schedule UI.
State Sync, Audit Logging, and Confirmations
Given any Travel Mode state change occurs (auto start, manual start, pause, resume, extend, auto end, manual end), Then the new state and schedule propagate to all signed-in devices within 5 seconds (p95), And offline devices reflect the updated state within 5 seconds of reconnection; And an audit log entry is recorded with UTC timestamp, actor (system or user), action type, previous state, new state, start/end times, and source device/session; And a confirmation notification is sent within 30 seconds describing the action and the effective schedule.
Travel Geo-Fence Login Policy
"As a traveler, I want logins restricted to my planned destinations so that attempts from unexpected regions are blocked or challenged."
Description

Enforce a geo-fence that allows logins from approved destinations during Travel Mode while treating other locations as high risk requiring step-up verification or denial based on policy. Support radius-based and region-based fences, accounting for airport transits and layovers. Integrate with IP reputation, GPS consent, and device OS location prompts; degrade gracefully when precise location is unavailable by using IP-only heuristics. Provide user and admin visibility of allowed regions and capture telemetry for tuning.

Acceptance Criteria
Approved Radius-Based Login During Travel Window
Given Travel Mode is active and a radius fence is configured with a center point and radius in meters And the travel start and end timestamps are currently in effect And the device reports GPS with accuracy <= 50 meters When the user attempts to log in from within the configured radius Then the login is allowed without step-up verification And the session risk score applies the in-itinerary modifier And the audit log records fence_id, location_source=GPS, distance_from_center (meters), decision=allow
Approved Region-Based Login With IP-Only Fallback
Given Travel Mode is active with one or more approved regions configured And the app requested OS location permission and the user denied precise location for this session And IP geolocation places the user inside an approved region with confidence >= 0.80 And IP reputation score is below the high-risk threshold When the user attempts to log in Then the login is allowed without step-up verification And the UI displays a banner indicating coarse location was used And the audit log records location_source=IP, ip_geo_confidence, matched_region_id, decision=allow
Unapproved Location Requires Step-Up or Denial per Policy
Given Travel Mode is active and the login attempt is outside all approved fences/regions by GPS or IP fallback And the tenant policy for off-itinerary is Step-Up When the user attempts to log in Then a step-up challenge (TOTP, WebAuthn, or backup code) is required And if step-up succeeds within 3 attempts, login is allowed and audit decision=allow_stepup And if step-up fails 3 times or times out, login is denied with reason=stepup_failed Given Travel Mode is active and the tenant policy for off-itinerary is Deny When the user attempts to log in from outside approved fences/regions Then login is denied with reason=geo_fence_violation and no step-up is offered And the audit log includes fence_match=false, decision=deny
Airport Transit and Layover Corridor Handling
Given the user’s itinerary includes airports with defined geofenced zones and a layover corridor with a valid time window And device location (GPS or IP) is within an airport zone or the defined corridor during the layover window When the user attempts to log in Then the login is treated as in-itinerary and allowed unless IP reputation is high-risk And if IP reputation is >= high-risk threshold, a step-up is required prior to allow And the audit log captures airport_or_corridor=true, layover_window_valid=true/false, decision outcome
New-Device Enrollment Blocked During Travel Mode
Given Travel Mode is active And the login flow detects this device is not enrolled When the user reaches the device enrollment step Then device enrollment is blocked by default with a message citing Travel Mode restrictions And if admin override policy is enabled, enrollment proceeds only after successful backup code verification And the audit log records enrollment_blocked=true, override_used=true/false, decision outcome
Admin Configuration of Geo-Fences and Itinerary
Given an admin provides a radius fence (center latitude/longitude and radius in meters) and/or region fences (ISO codes or polygons) And travel start and end timestamps are set When the admin saves the configuration Then server-side validation enforces required fields, numeric bounds, and valid polygons And a preview map renders all fences successfully And users see the updated allowed regions and dates in their profile within 60 seconds And the configuration is versioned with ability to retrieve prior versions via admin UI/API
Telemetry Capture and Visibility for Tuning
Given any login attempt during Travel Mode When a decision is made (allow, allow_stepup, deny) Then telemetry stores timestamp, decision, location_source (GPS/IP), GPS accuracy (m), IP geo confidence, IP reputation score, fence matched, distance to fence (m), step-up outcome, device_id_hash And coordinates are stored with privacy controls (hashed or rounded to >= 2 decimal places) And admin analytics dashboard displays aggregates (allow/deny rates, step-up rate, missing-location rate) and supports CSV export for the last 90 days And user and admin views display the currently allowed regions and active travel window

Role Blueprints

Prebuilt and customizable permission templates for VAs and bookkeepers. Pick a blueprint like Upload‑Only, Categorize+Match, or Reconcile+Export, then timebox it and preview “view‑as‑delegate” before granting. Reduces setup mistakes, speeds onboarding, and ensures the right access with one click.

Requirements

Blueprint Catalog
"As a freelancer, I want to pick a ready-made role for my bookkeeper so that I can grant the right access quickly without misconfiguring permissions."
Description

Provide a library of prebuilt permission templates tailored to TaxTidy tasks (Upload‑Only, Categorize+Match, Reconcile+Export, Export‑Only), each mapped to system permissions covering invoices, bank feeds, receipt uploads, categorization, reconciliation, and export functions. Include metadata such as description, risk level, and recommended delegate type, ensure mobile and web compatibility, and prepare localization-ready copy. Integrate with the existing ACL and role engine so each blueprint is declarative, testable, and versioned for consistent behavior across environments.

Acceptance Criteria
Catalog List Shows Prebuilt Blueprints with Metadata
Given an authenticated Owner opens the Role Blueprints Catalog on web or mobile When the catalog loads Then it displays at least the following prebuilt blueprints: Upload-Only, Categorize+Match, Reconcile+Export, Export-Only And each blueprint card shows: name, description, risk level, recommended delegate type And tapping/clicking a blueprint navigates to its detail view Given the catalog is loaded When the user searches for "Export" Then only blueprints whose name or description contains "Export" are shown
Blueprint Detail Exposes Mapped System Permissions
Given the user opens the detail view for each prebuilt blueprint When viewing the Mapped Permissions section Then permissions are listed as canonical IDs present in the ACL registry with no unknown or duplicate IDs And permissions are grouped by category: Invoices, Bank Feeds, Receipt Uploads, Categorization, Reconciliation, Export And Upload-Only allows only Receipt Uploads and denies Invoices, Bank Feeds, Categorization, Reconciliation, Export And Categorize+Match allows Categorization and Bank Feeds (read/match) and denies Receipt Uploads, Invoices, Reconciliation, Export And Reconcile+Export allows Reconciliation and Export and denies Receipt Uploads, Invoices, Categorization And Export-Only allows only Export and denies Receipt Uploads, Invoices, Bank Feeds, Categorization, Reconciliation
Blueprints Are Declarative and Versioned
Given blueprint definitions are stored as declarative JSON When validating against schema version 1.x Then each blueprint includes fields: id (UUID), name, version (semver), description, riskLevel ∈ {Low, Medium, High}, recommendedDelegateType ∈ {VA, Bookkeeper}, permissions[] of canonical IDs, localeKeys for name and description And schema validation passes with 0 errors Given a prebuilt blueprint is opened for edit When the user attempts to modify its permissions Then the system prevents direct edits and prompts to Create Custom Variant And the new custom variant is saved with a new id and an incremented version Given the same blueprint version is exported from staging and production When their definition hashes are compared Then the hashes match and the permission sets are identical
ACL Enforcement When Blueprint Is Applied
Given an Owner assigns blueprint Export-Only to delegate user D When D calls API endpoints for export features Then responses are 2xx and actions succeed And when D calls endpoints requiring non-export permissions (receipt upload, categorization, reconciliation) Then responses are 403 Forbidden and no side effects occur Given the Owner removes the assignment When D retries an export endpoint Then the response is 403 Forbidden
Localization-Ready Copy for Blueprint Catalog
Given the app locale is set to fr-FR with translations available When the catalog and blueprint detail views render Then blueprint names and descriptions are sourced via i18n keys and displayed in French Given a locale missing a translation for a blueprint description When the view renders Then the name/description fall back to en-US without rendering raw keys or placeholders And all blueprint copy strings have stable i18n keys matching the pattern /blueprints/{id}/{field}
Responsive Web and Mobile Catalog Experience
Given a mobile viewport width ≤ 414px When the catalog loads Then blueprint cards are readable without horizontal scrolling and primary tap targets are ≥ 44x44 px And opening a blueprint detail on mobile Then the permissions list is collapsible by category and scrolls vertically only Given a desktop viewport ≥ 1280px When the catalog loads Then the same blueprints and metadata are visible and actionable as on mobile (functional parity)
Blueprint Editor & Data Scoping
"As an account owner, I want to tailor a template’s permissions and scope so that my delegate gets only the access they need."
Description

Allow owners to clone and customize blueprints with granular permissions and data scopes, including read/write toggles for receipts, invoices, bank feeds, category management, reconciliations, and exports. Support scoping by bank account, workspace, date range, category, and vendor, plus PII masking controls for sensitive fields. Validate incompatible combinations and present a plain-language capability summary before saving. Persist changes as new blueprint versions aligned to the ACL schema to ensure consistent enforcement across mobile and web clients.

Acceptance Criteria
Clone and customize granular permissions
Given an owner opens Blueprint Editor and selects an existing "Categorize+Match" blueprint When the owner clones it and sets: receipts(read=true, write=false); invoices(read=true, write=true); bankFeeds(read=true, write=false); categories(manage=true); reconciliations(perform=true); exports(generate=false) Then the UI reflects the exact selections and the Save action is enabled And the persisted blueprint ACL contains exactly these flags And a delegate assigned this blueprint can create/edit invoices, view receipts and bank feeds, manage categories, perform reconciliations, and cannot upload receipts or generate exports
Apply multi-dimensional data scopes
Given an owner is editing a cloned blueprint When the owner sets scopes: workspace=Acme Studio; bankAccounts=[Chase-1234]; dateRange=2025-01-01..2025-03-31; categories=[Meals, Software]; vendors=[Apple, Uber] Then the scope summary displays the selected workspace, 1 bank account, the exact date range, 2 categories, and 2 vendors And only in-scope items are visible to the delegate across receipts, invoices, bank feeds, reconciliations, and exports And attempts to access out-of-scope data return HTTP 403 with code ACL_DENIED and are logged And clearing a dimension (e.g., vendors) broadens access accordingly and the persisted ACL reflects the change
Mask sensitive PII in views and exports
Given an owner enables PII masking: bankAccountNumbers=masked, taxId=masked, clientAddresses=visible When previewing as delegate and when the delegate uses mobile or web Then masked fields display obfuscated values (e.g., last4 only) in UI and are redacted in CSV/PDF exports generated by the delegate And owner/admin views remain unmasked And audit logs record that masking was applied for each export with blueprint version id
Validate incompatible permission combinations
Rule: For receipts, invoices, bankFeeds, and categories, Write implies Read; if Write=true while Read=false, Save is blocked and inline error "Write requires Read" is shown per entity Rule: exports.generate=true requires reconciliations.perform=true AND invoices.read=true AND bankFeeds.read=true; otherwise Save is disabled and a consolidated error list is shown Rule: reconciliations.perform=true requires bankFeeds.read=true; otherwise inline error is shown and Save remains disabled Rule: If PII mask=false while corresponding entity read=false, mask is auto-set to true and a non-blocking notice is shown Given any invalid state Then Save remains disabled until all blocking errors are resolved and validation updates in real time as toggles change
Preview plain-language capability summary before saving
Given permissions, scopes, and PII masks are configured When the owner clicks "Review summary" Then a plain-language summary renders that enumerates allowed actions per domain (view, create/edit, manage, reconcile, export), all scopes (workspace, bank accounts, date range, categories, vendors), and PII masking status And the summary content is generated from the same ACL model that will be persisted (1:1 parity) And any change to a toggle or scope updates the summary immediately And attempting to Save without viewing the summary prompts the owner to review before finalizing And the saved ACL matches the summary values in an automated parity check
Persist changes as new blueprint version aligned to ACL schema
Given a blueprint at version v2 exists When the owner saves changes Then a new version v3 is created with an incremented version id, createdAt timestamp, author, and ACL JSON that validates against the current ACL schema version (no validation errors) And v2 remains immutable and continues to govern existing delegate assignments until explicitly reassigned And the version history shows a diff of changed permissions, scopes, and masking settings And the owner can roll back to v2, resulting in the exact original ACL payload being applied
Consistent ACL enforcement across mobile and web clients
Given a delegate is assigned blueprint version v3 with defined permissions, scopes, and PII masks When the delegate performs the same in-scope and out-of-scope actions on iOS, Android, and Web Then all clients allow the same in-scope actions and deny the same out-of-scope actions with a consistent error code ACL_DENIED and message And server-side authorization logs record uniform policy decisions with blueprint version id and client identifier And cached data on each client is filtered to ACL scope, preventing display of out-of-scope records
Timeboxed Access & Auto-Revocation
"As a solo founder, I want to time-limit my VA’s access so that privileges expire automatically when the task is done."
Description

Enable start/end times, duration presets, and recurring access windows for blueprint assignments, with automatic revocation on expiry and optional renewal prompts. Integrate with notifications to alert owners and delegates before expiration, and with the auth layer to enforce real-time revocation across active sessions. Provide calendar link generation and audit entries for granted and revoked access to reduce lingering privileges and support compliance.

Acceptance Criteria
One-Time Timebox: Auto-Revocation and Live Session Termination
Given a delegate is assigned a Role Blueprint with start=now and end=T When current time reaches T Then all active delegate sessions are invalidated within 60 seconds and subsequent API/UI requests return 403 AccessExpired Given the delegate has multiple active devices When T is reached Then each device session is terminated and refresh tokens are blocked Given the delegate attempts access at T+60s When they open the app or call any protected endpoint Then access is denied and an expiry message with owner contact is displayed
Duration Presets and Custom Start/End Validation
Given the owner selects a duration preset (1h, 24h, 7d, Custom) When the assignment is saved Then start/end are computed and shown in the owner's local time with UTC equivalents and the delegate sees their local preview Given the owner enters a custom window where end <= start When saving Then the save is blocked and a validation error is displayed specifying the required ordering Given start is in the future When the delegate signs in before start Then access is blocked and a countdown to start is shown Given the owner edits the timebox When changes are saved Then the new window takes effect immediately and enforcement reflects the change on next request
Recurring Access Windows Enforcement (Timezone & DST)
Given the owner configures a weekly window Mon–Fri 09:00–17:00 in a chosen timezone When the delegate accesses inside that window Then access is granted; outside that window access is denied Given a DST transition occurs in the configured timezone When the next occurrence of the window is enforced Then the 09:00–17:00 local hours are preserved without a one-hour drift Given the owner and delegate are in different timezones When viewing the assignment Then each sees local times while enforcement runs in the configured timezone Given a recurrence end date is set When that date is reached Then the recurring access stops and auto-revocation occurs
Pre-Expiry Notifications to Owner and Delegate
Given an assignment with end time T When time is T-24h and T-15m Then owner and delegate receive notifications via their enabled channels (email, push, in-app) with blueprint name, end time, and a renewal link for the owner Given the end time is edited after a notification is sent When the new time is saved Then remaining notifications are rescheduled to the new T and duplicates within 5 minutes are suppressed Given a notification delivery attempt fails When retries are triggered Then the system retries up to 3 times with exponential backoff and logs the final status
Expiry Renewal Prompt and Extension Flow
Given the owner clicks Renew before T When a new duration or end time is selected Then the assignment is extended without requiring a new invite and the delegate retains context Given no action is taken by T When access auto-revokes Then the owner receives a post-expiry prompt with one-click Renew using the previous duration template for 24 hours Given the owner renews after expiry When the renewal is confirmed Then access is re-granted with start=now and the selected end time, and the delegate regains access on next request
Calendar Link Generation (ICS) for Timeboxed Assignment
Given an assignment is created or updated When the owner requests a calendar link Then a unique ICS URL is generated containing UID, SUMMARY with delegate and blueprint, DTSTART/DTEND with TZID, and RRULE for recurring windows Given the ICS URL is added to Google or Apple Calendar When the assignment window changes Then the calendar reflects the update within typical refresh intervals and shows correct local times Given an assignment is revoked early When the ICS feed is refreshed Then the event is shortened or cancelled accordingly
Audit Trail for Grant, Change, and Revoke
Given an assignment is created When the grant is saved Then an immutable audit entry records actor, subject, blueprint, permissions scope, start, end, recurrence, timestamp, and source IP Given revocation occurs (scheduled or manual) When revocation is processed Then an audit entry records reason, time, and the number of sessions terminated Given an access window is edited or renewed When the change is saved Then an audit entry captures before/after values and links to the prior entry Given a compliance export is requested for a date range When the export is generated Then a CSV or JSON file is produced with all matching entries and passes schema validation
View-As-Delegate Preview
"As an account owner, I want to preview the delegate’s view so that I can verify access is correct before granting it."
Description

Provide a safe preview mode that renders the product exactly as the delegate would see it under the selected blueprint and scope, including mobile and desktop layouts, while preventing any write actions. Highlight hidden sections and disabled actions, and run a permission lint to flag missing capabilities required by common tasks such as categorizing and matching. Integrate with analytics to log preview coverage and with the editor to resolve gaps before assignment.

Acceptance Criteria
Exact UI Parity in Preview by Blueprint, Scope, and Timebox
Given I select the blueprint "Categorize+Match", scope "Acme LLC", and timebox "Next 30 days" When I enter View‑As‑Delegate Preview Then only navigation items permitted by the blueprint are visible And owner‑only controls (e.g., Role Blueprints, Billing, Export) are not visible And lists and dashboards show only data within the selected scope and timebox And a non‑dismissible banner shows "Preview Mode", the selected blueprint, scope, and timebox
Write Actions Fully Suppressed in Preview Mode
Given I am in View‑As‑Delegate Preview When I attempt any write action (e.g., categorize a transaction, upload a receipt, edit a rule, change a tag) Then the action controls are disabled and display a tooltip "Disabled in Preview" And no data is created, updated, or deleted in the system And no write API calls (POST/PUT/PATCH/DELETE) are sent And the UI remains in its pre‑action state with no toasts indicating success
Highlights Overlay Shows Hidden Sections and Disabled Actions
Given I am in View‑As‑Delegate Preview with a selected blueprint When I toggle "Show Highlights" on Then areas hidden by the blueprint are represented by ghost placeholders labeled "Hidden by blueprint" And disabled actions display a lock icon and a tooltip listing the missing permissions And when I toggle "Show Highlights" off, the delegate view returns to normal with no overlays
Permission Lint Flags Missing Capabilities for Common Tasks
Given the blueprint "Upload‑Only" is selected When I run Permission Lint Then the lint report lists common tasks (e.g., Categorize Transactions, Match Receipts, Reconcile Accounts, Export Tax Packet) with Pass/Fail per task And for each Fail, the report enumerates specific missing permissions and suggested fixes And the Assign button displays a warning state while any task fails and requires explicit acknowledgment to proceed
Cross‑Device Preview Parity (Mobile and Desktop)
Given I am in View‑As‑Delegate Preview When I switch device mode to Mobile Then the UI renders the mobile layout and navigation consistent with the selected blueprint and scope When I switch device mode to Desktop Then the UI renders the desktop layout with the same permission constraints And at no point do disabled or hidden actions become available due to device mode
Analytics Logging of Preview Coverage and Flow Navigation
Given I start a View‑As‑Delegate Preview session When I navigate between key areas (e.g., Transactions, Receipts, Invoices) and switch device modes Then analytics events are emitted for session start, navigation, device switch, lint run, and session exit And each event includes blueprint_id, scope_id, timebox, device_mode, and anonymized workspace/session identifiers with no PII And at least 95% of sessions appear in analytics within 5 minutes of session end
Editor Integration to Resolve Lint Gaps Before Assignment
Given Permission Lint shows missing capability "Match Receipts" When I click "Fix in Editor" Then the Role Blueprint Editor opens with the current blueprint pre‑loaded and the missing capability pre‑selected When I save changes in the editor Then the preview auto‑refreshes and the lint re‑runs, reflecting updated Pass/Fail results And if I cancel without saving, the preview returns unchanged and lint results remain the same
One-Click Assign & Bulk Onboarding
"As a freelancer, I want to grant the same role to my VA and bookkeeper at once so that I can onboard both without repetitive setup."
Description

Streamline assignment by letting owners pick a blueprint and assign it to one or multiple delegates in a single action, inviting new users via email or SMS and applying to existing users instantly. Enforce prerequisites such as 2FA, NDA acknowledgment, and verified email before activating access. Provide real-time status feedback, conflict handling, and templated onboarding messages customized by blueprint. Integrate with the user directory, invite service, and permissions engine to ensure immediate, consistent access across devices.

Acceptance Criteria
Bulk One-Click Assignment to Mixed New and Existing Delegates
Given an owner selects a single Role Blueprint and a list of recipients containing existing users and new contacts (email and/or phone), When the owner clicks Assign, Then existing users receive the blueprint instantly and appear as Active within 60 seconds, And new contacts receive invites via the chosen channel within 10 seconds, And duplicate recipients are de-duplicated before sending, And the summary modal displays counts of Active applied and Invites sent, And the operation is idempotent so re-clicking Assign does not create duplicate invites or roles.
Prerequisite Gating: 2FA, NDA, and Verified Email Before Activation
Given a recipient who lacks any required prerequisite (2FA enabled, NDA acknowledged, verified email), When the owner assigns a Role Blueprint, Then the recipient’s status shows Pending Prereqs and no effective permissions are granted, And the invite or in-app prompt includes steps to complete the missing prerequisites, And upon completion of all prerequisites, Then the role auto-activates without owner intervention and status changes to Active within 60 seconds, And audit events are recorded for each prerequisite completion and activation.
Conflict Detection and Resolution with Existing Permissions
Given a recipient who already has permissions that overlap or exceed the selected blueprint, When the owner initiates assignment, Then the system flags a conflict and presents options: Replace existing, Merge with blueprint, or Skip, And the default selection is the least-privilege safe option (Skip), And if Replace is chosen, Then existing conflicting permissions are revoked and only the blueprint applies, And if Merge is chosen, Then resulting permissions are the union without duplication and are displayed as a diff before confirm, And choosing Skip leaves permissions unchanged and marks the recipient as Skipped, And all outcomes are logged to the audit trail.
Timeboxed Access: Start, End, and Auto-Expiry Enforcement
Given the owner sets a start time (now or scheduled) and an end time (date/time or duration) for a Role Blueprint assignment, When assignment is confirmed, Then access becomes effective at the start time and not before, And access is automatically revoked within 60 seconds after the end time across web, mobile, and API, And currently signed-in sessions lose permissions within 60 seconds of expiry, And extending or revoking early updates all devices within 60 seconds, And time computations respect the owner’s configured timezone and display it in the confirmation.
Templated Onboarding Messages Customized by Blueprint
Given a selected blueprint with a default onboarding template, When the owner previews the message, Then the preview resolves variables (recipient name, blueprint name, scope summary, timebox, prerequisite steps, deep link) and shows channel-specific formatting, And the owner can edit subject/body per assignment without altering the global template, And upon Assign, Then messages are sent via the chosen channel(s) with delivery status tracked (Queued, Sent, Delivered, Bounced) and visible to the owner, And links open the correct app context and preselect the assignment, And no message is sent to existing users when Apply instantly is selected unless the owner includes a notification.
Real-Time Status and Integration Consistency Across Services and Devices
Given an assignment is initiated, When viewing the assignment progress panel, Then per-recipient status updates stream in real time (≤5s latency) via websocket or long-poll, showing states: Active, Scheduled, Pending Prereqs, Invited, Skipped, Failed, Conflict, And the user directory, invite service, and permissions engine reflect consistent state within 60 seconds of any change, And the owner can filter by status, retry failed invites, and resend messages, And exporting the status list produces a CSV with recipient, channel, timestamps, and final outcome, And any integration error surfaces a human-readable message and a retry option without duplicating roles or invites.
Blueprint Audit Trails & Version Pinning
"As a business owner, I want clear logs and version control for roles so that I can prove who had access to what and when."
Description

Record an immutable audit trail of blueprint creation, edits, assignments, previews, activations, and revocations, including actor, timestamp, IP, and change diffs. Pin each assignment to a specific blueprint version so future edits do not silently escalate or reduce access, and provide an explicit upgrade workflow to migrate delegates to newer versions. Offer exportable logs for CPA or IRS reviews and alerts when assignments reference deprecated or high-risk versions. Integrate with logging, SIEM hooks, and export tooling.

Acceptance Criteria
Immutable Audit Trail for Blueprint Lifecycle Events
Given a user performs any lifecycle action (create, edit, preview, assign, activate, revoke) on a role blueprint When the action completes (success or failure) Then a corresponding audit record is written within 1000 ms including event_type, blueprint_id, blueprint_version, actor_id, actor_role, actor_org_id, timestamp (ISO 8601 UTC), source_ip, correlation_id, outcome, and error_code (if any) Given an audit record is written When it is persisted Then it is stored in an append-only medium and is non-deletable/non-editable by org users; any attempted modification is blocked and generates a security event Given an admin requests to view the audit trail When filtering by date range, event type, actor, or blueprint_id Then the system returns results within 2 seconds for up to 10,000 records ordered by timestamp desc
Field-Level Diff Capture on Blueprint Changes
Given a blueprint is edited When the change is saved Then the audit record contains a JSON diff of permissions, scopes, and constraints (added/removed/modified) and the previous_version and new_version identifiers Given a delegate assignment is updated (role, timebox, scope) When saved Then the audit record captures before and after values for each field, redacting secrets, and associates the change with the assignment_id Given a preview (view-as-delegate) is initiated When started and ended Then start and end audit records are stored with actor_id, target_delegate_id, duration_ms, and a sampling of accessed modules (names only)
Version Pinning Prevents Silent Permission Changes
Given a delegate is assigned to blueprint version N When a new blueprint version N+1 is published Then the delegate’s effective permissions remain exactly those of version N until an explicit upgrade occurs, and the assignment record includes pinned_version = N Given API and UI permission checks are executed When evaluating a pinned assignment Then they resolve permissions against the pinned_version only and ignore newer versions Given a blueprint version is archived or edited When existing assignments are evaluated Then no permission escalation or reduction is observed in access logs compared to the previous 24 hours for the same user and scope
Explicit Upgrade Workflow for Delegates
Given a newer blueprint version exists When an admin initiates upgrade for one or more delegates Then the UI shows a human-readable diff of permission changes, risk flags, affected scopes, and requires an explicit confirmation with an optional justification note (min 10 chars) Given the upgrade is confirmed When processed Then each upgraded assignment records an audit event with previous_version, new_version, approver_id, timestamp, and result (success/failure) and notifies the delegate via in-app and email within 60 seconds Given an upgrade causes reduced access When applied Then the system offers a one-click rollback to the prior version within 24 hours, recording a rollback audit event
Exportable Compliance Logs (CPA/IRS)
Given an org admin requests log export When specifying date range, event types, actors, and blueprints Then the system generates a downloadable export in CSV and JSONL formats with a data dictionary and field headers, and begins download within 60 seconds for up to 1,000,000 records Given an export is generated When completed Then the file includes a SHA-256 checksum, signing metadata (issuer, created_at), and a chain hash of records to prove integrity, and an export_created audit event is recorded Given large datasets exceed 1,000,000 records When exporting Then the system paginates into sequential files with no gaps or overlaps (verified by sequence numbers) and includes an index manifest file
Alerts for Deprecated or High-Risk Blueprint Versions
Given a blueprint version is marked deprecated or flagged high-risk When any active assignment references it Then the owner sees an in-app banner and receives an email alert within 15 minutes, including counts of affected delegates and a link to the upgrade workflow Given an assignment remains on a deprecated version for more than 30 days When daily alerting runs Then a reminder is sent no more than once every 24 hours until resolved or acknowledged, and an alert audit event is recorded each time Given an alert is acknowledged When acknowledged by an admin Then the banner is suppressed for 7 days for that admin and the acknowledgment is logged with actor_id and timestamp
SIEM/Webhook Integration for Security Events
Given SIEM forwarding is enabled When audit events are produced Then a JSON payload conforming to the documented schema is delivered over HTTPS with mTLS or signed HMAC within 120 seconds with at-least-once delivery semantics and exponential backoff on failure Given the SIEM endpoint is unavailable When retries exceed the configured threshold Then the system buffers up to 500,000 events per org, drops no events while buffer has capacity, and emits a health alert to org admins Given secrets for the webhook are rotated When new credentials are saved Then events switch over without loss or duplication and a credential_rotation audit event is recorded
Conflict Detection & Least-Privilege Validator
"As an account owner, I want the system to warn me about excessive or conflicting access so that I can keep my delegates on least-privilege settings."
Description

Automatically analyze an assignee’s effective permissions when multiple blueprints or legacy roles are applied, detecting overbroad access, conflicting scopes, and missing prerequisites. Provide real-time warnings, suggested downgrades, and one-click fixes to converge on least-privilege. Integrate with the ACL engine to compute permission unions and diffs, and with the UI to present a plain-language summary of what the delegate can see and do.

Acceptance Criteria
Effective Permission Union and Diff Calculation
Given an assignee has blueprints [Upload-Only, Categorize+Match] and a legacy role 'Reviewer', with timeboxes where Upload-Only is active and Categorize+Match is expired When the validator runs Then it computes the union of all currently active permissions and resources, excluding expired and not-yet-active grants And it computes the diff versus the selected target blueprint(s) least-privilege baseline And it exposes the result via ACL engine at GET /acl/effective?userId={id} and GET /acl/diff?userId={id}&baseline={blueprintId} returning 200 with JSON payloads And the computation completes within 300 ms p95 for up to 50 grants and 5,000 permission statements And results are deterministically identical across repeated runs with the same inputs
Overbroad Access Detection and Warning
Given the computed effective permissions include actions outside the target blueprint scopes or broader resource scopes When the admin is on the Assign Roles screen Then a real-time warning banner appears with total overbroad items count and severity badges [Critical, High, Medium] And a details panel lists each overbroad permission with action, resource scope, and source grant And the Confirm/Save action is disabled until Critical conflicts are resolved or an explicit "Proceed anyway" with typed confirmation is provided And the banner updates within 200 ms of any role selection or timebox change
Conflicting Scopes Detection with One-Click Suggestions
Given conflicting permissions exist (e.g., Export without View, Delete without Reconcile) When conflicts are detected Then each conflict shows a plain-language explanation and impact And suggested fixes are provided: remove permission X, add prerequisite Y, or downgrade scope Z And applying a suggestion updates the preview and reduces the conflicts count by at least one without introducing new conflicts And the apply operation returns 200 and completes within 400 ms p95 And an audit log entry is recorded with actor, changes, timestamp, and rationale "least-privilege suggestion applied"
Prerequisite Validation for Dependent Permissions
Given a permission requires prerequisites (e.g., Reconcile requires Categorize+Match) When an assignee lacks the prerequisites Then the system flags the missing prerequisites and ranks resolution options, preferring removal over addition for least-privilege And the UI provides a one-click "Add minimal prerequisites" option showing the exact additions before apply And "View-as-delegate" preview reflects the outcome before finalizing And unit tests assert prerequisite maps cover 100% of defined dependent permissions
Real-Time Validation During Role Assignment
Given the admin adds, removes, or edits a blueprint or timebox When any change occurs Then validation re-executes and updates warnings, conflicts, and summaries without page reload And p95 end-to-end UI update latency is <= 300 ms on a wired connection and <= 600 ms on mobile 4G And the Save button remains disabled while Critical conflicts or missing prerequisites persist And server-side validation runs on save and blocks persistence with HTTP 409 if the client is out of date, showing a non-destructive error message
Plain-Language Effective Access Summary
Given effective permissions are available When the admin opens the Summary or hovers 'view-as-delegate' Then the system generates sentences describing what the delegate can see and do, including resource scopes, date ranges, and excluded actions And no raw ACL codes or internal IDs are displayed And the summary includes at least three concrete examples (e.g., two invoice examples and one bank account scope example) when available And QA snapshot tests verify 1:1 parity between summary statements and the underlying ACL set
One-Click Least-Privilege Auto-Fix
Given overbroad access and/or conflicts exist When the admin clicks "Apply least-privilege" Then the system computes the minimal change set to resolve all conflicts and remove extraneous permissions while preserving required access of the chosen blueprint(s) And presents a before/after diff with counts of permissions removed/added/modified And applies changes atomically with rollback on failure and emits an audit event And completion p95 is <= 500 ms and success confirmation is shown; on failure, no partial changes persist

JIT Access

Just‑in‑time, task‑scoped elevation. Delegates request temporary access for a specific action (e.g., export CSV, edit rules); you approve via push or in‑app, with auto‑revert and audit capture. Minimizes over‑permissioning while keeping work unblocked.

Requirements

Task-Scoped Permission Templates
"As an account owner, I want predefined task-scoped templates so that delegates can request exactly the access they need without broad permissions."
Description

Define a catalog of discrete, least-privilege permission templates mapped to concrete TaxTidy tasks (e.g., Export Transactions CSV, Edit Categorization Rules, Link/Unlink Bank Feed, Manage Delegates). Each template encapsulates the minimal API scopes, UI capabilities, and data boundaries needed for that task. Templates are versioned, testable, and centrally managed; changes propagate to approval flows while preserving historical audit context. Integrates with the authorization layer to enforce scope at request, UI gating to surface request prompts, and the JIT engine to bind an elevation session to one or more templates. Enables faster, safer approvals and reduces over-permissioning across mobile and web.

Acceptance Criteria
Create and Publish Template: Export Transactions CSV
Given I am an org owner or admin, when I create a permission template named "Export Transactions CSV" with required fields (task mapping, API scopes=[transactions.read, exports.generate], UI capabilities=[ExportCSVButton], dataBoundaries=workspace=current), then saving returns HTTP 201 and the template has status="Draft". Given the draft template passes validation, when I publish it, then the template status becomes "Active", a semantic version "1.0.0" is assigned, and a stable templateId is generated. Given the template is Active, when I query the catalog by task="Export Transactions CSV", then exactly one Active template is returned with the specified scopes and capabilities. Given the template is Active, when I attempt to mutate immutable fields (templateId, version, createdAt), then the API returns HTTP 409 with errorCode=IMMUTABLE_FIELD. Given the template defines scopes, when any scope outside the allowed list (e.g., transactions.write) is included, then publish is blocked with HTTP 400 and errorCode=TEMPLATE_SCOPE_TOO_BROAD.
API Scope Enforcement at Request Time
Given a delegate has an approved JIT elevation bound to template "Export Transactions CSV" for workspace=W1, when they call GET /v1/transactions?workspace=W1, then the request returns HTTP 200 and response includes only transactions from W1. Given the same elevation, when they call POST /v1/transactions or PATCH /v1/rules, then the request returns HTTP 403 with errorCode=SCOPE_DENIED and scopeRequired displayed. Given the same elevation, when they call GET /v1/transactions?workspace=W2 (not granted), then the request returns HTTP 403 with errorCode=BOUNDARY_VIOLATION and no data leakage occurs. Given any denied call, when enforcement occurs, then an audit event auth.enforcement.denied is recorded with principalId, templateId, templateVersion, requestedScope, and correlationId.
UI Gating and JIT Prompt Surfacing (Mobile and Web)
Given a delegate without Export permission, when they view the Transactions screen, then the Export CSV button is visible but gated with a lock indicator and tooltip text indicating "Request temporary access". Given the delegate taps the gated Export button, when the JIT prompt opens, then it displays the template name, scope summary, data boundary, requested duration (default <=30 minutes), and requires justification text (min 10 chars) before submission. Given the owner receives the approval request, when they approve via push or in‑app, then within 5 seconds the delegate’s UI enables Export and the Export CSV action completes successfully. Given the elevation expires or the task completes (export job marked finished), when the session ends, then the Export button returns to gated state and the token loses the added scopes. Given both mobile and web clients, when the prompt is shown, then title, scope summary, and duration options are consistent and telemetry event jit.prompt.shown includes clientType in {mobile, web}.
Template Versioning and Change Propagation with Audit Integrity
Given template T v1.0.0 is Active and has N>0 active sessions, when a maintainer publishes T v1.1.0 (narrowed scopes), then all new approval requests reference v1.1.0 while existing active sessions continue using v1.0.0 until expiry. Given an approval created under v1.0.0, when I retrieve its audit record, then it includes templateId=T, templateVersion=1.0.0, and the record remains immutable after v1.1.0 is published. Given v1.0.0 is deprecated, when the last active session for v1.0.0 ends, then the system auto‑archives v1.0.0 (status=Archived) without altering historical audits. Given a breaking change is attempted (scope expansion) for a minor version, when publish is requested, then validation rejects with HTTP 400 and errorCode=SEMVER_INVALID_CHANGE. Given approval UI caches template metadata, when T is updated to v1.1.0, then the approval UI fetches and displays the latest version metadata within 60 seconds.
Elevation Session Binding to Multiple Templates
Given a delegate requests elevation for templates [Export Transactions CSV, Edit Categorization Rules] in a single session, when the owner approves both, then the issued token contains the exact union of scopes from both templates and excludes any other scopes. Given the multi‑template session is active, when the delegate performs an action covered by only one template (e.g., edit rules), then the UI element for that action is enabled while unrelated gated actions remain gated. Given the session is revoked by the owner, when revocation occurs, then all capabilities granted by both templates are immediately disabled in UI and API within 5 seconds, and an audit event jit.session.revoked is recorded with templateIds and versions. Given one template in the request is denied and the other approved, when issuance occurs, then a partial session is created for only the approved template and the approval screen clearly shows final included templates.
Template Validation, Linting, and Policy Tests
Given I submit a template for publish, when validations run, then missing mandatory fields, invalid dataBoundaries, or unrecognized API scopes cause HTTP 400 with structured errors per field. Given the template includes scopes exceeding least‑privilege heuristics (e.g., wildcard scope), when linting runs, then publish is blocked with errorCode=LINT_LEAST_PRIVILEGE and a suggested narrower alternative is returned. Given I run policy tests for a template, when executing provided test vectors (allowed and denied API calls), then all allows pass with HTTP 2xx and all denies return HTTP 403 with correct error codes. Given a dry‑run UI coverage check, when executed, then at least one UI capability is mapped to a live UI element in both mobile and web; otherwise publish is blocked with errorCode=UI_CAPABILITY_ORPHANED.
Push and In‑App Approval Flow
"As a delegate, I want to request and receive approval for a specific task from within my workflow so that I can stay unblocked without waiting for broad access."
Description

Provide a just-in-time approval workflow triggered when a blocked action is attempted. The requester sees a contextual modal prefilled with the task, scope, and suggested duration; upon submission, approvers receive push notifications (iOS/Android) and in-app alerts with one-tap Approve/Deny. Approval views display requester identity, task template, requested duration, reason, and risk indicators, and allow editing duration before approval. Supports fallback email magic links, rate limiting, and deduplication for repeated requests. Integrates with the notification service, device token registry, and the authorization service to mint a short-lived elevation token upon approval.

Acceptance Criteria
Contextual JIT Request Modal on Blocked Action
Given an authenticated delegate attempts an action they lack permission for When the system intercepts the blocked action Then a modal appears within 500 ms prefilled with task template, resource scope, suggested duration, and a reason input And risk indicators are displayed based on current scope and policy And submitting requires a non-empty reason (10–500 chars) and a duration within policy bounds And Cancel closes the modal and creates no request And Submit creates a JIT request with status "Pending" and displays a confirmation to the requester
Approver Push and In‑App One‑Tap Decision
Given a new pending JIT request targets an approver When the request is created Then the approver receives a push notification on all registered iOS/Android devices within 5 seconds And an in-app alert is shown on next app foreground within 1 second And opening the approval view shows requester identity, task template, requested duration, reason, risk indicators, and Approve/Deny actions And Deny allows an optional comment (0–300 chars) And a single tap confirms the decision subject to device biometric/lockscreen settings And duplicate notifications for the same request are suppressed per approver
Duration Edit and Policy Enforcement at Approval
Given an approver opens the approval view for a pending request When they edit the requested duration Then the UI enforces min/max policy (e.g., 5–120 minutes) and step increments (e.g., 5 minutes) And risk indicators update immediately to reflect the edited duration And attempts to exceed policy bounds are blocked with a visible error state And on Approve the edited duration is persisted and used for token expiry
Elevation Token Minting, Scope, and Auto‑Revert
Given an approver approves a JIT request When the approval is submitted Then the authorization service mints a short‑lived elevation token with unique ID, exact approved scope, and expiry = now + approved duration And the token becomes usable by the requester within 2 seconds And elevated permissions auto‑revert when the token expires and further attempts are denied And approvers or admins can revoke the token prior to expiry, immediately removing elevation
Fallback Email Magic Link When Push Is Unavailable
Given a pending JIT request exists and push delivery fails to all devices or no devices are registered When delivery failure is detected within 5 seconds of request creation Then a fallback email with a signed, single‑use magic link is sent to the approver within 60 seconds And the link opens the approval view with identical details and actions And the link expires after 15 minutes or upon first use And identity checks prevent use by non‑recipients
Rate Limiting and Deduplication of Repeat Requests
Given a requester submits multiple JIT requests for the same task and resource within a 2‑minute window When an identical pending request already exists Then the system returns "Request already pending" and does not create a new request And approver notifications are not re‑sent for deduplicated attempts And the system rate‑limits to at most 3 unique JIT requests per requester per 10 minutes, returning 429 and a UI message on exceed And dedup and rate‑limit events are logged with requester ID, task, resource, and timestamps
Audit Trail Capture and Retrieval
Given any JIT request lifecycle event (create, approve, deny, expire, revoke) occurs When the event is processed Then an immutable audit record is stored with request ID, requester ID, approver ID, timestamps, task template, resource scope, requested/approved duration, reason, risk indicators/score, delivery channels used, decision comment, and token ID (if minted) And authorized users can retrieve audit records via UI and API in ≤2 seconds And records are retained for 7 years and can be exported as JSON and CSV And all timestamps are recorded in UTC with millisecond precision
Timeboxed Auto‑Revert and Completion Hooks
"As a security-conscious owner, I want elevated access to auto-revert after a short window or once the task is done so that my account isn’t left overexposed."
Description

Automatically revoke elevated access when the approved duration expires or when the system detects task completion. Bind elevations to a session-scoped token and enforce timeouts via server-side watchdogs; implement app and backend completion hooks (e.g., export finished, rules saved) to proactively end elevation early. Support heartbeats for long-running operations, background/foreground transitions on mobile, and guaranteed revocation on app crash or network loss. Integrates with task templates to determine completion signals and with the authorization layer to update active scopes in real time.

Acceptance Criteria
Auto-Revert on Duration Expiry (Server-Enforced)
Given a delegate is granted JIT elevation for 10 minutes at 12:00:00 UTC via session token T When the time reaches 12:10:00 UTC Then the server revokes the elevation within 5 seconds regardless of client state And subsequent privileged API requests using token T receive 403 scope_expired And the authorization layer no longer lists the elevated scope for the account/session
Early Revocation on Task Completion Hooks
Given an elevation for the "Export CSV" task with a registered completion hook When the backend marks the export job status as succeeded for the requesting session Then the server revokes the elevation within 3 seconds And subsequent privileged API requests from that session are denied with 403 scope_expired And the UI hides privileged controls associated with the elevation within 5 seconds
Session-Scoped Token Binding and Invalidation
Given an elevation is bound to session token T and session ID S When the user logs out or a new session token T2 is issued for the account Then token T is invalidated within 1 second And no other session (not S) can use the elevated scope And any API request using token T receives 401 invalid_session
Heartbeat Maintenance for Long-Running Operations
Given a long-running task elevation with a 30-second heartbeat interval and a 90-second max idle threshold When the client sends a heartbeat every 30 seconds or less Then the elevation remains active until task completion or the approved duration cap When no heartbeat is received for more than 90 seconds Then the server revokes the elevation within 5 seconds and marks the reason as heartbeat_timeout
Mobile Background/Crash/Network Loss Revocation Guarantee
Given an active elevation on a mobile client When the app remains in background for more than 15 seconds or loses network connectivity for more than 30 seconds or crashes/force-quits Then heartbeats cease and the server revokes the elevation within 5 seconds of the threshold being crossed And on next foreground or relaunch, the client displays no elevated state and privileged API requests are denied with 403 scope_expired
Task Template–Driven Completion Signals
Given a task template "Edit Rules" defines completion signal "rule_saved" for the current session When the system receives the "rule_saved" signal for that session Then the elevation is revoked within 3 seconds And if no completion signal is received, the elevation persists only until the approved duration expires
Real-Time Authorization Scope Updates on Grant and Revoke
Given an elevation is granted for scope rules:write at 12:00:00 UTC Then the authorization layer reflects the active scope across all nodes within 2 seconds When the elevation is revoked due to completion or expiry Then the authorization layer removes the scope within 2 seconds And API requests started after revocation receive 403 scope_expired
Immutable Audit Trail and Evidence Export
"As a compliance reviewer, I want a complete, exportable audit of who requested and approved access and what was done so that I can validate controls and investigate issues."
Description

Capture an immutable, append-only audit record for every JIT event: request details (task, reason, requested duration), requester and approver identities, timestamps, device/IP, approval outcome, final duration, and actions performed during elevation. Provide searchable in-app views, retention controls, and export options (CSV/JSON/PDF) for compliance and incident review. Link audit entries to related TaxTidy artifacts (exports, rule changes, bank connections) and include evidence attachments where applicable. Ensures transparency and regulatory readiness without exposing sensitive content inappropriately.

Acceptance Criteria
Complete JIT Event Capture
Given a delegate submits a JIT access request for a specific task with a reason and requested duration from a device with a public IP And an approver reviews and approves or denies the request When the request lifecycle completes (request, decision, elevation start, elevation end) Then an audit entry is appended for each lifecycle step with non-null fields: request ID, task, reason, requested duration, requester ID, approver ID (if applicable), decision outcome, timestamps for each step, requester device fingerprint, requester IP, approver IP, final elevation duration, and a list of actions performed during elevation (action type, target artifact ID, timestamp) And each action performed is time-bounded to the elevation window and linked to related TaxTidy artifacts (e.g., export IDs, rule change IDs, bank connection IDs) And the audit entries can be retrieved via in-app view and API by ID with all fields matching the source events.
Immutable, Append-Only Audit Log with Tamper Evidence
Given an existing audit entry in storage When any actor or process attempts to update or delete the entry before its retention expiry Then the operation is blocked and a new tamper-attempt audit event is appended with actor identity, method, timestamp, and target entry ID And the audit store maintains a hash chain where each entry includes its own hash and the previous entry’s hash And a daily integrity anchor (e.g., Merkle root) is computed and stored in a separate, read-only index And running the integrity verification job over a date range returns 100% verification success for unmodified data And after the configured retention period expires, only the system purge job may remove entries, appending a retention-expiry event that preserves the integrity index for the covered period.
Searchable In-App Audit Views with Role-Based Access
Given audit data exists across multiple tenants with at least 100k events per tenant When an Auditor views the Audit screen and filters by date range, user, task type, decision outcome, IP/device, artifact type, and keyword Then results return the correct subset, sorted by newest first, with pagination (default 25 per page) and downloadable row-level detail And 95th percentile response time is ≤ 1.5s for queries returning ≤ 200 rows within a 100k-event tenant And a Delegate can view only their own JIT events and actions; sensitive fields (e.g., access tokens, full account numbers, exact device fingerprint) are masked or omitted for non-Auditor roles And unauthorized users cannot access the Audit views or API (HTTP 403), and access attempts are themselves audited.
Evidence Attachments with Redaction and Secure Storage
Given an audit event supports evidence attachments When an authorized user (Auditor or Admin) uploads attachments of types PDF, PNG, JPG, CSV, JSON, or TXT up to 20 MB per file and max 10 files per event Then the files are virus-scanned, stored encrypted at rest, and linked to the audit event with filename, size, content type, checksum, and upload timestamp And PII/secret patterns (e.g., SSNs, full bank account numbers, API tokens) are automatically redacted or masked in preview and export unless the user has Sensitive-Data permission and explicitly opts to include unredacted data And Delegates can view/download only attachments they uploaded; Auditors/Admins can view/download all; all downloads are audited And attempting to attach a disallowed type or oversize file is blocked with a clear error and audited.
Exports (CSV/JSON/PDF) with Checksums and Export Event Auditing
Given an Auditor selects a date range and filters and chooses an export format (CSV, JSON, or PDF) When the export job runs Then the generated package includes: the selected fields, resolved links to related artifact IDs, an optional evidence bundle (if selected and permitted), and a manifest.json containing row count, filters, generation timestamp, and SHA-256 checksums for each file and the overall package And exports > 10,000 events are processed asynchronously and complete within 10 minutes p95 with in-app notification and download link And export requests, completions, failures, and downloads are each recorded as audit events with requester ID, IP, filters, and file identifiers And sensitive fields remain masked in exports unless the requester has Sensitive-Data permission and explicitly includes them; masked fields are labeled accordingly in headers.
Retention Controls, WORM, and Legal Hold Enforcement
Given a Workspace Admin configures an audit retention policy (e.g., 7 years) and optionally places a legal hold on a date range or case tag When new audit entries are written Then they are immutable (WORM) until the later of retention expiry or legal hold release And the purge job runs daily, permanently removing only entries whose retention has expired and are not on legal hold, and appends a retention-expiry event with counts and range purged And attempts to shorten retention below existing commitments or to remove legal hold without authorization are blocked and audited And exports and searches do not include purged entries, while integrity anchors for historical periods remain verifiable.
Policy Controls and Risk Gates
"As an admin, I want to define policies and risk checks for JIT approvals so that sensitive actions require stronger controls without slowing routine work."
Description

Offer configurable policies that govern who can request/approve which task templates, maximum durations, business-hour windows, geo/IP restrictions, and mandatory MFA re-check at approval time. Support dual-approval for high-risk tasks (e.g., unlink bank feed, delete data) and automatic denial conditions (e.g., expired delegate agreement). Provide default policy presets per plan tier, with an admin UI for overrides and a rules engine for evaluation. Integrates with identity/MFA providers and the approval flow to enforce risk-based checks inline.

Acceptance Criteria
Role-Based Policy per Task Template with Plan Preset and Admin Override
Given a workspace on the Pro plan with default preset "Pro-Standard" for task template "Export CSV" and an admin override published as policy version v2 adding role "Manager" to approver roles When a user with role "Delegate" submits a request for "Export CSV" and a user with role "Manager" attempts to approve Then the approval succeeds and the effective approver roles include "Manager" per policy v2 And the decision audit record stores policy_id, policy_version=v2, matched_rule="task_template:export_csv.roles", requester_id, approver_id, and timestamp When a user with role "Contributor" attempts to request "Export CSV" Then the request is blocked with reason="insufficient_requester_role" and status remains "pending" And an audit event is recorded with decision="deny" and matched_rule="task_template:export_csv.roles"
Maximum Duration Enforcement and Auto-Revert
Given policy for task template "Edit Rules" sets max_duration=30m When a requester submits a JIT request for 45m Then the request is rejected with error_code="duration_exceeds_policy" and no approval is solicited When a requester submits a JIT request for 25m and it is approved Then elevated access is granted immediately and automatically revoked at T+25m And any post-revocation action attempts receive error_code="elevation_expired" And audit log contains start_time, end_time, revocation_event_id, and revocation_reason="duration_elapsed"
Business-Hour Window Enforcement (Org Timezone, DST-Aware)
Given workspace timezone is America/Los_Angeles and policy business_hours=Mon–Fri 09:00–18:00 When a valid request is submitted Tuesday at 07:30 local time Then the request is denied with reason="outside_business_hours" When a valid request is submitted Tuesday at 09:15 local time Then the request proceeds to approval as normal When a request is submitted during DST transition hour Then evaluation uses local wall-clock time for America/Los_Angeles and produces consistent allow/deny outcomes per the defined window And all decisions include evaluated_timezone, evaluated_local_time, and business_hours_window in the audit record
Geo/IP Restriction with Allowlist and Precedence Rules
Given policy allows countries=[US, CA], blocks countries=[CN], and allowlists CIDR 203.0.113.0/24 with precedence allowlist_cidr > country rules When a requester’s source IP geolocates to GB and is not in any allowlisted CIDR Then the request is denied with reason="geo_restricted" and country_code=GB in audit When a requester’s source IP is 203.0.113.5 (in the allowlisted CIDR) but geolocation resolves to CN or is unknown Then the request is allowed to proceed to approval due to CIDR precedence and audit records precedence_rule="allowlist_cidr" When headers include X-Forwarded-For but the trusted proxy list is configured Then evaluation uses the trusted client_ip and ignores untrusted headers, and decisions reflect the resolved client_ip in audit
Mandatory MFA Re-Check at Approval (Fail-Closed)
Given policy requires MFA step-up at approval with provider="Okta" and max_session_age=5m When an approver’s last MFA is older than 5m and they attempt to approve Then the system prompts for MFA re-check and blocks approval until completed When the approver successfully completes MFA within 60s Then the approval proceeds and audit includes mfa_provider="Okta", mfa_outcome="success", and mfa_timestamp When the MFA provider is unreachable or the approver fails MFA Then the approval is denied (fail-closed) with error_code in {"mfa_unavailable","mfa_failed"} and the request remains pending for other approvers if applicable
Dual-Approval Gate for High-Risk Task Template
Given task template "Unlink Bank Feed" is marked high_risk=true and policy requires approvals=2 from roles in {Owner, Admin} within approval_window=2h with distinct_approvers=true When Owner A approves at T0 and Admin B approves at T0+90m Then elevation is granted and start_time=T0+approval2_timestamp is recorded When Owner A attempts to provide both approvals Then the second approval is rejected with reason="duplicate_approver" When the second approval occurs after 2h from T0 Then the request expires with status="denied" and error_code="approval_window_elapsed" And audit captures approver_ids, approval_timestamps, distinct_approvers=true, and policy_version applied at final approval
Automatic Denial on Expired Delegate Agreement
Given a delegate’s agreement_status=expired and policy includes auto_denial_conditions=["agreement_expired"] When the delegate submits any JIT request Then the request is immediately denied prior to approval routing with error_code="agreement_expired" When the delegate re-attests and agreement_status=active and resubmits the same request Then normal policy evaluation resumes without auto-denial And audit entries include condition_key="agreement_expired", evaluation_stage="pre-approval", and decision="deny" for the first attempt
Contextual Request UX and Notifications
"As a requester, I want a simple, contextual way to ask for temporary access and track the decision so that I can continue my task with minimal friction."
Description

Deliver a streamlined requester experience with auto-populated context (screen, record, action), optional reason text, and attachments (e.g., screenshot). Show live request status, allow cancelation, and group notifications to reduce spam. Approvers get clear, concise notifications with deep links to approve, snooze, or request more info. Provide accessibility-compliant UI across mobile and web and localized copy. Integrates with the notification pipeline, analytics for UX metrics, and the audit system to capture message delivery and response timing.

Acceptance Criteria
Auto-Populated Context on Restricted Action
Given a delegate initiates a restricted action (e.g., Export CSV) on a specific record in TaxTidy (web or mobile) When the JIT request modal opens Then the modal auto-populates screen name, record identifier, action name, and proposed access scope/duration And the auto-populated fields are read-only and included in the submitted payload And the request can be submitted without entering a reason or attachments And if context retrieval fails, an error message appears and a retry action is available; submission is blocked until context is present
Optional Reason and Attachment Capture
Given the JIT request modal is open When the requester adds an optional reason Then the field accepts 0–500 characters with a live counter and prevents submission only if over the limit And pasted text is sanitized for XSS before submission When the requester attaches files Then up to 3 attachments are allowed, each ≤10 MB, types: png, jpg, jpeg, pdf, heic And previews (or file chips) are shown with remove controls and upload progress And failed uploads show inline error and allow retry; failed files are excluded from submission And submitted requests include attachment metadata (name, size, type, storage key) and are referenced in the audit record
Live Request Status and Cancellation
Given a JIT request is submitted When the requester views the request banner/thread Then status updates (pending, approved, denied, expired, canceled, info_requested) appear within 2s via realtime; fallback polling occurs every 10s And a timestamp of last state change is displayed And a countdown to expiry is shown when approved with a duration When status is pending Then a Cancel control is visible and enabled; invoking it sets status to canceled, notifies approvers, and prevents elevation even if a late approval arrives And all status changes are accessible via ARIA live regions and logged to audit with request_id
Requester Notification Grouping and Throttling
Given multiple state changes occur for the same request within a short period When notifications are sent to the requester Then they are grouped into a single in-app thread and at most one push and one email per 10-minute window unless the state changes class (e.g., pending→approved) And no more than 3 notifications per hour per user per request are sent across channels And notification preferences (channel on/off) are honored per user And delivered/opened events are captured per message and correlated to request_id
Approver Notifications with Deep Links and Actions
Given a JIT request is created When approvers are notified (push, in-app, email) Then the notification includes requester name, action, record identifier, reason snippet (≤120 chars), attachment count, and SLA hint And the notification provides actions: Approve, Snooze (15m, 1h, end-of-day), Request More Info And deep links open the approval screen with the request preloaded; if the app is unavailable, the link opens a responsive web view When an approver takes an action from the notification or approval screen Then the action is executed, the requester status updates within 2s, and the audit captures action type, actor, channel, and timestamp And push actionable notifications are offered where OS supports them
Accessibility Compliance Across Web and Mobile
Given users navigate the request and notification UIs When using keyboard-only, screen readers, or high-contrast modes Then all interactive elements are reachable in a logical tab order with visible focus, have programmatic names/roles/states, and meet WCAG 2.2 AA contrast And dynamic status updates use ARIA live (polite) without trapping focus And the UI supports system text scaling up to 200% without clipping or overlap And all imagery/actions have non-color cues and accessible labels When language is set to en-US or es-ES Then all strings, dates/times, and numbers are localized appropriately with graceful fallback to en-US
Analytics and Audit Integration for UX and Notifications
Given users interact with the JIT request and notifications When key steps occur (view_request_modal, submit_request, cancel_request, notification_delivered, notification_opened, approve, deny, snooze, request_info, decision_reverted) Then analytics events are emitted with schema: {event_name, request_id, user_id, role, channel, device_type, timestamp, locale} And 95th percentile latency from client to analytics collector is <3s And the audit system records message delivery timestamps per channel, first-open time, and time-to-decision, immutable and queryable by request_id And analytics redact PII from reason text and attachment contents; audit retains full context under access controls And data retention meets policy (≥1 year)

Selective Redaction

Mask balances, account numbers, client rates, and PII by role and context. Redaction applies across UI, exports, and notifications, with granular toggles per workspace. A “View As” switch lets owners verify what delegates can see, building trust without exposing sensitive data.

Requirements

Role-Based Redaction Policies
"As an owner, I want to control which sensitive fields each role can see so that I can collaborate without exposing private financial details."
Description

Define a role-to-visibility policy matrix (e.g., Owner, Admin, Delegate, Accountant, Viewer) that governs access to sensitive categories such as balances, account numbers, client rates, and PII. Enforce these policies consistently across UI, API, exports, and notifications. Provide secure defaults with per-workspace overrides, support inheritance and conflict resolution (deny-overrides-allow), and integrate with existing RBAC and permission checks. Include policy versioning and change logging for accountability.

Acceptance Criteria
Secure default redaction on workspace creation
Given a new workspace with no policy overrides When a non-Owner user (Admin, Accountant, Delegate, Viewer) requests any resource via UI, API, export, or notification that contains balances, account numbers, client rates, or PII Then those fields are redacted using the system redaction placeholder consistently across all channels And an Owner requesting the same resources sees the unredacted values And an audit log entry records the default policy version applied to the workspace
Per-workspace override for Accountant visibility
Given a workspace owner creates an override that allows the Accountant role to view balances and account numbers but denies client rates and PII When a user with the Accountant role accesses UI, API, exports, and receives notifications Then balances and account numbers are unredacted, while client rates and PII are redacted And the effective policy version and override ID are recorded in the audit log And a user with any other non-Owner role remains subject to the default deny policy for all sensitive categories
Deny-overrides-allow conflict resolution with inheritance
Given an organization-level policy allows Delegates to view balances, and a workspace-level policy denies Delegates from viewing balances When a Delegate accesses balances in that workspace via any channel Then balances are redacted in that workspace for the Delegate And the policy evaluation decision is logged showing deny-overrides-allow resolution with references to both policies
Cross-channel consistency for redaction outputs
Given a user with Viewer role triggers an invoice PDF export, views the invoice in the UI, calls the invoices API, and receives an invoice-paid email notification When the invoice contains client rates and PII Then client rates and PII are identically redacted across PDF, UI, API payload, and email content using the same placeholder And no sensitive values appear in filenames, email subjects, attachment metadata, search indexes, or logs
RBAC integration prevents privilege escalation
Given a user lacks permission to access bank accounts but has a role policy allowing viewing balances When the user requests the bank accounts API endpoint or navigates to the bank accounts UI Then access is denied (HTTP 403 for API; Not authorized UI state) regardless of the redaction policy And for a user who has the required permission but is denied by policy, the endpoint/UI respond successfully with balances redacted
Policy versioning, change logging, and rollback
Given an Owner updates the workspace redaction policy and saves a new version When the change is saved Then a new immutable policy version is created with actor, timestamp, diff, and workspace ID recorded in the audit log And subsequent requests enforce the new policy immediately And when the Owner rolls back to a prior version, that prior version becomes the active policy and the rollback event is logged
Context-Aware Field Masking Engine
"As a delegate user, I want sensitive values automatically masked in views I access so that I can work without accidental exposure."
Description

Implement a masking engine that identifies and redacts fields by context: document type (invoice, bank feed, receipt photo), field semantics (balances, account digits, client rate lines), and rendering surface (screen, export, notification). Support partial masking (e.g., last-4), format-preserving patterns, and irreversible removal in exports. Ensure localization-aware masking (currencies, date formats) and apply masks to OCR text layers and thumbnails. Target low-latency rendering (<50 ms added per view) and integrate with data model annotations and OCR pipelines.

Acceptance Criteria
Workspace-Level Redaction Toggles by Field and Surface
Given I am a workspace owner on the Redaction Settings screen And the workspace contains invoices, bank feeds, and receipt photos When I enable masking for Account Numbers on UI, Exports, and Notifications And I enable masking for Balances on Exports only Then account numbers are masked across all invoice, bank feed, and receipt views, exports, and notifications within 5 seconds And balances remain visible in the UI but are masked in all generated exports And the settings persist after reload and apply to all members except owners
Delegate Visibility Enforcement and Owner "View As" Verification
Given redaction toggles are enabled for Account Numbers, Balances, Client Rates, and PII across all surfaces And a delegate user (non-owner) is assigned to the workspace When the delegate opens any invoice, bank feed entry, or receipt photo Then masked fields render as format-preserving placeholders (e.g., ****1234, ———) and are not copyable or selectable And when the delegate receives an email or push notification related to the item, no masked values appear in subject, body, or payload And when the delegate downloads an export, masked fields are removed or replaced per policy And when an owner uses "View As: Delegate", the view exactly matches the delegate’s masked view
Partial Masking with Format-Preserving Patterns
Rule: Account numbers are masked to last-4 with separators preserved (e.g., 12-3456789-00 -> **-******-00), with at least 50% of digits replaced Rule: Card numbers display only last-4 (e.g., **** **** **** 1234) and are not Luhn-valid in masked form Rule: Client rate lines mask numeric amounts but retain units and labels (e.g., "Rate: $120/hr" -> "Rate: $—/hr") Rule: Email addresses and phone numbers mask local parts and middle digits respectively while preserving valid formats Rule: Masked placeholders are consistent across UI, exports, and notifications
Irreversible Redaction in All Exported Artifacts
Given a PDF, CSV, and ZIP tax packet export is generated When redaction is enabled for a field Then the exported files contain no recoverable original values in text, embedded images, hidden layers, annotations, metadata, or alt text And OCR text layers are sanitized so full-text search on the export cannot find the original values And copying from the export yields only masked placeholders And a forensic diff of masked vs unmasked exports confirms original values are absent
Localization-Aware Masking for Currency and Dates
Given the workspace locale is set (e.g., en-US, fr-FR) When masking balances and dates in UI, thumbnails, and exports Then currency symbols and grouping/decimal separators follow the locale while digits are masked (e.g., fr-FR "1 234,56 " -> " , ") And date formats preserve structure per locale while digits are masked (e.g., en-US "09/08/2025" -> "••/••/••••") And numeric alignment and column widths remain unchanged in tables And accessibility labels convey "masked value" in the selected locale
OCR Layer and Thumbnail Redaction on Receipt Photos
Given a receipt image has OCR-detected totals, dates, and card digits When the image is displayed full-size, with text overlays, or as a thumbnail Then opaque masks cover sensitive regions in the bitmap And OCR tokens for sensitive fields are replaced with masked tokens And regenerated thumbnails include the masks with no unmasked pixels of sensitive regions visible at any size And text search returns no results for masked OCR tokens for non-owner roles
Low-Latency Rendering and Pipeline Integration
Given masking is enabled and documents are annotated with field semantics from the data model and OCR pipeline When rendering any invoice, bank feed, or receipt view on supported devices Then masking adds <50 ms p95 and <75 ms p99 to render time per view And average CPU overhead for masking is <5% on mid-tier mobile devices And rules are applied via data model annotations and OCR outputs without per-view hardcoding And on engine error, masking fails closed (sensitive fields default to masked) and emits an error event
Workspace-Level Redaction Controls
"As a workspace admin, I want granular toggles for what gets redacted so that policies match each client’s compliance needs."
Description

Provide a settings UI to configure redaction categories per workspace with presets (Strict, Standard, Custom) and a per-role matrix editor. Include granular toggles for balances, account numbers, client rates, and PII subtypes, with bulk-apply to sub-workspaces. Offer a test preview within settings, policy version history, effective-date scheduling, and REST APIs for automation. Ensure mobile-friendly configuration and accessibility compliance.

Acceptance Criteria
Workspace Redaction Settings UI Core
- The settings UI exposes three presets: Strict, Standard, and Custom. - Selecting Strict sets all categories (balances, account numbers, client rates, and all PII subtypes) to redacted for all non-owner roles and locks matrix cells from editing. - Selecting Standard applies system defaults that redact account numbers and PII subtypes for non-owner roles while leaving other categories per default policy; cells remain view-only until Custom is chosen. - Selecting Custom unlocks the per-role/category matrix and granular toggles for editing. - Granular PII subtype toggles include at minimum: email address, phone number, physical address, and tax ID. - Saving persists the selected preset, matrix values, and granular toggles; a success confirmation appears; reloading the page shows the saved state. - Unsaved changes are indicated and can be discarded or saved explicitly.
Bulk Apply Policies to Sub-Workspaces
- The settings UI provides a Bulk Apply action that lists all sub-workspaces with selectable checkboxes and a Select All option. - Confirming Bulk Apply overwrites each selected sub-workspace's current redaction policy with the parent workspace's current policy. - A summary shows counts of successes and failures; partial failures do not affect successfully updated sub-workspaces. - For any failure, a retriable error with the sub-workspace identifier is displayed/logged. - Each updated sub-workspace records a new policy version noting the source as Bulk Apply and the parent version ID. - No changes occur to unselected sub-workspaces.
In-Settings Redaction Preview by Role and Context
- The settings UI provides a Preview panel allowing selection of role and context (in-app UI, exports, notifications). - The Preview reflects the current draft (unsaved) policy when "Preview Draft" is enabled and the last saved policy when disabled. - Sample views show masked/unmasked fields for balances, account numbers, client rates, and PII subtypes exactly as the selected role/context would see. - A downloadable sample export (e.g., CSV/PDF) matches the previewed redaction state. - No real user data is exposed in previews; only generated sample data is used.
Policy Version History and Restore
- Every Save (UI, API, Bulk Apply) creates a new immutable version with timestamp, author/source, and diff summary. - Version History lists versions in reverse chronological order and supports filtering by source (UI/API/Bulk). - Selecting a version shows its details and effective timeframe. - Restoring a prior version creates a new current version identical to the selected version and does not delete historical versions. - Audit records include who performed restore and from which version ID.
Effective-Date Scheduling and Policy Activation
- Users can schedule a drafted policy to activate at a future date/time in the workspace's timezone. - Scheduled entries are listed with status (Scheduled, Activated, Canceled) and target activation time. - Until activation, the current policy remains effective; previews clearly distinguish Current vs Scheduled. - At the scheduled time, the scheduled policy becomes active automatically and is recorded as a new version with source Scheduled. - Editing or canceling a scheduled policy updates its entry and audit log; only one scheduled activation can be pending per workspace at a time.
REST API for Redaction Policies Automation
- The API exposes endpoints to get/update current policy, list/create versions, schedule activation, and bulk apply to sub-workspaces. - Authentication requires OAuth2 with scopes permitting read/write of redaction policies; unauthorized requests return 401/403. - Write operations support idempotency via Idempotency-Key and optimistic concurrency via ETag/If-Match; conflicting updates return 409. - Validation errors return 422 with field-level messages; successful operations return standard codes (200/201/204) and JSON bodies per OpenAPI spec. - All API-sourced changes appear in Version History and audit logs with source API.
Mobile-Friendly Configuration and Accessibility Compliance
- The settings UI is fully usable on mobile viewports ≥320px wide without horizontal scrolling; controls have minimum 44px touch targets. - The role/category matrix supports responsive interaction (horizontal/vertical scroll as needed) while keeping headers visible (sticky) on mobile. - All interactive elements are operable via keyboard and screen readers; focus order is logical and visible; ARIA roles/labels are present for matrix cells and toggles. - Color contrast meets WCAG 2.1 AA; non-color indicators are provided for state changes; live regions announce save/restore/preview updates. - The preview and bulk actions are accessible and usable on mobile and desktop with equivalent functionality.
View-As Access Simulator
"As an owner, I want a View As switch to verify what delegates can see so that I can build trust and catch misconfigurations."
Description

Add a safe impersonation simulator that renders the application exactly as a selected role or specific user would see it, without granting live access. Provide a toggle in the header and mobile navigation, highlight masked areas, and show rule provenance for transparency. Generate shareable snapshots for verification. Prevent data mutation while in simulation mode and log all simulations for audit purposes.

Acceptance Criteria
View-As Toggle Access and Session Controls
- Given I am a workspace Owner or Admin with the "Use View-As" permission, when I open the desktop header or mobile navigation, then I see a "View As" toggle. - Given I lack the permission, when I open the header or mobile navigation, then I do not see the "View As" toggle. - Given I click the "View As" toggle, when I choose a target Role or search-and-select a specific active User from the current workspace and confirm, then the app enters simulation mode and displays a persistent banner indicating "Viewing as [Role/User]" with an "Exit" control. - Given the user search is open, when I type a name or email, then only active members of the current workspace are returned; deactivated or non-members cannot be selected. - Given simulation mode is active, when I navigate between pages, then the banner remains visible and the target remains unchanged until I exit simulation. - Given I sign out or reload the app, when it restarts, then simulation mode is off by default.
Redaction Fidelity and Highlighting in Simulation
- Given simulation mode is active, when I view balances, account numbers, client rates, or PII restricted for the target role/context, then those values are masked consistently across all visible screens. - Given simulation mode is active, when I toggle "Highlight masked areas" (default ON), then all masked elements display a visible overlay indicator without revealing underlying values. - Given simulation mode is active, when I attempt to reveal or copy masked content (select text, copy, print, or inspect), then the redacted values are not present in the rendered DOM, page source, print output, or clipboard. - Given simulation mode is active, when I switch between roles/users, then masks update immediately to reflect the new target without flashing unmasked data.
Simulation-Aware Exports and Notifications
- Given simulation mode is active, when I generate an export (PDF or CSV) or create a share preview, then the output contains only data visible to the simulated role/user and all restricted fields are masked identically to the UI. - Given simulation mode is active, when I trigger any notification or email preview, then no live notifications are sent and only a non-deliverable preview is shown with simulated redactions applied. - Given simulation mode is active, when an export or preview is generated, then it is watermarked "Simulated View" and includes target role/user and timestamp metadata. - Given simulation mode is active, when I attempt to schedule exports or notifications, then scheduling is disabled with a message indicating read-only simulation.
Rule Provenance for Masked Elements
- Given simulation mode is active, when I click/hover a masked element's info icon, then a panel displays the applicable redaction rule name, rule scope (workspace/global), matching condition (role/context), last modified by, and timestamp. - Given a masked element has multiple rules, when I open provenance, then the highest-priority rule is shown first with an ordered list of contributing rules. - Given I am on mobile, when I tap the info icon, then the same provenance details are shown in a bottom sheet. - Given provenance is displayed, when I review it, then no underlying masked values are revealed in the provenance content.
Mutation Safeguards in Simulation
- Given simulation mode is active, when I attempt a write action (create/update/delete records, settings changes, sending messages/exports), then the action is blocked client-side with a banner/toast stating "Read-only: Simulation mode," and no backend write request is sent. - Given simulation mode is active and a write API is called directly, when the server receives the request with the simulation session token, then it returns 403 SimulationMode and no data changes occur. - Given simulation mode is active, when I attempt to upload files or connect integrations, then the action is blocked and logged without side effects. - Given simulation mode is active, when I try to approve payments or modify bank connections, then the controls are disabled and no external providers are contacted.
Shareable Simulation Snapshots
- Given simulation mode is active, when I click "Generate snapshot," then the system creates a static, tamper-evident snapshot (PDF or static HTML) of the current view with redactions applied, watermarked "Simulated View," and includes target role/user, workspace, and timestamp metadata. - Given a snapshot is generated, when I copy a snapshot link, then it is tokenized, read-only, expires within 7 days by default, and cannot be used to navigate beyond the captured view. - Given a recipient opens the snapshot link, when the link is expired or revoked, then access is denied with an expiration message. - Given a snapshot is generated, when I review its contents and metadata, then no unmasked values are present in the file body or embedded metadata.
Simulation Audit Logging
- Given I start a simulation, when the session begins, then an audit log entry is recorded with initiator user, workspace, target role/user, start timestamp, and client IP/user agent. - Given I end a simulation, when the session ends, then the audit log entry is updated with end timestamp and duration. - Given I perform blocked actions or generate snapshots in simulation, when these events occur, then each event is appended to the audit trail with event type and outcome. - Given I am an Owner/Admin, when I view the Audit Log, then I can filter by event type "Simulation" and export results to CSV.
Export & Notification Sanitization Pipeline
"As a user sharing documents externally, I want exports and notifications to respect redaction so that nothing sensitive leaks outside the workspace."
Description

Create a centralized pipeline that applies redaction to all outbound artifacts, including PDF tax packets, CSV/JSON exports, emailed reports, push notifications, and webhooks. Ensure masked content is irrecoverable (e.g., remove text from PDF content streams or rasterize regions), and sanitize filenames, subjects, and message previews. Provide consistent watermarking (e.g., “Redacted”) and a validation suite to catch leakage. Expose configuration to align with workspace policies and log all sanitized outputs.

Acceptance Criteria
PDF Tax Packet Irrecoverable Redaction
Given a tax packet that includes balances, account numbers, client rates, or PII When a PDF export is generated under a policy requiring redaction Then all sensitive text in redacted regions is removed from the PDF content stream or rasterized at ≥300 DPI And copying, searching, or programmatic text extraction returns masked placeholders only And OCR on redacted regions yields no original sensitive values And PDF metadata (Title, Subject, Author, XMP) contains no sensitive values And each page shows a consistent "Redacted" watermark per style guide
CSV/JSON Export Field-Level Sanitization
Given a workspace redaction policy with field masks for balances, account numbers, client rates, and PII When CSV or JSON exports are generated Then all configured fields are masked per policy (e.g., account_number -> last4, client_rate -> "REDACTED") And keys/headers do not include sensitive values And filenames exclude PII and include a "-redacted" marker and artifact ID And row counts and required columns remain unchanged; JSON validates against schema vX.Y And no sensitive values appear in the byte stream (including BOM or control characters)
Outbound Notifications (Email and Push) Sanitization
Email: Given an email report with dynamic content and attachments When the email is queued to any non-owner role Then the subject and preview line contain no sensitive values and include a "[Redacted]" tag And HTML/text bodies replace masked fields per policy And attachments (PDF/CSV) are sanitized and watermarked as applicable And no PII appears in attachment filenames, Content-Disposition, or headers Push: Given a push notification triggered by a sensitive event When delivered to iOS/Android Then title/body previews contain no PII in collapsed or expanded states And notification payload and deeplink parameters exclude PII; server-side fetch uses role-appropriate view
Webhook Payload Sanitization and Integrity
Given workspace webhooks subscribed to events that include sensitive fields When a webhook is dispatched Then the payload is sanitized per policy before signing And the HMAC signature is computed over the sanitized payload bytes And delivery/retry logs store only the sanitized payload and metadata (no raw originals) And replay requests deliver the same sanitized payload versioned with policy_version
Policy and Context Application with View-As
Given workspace-level redaction toggles by role (owner, delegate, accountant) and channel (UI, exports, notifications, webhooks) When an artifact is generated for a specific recipient and channel Then the pipeline applies the correct policy variant based on recipient role, channel, and workspace settings And an owner using "View As [role]" sees exactly the output that role would receive (byte-for-byte for exports; field-for-field for notifications) And attempts to generate unredacted artifacts are allowed only when policy permits and are fully audited
Automated Leakage Detection and Delivery Gate
Given a validation suite with seeded datasets containing representative PII and sensitive patterns When CI/CD runs on builds and pre-send checks run on production-bound artifacts Then any detection of unmasked sensitive patterns fails the build/check and blocks delivery And channel-path test coverage is at least 95% across export types and notification/webhook templates And alerting creates a P1 incident with artifact IDs, policy_version, and detector details
Audit Logging of Sanitized Outputs
Given any outbound artifact processed by the sanitization pipeline When the artifact is delivered or blocked Then an immutable audit record is written with artifact_id, channel, recipient role, policy_version, redaction_counts, actor, timestamp, checksum of sanitized content, and storage URI And no raw pre-sanitized content or unmasked PII is persisted in logs And audit records are retained per workspace retention policy and are queryable within 2 seconds at p95
PII Detection & Rule Templates
"As a consultant, I want automatic detection and suggested redactions so that I don’t have to manually find and mask sensitive info."
Description

Deliver a library of PII detectors (SSN, EIN, routing/account numbers, phone, email, mailing address) combining regex, checksums, and OCR recognition, with ML-assisted suggestions for ambiguous cases. Ship industry templates (e.g., US freelancer, GDPR-friendly) mapping detected fields to redaction categories. Support allowlists, vendor/client exceptions, and configurable confidence thresholds that feed a review queue for human confirmation before enforcing.

Acceptance Criteria
OCR and Text Detection for SSN, EIN, and US Bank Routing/Account Numbers
Given a batch of documents containing SSNs, EINs, US bank routing numbers, and bank account numbers in mixed formats (plaintext PDFs, scanned images, and mobile photos) And OCR is enabled for the workspace When the documents are ingested Then the system extracts candidate strings via OCR and text parsing And validates US bank routing numbers using ABA checksum, rejecting non-conforming numbers And rejects SSNs with invalid prefixes (000, 666, 900–999) or blocks (00, 0000) And identifies bank account numbers as 6–17 digit sequences adjacent to account labels (e.g., "Account", "Acct", "A/C"), excluding sequences labeled as invoice/order IDs And classifies each valid match as SSN, EIN, Routing Number, or Bank Account Number with a confidence score between 0 and 1 And returns for each match the document ID, page number, character offsets (for text) or bounding box coordinates (for OCR), and a masked preview of the value
Detection of Emails, Phone Numbers, and US Mailing Addresses with Normalization
Given documents contain emails, US and international phone numbers, and US postal addresses including ZIP+4 When the detection job runs Then emails are detected in common formats and normalized to lowercase local@domain And phone numbers are detected and normalized to E.164 when country code is present or inferred from workspace locale And US addresses are detected as line blocks containing street, city, state, and ZIP/ZIP+4 and normalized into structured fields And each detection includes entity type, normalized value, confidence score, and location metadata And numeric strings adjacent to labels such as "Invoice", "Order", or "PO" are not classified as phone numbers
Apply Industry Template 'US Freelancer' to Redaction Categories
Given the workspace selects the "US Freelancer" template When template mapping is applied Then SSN, EIN, Routing Number, and Bank Account Number are mapped to redaction category "Financial Identifiers" And Email, Phone, and Mailing Address are mapped to redaction category "Contact PII" And the mapping is persisted in the policy store with a new version ID And the active category mapping is available to the redaction engine via API within 1 second of update
Confidence Threshold Routing to Review Queue and Auto-Enforcement
Given the workspace sets auto-enforce threshold to 0.85 And review threshold to 0.50 When detections are produced Then detections with confidence >= 0.85 are marked enforce=true and skip the review queue And detections with confidence >= 0.50 and < 0.85 are added to the review queue with enforce=false pending human confirmation And detections with confidence < 0.50 are not queued and not enforced And queue items display the highlighted snippet, entity type, confidence, and suggested category And a reviewer can approve or reject a queued item; approval sets enforce=true and rejection sets enforce=false and dismisses the item
Allowlist and Vendor/Client Exceptions
Given a vendor "Acme Bank" and a client "Bluebird Studio" are added to exceptions And the vendor exception allowlists routing and account numbers originating from Acme Bank statements And the client exception allowlists emails from the @bluebirdstudio.com domain When documents from these entities are ingested and detections occur Then detections matching allowlist rules are recorded but marked enforce=false regardless of confidence And detections not matching allowlist rules follow threshold routing and enforcement And an audit log entry captures the allowlist rule that suppressed enforcement
ML-Assisted Suggestions for Ambiguous Numbers
Given a document contains the string "12-3456789" without explicit labels indicating EIN or invoice number When detection runs Then the system proposes multiple classifications (e.g., EIN, Invoice Number) with associated confidences And the highest-confidence suggestion is preselected in the review queue And the reviewer can reclassify or mark as Not PII And the final reviewer decision is enforced if its confidence meets the auto-enforce threshold, otherwise it is stored as feedback only And accepted reviewer decisions are logged for model feedback and appear in the training corpus export
Workspace-Scoped Configuration Isolation and Template Switching
Given two workspaces A and B under the same account And workspace A uses the "US Freelancer" template with enforce threshold 0.85 and custom allowlists And workspace B uses the "GDPR-friendly" template with enforce threshold 0.90 and no allowlists When a change is made to A's thresholds or allowlists Then B's configuration remains unchanged And when switching A from "US Freelancer" to "GDPR-friendly" Then mappings, thresholds, and exceptions update to GDPR-friendly defaults, the previous A configuration is versioned and recoverable, and subsequent detections use the new mapping
Redaction Audit Trail and Override Workflow
"As a compliance-minded owner, I want an audit trail and controlled overrides so that I can meet regulatory requirements and handle exceptions safely."
Description

Maintain immutable logs of redaction events, including which fields were masked, the governing rule, and policy changes with actor and timestamp. Provide an inline request-unmask workflow with owner approval, time-boxed access tokens, and immediate revoke capability. Offer exportable audit reports and alerts on policy updates or redaction failures. Align retention and access to workspace compliance settings.

Acceptance Criteria
Immutable Audit Log for Redaction Events
Given any redaction-related action occurs (mask, unmask, render, export, notification, policy_change, failure) When the action is processed Then an append-only audit entry is written containing event_id (UUIDv4), timestamp (UTC ISO 8601), actor_id or system, workspace_id, resource_type and resource_id, field_names affected, governing_rule_id and rule_version, action_type, outcome (success|failure with code), request_id, previous_entry_hash, and entry_hash (SHA-256) And entries are stored in write-once media or logically write-once storage where updates/deletes are blocked by policy And any attempt to modify or delete an entry is rejected and a tamper_attempt event is logged with actor_id and timestamp And API and UI retrieval of audit entries must return correct hash chain continuity for 100% of fetched ranges
Comprehensive Event Capture Across UI, Exports, and Notifications
Given a redacted field is displayed in the app UI, included in an export, or referenced in a notification When the content is generated Then a corresponding audit entry records the surface (ui|export|notification), template or view_id, and render_result (redacted|unmasked|failure) And on any redaction failure, the user-facing value is masked with a fallback token (e.g., ███) and a failure event with error_code and stack_trace_id is logged And export jobs log per-file checksums and the list of redaction decisions applied to each file And notification sends log recipient roles and channel without exposing PII in the log payload
Inline Unmask Request with Owner Approval and Time-Boxed Token
Given a delegate user views a redacted field they are permitted to request When they click Request Unmask and submit a justification (min 10 characters) Then the system creates an approval task for workspace owners (and compliance_admins if configured) with snapshot of the resource and fields requested And upon approval, the system issues a scoped access token limited to the specific resource_id and field_names with a default TTL of 15 minutes (configurable 1–60 minutes per workspace policy) And the token allows only read access for the approved fields and is single-user, single-device, and single-session bound And every reveal event while the token is active is logged with token_id and view_context And if the request is denied or expires without action, the requester is notified and no token is issued
Immediate Revoke of Unmask Access Tokens
Given an unmask access token is active When an owner or compliance_admin clicks Revoke Access or changes a relevant redaction policy Then the token becomes invalid within 5 seconds across all services and devices And any in-flight or subsequent requests using the token receive HTTP 403 REVOKED and the UI re-masks the fields within 5 seconds And a revoke event is appended to the audit log with actor_id, token_id, and reason And the system confirms revocation success to the revoker and notifies the token holder
Exportable Audit Reports with Filters and Integrity Verification
Given an owner or compliance_admin opens Audit Reports When they apply filters (date range, actor_id, event_type, resource_type/id, rule_id, outcome) and request an export Then the system generates a report in CSV and JSON formats containing all matching entries within retention limits And reports include a signed manifest (JSON) with record_count, time_range, filters, file_checksums (SHA-256), and a signature using the platform signing key And for datasets > 100k rows, the export runs asynchronously and completion is notified in-app and via email with a pre-signed download URL valid for 24 hours And downloaded files reproduce the exact filtered results when re-imported and compared by checksum and record_count
Alerts on Policy Updates and Redaction Failures
Given a redaction policy is created, updated, or deleted, or a redaction failure occurs on any surface When the event is logged Then an alert is sent to owners and compliance_admins via in-app notification immediately and via email within 1 minute And the alert contains who (actor_id), what (event_type and diff for policy changes), when (timestamp), where (workspace_id, resource references), and recommended actions And alerts are deduplicated per event_type and rule_id for a 5-minute window and respect user notification preferences And alert delivery outcomes (delivered|bounced|suppressed) are logged in the audit trail
Retention and Access Controls Aligned to Compliance Settings
Given workspace compliance settings define audit retention (e.g., 3/7/10 years), legal holds, and access roles When retention is updated Then a purge schedule removes entries older than the configured period, excluding any under active legal hold, with purge events logged including counts purged And legal holds can be applied to specific resource_ids, rule_ids, or cases and prevent purge until released, with all changes audited And access to audit logs is restricted to owners and compliance_admins; delegates cannot view or export unless explicitly granted by role policy And all audit data is encrypted at rest and in transit; access attempts are logged with actor_id and outcome

Smart Queues

Rule‑driven approval lanes that batch similar items and auto‑route to the right approver. Auto‑approve low‑risk items under thresholds, highlight anomalies, and allow swipe‑to‑approve on mobile. SLA timers and reminders prevent bottlenecks so month‑end closes faster.

Requirements

Rule Builder & Queue Definitions
"As a freelancer, I want to create simple rules to route and batch my expenses so that I can review similar items together and send edge cases to my accountant."
Description

Provide a configurable rules engine and UI to define Smart Queues using conditions across source type (invoice, bank feed, receipt), merchant, category, amount bands, client/project, and Schedule C tax categories. Support AND/OR logic, ordering, and conflict resolution. For each rule, allow actions such as route to approver (self, delegated accountant), batch similar items, and auto-approve under defined risk thresholds. Include rule versioning, validation, sandbox preview against sample data, and import/export. Integrate with TaxTidy’s transaction/document models and event bus. Log rule evaluations for observability and troubleshooting.

Acceptance Criteria
Create Multi-Condition Rule With AND/OR and Grouping
Given an authenticated user with Rule Builder access When they add conditions across source type, merchant, category, amount band, client/project, and Schedule C category and group them with AND/OR using parentheses Then the UI validates the expression in real time and blocks Save on invalid syntax with an inline error referencing the exact clause within 200 ms And the rule can be saved only when all required fields (name, status, conditions, action) are present Given a valid rule expression When the user saves Then the rule persists with a unique ID, human-readable summary, and normalized JSON definition And the saved definition matches the UI configuration exactly Given a saved rule When previewed against a 5,000-item sample dataset Then matching items are returned in ≤2 seconds at p95 and the match count equals back-end evaluation results Given malformed field values (e.g., non-numeric amount band) When attempting to save Then validation errors specify the field, expected type, and allowed range and the rule is not saved
Rule Ordering and Conflict Resolution (First Match Wins)
Given multiple active rules When an item matches more than one rule Then only the highest-priority rule (top-most in order) applies and subsequent rules are skipped And the evaluation log records all candidate rules and the winning rule ID Given drag-and-drop reordering of rules When the user changes order and publishes Then the new order is persisted and takes effect for new evaluations within ≤5 seconds Given two rules have identical priority on save When saving Then the system enforces a deterministic tie-breaker by creation timestamp (earlier wins) and records the tie-break decision in the audit log Given a preview run When conflicts would occur Then the UI flags the item with “Conflicting rules” and displays the winning and suppressed rule IDs
Route to Approver and Batch Similar Items
Given a rule with action "Route to Approver" (self or delegated accountant) When the rule is active Then matching items are assigned to the specified approver in a queue named after the rule and visible within ≤5 seconds of ingestion Given batching keys are configured as merchant, category, and week When multiple matching items share the same keys Then they are grouped into a single batch with a count and total amount displayed in the queue Given an approver opens the Smart Queue When they approve a batch Then each item in the batch is marked Approved and the audit trail records rule ID, approver ID, timestamp, and batch ID Given an approver is removed or becomes invalid When routing would occur Then routing fails closed to the default approver and an alert is logged
Auto-Approve Under Defined Risk Thresholds
Given a rule action "Auto-Approve" with an amount threshold and allowed merchants/categories When a new item meets all rule conditions and is not flagged as an anomaly Then the item is auto-approved without human action and marked as such in its status Given an auto-approved item Then an immutable audit record includes rule ID, reason string, configured threshold, matched conditions, and risk score Given an item exceeds the threshold or is flagged anomalous When evaluated Then it is not auto-approved and is routed to the default review queue with reason "Threshold/Anomaly" Given a user with permission When they unapprove an auto-approved item Then the reversal is recorded with user ID, timestamp, and prior state
Rule Versioning, Validation, Publish, and Rollback
Given an existing Active rule When a user edits the rule Then a Draft version is created that does not affect the Active version until published Given a Draft passes validation When the user publishes Then it becomes Active with an incremented version number and an event "rule.published" is emitted on the event bus Given a need to revert When the user rolls back to a prior version Then that version becomes Active within ≤5 seconds and the former Active is archived with reason Given a Draft with validation errors When publish is attempted Then publishing is blocked and all field-level errors are shown inline and logged
Import/Export Rules with Schema Validation
Given at least one rule exists When the user exports rules Then a JSON file downloads containing rule definitions, versions, status, and metadata conforming to the documented schema version Given a user uploads a rules file for import When validation succeeds Then new rules are created as Disabled Drafts and existing rules with matching IDs are listed for optional overwrite before applying changes Given duplicate names or IDs in the import file When importing Then the system presents a dry-run summary showing adds, updates, renames, and conflicts and requires user confirmation to proceed Given a malformed import When processed Then the system rejects it and returns line/field-specific errors without creating or modifying any rules
Evaluation Integration and Observability Logging
Given a transaction or document is created or updated and published on the event bus When received by the rules engine Then the item is evaluated within ≤200 ms at p95 and actions (route/auto-approve/batch) are executed accordingly And an event "rule.evaluated" is emitted with correlation ID, winning rule ID (if any), and outcome Given any evaluation Then a persistent log record stores correlation ID, evaluated rule IDs, per-condition outcomes, action taken, and timestamps and is retrievable via UI/API for at least 13 months Given a temporary outage When the system recovers Then queued events are processed exactly once in original order and missing spans are visible in logs with retry counts
Risk Scoring & Auto-Approval Thresholds
"As a solo consultant, I want low-risk expenses to auto-approve so that I spend less time on obvious items and focus on exceptions."
Description

Implement a risk scoring model that combines amount thresholds, merchant trust lists, receipt presence, category confidence, duplicate detection, and historical acceptance rates. Allow configurable auto-approval thresholds per queue and exclusions for sensitive categories (e.g., meals, travel), first-time merchants, and transactions missing required receipts above IRS limits. Execute auto-approvals with full audit logging and provide bulk undo within a defined window. Integrate with anomaly signals and the approval audit trail.

Acceptance Criteria
Deterministic Risk Score Aggregation
Given a transaction with amount, merchant identity, receipt presence, category confidence, duplicate likelihood, and historical acceptance rate available When the risk engine computes the score Then it returns an integer score between 0 and 100 and persists the score with per-signal contributions Given the same transaction inputs are submitted multiple times When the risk engine computes the score Then the score and per-signal contributions are identical each time (deterministic) Given two otherwise identical transactions where only amount is higher in one When the risk engine computes their scores Then the higher-amount transaction’s score is greater than or equal to the lower-amount transaction’s score Given a merchant on the trust list vs. the same merchant not on the trust list (simulated) When the risk engine computes the score Then the trusted variant’s score is strictly lower and the audit shows a merchant-trust contribution reducing risk Given category confidence below a configured threshold and duplicate likelihood above a configured threshold When the risk engine computes the score Then the score includes positive contributions for low confidence and duplicate likelihood, and the audit shows the magnitudes Given a merchant-category pair with a high historical manual-acceptance rate configured When the risk engine computes the score Then the score includes a negative (risk-reducing) contribution sourced from historical acceptance and the audit attributes it to that signal
Per-Queue Auto-Approval Threshold Configuration
Given a Smart Queue with auto-approval threshold T configured and exclusions defined When a transaction with risk score <= T arrives and matches no exclusions Then it is auto-approved within 60 seconds and assigned to the queue’s approved state Given multiple Smart Queues with distinct thresholds When transactions arrive for each queue Then auto-approval decisions respect each queue’s own threshold without cross-queue interference Given an admin updates a queue’s threshold When the change is saved Then the new threshold version is recorded and applied to new transactions within 5 minutes Given a queue with no threshold configured When transactions arrive Then auto-approval is disabled and all items route to manual review
Exclusions: Sensitive Categories, First-Time Merchants, Missing Receipts
Given a transaction whose category is in the configured sensitive list (e.g., meals, travel) When evaluated for auto-approval Then it is never auto-approved regardless of risk score and is routed to manual with reason "SensitiveCategory" Given a transaction from a first-time merchant (no prior approved transactions) When evaluated for auto-approval Then it is never auto-approved and is routed to manual with reason "FirstTimeMerchant" Given a transaction with amount >= configured receipt_required_limit and no receipt attached When evaluated for auto-approval Then it is never auto-approved and is routed to manual with reason "MissingRequiredReceipt" Given a transaction matches any exclusion When routed to manual review Then the approval card displays all triggered exclusion reason codes and they are logged in the audit trail
System Auto-Approval Audit Trail
Given an item is auto-approved by the engine When the approval is recorded Then the audit trail entry includes: approver="Auto-Approval Engine", UTC timestamp, queue ID, rules version, risk score, threshold used, exclusion evaluation results, anomaly indicators, merchant ID, amount and currency, receipt presence, category and confidence, duplicate likelihood, historical acceptance rate snapshot, and the admin who last changed the threshold Given the same item is processed more than once due to retries When audit entries are written Then only one auto-approval audit record exists (idempotent) and duplicates are prevented by a stable event key Given an item is auto-approved When viewing the approval audit trail UI/API Then the audit entry is visible within 30 seconds of approval and is immutable thereafter
Bulk Undo Within Defined Window
Given auto-approved items exist and the undo window is configured (default 120 minutes) When an admin initiates a bulk undo for a selected set within the window Then the items revert to Pending Approval, are removed from downstream exports, and are re-queued to their originating Smart Queues Given a bulk undo is executed When the operation completes Then an audit entry "Auto-Approval Undone" is appended per item with actor, timestamp, reason, and batch ID Given a bulk undo request includes items outside the undo window When the request runs Then those items are skipped with a clear count and reasons, and no changes are made to them Given a bulk undo is retried for the same batch When the operation runs again Then no additional state changes occur to already undone items (idempotent)
Anomaly Signal Integration and Auto-Approval Suppression
Given the anomaly service flags a transaction with severity=high When the risk engine evaluates the item Then auto-approval is suppressed regardless of score and the item routes to manual with reason "AnomalyHigh" Given the anomaly service flags a transaction with severity=medium or low When the risk engine computes the score Then the score is increased by a configured contribution and the audit shows the anomaly signal and added risk Given the anomaly service is unavailable When evaluating items for auto-approval Then the system fails safe by disabling auto-approval for affected items and logs reason "AnomalyServiceUnavailable" Given an item is influenced by any anomaly signal When viewing the audit trail Then the anomaly type, severity, and contribution to the decision are present and human-readable
Anomaly Detection & Highlighting
"As a freelancer, I want unusual transactions highlighted with clear reasons so that I can quickly investigate and avoid mistakes that could create IRS risk."
Description

Detect and surface anomalies such as outlier amounts, unusual frequency, merchant-category mismatches, weekend/holiday spending, duplicate invoices, and mileage inconsistencies. Highlight flagged items within queues with badges and sort order, provide an explanation of why each item was flagged, and suggest next steps (request receipt, re-categorize, split). Sensitivity is tunable per user and respects manual overrides. Works across mobile and web with consistent signals.

Acceptance Criteria
Outlier Amount Detection & Highlighting
Given a user has 12 months of "Software" expenses averaging $50 (SD $10) and a new transaction of $250 And the user's anomaly sensitivity is set to Medium When the Smart Queue loads on both web and mobile Then the $250 transaction is flagged with type "Outlier Amount" and shows an "Anomaly" badge And the flagged item appears above non-flagged items in the queue And an explanation is visible stating "Amount $250 vs typical $50 ± $10 (z=20.0)" And suggestions include "Request receipt", "Split", and "Re-categorize" And the badge label, color, and icon are identical on web and mobile
Unusual Frequency & Weekend/Holiday Spend Flagging
Given vendor "StockArt" appears ≤1 time per 30 days in this user's history And four new charges from "StockArt" occur within 7 days, two on a Saturday and one on a federal holiday When the items are queued Then each of the four items is flagged with reasons including "Unusual frequency" and, where applicable, "Weekend/Holiday spend" And the flagged items sort above non-flagged items And the explanation lists each reason with supporting counts/dates And suggestions include "Request receipt" and "Re-categorize" And web and mobile show matching badges and sort order
Duplicate Invoice Detection and Actions
Given an invoice with number INV-8421 amount $1,200 date 2025-08-03 And a second invoice with number INV-8421 amount $1,200 date 2025-08-03 from the same merchant When both are ingested Then both records are flagged as "Possible duplicate" And the explanation cites "Same invoice number and amount/date match" And the duplicate items are sorted adjacent at the top of the queue And suggestions include "Merge", "Ignore duplicate", and "Request receipt" And mobile and web present the same duplicate indicator and actions
Merchant-Category Mismatch Flag
Given a transaction from merchant "Delta Airlines" is auto-categorized as "Software" When the queue loads Then the transaction is flagged "Merchant-Category Mismatch" And the explanation shows "MCC 4511 (Airlines) uncommon for category Software" And suggestions include "Re-categorize" with top 3 suggested categories And the flagged item sorts above non-flagged items with consistent badge on web and mobile
Mileage Inconsistencies Detection
Given a mileage claim of 120 miles for a trip between 123 A St and 456 B Ave on 2025-07-14 And the mapped distance via fastest route is between 8 and 12 miles When the item is queued Then the mileage item is flagged "Mileage inconsistency" And the explanation shows "Claimed 120 mi vs expected 10 mi (±2 mi tolerance)" And suggestions include "Edit miles" and "Split trip" And mobile and web show the same flag and ordering
User-Tunable Sensitivity Impacts Flags
Given a dataset of 100 "Meals" expenses with amounts clustered between $10 and $30 And a new expense of $45 is imported When the user sets anomaly sensitivity to High and reloads the queue Then the $45 expense is flagged as an outlier When the user sets sensitivity to Medium and reloads the queue Then the $45 expense is flagged as an outlier When the user sets sensitivity to Low and reloads the queue Then the $45 expense is not flagged And changes in flags reflect within 5 seconds of saving the setting And the current sensitivity setting is displayed in the explanation for flagged items
Manual Override Persistence and Non-Reflagging
Given an item is flagged "Merchant-Category Mismatch" When the user selects "Mark as OK" or re-categorizes the item and refreshes the queue Then the flag is removed immediately and the item moves to normal sort position And the override persists and the item remains unflagged on subsequent syncs and model retraining And the explanation displays "Manually overridden by <user> on <date/time>" And the same override status is visible on web and mobile
Mobile Swipe-to-Approve UX
"As a mobile-first freelancer, I want to swipe through queued items and approve them quickly so that I can stay current without opening my laptop."
Description

Deliver an iOS/Android interface optimized for rapid approval: swipe gestures (approve, send to accountant, reclassify, request receipt, snooze), batch multi-select, inline receipt thumbnails, and rule/risk badges. Support offline action queuing with eventual sync and conflict resolution. Provide accessibility (VoiceOver/TalkBack), haptics, and an undo snackbar. Deep link from notifications into the specific queue or item. Display SLA countdowns at item and queue level.

Acceptance Criteria
Swipe Actions with Undo and Haptics
Given I am viewing a Smart Queue on an iOS or Android device When I swipe right past the action threshold on an item Then the item is approved, a haptic feedback occurs, an undo snackbar appears for 5 seconds with an Undo button, and the item is removed from the list And when I tap Undo within 5 seconds Then the approval is reverted and the item returns to its previous position and status And when I partially swipe on an item Then the actions Approve, Send to Accountant, Reclassify, Request Receipt, Snooze are revealed with labels and icons and are tappable And when I invoke any action via swipe or long-press Then the same outcome is applied and a confirmation haptic plays respecting system settings
Batch Multi‑Select and Bulk Approval
Given I am in a Smart Queue list view When I long‑press an item Then multi‑select mode activates with checkboxes and a selected counter And when I select up to 100 items and tap Approve Then approvals are processed in bulk with a progress indicator and per‑item success/failure results, and a global Undo is available for 10 seconds to revert all successful approvals And when any item fails due to rule/risk flags or permissions Then that item remains unapproved with a visible failure reason and a Retry option
Inline Receipt Thumbnails and Rule/Risk Badges
Given items have attached receipts and rule/risk evaluations When I view the Smart Queue list Then each item shows an inline receipt thumbnail at least 64x64 px that opens a full‑screen viewer with pinch‑to‑zoom on tap And each item displays rule/risk badges with text labels indicating severity, and tapping a badge reveals a detail sheet describing the rule/risk And when an item has no receipt Then a placeholder thumbnail appears and the Request Receipt action is available
Offline Action Queue and Sync Conflict Resolution
Given my device has no network connectivity When I perform swipe or batch actions Then the actions are queued locally with timestamps and each affected item shows a Queued status And when connectivity is restored Then queued actions sync within 60 seconds in the order they were performed and success/failure toasts are shown And when a server‑side change conflicts with a queued action Then I am shown a conflict dialog with server state vs my action and options Keep Server State (default) or Reapply Action
Accessibility with VoiceOver/TalkBack
Given system screen reader (VoiceOver or TalkBack) is enabled When I navigate the Smart Queue Then items, thumbnails, SLA timers, and badges have meaningful accessibility labels and values, focus order follows visual order, and swipe actions are exposed as accessibility actions And text scaling up to 200% does not truncate critical controls, and interactive elements meet 44x44 pt minimum target size And color contrast for text and badges meets WCAG AA (4.5:1) And action haptics respect system settings and the undo snackbar is reachable and actionable via screen reader
Deep Linking from Notifications to Queue or Item
Given I receive a push notification for a specific queue or item When I tap the notification Then the app opens directly to the referenced queue or item detail within 2 seconds on cold start and 1 second on warm start And if authentication is required Then after successful login I am returned to the intended destination And if the item is missing or archived Then I see an informative message and am navigated to the parent queue
SLA Countdown Timers and Reminders
Given SLAs are configured for the queue When I view the queue list Then each item shows a live countdown to SLA breach in my local timezone and the queue header displays the count of items breaching within 24 hours And at T‑2h and T‑15m thresholds Then the item timer color changes (amber then red) and I receive an in‑app reminder with an option to enable push for the threshold And when offline Then timers display last‑known values with an Offline indicator and refresh on reconnection
SLA Timers, Reminders & Escalations
"As a freelancer, I want reminders and clear deadlines for approvals so that nothing delays my month-end close."
Description

Allow SLAs to be defined per queue (e.g., approve within 48 hours). Show countdown timers on items and queue headers. Send push/email reminders based on SLA stages and user preferences, with quiet hours and digest options. On breach, escalate by increasing priority, reassigning to an alternate approver, or triggering a rule-defined fallback. Provide an operations dashboard with aging, bottleneck metrics, and export. Integrate with notification services and deep links.

Acceptance Criteria
Per-Queue SLA Definition and Editing
Given I am a workspace admin and a queue exists When I set the queue SLA to 48 hours in Queue Settings and save Then the queue stores the SLA as 48 hours and the queue header displays "SLA: 48h" Given items are created after the SLA is changed to 48 hours When I open any new item in that queue Then its countdown is calculated from created_at to created_at + 48 hours Given open items existed before the SLA change When I view their timers Then their countdowns recalculate using the new SLA without altering created_at
Countdown Timers on Items and Queue Headers
Given an item created at T0 in a queue with a 48-hour SLA When the current time is T0 + 36 hours Then the item shows "12h remaining" and does not show "Overdue" until T0 + 48 hours Given a queue with N open items where X are overdue and Y are due within 24 hours When I view the queue list Then the header shows badges Overdue = X and Due < 24h = Y and a countdown to the next breach time Given I am on mobile or web and an item timer is visible When 60 seconds elapse Then the displayed time remaining updates without a page reload
Reminder Scheduling with User Preferences, Quiet Hours, and Digest
Given a 48-hour SLA with reminder stages at 75% and 100% and my preferences set to email at 75% and push at 100% When an item reaches 36 hours elapsed (75%) Then I receive an email reminder and no push notification Given my quiet hours are 21:00–07:00 local time When an item hits 100% during quiet hours Then no reminder is sent immediately and a digest entry is queued Given quiet hours end at 07:00 and at least one digest entry exists When the time is 07:05 Then I receive a single digest listing all items that reached reminder stages during quiet hours with deep links to each item Given I disable push notifications in my preferences When any reminder stage triggers Then no push notifications are sent to me
Escalation on SLA Breach: Priority Increase and Reassignment
Given an escalation rule to increase priority one level and reassign to Alternate Approver A on breach When an item reaches 100% of its SLA without approval Then the item's priority increases one level and the assignee becomes Approver A Given Alternate Approver A is unavailable and a fallback rule routes to the Queue Owner When the item breaches its SLA Then the item is reassigned to the Queue Owner Given an item has already escalated once When it remains overdue and time advances further Then no duplicate escalation actions are performed a second time
Operations Dashboard: Aging, Bottlenecks, and Export
Given I open the Operations Dashboard and filter by Queue Q and Last 30 Days When the data loads Then I see counts by aging buckets (0–24h, 24–48h, >48h), SLA breach rate, average time-to-approve per approver, and the top 3 bottleneck approvers by pending items Given I click Export CSV When the file downloads Then it contains one row per open item with columns: item_id, queue, created_at, sla_due_at, status (open/approved/overdue), current_assignee, priority, deep_link_url Given I approve an item from Queue Q When I refresh the dashboard Then the counts and metrics reflect the approval
Deep Links in Notifications Open Correct Context
Given I receive a push or email reminder for Item I in Queue Q with a deep link When I tap or click the link on a device with the app installed and I am authenticated Then the app opens directly to Item I's detail view in Queue Q Given I open the same link on a device without the app When the browser opens Then I am taken to the web view for Item I's detail after authentication Given I inspect the deep link URL from the notification payload When I parse its parameters Then it contains identifiers for the correct item ID and queue ID that match the payload
Similarity Batching & Clustering
"As a user, I want similar transactions batched together so that I can approve them in fewer steps."
Description

Group items into batches using configurable keys (merchant, category, amount band, client/project, recurring pattern). Update batches dynamically as new transactions arrive and show batch headers with aggregate stats. Enable one-tap bulk actions with the ability to exempt specific items. Expose APIs for batch membership and metrics. Operate across invoices, bank transactions, and receipt entities after entity matching to ensure cohesive queues.

Acceptance Criteria
Batch Creation by Configurable Keys
Given a batching configuration with keys merchant, category, amount_band, client_project, and recurring_pattern And a dataset of items (invoices, bank transactions, receipts) with those attributes populated and entity-matched When the batching job runs Then items with identical values for all configured keys are grouped into the same batch And items with any differing configured key value are placed in a different batch And the number of batches equals the number of unique key tuples in the dataset And each item is a member of exactly one batch
Dynamic Re-Batching on New Arrivals
Given an existing batch B with key tuple T and 10 items When a new transaction arrives that matches T Then within 60 seconds the item is added to batch B and the batch size increases by 1 And the batch header stats update accordingly When an item in batch B has its category edited so its key tuple becomes T2 Then within 60 seconds the item is removed from B and added to the batch with T2 (creating a new batch if none exists) And no duplicate batch membership occurs at any time
Aggregate Batch Headers
Given a batch with amounts [10.00, 20.00, 30.00] and transaction dates [2025-01-01, 2025-01-10, 2025-01-15] When the batch header renders Then it displays item_count=3, total_amount=60.00, average_amount=20.00, min_date=2025-01-01, max_date=2025-01-15 And currency formatting matches the workspace currency And values update within 60 seconds when batch membership changes
Bulk Actions with Exemptions
Given a batch of 12 items and the user initiates a bulk Approve action And the user exempts 3 specific items before confirmation When the user confirms the bulk action Then 9 items are approved and 3 exempted items remain pending with no status change And a confirmation summary shows 9 successes, 0 failures, 3 exempted And any per-item error is surfaced without blocking remaining approvals And the operation can be completed with one tap on mobile after exemptions are set
Cross-Entity Batching after Matching
Given an invoice, a bank transaction, and a receipt photo linked to the same canonical expense via entity matching and sharing the configured key tuple When batching runs Then all three items appear in the same batch And if an item is unlinked from the canonical expense, it is removed from that batch on the next run And items from different canonical expenses never share a batch, even if their configured key tuples match
Amount Band Keying Rules
Given amount bands configured as [0–49.99], [50–199.99], [200–999.99], [1000+] When items with amounts 0.00, 49.99, 50.00, 199.99, 200.00, 999.99, 1000.00 are processed Then they are assigned to bands 1, 1, 2, 2, 3, 3, 4 respectively And band boundaries are lower-inclusive and upper-exclusive, except the highest band which is open-ended and lower-inclusive And changes to amount band configuration take effect on the next batching run and re-partition existing batches accordingly
Batch Membership & Metrics APIs
Given the API is authenticated When the client calls GET /api/v1/batches?queueId={id}&page=1&pageSize=50 Then response 200 includes an array of batches each with id, key_tuple, item_count, total_amount, min_date, max_date When the client calls GET /api/v1/batches/{batchId}/items Then response 200 returns items with stable item_id and entity_type in {invoice, bank_transaction, receipt} When the client calls GET /api/v1/batches/{batchId}/metrics Then response 200 returns item_count, total_amount, average_amount, currency, last_updated_at And all endpoints support filtering by key fields and return results within 2 seconds for up to 10,000 items
Approval Audit Trail & IRS Notes
"As a freelancer, I want a clear audit trail of approvals and notes so that I can defend my deductions and share context with my accountant."
Description

Record a tamper-evident audit log for every decision: timestamp, actor, rule matched, risk score, action taken, before/after category, receipt linkage, and reason codes. Surface the log within each transaction and generate annotated notes that feed into IRS-ready tax packets. Support CSV/PDF export, monthly and tax-year bulk exports, and data retention aligned with IRS guidelines (7 years). Include search/filter on approvals for audits.

Acceptance Criteria
Tamper‑Evident Audit Log Entry Creation
Given a user approves, rejects, or re-categorizes a transaction via Smart Queues When the action is submitted Then the system creates a single append-only audit entry containing: event_id (UUID), timestamp (ISO 8601 with timezone), actor_user_id, actor_role, decision_type (approve|reject|edit), rule_id, rule_version, risk_score (0–100), action_taken, before_category, after_category, receipt_id (or null), reason_codes (from controlled list), source_channel (web|mobile|api), and request_ip And the entry stores event_payload_hash (SHA-256) and prev_entry_hash to form a verifiable hash chain And any attempt to modify an existing entry is blocked and instead records a new corrective entry referencing the prior event_id Given an integrity verification is requested for a transaction’s audit chain When the chain is validated Then the system reports Pass if all hashes and ordering are intact, otherwise Fail with the first violating event_id
Audit Log Visibility in Transaction Detail (Web and Mobile)
Given a user with AuditViewer or higher permissions opens a transaction detail When the Audit tab is selected Then the full chronological audit trail is displayed within 300 ms for trails ≤ 50 events, with pagination beyond 50 And each entry shows timestamp (localized), actor, decision_type, rule_id/version, risk_score, before→after category, receipt link status, and reason codes And mobile UI supports vertical scroll and copy/share of a single entry Given a user without permissions When they attempt to view the Audit tab Then access is denied with a standard 403 message and no data is leaked
IRS‑Ready Annotated Notes Generation for Tax Packets
Given a user exports a tax packet for a selected tax year When generation completes Then the packet includes an Annotated Notes section per transaction summarizing approval decisions, rule matched, risk score, final category, receipt linkage, and reason codes And PDF rendering preserves readable timestamps and actor initials; CSV includes machine-readable fields with headers matching the audit export schema And the notes section passes schema validation and contains 100% of transactions included in the packet
CSV/PDF Audit Log Export for a Date Range
Given a user selects a date range and filters in Approvals When Export CSV or Export PDF is clicked Then the system generates a file named audit_{org}_{yyyyMMdd}-{yyyyMMdd}_{tz}.(csv|pdf) And the CSV contains one row per audit event with all required fields; PDF provides a per-transaction grouped view And exports up to 250k events complete within 2 minutes via background job, with progress status and retry on failure And file integrity is verified via SHA-256 checksum and downloads require auth token valid for ≤ 15 minutes
Monthly and Tax‑Year Bulk Audit Exports
Given an admin schedules monthly and yearly audit exports When the schedule triggers Then the system generates a zipped archive per period containing CSV and PDF exports and a manifest.json with counts and checksums And the archive is stored in the org’s secure export bucket for 30 days and optionally delivered via secure link email And re-running the same period produces an idempotent export with identical checksums if source data unchanged
Search and Filter Approvals for Audits
Given a user opens the Approvals search When filters are set for actor, rule_id, risk_score range, decision_type, date range, amount range, category, receipt_linked (yes/no), and reason_codes Then the result set updates within 1 second for result counts ≤ 10,000 and is accurate to the selected filters And the user can save the filter set and export exactly the filtered results
Seven‑Year Data Retention and Purge with Legal Hold
Given an organization has completed a tax year When the retention timer reaches 7 years after the tax year end Then audit logs and annotated notes for that year are purged irreversibly within 30 days, with a purge receipt recorded externally to the chain And if a legal hold is active on the org or specific transactions, the purge is skipped and logged with hold_id And admins receive a 30-day prior notification and can export affected data before purge

Audit Ledger

Append‑only, hash‑chained activity logs that capture who did what, when, and from which device, plus the before/after state. Export a signed trail for auditors or clients, detect tampering, and anchor key approvals for indisputable compliance.

Requirements

Immutable Hash-Chained Ledger
"As a freelancer, I want my actions and data changes recorded in a tamper-evident chain so that I can demonstrate the integrity of my records if the IRS or a client questions them."
Description

Implement an append-only ledger where each event record includes a cryptographic hash that chains to the previous record’s hash, preventing undetectable edits or deletions. Each entry must store event type (e.g., import, categorize, edit, match, approve, export), timestamp, actor identity, device/session fingerprint, target entity IDs (invoice, receipt, bank transaction), and digests of any state diffs attached. Writes must enforce monotonic sequence numbers and verify the previous-hash pointer. Provide periodic background verification and repair routines across replicas. Generate a daily Merkle root of the ledger and sign it with TaxTidy’s KMS-managed key to provide organization-wide proof-of-integrity, with optional RFC 3161 timestamping for third-party attestation. Integrates at the platform level so all TaxTidy services (mobile, web, OCR, bank-feed, rules engine) emit standardized audit events.

Acceptance Criteria
Append-Only Write With Hash-Chain Enforcement
Given a ledger with last_sequence S and last_hash Hs When a new event arrives with sequence S+1 and prev_hash = Hs Then the write succeeds and the stored record’s hash = SHA-256(canonical_record_bytes) Given a new event whose sequence ≠ last_sequence + 1 When a write is attempted Then the write is rejected and no record is appended Given a new event whose prev_hash ≠ last_hash When a write is attempted Then the write is rejected and no record is appended Given concurrent writers When N events are committed Then recorded sequence numbers are strictly increasing by 1 with no gaps or duplicates and exactly one record exists per sequence number
Mandatory Event Schema and Field Validation
Given an event of type import, categorize, edit, match, approve, or export When the event is written Then the stored record contains non-null fields: event_type, server_assigned_timestamp (ISO-8601 UTC), actor_id, device_or_session_fingerprint, target_entity_ids (≥1), and state_diff_digests when applicable Given an event missing any required field When a write is attempted Then the write is rejected with a validation error and no record is appended Given a state diff is attached When the event is written Then a deterministic SHA-256 digest of the diff bytes is computed and stored in the record Given client-supplied timestamps When the event is written Then server_assigned_timestamp is used for ordering while client_timestamp (if present) is stored as metadata but does not affect sequencing
Periodic Chain Verification and Tamper Detection
Given a ledger with N records When the verification job runs hourly Then it recomputes each record hash from genesis through N, validates each prev_hash link, and confirms sequence continuity, persisting a verification report Given any hash mismatch or sequence gap is detected When verification runs Then the affected range is marked corrupted, an alert is emitted, further writes to that range are blocked, and a detection event is appended to the ledger Given multiple replicas exist When verification runs Then each replica reports its last_sequence and last_hash and discrepancies are flagged in a central health view
Replica Repair Without Mutating History
Given a replica diverged or missing records after sequence S When the repair routine runs Then it fetches the authoritative range S..N from a quorum source and appends missing records in order without altering existing records Given repair completes successfully When verification re-runs Then the replica’s sequence is continuous and last_hash equals the authority’s last_hash Given irreconcilable divergence is detected (e.g., conflicting records at the same sequence) When repair runs Then repair halts, the replica is placed read-only, and an incident with affected sequences is recorded
Daily Merkle Root Generation and KMS Signature
Given end-of-day UTC T When the daily job runs within 60 minutes of T Then it computes the Merkle root over that day’s records, signs the root with the KMS-managed key (e.g., ECDSA P-256) and stores the signature, key identifier, and signing algorithm Given RFC 3161 timestamping is enabled When the daily job signs the Merkle root Then it obtains and stores a TSA timestamp token; if the TSA is unavailable, retries occur with exponential backoff and the failure is surfaced in audit health Given a stored daily Merkle root and signature When verified offline using the published KMS public key Then signature verification succeeds for that root
Export Verifiable Audit Trail
Given a requested date/time range and optional entity filters When an audit export is generated Then the output contains ordered records with sequence numbers, per-record hashes, prev_hash pointers, daily Merkle roots within range, and corresponding KMS signatures Given the export includes inclusion proofs When an external verifier runs the provided verification utility Then it validates per-record hash chaining, Merkle inclusion proofs to the daily roots, and KMS signatures with zero failures Given filters cause records to be omitted When the export is generated Then inclusion or bridging proofs are provided so that chain integrity can still be verified for the included subset
Cross-Service Event Emission and Standardization
Given user actions occur via mobile, web, OCR service, bank-feed service, and rules engine When those actions are performed Then each service emits a standardized audit event conforming to schema v1 with the required fields and a schema_version field Given an ingestion SLO of p99 ≤ 5 seconds When a load test generates events across all services Then 99% of events are visible in the ledger within 5 seconds of server receipt Given a temporary outage in any emitting service When buffered events are retried Then events are eventually appended without loss, with correct server_assigned_timestamps, and the global hash chain remains valid
Full Before/After State Diff Capture
"As an auditor, I want to see exactly what changed in each record so that I can trace how reported amounts were derived and verify their accuracy."
Description

Capture and persist normalized field-level before/after snapshots for every mutating event (e.g., category change, amount edit, memo update, attachment add/remove). Store diffs as content-addressed blobs with schema versions, field paths, and redaction flags for sensitive data. Link diffs to the corresponding ledger entry via digest to keep the hash chain compact while enabling full reconstruction of prior states. Support large-object handling for attachments via references, not inline storage. Provide deterministic serialization to ensure reproducible diff hashing across services and platforms.

Acceptance Criteria
Capture Diff for Field Edit (Amount Change)
Given a transaction with amount.value = "100.00" and currency = "USD" in normalized form When the user updates amount.value to "120.00" Then the system persists a diff blob that includes schema_version, and a single field_change with field_path = "amount.value", before = "100.00", after = "120.00" And the diff blob excludes unchanged fields and computed/derived fields And the diff blob is content-addressed: digest = SHA-256(canonical_serialization(diff_blob)) And retrieving the diff by digest returns exactly these normalized before/after values
Schema Versioning, Field Paths, and Redaction Flags
Given a record with nested field tax.profile.ssn = "123-45-6789" marked sensitive When the ssn field is edited Then the diff blob includes schema_version = "v1" (non-empty), and a field_change with field_path = "tax.profile.ssn", before and after values present, and redaction.flag = true for that field And a request for the redacted view of the diff masks the sensitive values (e.g., "***-**-6789") while preserving field_path and change indicators And a request for the unredacted view by an authorized role returns the actual values And attempts to write a diff with an unknown or missing schema_version are rejected with a 400 error
Deterministic Serialization and Reproducible Diff Hashing
Given two independent services generate the same logical field_change set for a mutation When each service computes canonical_serialization(diff_blob) Then the byte representation is identical across services (UTF-8, sorted keys ascending, no insignificant whitespace, normalized scalars as strings for decimals and timestamps) And the computed SHA-256 digest is identical across services and platforms And re-serializing the parsed diff blob reproduces the exact same bytes and digest (idempotent) And altering only whitespace or object key insertion order in a non-canonical representation does not change the resulting digest after canonicalization
Attachment Add/Remove Stored as References (No Inline Blobs)
Given a user adds an attachment file image.jpg of size 3_145_728 bytes to a record When the change is committed Then the diff blob represents the change at field_path = "attachments[+]" with an attachment_ref object containing uri = "cas://sha256/{hash}", size_bytes = 3145728, media_type = "image/jpeg" And the diff blob contains no inline byte data for the attachment (no base64 or binary fields) And removing the same attachment persists a diff at field_path = "attachments[-]" referencing the same uri And the diff digest is computed over the canonical diff representation that includes only the reference metadata, not the attachment bytes
Link Diff Blob to Ledger Entry via Digest
Given a mutating event is recorded in the Audit Ledger When the diff blob is persisted Then the corresponding ledger entry stores diff_digest equal to the blob’s SHA-256 digest And the ledger chain hash changes only due to the diff_digest value, not the diff blob content itself, keeping the chain compact And GET /diffs/{diff_digest} returns the exact diff blob And if the stored diff blob is tampered with, recomputing its digest no longer matches the ledger entry’s diff_digest and the system flags integrity_error = true
Full Reconstruction of Prior State from Diffs
Given a record with an initial baseline snapshot and a sequence of N mutating diffs linked by digest When reconstructing the record state immediately before a specified diff Dk Then applying diffs from baseline up to D(k-1) yields a state identical (bit-for-bit in canonical form) to the recorded "before" state for Dk And reconstructing the state immediately after Dk matches the recorded "after" state for Dk And reconstruction yields identical results across services/platforms using the same canonical rules And attempts to reconstruct with a missing diff referenced by the ledger entry return a 409 (incomplete_history) error with the missing diff_digest listed
Actor and Device Attribution
"As a client sharing my books with an external CPA, I want clear attribution of who did what and from which device so that responsibility is unambiguous during reviews."
Description

Record verified actor identity and context with each ledger entry, including user ID, role (Owner, Accountant, Auditor-Guest), auth method (password, SSO, passkey), session ID, IP, geo (coarse), app/SDK version, OS, browser/device model, and API key or integration source (e.g., bank feed provider). Include fallback attribution for automated system actions (rules engine, imports). Normalize PII in device data to privacy-safe fingerprints. Expose attribution in UI detail panes and exports to provide who/when/where evidence.

Acceptance Criteria
Attribution for User‑Initiated Ledger Entry
Given an authenticated user with a verified identity edits an expense in TaxTidy When the change is saved and the audit ledger entry is created Then the entry includes user_id, user_role, auth_method, session_id, ip_address, geo_coarse, app_or_sdk_version, os_name_version, browser_or_device_model, and timestamp And each field is non-null and validated for format (e.g., IP v4/v6, ISO 8601 timestamp) And the entry links to the originating resource (resource_type, resource_id) And the attribution record is immutable once written (append-only)
Role and Authentication Method Captured Correctly
Given users with roles Owner, Accountant, and Auditor-Guest authenticate via password, SSO, and passkey When each user performs a distinct action that writes to the audit ledger Then the entry for each action records the correct user_role and auth_method values from the session at the time of action And the values are restricted to the allowed enumerations {Owner, Accountant, Auditor-Guest} and {password, SSO, passkey} And mismatched or missing role/method values cause the write to be rejected with an error logged
Device and Network Context Capture
Given a session originates from web or mobile When a ledger entry is created Then the entry records session_id scoped to the login session and stable across actions in that session And captures ip_address (public), geo_coarse (country and region only), os_name_version, browser_or_device_model, and app_or_sdk_version And geo resolution never exceeds region-level (no exact address or GPS coordinates) And device and network fields are captured within 200 ms of the action timestamp
Fallback Attribution for Automated Actions
Given an automated event occurs (e.g., bank feed import, rules engine auto-categorization, API integration) When the system writes a ledger entry Then actor_identity is recorded as system with actor_source in {rules_engine, bank_feed_provider, api_integration} And integration_source and api_key_id (or provider_account_id) are included when applicable And no end-user identifiers are populated And the entry contains an automation_job_id and trigger_reason And the format passes schema validation for automated entries
PII Normalization to Privacy‑Safe Fingerprints
Given device data includes potentially identifying attributes (e.g., device name, hardware IDs, full user agent) When attribution is recorded Then raw device names and hardware serials are not stored And a device_fingerprint is stored as a salted SHA-256 hash derived from approved non-sensitive components And user agent is parsed into normalized fields (os_name_version, browser_or_device_model) with no raw UA string stored And ip_address is stored separately; no MAC addresses or advertising IDs are stored And attempts to persist disallowed PII are blocked and logged
Attribution Visibility in UI and Export
Given a user opens a ledger entry detail pane in the UI When the entry is displayed Then the who/when/where attribution (user or system source, role, auth_method, timestamp, ip_address, geo_coarse, device/browser/OS, app_or_sdk_version, session_id, integration_source) is visible and readable within 2 seconds And sensitive values are redacted where appropriate (e.g., last octet of IPv4) And when an audit export (CSV and JSON) is generated, these fields are included with consistent headers/keys and a signature block over the exported payload And exported records round-trip back into the system without loss of attribution fields
Graceful Degradation and Reason Codes for Missing Context
Given some context cannot be captured (e.g., geo lookup timeout, SDK version unavailable) When the ledger entry is written Then missing fields are set to null and accompanied by an attribution_reason_code explaining the omission And the entry write is not blocked; integrity fields still recorded And a metric increments for each reason_code to enable monitoring And overall missing-attribution rate remains below 0.5% over a rolling 7-day window
Tamper Detection and Alerting
"As a workspace admin, I want immediate alerts if any audit records are inconsistent so that I can pause risky operations and remediate before sharing data externally."
Description

Continuously validate ledger integrity by re-computing hash links and verifying signatures, detecting gaps, out-of-order sequences, or mismatched digests. On integrity failure, raise real-time alerts to admins, surface a health banner in the UI, and block creation of signed exports until issues are resolved. Maintain redundant, append-only storage with quorum verification to prevent single-point corruption. Provide an integrity status API and audit dashboard with validation history, failure reason, and remediation actions.

Acceptance Criteria
Scheduled Hash-Chain Validation Pass
Given an organization ledger with at least one entry and no integrity defects When the validator runs on its schedule (every 5 minutes) or is manually triggered Then it recomputes the previous-hash chain from genesis to head and verifies each entry’s digital signature against the stored signer key And it confirms sequence numbers are strictly increasing without gaps And it marks integrity_status=pass for the ledger And it records a validation run containing startedAt, finishedAt, validatorVersion, headHash, entriesValidated, and status=pass in the validation history store And the run completes within 60 seconds for ledgers up to 100,000 entries And transient storage read failures are retried up to 2 times with exponential backoff and logged
Detection of Ledger Gap or Out-of-Order Entry
Given a ledger containing a missing sequence number, an out-of-order sequence, a mismatched previous hash, or an invalid signature When the validator runs Then it sets integrity_status=fail for the ledger And it persists failureReasonCode in {GAP, OUT_OF_ORDER, DIGEST_MISMATCH, SIG_INVALID} And it persists failingEntryId and failingSequenceNumber for the first offending entry And it writes a validation record with status=fail and details to the validation history And it does not mutate or rewrite any ledger entries
Real-Time Admin Alert on Integrity Failure
Given integrity_status transitions from pass or degraded to fail for a ledger When a validation run detects the failure Then all Org Admins receive an in-app notification and email within 30 seconds containing ledgerId, failureReasonCode, failingEntryId, detectedAt, and a remediation link And duplicate alerts for the same ledger and failureReasonCode are suppressed for 15 minutes And an escalation notification is sent to the designated security contact if no admin acknowledgment occurs within 10 minutes And admin acknowledgment is recorded with userId and acknowledgedAt
Health Banner and Signed Export Block on Integrity Failure
Given a ledger with integrity_status=fail When a user opens any Audit Ledger or Export view Then a persistent health banner is displayed with status=fail, failureReasonCode, and a link to details And the "Create Signed Export" action is disabled with a tooltip explaining integrity must be restored And API POST /api/v1/ledgers/{id}/exports responds 423 Locked with errorCode=INTEGRITY_FAILED and a human-readable message And exports remain blocked until integrity_status returns to pass for two consecutive validation runs within the last 15 minutes
Redundant Append-Only Storage with Quorum Verification
Given a write request to append a new ledger entry When the system attempts the append Then the entry is written to three independent append-only replicas And the operation succeeds only after at least 2 replicas (quorum) persist the entry with identical computed hash and prior headHash And on success the client receives 201 Created and the system records commitTimestamp and committedReplicaIds And if quorum is not achieved within 5 seconds, the operation fails with 503 Service Unavailable and no replica advances the committed head And a periodic (every 10 minutes) quorum verification compares heads and hashes across replicas and records any divergence And detection of divergence sets integrity_status=fail with failureReasonCode=REPLICA_DIVERGENCE and records the affected replicaIds
Integrity Status API and Dashboard History
Given an authenticated Org Admin with scope audit:read When they GET /api/v1/ledgers/{id}/integrity-status Then the response is 200 with JSON containing {status in [pass, fail, degraded, unknown], lastValidatedAt (ISO 8601), validatorVersion, headHash, entriesValidated, failureReasonCode|null, failingEntryId|null, remediationActions[]} And the endpoint responds within 300 ms p95 with Cache-Control: max-age=5 And unauthorized requests receive 401; cross-org access attempts receive 403 And the Audit Dashboard displays a 30-day validation history with filter by status and supports CSV export And history entries match the API data for the same time window
Remediation Clears Failure and Restores Exports
Given a ledger with integrity_status=fail and available remediation actions When an admin completes remediation and re-runs validation Then two consecutive validation runs with status=pass mark the ledger as healthy And open alerts are auto-resolved with resolvedAt and resolverId recorded And the health banner is cleared across the UI within 1 minute And the export block is lifted and signed exports can be created And a remediation record is stored with actionTaken, actorId, startedAt, finishedAt, and outcome
Signed Audit Trail Export (PDF + JSON)
"As a freelancer under review, I want to export a signed audit trail that an auditor can independently verify so that the review is faster and I avoid disputes about data integrity."
Description

Enable export of a time-bounded, filterable audit package (ZIP) that includes a canonical JSON ledger, referenced diffs, and a human-readable PDF timeline summary. Produce a detached signature and embed the certificate chain and signed timestamp to verify authenticity offline. Support filters by date range, entities, and event types, plus export presets (IRS packet, client summary, full forensic). Generate expiring, access-logged share links with optional passcode and watermarking. Ensure exports are immutable artifacts recorded back into the ledger with their own signatures.

Acceptance Criteria
Export ZIP Contains Canonical Artifacts
Given a user requests an audit export with default settings When the export completes Then a single .zip file is generated and downloadable And the zip root contains manifest.json, ledger.json, summary.pdf, and a diffs/ directory And ledger.json validates against the schema version declared in manifest.json And every event in ledger.json has eventId, actorId, timestamp (RFC 3339), device info, and before/after fields where applicable And every diff URI referenced in ledger.json exists under diffs/ and its SHA-256 matches the hash recorded in ledger.json And summary.pdf opens without error and lists the same event count as ledger.json for the exported scope And manifest.json includes exportId (UUID), createdAt (RFC 3339), requesterId, applied filters, preset (nullable), and SHA-256 hashes for all payload files
Detached Signature and Certificate Chain Validation Offline
Given export.zip and a detached signature file for it (e.g., export.sig) When the signature is verified offline without any network access Then the signature validates against the exact bytes of export.zip And the signature object contains the full certificate chain and a signed timestamp token And the signed timestamp is within 5 minutes of manifest.json.createdAt And the signing certificate subject matches the TaxTidy publisher identity and allows digitalSignature key usage And modifying any byte of export.zip causes signature verification to fail
Filtering by Date Range, Entities, and Event Types
Given filters start=2025-01-01T00:00:00Z, end=2025-01-31T23:59:59Z, entities=[E1,E2], eventTypes=[CREATE,UPDATE] When the export is generated Then ledger.json contains only events whose timestamp is within [start,end] inclusive And only events with entityId in {E1,E2} And only events with type in {CREATE,UPDATE} And summary.pdf reports the same filtered counts by type And events exactly at start and end boundaries are included And events outside the filters are absent
Export Presets: IRS Packet, Client Summary, Full Forensic
Given preset = "IRS packet" When exporting Then the package includes only tax-relevant events (income, expense, document uploads, approvals) and excludes device identifiers and IP addresses And summary.pdf includes a tax-year header and totals by category Given preset = "Client summary" with clientId = C123 When exporting Then the package includes only events linked to clientId C123 and redacts fields flagged internal-only And summary.pdf presents a timeline and per-entity counts Given preset = "Full forensic" When exporting Then the package includes all event types, full before/after diffs, device fingerprints, IP addresses, and raw headers And manifest.json sets preset = "forensic" and lists per-file SHA-256 hashes in an integrity section
Share Links with Expiry, Passcode, Watermark, and Access Logging
Given an export is created and a share link is generated with expiry = 7 days, passcode = enabled, watermark = "Confidential" When a recipient opens the link before expiry Then the recipient is prompted for the passcode and access is denied until the correct passcode is provided And upon correct entry, files are downloadable and summary.pdf renders the specified watermark on every page And an access log entry is appended to the audit ledger with timestamp, linkId, viewer IP, user agent, and success = true When the same link is requested after expiry Then the link responds with HTTP 410 Gone (or equivalent) and no file content is served And an access log entry is appended with success = false and reason = "expired"
Immutability: Export Artifact Recorded Back Into Ledger With Its Own Signature
Given an export completes successfully When the system appends the export record to the audit ledger Then a new EXPORT_CREATED event is added with exportId, manifest SHA-256, zip SHA-256, sig SHA-256, requesterId, applied filters, preset, and shareLinkId(s) And the event’s hash links correctly to the prior event per the ledger’s chain rules And attempts to update or delete this EXPORT_CREATED event via APIs are rejected (405 Method Not Allowed or equivalent) and no prior ledger links are altered And the event contains a signature reference that verifies against the stored hashes
Tamper Detection: Hash-Chained Ledger and Diffs Verify Offline
Given ledger.json in the export includes per-event hashes and a head hash covering the chain When an offline verifier validates the chain across all events and referenced diffs Then verification returns OK for the unmodified package And if any event body or referenced diff file is changed, the verifier reports failure and identifies the first mismatched eventId
Approval Anchoring and E-Sign Evidence
"As an accountant, I want approvals to be cryptographically anchored so that I can demonstrate non-repudiation of key sign-offs to clients and auditors."
Description

For key approvals (e.g., category lock, write-offs, mileage confirmation, quarter close, export authorization), generate a canonical approval object capturing signer identity, intent, consent, timestamp, context, and summary hash of approved content. Sign approvals using TaxTidy’s KMS keys and optionally anchor the approval hash with a trusted timestamp authority for non-repudiation. Support single- and multi-party approvals with sequence and quorum rules. Surface approval badges in the UI and include them in exports as verifiable artifacts.

Acceptance Criteria
Single-Party Category Lock Approval Canonicalization
Given an authenticated user initiates a category lock approval And the approved content is serialized to a canonical JSON representation When the user provides explicit consent and confirms the e-sign Then the system generates a canonical approval object including: approval_id, signer_id, signer_name, signer_email, device_id, intent, consent=true, timestamp (UTC ISO-8601), context (resource_type, resource_id, version), and content_hash (SHA-256 of the canonical JSON) And the approval object is signed with TaxTidy’s KMS key using ECDSA P-256 with SHA-256 And the signature verifies successfully against the corresponding public key And an append-only audit entry is written linking to the previous ledger hash
Multi-Party Sequential Approval with Quorum
Given an approval workflow configured with roles Preparer → Reviewer → Approver and a quorum of 2 of 3 required When participants sign in the defined sequence Then each collected signature is recorded as an immutable sub-approval with signer identity, timestamp, and content_hash identical to the root approval And the approval remains Pending until the quorum threshold is met in sequence order And out-of-order signature attempts are rejected with a 409 and do not alter the ledger And once quorum is met, the final approval object aggregates sub-approvals and is signed by TaxTidy’s KMS And the UI reflects status as Approved (2/3) with participant details
Trusted Timestamp Authority (TSA) Anchoring
Given TSA anchoring is enabled and reachable When an approval is finalized Then the system submits the approval content_hash to the TSA and stores the RFC 3161 timestamp token (TSR) And the approval record stores anchored=true, tsa_policy_oid, tsa_serial, and tsa_time And offline verification of the TSA token succeeds for the stored content_hash And if the TSA is temporarily unreachable, the system records anchored=false with retry_backoff metadata without blocking approval completion
UI Approval Badges and Evidence Drawer
Given an entity with an approval state (Pending/Approved/Rejected) When the entity is rendered in web and mobile clients Then an approval badge is displayed with status and quorum numerator/denominator where applicable And tapping/clicking the badge opens an evidence drawer showing signer(s), timestamps, device_id, content_hash, KMS key_id, and TSA anchored state And the badge is updated within 3 seconds of any approval state change via real-time or polling fallback And the badge and drawer have accessible labels and are operable via keyboard and screen readers
Exportable Verifiable Approval Artifacts
Given a user exports a tax packet containing approved items When the export is generated Then the export includes a machine-readable approvals.json with each approval object, KMS signature (base64 DER), and optional TSA token (base64) And a detached verification manifest (approvals.manifest.json) contains the export package hash and public key metadata And embedded PDF pages display an Approval badge with a QR linking to a verification endpoint And running the published verification tool against the export returns PASS for signatures and TSA tokens
Tamper Detection and Ledger Consistency for Approvals
Given an approval record is stored in the audit ledger When any field of the approval object, signature, or TSA token is altered post-write Then signature verification fails and the system flags the record as Tampered in verification results And the ledger chain verification detects a prev_hash mismatch if the ledger entry is altered and marks the chain as Broken from that point forward And exports containing tampered approvals emit a non-zero exit code in the verification tool and label affected items as Unverifiable
Access Controls and PII Redaction for Exports
"As a user sharing my records with a third-party auditor, I want to control what sensitive details are visible so that I can protect my privacy without compromising the audit trail’s validity."
Description

Provide role-based access controls for viewing ledger entries and diffs, including an Auditor-Guest role with read-only, time-bounded access. Implement redaction policies to mask sensitive fields (e.g., full PAN, bank account numbers, SSNs) in UI and exported artifacts, while preserving hashes for integrity. Offer export modes (full vs. redacted) with clear labeling, and log every export access/view as a new ledger event. Ensure compliance with GDPR/CCPA data minimization while retaining evidentiary value.

Acceptance Criteria
Auditor-Guest Time-Bounded Read-Only Access
- Given an Admin invites an Auditor-Guest with start_time and end_time, When the invitee accepts, Then the guest can sign in and view only permitted ledger entries and diffs within the defined time window. - Given an Auditor-Guest session, When attempting to create, edit, delete, tag, reclassify, approve, or change redaction/export settings, Then the action is blocked with 403, no data is modified, and the attempt is logged as a ledger event. - Given an Auditor-Guest, When exporting, Then only Redacted export mode is available and Full export mode is not presented nor accessible via API. - Given the end_time has passed, When the Auditor-Guest attempts any access, Then access is denied with 403, no content is returned, and a ledger event is created with reason "access_expired". - Given any Auditor-Guest view or export, When completed, Then a ledger event records actor_id, role, timestamp (UTC), ip, device_id, user_agent, action, resource_id, outcome, and prev_hash/hash linkage.
RBAC Enforcement for Ledger Views and Diffs
- Given a user with role Admin, Member, or Auditor-Guest, When requesting ledger entries/diffs, Then only entries authorized for that role and workspace/tenant are returned; cross-tenant data is never returned. - Given an unauthorized request for a specific ledger entry/diff, When processed, Then the response is 403 with zero bytes of sensitive content and a security_event ledger record is appended. - Given UI, API, and export endpoints, When applying permission checks, Then the same RBAC rules produce consistent results across all surfaces. - Given Admin role, When viewing, Then both Full and Redacted modes are available; Given Member role without "View Sensitive" permission, Then only Redacted values are visible; Given Auditor-Guest, Then only Redacted values are visible. - Given a permissions introspection endpoint, When called by an authenticated user, Then it returns the user’s effective permissions for viewing ledger entries, diffs, and export modes for traceability.
PII Redaction in UI and Diffs
- Given ledger data containing PAN, bank account numbers, routing numbers, SSNs, CVV, and DOB, When rendered in UI or diffs, Then values are masked to policy (e.g., PAN **** **** **** 1234; SSN ***-**-1234; bank acct ****1234) and labels indicate "Redacted". - Given a user lacking "View Sensitive" permission, When inspecting network payloads or DOM, Then unredacted values are never present; only redacted strings are delivered from the server. - Given API access by a user lacking "View Sensitive" permission, When requesting entries/diffs, Then the service returns redacted values and never transmits raw PII. - Given redacted display, When verifying integrity, Then salted hashes of original values are preserved and consistent across views/exports to support integrity proofs. - Given automated tests, When running redaction regex and edge-case suites, Then coverage is ≥95% and false negatives for PII leakage are ≤0.1%, with zero known occurrences in production smoke tests.
Export Modes: Full vs Redacted with Clear Labeling
- Given a user initiates export, When selecting mode, Then Redacted is the default for all roles and Full is available only to Admins with MFA validated in-session within the last 15 minutes. - Given a generated export, When opened, Then a visible header and embedded metadata state Export Mode (Redacted/Full), Timestamp (UTC ISO 8601), Workspace ID, Requestor ID, Role, Time Range, Source Ledger Head Hash, and Signature Algorithm. - Given Redacted mode, When inspecting content, Then sensitive fields are masked per UI policy and include salted hashes of originals to preserve evidentiary integrity; attachments with PII are excluded by default. - Given Full mode, When proceeding, Then user must acknowledge a warning and record a purpose string; both start and completion events are written to the ledger with mode and purpose. - Given any exported file, When signature verification is run, Then tampering invalidates verification and is reported as failed; intact files verify successfully.
Export Access/View Event Logging and Tamper Detection
- Given any export-related action (page view, mode selection, generation start, completion, download, link access, verification), When performed, Then an append-only ledger event is created with id, hash, prev_hash, actor, role, ip, device_id, user_agent, action, resource, timestamp, and outcome. - Given sequential events, When validating the chain, Then each event’s prev_hash matches the prior event’s hash and the chain verifies end-to-end. - Given an attempt to edit or delete a ledger event, When executed, Then the system returns 405/forbidden, makes no change, and appends a security_event noting the attempt. - Given geolocation/device mismatch heuristics, When an export download occurs from an anomalous location/device within 10 minutes of generation, Then the event is flagged "suspicious_access" and notification hooks are triggered.
GDPR/CCPA Data Minimization in Exports
- Given a user prepares an export for external sharing, When proceeding with defaults, Then only Redacted mode can be completed without Admin override and a required scope (date range and/or project) must be selected. - Given Redacted exports, When scanning the artifact, Then zero occurrences of unredacted PAN, SSN, bank account, routing number, or CVV patterns are detected; build/test fails if any are found. - Given export generation, When assembling fields, Then only necessary evidentiary fields are included (transaction id, date, amount, category, redacted payee/identifiers, hashes, approver, event metadata); optional PII-heavy attachments are excluded unless explicitly toggled by Admin. - Given Full exports, When initiated, Then Admin with MFA must select a lawful basis/purpose from a predefined list and enter free-text justification; this purpose is embedded in the artifact and logged. - Given completed exports, When inspected, Then an embedded Data Minimization Report lists included field classes, counts, scope, and mode to support compliance review.

Approval Playbooks

Templated workflows for recurring scenarios (reimbursables, mileage, mixed‑use, project reclasses). Define required attachments, policy checks, and approvers; auto‑assign steps to delegates. Ensures consistent decisions, fewer reversals, and clearer accountability.

Requirements

Playbook Template Builder
"As a freelancer who collaborates with my bookkeeper and CPA, I want to create reusable approval playbooks for recurring tax scenarios so that reviews run the same way every time and I don’t waste time reinventing steps."
Description

Provide a visual builder to create reusable approval playbooks for common scenarios (reimbursables, mileage, mixed‑use allocations, project reclasses). Enable admins to define step sequences, serial/parallel routing, required approver roles, evidence checklists, and conditional branches tied to transaction attributes (amount, category, project, vendor). Integrate with TaxTidy objects (expenses, invoices, mileage logs, receipt images) to prefill context and surface relevant fields. Include prebuilt templates and variables (e.g., {amount}, {category}) to accelerate setup and ensure consistency across teams. Expected outcome: standardized workflows that reduce reversals, shorten cycle time, and improve audit readiness.

Acceptance Criteria
Create Playbook with Serial and Parallel Steps
Given I am an admin user with access to the Playbook Template Builder When I create a new template and add steps A, B, C, and D Then I can arrange steps into serial order and convert at least one contiguous group to a parallel phase And I can reorder steps via drag-and-drop within and across phases And the builder visually distinguishes serial vs parallel routing And after saving and reopening the template, the exact order and routing configuration are preserved
Configure Conditional Branches by Transaction Attributes
Given an open template in the builder When I add a conditional branch using transaction attributes Then I can select attributes from amount, category, project, and vendor And I can choose operators appropriate to the attribute type (e.g., >, >=, =, !=, in list, contains) And I can combine multiple conditions with AND/OR and define an else/default path And the builder validates field-operator compatibility and prevents saving invalid rules with an inline error And after saving and reopening, the branch logic and default path are preserved
Define Approver Roles and Delegation Rules
Given a step selected in the builder When I specify approver roles and optional named approvers Then the builder enforces that at least one approver or role is defined per approval step And I can set a delegate for each approver role And the step summary clearly indicates primary approver(s) and delegate(s) And templates cannot be published if any approval step lacks an approver or has an invalid delegate reference
Attach Evidence Checklist Requirements
Given a template in the builder When I add an evidence checklist to a step Then I can define checklist items with label, Required/Optional, and attachment type (receipt image, invoice PDF, mileage log) And I can add policy checks such as minimum attachment count and receipt date within N days of transaction date And the builder prevents publish if any required checklist item is incomplete or misconfigured (e.g., unknown attachment type, missing policy parameter) And after saving and reopening, all checklist items and policy parameters persist
Integration Prefill from TaxTidy Objects
Given I select a TaxTidy object context for the template (expense, invoice, mileage log) When I insert variables into step instructions or policy checks Then the builder presents a field picker of relevant fields for the selected object And variables such as {amount}, {category}, {project}, and {vendor} are validated against the selected object schema And invalid or unresolved variables are flagged inline and block publish until corrected And when I change the object context, previously inserted variables that are no longer valid are clearly highlighted for update
Use Prebuilt Templates and Variables
Given I open the new template dialog When I choose a prebuilt template (reimbursables, mileage, mixed-use allocations, or project reclasses) Then the builder prepopulates step sequences, approver roles, and evidence checklists appropriate to that scenario And variables like {amount} and {category} are preinserted where applicable and validate successfully And I can customize any prebuilt element and save as a new reusable template without altering the original prebuilt template
Save, Publish, and Reuse Templates
Given I have edit permissions When I save a template as Draft with incomplete required fields Then the builder saves the draft and displays a validation summary preventing Publish And when all validation errors are resolved, the Publish action becomes enabled And upon publishing, the template appears in the Playbooks catalog as selectable for new approvals And duplicating a published template creates a new Draft copy with a unique name and identical configuration
Policy Rules Engine
"As a user responsible for staying compliant, I want policy checks to run automatically during approvals so that I can approve confidently and avoid non‑deductible or risky entries."
Description

Implement a declarative rules engine that evaluates compliance checks at each playbook step (e.g., receipt required over $75, IRS mileage rate validation, mixed‑use percentage justification, category eligibility). Rules must be configurable per playbook with pass/fail and warning severities, human‑readable explanations, and auto‑resolution suggestions. Leverage OCR and extracted metadata from receipts/invoices and bank feeds for rule inputs. Provide synchronous validation for submit/approve actions and asynchronous nightly rechecks when data changes. Expected outcome: consistent, policy‑aligned decisions with fewer errors and clearer rationale.

Acceptance Criteria
Per-Playbook Rule Configuration & Severity
Given two playbooks A and B And playbook A defines rules R1 and R2 with severities Fail and Warning respectively And playbook B defines rule R1 with severity Warning When I retrieve the rule configurations for A and B Then playbook A returns R1:Fail and R2:Warning And playbook B returns R1:Warning only And rule definitions include declarative conditions referencing standard inputs (e.g., amount, date, merchant, category, miles, rate, attachment_present, justification_text) And persisting changes to A does not modify B When the engine evaluates any playbook Then each result item includes rule_id, rule_name, severity, outcome (Pass|Fail|Warning), explanation, and auto_resolution_suggestion
Receipt Requirement Rule for Expenses Over $75
Given a rule in the playbook: "Receipt required when amount > 75" with severity Fail When validating an expense of $80 without an attached receipt Then the rule outcome is Fail And the submit/approve action is blocked And the explanation states the missing receipt requirement and the threshold And the auto_resolution_suggestion prompts the user to attach a receipt When validating an expense of $80 with a receipt image recognized by OCR Then the rule outcome is Pass When validating an expense of $74 without a receipt Then the rule outcome is Pass
IRS Mileage Rate Validation
Given the IRS standard mileage rate for 2025 is configured as 0.XX per mile for the expense date And a playbook rule enforces reimbursement <= IRS rate with severity Fail and < IRS rate as Warning When validating a mileage claim dated within 2025 for 120 miles at a rate greater than the IRS rate Then the rule outcome is Fail And the explanation cites the IRS rate and calculation And the auto_resolution_suggestion proposes reducing the rate to the allowed value When validating a claim at a rate lower than the IRS rate Then the rule outcome is Warning And the suggestion proposes updating to the IRS rate or confirming intentional under-claim When validating a claim with no explicit rate Then the engine computes reimbursement using the IRS rate and returns Pass
Mixed-Use Expense Percentage & Justification
Given a rule requires mixed-use percentage (1–99%) and justification text >= 20 characters for categories flagged as mixed-use When validating an expense categorized as mixed-use with percentage 60% and a 30-character justification Then the rule outcome is Pass When validating a mixed-use expense with missing percentage Then the rule outcome is Fail And the explanation states the missing percentage requirement And the auto_resolution_suggestion asks the user to enter a percentage 1–99 When validating with percentage 120% or 0% Then the rule outcome is Fail with an explanation of valid bounds When justification text is present but < 20 characters Then the rule outcome is Warning And the suggestion prompts expanding the justification
Category Eligibility via OCR/Metadata
Given a rule that only allows categories in {"Meals","Supplies","Software","Mileage"} for the selected playbook And OCR/bank feed extraction provides merchant name, MCC, and candidate category When validating an expense whose derived category is not in the allowed set Then the rule outcome is Fail And the explanation names the disallowed category and lists allowed options And the auto_resolution_suggestion offers to recategorize to the top 3 eligible categories based on MCC mapping When the derived category is ambiguous (confidence < 0.6) Then the rule outcome is Warning And the suggestion requests manual category confirmation When the derived category is in the allowed set with confidence >= 0.6 Then the rule outcome is Pass
Synchronous Validation on Submit/Approve
Given a playbook step with up to 50 active rules When a user triggers Submit or Approve Then the rules engine evaluates synchronously and returns a consolidated result within 1000 ms at P95 And the response includes all rule outcomes with explanations and suggestions And any Fail outcome blocks the action with a clear message and list of failing rules And Warning outcomes allow proceeding without blocking And the evaluation is deterministic for the same inputs (idempotent)
Nightly Asynchronous Rechecks & Notifications
Given items previously evaluated by the rules engine And source data may change (e.g., improved OCR, updated bank feed metadata, updated IRS rate, or rule configuration change) When the nightly recheck job runs at 02:00 UTC Then only items with changed inputs since last evaluation are re-evaluated And outcomes that change (Pass↔Fail or Pass/Fail↔Warning) are recorded in the audit log with before/after details and timestamps And assigned owners/approvers receive notifications summarizing impacted items and next steps And items that flip to Fail are flagged to block future approvals until resolved And the recheck job completes within the maintenance window with a success/failure report
Role-Based Approver & Delegation Matrix
"As a freelancer with collaborators, I want approvals to route automatically to the correct person or their delegate so that requests never get stuck and accountability is clear."
Description

Offer configurable approver roles (Owner, Bookkeeper, CPA, Project Lead) mapped to users, with step‑level assignment, backup/fallback approvers, and time‑bound delegation (OOO windows) to prevent stalls. Support required vs. optional approvals, quorum rules, and escalation paths on SLA breaches. Integrate with the account’s collaborator directory and permissions to ensure only authorized users can view financial details. Expected outcome: the right people approve at the right time, keeping accountability clear and cycle time low.

Acceptance Criteria
Role Mapping & Directory Permissions Sync
Given a collaborator is assigned an approver role (Owner, Bookkeeper, CPA, Project Lead) in the account directory, When the role mapping is saved, Then the user becomes eligible for step assignment within 5 minutes Given a collaborator is removed from the account directory or their role is revoked, When the next sync runs, Then they lose access to approval steps and financial details within 5 minutes Given a non-collaborator user ID is provided, When attempting to assign them to any approver role, Then the system rejects the assignment with a clear error message Given directory roles are updated, When viewing the approver matrix, Then the visible role-to-user mappings reflect directory changes without stale entries
Step-Level Assignment with Required/Optional & Quorum
Given a playbook step configured with 3 assigned approvers and a required quorum of 2, When any 2 assigned approvers approve, Then the step auto-completes and remaining optional approvals no longer block progression Given a required approver rejects, When the step is awaiting completion, Then the step status becomes Rejected and the workflow halts or follows the configured rejection path Given an approver is unassigned mid-step, When recalculating quorum, Then the required count updates accordingly and the step cannot complete unless the new required quorum is met Given per-role assignment (e.g., any CPA), When multiple users hold the role, Then the system allows any one of them to fulfill one approval slot unless configured otherwise
SLA Breach Escalation to Fallback Approver
Given a step SLA of 24 hours with a defined escalation chain and fallback approver, When the SLA is breached with no decision, Then the item is reassigned to the fallback approver within 5 minutes and an escalation event is logged Given the first fallback also exceeds the SLA, When the secondary escalation rule triggers, Then the item escalates to the next approver in the chain until a terminal approver is reached Given an escalation occurs, When notifications are sent, Then primary and escalated approvers receive alerts and the step shows the current accountable approver Given an escalated approver makes a decision, When the step completes, Then the audit trail records the escalation path and decision source
Time-Bound Delegation (OOO Windows) Auto-Activation
Given a primary approver configures an OOO window with a delegate, When the OOO window starts, Then all new and pending step assignments route to the delegate within 2 minutes Given the OOO window ends, When there are still pending items, Then ownership reverts to the primary approver within 2 minutes and future assignments go to the primary Given a delegate is not an authorized collaborator, When saving the OOO configuration, Then the system blocks the delegation with a validation error Given a delegate approves during the OOO window, When viewing the step history, Then the approval is recorded as performed by the delegate on behalf of the primary
Financial Detail Visibility Restricted to Authorized Approvers
Given a user without financial-view permission attempts to open an approval with financial details, When they access the step, Then the system denies access (HTTP 403 or in-app equivalent) and no financial data is rendered Given an approver with appropriate permissions opens an approval, When viewing attachments and amounts, Then all required financial details are visible and downloadable, and restricted fields remain masked per role policy Given role permissions change to remove financial-view, When the user refreshes or re-enters the step, Then access is revoked within 5 minutes Given a downloadable tax packet is linked to a step, When a non-authorized user attempts to download it, Then the system blocks the download and logs the attempt
Immutable Audit Trail of Approvals, Delegations, and Escalations
Given any approval action (approve, reject), When it occurs, Then an immutable log entry is created with timestamp, actor, role, step ID, and decision Given any reassignment, delegation, or escalation, When it occurs, Then the audit trail records the source, target, reason (SLA breach, OOO), and time Given an admin exports the audit trail for a time range, When the export completes, Then the file includes all relevant events with a stable unique identifier and no missing entries Given audit integrity is required, When attempting to modify or delete past log entries via UI or API, Then the system prevents the change and records the attempted mutation
Required Attachments & Evidence Enforcement
"As a user preparing for taxes, I want the system to require and verify supporting documents before approval so that my records are audit‑ready without manual chasing."
Description

Define per‑step evidence requirements (receipt photo, mileage log, client contract, bank transaction link) with blocking gates until all artifacts are provided or a justified exception is recorded. Provide mobile capture prompts, inline OCR verification (date, amount, merchant), and duplicate detection. Store attachments in TaxTidy with secure linkage to the underlying transaction and playbook step. Expected outcome: complete, verifiable documentation that flows into IRS‑ready annotated tax packets with minimal back‑and‑forth.

Acceptance Criteria
Mobile Receipt Capture Gate With OCR Verification
Given a pending approval step configured with required artifact type "Receipt Photo" and OCR checks for date, amount, and merchant When the submitter attempts to submit the step without attaching a receipt Then the submission is blocked and an inline error "Receipt photo required" is shown And the "Submit" action remains disabled Given the submitter attaches a receipt image via mobile capture or upload When OCR runs Then OCR completes within 5 seconds for 95% of images under 10 MB And the extracted date is within ±3 calendar days of the transaction date And the extracted amount equals the transaction amount within $0.01 And the extracted merchant name fuzzy-matches the transaction merchant with similarity ≥ 0.80 And all validations pass Then the "Submit" action becomes enabled Given any OCR validation fails When the user views the attachment row Then failed fields are highlighted with reasons And the user is prompted to retake, replace, or edit transaction fields (if permitted by policy) And submission remains blocked until all required validations pass or an approved exception is recorded
Mileage Log Enforcement With Exception Path
Given a mileage reimbursement playbook step requires artifact type "Mileage Log" and exception policy "Justification Required" When the submitter attempts to submit without attaching a mileage log Then submission is blocked with error "Mileage log required" and a "Request Exception" option is presented Given the submitter clicks "Request Exception" When they select a reason from the configured list and provide an explanation of at least 50 characters Then the exception request can be submitted And the step routes to the configured approver delegate And the step is labeled "Exception Pending" Given the approver reviews an exception When they approve it Then the submission gate is lifted for this step without the mileage log And the audit log records approver, timestamp, reason, and step id When they reject it Then the step remains blocked and the submitter is notified with the rejection reason
Client Contract Required for Mixed‑Use Expense
Given a mixed-use expense step requires artifact type "Client Contract" with policy checks: Client match, Coverage dates, Signature presence When a contract PDF is attached Then OCR/text extraction completes within 10 seconds for 95% of files under 20 MB And the detected client name matches the project/client on the transaction with similarity ≥ 0.85 And the contract coverage dates include the transaction date And at least one signature indicator is detected (e.g., "Signed", signature image, or e-sign metadata) Then the validation status is "Passed" and submission can proceed Given validation fails When the submitter attaches an alternative allowed artifact type "Statement of Work" Then the same policy checks apply And if passed, submission can proceed Else submission remains blocked until a valid artifact is provided or an approved exception is recorded
Bank Transaction Link Attachment For Reimbursables
Given a reimbursable expense step requires artifact type "Bank Transaction Link" When the submitter adds a link Then the link must resolve to an existing TaxTidy transaction ID belonging to the same user or workspace And the transaction currency and amount must match the expense within $0.01 And the transaction date must be within ±7 calendar days of the expense date And the link is verified reachable within 2 seconds Then the attachment is accepted and locked to the step Given the link is invalid, unreachable, or references a transaction already linked to another reimbursable claim When the submitter attempts to save Then an error is shown with the specific cause and the attachment is rejected
Duplicate Attachment Detection And Prevention
Given any attachment is uploaded to a playbook step When its checksum (SHA-256) matches an existing attachment on any step for the same transaction within the last 365 days Then a duplicate warning is shown and the upload is blocked Unless the playbook allows duplicates with justification, in which case the submitter must provide a reason of at least 30 characters and explicitly confirm "Use duplicate" Given the checksum matches an attachment linked to a different transaction within the last 365 days When uploaded Then a warning is shown And the upload proceeds only if the file metadata (date, amount, merchant extracted via OCR) differs by at least one field; otherwise require justification as above Given a duplicate is blocked or overridden When the event occurs Then an audit log entry is recorded with checksum, source attachment id, actor, timestamp, and decision (blocked/overridden)
Secure Storage, Linkage, And Tax Packet Inclusion
Given an attachment is accepted on a step When it is stored Then it is encrypted at rest And stored with metadata: attachment_id, transaction_id, playbook_step_id, uploader_id, upload_timestamp (UTC), content_type, size_bytes, checksum And access is limited to the transaction owner, assigned approvers, and workspace admins; unauthorized users receive HTTP 403 Given a tax packet is generated for a period including the transaction When the packet is built Then all accepted attachments linked to included transactions are embedded or linked in the packet with annotations containing step name, approver outcome, and validated OCR fields And the packet includes a completeness summary showing 100% of required artifacts present or approved exceptions for any missing items Given an attachment is replaced or deleted before packet generation When the change occurs Then a non-destructive version history is kept And the latest version is used in the packet And the audit log captures who, when, and why
Auto-Assignment & Event Triggers
"As a freelancer using bank feeds and receipt uploads, I want playbooks to start automatically when relevant items appear so that I don’t have to remember to initiate reviews."
Description

Automatically trigger appropriate playbooks based on ingestion events and classifications (e.g., new expense categorized as reimbursable, mileage entry detected, mixed‑use purchase flagged, project reclass request). Auto‑assign tasks with due dates, priorities, and SLA timers; send push/email notifications with deep links; and batch or throttle alerts to prevent notification fatigue. Provide a trigger debugger to preview which playbook would fire for a given transaction. Expected outcome: fewer missed approvals and faster processing without manual kickoff.

Acceptance Criteria
Reimbursable Expense Trigger and Auto-Assignment
- Given a new transaction is ingested and classified as Reimbursable, when it is saved, then the Reimbursable Approval playbook is instantiated exactly once and linked to the transaction. - Then tasks are auto-assigned to the configured approver or their active delegate, with due date computed per policy SLA (e.g., 2 business days) from trigger timestamp and priority set to High. - Then an SLA countdown timer is started and displayed on the task; time zone handling reflects the assignee’s locale. - Then push and email notifications are sent within 60 seconds to the assignee, each containing a deep link that opens the first actionable step with the transaction context preloaded on mobile and web. - Then an audit log entry records trigger source, playbook ID, assignee, due date, SLA, and notification message IDs. - And if the approver is marked out-of-office with a delegate, the delegate is the assignee and receives the notifications, with the routing noted in the audit log.
Mileage Entry Detected and Validated
- Given a transaction is recognized as mileage (source is mileage tracker or category = Mileage), when it is ingested, then the Mileage Verification playbook is triggered and linked to the transaction. - Then required attachment rules enforce a mileage log (photo, PDF, or GPS trace); if missing, the system assigns a request-to-user step and blocks approval until the attachment is provided. - Then the current tax-year per‑mile rate is applied to compute the reimbursement amount and displayed; if amount exceeds policy caps, the step is flagged and justification becomes mandatory. - Then the first step is auto-assigned to the submitter (to attach/confirm), the next to the approver; due dates are set per SLA for each step. - Then notifications are sent with deep links to the pending step(s); opening the link lands on the exact sub-step. - And if a duplicate mileage event with the same trip ID and date is received within 24 hours, no second playbook is created; a dedup audit entry is recorded.
Mixed-Use Purchase Allocation and Approval
- Given a transaction is flagged as Mixed‑Use by classification rules, when it is saved, then the Mixed‑Use Allocation playbook starts. - Then the system requires entry of business and personal percentages summing to 100% and a receipt image/PDF before allowing progression; validation errors are shown inline. - Then the system auto-calculates split ledger entries for business and personal portions and shows a preview. - Then an approval step is auto-assigned to the configured approver; if business portion exceeds the policy threshold, a justification field is mandatory. - Then SLA timers are set per step; 4 hours before SLA breach, an escalation notification is sent to the approver and CC to finance. - Then all actions (percentages, receipt file hash, approvals) are captured in the audit trail.
Project Reclass Request Routing and Execution
- Given a user requests a project change on a transaction, when saved, then the Project Reclass playbook is triggered and linked to the transaction. - Then the system validates the new project code exists and a reason is provided; otherwise the step cannot be submitted. - Then the approver is selected based on project ownership; if no owner is found, route to default Finance Approver; routing decision is logged. - Then upon approval, the system posts the reclass entry, updates the transaction’s project reference, and records before/after project codes in the audit log. - Then push/email notifications include deep links to the approval step and the final confirmation screen; due dates adhere to the configured SLA. - And if the request is rejected, the playbook closes with status Rejected and a rejection reason is required and stored.
Notification Batching and Throttling Controls
- Given multiple playbooks trigger tasks for the same user within a 10‑minute window, when sending push notifications, then a single summary push is sent per playbook type with a count of pending items and grouped deep links. - Then push notifications per user are capped at 6 per hour; additional alerts are rolled into an hourly digest without losing deep-link access. - Then emails generated within a 5‑minute window are combined into one email per user with grouped sections by playbook type. - Then SLA‑breach alerts bypass batching but still respect the hourly cap; breaches are always sent within 60 seconds of detection. - Then tapping a summary deep link opens a filtered inbox showing the grouped items; individual item deep links navigate to the specific step.
Trigger Debugger Playbook Preview and Trace
- Given a sample transaction (manual entry or JSON payload), when the Trigger Debugger is run, then the system shows the playbook that would fire, matched conditions, assignees, computed due dates, and notification previews without creating records or sending notifications. - Then a rule evaluation trace lists each condition with pass/fail and the final decision; the trace is exportable as JSON. - Then if multiple playbooks match, priority order and tie‑break rationale are displayed; the selected playbook is clearly indicated. - Then running the debugger against an existing transaction shows whether a playbook already fired and provides a link to the existing instance; re‑trigger is disabled unless explicitly forced with admin permission.
Audit Trail & Decision Rationale
"As a user who may be questioned later, I want a clear audit trail and rationale for each approval so that I can quickly justify deductions to my CPA or the IRS."
Description

Record an immutable audit trail for every playbook run: timestamps, actor identities, policy check outcomes, comments, exceptions with justifications, version of the playbook used, and the exact attachments reviewed. Expose a step‑by‑step timeline and generate an exportable summary that rolls into the annotated tax packet. Support redaction for PII where necessary while preserving evidentiary integrity. Expected outcome: defensible decisions and reduced time responding to audits or preparer questions.

Acceptance Criteria
Immutability and Tamper Detection
Given a playbook run is active or finalized When an audit event is recorded Then the event is written append-only with a monotonically increasing sequence ID and server UTC timestamp with millisecond precision And the event includes a SHA-256 content hash and a hash-chain pointer to the previous event And any attempt to modify or delete an existing event results in a new correction event; the original remains unchanged and visible And an integrity verification endpoint returns PASS for an untampered trail and FAIL with the first offending sequence ID for a tampered trail And integrity verification completes in under 2 seconds for trails up to 1,000 events
Complete Event Capture for Playbook Run
Given a playbook run includes steps, policy checks, attachments, comments, and approvals When users progress through steps, execute checks, upload or view attachments, add comments, approve or reject, and raise exceptions Then each action produces an audit event containing: actor user ID and role, authentication method, client timestamp and server UTC timestamp, step ID and name, action type, previous and new state, and playbook version (semantic version and commit hash) And policy check events include rule IDs, inputs snapshot, outcome (PASS/FAIL), and messages And exception events include exception code and non-empty justification text of at least 10 characters And attachment events include file ID, filename, MIME type, size (bytes), SHA-256 hash, source (invoice/bank/receipt), and view/download timestamps And 100% of steps have start and end events, and the event order is stable and strictly increasing by sequence ID
Step-by-Step Timeline UI
Given a user with permission opens the audit timeline for a run on mobile or desktop When the timeline loads Then events are displayed in chronological order grouped by step, with filters for actor, action type, outcome, and exceptions And policy outcomes are visually labeled (PASS/FAIL), comments are expandable, and attachments show preview thumbnails with a click-through to an immutable snapshot And the initial view renders within 1,500 ms for trails up to 300 events on a 3G Fast profile, and subsequent pages of 100 events load within 500 ms And a deep link to any event can be copied and, when opened by an authorized user, scrolls to and highlights that event And redaction mode can be toggled on/off (default on for non-PII roles), and the toggle action is logged as an audit event
Exportable Audit Summary and Packet Integration
Given a playbook run is finalized When a user exports the audit summary Then the system generates a ZIP containing: summary.pdf and summary.json, plus a manifest.json listing every attachment (file ID, filename, size, SHA-256, content-addressed URI) And summary.json includes run metadata, playbook version, event counts by type, all exceptions with justifications, approver identities and timestamps, and policy check outcomes And the export ZIP is referenced in the annotated tax packet with a stable ID and SHA-256 hash recorded in both the packet index and the audit trail And export completes within 10 seconds for runs with up to 300 events and a manifest of up to 200 attachments And a consumer can verify the ZIP integrity by recomputing the SHA-256 to match the recorded hash
PII Redaction with Evidentiary Integrity
Given PII redaction rules are configured (e.g., SSN, bank account numbers, email, phone) When viewing the timeline or exporting with redaction enabled Then all fields matching configured rules are masked in UI and in summary.pdf, and attachment previews use redacted derivatives; originals remain unchanged in storage And the manifest records both original file hash and redacted derivative hash, linking them by file ID And enabling or disabling redaction requires the View PII permission; the action and actor are logged as audit events And redaction details (rule IDs applied, fields affected, count of tokens masked) are captured in the audit trail for the export/view operation And no unmasked PII is displayed to users without View PII permission in any UI or export endpoint
Access Control and Share Links
Given role-based permissions for View Audit Trail and Export Audit Trail When an unauthorized user attempts to view or export a run’s audit Then the request is denied with HTTP 403 and an access attempt is logged with user ID, IP, and timestamp And when an authorized user creates a share link, they must set scope (view/export), redaction mode, and expiration (max 30 days); the link token is single-tenant and non-guessable (>=128-bit entropy) And all access via share links is logged, including recipient principal when authenticated, or token ID when not And revoking a share link invalidates it within 60 seconds across all edges, and subsequent use returns HTTP 410 And audit and access logs are retained for at least 7 years and are exportable on request
Mobile-First Approval Experience
"As a freelancer who works primarily on my phone, I want fast, simple approval actions with receipt capture so that I can keep my approvals moving between gigs."
Description

Deliver a responsive, mobile‑optimized approval UI with offline capture for receipts, quick approve/return actions, comment threads, and checklist progress. Support push notifications, biometric login for step confirmation, and deep links to specific approval steps. Ensure accessibility and low‑latency performance on typical cellular connections. Expected outcome: approvals happen quickly on the go, reducing cycle time and drop‑offs.

Acceptance Criteria
Quick Approve/Return from Mobile Push Notification
- Given a pending approval and notifications enabled, When a push notification for that item is delivered, Then the notification includes Approve and Return actions and displays the submitter, amount, and top required attachment indicator. - Given the user taps Approve from the notification, When biometric/PIN confirmation completes successfully, Then the approval is submitted within 2 seconds on LTE and the item's status updates to Approved with a confirmation shown in-app on next open. - Given the user taps Return from the notification, When they enter a mandatory comment of at least 5 characters, Then the return is submitted within 3 seconds on LTE and the submitter is notified via push/email. - Given the device is locked, When an action is attempted from the notification, Then the OS unlock or biometric prompt is required before submission. - Given a temporary network failure during submission, When retry policy runs, Then the app retries up to 3 times within 90 seconds and ensures idempotent single submission.
Offline Receipt Capture and Sync
- Given the device is offline (airplane mode), When the user captures a receipt photo from the approval step, Then the image is saved locally encrypted (AES-256), compressed to <= 1.5 MB, and the upload is queued. - Given connectivity is restored, When the app is foregrounded or background fetch occurs, Then queued receipts upload within 15 seconds and attach to the correct step; OCR starts within 5 seconds of upload and extracted fields populate the checklist. - Given a duplicate receipt is captured, When the content hash matches an existing attachment, Then the app prevents duplicate upload and informs the user. - Given an upload fails server-side, When retry policy runs, Then exponential backoff attempts up to 5 times and surfaces a retriable error with a Retry button.
Biometric Confirmation for Approval Step
- Given a playbook step is marked Biometric Required, When the user taps Approve or Return, Then the native biometric prompt (Face ID/Touch ID/Android Biometrics) appears. - Given biometric fails 3 times or is unavailable, When the user chooses fallback, Then a 6-digit app PIN is required; no bypass is permitted. - Given the user successfully authenticates, When the action is submitted, Then the approval/return completes and the audit log records the auth method and a server-synced timestamp. - Given the device has no biometrics enrolled, When the user first attempts an action, Then the app prompts to set an app PIN before continuing.
Deep Link to Specific Approval Step
- Given a valid deep link to a specific approval step, When the user taps it from email or push, Then the app opens directly to that step within 2 seconds and scrolls to the first incomplete checklist item. - Given the user is not authenticated, When the deep link is opened, Then the sign-in screen appears and, after successful auth, the user is routed back to the intended step. - Given the app is not installed, When the link is opened, Then the user is routed to the app store; after install and first open within 30 minutes, the link target is restored. - Given iOS and Android platforms, When the link is opened, Then Universal/App Links are verified (no browser interstitial) and handled in-app.
Comment Threads with Mentions and Attachments
- Given an approval step, When a user posts a comment, Then it appears instantly in the thread and persists after app restart. - Given a comment includes an @mention of a teammate, When posted, Then the mentioned user receives a push notification within 60 seconds and the comment displays a mention chip. - Given an attachment is added to a comment, When the file is <= 10 MB (jpg, png, pdf), Then it uploads (offline queued if needed), passes virus scan, and renders a preview on mobile. - Given a user edits or deletes their own comment, When within 10 minutes of posting, Then the thread shows Edited/Deleted with timestamp and the audit log retains original text.
Checklist Progress Visibility and Validation
- Given required items defined by the playbook (attachments, policy checks, approvers), When viewing the step on mobile, Then a checklist displays each item with status (Required, Optional, Completed, Error). - Given required items are incomplete, When the user attempts to Approve, Then the Approve button is disabled and a tooltip lists missing items. - Given all required items are satisfied and policy checks pass, When the user returns to the step, Then the Approve button becomes enabled and the progress shows 100% with a green state. - Given the user completes an item, When synced, Then progress state persists across sessions and devices within 10 seconds.
Mobile Performance and Accessibility Compliance
- Given a typical cellular connection (400 ms RTT, 1.5 Mbps), When the approval step screen is opened, Then time-to-interactive is <= 1.2 seconds and total downloaded payload <= 300 KB compressed. - Given a user taps any primary control (Approve, Return, Add Receipt), When the action sheet opens, Then visual response latency is <= 100 ms. - Given VoiceOver/TalkBack is enabled, When navigating the approval step, Then all interactive elements are accessible with labels, focus order is logical, contrast ratios >= 4.5:1, and tap targets are >= 44x44 pt. - Given a network error occurs, When loading the step, Then an error state is shown within 2 seconds with a Retry option and an offline indicator if applicable.

Access Radar

A live dashboard of delegate access and activity health. See inactive roles, unusual hours/locations, expiring timeboxes, and pending approvals at a glance. Get proactive nudges to tighten scopes or revoke dormant access, keeping your workspace tidy and safe.

Requirements

Unified Access Inventory
"As a workspace owner, I want a single, live view of who has access to what so that I can quickly assess and correct risky permissions without hunting through settings."
Description

Aggregate and display a real-time catalog of all delegates, roles, scopes, timeboxes, and connection sources across TaxTidy (e.g., invoice inbox, bank feeds, receipt capture, tax packet export). Provide a single dashboard view with per-delegate cards showing role, granted scopes, timebox start/end, last active timestamp, last action type, and connection origin (invite, link, OAuth). Pull data from the permissions service and event stream to keep status live; deduplicate identities across email, OAuth provider, and device IDs. Include filters (role, scope, risk, inactivity), search, and sortable columns. Surface status badges for inactive roles, expiring timeboxes, pending approvals, and unusual activity flags. Enforce least-privilege visibility: owners see all; delegates see only their own status. Expose a read-only API endpoint for inventory export to CSV/JSON.

Acceptance Criteria
Owner views live unified access inventory
Given I am an Owner user When I open the Access Radar inventory view Then I see one card per unique delegate with the fields: Role, Granted Scopes, Timebox Start (ISO-8601), Timebox End (ISO-8601), Last Active Timestamp (ISO-8601), Last Action Type, Connection Origin (invite|link|OAuth), Connection Source (invoice inbox|bank feeds|receipt capture|tax packet export) And each card’s identity is deduplicated across email, OAuth provider ID, and device IDs And no duplicate cards for the same person are present Given a delegate’s role/scope/timebox is changed in the permissions service When the change is committed Then the corresponding card reflects the change within 15 seconds p95 Given a new activity event for a delegate is received from the event stream When it is emitted Then that delegate’s Last Active Timestamp and Last Action Type update on the card within 10 seconds p95 Given a workspace with 10,000 delegates When the page first loads Then initial render completes in ≤2.0 seconds p95 and subsequent in-view updates complete in ≤400 ms p95
Filter by role, scope, risk flag, and inactivity
Given I have a populated inventory When I apply Role = {any subset} Then only delegates with those roles are shown Given I apply Scope = {any subset} When combined with a Role filter Then results reflect the intersection across filter groups (AND) and union within a group (OR) Given I apply Risk = Flagged When the filter is active Then only delegates with any active status badge of Unusual Activity or Pending Approval are shown Given I apply Inactivity ≥ N days (e.g., N=30) When the filter is active Then only delegates whose Last Active Timestamp is older than N days are shown Given multiple filters are active When I clear all filters Then the inventory resets to the full, unfiltered set And filter chips and counts update accordingly Given any filter action When results update Then response time is ≤400 ms p95 for up to 10,000 delegates
Search and sortable columns work together
Given I enter a search term When I search Then results match case-insensitive substring across delegate display name, email, OAuth provider ID, device ID, and Connection Origin/Source And the p95 time-to-first-result is ≤300 ms for up to 10,000 delegates Given results are displayed When I sort by any of: Role, Last Active Timestamp, Timebox End, Connection Origin, Connection Source, Delegate Name, Risk Status Then sorting is stable, ascending/descending toggles are available, and sorting composes with active filters and search Given pagination is enabled When I change sort or search Then the current page resets to page 1 and the total count reflects the sorted/searched set
Status badges for inactive, expiring, pending, unusual activity
Given a delegate’s role has no activity events for ≥30 consecutive days When the card is displayed Then an Inactive badge appears on the role Given a timebox end date is within 7 calendar days When the card is displayed Then an Expiring badge appears showing the number of days remaining Given one or more permissions requests for a delegate are awaiting approval When the card is displayed Then a Pending Approval badge appears with the count of pending items Given an activity event occurs between 22:00 and 06:00 in the delegate’s configured local time or from a country not seen for that delegate in the past 90 days When the card is displayed Then an Unusual Activity badge appears on the card Given multiple conditions are true When the card is displayed Then multiple badges render concurrently, each with accessible tooltip text explaining the condition
Least-privilege visibility enforced in UI and API
Given I am an Owner When I open the inventory or call the export API Then I can see/export all delegates in the workspace Given I am a Delegate (non-owner) When I open the inventory or call the export API Then I can only see/export my own delegate card and no aggregated totals beyond my record Given a Delegate attempts to access another delegate’s card via a deep link or API ID When the request is made Then the server responds 403 with error code ACCESS_DENIED and no sensitive fields are leaked Given the UI renders for a Delegate When data is fetched Then requests include the user context and server-side filtering enforces visibility (no client-side-only hiding)
Read-only export API for CSV/JSON with filter parity
Given the export endpoint GET /v1/access-inventory/export?format={csv|json} When called with the same filter/search/sort parameters as the UI Then the exported dataset matches the currently visible set and respects least-privilege rules Given format=csv When the response is returned Then it is UTF-8 encoded, RFC 4180 compliant with a header row, and includes fields: delegate_id, display_name, email, role, granted_scopes, timebox_start, timebox_end, last_active_ts, last_action_type, connection_origin, connection_source, badges Given format=json When the response is returned Then it is an array of objects with the same field names as the CSV header Given high-volume exports (≤100,000 rows) When requested Then the response is streamed, starts in <1s TTFB, and completes without truncation; Content-Disposition filename includes the current date YYYY-MM-DD Given a client sends POST/PUT/PATCH/DELETE to the export path When processed Then the API returns 405 Method Not Allowed Given a client exceeds 60 export requests per minute per token When the limit is surpassed Then the API returns 429 Too Many Requests with a Retry-After header
Mobile-first delegate card layout and accessibility
Given a mobile viewport of 375x812 When the inventory loads Then cards reflow to a single-column layout without horizontal scrolling and all required fields (Role, Scopes, Timebox, Last Active, Last Action, Origin, Source, Badges) are visible or accessible via an explicit expand control Given a screen reader is enabled When a card with badges is focused Then each badge has an aria-label describing the condition and severity, and color contrast meets WCAG AA Given keyboard navigation on desktop When tabbing through the grid Then focus order is logical, visible, and all controls (filters, search, sort toggles, export) are reachable and operable without a mouse
Inactivity & Expiry Monitor
"As a freelancer, I want dormant and expiring access clearly surfaced so that I can revoke or extend it proactively and reduce unnecessary risk."
Description

Continuously evaluate delegate activity and timeboxed access windows to detect dormant access and upcoming expirations. Define inactivity using event signals (login, document view, export, edit) within a configurable threshold (default 30 days), with timezone- and weekend-aware logic. Highlight dormant delegates and show days since last activity; flag timeboxes expiring within configurable windows (e.g., 3/7/14 days). Provide one-tap actions to revoke, pause, or extend access with preset durations and optional notes. Integrate with the inventory and notifications to surface counts and callouts. Include safeguards to avoid false positives during owner-defined blackout periods (e.g., tax off-season).

Acceptance Criteria
Detect Dormant Delegate via Event Signals
Given a workspace with the default inactivity threshold of 30 days and evaluation scheduled hourly And qualifying activity events are: login, document view, export, edit And a delegate’s last qualifying event occurred at T0 (workspace timezone) When no qualifying events occur for that delegate between T0 and T0 + 30 days Then the delegate is labeled “Dormant” in Access Radar with the last-activity timestamp recorded And the dormancy reason displays “No qualifying activity in 30 days” And if the workspace overrides the threshold to X days, the label applies at X days instead of 30
Timezone- and Weekend-Aware Inactivity Calculation
Given the workspace timezone TZ is configured And a delegate’s last qualifying event occurs near a day boundary or DST change in TZ When inactivity duration is computed Then the days-since-last-activity uses calendar days in TZ (not UTC) and is accurate across DST transitions And weekend days are counted consistently as calendar days in TZ (no off-by-one errors across weekends) And the evaluation timestamp displayed reflects TZ
Flag Upcoming Timebox Expirations
Given a delegate with a timeboxed access end at E in the workspace timezone And the expiration warning windows are configured to 3, 7, and 14 days (editable per workspace) When now is within any configured window before E Then the delegate is flagged as “Expiring in X days” where X matches the nearest window And the flag clears automatically if access is revoked or the end date is extended beyond all windows And changing the windows updates which delegates are flagged on the next evaluation cycle
Display Days Since Last Activity
Given a delegate marked Dormant When the Access Radar dashboard renders Then the delegate row shows an integer value for “Days since last activity” derived from the last qualifying event in the workspace timezone And if the delegate has never generated a qualifying event, the UI displays “Never” instead of a number And the value updates at most once per day and not more than 24 hours out of date
One-Tap Revoke, Pause, or Extend with Presets and Notes
Given a delegate listed as Dormant or Expiring When the owner taps Revoke Then the delegate’s access is revoked and an audit log entry is recorded with actor, timestamp, and optional note When the owner taps Pause with a preset duration (7, 14, or 30 days) Then the delegate’s access is suspended for the selected duration and an audit log entry is recorded When the owner taps Extend with a preset duration (7, 14, or 30 days) Then the delegate’s timebox end is extended accordingly and an audit log entry is recorded And all actions update the dashboard state within 1 minute
Owner-Defined Blackout Period Safeguard
Given the owner configures a blackout period with start and end in the workspace timezone When the evaluation runs within the blackout period Then no new delegates are labeled Dormant and no Dormant notifications are sent And timebox expiration flags continue to function normally And a visible indicator shows “Blackout active” with the configured dates
Inventory and Notifications Integration
Given Access Radar identifies N Dormant delegates and M Expiring delegates When the inventory module aggregates access health Then it surfaces counts matching N and M with the same filters applied When notifications are enabled Then the owner receives a summary that includes counts and deep links to affected delegates when N or M changes And clicking a notification opens Access Radar filtered to the relevant cohort
Anomaly Detection for Unusual Activity
"As a workspace owner, I want to be alerted to sign-ins or data access from atypical locations or hours so that I can verify or revoke access quickly."
Description

Detect and score unusual access patterns based on hour-of-day, day-of-week, geo-region/IP, device fingerprint, and data access volume relative to each delegate’s historical baseline. Use coarse geolocation (country/region/city) derived from IP, preserving privacy by not storing precise coordinates. Provide risk levels (low/medium/high) with reasons (e.g., first-time login from new country at 02:11; 10× export volume). Suppress noise with learning windows, known-travel tags, and allowlist of trusted networks. Surface anomalies in the dashboard, trigger alerts to owners, and offer inline actions: verify, require re-auth/2FA, or revoke. Log all detections and outcomes for model tuning and auditing.

Acceptance Criteria
High-Risk Login From New Country At Unusual Hour
Given a delegate with ≥14 days of baseline activity including a 95% active-hour interval of 09:00–19:00 local time and baseline countries = {US} And a device fingerprint history with last-seen dates When a login occurs at 02:11 local time on a Sunday from country = DE using a device fingerprint not seen in the past 60 days Then the system assigns a risk score ≥ 80 and risk level = high And reasons include first-time login from new country, login at 02:11 outside usual hours, new device fingerprint And only coarse geolocation (country/region/city) derived from IP is stored and displayed; no precise coordinates are stored
Learning Window Suppresses Early Noise
Given a new delegate with a learning window set to 14 days (configurable 7–30) When logins and data accesses occur during the learning window Then anomalies for hour-of-day, day-of-week, geo, and device are suppressed unless the computed risk score would be ≥ 90 And baseline profiles for hour-of-day, day-of-week, geo, device, and access volume are accumulated And after day 14 the normal anomaly thresholds activate automatically without manual intervention
Known-Travel Tag Mutes Expected Anomalies
Given a delegate has an active known-travel tag for 2025-09-10 to 2025-09-15 covering countries {US, DE} When a login occurs from DE at 01:00 on 2025-09-12 with a previously seen device Then geo/time-based anomaly contributions are suppressed or the resulting event is downgraded to risk level = low And reasons include known-travel window active And no owner alert is sent for low-risk events And the event is recorded in the audit log with suppression reason = known-travel
Trusted Network Allowlist Reduces False Positives
Given the organization allowlist contains CIDR 203.0.113.0/24 and SSID Office-WiFi When an access event occurs from IP 203.0.113.55 while connected to Office-WiFi Then geo/IP-based anomaly signals are neutralized and do not increase the risk score And an anomaly is only surfaced if other signals alone produce a risk score ≥ 80 And reasons include trusted network allowlist when suppression occurs
High Data Export Volume Detection And Response
Given a delegate’s 30-day baseline export volume mean = 200 records/day and standard deviation = 50 When the delegate exports 2,500 records within a single hour or the daily export volume exceeds 10× the baseline mean Then an anomaly is generated with risk level = high and reasons include 10× export volume with actual vs baseline metrics And an alert is sent to workspace owners within 60 seconds of detection And the anomaly presents inline actions: Verify, Require re-auth/2FA, Revoke
Dashboard Surfacing Of Anomalies With Risk And Reasons
Given an anomaly has been generated When it is displayed on the Access Radar dashboard Then the row shows delegate name, event time, risk level (low/medium/high), numerical risk score (0–100), top 3 reasons, geo (country/region/city), IP, device fingerprint label, and incident status (open/verified/remediated) And only coarse geo (country/region/city) is stored and displayed; no precise coordinates are persisted or shown And selecting the row opens a detail view with full reason list, historical comparisons, and available actions
Inline Remediation And Comprehensive Audit Logging
Given an open anomaly is visible to a workspace owner When the owner selects Require re-auth/2FA Then the delegate is prompted for step-up 2FA on the next sensitive action or next login, and the incident status updates to awaiting verification And when the owner selects Revoke, all active tokens for the delegate are invalidated within 10 seconds and new access attempts are blocked And when the owner selects Verify, the incident is marked verified and no further alerts are sent for that incident And all detections and actions are persisted in the audit log with detection_id, delegate_id, timestamps, features used, risk_score, risk_level, reasons, action, actor, outcome, and are queryable via the audit interface
Pending Approvals & Scoped Changes
"As an account owner, I want incoming access requests summarized and actionable in one place so that I can keep work moving without oversharing permissions."
Description

Provide an approvals center where owners can review and act on new delegate invitations, scope expansions, role changes, and timebox extensions. Present concise request summaries (who, requested scope/role, justification, requested duration, risk impact) with one-tap approve/deny and required reason on deny. Offer least-privilege templates and suggested shorter durations based on task type (e.g., receipt upload only, bank feed read-only). Apply approved changes immediately with versioned audit entries; denied requests notify the requester with rationale. Integrate with notifications (in-app, email, push) and support bulk actions while preventing over-broad approvals via confirmation gates.

Acceptance Criteria
Pending Requests Summary View
Given I am a workspace owner with at least one pending request When I open the Approvals Center Then every request card displays requester name and email, request type, current role/scope (if applicable), requested scope/role, requested duration in human-readable format, justification preview (first 140 characters with "See more"), risk impact label, and submitted timestamp And Approve and Deny actions are visible for each card
Approve Request: Immediate Application + Audit
Given there is a pending access request When I tap Approve on the request Then if the request exceeds recommended scope or duration, a confirmation gate warns of over-broad access and requires explicit Confirm to proceed And upon confirmation (or if not over-broad), the change is applied within 5 seconds And the request is removed from Pending and appears in Approval History And a versioned audit entry is written including request_id, approver_id, decision=approved, previous and new scope/role/duration, timestamp, and version increment And the requester is notified of the approval
Deny Request Requires Reason
Given there is a pending access request When I tap Deny on the request Then the system requires a non-empty denial reason (minimum 5 characters) before I can submit And upon submission no access changes are applied And the request moves to Approval History with decision=denied And a versioned audit entry is written including request_id, approver_id, decision=denied, rationale, and timestamp And the requester is notified with the denial rationale
Least-Privilege Templates & Suggested Durations
Given a pending request for new access or scope expansion When I open the request details Then the system displays least-privilege template suggestions based on task type (e.g., receipt upload only, bank feed read-only) And the system suggests a shorter duration when the requested duration exceeds the recommended maximum And selecting Apply Suggestion updates the pending request preview and recalculates the risk impact before approval And I can approve with the suggested template/duration in one tap
Bulk Approve/Deny with Confirmation Gates
Given there are multiple pending requests and I select two or more items When I choose Approve Selected or Deny Selected Then a confirmation dialog summarizes the total count and flags any high-risk or over-broad items And I must explicitly Confirm to proceed And for bulk Deny the system requires a single denial reason to apply to all selected items (editable per item before submit) And the system processes each item individually, applying approved changes within 5 seconds per item and creating individual versioned audit entries And I see a result summary indicating success/failure per item with retry options for any failures
Timebox Extension Approval Flow
Given there is a pending timebox extension request When I open its details Then I see the original expiry, the requested new expiry, and a suggested shorter duration if applicable And when I Approve, the new expiry takes effect within 5 seconds and a versioned audit entry is created with old and new expiry values And when I Deny, the existing expiry remains unchanged, a denial reason is required, and the requester is notified with the rationale
Notifications for Requests and Decisions
Given I am a workspace owner with in-app, email, or push notifications enabled When a new access request is submitted for my workspace Then I receive a notification on each enabled channel with a deep link to the Approvals Center within 60 seconds And when I approve or deny a request, the requester receives a decision notification on each enabled channel within 60 seconds that includes the denial rationale when applicable
Proactive Nudges & Suggestions
"As a busy freelancer, I want concise, actionable reminders with one-tap fixes so that my workspace stays tidy without constant monitoring."
Description

Implement a rules engine that generates timely, low-noise nudges to tighten scopes, revoke dormant access, shorten long timeboxes, enable 2FA for delegates, or confirm unusual activity. Provide configurable frequency (instant, daily digest, weekly digest) and channels (in-app, email, mobile push). Include clear, contextual suggestions with predicted impact (e.g., removing export scope reduces risk score by 20%) and deep links to one-tap actions. Respect user notification preferences and throttle to avoid alert fatigue. Track nudge outcomes to improve future recommendations and measure reduction in dormant access and over-scoped permissions.

Acceptance Criteria
Nudge Generation for Risky Access Patterns
Given a delegate has had no recorded activity in the last 30 days OR holds a role with at least one high‑risk permission beyond recommended scope, When the event is detected or the nightly batch runs at 01:55–02:00 UTC, Then a nudge is created within 5 minutes (event) or by 02:05 UTC (batch) including reason, suggested action (tighten scope/revoke/shorten timebox), predicted risk reduction (%) and severity. And no nudge is created if the delegate has activity within the last 30 days and no over-scope exists. And the nudge contains a deep link to the relevant one-tap action with delegate and role pre-selected.
Notification Frequency and Channel Configuration Enforcement
Given a user sets notifications to Weekly Digest via Email only and defines Quiet Hours 22:00–07:00 local time, When nudges are generated, Then no instant in-app or push notifications are sent and a single weekly email digest is delivered Mondays 09:00 local time containing all nudges from the prior week. Given a user sets Instant + In-app + Push, When a nudge is created, Then it is delivered on both channels within 60 seconds. Given a user unsubscribes from a nudge category (e.g., 2FA), When such nudges are generated, Then they are excluded from delivery but still logged for reporting.
Nudge Throttling and Noise Control
Given more than 3 instant-eligible nudges exist for a user in a rolling 24-hour window, When delivery is scheduled, Then send at most the 3 highest-severity nudges instantly and queue the remainder for the next digest. Given multiple nudges with the same target (same delegate and permission) are generated within 7 days, When delivery is planned, Then deduplicate to a single nudge and increment a count badge. Given a user snoozes a nudge for 14 days, When the snooze window is active, Then the same nudge is not reissued until after the window ends.
Actionability and One-Tap Deep Links
Given a nudge to revoke dormant access is delivered, When the user taps "Revoke now," Then the revoke page opens with the delegate and role pre-selected and the action completes in ≤1 confirmation step with success feedback within 2 seconds. Given the deep link token is expired or invalid, When the user taps the link, Then they are redirected to a safe fallback page with an error message and can re-initiate the action in ≤2 steps. Given a nudge to shorten a timebox, When the user taps "Shorten to 7 days," Then the new end date is pre-filled to 7 days from now and can be saved without additional manual entry.
Outcome Tracking and Recommendation Feedback
Given any nudge is delivered, When the user Acts, Dismisses, Snoozes, or Ignores (no interaction for 7 days), Then the outcome event is recorded with user ID, nudge ID, timestamp, and channel within 5 seconds of the event or detection. Given outcomes have been collected, When nightly model updates run, Then recommendations for nudges dismissed as not relevant are suppressed for 30 days and conversion rates are updated. Given at least one action was taken in the prior 30 days, When the Access Radar metrics panel is viewed, Then it displays percentage reduction in dormant access and over-scoped permissions compared to the previous 30-day period.
Unusual Activity and 2FA Enforcement Nudges
Given a delegate signs in from a country not seen for that delegate in the last 90 days or outside typical hours by >3 hours, When the event is detected, Then a high-severity confirmation nudge is sent to the workspace owner with "Confirm activity" and "Review access" actions. Given a delegate lacks 2FA, When any high-severity nudge is created for that delegate, Then include an additional suggestion to enable 2FA with predicted risk reduction ≥15% and a deep link to enable 2FA. Given the owner marks activity as suspicious, When the action is taken, Then the system suspends the delegate’s access and revokes active sessions within 60 seconds, pending owner re-approval.
Digest Composition and Grouping
Given a daily or weekly digest is generated, When compiling content, Then items are grouped by delegate, sorted by descending predicted risk reduction, include per-category counts, and limited to the top 20 items with a "view all" link. Given a nudge already appeared in a prior digest and remains unresolved, When compiling a new digest within 7 days, Then include it only if severity increased or new evidence exists; otherwise omit and show a summary count. Given the digest email is sent, When delivered, Then the subject is "[Access Radar Digest] YYYY-MM-DD — X items" and deep links render correctly in Gmail, Outlook, and Apple Mail.
Audit Trail & Compliance Export
"As a freelancer preparing for tax season or audits, I want an exportable record of access decisions and justifications so that I can demonstrate due diligence to clients or regulators."
Description

Maintain an immutable, time-stamped log of all access grants, scope changes, approvals/denials, verifications, revocations, and nudge outcomes, capturing actor, target, reason, and before/after state. Provide export options (CSV/PDF) with filters by date, delegate, and action type, including a summarized “Access Health” appendix suitable for clients or auditors. Redact sensitive fields (exact IP) while retaining sufficient evidence (region, device class). Sign exports with a hash and include a verification page for authenticity. Define retention and deletion policies aligned with TaxTidy’s data governance and allow owners to purge logs on workspace deletion.

Acceptance Criteria
Immutable Log Coverage and Field Completeness
Given Access Radar is enabled and audit logging is active And a workspace owner performs on delegate D: access grant, scope change, approval, denial, verification, revocation, and responds to a nudge When each action completes Then a distinct audit entry is appended per action with fields: timestamp (UTC ISO 8601), actor_id, actor_role, target_id, target_role, action_type, reason, before_state (JSON), after_state (JSON), request_id And entries are strictly append-only: any attempt to edit or delete an entry returns 403 and produces no change And entries are listed in reverse chronological order and are paginated consistently
Filterable CSV Export
Given at least 50 audit log entries exist across multiple dates, delegates, and action types When the owner requests a CSV export with filters: date range [start,end], delegate = D, action_types in [grant, revocation] Then only matching entries are included And the CSV includes a header row with all exported fields and excludes redacted raw fields And all timestamps are UTC ISO 8601 and sortable as text And the filename includes workspace_id and exported_at (UTC) timestamp And the export completes within 60 seconds for up to 10,000 matching rows
PDF Export with Access Health Appendix
Given audit log entries exist for date range R When the owner requests a PDF export for date range R Then the PDF includes: an Audit Log section with matching entries and an Access Health appendix summarizing counts by action type, inactive roles, unusual hours/locations events, expiring timeboxes, and pending approvals And the document has a table of contents, page numbers, workspace name, date range, and exported_at (UTC) And a verification page is included detailing export metadata and the cryptographic hash And the export completes within 60 seconds for up to 1,000 entries
Sensitive Data Redaction in Exports
Given audit entries include network and device data (e.g., IP address, user-agent) When generating CSV or PDF exports Then exact IP addresses are not present anywhere in the exports And a redacted "region" field (e.g., country/region) and a "device_class" field (e.g., Mobile/Desktop) are included instead And raw user-agent strings are not present; only device_class is shown And no redacted values are recoverable from file metadata or hidden content
Export Hash Signing and Authenticity Verification
Given a CSV or PDF export is generated When the SHA-256 hash is computed over the export file bytes Then the computed hash matches the value shown on the verification page (PDF) or verification.txt (CSV) And a detached .hash file is included containing: algorithm, hash value, workspace_id, date range, filters, and exported_at (UTC) And following the provided verification instructions on an altered export results in a verification failure
Retention and Deletion Policy Enforcement
Given the workspace retention policy is set to 7 years per TaxTidy governance And some audit entries are older than 7 years When the scheduled retention job runs Then entries older than the retention window are irreversibly purged and are no longer retrievable via UI or export And a system audit entry records the purge with cutoff date and count purged When a workspace owner deletes the workspace Then all associated audit logs are purged within 24 hours And any subsequent retrieval or export attempts return 404/Not Found for that workspace
Mobile-first Dashboard UX
"As a mobile-first user, I want a clear, fast dashboard with quick actions so that I can manage delegate access on the go between gigs."
Description

Design a responsive, mobile-first Access Radar experience with glanceable health indicators, color-coded risk chips, and swipeable cards for quick actions (approve, revoke, tighten scope, extend). Provide fast filters for risk level, inactivity, expiry window, and pending approvals. Optimize for performance on mid-range devices with incremental data loading and offline read cache of last-known state. Ensure accessibility (WCAG AA), haptics for critical actions, and dark mode support. Include contextual tooltips and inline education that explain statuses and suggested actions without overwhelming the user.

Acceptance Criteria
Glanceable Health Indicators & Inline Education
Given I open Access Radar on a mobile device with an active connection, when the dashboard renders, then each delegate card displays a health chip with label and icon for one of: OK, Watch, Risk. Rule: Health chips use a color-blind-safe palette and include a text label; no information is conveyed by color alone. Rule: If the backend flags unusual hours or unusual location, inactivity > 14 days, or a timebox expiring in ≤ 7 days, then a compact badge is displayed on the card. When I tap a chip or badge, then a tooltip appears within 300 ms explaining the status and offering the most relevant action (e.g., Tighten scope for Watch; Revoke for Risk) with a Learn more link. Rule: Tooltips are dismissible, non-blocking, and will not reappear for 30 days after dismissal; at most one educational nudge is shown per session.
Swipeable Cards with Quick Actions & Haptics
Given a delegate card is visible, when I swipe left ≥ 16 px, then action buttons Revoke and Tighten Scope are revealed; when I swipe right ≥ 16 px, then Approve and Extend are revealed. Rule: Tapping an action executes it; dragging past 60% of card width commits the focused action on release. Rule: Haptics: light feedback on action tray reveal; heavy feedback on committing Revoke; medium feedback on other commits. Rule: Destructive actions (Revoke) require a confirmation sheet; an Undo snackbar is shown for 5 seconds after commit. Rule: Optimistic UI applies within 150 ms; server confirmation arrives within 800 ms p95; on failure, the UI rolls back and an error with Retry is shown.
Fast Risk and Activity Filters
Given I open Filters, when I select risk levels (OK/Watch/Risk), inactivity windows (7/14/30+ days), expiry windows (<3, 3–7, >7 days), or Pending approvals, then the list updates within 150 ms without blocking scroll. Rule: Multiple filters can be combined; a pill summary with result count is displayed. Rule: Filter state persists across app restarts; deep links (e.g., ?risk=Risk&pending=true) reproduce the same filtered view. Rule: Clear All resets the list within 150 ms. Rule: Filters operate on cached data when offline and sync automatically on reconnect.
Incremental Loading on Mid‑Range Devices
Rule: On Pixel 6a and iPhone 11 class devices over 4G (300 ms RTT, 1.6 Mbps), p75 time to interactive ≤ 2.5 s for cold start. Rule: Initial dashboard data payload ≤ 200 KB gzipped; list items load in pages of 20 with skeleton placeholders visible within 100 ms. Rule: p95 next-page load latency ≤ 600 ms; p95 dropped frames during scroll ≤ 3%; no main-thread task > 100 ms during pagination. Rule: Peak memory for the dashboard ≤ 150 MB p95 while displaying up to 200 items.
Offline Read Cache of Last‑Known State
Given the device is offline, when I open Access Radar, then the last-synced dashboard is shown with a visible Last updated timestamp. Rule: Swipe actions are disabled offline with an explanation; filters and search operate against cached data. Rule: Pull-to-refresh while offline shows No connection within 300 ms; on reconnect, data auto-refreshes within 5 s or on manual refresh. Rule: Cache is encrypted at rest and expires after 30 days; if stale > 7 days, a warning banner is shown.
Accessibility (WCAG AA) Compliance
Rule: Text contrast ≥ 4.5:1 (normal) and ≥ 3:1 (large); interactive targets ≥ 44×44 dp with visible focus indicators. Rule: All controls expose accessible name, role, and state; list items have logical reading order; dynamic status changes are announced via polite live regions. Rule: VoiceOver/TalkBack users can reveal and activate quick actions without swipe gestures via accessible action menus. Rule: Respects Reduce Motion; animations are minimized and not required to complete tasks; tooltips are screen-reader accessible and dismissible.
Dark Mode Support with Meaningful Contrast
Rule: The dashboard follows the system theme by default with an in-app override; preference persists across sessions. Rule: Risk chip colors and icons remain distinguishable in dark mode with contrast ≥ 4.5:1 against surfaces; no color-only distinctions. Rule: No luminance flashes between theme transitions; images and illustrations adapt to maintain contrast. Rule: All states (hover, focus, pressed) remain visible in dark mode.

W‑9 AutoChase

Automatically request, collect, and validate W‑9s before you file. Send a secure mobile-friendly link to payees, auto-remind until complete, and attach the signed W‑9 to the payee record. Prevents last‑minute scrambles and eliminates incomplete filings.

Requirements

W‑9 Need Determination Rules
"As a freelancer who pays contractors, I want the app to decide who actually needs a W‑9 so that I don’t waste time chasing the wrong people or miss someone who does need one."
Description

Implements a rules engine that determines whether a payee requires a W‑9 based on tax residency, entity type, exemption status, and payment context. Uses existing TaxTidy payee profiles, invoice data, and country metadata to auto-detect US persons versus foreign entities, identify exempt recipients, and apply 1099-NEC eligibility thresholds. Surfaces a clear rationale to the user, supports manual override with justification, and routes foreign recipients toward W‑8 workflows. Drives downstream behavior by auto-creating or suppressing W‑9 requests and flagging payees as “W‑9 Required,” “Exempt,” or “Foreign—W‑8.”

Acceptance Criteria
US Individual Service Payee Exceeds $600 — W‑9 Required
Given a payee with tax residency "US" and entity type "Individual/Sole Proprietor" and no exemption And year-to-date eligible service payments via ACH/check/cash are >= $600 in the current calendar year And excluded payments (credit card/third‑party network), product purchases, and documented reimbursements are not counted When the rules engine evaluates the payee Then the payee decision state is set to "W‑9 Required" And a W‑9 request is auto-created and queued with reminders enabled And the decision rationale lists residency, entity type, YTD eligible amount, exclusions applied, and rule version/id And the decision snapshot is stored on the payee record with timestamp
US Corporation Not in Excepted Categories — Exempt
Given a payee with tax residency "US" and entity type in {C‑Corp, S‑Corp, LLC taxed as Corp} And the payment category is not "Legal services" and not "Medical/Healthcare" When the rules engine evaluates the payee Then the payee decision state is set to "Exempt" And no W‑9 request is created (suppressed) And the decision rationale explains corporate exemption and notes category checks passed
US Law Firm (Corp) Providing Legal Services — W‑9 Required Despite Corp
Given a payee with tax residency "US" and entity type in {C‑Corp, S‑Corp, LLC taxed as Corp} And the payment category is "Legal services" And year-to-date eligible payments are >= $600 in the current calendar year When the rules engine evaluates the payee Then the payee decision state is set to "W‑9 Required" And a W‑9 request is auto-created and queued with reminders enabled And the decision rationale cites the legal services exception to corporate exemption and shows threshold calculation
Foreign Payee Detected — Route to W‑8 and Suppress W‑9
Given a payee with country not equal to US or tax residency marked "Foreign" and no US person attestation on file When the rules engine evaluates the payee Then the payee decision state is set to "Foreign—W‑8" And any W‑9 request is suppressed/not created And the W‑8 workflow task/link is created and attached to the payee record And the decision rationale lists country, residency assessment, and explains routing to W‑8
Aggregation and Exclusion Rules for 1099‑NEC Threshold
Given multiple invoices and payments across the current calendar year with mixed categories and payment methods When the rules engine computes the year-to-date eligible amount Then it sums only service payments paid by non‑third‑party methods (e.g., ACH/check/cash) And it excludes product purchases, documented reimbursements, and payments made via credit card/third‑party networks And the computed eligible amount and exclusion breakdown are included in the rationale And the decision state reflects the comparison of computed amount against the $600 threshold
Manual Override with Justification and Audit Trail
Given an existing automated decision for a payee When an authorized user selects Override and chooses a new state from {"W‑9 Required","Exempt","Foreign—W‑8"} And enters a free‑text justification of at least 15 characters Then the decision state updates to the selected value And an audit log entry is created with user id, timestamp, prior state, new state, and justification And downstream actions are reconciled accordingly (create/cancel W‑9/W‑8 requests) And the UI marks the decision as Overridden and displays the justification
Decision Rationale Visibility and Traceability
Given the rules engine produces any decision for a payee When a user views the decision details in the UI or via API Then the rationale displays residency, entity type, exemption basis (if any), eligible amount with threshold comparison, payment method filters applied, category triggers, final state, and rule version/id And a contextual "Why?" link opens documentation for the rule set used And the rationale text is persisted with the decision snapshot for auditability
Secure Request Links & Access Control
"As a payor, I want to send secure, easy-to-open links to contractors so that their tax info stays protected while making it simple for them to complete the form on any device."
Description

Generates single-use, expiring, signed request links for payees to submit W‑9s via a mobile-friendly flow. Links are tokenized, carry no PII, and can be invalidated or reissued. Optional one-time passcode via email/SMS protects access. Enforces rate limiting and basic bot protection, logs access attempts for auditing, and supports custom branding. Ensures TLS in transit and encrypts tokens at rest. Integrates with TaxTidy notifications and payee records to track request state without exposing sensitive data.

Acceptance Criteria
Single-Use Expiring Tokenized Link Generation
Given an admin requests a W‑9, when TaxTidy generates a link, then the link contains a signed, opaque token with no PII and minimum 128-bit entropy. Given a generated link, when the default TTL (24 hours) elapses, then the link expires and returns HTTP 410 with an explanatory message and a re-request option. Given a generated link, when the admin sets a custom expiry between 1 and 14 days, then the link honors that TTL. Given a tokenized link, when the payee successfully submits the W‑9, then the link is immediately invalidated and cannot be reused. Given any link, when the signature or token is tampered with, then access is rejected with HTTP 401 and no sensitive details are revealed in the response.
Secure Mobile-Friendly W‑9 Submission Flow
Given a smartphone viewport 320–428px wide, when the payee opens the link, then the form renders without horizontal scrolling and maintains LCP ≤ 2.5s and TTI ≤ 3.8s on simulated 4G. Given the W‑9 form, when TIN/SSN/EIN is entered, then input is masked and client-side validation matches IRS format rules; values are only persisted on successful submission. Given the form, when a signature is captured, then touch and mouse input are supported and the stored signature file is ≤ 1 MB. Given submission with missing or invalid required fields, then inline error messages display per field and no partial data is saved. Given a successful submission, then the confirmation screen shows success without echoing back sensitive field values (e.g., full TIN).
Optional One-Time Passcode (OTP) via Email/SMS
Given OTP is enabled for a request, when the payee opens the link, then a 6-digit OTP is sent via the configured channel (email or SMS) and an OTP entry screen is shown. Given an OTP is sent, when 10 minutes elapse, then the OTP expires and any attempt to use it is rejected with an error prompting resend. Given OTP entry, when 5 consecutive failed attempts occur, then the link is temporarily locked for 15 minutes and an audit event is recorded. Given a valid OTP is entered, then access is granted and the OTP becomes single-use invalid. Given SMS delivery fails and fallback email is configured, then the OTP is sent via email and the payee is notified of the fallback.
Link Invalidation and Reissue Controls
Given an active link, when an admin selects Invalidate, then subsequent access returns HTTP 410 and the payee sees instructions to request a new link. Given an invalidated or expired link, when an admin selects Reissue, then a new unique tokenized link is generated and all prior tokens remain invalid. Given a reissue with Preserve history enabled, then the request timeline displays original and reissued events with timestamps and actor. Given a reissue with notifications enabled, then the payee receives the new link and the old link no longer functions. Given a reissued link, then no PII from the original request is embedded in the new URL.
Abuse Mitigation & Auditability (Rate Limiting, Bot Protection, Access Logs)
Given repeated access from the same IP, when requests exceed 30 GETs/minute or 10 POSTs/minute to the link, then further requests are throttled with HTTP 429 and a Retry-After header. Given a suspected automated client is detected, when risk signals trigger, then a human verification challenge is presented before form interaction is allowed. Given any access attempt (success, failure, expired, invalid signature), then an audit log entry is recorded with timestamp, masked IP (/24 IPv4 or /48 IPv6), user-agent hash, outcome, and request ID. Given the audit log is viewed by an authorized admin, then entries are filterable by request ID and exportable as CSV without exposing PII or token values. Given three HTTP 429 responses within 5 minutes from the same IP, then the IP is temporarily blocked for 15 minutes and an audit event is created.
TLS in Transit and Token Encryption at Rest
Given any HTTP request to the link endpoint, then it is redirected to HTTPS with HSTS enabled (max-age ≥ 15552000 seconds, includeSubDomains). Given TLS connections, then only TLS v1.2+ with approved cipher suites is accepted; weak protocols/ciphers are rejected. Given tokens stored at rest (databases, caches, backups), then they are encrypted using AES‑256‑GCM with KMS-managed keys rotated at least every 90 days. Given security scanning of the endpoint, then no mixed-content warnings or plaintext exposure of tokens is detected. Given access to system logs and backups, then tokens remain encrypted and unreadable without key access, verified via periodic key-separation tests.
Notifications & Payee Record State Tracking with Custom Branding
Given a request is created, then the payee record state updates to Requested with timestamp and no sensitive fields displayed. Given the payee opens the link, then the state updates to Opened with last access time. Given submission completes with signature, then the W‑9 is attached to the payee record and state updates to Completed with a document hash (not file contents) shown. Given notifications are enabled, when state changes to Requested, Opened, Completed, Expired, or Locked, then the configured TaxTidy channels send templated messages without PII or token values. Given custom branding (logo and primary color) is configured, when the link is opened, then branded assets render while link integrity, security headers, and content are unchanged.
Mobile W‑9 Payee Portal with Prefill
"As a contractor, I want a simple mobile form that already knows most of my details so that I can complete the W‑9 quickly without confusion or typos."
Description

Provides a responsive, accessible web flow that guides payees through the W‑9 with inline help, error checks, and real-time validation. Prefills known fields (name, business name, address, email) from the existing payee record or invoice data while allowing edits with change tracking. Supports both structured form entry and optional upload of an existing W‑9 PDF, extracting structured data where possible. Offers contextual explanations of tax classification choices and captures consent for electronic delivery. Localized copy and WCAG-compliant components ensure mobile-first usability.

Acceptance Criteria
Mobile Prefill on Portal Launch
Given a payee opens the secure W‑9 link on a mobile device (viewport width 320–414px) When the portal loads Then known fields (name, business name, address, email) are prefilled from the latest payee record or invoice data And unknown fields remain blank And prefilled fields are visually indicated as "Prefilled" without preventing edits And the page renders responsively with no horizontal scrolling And initial load Largest Contentful Paint ≤ 2.5s and Cumulative Layout Shift < 0.1 on a 4G network baseline
Editable Fields with Change Tracking
Given a prefilled field is edited by the payee When the value is changed Then the system records field name, old value (hashed for PII-sensitive fields), new value, timestamp, and actor = "Payee" And a non-intrusive "Edited" badge appears on the modified field And upon successful submission an audit record with the captured changes is attached to the payee record And the audit record can be retrieved via admin/API with a unique change ID
Real-time W‑9 Validation and Signature
Given the payee completes the W‑9 form fields When an entry violates W‑9 rules (e.g., missing required field, invalid TIN format, incompatible tax classification) Then an inline, accessible error message appears within 200ms and the field is marked invalid And the TIN input enforces 9 digits with SSN/EIN formatting and numeric mobile keyboard And the form cannot be submitted while errors remain; an error summary with anchors is shown at the top on submit And when all validations pass, the payee must certify and provide an e‑signature (typed name or touch signature) before submission And upon success, a confirmation screen with a reference ID is displayed and the signed W‑9 is attached to the payee record
PDF Upload with Data Extraction and Confirmation
Given the payee opts to upload an existing W‑9 When a PDF/JPG/PNG ≤ 10MB is uploaded Then the system extracts structured fields (name, business name, tax classification, address, TIN, signature date) where possible And extracted values are auto-populated into the form for review And low‑confidence or conflicting values are highlighted for the payee to confirm or edit before submission And if extraction fails or the file is unreadable, the payee is prompted to complete the structured form instead And both the original file and the confirmed structured data are stored and associated with the payee record
Tax Classification Guidance and Dependencies
Given the payee views the tax classification section When they open contextual help or change the classification Then plain‑language, localized explanations are displayed for each option And selecting "LLC" requires and validates the tax classification subtype (C‑Corp/S‑Corp/Partnership) And selecting "Individual/sole proprietor or single‑member LLC" enables SSN entry and disables EIN‑only validation And selecting Corporation/Partnership options requires EIN format validation
e‑Delivery Consent Capture and Audit Trail
Given the payee is ready to submit When they review the electronic delivery consent Then submission is blocked until the payee explicitly checks the consent checkbox And upon consent, the system records timestamp, IP, user agent, locale, and the consent text version And the consent record is attached to the payee profile and included in the submission receipt And the payee can download a PDF copy of the completed W‑9 and consent receipt And if consent is declined, the flow surfaces paper delivery instructions and marks the request as "Consent Declined" without submitting
Accessibility and Localization Compliance
Given a user with assistive tech or keyboard-only navigation accesses the portal in supported locales (en‑US, es‑US) When they navigate, complete, and submit the W‑9 Then all controls have accessible names/roles/states; focus order is logical; visible focus indicators are present And error messages are programmatically associated to fields and announced by screen readers And touch targets are ≥ 44px, and text scales to 200% without loss of content or functionality And automated accessibility tests report 0 critical violations (WCAG 2.2 AA) and manual checks for forms and color contrast pass And locale selection/detection updates all static copy and validation messages, with fallback to en‑US
E‑Signature & Audit Trail
"As a payor, I want a signed W‑9 with a verifiable audit trail so that I can prove compliance and avoid rejected filings or disputes."
Description

Captures legally compliant electronic signatures (typed or drawn) with explicit consent, producing an IRS-ready, flattened W‑9 PDF. Records a tamper-evident audit trail including signer identity attributes, timestamps, IP, and user agent, with a cryptographic hash for integrity verification. Stores signed artifacts in encrypted storage, associates them to the payee record, and enables secure download for both parties. Exposes an immutable event log for compliance reviews and dispute resolution.

Acceptance Criteria
Mobile Consent and E‑Signature Capture
- Given a payee opens the W‑9 link on mobile or desktop, When the signature step is displayed, Then an explicit consent checkbox with clear legal text is shown and must be actively checked before any signature input is accepted. - Given consent is checked, When the payee selects Type signature, Then the system renders at least two signature styles and captures the chosen style, typed legal name, and UTC timestamp. - Given consent is checked, When the payee selects Draw signature, Then the system captures the drawn signature image/strokes and UTC timestamp. - Given a signature method is used, When the payee submits, Then the system records signer IP address and user agent and binds them to the signature record. - Given consent is not provided or signature is missing, When submit is attempted, Then submission is blocked with a clear error and no artifacts are stored. - Given a network interruption during signing, When the session resumes, Then no partial signature is committed and the user is returned to the last confirmed step.
IRS‑Ready Flattened W‑9 PDF Generation
- Given a signed W‑9 submission, When the PDF is generated, Then all form fields are flattened and non‑editable in standard PDF readers (zero AcroForm fields remain). - Given the PDF is generated, Then the visible signature appearance (typed/drawn), printed legal name, and signing date are present in the signature area per IRS W‑9 layout. - Given the PDF is generated, Then all provided W‑9 data (name, address, TIN/EIN, classification, exemptions if any) render accurately and match the submitted values. - Given the PDF is generated, Then the document metadata contains a unique audit ID and the document’s SHA‑256 hash value. - Given the PDF is generated, Then automated validation confirms the file opens without errors in at least three common viewers (Adobe Reader, Apple Preview, Chrome PDF) and file size is ≤ 5 MB.
Tamper‑Evident Audit Trail Recording
- Given signing flow events occur, When any action happens (link created/opened, consent granted, signature method chosen, signature captured, submission, PDF generated, notifications), Then an event is recorded with type, actor, UTC ISO‑8601 timestamp, IP, user agent, and correlation IDs. - Given events are recorded, Then entries are append‑only and strictly ordered; attempts to modify or delete existing entries are rejected and logged. - Given a signing session completes, Then the audit trail contains the required minimum set of events and fields; if any required event/field is missing, the W‑9 is flagged invalid and download is blocked. - Given an audit trail entry is created, Then it includes a cryptographic linkage (e.g., running hash) to the previous entry to enable tamper detection.
Cryptographic Hash and Integrity Verification
- Given the final W‑9 PDF is produced, When hashing is executed, Then a SHA‑256 hash of the exact PDF bytes is computed and stored with the audit record and UTC timestamp. - Given a verification request is made, When the stored PDF is re‑hashed, Then the computed hash matches the stored hash and the system returns Verification Pass; otherwise it returns Verification Fail with details. - Given the stored PDF is altered or corrupted, When verification runs, Then a mismatch is detected, a high‑severity alert is raised, and the attempt is logged in the security audit log. - Given a user views the document details, When they click Verify integrity, Then the UI displays the current SHA‑256 hash, last verification time, and pass/fail status.
Encrypted Storage and Payee Association
- Given a signed W‑9 and its audit trail, When storing artifacts, Then they are encrypted at rest (AES‑256 or stronger) and accessible only via authorized application roles. - Given artifacts are stored, When viewing the payee record, Then the Signed W‑9 appears with signing date, audit ID, and integrity status, linked to the correct payee and payer organization. - Given an unauthorized user or a user from another organization requests access, When retrieval is attempted, Then access is denied with 403 and the attempt is logged. - Given artifacts are transmitted, When downloads or API fetches occur, Then TLS 1.2+ is enforced for all transfers.
Secure Download for Payer and Payee
- Given an authenticated payer or payee requests a signed W‑9, When authorization succeeds, Then a single‑use, time‑limited download URL (≤ 15 minutes) bound to the requester is generated. - Given a download occurs, Then the delivered PDF’s SHA‑256 hash matches the stored hash. - Given an expired or unauthorized link is used, When access is attempted, Then the system returns 403/410 and records the attempt in the audit log. - Given repeated download attempts, When thresholds are exceeded, Then rate limiting is applied and further attempts are temporarily blocked. - Given a download response is returned, Then security headers include no‑store/no‑cache and a safe Content‑Disposition filename.
Immutable Event Log for Compliance Review
- Given a compliance reviewer with appropriate permissions, When requesting the event log for a signed W‑9, Then the system returns a read‑only, paginated view and an export (PDF and JSON/CSV) containing all events and fields. - Given the event log is exposed, Then each entry includes its own hash and the previous entry’s hash (or equivalent mechanism) to prove immutability in the exported data. - Given an attempt is made to alter historical events, When a write is attempted, Then the operation is blocked and an administrative alert is generated and logged. - Given an export is generated, Then the export includes the document hash, audit trail hash, generation timestamp (UTC), and requesting user identity, enabling end‑to‑end reconciliation.
TIN/Name Validation & Error Handling
"As a freelancer preparing 1099s, I want the system to verify TIN and name details so that I don’t get hit with IRS mismatch notices or penalties."
Description

Validates SSN/EIN formats and, when available, checks name/TIN combinations via IRS TIN Matching or an approved intermediary. Queues and retries validation on service outages, flags mismatches with actionable guidance, and allows resubmission by the payee. Supports exempt-payee code validation and captures backup withholding indicators. Reflects validation status in the payee profile (Validated, Needs Review, Pending) and blocks 1099 prep on critical failures while providing override paths with justification.

Acceptance Criteria
Client/Server TIN Format Validation on W-9 Submission
Given a payee enters a TIN on the W-9 form When the TIN field loses focus or the form is submitted Then the client normalizes the input to digits-only and validates length = 9 And rejects known invalid TIN patterns (e.g., all zeros, 123456789, repeating digits) And distinguishes SSN vs EIN based on user selection and applies type-specific rules And displays a specific inline error message for the first failing rule without revealing the full TIN And prevents server submission until client-side checks pass When the server receives a TIN Then it re-validates normalization and rules, returning 422 with structured error codes on failure And suppresses TIN in logs, storing only last4 for diagnostics And completes client-side validation within 200 ms and server response within 1,000 ms on p95
IRS/Intermediary TIN Match Result Handling
Given a normalized TIN, payee legal name, and entity type are available When the TIN matching provider is reachable Then the system submits a single match request with required fields And receives one of: MATCH, PARTIAL_MATCH, NO_MATCH, or ERROR with a provider reference ID And persists provider, reference ID, timestamp, masked TIN (last4), and payload hash And sets payee.validationStatus to Validated on MATCH And sets payee.validationStatus to Needs Review on NO_MATCH or PARTIAL_MATCH And renders actionable guidance listing fields to verify (name, DBA, entity type) on failure states And emits an audit event TIN_MATCH_RESULT with outcome and provider metadata
Service Outage Queueing and Retry Policy
Given a TIN match attempt returns ERROR due to timeout, HTTP 5xx, or network failure When this occurs Then the request is enqueued with exponential backoff starting at 5 minutes, doubling up to 6 attempts or 24 hours maximum And payee.validationStatus is set to Pending with reason "Service unavailable" And the UI shows a non-blocking banner with the ETA for the next retry And metrics are recorded for attempts, successes, failures, and final state And on a successful retry, status updates per outcome and the banner is cleared And after max attempts without success, status becomes Needs Review with reason "Validation deferred - provider outage"
Mismatch Guidance and Mobile Resubmission Flow
Given a payee's TIN match outcome is Needs Review due to NO_MATCH or PARTIAL_MATCH When the payee opens the secure mobile link Then the form pre-fills prior W-9 data and highlights fields needing attention And displays concrete guidance (e.g., "Use legal name exactly as on SS card" or "Use entity legal name, not DBA") And allows correction of legal name, entity type, and TIN with inline validation And requires re-attestation and signature before resubmission And upon resubmission, triggers revalidation within 2 minutes and updates status accordingly And preserves a versioned history of submissions with timestamps and diffs
Exempt-Payee Code and Backup Withholding Capture
Given a payee selects an exempt-payee code and/or indicates backup withholding status When the W-9 is submitted Then the system validates the exempt-payee code against the IRS-allowed list and rejects invalid codes with a specific error And persists exemptPayeeCode and backupWithholdingIndicator in the payee profile And shows a badge for Exempt or Backup Withholding as applicable And includes these values in the validation summary, audit log, and downstream tax packet/export And does not skip TIN validation solely due to exempt status
Block 1099 Prep with Role-Based Override and Audit
Given a payee has validationStatus of Needs Review or Pending with a critical failure reason When a user attempts to start 1099 preparation for that payee Then the system blocks the action and lists the blocking reasons and remediation steps And displays an Override option only to Admin or Owner roles And requires a justification of at least 15 characters and a confirmation step to proceed And records override actor, timestamp, justification, and affected payee in an immutable audit log And marks the payee as Overridden and surfaces this tag in the payee profile and 1099 export review And allows 1099 prep to proceed only after successful override
Provider Fallback from IRS to Approved Intermediary
Given two consecutive provider outages or timeouts occur within 30 minutes when calling IRS TIN Matching When an approved intermediary is configured and enabled Then subsequent validation attempts route to the intermediary until IRS health is restored for 60 minutes And outcomes from the intermediary map to MATCH/PARTIAL_MATCH/NO_MATCH semantics identically And the system records which provider handled each attempt and maintains separate reference IDs And if both providers are unavailable, the request follows the queue and retry policy And user-facing messaging indicates the current provider without exposing sensitive details
Auto Reminders & Escalation
"As a busy payor, I want automatic reminders to nudge contractors until they complete the W‑9 so that I don’t have to chase them manually."
Description

Schedules and sends automated reminders over email/SMS until the W‑9 is completed, with configurable cadence, quiet hours, and maximum attempts. Stops reminders immediately upon completion and supports pause/resume. Tracks deliverability, bounces, and opt-outs, and escalates by notifying the payor with suggested next steps or alternate contact options. Provides template customization and merge fields, and logs all communications in the payee’s timeline for transparency.

Acceptance Criteria
Configure Cadence, Quiet Hours, and Max Attempts for Reminders
Given a payee without a completed W‑9 and cadence set to every 2 days at 10:00 local time, When the schedule runs, Then the system sends one reminder per enabled channel at 10:00 within non-quiet hours. Given quiet hours configured 21:00–08:00 recipient local time, When a scheduled send falls within quiet hours, Then the send is deferred to the next allowed window and the send timestamp reflects the deferral. Given max attempts set to 5 per channel, When the 5th attempt has been sent, Then no further reminders are sent via that channel and the request shows status Attempts Exhausted for that channel. Given the payor and payee are in different time zones, When scheduling sends, Then the system uses the payee’s time zone for cadence and quiet hours. Given cadence updated from every 2 days to daily, When the change is saved, Then the next send time recalculates from the save time without creating duplicate sends that day. Given channel preference is set to Email only, When the cadence triggers, Then only email reminders are sent and SMS is not queued.
Immediate Stop of Reminders Upon W‑9 Completion
Given an outstanding W‑9 request, When the payee completes and signs the W‑9, Then all queued and future reminders for that request are canceled within 60 seconds. Given an SMS reminder is queued but not yet sent, When a completion event occurs, Then the queued message is removed from the queue and not delivered. Given the submitted W‑9 fails validation and is marked incomplete, When validation fails, Then reminders continue per the configured cadence. Given completion occurs via external link or manual upload by the payor, When the request is marked complete, Then reminders stop immediately. Given multiple open W‑9 requests exist for the same payee, When one request is completed, Then only that request’s reminders stop and other requests continue unaffected.
Pause and Resume Reminder Schedule
Given an active reminder sequence, When the payor toggles Pause, Then no reminders are sent while paused and the UI shows Paused with a timestamp. Given a sequence is paused, When the cadence window passes, Then no messages are sent and no attempts are counted. Given a paused sequence, When the payor resumes, Then the next send time is recalculated from the resume time respecting quiet hours. Given a paused sequence, When the payor schedules a resume date and time, Then reminders restart at that time. Given the sequence is paused, When the payee completes the W‑9, Then the sequence is marked Completed and does not resume sending.
Track Deliverability, Bounces, and Opt-Outs
Given an email reminder is sent, When the provider reports Delivered, Opened, Bounced (hard or soft), or Spam Complaint, Then the corresponding event is stored with timestamp, provider code, and reason. Given an SMS reminder is sent, When the carrier returns Delivered or Undeliverable with error code, Then the event is stored with timestamp and code. Given a hard bounce or SMS permanent failure occurs, When further attempts are scheduled for that channel, Then that channel is auto-disabled for this request and flagged for alternate contact. Given the payee clicks Unsubscribe or replies STOP to SMS, When future reminders are scheduled, Then no further messages are sent on the opted-out channel and the opt-out is logged with timestamp. Given a soft bounce occurs, When the next cadence cycle runs, Then the system retries per cadence up to max attempts unless the status changes to hard bounce. Given deliverability events are received out of order, When recorded, Then the latest event state is reflected correctly in the payee’s channel status.
Escalation to Payor with Suggested Next Steps
Given max attempts are exhausted across all enabled channels, When the last attempt is processed, Then an escalation notification is sent to the payor within 5 minutes. Given escalation is triggered, When the payor views the notification, Then it includes a summary of attempts, last known delivery statuses, opt-out status, suggested next steps (call, request alternate email/phone), and a one-click action to resend via an alternate channel if available. Given an alternate contact method exists in the payee record, When escalation is generated, Then the notification surfaces that contact with masked details. Given an escalation is acknowledged, When the payor clicks Mark as Resolved or adds a note, Then the escalation status updates accordingly and the action is logged in the timeline. Given the payor adds a new contact detail from the escalation, When saved, Then the system offers to start a new reminder sequence to the new channel and records the consent status if applicable.
Template Customization with Merge Fields and Preview
Given a reminder template editor, When the payor inserts merge fields (Payee Name, Business Name, Due Date, Secure Link, Payor Name), Then preview renders with sample or real data and no unresolved placeholders are shown. Given a required merge field Secure Link is missing, When the template is saved, Then validation fails with an actionable error and the template cannot be enabled. Given a fallback value is defined for an optional field, When the source value is empty, Then the fallback appears in the rendered message. Given templates are customized per channel, When an SMS template exceeds 1600 characters, Then validation warns and prevents save until within the limit. Given a template is updated, When an active sequence uses it, Then subsequent sends use the new version and already sent messages remain unchanged. Given merge fields include user-provided content, When rendering in email, Then the output is escaped to prevent HTML or script injection.
Log All Communications in Payee Timeline
Given any reminder, event, or escalation occurs, When it is sent or received, Then an entry is created in the payee’s timeline with timestamp, channel, template name and version, subject or first 50 characters, and delivery status. Given a timeline entry exists, When the user expands it, Then the full message content is displayed with sensitive tokens masked except the last 4 characters. Given a queued message is canceled due to completion or pause, When cancellation occurs, Then a timeline entry records the cancellation reason and actor (system or user) with timestamp. Given deliverability or opt-out events are received, When logged, Then they are associated with the originating message in the timeline thread. Given the payor filters the timeline by W‑9 AutoChase, When the filter is applied, Then only related entries are shown with correct counts.
Attachment, Versioning & Renewal Tracking
"As a payor, I want each payee’s latest signed W‑9 stored and versioned in one place so that I can confidently file 1099s and retrieve documents instantly if audited."
Description

Automatically attaches the signed W‑9 PDF and extracted structured data to the payee record with version history, effective dates, and status. Highlights the latest valid version during 1099 preparation and preserves prior versions for audit. Detects meaningful changes (name, TIN, classification) and prompts for a refreshed W‑9; optionally schedules renewal reminders after a configurable interval. Enables export and API access to the latest W‑9 for downstream systems while enforcing role-based access controls.

Acceptance Criteria
Automatic Attachment and Data Extraction on W‑9 Completion
Given a payee completes and e‑signs a W‑9 via a TaxTidy link When the submission is verified as complete and passes TIN format validation Then the signed W‑9 PDF is stored and attached to the corresponding payee record And the following fields are extracted and saved as structured data on the payee: Legal Name, Business Name (if any), Federal Tax Classification, TIN, TIN Type (SSN/EIN), Address, Exemptions (if any), Signature Name, Signature Timestamp And the attachment metadata includes: source=AutoChase, file hash, file size, MIME type, uploader/payee ID, submission ID And the payee record displays a Latest Valid badge with signature date and validation status
Version History Creation and Preservation
Given an existing payee with one or more W‑9 versions When a new signed W‑9 is attached (automatic or manual) Then a new immutable version entry is created with a monotonically increasing version number And prior versions remain preserved and downloadable And the effective date defaults to the signature date and can be manually adjusted by authorized roles with an audit log entry And the UI lists all versions with version number, effective date, status (Valid, Superseded, Invalid), and signer And deleting a version is not permitted; only marking as Invalid with a required reason is allowed
Latest Valid Version Selection for 1099 Preparation
Given 1099 preparation is initiated for a tax year When the system selects a W‑9 for each payee Then the algorithm chooses the latest version with status=Valid and effective date on or before 12/31 of the tax year And if no Valid version exists, the payee is flagged Blocking: Missing Valid W‑9 with a one‑click action to request one And if multiple Valid versions exist, the one with the most recent effective date is highlighted And the selected version’s key fields (Name, TIN last4, Classification, Signature Date) are shown in the prep UI and export
Meaningful Change Detection and Refresh Prompt
Given a new W‑9 is received for a payee When the Legal Name or TIN or Federal Tax Classification differs from the most recent Valid version Then the system flags Meaningful Change Detected and prompts the user to confirm refresh And upon confirmation, the prior version status becomes Superseded and the new version becomes Valid And if the change is accidental, the user can mark the new version Invalid with a required reason, recorded in the audit trail And a notification is sent to the workspace owner for any TIN change
Configurable Renewal Scheduling and Auto‑Reminders
Given renewal scheduling is enabled in workspace settings with an interval (in months) When a payee’s latest Valid W‑9 reaches the renewal threshold (e.g., 30 days before renewal date) Then the system schedules and sends renewal reminders per the cadence (e.g., 30/14/7/1 days) until completion or snooze And upon receipt of a new Valid W‑9, all pending reminders are canceled and the next renewal date is recalculated from the new effective date And authorized users can override the interval per payee or snooze reminders for N days And all reminder events and outcomes are logged on the payee timeline
Export and API Access with Role‑Based Controls
Given a user with role Accountant or Owner requests export of W‑9s When exporting selected payees or all Then the system produces a ZIP containing PDF files and a JSON/CSV manifest of the latest Valid W‑9 structured data and metadata And API GET /payees/{id}/w9/latest returns 200 with links to PDF and JSON for authorized API tokens, and 403 for unauthorized roles And TIN is redacted to last4 in UI/exports by default; full TIN is returned only via API scopes with SensitiveData:Read And all access (success and denial) is recorded with user/token, timestamp, and IP address

TIN Match Sync

Pre‑flight name/TIN checks with guided fixes to reduce IRS B‑Notices and rejections. Run instant or batch validations, flag mismatches with plain‑language reasons, and apply suggested corrections before you hit e‑file.

Requirements

TIN Match Service Connector
"As a freelancer who pays subcontractors, I want TaxTidy to verify name/TIN pairs against authoritative sources so that I can avoid IRS B‑Notices and rejected filings."
Description

Implement a provider-agnostic connector to verify legal name and TIN combinations against authoritative services (e.g., IRS TIN Matching or certified third-party providers). Support secure authentication, environment-specific credentials, request/response normalization, and field-level encryption for transmitted TINs. Log match results with non-sensitive metadata, store minimal necessary data, and expose a consistent internal API for both instant and batch checks.

Acceptance Criteria
Instant TIN/Name Match via Internal API
Given a registered internal client with scope "tin.match:instant" and a valid payload {name, tin, type} And the connector is configured with an active provider for the current environment When the client POSTs to /v1/tin-match over TLS 1.2+ with a unique Idempotency-Key header Then the service validates inputs (non-empty name, TIN format valid for type SSN/EIN) and returns 400 with error details if invalid And on valid input, the service calls the provider and returns HTTP 200 within 2 seconds p95 And the response body conforms to schema v1 with fields {status ∈ [MATCH, NO_MATCH, PARTIAL, ERROR], reasonCode, provider, correlationId, receivedAt, completedAt} And the response masks the TIN as ****-**-#### and never includes plaintext TIN or full name And an audit event is emitted containing correlationId, provider, status, duration, without storing plaintext TIN
Batch TIN Match Job Processing
Given a CSV or JSON Lines file containing 1–10,000 records with fields {referenceId, name, tin, type} And a callbackUrl is provided When the client POSTs to /v1/tin-match/batch with the file, callbackUrl, and a unique Idempotency-Key Then the service returns 202 with {jobId} And the job processes records with at-least-once semantics and deduplicates by referenceId within the job And per-record results are available via GET /v1/tin-match/batch/{jobId}/results with pagination and conform to the normalized schema And validation errors mark records as INVALID with reasonCode and do not block other records And the service posts a completion callback with summary counts {MATCH, NO_MATCH, PARTIAL, INVALID, ERROR} and total processing time
Provider-Agnostic Routing and Failover
Given at least two providers are configured with credentials and routing rules {primary, secondary} When the primary provider is healthy Then requests are routed to the primary and responses are normalized identically regardless of provider When the primary returns retryable errors (HTTP 429/5xx or network timeout) after 2 retries with exponential backoff and jitter Then the connector fails over once to the secondary, records fallbackUsed=true in metadata, and returns the secondary’s normalized result When both providers fail or return non-retryable errors Then the service returns HTTP 502 with status=ERROR and reasonCode=PROVIDER_UNAVAILABLE and emits a health metric
Environment-Specific Credentials and Isolation
Given environments {dev, stage, prod} with distinct endpoints and credentials stored in a secret manager When a request is served in any environment Then only that environment’s credentials are loaded and only that environment’s endpoints are called And production credentials are not readable or usable in non-prod environments (IAM denies are verified by automated tests) And rotating credentials in the secret manager takes effect without redeploy within 5 minutes And outbound requests include an environment-specific user agent and no secrets are logged
Field-Level Encryption and PII Redaction
Given any operation that handles TIN values When preparing an outbound provider request or persisting transient job data Then the TIN field is encrypted at field level (AES-256-GCM with KMS-managed keys) and stored/sent only as ciphertext or token And logs, traces, and metrics never include plaintext TIN or full name; TIN is masked to last4 when necessary And storage contains at most: referenceId, last4Tin, HMAC(tin), status, reasonCode, provider, timestamps, correlationId And key rotation is supported; decrypt with previous keys and re-encrypt with the new key without data loss And static and runtime scanners report zero occurrences of unencrypted TIN in code, configs, logs, and data stores
Normalized Response Schema and Reason Codes
Given heterogeneous provider response formats and codes When the connector returns results for instant or batch operations Then the response adheres to JSON Schema TinMatchResult.v1; invalid mappings are rejected and surfaced as status=ERROR with reasonCode=UNKNOWN_PROVIDER_CODE if applicable And status is one of [MATCH, NO_MATCH, PARTIAL, INVALID, ERROR]; reasonCode is from a documented set (e.g., NAME_MISMATCH, TIN_NOT_FOUND, INPUT_INVALID, PROVIDER_UNAVAILABLE) And provider-specific fields are excluded from the public schema; internal providerRaw details appear only in internal logs And translations are deterministic: the same provider input yields the same normalized output and correlationId across retries
Batch Validation Engine
"As a user managing many vendors, I want to run batch TIN validations so that I can catch mismatches across my list at once before I e‑file."
Description

Provide a scalable job processor to validate large sets of vendors/payees in bulk from selected records or CSV import. Include de-duplication, rate-limit aware throttling, progress tracking, resumable jobs, and exportable results (CSV/JSON). Display a summary (matched, mismatched, needs review, errors), and write results back to vendor records with timestamps and TTLs for re-checks.

Acceptance Criteria
Bulk Validation from CSV Import at Scale
Given a CSV file containing headers name and tin and optional vendor_id with 10 to 50,000 data rows When the user uploads the file and confirms batch validation Then the system validates headers and rejects the upload with a descriptive error if required headers are missing And the system parses the file, trimming whitespace, ignoring blank lines, and recording row numbers And rows missing name or tin are moved to the error bucket with their row numbers and reasons And a batch job is created with a job_id, source set to csv_import, and total_items equal to the count of rows with both name and tin present before de-duplication And the job state is queued within 5 seconds of submission
Bulk Validation from Selected Vendor Records in UI
Given the user selects 1 to 10,000 vendor records in the UI When the user starts a batch validation from the selection Then a batch job is created with job_id, source set to selection, and total_items equal to the number of selected vendors before de-duplication And the job immediately snapshots the selected vendor IDs to ensure a stable input set even if the underlying list changes And the job state is queued within 5 seconds of submission
De-duplication of Validation Targets Within a Job
Given a batch job input that may contain duplicate vendors or repeated name/tin pairs When the job is prepared for processing Then the system normalizes name by trimming and collapsing internal whitespace and uses case-insensitive comparison And the system de-duplicates by the composite key (normalized_name, exact_tin) And only one external validation request is made per unique composite key per job And all duplicates inherit the same validation result and reason And the summary counts and exports reflect the number of unique validations and separately indicate duplicate rows as duplicates
Rate-Limit Aware Throttling and Backoff
Given the external TIN-matching service enforces request rate limits and may return 429 with Retry-After When the batch job runs Then the processor enforces a configurable concurrency and request rate not exceeding the provider limits And upon 429 responses, the processor honors Retry-After and uses exponential backoff with jitter And no requests are sent during the mandated backoff window And no validation attempt is retried more than 3 times And the job completes without provider rate-limit violations logged as errors
Progress Tracking and Accurate Summary Totals
Given a batch job is running or completed When the client requests job progress Then the system returns total, processed, pending, percent_complete, and estimated_time_remaining with updates at least every 5 seconds while running And per-status counts are maintained for matched, mismatched, needs_review, and error And on completion, processed equals total, and the sum of matched + mismatched + needs_review + error equals total unique validations And the final job status is completed with a finished_at timestamp
Resumable and Idempotent Job Recovery
Given a running batch job is interrupted by worker restart, deployment, or manual pause When processing resumes Then items already validated are not re-validated within their TTL window And the job continues from the last durable checkpoint without data loss And a manually paused job can be resumed by an authorized user and returns to running state within 5 seconds And if a fatal error occurs, the job status is failed with a machine-readable error_code and human-readable message, and the job can be retried to create a new attempt linked to the original job_id
Write-back to Vendor Records and Exportable Results
Given validation results are produced for each unique input When results are finalized Then for inputs that map to existing vendor records, the system writes validation_result (matched|mismatched|needs_review|error), reason, validated_at (ISO-8601 UTC), expires_at (TTL), and last_job_id to the vendor record atomically And for inputs without an existing vendor record, the result is stored in the job results and included in exports And the system provides downloadable CSV and JSON exports containing job_id, vendor_id (if any), name, tin, result, reason, validated_at, expires_at, and error_code (if any) And export files are UTF-8 encoded and streamable, and for up to 50,000 rows are available for download within 60 seconds of job completion And export row counts and per-status totals match the job summary
Instant TIN Check UI
"As a mobile user adding a new payee, I want instant feedback on their TIN and name so that I can fix issues while I have the information handy."
Description

Add mobile-first, accessible UI components for on-the-fly name/TIN validation at data entry. Trigger checks on field blur or save with visible states (valid, mismatch, pending, error). Auto-format TIN input, mask sensitive digits, and surface results within 1–2 seconds under normal load. Provide localized, plain-language messages with clear next steps.

Acceptance Criteria
Mobile Blur-Trigger Validation with Visible States
Given the user has entered a 9-digit TIN and the TIN or Name field loses focus on a mobile form, when the blur event fires, then exactly one validation request is sent within 100 ms and a visible "Pending" state is shown within 100 ms. Given a validation is pending, when the user resumes typing in the same field, then no additional validation requests are sent until 300 ms after typing stops or the field blurs again. Given the service returns a valid match, when the response is received, then the field transitions to a "Valid" state with icon and concise text, replacing the pending state. Given the service returns a mismatch, when the response is received, then the field transitions to a "Mismatch" state with icon and concise text plus a "See suggested fix" action. Given the input fails client-side format rules (non-numeric or fewer than 9 digits), when the field blurs, then no validation request is sent and an inline format error is displayed.
Save-Action Validation with Blocking Mismatch Handling
Given the user taps Save with an unvalidated Name/TIN pair, when Save is pressed, then a validation is triggered and the form indicates a "Pending" state until validation completes or a 2-second timeout occurs. Given validation returns "Mismatch" after Save is pressed, when the result is displayed, then the save is blocked, a localized inline banner explains the mismatch in plain language, and primary action changes to "Apply suggested correction" with a secondary "Review details" link. Given the user applies the suggested correction, when revalidation returns "Valid", then the Save action becomes enabled and completes successfully without additional prompts. Given validation returns "Valid" after Save is pressed, when the result is displayed, then the record is saved immediately and a success toast confirms completion.
Auto-Formatting and Masking of TIN Input
Given TIN type is set to SSN, when the user types digits, then the input auto-formats as XXX-XX-XXXX and rejects non-numeric characters. Given TIN type is set to EIN, when the user types digits, then the input auto-formats as XX-XXXXXXX and rejects non-numeric characters. Given the user leaves the TIN field, when the value is displayed, then all but the last four digits are masked (e.g., ***-**-1234 for SSN or **-***1234 for EIN) and a "Show" toggle reveals the full value for up to 10 seconds. Given the TIN field regains focus, when the user edits, then the value is unmasked for editing and is re-masked on blur. Given the user copies a masked TIN, when "Show" is not active, then the copied value remains masked; when "Show" is active, then the copied value contains the full TIN.
Localized, Plain-Language Messages with Guided Fixes
Given the app locale is en-US or es-US, when validation completes, then all user-visible validation texts (Pending, Valid, Mismatch, Error) are displayed in the current locale with plain-language phrasing and no IRS jargon. Given a mismatch result, when guidance is shown, then the message includes a human-readable reason (e.g., "The name doesn’t match IRS records for this TIN") and a clear next step with a primary action "Apply suggested correction" that previews the suggested name. Given a suggested correction is applied, when the name field updates, then an automatic re-validation occurs and the final state is displayed within 2 seconds under normal load. Given no suggestion is available, when a mismatch occurs, then guidance offers manual steps ("Verify the legal name on W-9 or IRS notice") and a link to a localized help article.
Accessibility: Screen Reader Announcements and Focus Management
Given any validation state change (Pending, Valid, Mismatch, Error), when it occurs, then an aria-live="polite" region announces the new state within 1 second using concise, localized text. Given an error or mismatch banner is rendered on Save, when it appears, then keyboard focus moves to the banner header and returns to the triggering control upon dismissal. Given state-indicating icons and colors are used, when evaluated, then non-color cues (icons/text) are present and color contrast for text/icons meets WCAG 2.2 AA (text ≥ 4.5:1; UI components/graphics ≥ 3:1). Given a masking "Show/Hide" toggle is present, when tested with screen readers, then it has an accessible name, role, and pressed state, and is reachable via keyboard and switch control.
Performance: Result Display Within 2 Seconds Under Normal Load
Given normal load conditions, when validation is triggered on blur or Save, then the 95th percentile end-to-end time from trigger to rendered result is ≤ 1.5 seconds and the 99th percentile is ≤ 2.0 seconds, measured over ≥ 1,000 events. Given the service responds, when the UI receives the response, then the visible state updates within 200 ms. Given a response is not received within 2.0 seconds, when the timer elapses, then a non-blocking "Still checking…" indicator appears with a Retry control and validation continues in the background. Given the user navigates away while validation is pending, when the route changes, then the in-flight request is canceled and no error toast is shown.
Error Handling and Retry on Timeout or Service Failure
Given the validation service returns a 4xx due to invalid TIN format, when the result is handled, then an inline format error is shown and no sensitive data is displayed. Given the validation service returns a 5xx or a network timeout > 2 seconds, when the result is handled, then a localized, plain-language error is shown ("Unable to validate right now") with a Retry action; user input remains intact. Given three consecutive failures occur within 10 minutes for the same record, when the user blurs the field again, then automatic re-validation is suppressed for 5 minutes (backoff) and the UI explains how to retry manually. Given the user taps Retry after a failure, when the next attempt succeeds, then the error state clears and the resulting Valid/Mismatch state is displayed.
Guided Fixes & Smart Suggestions
"As a user, I want clear explanations and suggested corrections when a TIN check fails so that I can resolve issues quickly without guessing."
Description

Offer human-readable mismatch reasons and ranked suggestions (e.g., remove punctuation, swap first/last name, use legal entity name from W‑9, update disregarded entity rules). Provide one-click application of suggested corrections with preview and undo, and track confidence scores. When data is insufficient, prompt users to request a new W‑9 via the outreach flow.

Acceptance Criteria
Instant Mismatch: Punctuation Removal Suggestion
Given a payee record with name including punctuation characters and a TIN that fails an IRS name/TIN match When the user runs an instant TIN validation Then the system displays a human-readable mismatch reason indicating punctuation may be causing the mismatch And the top suggestion proposes removing punctuation from the name And the suggestion includes a preview of the normalized name and a numeric confidence score formatted 0.00–1.00 And suggestions are ranked by confidence descending And no data is changed until the user explicitly applies a suggestion
Batch Validation: Ranked Suggestions and Reasons
Given a batch validation is run on a list of 100 or more payee records When processing completes Then each mismatched record shows at least one human-readable reason and up to three ranked suggestions with confidence scores formatted 0.00–1.00 And suggestions for each record are sorted by confidence descending And users can navigate mismatches and apply suggestions per record with a single click/tap And batch results persist so that returning to the batch shows the same reasons, suggestions, and scores
One-Click Apply with Preview and Undo
Given a mismatched record with one or more suggestions When the user clicks/taps Apply on a suggestion Then a preview shows field-level before/after for name and/or TIN changes And the user can Confirm to commit the change And upon commit, an audit log entry is recorded with user, timestamp, fields changed, prior values, new values, selected suggestion id, and confidence score And an Undo control is displayed for at least 10 minutes after commit And clicking Undo reverts the changes and records a reversal entry in the audit log
Use Legal Entity Name from W-9
Given a mismatched record with an attached W-9 that contains a Legal Name differing from the current payee name When validation runs Then a suggestion proposes replacing the payee name with the Legal Name from the W-9 And the reason cites the discrepancy and references the W-9 And the suggestion displays the proposed Legal Name and confidence score 0.00–1.00 And a View W-9 link opens the source document And applying the suggestion updates the name field only (not the TIN) and records the change in the audit log
Disregarded Entity Rule Update
Given a payee marked as a disregarded entity on the W-9 with an owner name and an EIN/SSN that fails name/TIN match When validation runs Then a suggestion proposes updating the reported name to the owner’s name per disregarded entity rules and, if applicable, aligning the TIN type And the reason explains in plain language that the IRS requires the owner’s name for disregarded entities And the suggestion includes the proposed owner name, any TIN type change, and a confidence score 0.00–1.00 And applying the suggestion updates the name and TIN type fields atomically and logs the change
Insufficient Data Triggers W-9 Outreach
Given a mismatched record where no suggestion exceeds a confidence score threshold of 0.40 or required fields are missing When validation completes Then the system prompts the user to request a new W-9 via the outreach flow And the prompt includes prefilled recipient details and a selectable email or SMS template And sending the request records a timestamped outreach entry linked to the payee And the record is flagged as “Needs W-9” and blocked from e-file until resolved
Confidence Scores Display and Persistence
Given any generated suggestion When it is displayed to the user Then the confidence score is visible with two decimal places (0.00–1.00) and a tooltip explaining what the score represents And the score, algorithm version, and inputs used are stored with the suggestion for auditability And the same score and ordering are reproduced on subsequent views until data changes or the model version increments, in which case a new score is generated and versioned
Mismatch Resolution & W‑9 Outreach
"As an account owner, I want an organized workflow to request and collect corrected W‑9s from vendors so that I can resolve TIN mismatches and keep compliant records."
Description

Create a resolution workflow and queue for flagged records, with status transitions (Needs Outreach, Awaiting Response, Corrected, Dismissed). Enable templated email/SMS requests for W‑9 collection, e-sign support, and secure upload links. Auto-attach received W‑9s to the vendor record, re-run validation on update, and maintain a full audit trail of communications and changes.

Acceptance Criteria
Resolution Queue Visibility and Controls
Given there are vendors flagged by TIN Match with mismatches When a user with role Preparer opens the TIN Match Sync Resolution Queue Then only flagged records are listed with columns: Vendor Name, TIN, Mismatch Reason, Status, Last Outreach, Assignee And the user can filter by Status (Needs Outreach, Awaiting Response, Corrected, Dismissed), Mismatch Reason, Assignee, and Last Outreach date range And the user can sort by Last Activity and Mismatch Severity And the queue paginates 50 records per page and returns the first page in under 3 seconds for up to 1,000 flagged records
Status Transitions and Validation
Given a flagged vendor record exists in the Resolution Queue When the user updates its status Then only these transitions are allowed: Needs Outreach -> Awaiting Response; Awaiting Response -> Corrected; Awaiting Response -> Dismissed; Awaiting Response -> Needs Outreach; Corrected -> Needs Outreach; Dismissed -> Needs Outreach And changing status to Dismissed requires a mandatory reason note (min 10 characters) And each status change records user, timestamp, previous status, new status, and optional note in the audit trail And moving to Awaiting Response requires at least one outreach to be queued or sent
Templated W-9 Outreach via Email and SMS
Given a flagged vendor requiring W-9 collection When the user selects Send Outreach and chooses Email and/or SMS Then the user can select a template and preview the rendered message with placeholders {vendor_name}, {business_name}, {secure_link}, {due_date} And the user can send a test to self before sending to the vendor And upon send, each message is queued and delivery status is tracked as Sent, Delivered, Bounced, or Failed And if the SMS recipient replies STOP, further SMS to that number is suppressed and the record is marked opted out And Last Outreach timestamp and channel are updated and logged in the audit trail
Secure W-9 Capture via E-sign or File Upload
Given a recipient opens the secure outreach link When they choose E-sign W-9 Then the form enforces IRS-required W-9 fields, validates TIN formats, and captures electronic signature, timestamp, IP, and user agent And a flattened PDF of the executed W-9 is generated and stored When they choose Upload W-9 instead Then the link accepts PDF, JPG, or PNG up to 10 MB, scans for malware, and encrypts the file at rest And the upload link is tokenized, single-recipient, and expires after 7 days or immediately after successful submission And the page displays vendor name and request details without exposing full TIN
Auto-Attach W-9 and Data Extraction
Given a W-9 is received via e-sign or upload When the document is stored Then it is attached to the correct vendor record and marked as the latest W-9 version with timestamp And the system extracts Legal Name, Business Name (if any), TIN, and Federal Tax Classification And if a prior W-9 exists, a new version is created while preserving access to older versions And duplicate submissions within 24 hours with identical TIN are deduplicated and cross-referenced
Auto Re-Validation and Queue Update
Given a vendor record has a new W-9 attached or an updated Name/TIN When the record is saved Then TIN Match validation re-runs within 5 minutes And on match success, the mismatch flag is cleared, status updates to Corrected, and the record is removed from the active queue And on mismatch, the status remains, the mismatch reason updates, and the record stays in the queue And the revalidation result is written to the audit trail
Audit Trail and Export
Given outreach, status changes, or data edits occur on a flagged record When a user views the Audit Trail for that record Then entries include event type, actor (user/system), UTC timestamp, before/after values for changed fields, message delivery statuses, and linked artifacts (W-9 file/version) And entries are immutable and filterable by event type and date range And users with Admin role can export the audit trail for selected records to CSV within 60 seconds
PII Security & Compliance Controls
"As a security-conscious user, I want TIN data handled securely and compliantly so that my business and my contractors are protected."
Description

Enforce least-privilege RBAC for TIN access, field-level encryption at rest (AES‑256) and in transit (TLS 1.2+), masking in UI, and secrets management with rotation. Implement consent logging, retention policies, and right-to-delete workflows. Maintain immutable audit logs for access and changes, and redact sensitive values from logs. Document provider Terms of Use compliance and conduct regular security reviews.

Acceptance Criteria
Least-Privilege RBAC for TIN Access
Given a user without TIN_View privilege attempts to access a TIN via API or UI When they request the resource Then the response is 403 Forbidden, the TIN remains masked in the UI, and an audit record is written with user ID, role, resource ID, timestamp, and outcome=Denied Given a user with role=Compliance Admin or Tax Ops Analyst When they click Reveal TIN or call the TIN read endpoint Then the full TIN is returned/displayed for a maximum of 60 seconds, the action requires reason input, and an audit record is written with rationale and outcome=Approved Given a user’s role is revoked or downgraded When 1 minute elapses or a new request is made Then access to unmasked TIN is denied and permission evaluation reflects the change within 60 seconds Given a service account without scope=tin:read:decrypted When it requests decrypted TIN from the key service Then access is denied with 403 and the event is audited
PII Encryption at Rest and In Transit
Given any TIN or PII field is persisted to databases, object storage, or search indexes When the write occurs Then the value is encrypted at field level with AES-256 using managed KMS keys, and plaintext is never stored Given key rotation is triggered When rotation completes Then new writes use the new key, old data remains decryptable, key metadata shows rotation date, and no downtime occurs Given a client connects to any endpoint that transmits PII When the TLS handshake negotiates below TLS 1.2 or weak ciphers Then the connection is rejected and the response includes information to upgrade; HSTS is present with max-age >= 15552000 Given an internal service makes an HTTP call carrying PII When the scheme is http Then the request is blocked and a security alert is raised
UI Masking with Role-Based Reveal and Justification
Given any screen renders a TIN When the page loads Then the TIN is masked to last 4 digits (e.g., ***-**-1234) for all users by default Given an authorized user clicks Reveal TIN When the confirmation modal enforces reason input and recent MFA (<=5 minutes) Then the full TIN is shown for <=60 seconds, copy-to-clipboard is enabled, and an audit entry is created with reason and duration Given an unauthorized user attempts to copy or export a masked TIN When they use UI controls or keyboard shortcuts Then only the masked value is copied/exported Given a search is performed When results include TIN fields Then only masked tokens are shown and full TIN is never included in client-visible payloads
Secrets Management and Automated Rotation
Given an application component needs credentials or keys for TIN processing When it retrieves secrets Then secrets are stored/retrieved exclusively via the approved Secrets Manager, never hardcoded or in environment variables, and retrievals are audited Given rotation policy is configured to 90 days for application credentials and 365 days for KMS data keys When rotation jobs run Then new versions are active, old versions are disabled within 7 days, no downtime occurs, and a rotation report is generated Given CI scans source and container images When a hardcoded secret or TIN pattern is detected Then the build fails with severity=High and a notification is sent to the Security channel
Consent Logging, Data Retention, and Right-to-Delete Fulfillment
Given a contractor’s TIN is to be validated When consent has not been recorded for the TIN Match purpose Then the workflow is blocked with 412 Precondition Failed and a prompt is shown to capture consent Given consent is captured When the user confirms Then a consent record is stored with subject ID, purpose, scope, timestamp, actor, and versioned policy text hash, and the record is queryable by subject ID Given retention policy is 7 years for tax records and 2 years for raw TIN artifacts When a record exceeds its retention window Then the record is purged from primary stores within 24 hours and the purge event is audited Given a right-to-delete request is submitted for a subject When the request is approved Then TIN and related PII are deleted or irreversibly pseudonymized in operational stores within 30 days, backups are scheduled for natural expiry without PII reconstruction, and a deletion certificate is generated
Immutable Audit Trails and Sensitive Log Redaction
Given any access to TIN or changes to security configuration occur When the event completes Then an audit record is appended with who, what, when, where (IP/agent), why (reason), correlation ID, and outcome, and cannot be modified or deleted Given an engineer attempts to alter or delete an audit record When the request is made Then the platform denies the action and records a tamper-attempt event; verification shows logs stored in WORM or hash-chained storage Given application or audit logs are emitted When TIN or PII values would be logged Then values are masked or tokenized (last 4 only), and any raw PII is blocked by a logging interceptor and a security metric alert is raised Given an auditor exports logs for a date range When the export job runs Then the file is complete, time-ordered, checksum-verified, and excludes raw TIN values
Provider Terms of Use Compliance and Regular Security Reviews
Given the TIN Match Sync feature integrates with external providers or IRS systems When a new provider is added or a Terms of Use changes Then a compliance checklist is completed, ToU links and allowed use cases are documented, data handling mapping is updated, and approvals from Legal and Security are recorded Given a release is prepared that uses a provider API When CI/CD runs Then the pipeline verifies ToU acceptance on file, enforces rate limits and purpose restrictions via configuration, and blocks deployment if compliance artifacts are missing Given the quarterly security review cycle When the quarter ends Then dependency scans, SAST/DAST, and penetration test summaries are documented; all Critical findings remediated within 14 days and High within 30 days; evidence is attached to the review record Given a control failure is detected (e.g., TLS misconfiguration or missing masking) When an issue is opened Then a remediation ticket is created within 1 business day, linked to the control, and closed only after verification tests Pass
Resilience, Caching, and Rate Limit Handling
"As a user, I want TIN checks to be reliable even during provider issues so that my workflow is not blocked."
Description

Introduce idempotent requests, exponential backoff, and circuit breakers for provider outages. Cache successful name/TIN matches with a configurable TTL and ‘stale-while-revalidate’ strategy to reduce costs and latency. Provide observability (metrics, traces, alerts), and graceful fallbacks with clear user messaging when checks are delayed or queued.

Acceptance Criteria
Idempotent TIN Match Request Retries Do Not Duplicate Checks
Given an API consumer provides an Idempotency-Key and identical payload within 24 hours When the request is retried due to network errors or 5xx Then the service returns the original result with 200 and Idempotent-Replay: true without re-calling the provider Given the same payload is sent without an Idempotency-Key When repeated within 5 minutes Then the service deduplicates based on normalized name+TIN+time window and performs only one provider call Given concurrent duplicate requests with the same Idempotency-Key When processed Then only one provider call occurs and other requests wait and receive the same result within 2 seconds of completion Given different payloads share the same Idempotency-Key When received Then the second request is rejected with 409 Conflict and error code IDEMPOTENCY_KEY_MISMATCH
Exponential Backoff on 429/5xx with Max Retry Cap
Given a provider response is 429 or 5xx (excluding 501) and the error is retryable When retrying Then use exponential backoff with jitter: baseDelay=500ms, factor=2, jitter=±30%, maxDelay=30s, maxRetries=5 or total backoff ≤60s, whichever occurs first Given a successful retry occurs When returning the result Then include retry metadata (attempt count, cumulative delay) in logs and metrics Given a non-retryable status (400, 401, 403, 404, 422) is returned When handling the response Then do not retry and return an error with a machine-readable reason code and remediation hint Given a 429 includes a Retry-After header When scheduling the next attempt Then honor Retry-After up to maxDelay
Circuit Breaker Trips and Auto-Recovers on Provider Outage
Given failure rate ≥50% over the last 20 requests or there are 5 consecutive failures When evaluating the circuit Then transition to OPEN for 60 seconds and short-circuit further provider calls Given the circuit is OPEN When a request arrives Then return 503 SERVICE_UNAVAILABLE with code CIRCUIT_OPEN and enqueue the request with an ETA without calling the provider Given the OPEN timeout expires When in HALF-OPEN Then allow 1 probe every 30 seconds; if 2 consecutive probes succeed transition to CLOSED; if any fails return to OPEN for 60 seconds Given circuit state changes occur When emitting telemetry Then breaker_state metrics and structured logs are produced with timestamps and reasons
Cache Hit with TTL and Stale-While-Revalidate
Given a prior successful exact match for normalized name+TIN exists When requested again within TTL (default 24h, configurable 5m–30d) Then return the cached result with Cache-Status: hit and do not call the provider Given the cache entry is expired but within SWR window (default 12h, configurable 5m–7d) When requested Then return the stale result immediately with Cache-Status: stale and trigger background revalidation; update cache if provider returns within 2 minutes Given revalidation fails When evaluating cache policy Then keep the stale result and record failure count; after 3 consecutive revalidation failures mark as revalidate_suppressed for 30 minutes Given data privacy rules When persisting cache keys Then use a salted hash of normalized name+TIN; no PII is stored in plaintext
Batch Validation Rate Limit Handling and Queueing
Given a batch of up to 10,000 records When provider limits are 100 rps and 5,000 rpm Then throttle to ≤95% of limits using a token bucket and enqueue excess; no 429 is emitted by our service Given queueing occurs When tracking progress Then per-item status transitions to queued→processing→completed with timestamps; 95% of queued items start within 2 minutes Given a user cancels a batch When processing Then queued-but-not-started items are removed within 10 seconds and reported as canceled without provider calls Given a partial provider outage during a batch When the breaker opens Then remaining items are queued and resume automatically when the circuit closes, preserving FIFO order
Observability: Metrics, Traces, and Alerts Emitted
Given production traffic When operating Then emit metrics: requests_total, requests_success_total, requests_error_total by code, latency_ms p50/p95/p99, retry_attempts_total, cache_hit_ratio, breaker_state, queue_depth, batch_throughput, cost_per_check Given at least 10% sampled requests When tracing Then create distributed spans for client→service→provider with attributes: idempotency_key hash, attempt, cache_status, breaker_state, provider_latency, and result_code Given error rate >5% over 5 minutes or p95 latency >2s When evaluating alerts Then send notifications to PagerDuty and Slack with runbook links; alerts auto-resolve after 10 minutes below thresholds Given any alert fires When auditing Then create an incident log entry with correlation IDs and the latest deployed changeset hash
Graceful User Messaging During Delays and Queued Checks
Given a check is queued or delayed due to retries, backoff, or an open circuit When rendering the UI Then show status badges (Queued, Delayed, Revalidating) with ETA and plain-language reason; messages meet readability grade ≤8 Given a delay exceeds 60 seconds When notifying the user Then send an in-app notification and optional email (if enabled) with current ETA and next update window Given the final result becomes available When updating the UI Then auto-refresh within 5 seconds, remove delay banners, and display a timeline of processing steps with durations Given an unrecoverable error occurs When presenting recovery options Then display clear actions (Edit info, Retry later, Contact support) with an error code and link to a help article

State e‑File Relay

File once; we route everywhere that applies. Automatically determine state thresholds, handle CF/SF participation and exceptions, generate state‑specific formats, and track acknowledgments alongside your federal submission—no extra portals to juggle.

Requirements

Automated Nexus & Threshold Determination
"As a freelancer, I want TaxTidy to tell me exactly which states I must file in and why so that I avoid penalties and unnecessary returns."
Description

Determines which U.S. states require a filing based on residency, economic nexus thresholds, client locations, sourced revenue, and presence signals. Ingests user profile data, invoices, 1099s, receipt geotags, and bank feed metadata to infer state-level obligations and de minimis exceptions. Applies tax-year–versioned rules for physical/economic nexus, market-based vs. cost-of-performance sourcing, reciprocity, part-year residency, and local add-ons (e.g., city taxes). Produces a rationale per state (rule hits, amounts, dates) with confidence scoring and an audit trail, exposing results to downstream e-file generation and submission orchestration.

Acceptance Criteria
Economic Nexus Threshold Evaluation From Invoices and Bank Feeds
Given the tax year Y and the state ruleset versioned for Y with revenue and transaction thresholds and de minimis exceptions And given normalized invoices, 1099s, and bank feed revenue categorized by service type with customer and destination state metadata When the system aggregates sourced revenue and transaction counts per state for Y Then for each state it flags requires_filing=true when revenue >= threshold_revenue OR transactions >= threshold_transactions, unless a de_minimis exception explicitly applies And it records state_code, threshold_revenue, threshold_transactions, aggregated_revenue, transaction_count, first_exceedance_date, and exception_applied (true/false) And it excludes non-taxable receipts identified by the state's rule definitions from the aggregation And unit tests using fixture data for at least 5 states assert exact boolean outcomes and numeric totals within ±$0.01 and exact dates
Physical Presence Nexus From Geotagged Receipts and On‑Site Work
Given a state's physical presence ruleset for Y with presence_day thresholds and listed exceptions And given receipt geotags, merchant locations from bank feeds, and invoice line items marked on-site with service dates and locations When the system infers presence_days and on_site_visits per state within Y Then it marks requires_filing=true for states where presence_days >= state.presence_days_threshold OR on_site_visits triggers nexus per rule And it records presence_days, on_site_visits, triggering_rule_id, and trigger_date in the state's rationale And it suppresses nexus when presence_days < threshold and an occasional_presence exception applies, recording exception_applied=true And automated tests validate computed presence_days for sample datasets to exact day counts
Sourcing Method Application (Market‑Based vs Cost‑of‑Performance)
Given a state marked market_based for services for Y and another marked cost_of_performance with apportionment percentages And given invoices with customer_state, service_performed_states with hours or cost percentages, and digital delivery metadata When the system computes sourced_revenue by state Then for market_based states it assigns revenue to customer_state and for cost_of_performance states it assigns revenue to the state(s) with highest proportion of costs, following tie-break rules in the ruleset And it outputs per-invoice allocation records showing invoice_id, rule_applied, source_states, amounts, and percentages totaling exactly 100% with rounding consistency to 2 decimals and ledger reconciliation to the original invoice total And unit tests verify allocations for provided fixtures match expected per-state amounts exactly
Residency and Part‑Year Residency Determination
Given the user profile with domicile state, residency periods (start/end), addresses, and move dates for Y And given state rules defining resident, part_year, and nonresident status tests When the system evaluates residency across Y Then it assigns each state a residency_status of resident, part_year with exact start/end dates, or nonresident And it requires filing for resident and part_year states when any taxable income is allocated to that period, and sets no_filing_reason when none is allocated And tests with sample profiles assert correct statuses and date ranges to the day
Reciprocity Handling for Neighboring States
Given a reciprocity agreements dataset for Y that applies only to wages/salaries and not to 1099/business income And given income records separated by type (W-2 wages vs 1099/business) with payer and work location states When the system determines nonresident filing obligations Then it suppresses nonresident filings for wage-only income covered by a valid reciprocity agreement while preserving resident filing and credit-for-taxes-paid rules And it does not suppress filings for 1099/business income even when reciprocity exists And the rationale records reciprocity_agreement_id, income_types_affected, and suppression_decision=true/false per state And tests cover at least three reciprocity pairs and mixed-income cases
Local Jurisdiction Add‑Ons (City/County) Detection
Given a mapping of local tax jurisdictions and thresholds for Y by geocoded location And given invoice ship-to/bill-to, service location geotags, and merchant addresses When the system evaluates local obligations Then it identifies applicable city/county filings or add-ons for each state obligation, with jurisdiction_code, threshold values, and computed amounts or triggers And it produces a local_requirements array per state and marks local_required=true when any jurisdiction applies And unit tests verify detection for at least 5 jurisdictions with edge cases at threshold boundaries
Rationale, Confidence Score, Audit Trail, and Rule Versioning Output
Given state determinations computed for Y When the system emits results to the e-file orchestration interface Then for each state it outputs a rationale object containing rule_hits (with rule_ids), amounts, dates, exception_flags, residency_status, sourced_revenue, and presence metrics And it includes confidence_score between 0.00 and 1.00 computed from data completeness and consistency, and sets review_required=true when confidence_score < 0.80 And it stamps rule_version=Y.vX and data_lineage pointers (source_document_ids with timestamps and checksums) And it exposes the results via a versioned API endpoint and message to an internal bus, both conforming to the published JSON schema with schema validation passing 100% And the audit trail is immutable for at least 7 years, with append-only entries and user-visible change logs including who/when/what for any overrides
CF/SF Participation Router & Exception Handling
"As a filer, I want TaxTidy to automatically use the right e-file route for each state so that I don’t have to manage different portals or rules."
Description

Selects the correct transmission channel per state and tax year, preferring IRS Combined Federal/State (CF/SF) when supported and automatically falling back to direct state gateways for non-participating or out-of-scope scenarios. Encodes prerequisites such as states that require federal acknowledgment before accepting a state return, packaging differences, and state-specific authentication. Maintains a versioned catalog of state capabilities, feature flags for phased rollouts, and logs the routing decision for traceability.

Acceptance Criteria
CF/SF Channel Selected When Eligible
Given a state S that participates in CF/SF for tax year TY in the catalog and the taxpayer’s forms are in-scope for CF/SF and the feature flag route.cfsf.enabled is true and all required credentials are present When a state filing for S in TY is initiated Then the router selects channel = "CF/SF" And the decision record includes reason_codes = ["CF_SF_ELIGIBLE"], state = S, tax_year = TY, catalog_version, and evaluated feature flags And no direct-state transmission attempt is made And the submission status is set to "Submitted - CF/SF"
Direct State Gateway Fallback for Non-Participating or Out-of-Scope
Given a state S that is not in CF/SF for tax year TY or the requested forms/return type are marked out-of-scope for CF/SF in the catalog and direct state gateway capability is available When a state filing for S in TY is initiated Then the router selects channel = "STATE_DIRECT" And the decision record includes reason_codes containing either "FALLBACK_NON_PARTICIPATING" or "FALLBACK_OUT_OF_SCOPE" And the transmission host/protocol matches the catalog entry for S and TY And no CF/SF transmission attempt is made And the submission status is set to "Submitted - State Direct"
Federal Acknowledgment Prerequisite Enforcement
Given a state S that requires federal acknowledgment before accepting a state return as defined in the catalog And a federal submission exists with acknowledgment status in {Accepted, Pending, Rejected, Unknown} When a state filing for S is requested Then if federal acknowledgment = Accepted, the router proceeds with normal channel selection and records prerequisite_evaluation = "FED_ACK=Accepted" And if federal acknowledgment ∈ {Pending, Unknown}, the filing is blocked, no transmission is attempted, the user is shown a message indicating the prerequisite, and an auto-retry is scheduled per polling policy And if federal acknowledgment = Rejected, the filing is blocked with error_code = "PREREQUISITE_FED_REJECTED" and guidance to correct and refile federal first And the decision log captures federal_ack_status, prerequisite_required = true, and action_taken
Packaging and Schema Compliance per Channel and State
Given a selected channel (CF/SF or STATE_DIRECT) for state S and tax year TY and the catalog-defined packaging and schema versions for S and TY When the filing payload is constructed Then the payload validates against the specified schema with zero validation errors And required attachments are included with correct MIME types, filenames, and encodings per catalog And envelope/headers conform to the channel-specific requirements (e.g., message type, namespaces, identifiers) And form ordering and identifiers follow catalog rules And a packaging_validation_result = "PASS" is stored with schema_version and channel
State-Specific Authentication Handling
Given a direct state gateway for state S that requires authentication type in {OAuth2, Certificate, API Key, Mutual TLS} as specified in the catalog When a transmission is attempted Then the system uses the correct authentication flow for S and TY and obtains/attaches valid credentials And if credentials are missing or expired, the transmission is not attempted and an error_code = "STATE_AUTH_FAILURE" with remediation steps is returned And secrets are never written to logs; only redacted identifiers are logged And successful transmissions record auth_method_used and token/certificate metadata (non-sensitive) in the decision log
Versioned Catalog, Feature Flags, and Decision Audit Logging
Given a versioned catalog with effective dates by state and tax year and feature flags controlling rollout When the router evaluates any state filing Then it resolves catalog entries by state S and tax year TY using the correct effective version and applies feature flags to the decision And a structured decision log entry is written containing at minimum: routing_decision_id, submission_id, state, tax_year, selected_channel, reason_codes, evaluated_rules, federal_prereq_required, federal_ack_status (if applicable), catalog_version, feature_flags_evaluated, and correlation_ids And toggling a relevant feature flag from true to false (and vice versa) results in a recomputed selected_channel consistent with catalog rules, with both decisions logged And decision logs are queryable by submission_id and routing_decision_id for traceability
State e-File Schema Generation & Business Rule Validation
"As a user, I want my state returns to be assembled and validated correctly so that they’re accepted the first time without manual fixes."
Description

Generates state-specific e-file payloads (XML/JSON) and attachments from TaxTidy’s normalized tax data, supporting resident, part-year, and nonresident variants. Applies per-state schemas and business rules (cross-field checks, numeric tolerances, date and rounding rules, required attachments) prior to submission. Includes digital signature/PIN capture, PII redaction where needed, and pre-flight validation against official schemas and rule packs, with clear error messages mapped back to data fields.

Acceptance Criteria
Resident Return Payload Generation
Given a supported state and tax year with residency = Resident and normalized taxpayer data is complete When the state e-file generator is invoked Then a state-specific payload is produced in the required format (XML or JSON) for that state And the schema version in the payload matches the configured state version And only fields applicable to the state are present per the mapping And for identical input, the generated payload is byte-identical across repeated runs And the payload is stored with a unique artifact ID and generation timestamp
Part‑Year and Nonresident Variants
Given a taxpayer marked Part-Year or Nonresident with state-specific period and allocation data populated When the payload is generated for that state Then the correct residency variant structure is produced per the state's schema And income and deduction amounts are allocated per provided period/allocation data And resident-only sections are omitted or zeroed per state guidance And the variant passes the same schema validation as resident returns
Official Schema Validation (XSD/JSON Schema)
Given a generated state payload and the state's official schema reference When schema validation is executed Then the payload validates with zero errors and zero unresolved warnings And deprecated fields disallowed by the selected schema version are absent And enumerations and formats (dates, ZIP codes, SSN format) conform to the schema
Business Rules: Cross-Field, Rounding, Tolerances, Dates
Given a state rule pack with cross-field equations, numeric tolerances, rounding, and date rules When rule validation runs on the payload and source data Then all cross-field checks pass (e.g., totals equal sum of components within ±$1 tolerance where allowed) And rounding is applied per state rule (e.g., round to nearest dollar) and reflected consistently across fields And date fields conform to the state's expected format and valid ranges And fields prohibited from being negative are non-negative; permitted negatives are preserved
Required Attachments and PII Redaction
Given a state that requires supporting attachments (e.g., W-2s, 1099s, schedules) and any redaction rules When the payload is assembled Then all required attachments are included in the required encoding (e.g., PDF base64) with correct content type and labels And attachments and payload fields subject to PII redaction are redacted per state rules (e.g., SSN truncated, account numbers masked) And attachment references in the payload match their content hashes and sizes And omission of a required attachment produces a blocking pre-flight error listing the missing items
Digital Signature/PIN Capture and Embedding
Given taxpayer (and preparer, if applicable) identity verified and consent captured When the e-sign flow is completed Then state-required signature/PIN fields are captured in the required format and embedded in the payload And a timestamp and IP (where required) are recorded and associated to the signature And signatures/PINs are masked in logs and audit trails And missing or invalid signature data blocks submission with a clear, actionable error
Pre-Flight Validation Report and Field-Level Error Mapping
Given schema and rule validation failures occur When pre-flight validation is executed Then a single report is produced listing each error with severity, state rule or schema citation, and human-readable message And each error is mapped to the originating normalized data field path and UI control ID And the report includes fix suggestions where mapping exists and is exportable (JSON) for support And zero errors of severity 'Error' marks the payload as ready for submission
Multi-State Submission Orchestration & Resilient Delivery
"As a busy freelancer, I want to file all my required state returns with a single action so that I save time and avoid coordination errors."
Description

Coordinates one-click submission across all applicable states, sequencing transmissions based on dependencies (e.g., federal-first states), state maintenance windows, and rate limits. Provides idempotent, queue-backed delivery with retries, exponential backoff, and dead-letter handling. Ensures secure transport, request signing, and checksum verification, with observability (metrics, logs, traces) and operational runbooks for support.

Acceptance Criteria
Sequenced Submission with Federal-First Dependency
Given a filing package includes states configured with requires_federal_acceptance=true and others with requires_federal_acceptance=false And the user initiates a single submission When transmissions are orchestrated Then the federal return is transmitted before any requires_federal_acceptance=true state And non-dependent states begin transmission within 60 seconds of submission And dependent states begin transmission within 120 seconds of receiving a federal "ACCEPTED" acknowledgment And no dependent state transmission occurs prior to federal "ACCEPTED" And all transmissions and acknowledgments are recorded with a shared correlation_id
Adaptive Scheduling for Maintenance Windows and Rate Limits
Given State X has a configured maintenance window [start_utc, end_utc] and a per-state rate limit of L requests/minute And a submission targets State X during its maintenance window When the orchestrator schedules transmissions Then the transmission to State X is queued and starts within 60 seconds after end_utc And the system does not exceed L requests over any rolling 60-second interval for State X And queued transmissions are spread with up to 20% random jitter to avoid bursts And the UI shows State X as "Queued (Maintenance Window)" until dispatch
Idempotent Delivery and De-duplication across Retries
Given an idempotency_key is generated per state submission And the same submission is retried or re-triggered within 24 hours using the same idempotency_key When the orchestrator processes the request Then at most one outbound transmission occurs per state endpoint for that idempotency_key And subsequent attempts return the original submission_id and final status without re-sending on the wire And the outbound request log shows exactly one transmission per state for the idempotency_key And duplicate attempts are labeled "DuplicateIgnored" in audit logs
Exponential Backoff with Retryable Error Classification
Given a state transmission fails with a retryable condition (HTTP 429, 5xx, or network timeout) When retries are attempted Then the backoff schedule approximates [1s, 2s, 4s, 8s, 16s] with ±20% jitter, capped at 5 attempts And the total elapsed retry time does not exceed 30 minutes And non-retryable errors (HTTP 4xx excluding 408/429) are not retried And retry_count and last_error_code metrics are emitted per attempt And success after a retry updates the submission status without duplicate transmissions
Dead-Letter Queue Handling and On-Call Escalation
Given a message exceeds max retry attempts or encounters a non-retryable error When the message processing terminates Then the message is moved to the dead-letter queue with fields: submission_id, state_code, idempotency_key, first_seen_at, last_error_code, attempt_count, payload_hash And an alert is sent to the on-call channel within 60 seconds containing submission_id and runbook_url And authorized operators can requeue the message via admin tooling with a single action And an immutable audit log entry records the requeue action with actor, timestamp, and reason And DLQ depth is exposed via metrics and dashboards
Secure Transport, Request Signing, and Checksum Verification
Given outbound connections to state or relay endpoints When transmitting payloads Then TLS 1.2+ with approved cipher suites is enforced And each request includes a Signature header generated with HMAC-SHA256 over a canonicalized payload and timestamp And a SHA-256 checksum header or trailer is included and verified against the received payload And on any signature or checksum mismatch, the transmission is aborted, no retry is attempted, and a security_event log is produced without leaking secrets And keys used for signing are KMS-managed and never written to logs
End-to-End Observability: Metrics, Logs, Traces, and Dashboards
Given submissions are processed through the orchestrator When observing the system Then the following metrics are emitted with state_code and outcome labels: submission_attempts_total, submission_success_total, retry_attempts_total, dlq_messages_total, queue_depth, e2e_latency_seconds (p50/p95/p99) And 95% of submission traces include spans for enqueue, transmit, ack_poll, and persist with a shared trace_id and correlation_id And structured logs include request_id, submission_id, state_code, attempt and are queryable within 1 minute And dashboards show per-state success rate and backlog And SLO alerts trigger when p95 e2e_latency_seconds > 10 minutes for 5 consecutive minutes or success_rate < 99% over 1 hour
Acknowledgment Tracking, Rejection Mapping, and Remediation
"As a user, I want clear, real-time status and actionable guidance on any state rejections so that I can resolve issues quickly without tax expertise."
Description

Tracks per-state acknowledgment lifecycle in real time (received, accepted, rejected, accepted-with-warnings), normalizing disparate status codes into a unified timeline alongside the federal submission. Maps rejection codes to human-readable causes and guided fixes, links issues to source documents, and supports one-click correction and resubmission. Provides exportable audit logs, notifications, and SLAs for polling and state heartbeat checks.

Acceptance Criteria
Unified Acknowledgment Status Normalization
Given a state gateway returns any raw acknowledgment code or message When the system ingests the response Then it maps the raw code to one of [Received, Accepted, Rejected, Accepted with Warnings, Unknown] according to the maintained mapping table And stores the raw code, mapped status, jurisdiction, and UTC timestamp in the event log And displays the mapped status in the UI within 60 seconds of receipt And if the raw code is unmapped, it sets mapped status to Unknown, flags the event, and creates a mapping task with severity High
End-to-End Timeline Alignment with Federal Filing
Given a federal return and one or more state returns are filed as part of the same session When acknowledgment events are received Then the filing timeline shows a single chronological sequence combining federal and all states with event type, jurisdiction, and timestamp And events are time-ordered to the second with a deterministic tiebreaker (federal before state on equal timestamps) And the timeline updates within 60 seconds of event receipt And users can filter by jurisdiction without altering underlying order And each event shows its source (CF/SF relay, direct state, or aggregator)
Error and Warning Mapping to Human-Readable Causes and Guided Fixes
Given a state rejection code or message is received When the user opens the details Then the UI shows a plain-language cause specific to that jurisdiction and code And links the cause to the exact source document and field(s) involved And presents a step-by-step fix with pre-filled values and validation rules And provides authoritative references (state bulletin or schema) when available Given an Accepted with Warnings status is received When the user opens warning details Then the UI shows warning descriptions, impact, and optional remediation steps And resolving warnings does not block the filing's Accepted status
One-Click Correction and Resubmission
Given a filing is in Rejected status with identified fix steps When the user selects "Fix & Resubmit" Then the system opens the correction form scrolled to the first failing field And prevents resubmission until all blocking validations pass client-side and server-side And upon submission, packages and sends the corrected payload to the appropriate state or CF/SF relay And appends a new attempt to the audit chain linking to the prior rejection event id And updates status to Resubmitted and resumes polling without requiring the user to re-authenticate
Polling SLA and State Heartbeat Checks
Given a filing is in Submitted or Resubmitted state When awaiting acknowledgments Then the system polls the state endpoint at least every 5 minutes until first response, then every 30 minutes until a final status (Accepted | Rejected | Accepted with Warnings) And records each poll attempt with timestamp, endpoint, request id, and outcome And raises a heartbeat alert if no response or successful poll occurs for 60 consecutive minutes within filing hours And escalates to user notification if no final status within 24 hours, including next steps
Notifications for Lifecycle Events and SLA Breaches
Given the user has enabled notifications When an acknowledgment status changes or an SLA breach occurs Then the user receives an in-app notification immediately and an email/push within 2 minutes And notifications include filing id, jurisdiction, prior status, new status, and recommended action And notification preferences can be configured per channel and per severity And duplicate notifications for the same event are suppressed within a 10-minute window
Exportable, Tamper-Evident Audit Log
Given a filing exists with one or more events When the user requests an export Then a downloadable CSV and JSON are generated within 5 seconds for up to 5,000 events And each row includes event id, filing id, jurisdiction, raw code, mapped status, timestamp (UTC), actor, request id, attempt number, and checksum And the export includes a file-level SHA-256 and per-row HMAC to provide tamper evidence And the export reflects the current timeline order and includes resubmission linkages
State Payments & Extension Workflow
"As a taxpayer, I want TaxTidy to handle my state payments and extensions in the same flow as filing so that I don’t miss deadlines or juggle separate tools."
Description

Calculates per-state balances due and supports scheduling electronic funds withdrawal or generating payment vouchers, including split payments and payment limits. Enables state extension e-file when thresholds are met or requested, aligning with federal extension where applicable, and tracks confirmation numbers and deadlines. Surfaces reminders, payment statuses, and reconciliation back to the dashboard.

Acceptance Criteria
Per-State Balance Calculation from Aggregated Data
Given the user has connected invoices, bank feeds, and receipt photos with categorized tax data for at least one state, When the system performs state tax computations, Then it calculates taxable income, credits, withholdings, estimated payments, and balance due/refund per state using the current rules library and displays the breakdown, with totals matching reference calculations within $1.00. Given the user updates a transaction category or adds/removes a document affecting state liability, When recalculation is triggered, Then all affected state balances refresh within 10 seconds and an entry is recorded in the audit log with before/after amounts. Given a state's filing threshold is not met, When computations complete, Then that state is marked "No filing required," balance due is $0.00, and the rule version/date is captured for traceability. Given partial-year residency or nonresident income allocations exist, When state computations run, Then allocations apply per state rules and the resident/nonresident status is reflected in the output summary.
Electronic Funds Withdrawal Scheduling with Limits
Given a state supports electronic funds withdrawal (EFW), When the user selects EFW and enters an amount and date, Then the date picker constrains to the state's allowed window (no earlier than today, no later than the state's due/extension date) and the amount must be between $1.00 and the state's per-payment maximum. Given an entered amount exceeds the state's per-payment limit, When the user proceeds, Then the system blocks submission and offers to split into N payments such that each payment is <= the limit and the sum equals the total. Given a payment is scheduled, When submission occurs, Then a submission record is created with payment amount, date, bank account last4, state, and a client-side receipt ID, and the UI displays "Scheduled" within 5 seconds. Given a scheduled payment is pending and before the state's cancellation cutoff, When the user cancels, Then the payment is canceled and removed from the outgoing queue, and a cancel confirmation is displayed and logged.
Payment Voucher Generation with Split Payments
Given the user selects "Pay by voucher" for a state, When the total due is provided, Then the system generates a PDF voucher package with prefilled taxpayer info, barcode (if supported), state tax year, and amount fields. Given the total due exceeds the state's per-voucher limit or the user requests split payments, When generating vouchers, Then multiple vouchers are created with unique sequence identifiers where each amount is <= the limit and the sum equals the total due (rounding variance <= $0.01). Given vouchers are generated, When the package is downloaded, Then the file name follows the convention StateAbbrev_TaxYear_TaxpayerLast4_Vouchers.pdf and the PDF passes a basic PDF/A validation. Given vouchers are generated, When printing instructions are shown, Then the system displays the correct remittance address and due date instructions for that state and year.
Automatic State Extension Filing Eligibility and Alignment
Given the estimated state balance due and filing threshold rules, When the user is approaching a due date, Then the system flags states requiring or allowing extensions and preselects extension filing where thresholds are met. Given the user has filed or is filing a federal extension, When a state aligns with the federal extension, Then the state extension adopts the federal extension date and references the federal confirmation number if required by the state. Given a state supports e-filed extensions, When the user confirms, Then the extension is transmitted electronically; otherwise, a completed printable extension form is generated with mailing instructions. Given an extension payment is scheduled as part of the extension, When the extension is transmitted, Then the payment amount is included in the state's balance tracking and not double-counted in later payment workflows.
Acknowledgments, Confirmation Numbers, and Deadline Tracking
Given an e-filed payment or extension is submitted, When the state acknowledgement is received, Then the system stores the acknowledgment status (Accepted/Rejected/Pending), the confirmation number, the received timestamp (UTC), and the raw ack payload, and displays status within 15 minutes. Given an e-file is rejected, When the rejection arrives, Then the system surfaces the state error code/message and provides a retry option or switch-to-voucher option as applicable. Given no acknowledgment is received within 24 hours for a submitted item, When monitoring runs hourly, Then the system flags the item as "Delayed," notifies the user, and begins automated status inquiries if the state supports them. Given a state has a payment or extension deadline, When the dashboard renders, Then the deadline is displayed with the correct timezone and observes weekend/holiday rollover rules for that state.
Dashboard Reminders and Payment Status Synchronization
Given there are scheduled payments, vouchers to mail, or pending extensions, When the user opens the dashboard, Then the system shows a consolidated list with current status (Scheduled/Paid/Pending/Rejected/Needs Mailing) and the next action per item. Given a scheduled EFW payment clears or an acknowledgment of payment acceptance is received, When reconciliation runs, Then the item status changes to "Paid" within 1 hour and the completion timestamp is logged. Given the connected bank feed shows a debit matching a scheduled payment amount (+/- $0.01) within a 3-day window of the scheduled date, When reconciliation runs, Then the bank transaction is linked to the payment and the status is confirmed even if the state ack is delayed. Given reminders are enabled, When an item is due in 7, 3, or 1 day(s), Then the user receives a push/email reminder with the item details and a link to act, with the ability to Snooze or Dismiss.
Multi-State, CF/SF Exceptions, and Non-EFW States Handling
Given the user has nexus in multiple states, When the workflow runs, Then separate state payment/extension items are created for each state with state-specific options and due dates, and a combined summary total is displayed. Given a state participates in CF/SF for extensions or payments but has exceptions, When transmitting, Then the system uses CF/SF where allowed and falls back to direct state submission where required, clearly indicating the route taken per state item. Given a state does not support EFW, When the user attempts to select EFW, Then the option is disabled with an explanation and "Generate voucher" is preselected. Given conflicting state residency rules impact allocation, When computations and submissions are prepared, Then the system requires the user to confirm residency status per state before enabling transmission and records the confirmation.

Reject Shield

Catch errors before the IRS does. Business‑rule and schema validation scans each 1099‑NEC for common reject causes (amount rounding, address formats, TCC/Payer data) and provides one‑tap fixes. Improves first‑pass acceptance and saves re‑submission fees.

Requirements

1099-NEC Schema Validation
"As a freelancer preparing 1099-NECs, I want my forms checked against IRS technical rules so that I avoid immediate rejections due to formatting or schema errors."
Description

Implement server-side and client-side validation for 1099-NEC forms against the latest IRS e-file specifications (e.g., Publication 1220) and paper layout constraints. Enforce required field presence, field lengths and formats (EIN/SSN, ZIP+4, state codes), monetary precision and rounding, date formats, and TCC/Payer record structure. Provide versioning by tax year, machine-readable error codes with severities, and localized messages. Integrate with TaxTidy’s form editor and submission flow to run on save, import, and pre-submit, exposing an API for other modules to invoke validations.

Acceptance Criteria
Tax Year–Specific Schema Selection and Versioning
Given a 1099‑NEC draft with taxYear specified When validation runs Then the validator loads the IRS schema for that taxYear and records schemaVersionUsed in the results Given a 1099‑NEC draft with an unsupported taxYear When validation runs Then a single issue is returned with code=SCHEMA_VERSION_UNSUPPORTED and severity=Error, and no field-level rules are evaluated Given a 1099‑NEC draft that previously validated under a different taxYear When the user changes taxYear and re-validates Then issues not applicable to the new schema version are removed and new issues for the new version are produced
Required Fields and Format Enforcement (EIN/SSN, ZIP+4, State Codes)
Given an incomplete 1099‑NEC When validation runs Then all missing required fields per schema are reported with code=MISSING_REQUIRED_FIELD and precise fieldPath for each Given payer or recipient TIN values When validation runs Then values must match allowed patterns per schema (e.g., EIN 9 digits with optional hyphen, SSN 9 digits with optional hyphens) or return code=INVALID_TIN_FORMAT severity=Error Given address fields When validation runs Then ZIP must be 5 digits or ZIP+4 (NNNNN-NNNN) or return code=INVALID_ZIP_FORMAT; state must be a valid USPS two-letter code for the schema year or return code=INVALID_STATE_CODE Given name and address lines When validation runs Then values exceeding schema-defined max length return code=FIELD_LENGTH_EXCEEDED with the number of excess characters in details
Monetary Precision, Rounding, and Nonnegative Amounts
Given any amount field defined by the schema When validation runs Then values must have at most two decimal places; more precision returns code=AMOUNT_PRECISION_EXCEEDED severity=Error Given any amount field When validation runs Then amounts must be >= 0 where the schema prohibits negatives; otherwise return code=NEGATIVE_AMOUNT_NOT_ALLOWED severity=Error Given any amount field subject to rounding rules When validation runs Then rounding is applied per schema-year rule before submission preview; if the rounded value differs from input, return a Warning with code=AMOUNT_ROUNDED including original and rounded values
Payer and TCC Record Structure Validation
Given a filing package containing 1099‑NEC records When validation runs Then TCC must be present and match the schema-defined pattern or return code=INVALID_TCC_FORMAT severity=Error Given payer records linked to recipient records When validation runs Then required payer fields (name, address, TIN) must be present and associated to each recipient or return code=PAYER_RECORD_INVALID severity=Error Given multiple recipient records under the same payer When validation runs Then payer-level fields must be consistent across those records or return code=PAYER_RECORD_MISMATCH severity=Error
Validation Triggers in Editor, Import, and Pre‑Submit
Given a user edits a 1099‑NEC in the form editor When the user saves Then validations execute and the UI displays a count of issues by severity, and submission is disabled while any Error severity issues exist Given a user imports 1099‑NEC data (CSV/API) When import completes Then validations execute and errors are attached to failed rows with rowNumber and fieldPath for each issue Given a user initiates submission When pre‑submit checks run Then the system blocks submission if any Error severity issues remain and focuses the first error field in the editor; client and server results must match before enabling submission
Machine‑Readable Errors with Severity and Localization
Given validation finds violations When results are returned Then each issue includes code (stable string), severity in {Error, Warning, Info}, fieldPath (JSON Pointer), messageKey, messageLocalized for the current locale, and schemaVersionUsed Given the user changes locale When validations are re-requested or messages are refreshed Then messageLocalized appears in the new language while code, severity, and fieldPath remain unchanged Given multiple identical violations for the same fieldPath and code When validation runs Then only one issue is returned per code+fieldPath per run (no duplicates)
Public Validation API for 1099‑NEC
Given another module needs to validate a 1099‑NEC payload When it invokes the Validation API with resourceType=1099‑NEC and provides payload and taxYear Then the API returns 200 with a JSON body containing issues[] (each with code, severity, fieldPath, messageKey, messageLocalized) and metadata.schemaVersionUsed Given the Validation API is called without a taxYear or with malformed input When the request is processed Then the API returns 400 with code=INVALID_REQUEST and details of missing/invalid parameters Given the Validation API is called with an unsupported taxYear When the request is processed Then the API returns 422 with code=SCHEMA_VERSION_UNSUPPORTED and no field-level issues
Business Rule Library
"As a solo consultant, I want business-level checks that catch issues humans make (like duplicate recipients or malformed addresses) so that my filings pass on the first attempt."
Description

Create a maintainable rule engine capturing common IRS reject causes beyond schema, including address normalization requirements, payer/recipient name formats, missing/invalid TCC or Payer State ID, duplicate recipient detection, Box 1 amount consistency and rounding, state/province handling for domestic vs. foreign addresses, and phone/ZIP standards. Support rule metadata (ID, severity, rationale, fix strategy), enable/disable per tax year, and telemetry on rule hits. Integrate with TaxTidy data sources (payer profile, recipients, invoices, bank feeds) to cross-check and enrich validations.

Acceptance Criteria
Rule Metadata, Severity, and Year Enablement
Given a validation rule is created When it is saved Then it must include fields: ruleId (format RR-####), title, severity (Error|Warning|Info), rationale (<=280 chars), fixStrategy (code), and actionLabel Given a taxYear context is selected When rules are loaded Then only rules enabled for that taxYear are evaluated and rules disabled for that year are skipped Given a taxYear with no explicit rule set When rules are loaded Then the engine falls back to the most recent prior defined year and logs an Info event Given a rule with severity Error produces at least one finding When submission readiness is computed Then status is Blocked until findings are fixed or the rule is disabled for the taxYear by an admin Given a violation has a fixStrategy When the user taps One‑Tap Fix Then the mapped fixer executes, updates the underlying data, and triggers re‑validation; the violation’s resolution is recorded as Fixed, Deferred, or Failed
Validate TCC and Payer State ID
Given a payer profile exists for the filing year When validating Then TCC must be present and match pattern [A-Z0-9]{5}; otherwise create an Error with code RR-TCC-001 and provide a One‑Tap Fix to open TCC entry Given a payer files in a state that requires a Payer State ID When validating Then Payer State ID must be present and be 4–20 alphanumeric characters; otherwise create an Error RR-PSID-001 with One‑Tap Fix linking to payer state settings Given a TCC value exists in both payer profile and current return payload When validating Then the values must match; on mismatch create a Warning RR-TCC-002 offering One‑Tap Fix to sync profile value into the return Given invalid TCC format is present When user attempts to submit Then submission is blocked and the offending field is focused after returning from the e‑file check
Box 1 Rounding and Source Consistency
Given a 1099-NEC Box 1 amount exists When validating Then Box 1 must equal round(sum(raw payment amounts)) to the nearest whole dollar using .50 up; otherwise create Error RR-AMT-001 with One‑Tap Fix to set Box 1 to expected Given source transactions from invoices and bank feeds are tagged taxable for a recipient and year When validating Then their currency must be USD or have a recorded FX conversion; if not, create Error RR-AMT-002 requiring FX rate entry Given Box 1 is zero but at least one taxable payment exists When validating Then create Error RR-AMT-003 suggesting One‑Tap Fix to populate Box 1 from sources Given no taxable payments exist and a 1099-NEC is prepared When validating Then create Warning RR-AMT-004 suggesting deletion or reclassification
Duplicate Recipient Detection and Resolution
Given multiple 1099-NEC forms exist for the same payer and tax year When validating Then flag duplicates where TIN matches and at least one of {normalized name match ≥ 0.9, normalized address match} is true; create Warning RR-DUP-001 Given an exact duplicate exists (same payer, recipient TIN, and Box 1) When validating Then create Error RR-DUP-002 to prevent double submission Given user selects One‑Tap Merge on a duplicate pair When executed Then merge sums Box 1, retains the most recently updated address, archives the duplicate, and re‑runs validation automatically Given potential duplicates are dismissed by the user When validating Then record resolution Deferred and suppress the same duplicate pair for the session unless data changes
Domestic vs. Foreign Address Rules
Given recipient or payer address has country US or empty When validating Then State must be a valid USPS 2‑letter code, City non‑empty, and ZIP must be 5 or ZIP+4; otherwise create Error RR-ADR-001 with One‑Tap Normalize Given address country is not US When validating Then State must be empty, Province/Region and Country must be present, and Postal Code must be present; otherwise create Error RR-ADR-002 with One‑Tap Fix to move state into province and require country Given a foreign address is present When generating e‑file payload Then use ForeignAddress fields per schema and ensure ASCII transliteration is applied when non‑ASCII characters are detected
Phone and ZIP Standards Normalization
Given a payer contact phone is stored When validating Then phone must normalize to E.164 format +1XXXXXXXXXX for US numbers; otherwise create Warning RR-PHN-001 with One‑Tap Normalize that strips punctuation and country code adjusts Given a ZIP is present When validating Then format must be NNNNN or NNNNN-NNNN; otherwise create Error RR-ZIP-001 with One‑Tap Normalize Given a required phone or ZIP field is missing for e‑file metadata When validating Then create Error RR-CON-001 and block submission until populated
Rule Hit Telemetry and Analytics
Given any rule produces a finding When recorded Then telemetry must include ruleId, taxYear, entityType (Payer|Recipient|Form), severity, timestamp, hitId, resolution (Open|Fixed|Deferred|Failed), and dataSources referenced; TINs are hashed and addresses truncated to city/state Given telemetry is enabled When findings occur Then ≥95% of events are delivered to the analytics sink within 5 seconds, with retry up to 3 attempts on failure Given a user has opted out of analytics When validating Then no telemetry events are sent and local counters are suppressed Given telemetry is flowing When queried in analytics by ruleId and day Then counts match in-app totals within a ±2% tolerance
One-Tap Auto-Fix
"As a mobile-first user, I want one-tap fixes for common errors so that I can correct issues quickly without understanding every IRS formatting rule."
Description

Provide actionable, safe auto-fixers for common validation failures with a single tap, including rounding Box 1 amounts, standardizing payer/recipient names and phone formats, filling TCC/Payer data from the TaxTidy profile, normalizing addresses via USPS/CASS, and auto-deriving missing state abbreviations or ZIP+4 where possible. Present a preview with before/after diffs, allow “Fix All,” support undo, and log all changes. Integrate fixes directly into the editor, checklist, and batch workflows.

Acceptance Criteria
One-Tap Round Box 1 in Editor
Given a 1099-NEC with Box 1 amount containing cents and a validation error for non-whole-dollar values When the user taps One-Tap Auto-Fix on the Box 1 error in the editor Then the Box 1 amount is rounded to the nearest whole dollar (.50+ rounds up; <.50 rounds down) And the updated whole-dollar value is displayed in the Box 1 field and saved to the record And the Box 1 validation error is cleared immediately And a confirmation toast shows “Rounded from $X.XX to $Y.00” within 1 second And an immutable change log entry is recorded with rule=rounded_amount, field=box1, old_value, new_value, user_id, timestamp And if the value is already a whole dollar, no change occurs and a “No changes needed” notice is shown
Auto-Fill TCC and Payer Data from Profile
Given a 1099-NEC with missing TCC and/or Payer fields flagged by validation When the user taps One-Tap Auto-Fill in the editor or checklist Then missing TCC/Payer fields (TCC, Payer Name, EIN, Address, Phone, Email where applicable) are populated from the TaxTidy profile And non-empty fields are not overwritten unless the user explicitly selects Overwrite in the preview And all populated values pass schema/business rules (TCC length/charset, EIN format XX-XXXXXXX, phone format, email format) And any fields that cannot be populated due to missing profile data are listed to the user with required actions And changes are presented in a preview with before/after diffs prior to apply And upon apply, each field change is logged (field, old, new, rule=profile_autofill, user_id, timestamp) And operation completes within 1 second for a single form on a typical network
USPS/CASS Address Normalization and ZIP+4 Derivation
Given a payer or recipient address with formatting errors, missing state abbreviation, or missing ZIP+4 When the user taps Normalize Address Then the address is validated via USPS/CASS and normalized to USPS-standard formatting (street line, city, 2-letter state, ZIP+4 when available) And if multiple CASS candidates are returned, the user is prompted to choose; no auto-apply occurs without selection And if CASS returns deliverability=Y, the normalized address is applied; if deliverability is unknown/ambiguous, the system surfaces the issue without auto-changing And missing state abbreviations are derived from the resolved city/ZIP; missing ZIP+4 is appended when available And the address-related validation errors are cleared after successful apply And the change appears in the before/after preview and is logged with rule=address_normalization per field changed And on network/API failure, no changes are made and a retry option is offered
Standardize Names and Phone Formats
Given payer/recipient name or phone number fails formatting validation When the user taps One-Tap Auto-Fix for identity formatting Then names are trimmed, multiple spaces collapsed, and non-printable characters removed without altering token order (e.g., LLC, Inc., suffixes retained) And phone numbers are standardized to E.164 for US (+1XXXXXXXXXX) by stripping non-digits, inferring country=US when missing, and validating 10-digit requirement And if the phone has fewer than 10 or more than 15 digits, no change is made and a specific error is displayed And all proposed changes are shown in a before/after preview with per-field selection And applied changes clear related validation errors and are logged with rule=format_standardization per field And operation completes within 1 second for a single form
Preview Before/After Diffs with Fix All
Given one or more validation failures with available auto-fixes on a form When the user opens the Auto-Fix preview Then a side-by-side before/after diff lists each field change with rule labels and checkboxes selected by default for safe fixes And the user can deselect any proposed change and tap Fix All to apply only the selected fixes And the Fix All operation is atomic for the selected form: either all selected changes apply or none; on partial failure, all changes are rolled back and errors are shown And after apply, a summary modal shows count of fields changed and which validation errors were cleared And all applied changes are logged in a single transaction batch id linking the individual field change entries And the Fix All action is available in both editor and checklist contexts
Undo Last Auto-Fix with Full Revert
Given one or more auto-fixes were applied (individual or Fix All) on a form When the user taps Undo Then all fields changed by the most recent auto-fix action are reverted to their exact previous values within 1 second And any validation errors previously cleared by that action are re-evaluated and reinstated if applicable And a log entry is created linking the undo to the original change batch id with rule=undo_autofix And Undo is disabled after the form is submission-locked, with a tooltip explaining why And Redo is available to re-apply the same change set until another edit occurs
Batch Workflow Auto-Fixes with Progress and Error Handling
Given the user selects multiple 1099-NECs in a batch workflow with auto-fixable validation errors When the user opens the batch Auto-Fix preview and taps Fix All Then the system displays a preflight summary (records selected, fixes by rule, conflicts requiring manual review) and requires confirmation And upon confirmation, fixes are applied per record with a real-time progress indicator and per-record statuses (Pending, Fixed, Skipped, Failed) And the batch continues on individual record failures; failures provide actionable reasons and links to open in editor And the batch operation is idempotent; re-running does not duplicate prior successful fixes And median completion time for 200 records is under 2 minutes on a typical network And a downloadable batch change log (CSV/JSON) is generated with batch id, per-record entries, and rule summaries
Inline Validation & Guidance
"As a user filling a 1099-NEC, I want instant feedback while I type so that I can fix problems before I move on to the next field."
Description

Embed real-time, field-level validation in the 1099-NEC editor with inline error states, tooltips linking to rule explanations, and suggested fixes. Support keyboard and mobile gestures, accessible announcements (ARIA), and latency-tolerant behavior with local checks even when offline. Sync with server-side validation to prevent drift and ensure consistent results across devices.

Acceptance Criteria
Inline Amount Validation in 1099‑NEC Editor
Given any amount field contains more than two decimal places, When the field loses focus or the user attempts to submit, Then an inline error appears within 200 ms with the message "Amounts must be rounded to the nearest cent" and a "Round to $X.XX" suggested fix control, And when the user activates the suggestion via click, tap, or Enter, Then the value is rounded and the error clears immediately. Given a negative value is entered in a non‑negative amount field, When the field loses focus or submit is attempted, Then an inline error appears with the message "Amount cannot be negative" and submission is blocked until corrected. Given non‑numeric characters (excluding a single decimal point) are entered, When input occurs, Then the field prevents invalid characters and displays an inline helper "Numbers only" without losing focus. Given a locally validated amount is saved, When server‑side validation runs, Then no amount‑format errors are returned for that field; otherwise, the server error is displayed inline within 200 ms and overwrites the local pass state.
Payer/Payee TIN and Address Formatting Guidance
Given a TIN is entered with fewer or more than 9 digits or includes non‑digits, When the field loses focus, Then an inline error appears "TIN must be 9 digits" with a "Remove formatting" suggested fix that strips dashes/spaces, And activating the fix normalizes the value and clears the error if 9 digits remain. Given a US state is entered not as a 2‑letter uppercase code, When the field loses focus, Then a suggestion "Convert to uppercase and valid state code" is offered; activating it converts case or prompts selection from a validated list. Given a ZIP code is 9 digits without a hyphen, When the field loses focus, Then a suggestion "Format as ZIP+4" is offered and formats to 12345‑6789; if only 5 digits are present, no error is shown. Given address fields exceed the schema length limits, When typing exceeds the limit, Then a character counter warns at 10 remaining and prevents entry beyond the max.
Inline Error Tooltips with Rule Explanations and One‑Tap Fixes
Given a field is in an error state, When the user focuses the error icon via keyboard or taps it, Then a tooltip opens within 150 ms containing a plain‑language explanation and a link "Learn the rule" that opens the rule article in‑app without navigating away. Given a tooltip is open, When Esc is pressed, focus moves, or a tap/click occurs outside, Then the tooltip closes and focus returns to the field. Given multiple fields have errors, When a tooltip opens on one field, Then any other open tooltip closes to ensure only one is visible. Given a suggested fix is available in the tooltip, When the user activates it, Then the field value is transformed as suggested, the tooltip closes, and the error state clears if the field now passes validation.
Offline Local Validation and Latency‑Tolerant Behavior
Given the device is offline or the server validation response exceeds 800 ms, When a field loses focus, Then local validation runs using the last downloaded ruleset and displays pass/fail within 100 ms. Given local validation has run while offline, When connectivity is restored, Then the client automatically revalidates against the server and reconciles results; if there is a difference, the server result replaces the local result and a non‑blocking banner "Rules updated, review changes" is shown. Given submission is attempted while server validation is pending, Then the submit action is blocked with an inline summary stating which fields require server confirmation, and the UI remains responsive with a spinner not exceeding 30 seconds before timeout. Given a suggested fix is local‑only, Then it is available offline; Given a fix requires server data, Then it is labeled "Requires connection" and is disabled while offline.
Keyboard Navigation and Mobile Gesture Support
Given validation errors exist, When the user presses Enter on a suggested fix control, Then the fix is applied; When Esc is pressed while a tooltip or error message is focused, Then it closes without changing data. Given the user navigates via keyboard, When Tab/Shift+Tab is used, Then focus moves sequentially through inputs and error icons in a logical order and never traps focus. Given the user attempts to submit with errors, When the error summary appears, Then focus moves to the first invalid field and a "Next error" control becomes available; pressing Alt+N (desktop) or tapping the "Next error" pill (mobile) focuses the next invalid field. Given the app is used on mobile, When the user taps an inline error chip, Then the view scrolls the field into view with 16 px offset and opens the tooltip.
Accessible ARIA Live Announcements for Validation
Given a field enters an error state, When the error message renders, Then the message is exposed via aria-live="polite", the field has aria-invalid="true", and aria-describedby references the error element id. Given a blocking error prevents submission, When the user presses submit, Then a summary region with role="alert" announces the number of errors and the first error message via screen reader once. Given a tooltip opens, Then it uses role="tooltip" and is programmatically associated to its trigger via aria-controls/aria-expanded, and is reachable by keyboard. Given color is the only differentiator of state, Then an additional non-color indicator (icon/text) is present; error text and icons meet WCAG 2.1 AA contrast.
Server–Client Validation Consistency and Drift Prevention
Given the editor loads, When validation rules are fetched, Then the client stores a ruleset version id and displays it in debug logs; any local validation attaches this version id to results. Given a record is saved, When server validation completes, Then the server returns a ruleset version and a validation hash; If the versions differ from the client, the client reloads rules and revalidates automatically. Given the same 1099‑NEC payload is validated locally and on the server under the same ruleset version, Then the pass/fail results match for all fields (0 discrepancies); any discrepancy is logged with field ids and sent to telemetry.
Pre-Submission Readiness Check
"As a filer about to submit, I want a clear pass/fail review and checklist so that I know my forms will be accepted on the first try."
Description

Add a consolidated readiness review that runs full schema and business-rule validation across the form, producing a pass/fail gate, severity-ordered checklist, and a readiness score. Allow direct navigation to issues and embedded one-tap fixes. Expose an exportable summary for records and include readiness status inside the IRS-ready packet metadata.

Acceptance Criteria
Run Readiness Check and Display Pass/Fail, Score, and Severity-Ordered Checklist
Given a user has filled required 1099-NEC fields for a single payer–recipient form and opens the Pre-Submission Readiness Check When the user taps "Run Readiness Check" Then the system runs schema validation against the configured IRS 1099-NEC e-file schema for the selected tax year and all business rules And displays a gate label of "Pass" if zero Blockers are found, otherwise "Fail" And displays a readiness score from 0–100 calculated per the scoring policy and visible on screen And renders a checklist sorted by severity (Blocker > Warning > Info) with counts by severity And completes the validation and renders results within 5 seconds on a typical mobile connection
One‑Tap Fixes Apply and Re‑Validate Identified Issues
Given a checklist item supports an auto-fix (e.g., amount rounding to whole dollars per IRS, ZIP+4 address format, TIN/TCC length, payer name formatting) When the user taps "Fix" on that item Then the system applies the change instantly and shows a success confirmation And re-validates the affected rules and updates the item's status within 1 second And marks the item as "Resolved" and removes it from Blockers if applicable And logs the fix with rule ID, masked old/new values, user ID, and timestamp And provides an "Undo" option for 15 minutes that reverts the change and re-validates
Issue List Deep‑Links to Exact Form Field/Section
Given a user is viewing the readiness checklist When the user taps any issue row Then the app navigates to the exact form field/section with the issue in the 1099-NEC editor And highlights the field and shows inline guidance/error text referencing the rule ID And the back action returns to the checklist, preserving scroll position and filter state And accessibility labels announce the field, error, and available fix action
Export Readiness Summary (PDF and JSON) with Audit Details
Given a readiness check has been run at least once in the current session When the user selects "Export Summary" Then the app generates both a PDF and a JSON file containing: payer and recipient identifiers (names + masked TINs), tax year, timestamp (UTC ISO 8601), readiness pass/fail, score, rule set version, list of issues with rule IDs, severities, messages, and resolution statuses with timestamps, and app version And the files are named using pattern {payerName}_{recipientName}_1099NEC_Readiness_{YYYYMMDD_HHMMSSZ} And the user can share or save the files via the system share sheet And export starts within 1 second and completes within 3 seconds after generation
Embed Readiness Status and Metadata into IRS‑Ready Packet
Given the readiness gate is Pass for the form When the user generates the IRS‑ready packet Then the packet metadata includes readiness: pass/fail, score, timestamp (UTC ISO 8601), ruleset version hash, checklist digest (SHA-256), and the user ID who ran the check And the metadata is retrievable via the packet details screen and included in the exported packet manifest file And if the user regenerates the packet after changes, the metadata is updated to reflect the latest check
Comprehensive Schema + Business‑Rule Validation and Scoring Logic
Given a 1099‑NEC draft When the readiness check runs Then the system validates against the IRS schema for the configured tax year and internal business rules including: payer TCC and TIN presence/format, recipient TIN/SSN/ITIN format, address formats (US/International), numeric field rounding and ranges, box totals consistency, state codes, and required fields And each finding includes a unique rule ID, severity (Blocker, Warning, Info), human‑readable message, and suggested fix or auto‑fix availability And schema-violating issues are always classified as Blockers And the scoring model weights severities as follows: any Blocker sets score to 0 until resolved; each Warning deducts 5 points from a base of 100 down to a minimum of 50; Info deducts 0 points; the policy is visible via a help link on the results screen
Batch Readiness for Multiple 1099‑NECs with Aggregate Status
Given a user selects a batch of multiple 1099‑NECs (2–25 forms) When the user runs the readiness check Then the system produces per‑form pass/fail, score, and severity counts, and an aggregate summary with totals And allows filtering to show forms with Blockers first and drill‑down to a single form’s checklist with deep‑linking And prevents submission of the batch until all forms have zero Blockers And completes validation for up to 10 forms within 12 seconds and up to 25 forms within 30 seconds on a typical LTE/Wi‑Fi connection
Batch Validation & Bulk Fix
"As a user with many recipients, I want to validate and fix all forms in one go so that I save time and reduce repetitive work."
Description

Enable validation of multiple 1099-NECs at once with a batch summary view showing counts by severity, recipients impacted, and recommended bulk actions. Support applying safe auto-fixes across the batch, background processing with progress indicators, and notifications upon completion. Integrate with imports and annual close-out workflows for high-volume users.

Acceptance Criteria
Batch Validation Initiated from Import Flow
Given a user imports between 10 and 5000 1099-NEC records successfully in a single file When the user selects "Validate All" on the import confirmation screen Then a batch validation job is created within 3 seconds And the UI navigates to the Batch Summary view within 2 seconds And the displayed batch job ID matches the backend job log entry
Batch Summary Shows Severity and Impact
Given a batch validation job is running or complete When the user opens the Batch Summary view Then the view displays: total forms, Critical count, Warning count, Info count, and unique recipients impacted And clicking a severity filter updates the list within 500 ms to show only items of that severity And unique recipients impacted equals the count of distinct (Recipient TIN, Recipient Name) pairs with at least one error And all counts match backend job metrics exactly
Recommended Bulk Actions Generated
Given the batch contains fixable error types (e.g., amount rounding, address normalization, TCC/Payer data fill) When the system proposes bulk actions Then each proposal shows: error type, rule reference, estimated affected forms, safety level (Safe or Needs Review), and a preview of at least 5 sample changes And only actions marked Safe are eligible for bulk apply And Safe actions must achieve >= 99.5% precision in a dry-run validation prior to enabling Apply
Apply All Safe Auto-Fixes
Given at least one Safe bulk action is available When the user clicks "Apply All Safe Fixes" and confirms Then fixes are applied in the background with per-record transactional updates and up to 2 retries on transient failures And throughput is at least 1000 forms per minute for batches up to 5000 forms And an audit log records for each changed record: record ID, fields changed (old -> new), rule ID, timestamp, and actor And no fields outside the targeted changes are modified (field-level diff equals planned changes) And upon completion, automatic re-validation runs and reduces impacted error counts by the number of successful fixes +/- 1%
Background Processing with Progress and Resilience
Given a batch is in Validate, Propose, Apply, or Re-validate phase When the user views the Batch Summary Then a progress indicator shows percent complete, items processed/total, current phase, and ETA And progress updates at least every 2 seconds And the job continues if the user navigates away or logs out, with progress resuming display within 2 seconds upon return And transient failures are retried up to 3 times with exponential backoff And the job is marked Failed only if > 0.5% of items remain unprocessed after retries or a fatal error occurs, with a clear error message and retry option
Completion Notifications with Outcome Summary
Given a batch job completes successfully or fails When completion is reached Then the user receives an in-app notification within 5 seconds and an email/push within 60 seconds (respecting notification preferences) And the notification includes: batch name/ID, total forms, fixes applied, remaining Critical/Warning counts, recipients impacted, and links to export CSV of issues and open the batch And notifications are sent only to users with access to the batch's workspace
Annual Close-Out and High-Volume Integration
Given a user is in the Annual Close-Out workflow with 500 to 10000 forms across multiple imports When they reach the "Validate 1099s" step Then they can trigger batch validation from the wizard And the step displays consolidated counts across all related batches And the step is marked Complete only when Critical = 0 across all batches And if Auto-validate after import is enabled, validation starts within 5 minutes of import completion and links to the current annual cycle And role-based access restricts bulk fixes to Admin or Preparer roles; Viewer cannot trigger bulk fixes
Compliance Audit Trail
"As a cautious filer, I want a transparent history of what was changed and why so that I can defend my filing decisions if questioned."
Description

Record a tamper-evident audit log of validations and fixes, capturing timestamps, user, source (auto vs. manual), rule IDs, and before/after values. Provide diff views, rollback for non-submitted forms, and include a summarized change report in the tax packet. Ensure secure storage, retention by tax year, and export for support or advisor review.

Acceptance Criteria
Auto Validation Event Logged with Mandatory Metadata
Given a 1099-NEC form is evaluated by the validation engine When rule RULE_ID is executed on field(s) FIELD_PATHS Then an audit entry is appended capturing: timestamp (UTC, ISO 8601), formId, ruleId, fieldPaths, outcome (pass|fail), userId=system, source=auto, beforeValue(s), afterValue(s), correlationId And beforeValue(s)=afterValue(s) when the validation does not mutate data And the entry includes prevHash and hash computed over the canonicalized entry payload And the entry is durably written to the audit store with p95 latency <= 200ms And the stored entry can be retrieved by formId and ruleId
Manual One‑Tap Fix Logged with Before/After Values
Given a user applies a one‑tap fix suggested by Reject Shield When the fix mutates one or more fields on a 1099-NEC Then an audit entry is appended capturing: timestamp (UTC), formId, userId, source=manual, ruleId, fieldPaths changed, beforeValue(s), afterValue(s), fixId And afterValue(s) match the current persisted form values And the entry is linked in the hash chain via prevHash and hash And p95 write latency <= 300ms And the entry is retrievable when filtering by userId and formId
Tamper‑Evident Hash Chain Verification and Append‑Only Enforcement
Given a sequence of audit entries exists for a form When integrity verification runs Then the computed hash chain validates end‑to‑end from the first to the most recent entry And altering any stored entry causes verification to fail and reports the first broken link And update or delete operations on committed entries are rejected by the store with an error And verification completes within 2s for up to 1,000 entries
Field‑Level Diff View Between Versions
Given a 1099-NEC has two or more audited versions When the user opens the Diff view and selects any two versions Then the UI displays only changed fields with before and after values, ruleIds, timestamps, and actor (system or user) And additions, deletions, and edits are visually distinguished And the view supports pagination or virtual scrolling for 200+ changed fields And the diff view loads within 2s p95 for up to 1,000 audit entries
Rollback to Prior Version for Non‑Submitted Forms
Given a 1099-NEC has not been submitted to the IRS And the user has selected a prior audited version When the user confirms rollback Then the system restores the form to the selected version’s values And creates a new audit entry with action=rollback, source=manual, beforeSnapshotHash, afterSnapshotHash, and timestamp And all validation rules are automatically re‑run and results logged And rollback is blocked with a clear error if the form status is Submitted or E‑Filed And the rollback operation completes within 3s p95
Summarized Change Report Included in Tax Packet
Given a tax packet is being generated for a filing When the packet build process completes Then the packet includes a Change Summary section containing totals of validations, fixes, rollbacks, and the date/time of last change And the summary lists up to the last 10 changed fields with their final values and last modified timestamps And the Change Summary appears in the packet PDF and in the packet metadata JSON And the counts in the summary reconcile 1:1 with the underlying audit log on verification
Secure Storage, Tax‑Year Retention, and Export for Review
Given audit entries are persisted When storing and retrieving entries Then data is encrypted at rest and in transit (TLS 1.2+) and access is restricted to authorized roles And entries are partitioned and queryable by taxYear and formId And entries remain retrievable for their configured retention period per taxYear And a user with Support or Advisor permission can export a selected taxYear’s audit log for a payer as CSV and JSON including: timestamp, userId, source, ruleId, fieldPaths, before/after values, prevHash, hash And exports up to 50,000 entries are generated within 60s and delivered via a time‑limited download link

Correction Flow

Make corrections without the chaos. Select a filed form, mark it corrected, adjust only the changed fields, and re‑file with a clear audit trail. Recipient copies are reissued automatically, and deadline trackers keep you compliant.

Requirements

Mark-as-Corrected Entry Point
"As a freelancer who needs to correct a filed information return, I want to mark the original as corrected and open a guided correction case so that I can fix errors without refiling from scratch."
Description

Provide a dedicated entry point to select a previously filed information return (e.g., 1099-series) by tax year, payer, and recipient, then mark it as “Corrected” to open a guided correction case. Capture a read-only snapshot of the original filing, create a unique correction case ID, and enforce permissions so only authorized users can initiate. Support search, filters, and mobile-first navigation, and prevent ad-hoc edits to the original after a correction case is created.

Acceptance Criteria
Select Filed Return by Year, Payer, Recipient
Given I am on the Correction Flow entry point with at least one filed information return in the workspace When I apply filters by tax year, payer, recipient, and form type Then the list updates to show only matching filings within 2 seconds for up to 500 records And the search box supports prefix and exact match on recipient name, last-4 TIN, and document ID And I can sort by tax year, payer, recipient, filing date, and status And selecting a single row enables the Mark as Corrected action
Permission-Gated Mark-as-Corrected Action
Given my user role lacks Initiate Correction permission for the selected payer account When I view the filed return list Then I do not see the Mark as Corrected control for any filing in that payer And API attempts to initiate return 403 with error code PERM-INIT-CORR-DENIED Given my user role has Initiate Correction permission When I select an eligible filed return Then the Mark as Corrected control is enabled
Start Correction Case From Selected Filing
Given I have selected a single filed return eligible for correction When I choose Mark as Corrected Then a correction case is created with an ID matching pattern CORR-{YYYY}-{7 alnum} And the case opens to step 1 of the guided flow And the case is linked to the original filing ID and tax year And the original filing’s status changes to Correction In Progress And the Mark as Corrected control is disabled for that filing
Snapshot and Original Filing Immutability
Given a correction case has been created When I view the case details Then a read-only snapshot of the original filing data and source PDFs is displayed and stored with timestamp and SHA-256 checksum And all original filing fields are non-editable across the product UI and API And any attempt to update the original filing via API returns 409 ORIGINAL_LOCKED_WITH_CORRECTION
Duplicate/Parallel Correction Case Prevention
Given a correction case already exists for the original filing and is open When another user attempts to initiate a new correction on the same filing Then the system prevents creation and shows message "A correction case is already open" with a link to the existing case And concurrent initiation requests within 2 seconds result in one case only via idempotency enforcement; subsequent requests return 409 DUPLICATE_CORR_ATTEMPT
Mobile-First Entry Point Usability
Given I access the entry point on a mobile device (viewport width 375–430 px) When I use search, apply filters, and select a filing Then all interactive controls have minimum 44x44 px touch targets and are reachable without horizontal scrolling And the list uses virtual scrolling and loads within 2.5 seconds for 200 records on a typical 4G network And the Mark as Corrected action is accessible from the detail view and list row actions And back navigation preserves search and filter state
Audit Trail and Event Logging on Initiation
Given a correction case is initiated When I view the activity log for the filing or case Then I see an entry with actor, timestamp (UTC), IP/device, case ID, and initiating channel (UI/API) And export of the audit log includes the snapshot checksum and permission scope used And a product analytics event correct_case_initiated is emitted with payer_id, form_type, tax_year, recipient_id, and case_id (pseudonymous) within 2 seconds
Field-Scoped Edit Mode and Diff
"As a user making a correction, I want to edit only the fields that changed and see a clear before/after diff so that I avoid introducing new mistakes and can justify the change."
Description

Enable a correction workspace where only eligible fields are editable, locking all others to the original values. Provide inline validation, required reason codes, and annotations for each changed field. Display before/after diffs, compute impacted totals in real time, autosave drafts, and support accessibility and mobile gestures. Prevent inconsistent changes with form-specific rules and surface helpful tips tied to IRS guidance.

Acceptance Criteria
Field-Scoped Edit Locking
Given a filed form is opened in the Correction workspace When the workspace loads Then only fields marked as editable by the form’s rules are focusable and accept input And all non-eligible fields are read-only with a lock icon and a tooltip explaining the lock reason And attempts to modify locked fields via typing, paste, or API are prevented and not persisted And tab order skips locked fields on desktop and mobile
Inline Validation, Reason Codes, and Annotations
Given an eligible field is changed When the user attempts to move focus away or save Then a reason code selection is required and an annotation text field is required And the annotation enforces a minimum of 10 characters and a maximum of 500 characters And inline validation messages appear within 300ms of the triggering action And the refile action remains disabled until every changed field has a valid reason code and annotation And each change is captured with timestamp, user ID, reason code, and annotation in the draft audit log
Before/After Diff With Real-Time Impacted Totals
Given at least one field has been changed When viewing the diff panel Then the original and updated values are displayed side-by-side per field with accessible highlights And impacted totals and derived amounts recompute in real time within 200ms of keystroke And impacted totals are flagged with an indicator and updated values And performing Undo on a field reverts the diff and recomputed totals for that field
Autosave and Draft Recovery
Given the user is editing in the Correction workspace When the user types or changes a value Then an autosave occurs within 2 seconds after field blur or at least every 15 seconds during active editing And on network failure a non-blocking warning appears and autosave retries up to 5 times with exponential backoff And closing or crashing the app restores the last autosaved draft within 1 second of reopening with a visible “Draft restored” banner including timestamp And a version history retains the last 20 autosave snapshots and allows restore to any snapshot without data loss
Form-Specific Consistency Rules Enforcement
Given the form type has defined cross-field rules When a dependent field is changed (e.g., a box affecting totals) Then all associated rules are re-evaluated within 150ms And violations are surfaced inline and in a rules panel with links to each offending field And prohibited overrides are blocked with explanatory messaging And the refile action is disabled until all violations are resolved
Accessibility and Mobile Gesture Support
Given the user navigates the Correction workspace When using keyboard only Then all controls are reachable with Tab/Shift+Tab, actionable with Enter/Space, and have visible focus indicators And screen readers announce field name, original value, new value, validation state, and reason/annotation requirements with aria-live polite updates for diffs And color contrast for text and indicators is at least 4.5:1 and information is not conveyed by color alone When using touch on a mobile device Then touch targets are at least 44x44 px, common gestures (tap, swipe to dismiss tips) work with non-gesture alternatives, and pinch-to-zoom is supported without layout breakage And input latency for editing is under 100ms on midrange mobile devices
IRS Guidance Tips and Contextual Help
Given focus enters an editable field with available IRS guidance When the field is focused Then a contextual tip appears within 300ms showing a concise summary, citation (form/line/publication), and a link to the full source And the tip is dismissible, does not block input, and is reachable by keyboard and screen reader And if guidance is unavailable, no tip is shown And if the network fails, a cached summary is shown or the tip suppresses gracefully without errors
Automated Recipient Reissue
"As a payer who issued forms, I want corrected recipient copies to be reissued automatically so that my contractors are promptly informed and compliant records are maintained."
Description

Automatically generate and deliver corrected recipient copies labeled as corrected via preferred channels (secure portal, email with secure link, or print-and-mail fulfillment). Update the recipient portal with version history, send notifications, and capture delivery status and read receipts where available. Respect communication preferences and rate limits, and provide resend controls and proof-of-delivery artifacts.

Acceptance Criteria
Email Reissue With Corrected Label And Secure Link
Given a filed recipient form is marked "Corrected" and the recipient’s preferred channel is Email And a corrected PDF is generated for the form/version When the reissue process executes Then the recipient receives exactly one email within 60 seconds containing a single-use, expiring HTTPS link to the corrected copy And the corrected PDF’s first page and file metadata are labeled "Corrected" per IRS convention And the email subject and body clearly state "Corrected" and include the version number (e.g., v2) And no unencrypted PII beyond recipient first name and masked TIN (last 4) appears in the email And a delivery status is recorded; if the client supports it, a read receipt/open event is captured and linked to the version
Recipient Portal Version History Update
Given a filed recipient form is marked "Corrected" and the recipient has Portal access When the corrected copy is published Then the portal displays a version history list showing prior and current versions with timestamps, version numbers, and actor And the latest version is labeled "Corrected" and set as default view And prior versions are read-only and watermarked "Superseded" And an activity log entry is created for "Corrected copy issued" with user, timestamp, channel, and IP And access permissions and sharing settings remain unchanged from the prior version
Print-And-Mail Fulfillment For Corrected Copies
Given the recipient’s preferred channel is Print-and-Mail and a corrected copy is generated When the fulfillment job runs Then the job queues the print package the same business day if initiated before 5:00 PM recipient local time, otherwise next business day And the envelope and cover sheet indicate "Corrected" prominently And the mailing uses the recipient address on file at correction time and validates via USPS CASS And a USPS tracking number is assigned and stored And a proof-of-mailing artifact (PDF manifest + tracking) is saved and linked to the corrected version
Respect Preferences And Rate Limits
Given recipient communication preferences and do-not-contact flags are configured When corrected copies are reissued in batch Then messages are sent only via channels explicitly allowed by the recipient And no messages are sent if all channels are disallowed; the item is flagged for manual follow-up And channel sends do not exceed configured global and per-recipient rate limits And excess sends are queued and executed later without duplication And all rate-limit decisions and queue times are logged
Resend Controls And Proof-Of-Delivery
Given a corrected copy exists and a support user with Resend permission opens its detail page When the user selects a channel and clicks Resend Then only the latest corrected version is delivered via the selected channel And the action is logged with actor, timestamp, channel, and recipient ID And resend count and last sent timestamp are updated on the record And a new proof-of-delivery artifact (ESP event, portal notification ID, or USPS tracking event) is attached And preferences and rate limits are enforced
Delivery Status And Read-Receipt Capture
Given corrected copies are reissued across Email, Portal, and Print-and-Mail channels When delivery events occur Then the system records for Email: sent, delivered, bounce type, and open/read if available And for Portal: notification delivery status and first-view timestamp by recipient And for Print-and-Mail: tracking events through delivery scan And each event is linked to the corrected version ID, recipient ID, channel, and UTC timestamp And a status report/API returns the latest per-channel state within 2 minutes of event receipt
E-File Resubmission and Status Tracking
"As a filer, I want the system to generate and submit the corrected e-file and track its status so that I know when the correction is accepted and what to do if it’s rejected."
Description

Assemble the corrected filing payload per IRS/state schemas, referencing the original submission as required (e.g., corrected indicator, control numbers, sequence IDs). Submit via integrated e-file gateways, handle synchronous and asynchronous responses, and map statuses (queued, transmitted, accepted, rejected, needs attention). Implement smart retries, error code explanations, remediation steps, and fee handling. Store submission receipts and acknowledgments in the correction case.

Acceptance Criteria
Corrected Filing Payload Assembly and Reference Linking
- Given a correction case linked to an original filing, When the user selects “Mark as corrected” and edits only the changed fields, Then the system assembles a payload conforming 100% to the IRS/state schema for the selected form and tax year (schema validation passes with zero errors). - Given an original submission with control/sequence identifiers, When the corrected payload is generated, Then the correctedIndicator is set per schema, original control/sequence IDs are included as required, and a new unique resubmission ID is generated and linked to the original. - Given form-specific correction rules, When constructing the payload, Then only fields permitted to change are altered and all required unchanged references are preserved; disallowed changes cause a blocking validation error identifying the exact offending fields. - Given a prepared corrected payload, When pre-flight validation runs, Then any missing required references (e.g., original document ID, TCC, tax year) produce actionable errors before transmission. - Given a successful pre-flight, When the user clicks “Re-file,” Then the payload snapshot is versioned, checksumed, and attached to the correction case before any transmission occurs.
Synchronous Gateway Response Handling on Resubmission
- Given a valid corrected payload, When it is submitted to the e-file gateway, Then the system records the HTTP status, correlation/transaction ID, and raw response body within 1 second of receipt. - Given a 2xx response with gateway acknowledgement, When mapping the immediate outcome, Then the submission status is set to Queued or Transmitted per gateway semantics, and the correlation ID is stored on the correction case. - Given a 4xx schema/business rule rejection returned synchronously, When mapping the outcome, Then the submission status is set to Rejected, the exact error codes/messages are persisted, and the user sees a banner with next-step guidance. - Given a non-2xx transport error (e.g., 502/503/504), When handling the response, Then the status is set to Queued (Retrying) and smart retry is scheduled per policy without creating duplicate filings.
Asynchronous Acknowledgment Mapping and Status Updates
- Given a submission in Queued/Transmitted, When an asynchronous acknowledgment file or webhook is received, Then the system parses it and updates status to Accepted, Rejected, or Needs Attention within 5 minutes. - Given an Accepted acknowledgment, When updating the case, Then the acknowledgment artifact (raw + normalized), timestamps, and any assigned document locators are stored and downloadable. - Given a Rejected acknowledgment with one or more error codes, When updating the case, Then all codes are mapped to friendly explanations and affected fields are highlighted for remediation. - Given no acknowledgment received, When polling, Then the system polls at 15m, 30m, 60m, 120m intervals (max 24h), and marks the case Needs Attention – Stalled after 24h with guidance to contact support. - Given an acknowledgment that supersedes a prior intermediate status, When applied, Then the most recent status is reflected in UI and history logs show a complete timeline without gaps.
Smart Retries with Idempotency for Transient Failures
- Given a transient transport failure (network timeout, 5xx), When retrying, Then the system performs up to 3 retries with exponential backoff of ~2s, ~4s, ~8s plus 0–500ms jitter. - Given a retry attempt, When sending the request, Then an idempotency key derived from the correction case and payload checksum is included so the gateway will not create duplicate filings. - Given a business-rule rejection (4xx with known codes), When evaluating retry eligibility, Then no automatic retries occur and the case is marked Rejected with remediation guidance. - Given retries are exhausted, When updating status, Then the case transitions to Needs Attention – Retry Exhausted and a support escalation option is presented.
Error Code Explanations and Guided Remediation
- Given a gateway/IRS error code is present (sync or async), When rendering the error, Then a human-readable title, cause, and concrete remediation steps are shown, with links to source docs where available. - Given a rejected submission, When the user clicks Fix & Resubmit, Then the correction form reopens pre-populated with prior values and highlights only fields implicated by the error codes. - Given the internal error mapping catalog, When running against the staging test suite, Then ≥95% of encountered codes have mapped explanations and steps; unmapped codes fall back to raw text with a tracking tag for content completion. - Given a remediation is completed, When re-validating, Then blocking errors must resolve before refile is enabled; warnings can be overridden with a reason captured in the audit log.
Resubmission Fee Calculation and Payment Handling
- Given pricing rules for corrections, When preparing to re-file, Then the estimated e-file fee (including waivers/credits) is calculated and shown before submission. - Given the user confirms re-file, When charging the payment method, Then the fee is authorized and captured successfully before the payload is transmitted; failures block transmission with a clear message. - Given a credit balance exists, When applying charges, Then credits are consumed first and the remaining amount is charged; the invoice reflects both line items. - Given a fee waiver policy applies (e.g., first correction within 24h), When evaluating fees, Then the total due is $0 and the waiver reason is recorded on the transaction. - Given payment is successful, When finalizing, Then a payment receipt is attached to the correction case and included in the audit trail.
Receipts and Acknowledgments Storage in Correction Case
- Given any submission or acknowledgment event, When artifacts are generated (payload snapshot, receipt, ack), Then each artifact is stored with checksum, timestamp, actor, and immutable versioning linked to the correction case. - Given stored artifacts, When accessed by an authorized user, Then receipts and acknowledgments are viewable in-app and downloadable as PDF/JSON without data loss. - Given retention requirements, When evaluating storage policy, Then all artifacts are retained for at least 7 years and deletions are prohibited except via compliance workflows with dual authorization. - Given the audit log, When events occur (submit, retry, ack received, status change, payment), Then a chronological, tamper-evident log entry is recorded with before/after states.
Audit Trail and Version History
"As a user subject to audits, I want a complete, immutable history of all correction actions so that I can demonstrate compliance and accountability."
Description

Record an immutable, timestamped log of all correction actions, including user identity, IP/device, field-level changes, notes, attachments, and system events. Provide version history with diff views, exportable audit reports (PDF/CSV/JSON), and tamper-evident hashing for integrity. Support retention policies, PII redaction in exports, and admin oversight to meet audit and compliance requirements.

Acceptance Criteria
Immutable Log Entry on Correction Mark
Given a filed form exists and a user with edit permissions marks it as Corrected When the user confirms the action Then the system writes one immutable audit entry with fields: event_type='correction_marked', form_id, correction_id, version, user_id, user_role, timestamp (UTC ISO 8601 with offset), source_ip (IPv4/IPv6), device_id or fingerprint, user_agent, optional note, attachment_ids (if any) And the entry includes content_hash and prev_hash linking it into a tamper-evident chain And the entry becomes queryable in the audit trail within 2 seconds of confirmation And any attempt to alter or delete the entry is rejected and logged as event_type='tamper_attempt' with details
Field-Level Change Capture and UI Diff View
Given a corrected form with existing data and audit logging enabled When the user edits one or more fields and saves the correction Then the audit entry records only changed fields with before_value, after_value, field_id, and json_path And PII-designated fields are stored but displayed masked in the UI for non-admin users And attachment changes are captured with file_name, mime_type, byte_size, file_hash (SHA-256), and change_type in {added, removed} And the UI diff view highlights changed fields and renders within 1.5 seconds for up to 500 changed fields And the diff view allows filtering by field name and change type
Version History Listing and Two-Way Comparison
Given a form has multiple correction versions When a user opens Version History Then the system lists versions in chronological order with version_id, author (name/id), timestamp, change_count, and summary note And the user can select any two versions to compare and see field-level and attachment diffs And version snapshots are read-only and cannot be edited or deleted And the latest version is clearly labeled and the default comparison is latest vs previous
Audit Report Export with PII Redaction Controls
Given a user with export permission requests an audit report for a form or date range When the user selects format in {PDF, CSV, JSON} and confirms export Then the export includes audit entries, diffs, and metadata: generated_at (UTC), generator_user_id, record_count, data_hash (SHA-256) And redaction is enabled by default; disabling requires admin role and a reason that is logged And when redaction is enabled, PII fields (e.g., SSN/TIN, bank account, email, phone, street address) are masked with consistent placeholders; attachment contents are excluded while attachment metadata is included And the file is available within 10 seconds for up to 10,000 entries and record_count matches the query result
Tamper-Evident Hash Chain Verification
Given an auditor initiates integrity verification for a form's audit trail When the system recomputes hashes over all ordered entries in scope Then verification returns Pass if all content_hash and prev_hash validations succeed, otherwise Fail with the first mismatched entry_id And the verification outcome is logged as a system event with timestamp, initiator, scope, and result And the UI displays Integrity status as OK or Compromised for the selected scope
Retention Policy Enforcement and Admin Oversight
Given an organization-level retention policy is configured When an audit entry reaches its retention end date Then the system auto-archives or purges per policy and writes a non-PII tombstone including entry_id, purge_timestamp, policy_id, and hash pointer And admins can view and modify retention policies; all changes require confirmation and are logged with before/after values and approver identity And queries and exports exclude purged content by default and include tombstones when the include_tombstones option is selected And PII redaction rules apply to exports regardless of retention status unless an admin explicitly disables redaction with a logged reason
System Event Logging for Reissue and Deadlines
Given the Correction Flow triggers downstream system actions When recipient copies are reissued or deadline trackers are updated due to a correction Then the audit trail records system events with event_type in {'recipient_reissue_sent','deadline_tracker_updated'}, affected_recipient_ids, delivery_channel, status in {queued, sent, failed}, timestamps, and correlation_id And retries and failures record error_code and error_message and link to the initiating correction_id And these system events appear in version history views, are included in exports, and are linked into the hash chain
Deadline and Compliance Assistant
"As a user with filing obligations, I want deadline reminders and compliance checks for corrections so that I don’t miss statutory windows or incur penalties."
Description

Calculate correction deadlines based on form type, jurisdiction, and delivery method, and surface countdowns and risk indicators within the correction case. Send configurable reminders, block or warn on late corrections, and maintain a rules engine that is updateable as regulations change. Provide calendar integrations, timezone awareness, and a compliance checklist to ensure all steps are completed before submission.

Acceptance Criteria
Auto-calculate deadlines by form, jurisdiction, and delivery method
- Given a correction case with form type, jurisdiction, and delivery method set, When the case is created or any of those fields change, Then the system calculates a statutory deadline (date and time) using the active ruleset and stores the deadline, timezone, and ruleset version on the case. - Given deadlines are calculated, When the timezone is applied, Then the jurisdiction’s statutory timezone is used; if unspecified, the jurisdiction’s capital city timezone is used. - Given a valid rules match, When the deadline is displayed, Then the UI shows the deadline and a rule details link that reveals the matched rule name, citation, and ruleset version. - Given no rule match exists, When the user attempts to proceed to submission, Then an error “Deadline unavailable—rules missing” is shown and submission is blocked. - Given inputs change after an initial calculation, When recalculation occurs, Then it completes within 1 second and an audit entry records old and new deadlines and the rule reference.
Display countdown timer and risk indicators in correction case
- Given a case with a stored deadline, When viewing the case, Then a countdown displays days (D-n/D0/D+), hours, and minutes remaining based on the jurisdiction timezone. - Given time thresholds, When time remaining is >7 days, Then a green “On track” badge is shown; When 7 days to 24 hours remain, Then an amber “At risk” badge is shown; When <24 hours remain, Then a red “Urgent” badge is shown; When past deadline, Then a red “Late” badge with D+ value is shown. - Given the case view remains open, When at least 1 minute elapses, Then the countdown updates without page reload. - Given accessibility requirements, When a screen reader is used, Then the countdown and risk badge have descriptive ARIA labels and are announced correctly.
Configurable reminder scheduling and delivery
- Given a case with a deadline, When configuring reminders, Then the user can add offsets (e.g., 14d, 7d, 3d, 1d, 2h, and custom minutes/hours/days before deadline). - Given notification channels, When saving reminder settings, Then Email, Push, and In‑App channels can be selected individually or in combination. - Given quiet hours are set for the user’s timezone, When a reminder would trigger within quiet hours, Then it is deferred to the next allowed time window. - Given a reminder is sent, When delivery succeeds or fails, Then the audit trail records timestamp, channel, template id, and delivery status. - Given the deadline changes, When reminders are rescheduled, Then future reminders shift relative to the new deadline and duplicates are not created.
Late correction warnings and blocking behavior
- Given policy is set to Block late submissions, When current time is after the deadline in the jurisdiction timezone, Then submission is blocked with the message “Late—submission blocked by policy” and an admin override is required to proceed. - Given policy is set to Warn on late, When current time is after the deadline, Then a modal warning displays lateness duration and requires user acknowledgement before proceeding. - Given a late evaluation occurs, When a block or warn action is taken, Then the policy version, evaluation timestamp, and evaluator (system) are recorded in the audit trail. - Given timezones vary, When evaluating lateness, Then current time is compared against the stored deadline normalized to the jurisdiction timezone.
Rules engine update workflow and versioning
- Given a compliance admin edits rules, When saving, Then rules can be saved as Draft, validated for syntax, include citations, and executed against test cases; publishing requires a unique version tag and effective date. - Given a new ruleset version is published with an effective date, When a new case is created on/after that date, Then the new version is applied; existing cases retain their locked version unless manually recalculated by a permitted user. - Given a case calculates a deadline, When rules are applied, Then the case stores ruleset version, matched rule id, and effective date, and these details are immutable in the audit trail. - Given invalid rule logic exists, When publishing is attempted, Then validation fails with line/field-level errors and publish is blocked.
Calendar integration with timezone awareness
- Given the user connects Google Calendar or Outlook, When a deadline is set on a case, Then a calendar event is created with title, deadline time in jurisdiction timezone, converted display for the user’s timezone, and a link back to the case. - Given reminder offsets exist, When the calendar event is created, Then calendar reminders are added matching supported offsets. - Given the deadline changes or the case is closed, When synchronization runs, Then the calendar event is updated or deleted within 2 minutes without creating duplicates. - Given DST transitions, When the deadline crosses a DST change, Then the calendar event time remains correct relative to the jurisdiction’s legal deadline.
Compliance checklist gating submission
- Given a correction case, When opening the submission step, Then a compliance checklist tailored to form type and jurisdiction is displayed. - Given required checklist items, When attempting submission, Then submission is disabled until all required items are completed or explicitly waived with a reason by a user with waive permission. - Given system-detectable items, When those actions complete (e.g., deadline set, recipient copy queued), Then the checklist auto-marks them complete with timestamps. - Given submission occurs, When the case is finalized, Then the completed checklist is exported to PDF and attached to the audit trail. - Given the deadline is past, When viewing the checklist, Then a required “Late filing acknowledgement” item is present and must be completed to proceed.
Downstream Data Reconciliation
"As a user whose reports feed my taxes, I want corrections to propagate to all related summaries and packets so that my books and filings stay consistent."
Description

Propagate approved corrections to linked transactions, categorizations, summaries, and the IRS-ready tax packet while preserving the original view for reference. Flag and re-run affected reports, maintain data lineage, and notify collaborators (e.g., accountants) of changes. Ensure idempotent updates, reconcile totals across modules, and surface any required follow-up actions to keep books and filings consistent.

Acceptance Criteria
Approved Correction Propagates to Linked Records and Tax Packet
Given a filed form with linked transactions, categorizations, summaries, and an IRS-ready tax packet And a user marks the form as corrected and edits specific fields When the correction is approved Then only dependent records impacted by the changed fields are updated And the IRS-ready tax packet is regenerated as a new version And the original filed form and prior packet version remain viewable and locked for edits And all updated records reference the correction version ID and timestamp
Idempotent Update Processing
Given an approved correction has been processed When the same correction event is re-submitted, retried, or the reconciliation job is re-run Then no duplicate records, double-counted totals, or additional notifications are produced And downstream records retain the same version IDs and values as the prior successful run And the system logs an idempotent no-op outcome
Affected Reports Flagging and Auto Re-run
Given scheduled and on-demand reports include data from the corrected scope When a correction is approved Then all affected reports are flagged as out-of-date with a reason referencing the correction And re-run jobs are queued for each affected report And upon completion, report versions are incremented with lineage linking to the correction And users see an Updated badge and can compare before/after totals
Data Lineage and Audit Trail Visibility
Given a correction updates downstream data When a user opens any updated transaction, categorization, summary, report, or packet Then the UI shows a lineage trail including source form, fields changed, old vs new values, approver, version ID, and timestamp And the user can access a diff view and export the audit trail to CSV or PDF And API consumers can retrieve the same lineage via an endpoint using the correction ID
Collaborator Notification with Change Summary
Given collaborators (e.g., invited accountant) have access and notification preferences enabled When a correction is approved and reconciliation completes Then collaborators receive a single consolidated notification with what changed, impacted totals, reissued packet version link, and required follow-ups And notification delivery is recorded with status (sent, bounced, read) and de-duplicated across channels And sensitive fields are redacted according to role permissions
Cross-Module Totals Reconciliation and Alerts
Given module totals (e.g., expense categories, period summaries, tax packet schedules) depend on corrected data When reconciliation runs Then totals across modules are internally consistent within defined rounding rules And any discrepancy is flagged with a blocking alert linking to affected records And a resolution guide is suggested in-app
Follow-up Actions and Compliance Deadlines Surfaced
Given a correction requires additional actions such as amended filing, bank rule update, or recipient copy reissue When reconciliation completes Then the system creates follow-up tasks with due dates tied to relevant deadlines and assigns default owners And tasks appear in a centralized queue with statuses (Open, In Progress, Done) and deep links to the items to resolve And completing tasks updates the audit trail and clears related alerts

Recipient Vault

Deliver recipient copies securely with e‑consent. Payees get a self‑serve portal to download forms, update addresses, and choose e‑delivery; optional print‑and‑mail fulfillment covers holdouts. Read receipts and delivery status cut down on disputes.

Requirements

Secure Recipient Portal Access
"As a payee, I want secure, link-based access to my tax forms so that I can quickly download them on mobile without creating another account."
Description

Provide a mobile-first, secure portal where payees can access their tax forms via time-bound magic links and optional two-factor authentication. Access is scoped to specific payer, tax year, and form types, with encryption at rest and in transit, device and browser compatibility, rate limiting, and brute-force protections. Links are single-use by default with configurable expirations and revocation. The portal integrates with TaxTidy’s document store and permissions layer to serve IRS-ready, annotated packets and supports responsive design for seamless download on phones and tablets.

Acceptance Criteria
Magic Link Lifecycle (Time-Bound, Single-Use, Revocation)
Given a payer configures magic link TTL to 10 minutes When a link is generated for a payee Then the link expires 10 minutes after issuance and subsequent requests return 410 Gone with error_code=LINK_EXPIRED Given a valid unused link is opened by the intended payee within its TTL When the token is redeemed Then the token becomes invalid and any further use returns 410 Gone with error_code=LINK_USED Given a payer revokes a previously issued link When the revoked link is opened Then access is denied with 410 Gone with error_code=LINK_REVOKED and an audit log entry records revocation, requester IP, and timestamp Given a new link is regenerated for the same payee When the payee uses the new link Then the old link remains invalid and the new link grants access
Optional Two-Factor Authentication Challenge
Given the payer has 2FA required for recipient portal When a payee redeems a valid magic link Then the portal prompts for a one-time code delivered via the payer-configured channel and does not show documents until successful verification Given a correct one-time code is entered within 5 minutes of issuance When the code is submitted Then access is granted and the session is marked 2FA-verified for 30 minutes Given incorrect codes are entered 5 times within 10 minutes When the next code attempt is submitted Then the portal enforces a 10-minute lockout for that payee and IP and returns 423 Locked with error_code=2FA_LOCKED Given the payer has 2FA disabled When a payee redeems a valid magic link Then the portal grants access without a 2FA challenge
Scoped Access by Payer, Tax Year, and Form Type
Given a payee is authenticated via a magic link scoped to payer A, tax year 2024, forms [1099-NEC] When the payee views the portal Then only documents matching payer A, tax year 2024, and form type 1099-NEC are listed Given the same payee attempts to access a document belonging to payer B or a different tax year or form type via a direct URL or API When the request is made Then the request is denied with 403 Forbidden and no information is leaked about the existence of the resource Given the payee has no documents for the scoped parameters When the portal loads Then an empty-state message is shown and no other payers or years are discoverable
Encryption In Transit and At Rest Enforcement
Given any HTTP request to the recipient portal domain When the connection is initiated over HTTP Then the client is redirected to HTTPS and the response includes HSTS with max-age >= 15552000, includeSubDomains, and preload Given a TLS connection to the portal When inspecting the TLS handshake Then TLS version is 1.2 or higher and weak ciphers are rejected Given a document is stored and served via the portal When retrieving it from storage Then server-side encryption at rest is enabled (AES-256 or KMS-managed) and the object metadata indicates encryption is applied Given personally identifiable information is processed When application logs are generated Then PII values (SSN, TIN, address, email) are redacted or not logged
Responsive Mobile-First Download Experience
Given a smartphone (iOS Safari 17, 390x844) and an Android device (Chrome 125, 412x915) When the payee opens the portal Then layout is responsive with no horizontal scrolling, tap targets >= 44px, and primary actions visible without zoom Given the payee taps Download on a form packet When the file is served Then Content-Disposition is attachment with a filename in the format {PayerName}_{TaxYear}_{FormType}.pdf and the file opens/downloads successfully on iOS and Android Given common tablet and desktop breakpoints (768px, 1024px, 1440px) When resizing the viewport Then content reflows without overlap or truncation and Lighthouse Performance and Best Practices scores are >= 90 on a 4G throttled profile
Rate Limiting and Brute-Force Defense
Given repeated token verification requests from the same IP or email When more than 10 attempts occur within 1 minute Then subsequent requests receive 429 Too Many Requests with a Retry-After header and attempts are logged Given 2FA code submissions for a single payee When more than 5 incorrect codes are submitted within 10 minutes Then further attempts are blocked for 10 minutes and return 423 Locked Given excessive requests against the link redemption endpoint from a single IP (e.g., >100 in 5 minutes) When the threshold is exceeded Then the IP is temporarily throttled for 15 minutes without affecting other users, and an alert is sent to security monitoring
Document Store Integration and Annotated Packet Delivery
Given a payee accesses the portal with valid scope When listing available documents Then the portal displays IRS-ready annotated packets with correct metadata (payer name, tax year, form type, issue date) Given the payee downloads a packet When the file is served Then the packet content matches the latest version in the document store (by checksum) and the response time is <= 2 seconds p95 under normal load Given a packet is updated in the document store When the payee refreshes the portal within 60 seconds Then the latest version is shown and the previous version is not accessible Given a requested document is missing or the payee lacks permission When the download is attempted Then the portal returns 404 Not Found or 403 Forbidden respectively with a user-safe error message that does not expose internal IDs
E‑Delivery Consent Capture & Audit Trail
"As a payer admin, I want to collect and retain e‑delivery consent with a clear audit trail so that I can deliver forms electronically while meeting IRS requirements."
Description

Implement an IRS-compliant e‑delivery consent flow that presents required disclosures, records explicit consent per recipient with scope (form types and tax year), captures timestamp, IP, and user agent, and stores an immutable audit trail. Support consent revocation with immediate channel switch to paper delivery, versioned disclosure text, and consent renewal each tax year if needed. Expose consent status to delivery logic and the admin dashboard, and provide exportable compliance reports for audits.

Acceptance Criteria
Explicit E‑Delivery Consent Capture (Per Form Type and Tax Year)
Given a recipient without active e‑delivery consent for a specific form type and tax year, When they open the consent flow, Then the system displays IRS‑required disclosures including scope, hardware/software requirements, how to withdraw consent, and paper alternative. And the consent control is unchecked by default and requires an explicit affirmative action to proceed. When the recipient provides consent, Then the system records: recipient ID, scope (form type(s), tax year(s)), timestamp (UTC ISO 8601), IP address, user agent string, disclosure version ID/hash, language/locale, and sets status to Active. Then the recorded consent is available to delivery logic and visible in the admin dashboard within 5 seconds.
Immutable Consent Audit Trail
Given any consent event (granted, renewed, revoked), When it is saved, Then it is appended to an immutable audit log with a unique ID, timestamp, event type, actor, IP, user agent, disclosure version ID/hash, and previous‑record reference/checksum. When an admin attempts to modify or delete an existing audit record, Then the system prevents the action, returns 403/blocked, and logs the attempt. When an audit integrity check is run over the log, Then it reports "OK" for unaltered chains and "FAIL" for any tampering, with the first invalid record ID. When requested via UI or API, Then the system returns the full, ordered audit history for a recipient within 2 seconds for up to 10,000 records.
Consent Revocation and Immediate Channel Switch
Given a recipient with active e‑delivery consent, When they revoke consent via portal or an admin records a revocation, Then the system sets consent status to Revoked with timestamp, IP, user agent, and revocation reason/source. Then all queued or future deliveries within the revoked scope are switched to paper within 60 seconds, and any pending e‑delivery jobs are cancelled with reason "Consent Revoked". Then the recipient receives a confirmation notice of revocation and paper‑delivery fallback is displayed in their portal. Then the admin dashboard reflects status Revoked and the audit trail includes a Revoked event.
Annual Consent Renewal (Policy‑Controlled)
Given organization policy "Annual Consent Renewal" is enabled, When a new tax year begins or forms are prepared for that year, Then recipients with prior‑year consent are prompted to renew consent for the new year before e‑delivery proceeds. When consent is not renewed, Then delivery logic routes forms for that year to paper and records the reason "Consent Not Renewed". When consent is renewed, Then a new consent event is recorded with scope including the new tax year and linked to the current disclosure version. Given policy "Annual Consent Renewal" is disabled, Then prior consents remain valid across years within their defined scope without prompting.
Versioned Disclosure Text and Linking
Given disclosure text is updated, When a recipient enters the consent flow, Then the latest disclosure version is displayed with version ID/date and a stable content hash. When consent is recorded, Then the audit record stores the exact disclosure version ID/hash and a snapshot or reference sufficient to reproduce the text shown at the time. When viewing historical consent in the admin dashboard or export, Then the original disclosure version and content snapshot associated with that consent are retrievable.
Delivery Logic Respects Consent Status and Scope
Given a form is ready to deliver, When delivery logic runs, Then it checks consent status scoped by recipient, form type, and tax year in real time. Then if consent is Active and in scope, the system performs e‑delivery and logs decision "E‑Delivery: Consent Active". Then if consent is missing, revoked, or out of scope, the system routes to print‑and‑mail and logs decision "Paper: No Valid Consent". When consent changes during processing, Then e‑delivery is aborted prior to send and re‑routed to paper with reason "Consent Changed". Concurrent delivery attempts for the same recipient/form/year produce a single consistent outcome with idempotent decision logging.
Compliance Report Export
Given an admin with reporting permission, When they request a compliance export by date range, tax year, and/or recipient filters, Then the system generates a CSV and JSON file including: recipient ID, name/email, consent status, scope, event history (granted/renewed/revoked), timestamps (UTC), IPs, user agents, and disclosure version IDs/hashes. Then the export completes within 60 seconds for up to 100,000 consent records and is available for download with a unique report ID, file checksum, and audit log entry. When retrieving via API, Then the same fields and filters are supported, and access is enforced with authentication/authorization.
Recipient Profile & Address Management
"As a payee, I want to update my contact information so that my tax forms are delivered correctly whether electronically or by mail."
Description

Enable recipients to self-serve updates to mailing address, email, and phone within the portal with validation, change history, and optional payer approval workflows. Synchronize updates to payer contact records via secure APIs and surface deltas to admins. Validate addresses against postal standards and flag undeliverable addresses. Tie delivery channel to current consent status and contact preferences to ensure accurate electronic or paper delivery.

Acceptance Criteria
Recipient edits mailing address with postal validation
Given an authenticated recipient is on the Profile page When they enter a new US mailing address and click Save Then the system validates against postal standards, normalizes to USPS format, and stores structured fields (street, unit, city, state, ZIP+4) within 2 seconds Given the entered address cannot be validated as deliverable When the recipient clicks Save Then the system displays suggested corrections and a warning, allows save only after explicit confirmation, and flags the address status as Undeliverable Given an international address is entered When the recipient clicks Save Then the system validates against applicable postal standards, preserves locale-specific formatting, and flags deliverability status accordingly Given an address is saved When viewing the profile Then the UI displays the deliverability status (Deliverable/Undeliverable/Unknown) and the last validated timestamp
Recipient updates email and phone with verification
Given a recipient updates their primary email When they click Save Then the system sends a verification link to the new email, marks it Unverified, and does not use it for delivery until verified Given the recipient clicks the verification link within 24 hours When the link is opened Then the email is marked Verified and becomes eligible for e‑delivery Given a recipient updates their mobile phone When they click Save Then the system sends a one-time code via SMS, marks the phone Unverified, and requires correct code entry to mark as Verified Given delivery is attempted and the email/phone is Unverified When the delivery channel is evaluated Then the channel does not use the unverified contact method Rule: Only one primary email and one primary phone may be active; updates replace prior primaries and retain them in change history
Change history and audit log availability
Given a recipient changes email, phone, or address When the change is submitted Then the system records an immutable audit entry with before/after values (PII masked where appropriate), actor, timestamp (UTC), IP/device fingerprint, and change source (self‑serve/API) Given a recipient or admin views change history When the history panel is opened Then entries are listed chronologically with pagination, filterable by field and date range, and exportable to CSV Rule: Audit entries are write-once and retained for at least 7 years Rule: Audit entries appear within 1 second of a successful change
Payer approval workflow for contact changes
Given the payer has enabled approval for contact updates When a recipient submits a change Then the change is set to Pending Approval, the prior value remains effective, and payer admins are notified Given a payer admin reviews a pending change When they approve it Then the new value becomes effective immediately, the audit log captures approver, and the recipient is notified of approval Given a payer admin rejects a pending change with a reason When rejection is submitted Then the value remains unchanged, the recipient is notified with the reason, and the pending item is closed Rule: If a new change is submitted while a prior change is Pending Approval, the older pending item is superseded and marked Superseded with a link to the latest submission
Synchronization to payer system via secure API
Given a contact change becomes effective (approved or auto‑effective if no approval required) When synchronization is triggered Then the system sends only changed fields to the payer API over TLS 1.2+ using OAuth 2.0, includes an idempotency key, and receives a 2xx response within 5 seconds for 95% of requests Given the payer API returns a transient error (5xx or 429) When synchronization occurs Then the system retries up to 3 times with exponential backoff and jitter; on final failure, it queues for retry and alerts admins Given synchronization ultimately fails after all retries When viewing admin deltas Then the failed sync is surfaced with error details, next retry ETA, and a manual retry option Rule: Successful sync updates the recipient and admin views with a Synced status and timestamp; all payloads exclude unverified contact methods
Admin delta surfacing and notifications
Given any recipient contact field changes or fails to sync When an admin opens the Recipient Deltas view Then they see a sortable, filterable list with per‑record diffs (before/after), status (Pending Approval/Approved/Rejected/Synced/Failed), and timestamps Given an admin needs an offline review When they export the deltas Then a CSV is generated within 10 seconds including recipient ID, fields changed, status, actor, and links to audit entries Rule: Admins can subscribe to daily email digests summarizing counts and critical failures; enabling/disabling digests updates immediately
Delivery channel selection based on consent and preferences
Rule: If recipient has Active e‑consent AND a Verified email, Then delivery channel = Electronic; else if no Active e‑consent OR email Unverified AND address Deliverable, Then delivery channel = Print‑and‑Mail; else delivery = On Hold with prompts to update contact info Rule: If address is flagged Undeliverable and e‑consent is not active, Then delivery status = On Hold and recipient is prompted to correct address or provide e‑consent Given consent is granted or withdrawn, or contact verification status changes When reevaluating delivery eligibility Then the selected channel updates within 1 minute and an audit entry records the decision and rule path Rule: Recipient contact preferences (email vs SMS notifications) are honored for electronic delivery notifications; opting out of SMS disables SMS notifications without affecting delivery channel
Delivery Orchestration & Notifications
"As a payer admin, I want automated delivery with reliable notifications so that recipients receive their forms without manual intervention."
Description

Create a delivery engine that selects channels per recipient based on consent and preferences, publishes forms to the portal, and sends branded email/SMS notifications with customizable templates. Include retry policies, bounce handling, link expiration management, and rate controls. Support scheduled, bulk, and on-demand sends, and maintain per-recipient delivery timelines. Integrate with notification providers and TaxTidy’s document pipeline to ensure forms are available before alerts are sent.

Acceptance Criteria
Consent-Driven Channel Selection and Fallback
Given a recipient with recorded e-consents (email: true/false, sms: true/false) and a ranked delivery preference And the account has print-and-mail enabled When delivery orchestration runs for that recipient Then the engine selects only consented channels, honoring the recipient’s rank order And if no e-consented channels exist, the item is queued for print-and-mail with status "Pending Mail" And the chosen channel and decision rationale are stored on the delivery record
Portal Publish Gate Before Notifications
Given a finalized form exists in the document pipeline and is marked "Ready" And the form has a portal document ID When the engine prepares notifications Then the form is published to the recipient portal and returns an accessible link within 120 seconds And notifications are not enqueued until portal publish succeeds (HTTP 201) And if publish fails, no notifications are sent and a structured error is logged with correlation ID
Branded Templates and Variable Resolution
Given workspace branding (logo, colors, from-name, reply-to) and a selected email/SMS template with variables And a preview is requested When a send is triggered Then variables resolve from recipient and form context; any unresolved variable blocks the send with a validation error listing missing keys And the email subject/body and SMS body render without template syntax artifacts And the template version and branding snapshot used are recorded on the delivery record
Scheduling Modes with Rate Controls
Given a scheduled send is set for a future timestamp in the account timezone And provider rate limits are configured (emails/min, SMS/sec) When the scheduled time arrives or a bulk or on-demand send is initiated Then messages are enqueued within 60 seconds of the scheduled time or within 30 seconds for on-demand And dispatch respects configured rate limits without exceeding them And any deferrals due to rate caps are recorded with next-at timestamps per channel
Retry, Bounce Handling, and Provider Failover
Given a transient provider error (HTTP 429 or 5xx) occurs during send When retry processing runs Then the engine retries up to 3 times with exponential backoff (1m, 5m, 30m) And on sustained failure, the delivery status is set to "Failed-Transient-Exhausted" And on permanent bounce/invalid/unsubscribe, the status is set to "Failed-Permanent" with no further retries And if the primary provider is degraded, the system fails over to a secondary provider and records provider selection in the delivery record
Secure Link Expiration and Regeneration
Given a notification contains a time-bound portal access link When 30 days have elapsed since issuance Then the link returns 401 Unauthorized and displays an "Expired Link" message And the recipient can request a new link via a one-click "Resend Link" action And upon regeneration, a new token is issued, prior tokens are revoked, and the delivery record is updated with the new expiration timestamp
Per-Recipient Delivery Timeline and Read Receipts
Given a delivery job is initiated for a recipient When lifecycle events occur (queued, sent, delivered, opened/read via open pixel or portal link click, bounced, failed, portal-download) Then each event is captured with UTC timestamp, provider message ID, and metadata And events appear in the per-recipient timeline UI and are exportable as CSV and JSON And 99% of provider webhooks are processed and reflected in the timeline within 2 minutes of receipt And timelines are retained for at least 7 years
Read Receipts & Delivery Status Tracking
"As a payer admin, I want visibility into views and downloads so that I can resolve delivery disputes quickly and confidently."
Description

Track end-to-end delivery states including email/SMS send, delivery, open, portal view, and document download events. Provide a recipient-level timeline and aggregate dashboards, with filters by payer, form type, and tax year. Generate exportable reports and offer webhooks for downstream systems. Respect privacy settings and consent while enabling dispute resolution with verifiable event evidence.

Acceptance Criteria
Track and persist multi-channel delivery events
Given a recipient has valid email and mobile number and a 1099 form is queued for delivery via email and SMS When TaxTidy initiates delivery Then the system records events per channel: send_initiated, provider_accepted, delivered, with provider message IDs And when the recipient opens the email, views the portal, or downloads the document, events email_opened, portal_viewed, and document_downloaded are recorded And each event is persisted within 5 seconds of the provider callback or user action and includes: event_type, channel, occurred_at (UTC ISO 8601), recipient_id, payer_id, form_type, tax_year, correlation_id/message_id And duplicate provider callbacks with the same provider event ID are idempotently ignored And failure states (bounced, undelivered, spam_complaint) are captured with provider error code and description
Recipient timeline displays verifiable event history
Given a payer user with permission "View Delivery Status" opens a recipient's timeline When the timeline loads Then events display in reverse chronological order with channel and event_type indicators and human-readable timestamps in the user's timezone And the latest status pill reflects the highest-progress state reached per channel (e.g., Delivered, Opened, Viewed, Downloaded, Bounced) And expanding an event reveals evidence (provider message ID, error code if any, IP/user-agent if consent allows) And the timeline loads within 2 seconds for up to 200 events and supports pagination for larger histories And all timeline reads are audit-logged with user_id, recipient_id, and timestamp
Dashboard filtering and metrics by payer, form type, tax year
Given the delivery dashboard is open When the user applies filters for payer(s), form type(s), tax year(s), and status Then the recipient table and KPI tiles (Sent, Delivered, Opened, Viewed, Downloaded, Bounced, Undelivered) update to reflect the filtered dataset And KPI counts equal the totals visible in the table under the same filters And filters support multi-select and persist via URL parameters across refresh And the first page of filtered results loads within 3 seconds for datasets up to 50,000 recipients And clearing filters restores the unfiltered baseline view
Export delivery status report
Given a user selects Export on the dashboard with active filters When the export is requested Then a CSV is generated with one row per recipient per form and columns: recipient_id, recipient_name, email_masked, phone_masked, payer_id, form_type, tax_year, latest_status, first_sent_at, delivered_at, first_open_at, first_view_at, first_download_at, bounce_code, last_event_at And the exported row count matches the number of rows in the filtered view at the time of request And all timestamps are UTC ISO 8601 And the file is available within 60 seconds and the download link expires after 24 hours And an audit record is created with user_id, filter_summary, row_count, and export_id
Webhook notifications for delivery events
Given a webhook endpoint with a shared secret is configured and enabled When delivery events occur Then TaxTidy POSTs JSON payloads within 30 seconds containing event_id, event_type, occurred_at (UTC), channel, recipient_id, payer_id, form_type, tax_year, message_id, latest_status And each request includes HMAC-SHA256 signature header X-TaxTidy-Signature over the raw body and an X-Idempotency-Key unique per event_id And on non-2xx responses, retries use exponential backoff up to 10 attempts over 24 hours And duplicate deliveries with the same X-Idempotency-Key are safely deduplicated by consumers And a Replay control allows resending events from the last 30 days to the configured endpoint
Privacy and consent enforcement for tracking
Given a recipient has not provided e-consent for e-delivery When a form is sent Then the system records send and provider delivery/undelivered statuses but does not embed email open tracking pixels or collect IP/user-agent And the timeline and exports indicate "No consent—limited tracking" for open/view/download metrics Given a recipient has provided e-consent When the recipient opens email, views the portal, or downloads documents Then device metadata (IP, user-agent) is captured, stored encrypted, and displayed only to authorized roles And if consent is revoked, new emails are sent without open tracking and previously stored device metadata is redacted within 24 hours while preserving event timestamps and types And when a Do Not Track header is present on portal access, device metadata is not stored even with consent
Generate dispute evidence bundle
Given a compliance user selects "Generate Evidence Bundle" for a recipient and form When the bundle is requested Then a PDF is produced containing the complete event timeline, consent record snapshot, provider delivery logs (IDs and statuses), email headers, webhook delivery log, and latest document hash And the bundle includes a SHA-256 hash of the underlying JSON event log and a generated_at timestamp And the bundle is available within 30 seconds and accessible only to Compliance or Admin roles And the bundle is stored immutably and a retrieval audit log is recorded
Print‑and‑Mail Fulfillment Integration
"As a payer admin, I want optional print‑and‑mail fulfillment so that recipients without e‑consent still receive required forms on time."
Description

Integrate with a certified print-and-mail vendor to produce print-ready packets with cover sheets, perform address validation and standardization, batch jobs, and track mailings. Support fallback to paper for non-consenting or unreachable recipients, capture proof of mailing, and expose status and costs in the admin dashboard. Include SLA monitoring and automatic reissues for returned mail with updated addresses when available.

Acceptance Criteria
Batch Packet Generation and Vendor Submission
Given recipients requiring paper delivery with finalized tax forms for a selected tax year When an admin initiates a print batch Then the system generates one print-ready PDF per recipient on US Letter with a vendor-approved cover sheet containing a 2D barcode encoding job_id and recipient_id And each PDF passes vendor preflight checks and page count validation And the batch is submitted to the vendor API with metadata (mail_class, requested_mail_date, return_address) And a vendor job_id is returned and stored And per-recipient line items are recorded with the vendor job_id and internal batch_id And the batch status is set to "Queued" And any submission error triggers up to 3 retries with exponential backoff and error logging And after retries, failed recipients are marked "Submission Failed" and excluded from the submitted count
Address Validation and Standardization
Given a recipient’s mailing address prior to batch submission When address verification runs Then the address is validated via USPS CASS/NCOA (or vendor equivalent) and standardized to USPS format And deliverability result and DPV code are stored on the recipient record with timestamp and source And addresses rated undeliverable are flagged "Invalid Address" and are excluded from submission by default And admin override requires a reason and is audit-logged And the standardized address is used for the cover sheet and print job
E‑Consent and Reachability Fallback to Paper
Given a recipient without recorded e-consent by the configured cutoff datetime or marked unreachable due to email hard bounce When delivery channels are resolved for the filing run Then the recipient is assigned to print-and-mail delivery And the rationale (no consent or unreachable) is stored on the delivery decision with timestamp And recipients with confirmed e-delivery (portal download recorded) are excluded from print-and-mail And a summary counts paper fallbacks per reason in the batch report
Proof of Mailing Capture and Storage
Given a vendor job transitions to mailed status When the vendor provides proof artifacts Then the system stores proof-of-mailing artifacts (e.g., manifest, IMb/acceptance scans) linked to each recipient item And each artifact has a checksum, upload timestamp, and immutable storage location And admins can view and download proofs from the recipient timeline And the system records the USPS induction date per piece
Admin Dashboard Status and Cost Visibility
Given an admin opens the mailing dashboard for a tax year When a batch is selected Then the dashboard displays per-job and per-recipient statuses (Queued, Printing, Mailed, In-Transit, Delivered, Returned, Submission Failed) And unit and total costs (print, postage, fees) are displayed and reconcile to the vendor API totals within ±1% And filters and CSV export are available for status and date range And statuses and costs refresh automatically at least every 15 minutes or via manual refresh
SLA Monitoring and Alerts
Given a batch has a configured SLA (e.g., Induct by YYYY-MM-DD) When projected or actual vendor timelines exceed the SLA threshold Then the batch is flagged "At Risk" and an in-app and email alert is sent to admins And escalation notifications are sent every 24 hours until resolved And the dashboard shows SLA met/missed for each batch and piece And all alerts are audit-logged with timestamps and recipients
Automatic Reissue for Returned Mail with Updated Address
Given a recipient piece is marked Returned by the vendor with a return reason code And a newer validated address exists on file or is provided via NCOA update When auto-reissue is enabled for the filing run Then the system generates a corrected packet and submits a new print job for that recipient within 24 hours And the reissue links to the original piece and carries a new job_id with a reissue flag And original and reissue costs are recorded and visible to admins And the recipient delivery timeline shows the return and reissue events
Admin Dashboard & Resend Controls
"As a payer admin, I want a unified dashboard with resend and channel controls so that I can manage Recipient Vault deliveries at scale and stay compliant."
Description

Provide a payer-facing dashboard to search and filter recipients, view consent and delivery status, initiate resends, revoke or extend links, switch delivery channels, and perform bulk actions. Include audit logs for administrative actions, export capabilities, and alerts for at-risk deliveries approaching deadlines. Integrate with the delivery engine, consent store, and print‑and‑mail status feeds for a unified operational view.

Acceptance Criteria
Search, Filter, Sort, and Export Recipients
Given I am on the Admin Dashboard recipient list When I search by name, email, or TIN Then results include only recipients whose name, email, or TIN contain the query (case-insensitive, partial match) Given filters for form year, form type, consent status, delivery channel, delivery state, read status, print status, and last activity date When I apply multiple filters Then results satisfy all filters (logical AND) and the active filter count is displayed Given a sorted column selection When I sort by Last Activity or Name or Delivery State Then results are sorted accordingly and sorting is stable across pages Given pagination size of 50 When I navigate pages Then total count, page count, and item positions remain consistent with the applied filters Given I have a filtered result set of 100,000 rows or fewer When I click Export Then a CSV is generated within 60 seconds containing the visible columns plus Recipient ID and Correlation ID; PII columns are masked according to my role Given an export request completes or fails When I view Export History Then status is recorded with timestamp, requester, filter summary, row count, and file link or error
Recipient Detail: Consent, Delivery Timeline, and Read Receipts
Given I open a recipient detail drawer When data loads Then I see current consent status (opted-in/out, method, timestamp, IP), delivery channel, portal link status (active/revoked, expiry), and the last 10 delivery attempts with statuses and timestamps Given read receipts exist When I view the timeline Then message open events display with timestamp and channel Given the system integrates with the delivery engine, consent store, and print-and-mail feeds When data is fetched Then values reflect the latest state within 5 minutes of source updates and include source-of-truth identifiers Given a data fetch fails or is stale beyond 5 minutes When I view the panel Then a non-blocking data-delayed indicator appears and a retry control is available Given role-based access control When a user without PII scope opens the drawer Then sensitive fields are masked while operational statuses remain visible
Resend Delivery Link with Rate Limits and Templates
Given a recipient with e-delivery channel and valid e-consent When I click Resend Then the delivery engine is invoked with the selected template (email or SMS), recipient contact, and a correlation ID, and the dashboard shows a pending state Given resend rate limits of 3 attempts per channel per recipient per 24 hours When limits are exceeded Then the Resend action is disabled and an explanatory tooltip is shown Given a resend succeeds When the delivery engine acknowledges Then the last delivery attempt row updates within 10 seconds and the recipient receives the message Given a resend fails (e.g., hard bounce, SMS blocked) When the engine returns an error Then the attempt is logged with error code, the channel is marked at-risk, and remediation suggestions are displayed Given an admin performs a resend When the action completes Then an immutable audit log entry records actor, timestamp, reason (optional), recipient ID, channel, template ID, and result
Revoke or Extend Portal Link
Given an active portal link When I click Revoke and confirm Then the link is invalid within 5 seconds and subsequent access returns HTTP 410 with a user-friendly message Given an active or expired link When I Extend by N days (1–30) Then the new expiry is set in the consent store and delivery engine tokens are rotated; the detail view reflects the new expiry Given link changes occur When they are saved Then previous token values are invalidated, a notification option is presented, and an audit log entry is created Given the link is revoked When I attempt to resend e-delivery Then the system requires re-issuing a new link or obtaining consent before sending
Switch Delivery Channel Between E‑Delivery and Print‑and‑Mail
Given current channel is e-delivery and consent is missing or revoked When I switch to print-and-mail Then a print job is created with the correct form set and address on file, and the delivery state changes to Print Pending Given current channel is e-delivery with valid consent When I switch to print-and-mail Then I must provide a reason; the system warns about consent override and proceeds only after confirmation Given current channel is print-and-mail and e-consent is valid When I switch to e-delivery Then the e-delivery link is generated and the print job (if not yet printed) is canceled with status updated in the print feed Given a print job is In Progress When I attempt to switch channels Then the action is blocked with guidance to retry after the job completes or is canceled Given any channel switch When it completes Then downstream systems reflect the new channel within 2 minutes and an audit log captures before/after values
Bulk Actions and Job Processing
Given a filtered list or saved segment When I Select All or pick specific recipients Then I can initiate bulk Resend, Revoke Links, Extend Links, or Switch Channel for up to 5,000 recipients per job Given a bulk job starts When processing begins Then a job tracker displays progress, success count, failure count, and ETA, updating at least every 10 seconds Given partial failures occur When the job completes Then failures are downloadable as a CSV with recipient ID, action, error code, and message; successful items are excluded from retries Given idempotency keys per recipient-action When a bulk job is retried Then already successful items are skipped without side effects Given a bulk job is created When it finishes Then an audit log summarizes the job with actor, scope (filters), counts, duration, and job ID
At‑Risk Delivery Alerts and Action Center
Given a regulatory deadline is within 72 hours and a recipient has Not Delivered or Not Read status When the system scans hourly Then the recipient appears in the At-Risk queue with reason and time remaining Given I open the At-Risk queue When items are listed Then I can sort and filter by reason (bounce, SMS blocked, no consent, print delay) and deadline proximity Given I select items in the At-Risk queue When I apply Quick Actions (resend, switch channel, extend link) Then the actions execute via the bulk job framework with progress feedback Given an item is snoozed for 24 hours or dismissed with reason When the queue refreshes Then snoozed items are hidden until the snooze expires and dismissed items are not re-queued unless their state changes Given alerts are generated When duplicates are detected across scans Then the system suppresses duplicates and increments an attempt counter per recipient

Starter Sprint

A 7‑day, step‑by‑step plan that gets you 1099‑ready fast. Each day unlocks a bite‑size task—connect an account, snap a receipt, confirm a category—paired with plain‑language tips. Progress is tracked with a readiness score, and by day seven your categories are tuned and your first quarterly packet is assembled.

Requirements

Daily Task Unlocker
"As a busy freelancer, I want one bite-size task to unlock each day so that I can make steady progress without feeling overwhelmed."
Description

Implements a 7-day, time-zone–aware schedule that reveals one guided task per day from enrollment (connect an account, snap a receipt, confirm a category, etc.). Enforces gating logic (cannot access Day N+1 until Day N core actions are complete or explicitly skipped) with optional fast-track when prerequisites are satisfied early. Persists per-user progress, supports catch-up if a day is missed, and resumes seamlessly across devices. Integrates with the task engine, progress tracker, analytics events, and notification triggers. Provides APIs to query/update unlock state, emits telemetry for completion/falloff, and handles edge cases (date changes, daylight saving, manual clock drift).

Acceptance Criteria
Time‑Zone–Aware 7‑Day Unlock Schedule From Enrollment
Given a user enrolls at 2025-09-08T15:30 in timezone America/Los_Angeles, When checking unlock times, Then Day 1 unlocks immediately, And Day 2 earliest eligibility is 2025-09-09T00:00-07:00, And Day 7 earliest eligibility is 2025-09-15T00:00-07:00. Given a user enrolls in timezone Asia/Kolkata at 2025-09-08T09:10+05:30, When checking unlock times, Then Day 2 earliest eligibility is 2025-09-09T00:00+05:30 local midnight. Given a user changes their profile time zone after enrollment, When evaluating future day eligibility, Then future eligibility timestamps are recalculated to the new time zone’s 00:00 boundary, And no day unlocks earlier than it would have under the original time zone (no net-early unlock).
Day Gating and Catch‑Up Enforcement
Given Day N is incomplete and not skipped, When the user attempts to access Day N+1 via UI or API, Then access is prevented (API 403 gating_violation and UI locked state) until Day N is complete or skipped. Given Day N remains incomplete at the start of Day N+1, When the user completes Day N later that same day, Then Day N+1 unlocks immediately upon completion (catch‑up) without waiting for the next midnight. Given the user explicitly taps “Skip Day N” and confirms, When re-checking access, Then Day N+1 is unlocked immediately, And the skip is recorded with timestamp and reason, And completing Day N later does not re-lock Day N+1. Given Day N unlocks, When the user opens it, Then the guided task is retrieved from the task engine and loads within 2 seconds p95.
Fast‑Track Unlock When Prerequisites Are Already Satisfied
Given the system detects Day N+1 core prerequisites are already satisfied, When the user completes Day N core actions, Then Day N+1 unlocks immediately within the same calendar day (no midnight wait). Given multiple downstream days (N+1..K) have all prerequisites satisfied, When Day N is completed, Then Days N+1..K unlock in order immediately, capped at Day 7, And the readiness score updates per unlock. Given fast‑tracked unlocks occur, When reviewing audit logs, Then each unlock includes reason=fast_track with prerequisite references (IDs/timestamps).
Progress Persistence and Cross‑Device Resume
Given a user completes Day N on Device A while online, When they open the app on Device B, Then Day N completion and Day N+1 unlock state reflect within 5 seconds p95. Given Device A performs Day N actions offline, When connectivity is restored, Then completion events sync exactly once (idempotent) and unlock state reconciles without duplicate unlocks. Given the user logs out and back in on any device, When the app reloads, Then the server is the source of truth for unlock/completion states and matches the last known state.
Unlock State APIs: Query, Update, and Authorization
Given an authenticated user, When calling GET /starter-sprint/state, Then the response includes per-day state in {locked, eligible, unlocked, complete, skipped}, eligibility_at (ISO 8601 with time zone), and last_updated, With HTTP 200 and p95 latency ≤300 ms. Given a valid request to POST /starter-sprint/day/{n}/complete, When Day n is unlocked, Then the API returns 200 with updated sprint state, And idempotency is enforced via Idempotency-Key; identical retries within 24h return the same result. Given an attempt to unlock ahead of gating, When calling POST /starter-sprint/day/{n+1}/unlock without Day n complete/skip, Then the API returns 403 with code=gating_violation and no state change. Given a request with an access token for a different user, When calling any unlock state endpoint, Then the API returns 401/403 and does not disclose state or metadata.
Telemetry and Notification Events for Unlocks and Completions
Given enrollment, unlock, completion, and skip events occur, When events are emitted, Then analytics payloads include {user_id, sprint_id, day, action, timestamp, trigger|reason} with no PII in free-text, And ≥99% are delivered within 60 seconds p95. Given a day becomes eligible at local 00:00 but is gated, When eligibility is met later by completion or skip, Then an unlock event records trigger in {catch_up, fast_track} accordingly, And the readiness score update event is emitted. Given notifications are enabled and quiet hours 21:00–07:00 are set, When a day becomes eligible and ungated, Then a single push notification is scheduled for 09:00 local (or sent immediately if after 09:00), And duplicates for the same day are suppressed.
Edge Cases: Date Changes, DST Transitions, and Manual Clock Drift
Given a DST spring‑forward transition (e.g., America/Los_Angeles 2025-03-09), When eligibility crosses the missing hour, Then day unlock uses the calendar 00:00 local boundary and is not delayed by the lost hour. Given a DST fall‑back with repeated hour, When eligibility is 00:00 local, Then only one unlock occurs; duplicate unlock attempts are ignored and audit logs show a single transition. Given the device clock differs from server time by more than 5 minutes, When determining eligibility and recording completion, Then server time is authoritative; manual clock changes do not advance unlock eligibility.
Readiness Score Engine
"As a solo consultant, I want a clear readiness score so that I know exactly how close I am to being 1099-ready and what to do next."
Description

Calculates and surfaces a real-time 0–100 readiness score reflecting 1099 preparedness based on weighted inputs: connected accounts, % of transactions categorized, number of receipts captured and matched, income coverage, unresolved exceptions, and packet completeness. Exposes a scoring API and event-driven updates to the UI, with thresholds for color/status labels (e.g., Not Ready, On Track, Ready). Supports configurable weights, auditability of component contributions, and A/B test variants. Integrates with data pipelines (bank feeds, OCR, categorizer), the dashboard, and Starter Sprint progress views. Caches efficiently, recalculates incrementally on data change, and guards against double-counting and duplicates.

Acceptance Criteria
Realtime Incremental Score Update on Data Change
Given a user has an existing readiness score snapshot and a single transaction is newly categorized When the engine receives a transaction.categorized event with user_id and idempotency_key Then it recalculates only the affected components and updates the total score within 2 seconds Given a new receipt is matched to an existing transaction When a receipt.matched event arrives Then the receipts and matched metrics update and a score.updated event is published with new_score, delta, components, and correlation_id Given the dashboard and Starter Sprint views are subscribed to score.updated for the user When the event is published Then both views display the new score and status within 3 seconds without page refresh
Weighted Component Calculation and Audit Breakdown
Given a weights configuration whose weights sum to 1.0 When a score is computed Then each component value v_i is normalized to [0,1] and total_score equals round(100 * sum(w_i * v_i)) Given a request to GET /score When it returns Then the payload includes per-component raw_metrics, normalized v_i, weighted_contribution, total_score, status_label, color_code, config_version, calculated_at Given a historical score_id When GET /score/{score_id}/audit is called Then it returns the exact inputs and weights used and recomputation yields the same total_score
Threshold Mapping to Status and Color Labels
Given default thresholds Not Ready: <40, On Track: 40–74, Ready: >=75 When a score is mapped Then boundary values 40 and 75 yield On Track and Ready respectively Given a configured custom threshold set is activated When new scores are computed Then status_label and color_code reflect the active thresholds for all responses and events Given a GET /score response When consumed by clients Then it includes status_label in {"not_ready","on_track","ready"} and a hex color_code consistent with the active thresholds
Duplicate Source Events and Records Do Not Double‑Count
Given two bank transactions with the same external_id and fingerprint When both are ingested Then they count as one toward % categorized and income coverage metrics Given a receipt photo is uploaded twice producing identical OCR hashes When matching runs Then at most one match contributes to receipts_captured and matched counts Given the same domain event is re-delivered with the same idempotency_key When processed Then no change to component metrics or total score occurs and no additional score.updated event is emitted
Scoring API Performance and Caching
Given no data changes since the last calculation When GET /score is called with If-None-Match matching the current ETag Then the response is HTTP 304 Not Modified Given a cached score exists and the client does not send If-None-Match When GET /score is called Then P95 latency is <= 300 ms and the payload includes cache=true and an ETag header Given a component metric changes When the change event is processed Then cache is invalidated within 100 ms and the next GET /score returns the recomputed score with cache=false and P95 latency <= 500 ms
Starter Sprint Progress View Integration
Given a user completes a daily Sprint task that affects a score component (e.g., connects an account, categorizes a transaction, matches a receipt) When the action is successful Then the Sprint progress view updates the readiness score and status within 3 seconds to match the dashboard Given Day 7 packet assembly completes When packet completeness reaches 100% Then the packet component contribution v_packet=1.0 is reflected in the score and the status is re-evaluated accordingly Given the Sprint view shows a progress ring When the score crosses a threshold Then the ring’s label and color update to the mapped status in the same frame as the score change
A/B Variant Support and Cohort Consistency
Given active A/B variants with distinct weight configurations When a user is assigned to a variant Then all score computations and events for that user include variant_id and use that variant’s weights Given the same user session across devices When scores are computed within the experiment window Then the variant assignment remains stable for at least 30 days or until the experiment ends Given two users in different variants with identical raw metrics When scores are computed Then their total scores differ only by the weight configurations applied
Account Connection Wizard
"As a freelancer starting the sprint, I want to connect my accounts quickly and securely so that my transactions and invoices flow in automatically."
Description

Provides a guided, mobile-first flow to connect bank feeds and invoicing platforms using secure OAuth/aggregators (e.g., Plaid/Finicity) with granular account selection and permission scopes. Includes credentialless token storage, encryption at rest/in transit, and consent screens describing data usage. Supports fallback CSV import and document vault linking if a provider is unavailable. Features robust error handling, retry, and status polling; verifies connectivity via test fetch; and surfaces clear success/failure states. Emits events to trigger readiness score updates and daily task completion. Localizes copy, tracks funnel analytics, and adheres to compliance and security best practices.

Acceptance Criteria
Mobile OAuth Bank Connection with Granular Account Selection
Given a signed-in user on a mobile device When they launch the wizard and choose "Connect bank" Then they are redirected to a supported aggregator OAuth (Plaid/Finicity) within 2 seconds and the redirect includes app name and requested read-only scopes. Given the aggregator returns available accounts When the account selection screen loads Then the user can select one or more accounts and only the selected account IDs are authorized for access; unselected accounts are not accessible by the app. Given the user completes the OAuth flow successfully When redirected back to TaxTidy Then a confirmation screen displays the provider name and the count of linked accounts, and a test fetch begins automatically. Given an account was previously connected When the user attempts to connect the same account again Then the wizard prevents duplicate linkage and offers a refresh option instead.
OAuth Invoicing Platform Connection with Minimal Permission Scopes and Explicit Consent
Given a user chooses "Connect invoicing" When they are redirected to the provider's OAuth screen Then the requested scopes are the minimal set required (read-only invoices, clients, payments) and the consent screen lists data usage in plain language with bullet points under 120 characters each. Given consent is granted When the OAuth callback is received Then a consent record is stored containing timestamp, user ID, provider, scopes, IP address, and locale, and it can be retrieved for audit. Given consent is denied or the flow is aborted When the callback contains an error or the user closes the window Then the wizard displays an informative message and offers retry or CSV/document vault fallback without losing progress.
Security and Compliance: Credentialless Tokens and Encryption
Given any provider connection is established When tokens are stored Then only access/refresh tokens (no user credentials) are persisted and are encrypted at rest using AES-256-GCM with keys managed by KMS and rotated at least every 90 days. Given data is in transit When any network call occurs Then TLS 1.2+ is enforced and HSTS is enabled on web endpoints. Given consent is granted When an audit review is performed Then an immutable audit log entry exists containing user ID, provider, scopes, purpose-of-use, consent text version, and expiry (if applicable). Given engineers review system logs When token values would be output Then tokens are fully redacted in application logs and monitoring dashboards.
Fallback Data Onboarding via CSV Upload and Document Vault Linking
Given a provider is unavailable or the user opts out of OAuth When the user selects CSV upload Then the wizard accepts .csv files up to 20 MB, validates required columns (date, amount, description, type), and shows a preview with detected mappings. Given the preview is confirmed When the import runs Then at least 95% of rows with valid schema are imported, duplicates (by date+amount+merchant) are skipped, and a summary of imported/skipped/error counts is displayed. Given the user links a document vault (e.g., cloud drive) When authorization completes Then the selected folder path is stored and a background scan indexes supported file types (PDF, JPG, PNG) and extracts receipt metadata (date, total, merchant) with a progress indicator. Given the import or vault scan fails When errors occur Then the user sees actionable error details and can retry or download a template CSV, and failures are logged with a correlation ID.
Connectivity Verification, Retry, and Status Polling
Given a provider connection is established When a test fetch runs Then at least one recent transaction or invoice fetch completes within 30 seconds or a timeout error is surfaced with retry options. Given the provider returns a pending status When status polling occurs Then the UI shows a non-blocking spinner with estimated wait and the system retries with exponential backoff (max 5 attempts) before failing gracefully. Given a transient error (HTTP 429/5xx) is received When retries are exhausted Then the wizard shows a provider-specific message and enables a "Try again" call-to-action after 60 seconds; no duplicate requests are in flight.
Clear Success and Failure States with Actionable Guidance
Given a successful connection or import When completion occurs Then the wizard displays a success state with provider logo, accounts/items linked count, and timestamp, and marks the day’s task as complete. Given a failure occurs When the user views error details Then they see a concise message, error code, next steps, and options: retry, switch provider, or use fallback; the UI preserves entered data. Given the user navigates back or closes mid-flow When they return to the wizard Then progress is restored to the last completed step without duplicating connections or imports.
Events, Analytics, and Localization
Given key milestones (connect_start, oauth_redirect, connect_success, connect_fail, csv_import_success, test_fetch_success) When they occur Then a single idempotent analytics event is emitted with hashed user ID, provider, step, duration, and result; duplicate emissions within 60 seconds are deduplicated. Given a successful connection or import When events emit Then a readiness score update event and a daily task completion event are published to the event bus within 2 seconds and acknowledged by the consumer. Given the device locale is supported (en, es) When the wizard renders Then all copy is localized via the i18n framework; if unsupported, fallback to English; no hard-coded strings are present in the UI.
Quick Categorization Coach
"As a creative freelancer, I want quick suggestions I can confirm or fix so that my expenses are categorized accurately without spending hours."
Description

Delivers a guided, bite-size categorization flow that presents recent uncategorized transactions one at a time with auto-suggested IRS Schedule C categories and plain-language explanations. Supports confirm/edit, swipe/keyboard shortcuts, bulk accept of confident suggestions, and undo. Captures user feedback as training signals for the categorization model and maintains a rules layer for merchant/category overrides. Ensures accessibility and mobile ergonomics, persists session progress, and ties completion to the day’s task. Integrates with the transaction store, ML inference service, readiness score engine, and audit trail. Provides metrics on accuracy, speed, and user corrections.

Acceptance Criteria
Mobile One-at-a-Time Categorization Flow
Given I have at least one uncategorized transaction in the last 30 days When I open the Quick Categorization Coach on a mobile device Then the first transaction is presented full-screen with a single auto-suggested Schedule C category, confidence score, and a one-sentence plain-language explanation And the UI allows Confirm and Edit actions, plus swipe-right = Confirm and swipe-left = Edit, and keyboard shortcuts (Enter=Confirm, E=Edit) when a keyboard is present And Confirm immediately saves the category to the transaction store and advances to the next item within 300 ms And Edit opens a searchable category list with top 5 suggestions prefilled and saving returns to the flow within 500 ms And each Confirm or Edit emits a training signal (action, old_category, new_category, confidence, merchant, amount, timestamp) and a metrics event (categorize_action with duration_ms) And the flow displays remaining count and updates it after each action
ML Suggestion and Fallback Behavior
Given the ML inference service is available When a transaction is loaded in the coach Then the app requests suggestions once and displays the top suggestion with confidence and an explanation within 800 ms on average And if confidence < 0.50, the suggestion is visually marked as Low Confidence and Edit is the primary action Given the ML inference service is unavailable, slow (>2 s), or returns an error When a transaction is loaded Then the coach shows a No Suggestion state with an empty category preselected, allows manual selection, and logs a fallback event without blocking progression And manual selections in fallback still emit training signals and metrics
Bulk Accept of High-Confidence Suggestions
Given there are ≥ 10 uncategorized transactions with model confidence ≥ 0.85 When I tap Bulk Accept Then I see a preview list with count, total amount, and the category to be applied per item, with the ability to deselect individual rows And confirming applies the suggested categories to all selected items, writes to the transaction store, and updates remaining count accordingly And the operation completes within 2 seconds for up to 300 transactions And each applied item emits a batched training signal marked as auto_accept=true and a single bulk_accept metrics event with item_count and duration_ms And an Undo Bulk Accept option appears for 60 seconds
Undo/Redo and Audit Trail Recording
Given I have performed one or more categorization actions in the coach (including bulk accept) When I tap Undo Then the last action is reverted in the transaction store within 500 ms and the UI returns to the reverted transaction if applicable And I can tap Redo to reapply the action And the system supports undo/redo depth of at least 20 actions per user session And every action and reversal writes an audit trail record with user_id, transaction_id, before_category, after_category, action_source (ml|user|rule|bulk), timestamp, and correlation_id And audit records are queryable by transaction_id within 1 second
Merchant Rule Overrides and Precedence
Given I create a rule mapping merchant = "Starbucks" to category = "Meals" When new or existing uncategorized Starbucks transactions are processed in the coach Then the rule is applied before ML suggestions and the category defaults to Meals with a Rule Applied indicator And I can enable backfill to apply the rule to up to 1,000 past uncategorized transactions, completing within 5 seconds And rule precedence is: user rule > user manual edit > model suggestion; this precedence is reflected in the audit trail And I can edit, disable, or delete the rule, with changes taking effect on the next transaction load And wildcard merchant patterns (e.g., "UBER*"), case-insensitive matching, and EIN-based matching are supported
Progress Persistence and Starter Sprint Integration
Given I have categorized some transactions and leave the coach When I return later (same device or another) Then the session resumes at the next uncategorized transaction and reflects the correct remaining count And the day’s Starter Sprint task is marked complete when I categorize at least 20 transactions or 90% of today’s uncategorized set (whichever is lower) And upon task completion, the readiness score engine is updated within 2 seconds and the UI shows a completion toast with the new score delta And progress and task completion status persist across app restarts and are consistent across devices
Accessibility and Mobile Ergonomics Compliance
Given I use a screen reader When I navigate the coach Then all actionable elements have descriptive labels, focus order is logical, and announcements include suggestion, confidence, and explanation And the coach supports full keyboard navigation (tab order, visible focus, shortcuts) and meets WCAG 2.1 AA color contrast And primary touch targets are at least 44x44 px with 8 px spacing; swipe gestures have a discoverable affordance and haptic feedback on confirm And orientation changes (portrait/landscape) preserve state without layout shift that hides critical controls
Receipt Quick Capture
"As a freelancer on the go, I want to snap a receipt and have it auto-matched so that I don’t lose deductions or spend time filing paperwork."
Description

Enables fast, reliable receipt capture with camera-based auto-detect, crop, de-skew, and enhancement, plus file upload for emailed receipts. Performs on-device/offline capture with a sync queue, then server-side OCR to extract date, total, merchant, and tax fields. Matches receipts to transactions by amount/date/merchant with confidence scoring and flags conflicts or duplicates. Allows manual attach/detach, adds notes, and stores originals in the document vault with immutable references for audit. Emits events to update readiness and daily task completion. Ensures PII redaction where needed and enforces secure storage and retention policies.

Acceptance Criteria
Receipt Capture via Camera or Upload (Offline-Compatible)
Given the device is offline, When the user takes a photo of a paper receipt on a contrasting surface, Then the app auto-detects edges within 1 second, auto-crops, de-skews, applies enhancement, and saves the result to a local sync queue with status "Pending Upload". Given low-light or an angle up to 25°, When enhancement completes, Then the final image has a minimum longest edge of 1600 px, file size ≤ 2 MB, and passes an OCR precheck with average character confidence ≥ 0.70. Given the user selects an emailed receipt (JPEG/PNG/HEIC/PDF ≤ 10 MB), When upload is confirmed, Then a preview is shown and the file is added to the sync queue. Given network connectivity becomes available, When a queued item exists, Then upload starts within 10 seconds, retries with exponential backoff up to 5 times on transient errors, and marks the item "Uploaded" upon success.
Server-Side OCR Key Field Extraction
Given an uploaded receipt image or PDF, When OCR processing completes, Then date, total, merchant, and tax fields are extracted and normalized (ISO date, currency+amount, merchant string, tax amount or null). Then accuracy thresholds hold on a benchmark set: total parsed within ±$0.01 on ≥ 98% of valid receipts; date parsed correctly on ≥ 95%; merchant name similarity ≥ 0.80 to known merchant lists when applicable; tax amount recalled on ≥ 90% of receipts that print tax. Then multi-currency symbols are recognized and currency code is set when a symbol or ISO code is present; otherwise default to the account currency. Then the 95th percentile OCR completion time per receipt is ≤ 30 seconds with a 60-second hard timeout that sets status to "Needs Review" without data loss.
Auto-Match Receipts to Transactions with Confidence Scoring
Given extracted amount, date, and merchant and a connected bank feed, When matching runs, Then a confidence score in [0,1] is computed using signals: amount exact or within min($0.50, 1%) (weight 0.5); date within ±3 days (weight 0.3); merchant similarity ≥ 0.70 (weight 0.2). Then if score ≥ 0.80, the top candidate is auto-attached; if 0.60 ≤ score < 0.80, a suggested candidate is presented for review; if score < 0.60, the receipt remains "Unmatched". Then matching completes within 10 seconds of OCR completion for ≥ 95% of receipts. Then all scoring inputs and the selected transaction ID (if any) are recorded in the audit log.
Duplicate and Conflict Detection
Given a new receipt is captured or uploaded, When compared to existing receipts, Then it is flagged as a duplicate when perceptual hash distance ≤ 5 or OCR text similarity ≥ 0.95 and total/date match, preventing auto-attach and prompting user action. Given a receipt is already attached to a transaction, When another receipt attempts to attach to the same transaction, Then a conflict is raised and the second attach is blocked unless the user explicitly overrides with a reason. Then duplicate merge and discard actions are available, and the chosen action updates status and writes an audit entry with timestamp and user ID.
Manual Attach/Detach and Notes with Audit Trail
Given a receipt in "Unmatched" state, When the user searches by merchant/amount/date and selects a transaction, Then the receipt is attached, status changes to "Attached", and confidence is set to "Manual". Given an "Attached" receipt, When the user detaches it, Then the link is removed, status returns to "Unmatched", and an audit entry records who performed the action, when, and the provided reason. Given a receipt, When the user adds or edits a note up to 500 characters, Then the note is saved, versioned, and visible on both the receipt and the attached transaction views.
Secure Storage, Immutable References, PII Redaction, and Retention
Given a receipt is stored, Then the original binary is saved in the document vault with a content-addressed ID (SHA-256), WORM retention of 7 years, and metadata including checksum, MIME type, size, and UTC creation time. Then all receipt binaries are encrypted at rest with AES-256 and transmitted over TLS 1.2+; access requires an authenticated user with receipts:read scope. Given rendered or exported views, When PAN-like patterns (16-digit 4-4-4-4) or email addresses appear, Then digits are masked to last 4 and emails are masked as f***@d***.tld; originals remain unaltered; redaction is configurable in export settings. Given a retrieval by ID, When the file is fetched, Then the returned checksum matches stored checksum; any integrity mismatch is logged and blocks download.
Event Emission for Readiness and Daily Task Completion
Given receipt capture and processing milestones, When actions occur, Then events are emitted to the internal bus: receipt.captured, receipt.uploaded, receipt.ocr.completed, receipt.matched, receipt.duplicate, receipt.attached, receipt.detached; payload includes user_id, receipt_id, transaction_id (nullable), timestamp (ISO 8601 UTC), source, and readiness_delta. Then ≥ 99% of events publish within 10 seconds of the triggering action; offline-captured events are queued and delivered on reconnect in FIFO order per receipt_id. Given the Starter Sprint flow, When the first successful attachment occurs during the Day 2 task, Then the daily task is marked complete and readiness score increases via readiness.updated with the configured delta.
Plain-Language Tips CMS
"As a first-time user, I want simple, plain-language tips for each step so that I understand what to do and why it matters."
Description

Provides a lightweight content management system for Starter Sprint tips and microcopy, allowing product and compliance teams to author, version, and schedule day-specific guidance without code changes. Supports variants by user type, locale, and platform; basic formatting; inline links to help; and image snippets. Integrates with the daily task UI to render contextual tips, with analytics hooks for impression/click-through and outcome attribution. Includes review/publish workflows, rollback, and content gating aligned with the unlock schedule. Ensures performance via caching and safeguards against broken or outdated tips.

Acceptance Criteria
Day-Specific Tip Authoring, Scheduling, and Gating
Given a draft tip targeted to Day 3, When it is scheduled for a future timestamp and approved and published, Then it is not returned by the tips API before the Day 3 unlock and before the scheduled timestamp. Given Day 3 is unlocked and the scheduled timestamp is reached, When the app requests tips for Day 3, Then the published tip payload is returned with content_id, version, schedule_start, schedule_end, and gated=false. Given the tip has schedule_end in the past, When the app requests tips, Then the tip is not returned. Given a new draft version exists, When it is not published, Then production serves the last published version and the API includes version metadata. Given a tip is unpublished, When the app requests tips, Then a default fallback tip is returned and the API responds HTTP 200 with fallback=true.
Variant Targeting by User Type, Locale, and Platform
Given variants exist for user_type (creative, consultant), locale (en-US, es-US), and platform (iOS, Android, Web), When a user with user_type=creative, locale=es-US, platform=iOS requests Day 1, Then the creative+es-US+iOS variant is returned. Given no exact platform variant exists but a user_type+locale match exists, When fetching the tip, Then the system returns the closest match and sets variant_fallback=platform in the payload. Given no variant matches any attribute, When fetching the tip, Then the default variant is returned and variant_fallback=all is set. Given a variant is returned, When analytics events are emitted, Then the same variant_id appears in impression and click events for that session.
Rendering and Formatting Safety
Given a tip body using allowed markdown (bold, italic, list), an inline help link, and one image snippet, When rendered in the daily task UI, Then formatting matches the design spec and links open in the in-app webview. Given an image 404s or exceeds 500 KB, When rendering the tip, Then the image is replaced with its alt text and a placeholder, and cumulative layout shift does not exceed 0.1 (or 100 px equivalent for native). Given a tip contains disallowed HTML or any script, When saving in the CMS, Then validation fails with a clear error and the content cannot be published. Given a link is external, When it is tapped, Then a click-through analytics event is emitted and the webview enforces no JavaScript injection and strips disallowed tracking parameters.
Review, Approval, Publish, and Rollback Workflow
Given roles product_editor and compliance_reviewer, When a tip is submitted for review, Then at least one approval from each role is required before the Publish action is enabled. Given a tip is published, When the audit log is viewed, Then it shows approvers, publisher, timestamps, version, and a diff summary of changes. Given a rollback to version N-1 is initiated, When confirmed, Then version N-1 is served by the API within 60 seconds and the audit log records the rollback. Given a user lacks publish permission, When attempting to publish, Then the action is blocked and a permission_denied entry is logged.
Caching, Invalidation, and Performance
Given CDN caching is enabled with TTL aligned to the next unlock time, When a tip is fetched, Then P95 tips API latency is ≤200 ms on cache hits and ≤500 ms on cache misses under 100 RPS. Given a tip is published or rolled back, When cache invalidation runs, Then all edge nodes serve the new version within 60 seconds and the ETag changes. Given the client sends If-None-Match with a current ETag, When the tip content is unchanged, Then the API returns HTTP 304 with no body. Given prefetching is enabled, When the user completes Day N, Then Day N+1 tips are prefetched and stored locally with expiry at the Day N+1 unlock time.
Analytics Hooks and Outcome Attribution
Given a tip impression occurs, When the tip is visible on screen for ≥1 second, Then a single impression event per session is recorded with fields {content_id, variant_id, day, user_id_hash, locale, platform, timestamp}. Given a link in a tip is tapped, When the webview opens, Then a click event with {content_id, variant_id, link_url, link_position} is sent and deduplicated within 30 seconds. Given the user completes the associated daily task within 24 hours of an impression, When analytics is processed, Then an outcome_attribution event links the completion to the most recent impression via attribution_id. Given transient network errors, When analytics transmission fails, Then retries with exponential backoff (up to 3 attempts) achieve ≥95% delivery within 5 minutes; remaining failures are queued for next app launch.
Content Integrity and Freshness Safeguards
Given a tip is submitted for publish, When automated checks run, Then publish is blocked if any link returns HTTP 4xx/5xx, any image lacks alt text, compliance_review_date is older than 180 days, or deprecated categories are referenced. Given monitoring detects repeated 404s for a published image or link, When ≥5 errors occur within 10 minutes, Then the tip is automatically suppressed, a fallback tip is served, and an alert is sent to the on-call channel. Given a compliance expiry is 14 days away, When nightly checks run, Then the tip is flagged as expiring and a review task is created for content owners while the tip remains publishable.
Quarterly Packet Auto-Assembly
"As a 1099 worker, I want my first quarterly packet assembled automatically so that I can file or send it to my accountant without extra work."
Description

Automatically compiles an IRS-ready, annotated tax packet when the sprint is completed or readiness crosses a threshold. Aggregates categorized expenses, income summaries, receipt images, and notes; embeds cross-references between transactions and receipts; and includes a change log/audit trail. Provides preview, PDF and CSV export, secure share links with expiry, and storage in the document vault. Supports re-generation as data changes, with versioning and draft/watermark states. Integrates with the readiness engine, categorizer, receipt store, and export service, and validates completeness/consistency before marking the sprint as done.

Acceptance Criteria
Auto-Assembly Trigger on Sprint Completion or Readiness Threshold
Given a user has completed all 7 Starter Sprint tasks or the readiness score meets/exceeds the configured threshold, When the condition is met, Then the system generates a new Quarterly Tax Packet in Draft state within 60 seconds and associates it to the correct tax quarter. Given auto-assembly begins, When required prerequisites are missing (e.g., no connected income or expense source), Then assembly is aborted, no packet is created, and the user sees a blocking validation message listing missing prerequisites.
Validation Gate and Sprint Done State
Given a Draft packet exists, When validation runs, Then the system verifies: totals reconcile across sources, all transactions included have a category, all receipt links resolve, and required sections (income summary, expense by category, notes, change log) are present. Given validation passes, When the readiness engine finalizes the packet, Then the Sprint status updates to Done and the packet state becomes Final; else the Sprint remains In Progress and an actionable blocking list is shown.
Packet Contents and Cross-References
Given the packet is generated, When content is assembled, Then it includes: categorized expense totals by IRS category, income summaries by source, embedded receipt images, and user notes for flagged items. Given a transaction has a receipt, When the packet is viewed or exported, Then the transaction entry links to its receipt image and the receipt page references its transaction (bidirectional cross-reference), with zero broken links. Given totals are computed, When compared to source data for the quarter, Then differences do not exceed $0.01 due to rounding.
Preview and Export (PDF/CSV) with Watermarking
Given a Draft packet exists, When previewed in-app, Then each page renders within 2 seconds and displays a visible DRAFT watermark; when the packet is Final, no watermark is present. Given a user requests exports, When PDF and CSV generation runs, Then both complete within 90 seconds, PDF size is ≤ 25 MB, CSV follows the defined schema (transaction_id, date, description, amount, category, receipt_url, notes), and both match the previewed data. Given an export fails, When the error occurs, Then the user is shown a retriable error with cause and the failure is recorded in the audit log.
Secure Share Links with Expiry and Revocation
Given a Final or Draft packet, When a share link is created, Then a unique, unguessable URL is generated, read-only access is enforced, and the link is pinned to a specific packet version. Given an expiry is set (default 30 days, configurable 1–90 days), When the expiry elapses, Then the link returns 410 Gone and no content is accessible. Given a user revokes a link, When the revocation is saved, Then the link immediately becomes invalid and future accesses are blocked; access attempts are logged.
Re-Generation, Versioning, and Change Log
Given the underlying data changes (e.g., new transactions, recategorization, new receipt), When the user or system triggers re-generation, Then a new packet version is created within 60 seconds, the previous version remains immutable, and the version number increments by 1. Given a new version is created, When the change log is generated, Then it lists added, removed, and modified items with timestamps, user/system actor, and affected transaction IDs. Given a packet is in Draft, When the user explicitly finalizes, Then the state changes to Final and the watermark is removed; re-generation after Final creates a new Draft version.
Document Vault Storage and Retrieval
Given a packet (Draft or Final) is created, When it is saved, Then it appears in the Document Vault under the correct quarter and tax year with metadata (version, state, created_at, finalized_at, readiness_score). Given a user opens the Vault, When searching by quarter, year, or version, Then the packet is returned within 2 seconds and can be opened to preview or export. Given a packet is updated to a new version, When the Vault is viewed, Then previous versions remain accessible as read-only with clear version labels.

Deduction Drills

Interactive, real‑world mini-scenarios (home office, mileage, gear, mixed‑use) that teach what’s deductible and why. Your answers auto‑toggle relevant settings and policies, prefill categories, and estimate savings so you learn while configuring your workspace—no tax jargon required.

Requirements

Interactive Scenario Engine
"As a freelance creative using TaxTidy on my phone, I want to walk through simple, real-world deduction scenarios so that I can understand what applies to me without reading tax jargon."
Description

Build a mobile-first engine that delivers interactive, branching mini-scenarios (home office, mileage, gear purchases, mixed-use assets) with step-by-step questions, progress save/restore, and contextual hints. The engine must support multiple input types (single/multi-select, sliders for percentages, date pickers) and compute scenario outcomes at each step. Scenarios are defined by a content schema so tax specialists can author and update flows without code deployments. The engine exposes events (answer selected, outcome updated, scenario completed) for downstream consumers (policy toggles, estimator, category mapping) and logs analytics to measure completion rate and friction points. It must perform smoothly on low-bandwidth mobile connections and support resuming across devices via user account sync.

Acceptance Criteria
Mobile Branching Flow with Per‑Step Outcome Updates
Given a published scenario "Home Office 2025 v1" with branching rules and step outcomes defined in the content schema When the user answers a step and taps Next Then the next step is selected according to the evaluated branch rule within 150 ms of input on a mid‑range mobile device And the outcome preview recalculates using the schema formulas and displays within 500 ms And changing a previous answer re‑evaluates downstream steps and outcomes, resetting invalidated answers per schema settings And the progress indicator reflects the correct percent complete after each step with ±2% tolerance
Multi‑Input Support and Validation on Mobile
Given a scenario step configured for single‑select, multi‑select, slider (0–100%), and date inputs When the user interacts with each input type on a mobile device Then single‑select enforces exactly one choice and advances on Next without errors And multi‑select enforces schema min/max selections and displays inline validation within 100 ms if violated And the slider supports 0–100% with 1% increments, shows the numeric value, and snaps to increments on release And the date picker enforces schema min/max dates and disallows invalid ranges with an accessible error message And all inputs are labeled with accessible names, support screen readers, and are keyboard navigable And invalid inputs prevent progression and do not emit outcome updates until valid
Progress Autosave and Cross‑Device Resume
Given the user is authenticated and has started a scenario When an answer is submitted or a step is viewed for >1s Then progress is autosaved locally within 200 ms and enqueued for sync to the account And when the device regains connectivity, queued progress syncs within 5 s And when the same user opens the scenario on a second device Then the engine restores the latest progress to the last answered step with all prior answers intact within 5 s of app open And if concurrent edits occur, the most recent timestamp wins and a non‑blocking banner indicates which answers were resolved
Content Schema Authoring, Validation, and Hot Publish
Given a tax specialist uploads a new scenario JSON adhering to schema version X When the file is validated server‑side Then validation errors are returned with line/field references, or a success response is returned within 2 s And on publish, the new version becomes available to users without an app deployment within 60 s And in‑progress users remain on their original version; new starts receive the new version And an admin can roll back to the prior version within 60 s, with changes reflected to new starts only And the schema supports steps, inputs, branching rules, outcome formulas, hints, localization, and analytics metadata fields
Event Emission for Downstream Consumers
Given the engine is running a scenario with downstream consumers registered (policy toggles, estimator, category mapper) When the user selects an answer Then an answer_selected event is emitted within 200 ms containing event_id, user_id_hash, scenario_id, step_id, answer_id, timestamp And when outcomes change Then an outcome_updated event emits computed fields and deltas and the estimator updates the savings preview within 1 s And when the scenario is completed Then a scenario_completed event is emitted and policy toggles and category mappings apply immediately to the user workspace within 2 s And events are delivered with at‑least‑once semantics and deduplicated by event_id
Analytics for Completion and Friction
Given analytics is enabled When a user interacts with a scenario Then events scenario_started, step_viewed, answer_selected, hint_viewed, outcome_updated, step_abandoned, and scenario_completed are logged with user_id_hash, scenario_id, step_id, timestamp, duration_ms, device_type, network_type And per‑event payload size does not exceed 2 KB and is batched to send within 3 s or 5 events, whichever comes first And a completion rate metric per scenario_id is available in the analytics store within 15 min of event generation And a friction report ranks steps by abandonment rate and median time‑to‑answer for the last 7 days
Low‑Bandwidth Mobile Performance and Responsiveness
Given a simulated 3G/400 Kbps connection on a mid‑range mobile device When the user opens a scenario for the first time Then the scenario UI becomes interactive within 3.0 s at the 75th percentile and 5.0 s at the 95th percentile And total scenario payload (excluding cached framework) is ≤250 KB on first load and ≤50 KB on subsequent steps And advancing between steps occurs within 500 ms median and 1.0 s p95 without blocking input And if rich media hints fail to load within 1.5 s, a plaintext fallback is shown automatically
Answer-Driven Policy Auto-Configuration
"As a solo consultant, I want my answers to automatically configure the right tax settings so that my workspace reflects my situation without me hunting through preferences."
Description

Translate scenario answers into concrete workspace settings and tax policies (e.g., home office method: simplified vs. actual, mileage method: standard vs. actual, depreciation vs. section 179 for gear, mixed-use allocation rules). Show a change preview with a human-readable diff, require explicit user confirmation, and provide one-click apply and one-click undo. All changes generate a versioned policy record with timestamps and author (user/system), and emit events for other modules to react (e.g., re-categorization). Include a "simulate" mode to preview impacts without persisting changes.

Acceptance Criteria
Home Office Method Auto-Configuration
Given the user completes the Home Office Drill with answers indicating the simplified method When the system maps answers to workspace policies Then home_office.method in the pending changes is set to simplified And fields only applicable to the actual method (e.g., home_office.actual_expenses, home_office.square_footage) are marked to be cleared in the pending changes And no policy is persisted until the user confirms Apply
Gear Expense Policy Auto-Selection (Section 179 vs Depreciation)
Given the user completes the Gear Drill choosing to expense immediately and indicates business use is eligible When the system maps answers to workspace policies Then gear.expense_method in the pending changes is set to section_179 And if the current policy is depreciation, the diff shows gear.expense_method from depreciation to section_179 And no policy is persisted until the user confirms Apply
Change Preview with Human-Readable Diff and Confirmation Gate
Given a pending change set exists from any Deduction Drill When the user opens the change preview Then the preview displays a human-readable diff showing field labels, previous values, and proposed values for only the changed settings And the Apply action remains disabled until the user explicitly confirms the changes And dismissing the preview without applying discards the pending change set
One-Click Apply Persists Policies and Emits Events
Given the user has confirmed the pending changes When the user clicks Apply Then all pending policy changes are persisted atomically to the workspace And a policy.change event is emitted once with the diff payload and author=user And the apply action is idempotent within the session to prevent duplicate writes
One-Click Undo Fully Reverts Last Applied Policy Change
Given a policy change was applied in the current session When the user clicks Undo Then the workspace policies revert exactly to the immediately previous version And a new version record is created with author=user and action=undo And a policy.change event is emitted indicating the revert
Versioned Policy Record Created on Apply
Given the user applies changes from a Deduction Drill When the changes are persisted Then a versioned policy record is stored with a unique version_id, ISO-8601 UTC timestamp, author (user/system), source=Deduction Drills, and the full diff payload And the record appears in policy history and is retrievable via API and UI
Simulation Mode Preview Without Persistence or Events
Given the user runs a Deduction Drill in Simulate mode When the system generates the change preview Then the human-readable diff and estimated impacts are shown And Apply is not available and no changes are written to the workspace And no version record is created and no events are emitted
Category Prefill and Backfill Mapping
"As a freelancer, I want my expenses to be auto-categorized based on what I learned in the drills so that I don’t have to reapply the same logic manually."
Description

Map scenario outcomes to TaxTidy expense categories and rules. Prefill categories for new transactions and optionally backfill the last 90–365 days based on user-selected scope. Present a review queue with batched suggestions, confidence scores, and clear rationales tied to the scenario that informed each suggestion. Support exceptions and lock flags to prevent future overrides. Integrate with existing bank feeds, invoices, and receipt OCR so new data inherits the configured mappings automatically.

Acceptance Criteria
Auto-Prefill from Deduction Drill Outcomes
Given I complete a Deduction Drill and save my answers When I enable category prefill from that scenario Then mapping rules are created linking scenario outcomes to specific TaxTidy categories and rule conditions (e.g., merchant contains, MCC equals, tag equals, amount ranges) And new transactions matching those conditions are auto-categorized within 60 seconds of ingestion And each applied mapping references the originating scenario by name and ID And the mapping is visible in Mapping Settings with fields: name, conditions, target category, source=Deduction Drills, createdBy, createdAt
Scoped Backfill for Last 90–365 Days
Given I choose Backfill and select a date scope within the range 90–365 days When I confirm the backfill Then only transactions in the selected date range are evaluated for suggestions And locked transactions and transactions in filed tax periods are excluded from changes And processing reports counts for evaluated, suggested, excluded, and errors And 95% of jobs complete within 10 minutes per 10,000 transactions And backfill results are added to the Review Queue as suggestions; no categories are changed until accepted
Review Queue with Confidence and Rationale
Given suggestions exist from prefill or backfill When I open the Review Queue Then suggestions are grouped in batches by rule/scenario And each item shows proposed category, confidence score (0–100), scenario reference, and a rationale listing matched signals (e.g., merchant, MCC, keywords) And I can batch-accept or batch-reject a group or a selected subset And Accept applies the category immediately and records audit entries; Reject dismisses the suggestion and optionally creates an exception rule And the queue updates counts for accepted, rejected, and remaining without page reload
Exceptions and Lock Flags Prevent Overrides
Given I mark a transaction or vendor as Locked When automated mapping evaluation runs Then no automated rule changes the category of the locked item And attempts to change are logged with reason=blockedByLock And manual edits require explicit Unlock confirmation Given I create an exception rule (e.g., exclude merchant "Apple" from the Gear rule) When evaluation runs Then the exception takes precedence and no suggestion is produced for matching items
Automatic Inheritance for New Bank, Invoice, and Receipt Data
Given new items arrive via bank feeds, invoice imports, or receipt OCR When ingestion and normalization complete Then configured mapping rules are evaluated and applied within 120 seconds And if multiple sources describe the same transaction, the unified record is categorized once without duplication And the applied category is consistent across bank, invoice, and receipt views and is attributed to the originating scenario
Audit Trail, Undo, and Filed-Period Safeguards
Given categories are applied via prefill or accepted via backfill When I view a transaction's history Then I see before/after category, rule ID, scenario reference, actor (system/user), and timestamp And I can undo a backfill batch within 30 days to restore prior categories And transactions in tax periods marked "Filed" are never auto-changed; only suggestions may be shown with a warning And exporting an IRS-ready packet reflects only accepted and applied categories
Real-Time Savings Estimator
"As a user configuring my deductions, I want to see an estimate of how much I’ll save so that I can choose the options that provide the best outcome for me."
Description

Calculate and display a live estimate of potential tax savings as users answer scenario questions. Combine ordinary income and self-employment tax effects, apply IRS limits (e.g., home office square footage caps, Section 179 thresholds), and reflect mixed-use percentages. Present conservative and optimistic ranges with clear assumptions and a disclaimer. Expose a breakdown by deduction type and provide deltas when toggling options (e.g., standard vs. actual mileage). Cache intermediate computations for instant feedback on mobile.

Acceptance Criteria
Live Savings Update on Answer Change
Given the user is in a Deduction Drill and has provided minimum required inputs When the user changes an answer that affects deductions Then the estimator recalculates and updates total and range without page reload And updates appear atomically with no partial values And p90 latency on mobile after the first compute is <= 200 ms; cold compute <= 1200 ms And if compute exceeds 500 ms a loading shimmer is shown until values settle And intermediate results are cached locally so subsequent updates avoid network calls And currency is formatted in the user locale and rounded to the nearest whole dollar
Combine Ordinary Income and Self-Employment Tax Effects
Given filing status, projected taxable income, and self-employment income are set When the estimator computes potential savings Then it includes both ordinary income tax and self-employment tax effects per current-year configured rates And applies configured interactions (e.g., half SE tax deduction) before computing totals And exposes a tooltip showing ordinary vs SE components with amounts And for test fixtures P1, P2, P3, total savings equals the sum of components within ±1% or $1 tolerance of reference results
IRS Limits and Caps Are Applied
Given a candidate deduction exceeds an IRS limit in the current-year rules dataset When the estimator computes savings Then the deductible base is clamped to the limit before tax rate application And the affected line item is labeled "Limited by IRS rules" with a link to assumptions And unit tests for home office area, Section 179 amount, and vehicle thresholds pass with expected clamping And where limits are income-phased, the phase-out logic is applied and reflected in assumptions
Mixed-Use Percentages Affect Savings
Given an expense or asset has a business-use percentage set by the user When the percentage is changed Then the eligible base is multiplied by that percentage and totals update accordingly And p90 update latency after first compute is <= 200 ms And the assumptions panel lists the applied percentage And for fixture cases at 0%, 40%, and 100%, totals scale linearly within ±1% tolerance
Range Presentation with Assumptions and Disclaimer
Given sufficient inputs exist to compute bounds When results are shown Then both Conservative and Optimistic savings are displayed with labels and a range badge And an Assumptions link opens a panel listing inputs and rules used for each bound And uncertain items are toggled off in Conservative and on in Optimistic per rule flags And a visible disclaimer states estimates are educational and not tax advice, within one viewport scroll And toggling an assumption updates only the corresponding bound and recalculates within 300 ms
Breakdown by Deduction Type
Given computed savings exist When viewing the breakdown Then each deduction type (e.g., Home Office, Mileage, Gear) shows deductible base, applied rate(s), and savings contribution And items are sorted by contribution descending And the sum of contributions equals the displayed total within ≤ $1 rounding difference And expanding an item reveals calculation details and any applied limits And each line item has accessible labels announcing its amount
Deltas on Method Toggles (Standard vs Actual Mileage)
Given two mutually exclusive methods exist for a deduction When the user toggles between methods Then the estimator recalculates and displays an inline delta next to the total and the affected line item And the delta equals new total minus prior total within $1 tolerance and includes a +/− prefix with compliant contrast And the delta persists for at least 3 seconds or until the next change And an Undo control restores the prior selection and totals within 200 ms
Plain-Language Guidance and Contextual Tips
"As a creative professional, I want clear explanations in everyday language so that I understand why something is deductible without needing a tax background."
Description

Provide just-in-time, plain-language explanations for each question and outcome, including examples tailored to creative freelancers and consultants. Avoid jargon; include short why-it’s-deductible notes, edge-case callouts, and links to deeper articles and IRS references. Support expandable tooltips and a quick glossary. Content must be localized-ready and maintain a target reading level equivalent to 8th–9th grade. Surface a gentle compliance reminder (e.g., substantiation needed) when relevant.

Acceptance Criteria
Readability and Plain-Language Compliance
- Short tip body ≤ 80 words; expanded body ≤ 200 words. - English Flesch–Kincaid grade between 8.0 and 9.9 for both short and expanded bodies. - Passive-voice sentences ≤ 10% of total; average sentence length ≤ 18 words; no sentence > 25 words. - Uses second-person ("you") in the opening sentence. - No tax-jargon terms from the controlled list unless accompanied by an inline plain-language definition or glossary tag.
Contextual Tips on Every Question and Outcome
- 100% of Deduction Drills question screens and outcome summaries display a visible help icon or Learn chip. - Tip opens within 250 ms on click/tap and via keyboard (Enter/Space) and closes with Esc or outside click. - Tip panel does not overlap primary action buttons and is fully readable on mobile screens ≥ 320 px width. - Focus returns to the trigger after the tip closes.
Expandable Tooltips and Quick Glossary
- Each tip includes a collapsed More details section that expands/collapses without reloading; default state is collapsed. - Expand/collapse is operable via keyboard and announced to screen readers (aria-expanded updates correctly). - Glossary-tagged terms show a 1–2 sentence definition tooltip ≤ 160 characters on hover/tap. - Quick glossary opens within one click from any tip and returns search results within 300 ms for a 10k-term dataset. - All glossary terms referenced in tips resolve to a single source of truth; no broken or missing definitions.
Tailored Examples, Why Notes, and References
- Each tip includes at least one example tailored to creative freelancers or solo consultants (e.g., photographer, designer, developer, consultant scenarios). - Each tip includes a "Why this is deductible" line ≤ 120 characters using non-jargon language. - Each tip provides two references: an in-app Learn more article and an external IRS reference; both links return HTTP 200 and have accurate titles. - External IRS links open in the system browser; in-app articles open in the in-app reader/webview.
Edge-Case Callouts and Decision Boundaries
- An Edge case callout is displayed only when relevant rule conditions are met and hidden otherwise. - For test inputs: (a) Mixed-use gear with business-use percent between 10% and 20%, (b) Home office marked not exclusively used, (c) Mileage entry with a personal stop, the callout renders with boundary guidance and one concise example. - Edge case callout length ≤ 100 words and includes a clear Next step action/link.
Compliance Reminders and Substantiation Prompts
- For deductions requiring documentation (e.g., mileage logs, home office measurements, receipts/invoices ≥ $2,500 per item), display a non-blocking reminder listing acceptable proof types. - Reminder appears adjacent to the relevant tip, uses neutral styling, and is dismissible; it does not block progression. - Reminder triggers on first exposure per relevant deduction type per session and does not reappear on the same screen after dismissal. - Reminder includes a link to record-keeping guidance; link returns HTTP 200.
Localization-Ready Content and Glossary Internationalization
- All tip and glossary strings are externalized to localization files with unique keys; zero hardcoded UI text. - ICU MessageFormat placeholders are used for variables (numbers, dates, currency); no string concatenation. - Pseudo-localization test (+30% length, accented characters, and bidi) shows no truncation or layout breakage on mobile and desktop. - Glossary and tip strings support fallback to English when a locale is missing. - No language-specific text embedded in images; all user-visible text is translatable.
Decision Annotations and Exportable Audit Trail
"As a user concerned about audits, I want a clear record of how my deductions were determined so that I can confidently justify them if questioned."
Description

Store a durable record of scenario answers, the resulting policy decisions, and the rationale shown to the user. Attach annotations to affected transactions and categories, and include them in the IRS-ready tax packet export. Maintain versioning to show what changed, when, and why, with rollbacks. Provide a downloadable audit bundle (PDF summary plus JSON detail) that ties deductions to user inputs and supporting documents (receipts, invoices), aiding audits and accountant reviews.

Acceptance Criteria
Persisting Decision Rationale from Deduction Drills
Given a user completes a Deduction Drills scenario and saves or exits When the session ends and the user later returns on any device Then the system shall persist and retrieve the scenario ID, question IDs and answers, applied policy decisions, and the exact rationale text shown to the user And the persisted record shall include timestamps, user ID, scenario version, and app version And reopening the scenario shall prefill prior answers and display the same rationale text verbatim And the stored record shall be immutable (no edits), with any change creating a new version entry
Annotating Transactions and Categories from Scenario Outcomes
Given a Deduction Drills scenario applies policy decisions that affect transactions and categories When those decisions are applied Then each impacted transaction shall receive an annotation including policy ID, scenario reference, rule/rationale summary, and estimated deduction impact And affected categories shall display a category-level annotation summarizing scope and impact And the IRS-ready packet shall include these annotations at both transaction and category levels And if a transaction is later reclassified by the user, the audit trail shall record the change, preserve the prior annotation, and add a new annotation reflecting the reclassification
Version History and Rollback of Policy Decisions
Given a user changes a policy decision via Deduction Drills When the change is saved Then the system shall create a new version entry that records what changed (fields and values), when (timestamp), who (user ID), and why (captured reason or system note) And a human-readable diff shall be available showing before/after values and impacted counts (transactions, categories) When a user performs a rollback to a prior version Then the prior policy decisions shall be reinstated, affected annotations updated accordingly, and a new rollback event recorded without deleting any versions And reapplying the newer version shall be possible and idempotent
Exporting IRS-Ready Audit Bundle (PDF + JSON)
Given a user initiates an audit export for a selected tax year When the export completes Then the user shall receive a single downloadable bundle containing: a PDF summary and a JSON detail file And the PDF shall include account/tax year, completed scenarios, active policies, rationale summaries, total deductions by category, and current version ID/timestamp And the JSON shall include scenario answers, policy decisions, per-transaction/category annotations, document references, and full version history with event metadata And the JSON shall validate against the defined schema and include checksums for referenced documents And for datasets up to 10,000 transactions and 2,000 documents, the export shall complete within 60 seconds
Linking Supporting Documents to Deductions
Given receipts and invoices are attached to transactions in TaxTidy When an audit bundle is exported Then each deductible transaction in the JSON shall reference its supporting document IDs, filenames, and checksums And the PDF shall display document counts (and thumbnails where available) per transaction or category summary And if a referenced document is missing or unreadable, the export shall flag the item with a clear reason and include it in a validation section And documents linked to multiple transactions shall be referenced in each relevant item without duplication in storage
Access Control and Redaction Options for Audit Bundle
Given an authenticated session When a non-owner, non-invited accountant attempts to export an audit bundle Then the system shall deny with HTTP 403 and record the attempt in the audit log When the account owner or invited accountant exports with PII redaction enabled Then bank account numbers, vendor personal emails/phone numbers, and home addresses shall be masked in the PDF, and the JSON shall include redaction flags for each masked field And all audit exports shall be logged with user ID, role, timestamp, IP, and export parameters

Category Quiz

A quick, visual quiz that turns confusing expense labels into easy choices. As you classify sample transactions, TaxTidy learns your style, seeds smart rules, and generates a pocket ‘What Goes Where’ guide you can reference on mobile. Faster categorization with fewer second guesses.

Requirements

Mobile-First Quiz UI
"As a mobile-first freelancer, I want an intuitive, fast quiz to categorize sample expenses so that I can confidently choose categories without digging through complex menus."
Description

An interactive, mobile-first quiz interface that presents sample transactions as swipeable cards with concise details (merchant, amount, date, context hints) and clear category choices as tappable chips. Supports tap or swipe selection, back/skip, progress indicator, and quick tooltips for category definitions. Optimized for under two seconds initial load, responsive across iOS, Android, and modern browsers, with tactile feedback and accessible controls (WCAG 2.1 AA). Integrates with TaxTidy sessions and analytics to record selections, skips, and time-on-question. Emits structured events to the learning engine and persists selections to the user profile. Guards against accidental input with undo/confirm patterns and handles offline or spotty connectivity with queued events and retry.

Acceptance Criteria
Initial Mobile Load and Responsive Rendering
Given a new user opens the Category Quiz UI on a supported mobile browser with no warm cache, When the user navigates to the quiz, Then the first interactive card renders and is usable within 2000 ms of navigationStart. Given viewport widths from 320 px to 768 px on iOS Safari 15+ and Android Chrome 100+, When the quiz loads, Then there is no horizontal scrolling, content fits within the viewport, and all text remains legible without overlap or clipped controls. Given the first card is visible, When the user scrolls or swipes, Then frame rate stays ≥55 fps for 90% of interaction duration over a continuous 10-second window on mid-tier 2020+ devices. Given the interface loads, When layout occurs, Then cumulative layout shift (CLS) remains <0.1 until first interaction. Given the first input occurs, When the user taps a chip, Then input delay is <100 ms on mid-tier 2020+ devices.
Swipe and Tap Selection with Haptics
Given a visible transaction card with category chips, When the user taps a category chip, Then the chip becomes selected, the choice is applied, and the next card appears within 350 ms. Given swipe gestures are enabled, When the user swipes the card past a 60% horizontal threshold or with a fling velocity >800 px/s toward a mapped category, Then that category is selected; partial swipes below threshold snap back with no selection. Given the device supports haptics, When a selection is finalized, Then a light haptic feedback is emitted exactly once; on unsupported devices, no errors are thrown and no feedback is attempted. Given a selection is applied, When the UI updates, Then the selected chip state is visually and programmatically (aria-selected=true) reflected.
Undo/Confirm and Accidental Input Protection
Given a selection is made, When the undo affordance appears, Then it remains visible and actionable for at least 3 seconds without blocking primary interactions. Given the user taps Undo within the window, When the action executes, Then the selection is reverted, the original card is restored, no new card is advanced, and a selection_undone state is recorded. Given a selection is made via a rapid double-tap or conflicting gesture, When confirm mode is triggered, Then a confirmation sheet appears with Confirm and Cancel; Confirm applies and advances, Cancel restores the pre-selection state. Given the undo window elapses without action, When the user proceeds, Then the selection is locked and can only be changed by navigating Back to that card.
Back and Skip with Accurate Progress
Given a session contains N cards, When the user taps Skip on a card, Then no category is assigned, a skip state is recorded, and the progress indicator advances to reflect (completed+skipped)/N within 200 ms. Given the user taps Back from any card after the first, When navigation occurs, Then the previous card displays with its prior state (selected or skipped) fully restored and editable. Given the user is on the first card, When Back is invoked, Then no backward navigation occurs and the Back control presents as disabled both visually and to assistive tech (aria-disabled=true). Given the final card is acted upon, When the last selection or skip completes, Then a completion view appears within 500 ms and the progress indicator shows 100% with session completion state set.
Category Tooltips and Definitions
Given a category chip shows an info icon, When the user taps the icon or long-presses the chip for ≥500 ms, Then a tooltip opens within 200 ms showing the category name and a definition of ≤120 words. Given the tooltip is open, When the user taps outside, presses Escape, or performs a back gesture, Then the tooltip closes without shifting the underlying layout or changing the current selection. Given assistive technology is active, When the tooltip opens, Then focus moves to the tooltip, is trapped until dismissed, and the tooltip is announced with an accessible name and role of tooltip or dialog; closing returns focus to the invoking control.
Accessibility and Control Targets (WCAG 2.1 AA)
Given the quiz UI is evaluated, When checked against WCAG 2.1 AA, Then text contrast is ≥4.5:1 (normal) and ≥3:1 (large), icon/button contrast is ≥3:1, and all interactive targets are ≥44×44 dp with visible focus indicators at ≥3:1 contrast. Given a screen reader is enabled, When navigating a card, Then merchant, amount, date, and context are announced with concise labels; category chips announce their name and selected state; the progress is announced as position x of N. Given keyboard input is used, When tabbing and using arrow/enter/space, Then all controls (chips, Skip, Back, tooltip) are reachable and operable; swipe actions have keyboard equivalents (e.g., left/right arrows for previous/next or chip cycling). Given the user sets system text size to 200%, When the quiz renders, Then content reflows without loss of information or functionality, and no controls overlap or become off-screen without a path to reach them. Given a selection or undo completes on a haptics-capable device, When feedback is emitted, Then one light impact is produced and no duplicate haptic events occur for a single action.
Analytics, Event Emission, Offline Queue, and Persistence
Given a quiz session is active, When any of the following occurs: card_shown, tooltip_opened, category_selected, skipped, back_navigated, selection_undone, session_completed, Then a structured event conforming to schema category_quiz_v1 is created with fields: event_id (UUIDv4), user_id, session_id, card_id, timestamp (ISO 8601), action, time_on_question_ms (for selection/skip), and category_id where applicable. Given connectivity is online, When an event is created, Then it is sent to analytics and learning-engine endpoints within 200 ms and acknowledged (HTTP 2xx); on success, it is marked delivered and removed from the local queue, and the corresponding selection is persisted to the user profile via the profile API within 500 ms. Given connectivity is offline or a transient 5xx/timeout occurs, When an event is created, Then it is queued durably in local storage with FIFO ordering and retried using exponential backoff (1s, 2s, 4s … max 60s) up to 7 attempts; events are idempotent via event_id and do not create duplicates on the server. Given the app restarts before connectivity returns, When it relaunches, Then the queue is restored and delivery resumes automatically; a non-blocking "Working offline" banner is shown while unsent events exist. Given events and selections eventually sync, When the user returns to the quiz later, Then prior selections are present in their profile and the learning engine acknowledges processing (HTTP 2xx), verified via success metrics in logs.
Adaptive Categorization Engine
"As a freelancer, I want TaxTidy to learn from my choices so that future transactions are auto-categorized the way I prefer."
Description

A learning component that converts quiz answers into weighted categorization rules and keyword/merchant heuristics. Combines merchant names, memo tokens, amounts, and receipt OCR terms to compute category probabilities and confidence thresholds. Seeds the user’s auto-categorization profile for cold start and continuously updates as the quiz progresses. Integrates with the existing transaction classification pipeline to apply learned rules to historical and incoming transactions, with versioning, audit logs, rollback, and conflict resolution against global rules. Exposes an API for rule scoring, preview, and bulk application.

Acceptance Criteria
Quiz Answers Generate Weighted Rules and Heuristics
Given a quiz answer is submitted with merchant, memo tokens, amount, and OCR terms When the engine processes the answer Then it updates the user's rule set with weights normalized to [0,1] per feature and category And it records feature provenance for each weight (merchant|memo|amount|ocr) And it persists a new immutable rule-set version with an incremented version number And p95 processing latency per answer is <= 200ms And the rule set is queryable by userId and version
Incremental Learning During Quiz Progress
Given the engine receives sequential quiz answers for a user When each answer is processed Then it recalculates category scores for up to 50 queued preview transactions using the latest weights And returns updated top-3 category predictions with probabilities for each preview item And p95 recalculation latency for the preview batch is <= 500ms And an audit entry records the before/after weight deltas and affected preview items
Cold-Start Seeding and Historical Backfill
Given a user completes at least 10 quiz answers When seeding is triggered for the user's profile Then the engine scores historical transactions from the last 180 days (up to 5,000 items) And auto-applies categories only when probability >= 0.80 (default threshold) And marks transactions below threshold as Needs Review without changing their current category And completes scoring within 60 seconds for 5,000 transactions And produces a summary (total_scored, auto_applied, needs_review, threshold_used, rule_version)
Live Pipeline Application with Confidence Thresholding
Given a new transaction enters the classification pipeline for a user When scoring is invoked with the user's latest active rule-set version Then the engine returns category, probability, top contributing features, and rule_version And if probability >= threshold, the category is auto-applied; otherwise the transaction is marked Needs Review And user-locked categories are never overwritten And p95 end-to-end scoring latency per transaction is <= 150ms And decisions are idempotent for identical inputs (same features, same rule version, same threshold)
Versioning, Audit Logging, and Rollback
Given multiple rule-set versions exist for a user When a rollback to a prior version is requested Then the active version pointer switches to the target version and subsequent scoring uses that version And an audit log entry records actor, timestamp, from_version, to_version, and reason And rollback completes within 5 seconds And no historical audit entries are mutated or deleted
Conflict Resolution Against Global Rules
Given both a user-specific rule and a global rule match a transaction When category probabilities are computed Then precedence is applied in order: user > account-level > global And for equal precedence, choose the higher probability; for equal probability, choose the more specific rule (exact merchant > fuzzy; longer token match > shorter) And the decision records conflict_details with competing rule_ids, scores, and chosen_reason And the same inputs always yield the same outcome
Scoring, Preview, and Bulk Application API
Given an authenticated POST /v1/categorization/score request with up to 1,000 transactions When the payload is valid Then respond 200 with per-item top-3 categories, probabilities, explanations, and rule_version And p95 latency is <= 800ms for a batch of 1,000 And invalid schema returns 400; invalid/expired token returns 401 Given POST /v1/categorization/preview with dryRun=true and a rule_set_version When a valid sample set is provided Then respond with predicted categories and a diff from current categories without persisting changes And include a summary (total, would_auto_apply, would_need_review) Given POST /v1/categorization/apply-bulk with a list of transaction_ids and rule_set_version When the request is accepted Then return 202 with jobId and process asynchronously respecting thresholds and skip locked transactions And GET /v1/categorization/jobs/{jobId} returns status in {queued,running,succeeded,failed} with counts (processed, auto_applied, skipped, errors)
Rule Review & Bulk Apply
"As a user, I want to review and approve the rules before they change my data so that I trust the system and can correct mistakes."
Description

A post-quiz confirmation step that summarizes proposed rules in plain language, previews impact (counts and examples), and lets users approve, edit, or discard individual rules. Supports bulk application to past 12–24 months of transactions and sets defaults for future imports. Provides conflict detection (such as overlapping merchant rules), priority ordering, and safe-apply with undo. Changes are tracked with timestamps, actor, and rule version for auditability. Integrates with notifications to confirm applied changes and with the classification pipeline to reprocess affected items asynchronously.

Acceptance Criteria
Plain-Language Rule Summary Render
Given a user completes the Category Quiz and opens Rule Review, When the proposed rules load, Then each rule card shows a human-readable description including its conditions (merchant/keyword/amount/source) and its action (target category and any memo/tag). Given the rule cards are displayed, When a rule card is viewed, Then it also displays whether it will be a default for future imports and exposes editable fields for conditions and action without navigating away.
Impact Preview Accuracy
Given a selected historical range of 12 or 24 months and a proposed rule, When the impact preview is shown, Then it displays the total count of matching historical transactions computed from a snapshot taken at preview time, and shows 3 example transactions (most recent first) with merchant, date, and amount. Given the impact preview is visible, When the count is compared to the snapshot query, Then the count equals the number of eligible transactions in the snapshot; if zero, the rule is labeled "No past matches" and bulk-apply for that rule is disabled.
Approve, Edit, or Discard a Rule
Given the Rule Review screen, When the user approves a rule, Then the rule is marked Approved and included in the Apply summary. Given a rule is opened in Edit, When the user updates conditions or action and saves, Then required fields are validated, errors are shown inline, and on success the rule summary and impact preview recompute within 5 seconds. Given a rule is open for editing, When the user cancels, Then no changes persist. Given a rule is visible, When the user discards it, Then it is removed from the apply set and will not be set as a default for future imports. Given an approved rule has "Default for future imports" enabled, When new transactions are imported that match the rule, Then those transactions are auto-categorized per the rule.
Bulk Apply Range and Execution
Given the user opens the range selector, When choosing a historical window, Then options include 12 months (default) and 24 months, and only one range can be selected. Given at least one rule is approved, When the user clicks Apply, Then a background job is enqueued within 10 seconds and a non-blocking progress banner appears, and the job processes only transactions within the selected range. Given a dataset of up to 10,000 transactions in range, When apply begins, Then processing starts within 60 seconds and completes within 10 minutes. Given the apply job completes, When the summary renders, Then it lists per-rule applied counts that match the preview snapshot taken at apply time.
Conflict Detection and Priority Ordering
Given two or more rules could classify the same transaction, When Rule Review computes impacts, Then a conflict indicator is shown on affected rules and a banner explains the overlap. Given a conflict exists, When the user opens Priority Ordering, Then rules can be reordered via drag-and-drop, and the "winning rule" for a sample transaction updates immediately based on the new order. Given unresolved conflicts are present, When the user attempts to Apply, Then apply remains disabled until a priority order is saved or the user confirms "Use suggested order", and the saved order persists for future imports and reprocessing.
Safe Apply with Undo
Given a bulk apply completes successfully, When the confirmation appears, Then an Undo Apply control is visible for 15 minutes with a countdown timer. Given the Undo control is clicked within 15 minutes, When the reversal job runs, Then all changes made by the apply (transaction classifications and any memo/tag set by the rules, and rule defaults for future imports) are reverted to their prior state, and the operation is idempotent. Given a partial failure occurs during apply or undo, When the job finishes, Then the user sees an error summary listing failed items and a retry option for the failed subset.
Audit Trail and Notifications
Given any rule is approved, edited, discarded, applied, or undone, When the action is saved, Then an audit record is written with timestamp, actor, rule ID, rule version, and old/new values. Given a bulk apply completes, When notifications are dispatched, Then the user receives an in-app and email notification within 60 seconds that includes the apply job ID, number of rules applied, number of transactions changed, and a link to the audit log. Given the audit log is opened, When filtering by date, actor, rule ID, or job ID, Then results return within 2 seconds for ≤10,000 records and include all related state transitions (queued, started, completed, undone).
Personalized Pocket Guide
"As a freelancer, I want a simple, personalized guide of what goes where so that I can quickly reference categories on the go."
Description

A compact, personalized ‘What Goes Where’ guide generated from the user’s quiz responses and transaction patterns. Displays top categories, examples, and do/don’t tips, optimized for quick mobile reference and offline access. Searchable and bookmarked within the app, with one-tap sharing or export to PDF. Automatically updates when rules change and includes a changelog badge to highlight new guidance. Integrates with the taxonomy service for IRS-aligned descriptions and with localization to reflect region and terminology as needed.

Acceptance Criteria
Guide Generation After Quiz Completion
Given a user has completed the Category Quiz and has at least 10 categorized transactions in their history When the Personalized Pocket Guide is generated Then it includes the top 10 categories ranked by frequency from the user’s transactions And each included category displays an IRS-aligned description from the taxonomy service And each included category shows at least 2 example transactions, if available; otherwise 0–1 when fewer exist And each included category shows one "Do" tip and one "Don’t" tip And the initial generation completes within 5 seconds on a mid-tier mobile device And the guide is saved to the device for later access
Instant Offline Access and Sync
Given a guide has been generated on the device When the device is offline Then the full guide content (categories, descriptions, examples, tips, icons) is viewable without errors And tapping previously loaded images or icons does not trigger network calls And Export to PDF is available and completes offline When the device regains connectivity and the app is foregrounded Then the guide checks for updates and syncs within 60 seconds And the "Last updated" timestamp reflects the latest successful sync
In‑App Search and Bookmarks
Given a generated guide is open When the user enters a search term of at least 2 characters Then results return within 300 ms for a guide of up to 500 entries And matches highlight occurrences in category names, IRS descriptions, examples, and tips When the user bookmarks a category or tip Then it appears under Bookmarks within 1 second and persists across app restarts When a bookmark is removed Then it no longer appears in Bookmarks
One‑Tap Share and PDF Export
Given a generated guide is open When the user taps Share Then a native share sheet opens within 500 ms And an option to Export to PDF is available in one tap When Export to PDF is selected Then a PDF renders within 3 seconds for a guide up to 20 pages And the PDF contains a cover with user name (or account alias), tax year, generation timestamp, app version, and taxonomy version And the PDF includes a clickable table of contents and page numbers And the PDF file size is <= 2 MB for a guide with up to 10 categories and 20 example images
Auto‑Updates and Changelog Badge
Given user categorization rules or taxonomy data change When the app is foregrounded or receives a push update Then the guide regenerates in the background within 60 seconds And a "New" badge with the count of updated items appears on the guide entry point When the user opens the changelog Then it lists updated categories with before/after summaries and timestamps And viewing the changelog clears the badge until the next change And the guide metadata displays the current ruleset and taxonomy versions
Taxonomy Integration and IRS Alignment
Given connectivity to the taxonomy service is available When generating or updating the guide Then each category includes the IRS-aligned label and description returned by the service And categories are mapped to the correct IRS code where applicable When the taxonomy service is unreachable Then the guide uses the last cached taxonomy (no older than 30 days) and displays an "Out of date" banner And a retry occurs automatically on next app foreground
Localization and Regional Terminology
Given the user’s device language and region settings (or in-app locale setting) When generating the guide Then all category names, descriptions, examples, dates, and currency formats reflect the selected locale And right-to-left languages render correctly, including PDF export And untranslated strings in supported locales = 0 When the user switches locale Then the guide regenerates with the new locale within 30 seconds, and the "Last updated" timestamp is refreshed
Sample Transaction Selection
"As a user, I want the quiz to show representative and tricky examples from my own data so that my answers actually improve categorization accuracy."
Description

A sampling service that curates 15–25 representative transactions spanning high-volume merchants, ambiguous labels, and edge cases across bank feeds, invoices, and receipt photos. Deduplicates and redacts sensitive details, infers context such as recurring subscription versus one-off purchase, and tags items by difficulty or uncertainty to maximize learning value per question. Includes cold-start fallbacks using industry-relevant exemplars when user data is sparse. Ensures coverage of key IRS Schedule C categories commonly used by freelancers and creatives. Exposes parameters for quiz length, category coverage targets, and refresh cadence.

Acceptance Criteria
Curating 15–25 Representative Transactions
Given quiz_length is set to 20 (min 15, max 25) and the user has ≥ 200 transactions in the last 12 months across bank feeds, invoices, and receipt photos When the sampling service runs Then it returns exactly quiz_length unique transactions between 15 and 25 And at least 30% are from bank feeds, 30% from invoices, and 20% from receipt photos when such sources exist; otherwise reallocate proportionally to available sources And the set includes at least 3 high‑volume merchant samples, at least 3 ambiguous‑label samples, and at least 2 edge‑case samples when available And selection completes in ≤ 1.5s at p50 and ≤ 3.0s at p95 for ≤ 10k transactions over 12 months
Deduplication and Sensitive Detail Redaction
Given source data may contain duplicates or near‑duplicates across bank feeds, invoices, and receipt photos When the sampling service returns the quiz set Then no two selected items refer to the same real‑world transaction (exact duplicate rate = 0) And near‑duplicate pairs (similarity ≥ 0.92 on merchant+amount+date) are ≤ 1 per quiz set And PANs, bank account numbers, routing numbers, email addresses, phone numbers, street addresses, and full names are redacted or tokenized per policy before display And automated PII scanning detects 0 violations in the payload And redaction preserves merchant alias, amount, and month
Context Inference for Recurring vs One‑Off Purchases
Given at least 3 occurrences of a merchant with a regular cadence (±5 days) and similar amounts (±10%) When the sampling service annotates transactions Then items are labeled recurring with precision ≥ 90% and recall ≥ 85% against a curated validation set And at least 2 recurring and 2 one‑off items are included in the quiz when available And recurring items include an inferred frequency label (e.g., monthly, annually)
Difficulty/Uncertainty Tagging and Selection for Learning Value
Given a model computes an uncertainty score in [0,1] and difficulty tags {easy, ambiguous, edge} When the sampling service composes the quiz Then every selected item has difficulty and uncertainty metadata And ≥ 40% of items are ambiguous or edge when such items exist And the median uncertainty of the set is ≥ 0.35 (configurable) And the top‑k (k=5) most uncertain items are included unless diversity constraints (source or category) would be violated, in which case the next item by uncertainty is used
Cold‑Start Sampling with Industry‑Relevant Exemplars
Given the user has < 50 historical transactions in the last 12 months When the sampling service runs Then ≥ 70% of the quiz set is populated from industry‑relevant exemplars for the selected industry persona And exemplars are clearly labeled as sample and contain no real user data or PII And the quiz covers at least 8 of the top IRS Schedule C categories for freelancers/creatives or meets configured category_coverage_targets, whichever is stricter
Parameterization and Refresh Cadence
Given parameters quiz_length, category_coverage_targets, and refresh_cadence are set via configuration or API When parameters are updated successfully Then the next quiz produced after the start of the next refresh window respects the new values And an audit record captures timestamp, actor, old→new values, and quiz_id linkage And if refresh_cadence is daily/weekly/monthly, a new quiz sample is generated within 10% of the cadence interval (e.g., 24h ± 2.4h) And if constraints cannot be satisfied (e.g., insufficient data), the API returns HTTP 409 with a machine‑readable reason and suggested fallback
IRS Schedule C Category Coverage on Rich Data
Given the user has ≥ 200 categorized historical transactions and category_coverage_targets specify minimum counts per category When the sampling service selects transactions Then the quiz set covers at least 10 commonly used Schedule C categories for freelancers (e.g., Advertising, Office Expense, Supplies, Meals, Travel, Utilities, Software/Subscriptions, Contract Labor, Dues/Subscriptions, Other Expenses) or meets the configured targets And no single category exceeds 25% of the quiz unless dictated by targets or available data distribution And a category coverage report is emitted with actual vs target counts
Progress Sync & Resume
"As a busy freelancer, I want my quiz progress saved and synced so that I can stop and resume on any device without losing work."
Description

Persistent quiz state management that auto-saves after each response and enables seamless resume across devices. Handles session timeouts, app restarts, and data refreshes without losing progress. Supports quiz versioning so that users returning after data changes are offered a short ‘delta’ set rather than repeating completed items. Stores state in a secure backend tied to the user account with conflict resolution for concurrent sessions. Emits checkpoints for analytics and gracefully recovers from network failures with idempotent retries.

Acceptance Criteria
Auto-Save After Each Response
Given an authenticated user is taking the Category Quiz and is online When the user selects an answer and advances to the next item Then the response, current position, quiz_id, quiz_version, and server timestamp are persisted within 1 second And the local cache reflects the same state atomically And if the app is force-closed and reopened within 24 hours, the quiz resumes at the next unanswered item without re-asking saved items
Seamless Cross-Device Resume
Given the user answered up to question K on Device A And a checkpoint was committed to the backend When the user signs in on Device B within 2 minutes Then Device B fetches the latest checkpoint and opens at question K+1 within 3 seconds And no previously completed items are shown again And when Device A later resumes, it reflects the same checkpoint without divergence
Session Timeout and App Restart Recovery
Given the user is inactive long enough for session expiration When the user returns and re-authenticates Then the last saved checkpoint is restored and the quiz resumes at that point And any locally queued answers not yet synced are submitted after login And no duplicate questions or lost answers occur during recovery
Quiz Versioning With Delta Set
Given the user completed items under quiz_version v1 And quiz_version v2 introduces N new items and M modified items When the user resumes after the version upgrade Then the user is presented only N+M items consisting of the new and modified items And previously completed unmodified v1 items remain marked done and are not re-asked And the completion record stores v2 and a migration map of re-asked items
Concurrent Session Conflict Resolution
Given the same user has two active sessions (A and B) And both submit different answers for the same question within 10 seconds When the backend receives both submissions Then the backend keeps the answer with the latest server-received timestamp and discards the other as a conflict loser And both clients converge to the resolved answer within 5 seconds of resolution And only one answer is stored in the persistent state And an audit log records both attempts with device_id and timestamps
Network Failure Recovery With Idempotent Retries
Given the device is offline or the backend returns a 5xx error When the user submits an answer Then the client assigns a unique idempotency key and queues the submission locally And retries with exponential backoff until success or 5 minutes elapse And upon connectivity, the backend processes each key at-most-once and returns the committed checkpoint And the user can continue answering up to 20 queued items while offline without blocking And no duplicate answers or checkpoints are created after retries
Analytics Checkpoints Emission
Given the user interacts with the quiz When events occur (quiz_started, answer_recorded, pause, resume, version_migrated, quiz_completed) Then a checkpoint event is created with user_id, quiz_id, quiz_version, question_id (if applicable), session_id, device_id, timestamp, and idempotency_key And events are sent within 10 seconds when online or queued offline and sent on reconnect And downstream receives no duplicate events for the same idempotency_key

Snap Coach

Guided practice for your first five receipts with instant feedback. See OCR in action, learn how to frame photos, and watch matches form to bank transactions in real time. Earn a ‘receipt ready’ badge and enable auto‑match defaults with confidence.

Requirements

Guided First-Five Receipts Onboarding
"As a first-time freelancer using TaxTidy, I want a guided flow for my first five receipts so that I can learn best practices and feel confident using Snap Coach."
Description

A mobile-first, step-by-step flow that walks new users through capturing and submitting their first five receipts with Snap Coach. The flow provides inline tips, a visible progress indicator, and resumable state so users can stop and continue later. It integrates with the existing OCR pipeline and bank feeds to showcase end-to-end processing, while logging analytics for each step (capture, retake, confirm) to measure completion and friction. The experience is localized, accessible, and gated to trigger only for first-time users (or until completed), with a manual restart option from settings.

Acceptance Criteria
First-Time Gate and Manual Restart
Given a first-time user who has not completed the flow, When they launch the mobile app, Then the Snap Coach onboarding is presented within 3 seconds. Given a user who has completed the flow, When they launch the app, Then the Snap Coach onboarding does not auto-trigger. Given a user navigates to Settings > Onboarding, When they tap "Restart Snap Coach" and confirm, Then progress resets to 0/5 and the onboarding relaunches at step 1. Given a user has partially completed (1–4 receipts), When they relaunch the app, Then they are prompted to continue from their last step.
Guided Capture and Inline Tips for First Five Receipts
Given the camera screen is open, When a receipt is in frame, Then on-screen guides show edge detection and real-time tips on framing, glare, and blur. Given a capture is taken, When the OCR preview is shown, Then "Retake" and "Confirm" controls are visible and operable; Confirm is enabled only after the preview renders. Given OCR confidence < 0.60, When the user attempts to confirm, Then a nudge appears recommending a retake; the user may override and proceed. Given the device locale is supported, When tips render, Then all instructional text appears in that locale without truncation; screen readers announce tip text and controls with appropriate labels and contrast meets WCAG AA.
Progress Indicator Accuracy and Completion
Given the user is in the flow, When a receipt is confirmed, Then the progress indicator increments by 1 and displays "n of 5" with a proportional progress bar. Given the user retakes a photo before confirming, When the retake completes, Then the progress indicator value does not change. Given the user confirms the fifth receipt, When the success screen is shown, Then progress displays 5/5 and the flow is marked complete (no further auto-trigger). Given a screen reader is active, When progress changes, Then an announcement states the new count (e.g., "3 of 5 receipts completed").
Resumable State Persistence Across Sessions
Given the user exits before completion, When they reopen the app within 30 days, Then the flow resumes at the last incomplete step with prior captures and OCR previews intact. Given the device is offline at capture time, When connectivity returns, Then pending uploads are sent automatically and the flow advances without losing state. Given the user taps "Save and exit", When the confirmation appears, Then state is saved within 1 second and is available on next launch. Given images are cached locally, When inspected via platform security APIs, Then they are encrypted at rest per OS defaults.
Real-Time OCR Processing and Bank Match Feedback
Given a receipt is captured, When processing begins, Then an OCR loading indicator appears within 500 ms and initial extracted fields display within 5 seconds on a good network. Given a bank transaction exists within ±7 days of the receipt date and amount difference ≤ $0.50 or 1% (whichever is greater), When OCR completes, Then a match suggestion with confidence score appears within 15 seconds. Given no eligible match exists, When OCR completes, Then a "No match yet" state is shown with a "Retry" control; retries are rate-limited to once per 60 seconds. Given OCR fails 3 times on the same receipt, When the third failure occurs, Then the user is offered manual entry and may proceed without a photo.
Analytics Event Logging and Funnel Metrics
Given the flow is used, When key actions occur, Then the following events are logged with required properties: onboarding_started, camera_opened, receipt_captured, ocr_preview_shown, retake_used, confirm_submitted, step_completed, flow_completed, flow_abandoned, resume_flow, match_suggested, match_accepted, match_declined. Given events are queued, When the device has connectivity, Then 95% of events are delivered to the analytics pipeline within 60 seconds; offline events are persisted and sent on reconnect. Given 100 test sessions, When events are inspected, Then event schemas match the tracking plan and funnel completion rate can be computed with no missing step_completed events.
Badge Award and Auto-Match Enablement
Given the user confirms the fifth receipt, When the success screen is displayed, Then a "Receipt Ready" badge is awarded and visible in Profile and success UI. Given the badge is awarded, When the opt-in prompt appears, Then the default "Auto-match receipts to bank transactions" setting is presented with options "Enable now" and "Keep off"; the user must choose to proceed. Given the user enables auto-match, When future receipts are captured, Then auto-match runs by default; if they keep off, it remains disabled; the current setting is persisted and synced within 60 seconds. Given onboarding is marked complete, When the user relaunches the app, Then Snap Coach onboarding does not auto-trigger; the manual restart option remains available in Settings.
Real-time OCR Feedback & Confidence
"As a user capturing a receipt, I want instant feedback on extraction quality so that I can correct issues before saving."
Description

Instant, in-session OCR that previews extracted fields (merchant, amount, date, category suggestions) and surfaces confidence scores with contextual prompts to retake or correct. The UI highlights low-confidence fields, supports quick edits, and updates in real time as the user adjusts framing or retakes. It handles offline capture with queued processing, enforces privacy by running lightweight checks on-device when possible, and gracefully falls back to server OCR with latency-spinners and retries. Error states are clear and reversible.

Acceptance Criteria
Instant OCR Preview with Confidence and Highlights
Given the camera view is open and a receipt is in frame When focus is stable for at least 300ms Then on-device OCR runs and previews merchant, amount, date, and category suggestion within 700ms at the 95th percentile And each field displays a numeric confidence score (0–100) And fields with confidence <80% are visually highlighted And if any field’s confidence <50%, a contextual prompt offers Retake, Adjust framing tips, or Edit manually
Real-Time Updates on Framing Adjustments and Retakes
Given a live camera session with OCR preview visible When the user adjusts framing improving edge detection or contrast Then OCR re-runs and updates fields and confidences within 500ms without creating duplicate receipts When the user taps Retake Then the prior capture is versioned and the new OCR replaces the preview And the user can revert to the previous version within the session
Quick In-Session Field Editing and Validation
Given OCR has populated fields When the user edits merchant, amount, date, or category Then the changes persist to the receipt record immediately And category suggestions re-rank based on the edits And amount enforces locale currency format with inline error messages on invalid input And date cannot be set more than 7 days in the future And any corrected field’s confidence is set to 100% and its highlight is removed
Offline Capture with Queued Processing
Given the device is offline When the user captures a receipt Then the image is stored locally (<10MB), timestamped, and marked Queued And the user can enter edits while offline When connectivity is restored Then queued receipts are OCR-processed within 10 seconds and synced And failures display a retriable status with a reason code and Retry action And offline edits are merged without being overwritten by OCR
Privacy-First On-Device Checks with Server Fallback
Given the device supports the on-device OCR model When a receipt is captured Then OCR runs on-device and the image is not uploaded until the user confirms Save If on-device OCR fails or aggregate confidence for merchant+amount <70% Then server OCR is invoked with a visible spinner and privacy notice And data is transmitted over TLS 1.2+ and deleted from the server within 24 hours after processing And if the user has opted out of server OCR in Settings, the app offers manual entry instead of upload
Latency Indicators, Retries, and Resilience
Given server OCR is in progress When processing exceeds 2 seconds Then a latency spinner and status text are shown If processing exceeds 8 seconds or a network error occurs Then the system retries up to 3 times using exponential backoff (1s, 2s, 4s) And the user may Cancel to leave the receipt in Needs OCR with a Retry button And partial OCR results never overwrite user-edited fields; user edits take precedence on merge
Clear, Reversible Error States
Given any OCR, sync, or save error occurs Then the UI presents a human-readable message with an error code, a Retry action, and a Send Feedback link And no error is shown as toast-only; a persistent inline banner or modal is used And the user can Undo the last edit per field and Restore original OCR values within the current session And deleting any queued or processed receipt requires confirmation and supports Restore from Trash within 30 minutes
Camera Framing Assist & Quality Gate
"As a mobile user, I want on-screen framing guidance and quality checks so that my receipt images are readable and pass OCR reliably."
Description

On-screen overlays, edge detection, and haptic cues guide users to align receipts, reduce glare, and avoid blur. Automatic cropping, de-skew, and multi-shot burst selection ensure the best frame is chosen. A quality gate enforces minimum thresholds (focus, contrast, coverage) before allowing submission, with actionable tips to fix failures. Works across common device cameras, respects low-light conditions with auto-enhancement, and stores only the final accepted image to minimize footprint.

Acceptance Criteria
Framing Overlay and Haptic Alignment Cue
Given the user opens the Snap Coach camera with a receipt in view When all four receipt edges are detected with IoU ≥ 0.90 to the detected contour and each corner is ≥ 2% inside frame bounds Then the overlay changes from gray to green and a single medium-impact haptic is emitted within 150 ms (or visual checkmark if haptics unavailable) And the time from initial edge detection to alignment lock is ≤ 1.5 seconds And if glare area > 5% or the live blur metric exceeds the warning threshold, the overlay shows an amber state with a warning icon until conditions improve
Auto Crop, De‑Skew, and Burst Best‑Frame Selection
Given the user taps the shutter When a 5‑frame burst is captured within 400 ms Then the system selects the frame with highest sharpness (variance of Laplacian) and lowest glare area as the best frame And auto‑crops the image to include the receipt polygon with a uniform 2% margin on all sides And de‑skews the image so residual rotation ≤ 1° and keystone distortion ≤ 3% And the time from shutter to processed preview is ≤ 1.2 seconds on a reference mid‑tier device
Quality Gate Blocks Submission with Actionable Tips
Given a processed receipt image is ready for submission When the quality gate evaluates focus, contrast, glare, and coverage Then submission is enabled only if all pass: sharpness ≥ 150 (variance of Laplacian), RMS contrast ≥ 0.08, glare area within receipt polygon ≤ 5%, all four corners present with ≥ 2% margin from image edges And if any check fails, the submit action is disabled and up to two actionable tips appear within 300 ms indicating the top failure reasons (e.g., Hold steady, Reduce glare, Move closer, Increase light, Fill the frame, Clean lens) And re‑evaluation occurs at ≥ 5 Hz; when all checks pass, submit becomes enabled immediately
Low‑Light Auto‑Enhancement Meets Gate
Given ambient light is low (EV ≤ 6 or ISO auto‑selection ≥ 1600 indicates low light) When the user captures without flash Then auto‑enhancement applies denoise and exposure/contrast adjustments And the final image meets the same quality gate thresholds without introducing halos or > 1% highlight clipping And if the gate still fails due to light, a tip suggests enabling flash or moving to better light And enhancement adds ≤ 500 ms to processing time
Cross‑Device Camera Compatibility and Fallbacks
Given a test matrix of supported devices (iOS 15+ and Android 11+ across low/mid/high tiers) When launching the camera, using framing assist, and capturing to pass the gate Then the camera preview starts within 800 ms at P90 across the matrix And no camera API crashes or ANRs occur during a 10‑minute session And haptic cues gracefully fall back to visual indicators on devices without haptics And for identical test images, quality gate metrics vary by ≤ 5% across devices
Store Only Final Accepted Image
Given a user performs multiple retakes in a session When an image first passes the quality gate and the user confirms submission Then only the final accepted image is written to persistent storage and uploaded And all intermediate burst frames and rejected images are deleted from disk and cleared from memory within 2 seconds And the app sandbox contains at most one image file per submitted receipt after the session ends And the saved image size is ≤ 1.5 MB while preserving legibility for OCR (minimum effective 150 DPI on the cropped receipt)
Live Bank Match Preview
"As a user reviewing a captured receipt, I want to see matching bank transactions in real time so that I can confirm links and trust the automation."
Description

After OCR, the system queries linked bank feeds to display real-time candidate matches based on amount, date proximity, and merchant normalization. The UI shows top matches with confidence and allows one-tap confirm, choose another, or skip. Matches update dynamically if the user edits extracted fields, and selections are non-destructive with full audit trails. Supports multiple bank accounts, handles partial matches, and clearly explains why a match is suggested to build trust.

Acceptance Criteria
OCR Receipt Generates Real-Time Bank Match Candidates
Given a receipt is successfully OCR-processed with amount, date, and merchant extracted And at least one bank account is linked and bank feed data is available When OCR completes Then the system queries all linked bank feeds and returns candidate transactions And the first results render in the UI within 2 seconds on a 4G connection And at least the top 3 candidates (or all if fewer) are displayed sorted by descending confidence And each candidate shows amount, transaction date, normalized merchant, source account, and a confidence score (0–100%)
One‑Tap Confirm/Choose/Skip Actions
Given candidate matches are displayed When the user taps Confirm on a candidate Then the receipt is linked to that bank transaction And the UI shows success within 500 ms and collapses the match list And an Undo option is available for 10 seconds When the user taps Choose Another Then they can select a different candidate and confirm within the same view When the user taps Skip Then no link is created and the receipt remains unmatched
Dynamic Match Refresh on Field Edits
Given matches are displayed for a receipt When the user edits amount, date, or merchant fields Then the system recalculates candidates and refreshes the list within 1 second of the change And confidence scores and ordering update to reflect the new values And the user can revert the field edit to restore the prior candidate list
Matches Span Multiple Linked Accounts
Given the user has two or more bank accounts linked When candidates are generated Then transactions from all linked accounts are considered And each candidate clearly labels its source account and last sync time And the user can filter candidates by account without leaving the view
Partial and Fuzzy Matches Are Handled Gracefully
Given no exact match exists on amount and date within ±3 days When candidates are generated Then partial matches are displayed that satisfy at least two of three signals: amount within ±5%, date within ±7 days, merchant similarity ≥0.70 And partial matches are visually distinguished from exact matches And the user can expand details to see which signals matched and which did not
Transparent Why‑Suggested Explanations
Given a candidate is displayed When the user opens the Why suggested panel Then the UI lists contributing factors with their values (amount delta, date delta, merchant similarity) And shows the confidence as a percentage with a one‑sentence rationale And provides a link to learn about matching logic without leaving the flow
Non‑Destructive Linking with Full Audit Trail
Given a receipt is linked to a bank transaction via Confirm When an auditor views the receipt’s history Then the audit trail includes timestamp, user, selected transaction ID, prior candidates, and field edits that influenced matching And users can unlink and relink without data loss And all changes are logged with before/after values and are reversible
Progress Tracker & Receipt-Ready Badge
"As a new user, I want to see my progress and earn a 'receipt ready' badge so that I know I’ve completed training and unlocked benefits."
Description

A persistent progress widget tracks completion of the first five coached receipts, celebrating milestones and awarding the ‘receipt ready’ badge when criteria are met (e.g., five receipts passed quality gate and confirmed or skipped match). Badge state is stored in the user profile, appears on the dashboard, and can trigger downstream eligibility checks. Events are instrumented for funnel analytics to optimize the coaching experience.

Acceptance Criteria
Progress Widget Updates on Coached Receipt Completion
- Given a user has 0/5 completed, when a coached receipt passes the quality gate and the user either confirms a bank match or explicitly selects Skip Match, then the progress widget increments by 1 within 2 seconds and displays N/5. - Given a receipt fails the quality gate or has no match decision (neither confirmed nor skipped), then the progress does not increment and no milestone is shown. - Given a previously counted receipt is edited to fail the quality gate or is deleted before the badge is awarded, then the progress decrements within 2 seconds and the UI reflects the new count. - Given the badge has already been awarded, then subsequent edits or deletions do not reduce the displayed 5/5 completion nor remove the badge.
Receipt-Ready Badge Awarding and Storage
- Given the user completes the fifth coached receipt that meets completion rules (quality gate passed AND match confirmed OR match explicitly skipped), then award the 'receipt ready' badge exactly once within 2 seconds. - Then persist to the user profile: badge_id='receipt_ready', earned_at in ISO-8601 UTC, and earned_source='snap_coach_v1'. - Then render the badge on the dashboard on next load or within 2 seconds if already viewing. - Given the user logs out/in or switches devices, then the badge displays based on the stored profile state without requiring the user to repeat steps. - Given the badge already exists in the profile, then no duplicate awards or duplicate events are created.
Milestone Feedback in Progress Widget
- Given the user reaches 1/5, 3/5, or 5/5 completions, then show a non-blocking celebratory confirmation (icon + text or toast) for <= 2 seconds and keep primary actions available. - Given OS/device Reduce Motion is enabled, then animations are suppressed while an equivalent static confirmation is shown. - Then record a milestone_reached analytics event including user_id, milestone_value (1,3,5), and event_time.
Analytics Event Instrumentation for Coaching Funnel
- Given the user views the Snap Coach screen, then emit coach_viewed once per session with user_id, session_id, and event_time. - When a receipt passes the quality gate, then emit quality_gate_passed with receipt_id, ocr_confidence, fields_present (date, amount, merchant), and event_time. - When a bank match is decided, then emit match_confirmed or match_skipped with receipt_id, transaction_id (if applicable), reason (if skipped), and event_time. - When progress increments, then emit progress_incremented with progress_count and event_time. - When the badge is awarded, then emit badge_awarded with badge_id='receipt_ready', user_id, and event_time. - All events are delivered to the analytics pipeline with >=99% success within 60 seconds and are idempotent via a unique event_id.
Downstream Eligibility Trigger: Auto-Match Defaults
- Given the user has earned the 'receipt ready' badge, then the eligibility endpoint returns eligible=true for feature 'auto_match_defaults' within 5 seconds of award. - Given eligible=true, then the Auto-Match Defaults toggle in Settings > Matching is enabled and can be turned on/off by the user; given eligible=false, the toggle is disabled and displays explanatory helper text. - Given the badge is revoked by admin (if supported), then eligibility flips to false within 5 seconds and the UI disables the toggle without changing the current on/off state until the user next interacts.
State Persistence and Cross-Device Sync
- Given progress changes (increment or decrement), then the server-side state is updated and the progress widget rehydrates to the same value after app restart within 2 seconds on a stable connection. - Given the user completes a receipt while offline, then the local widget updates immediately and syncs the server state on reconnection without double counting the same receipt (deduped by receipt_id). - Given the user completes a receipt on Device A, then Device B reflects the updated progress within 10 seconds via push or next foreground refresh.
Auto-Match Defaults Opt-In Gate
"As a cautious user, I want a clear opt-in to enable auto-match defaults after training so that I can adopt automation when I’m ready and retain control."
Description

Upon earning the badge, present a clear, reversible opt-in to enable auto-match defaults. The modal explains what changes, provides a summary of default behaviors (auto-link on high confidence, prompt on medium, hold on low), and offers an immediate toggle in Settings. Includes safe rollback, audit logs for automated decisions, and A/B test hooks to measure adoption and downstream support impact.

Acceptance Criteria
Badge Earned Triggers Opt-In Modal
Given a user earns the “Receipt Ready” badge in Snap Coach When the badge is awarded Then display an opt-in modal within 500 ms And the modal contains a clear explanation of Auto‑Match Defaults And the modal lists behavior summary exactly: “Auto‑link on high confidence”, “Prompt on medium confidence”, “Hold on low confidence” And the modal presents primary action “Enable Auto‑Match Defaults” and secondary action “Not Now” And the modal includes a deep link labeled “Manage in Settings” And event “optin_modal_viewed” is logged with user_id, session_id, timestamp, and ab_variant
Opt-In Enablement and Settings Toggle
Given the user selects “Enable Auto‑Match Defaults” in the modal When the action is confirmed Then set Auto‑Match Defaults = On in Settings immediately And show a success toast “Auto‑Match Defaults enabled” within 2 s And create an audit entry with actor=user, action=opt_in, channel=modal, timestamp And log analytics event “optin_enabled” with user_id and ab_variant Given Settings > Auto‑Match Defaults is toggled On When the user enables it from Settings Then the same state change, toast, audit log, and analytics event occur
Reversible Opt-Out and Safe Rollback
Given Auto‑Match Defaults is On And the user navigates to Settings When the user toggles Auto‑Match Defaults Off and confirms Then prevent creation of new automated matches immediately And present an optional rollback to revert automated matches created in the last 7 days And show a pre-rollback summary count of affected matches And on completion, mark reverted matches with reason=rollback and previous_state refs in audit log And emit events “optin_disabled” and “automatches_rolled_back” with item_count and duration_ms
Automated Decision Audit Logging
Given Auto‑Match Defaults is On When the system evaluates a receipt–transaction pair Then write an audit record per decision with fields: decision{linked|prompted|held}, confidence_score(0–1), receipt_id, transaction_id, model_version, user_id, timestamp, channel=auto_match And audit records are immutable and queryable by user_id and time range And export to CSV returns requested records with 100% field coverage And p95 audit write latency ≤ 200 ms with ≥ 99% success rate And failures trigger non-blocking alerting and retried with exponential backoff up to 5 attempts
A/B Test Instrumentation and Metrics
Given eligible users earn the badge When they are shown the opt-in experience Then randomly assign control|treatment per allocation configured in remote config And persist assignment for 90 days (sticky per user) And attribute exposure, clicks, enablement, and support-contact events to the assigned variant And publish metrics within 15 minutes: opt-in rate, median time-to-enable, support ticket rate per 1k users, and 30-day retention delta by variant
Confidence-Level Behavior Enforcement
Given Auto‑Match Defaults is On When a new receipt is OCR’d and candidate matches are scored Then if confidence ≥ 0.90, auto-link within 5 s and post an activity feed entry And if 0.60 ≤ confidence < 0.90, queue a user prompt with the top match preselected And if confidence < 0.60, hold with no link and place the item in “Needs Review” And every outcome writes an audit record as specified in audit logging And prompts respect a 24-hour SLA for user notification
Dismissal, Reminders, and Accessibility
Given the user selects “Not Now” on the opt-in modal When the user returns within 14 days Then show a single inline reminder card (not a modal) in Snap Coach And do not show more than 1 reminder per 7 days And enabling Auto‑Match from any entry point suppresses all future reminders And the modal and reminders meet WCAG 2.1 AA for focus order, labels, and screen reader announcements

Habit Nudger

Friendly, flexible reminders that fit your routine—like a Friday Five cleanup or monthly mileage check. Smart snooze adjusts timing to your work patterns, bundling low‑effort wins into a single nudge so you stay consistent without notification fatigue.

Requirements

Adaptive Reminder Scheduling
"As a busy freelancer, I want reminders to arrive when I’m actually free to act so that I can clear quick tax chores without constant interruptions."
Description

Build a learning-based scheduler that times nudges to when each freelancer is most likely to complete quick tax tasks. The engine analyzes recent app sessions, transaction review times, receipt-upload patterns, and timezone to propose send windows, while honoring user-defined quiet hours and platform Do Not Disturb. It must degrade gracefully to a fixed schedule until enough data exists, and continuously update as routines shift (e.g., during busy seasons). Integrates with the Tasks service (to know available actions), Notification service (to enqueue sends), and Preferences (to respect opt-ins). Success is measured by increased nudge completion rate and reduced dismissal rates, with safeguards to cap daily/weekly nudges to prevent fatigue.

Acceptance Criteria
Cold Start Fallback Scheduling
Given a user with fewer than 5 meaningful interaction events in the last 14 days (sessions, reviews, or uploads) When the scheduler runs Then it assigns a fixed default send time window (e.g., Fridays 4–6 PM in the user’s timezone) And enforces max 1 nudge/day during fallback And transitions to adaptive scheduling automatically once ≥5 events are collected And all scheduling decisions are logged with reason "cold_start_fallback"
Personalized Send Window Learning
Given at least 5 interaction events across ≥2 signal types in the last 14 days When computing the next 7 days of send opportunities Then the engine generates ≥2 candidate windows per day scored by completion likelihood And selects the highest‑scoring permissible window within the next 48 hours And uses signals: session start times, transaction review timestamps, receipt‑upload times, and timezone And records for each decision: selected window, score, top 3 features, and observed outcome (completed, dismissed, ignored) And updates model scores at least every 24 hours And computes and stores per‑user 28‑day completion and dismissal rates
Quiet Hours, DND, and Preferences Enforcement
Given user‑defined quiet hours, channel opt‑ins, and platform Do Not Disturb When a candidate nudge time falls within any restricted period or the channel is opted out Then the nudge is not sent during the restricted period and is rescheduled to the next permissible window within 24 hours And no notification API call is made during platform DND And all suppressed or rescheduled nudges are logged with explicit suppression reason and next attempt time
Smart Snooze Timing and Bundling
Given a user snoozes a nudge for a short task When rescheduling the next attempt Then the scheduler shifts the next send into the user’s next active window based on prior engagement (e.g., most common 2‑hour block) And aggregates eligible low‑effort tasks (≤2 minutes each) into a single bundled nudge when ≥2 tasks are available within the next window And enforces a maximum of 2 snooze‑driven retries per nudge before auto‑cancel And each snooze reschedule and bundle composition is recorded in audit logs
Task and Notification Service Integration
Given Tasks service indicates at least one available action When a send window is selected Then the scheduler enqueues a notification with task identifiers and metadata to the Notification service and receives an acknowledgment (HTTP 2xx or queue ack) And on transient failure, it retries up to 3 times with exponential backoff And on permanent failure, it marks the nudge as skipped with reason and does not exceed fatigue caps And if no tasks are available, no nudge is scheduled
Nudge Fatigue Caps
Given global caps of max 2 nudges per day and 5 per rolling 7 days per user When scheduling any new nudge Then the scheduler enforces caps before enqueueing And any nudge exceeding caps is deferred to the next permissible day or dropped if stale, with reason logged And caps are evaluated per channel and in aggregate
Seasonal and Routine Shift Adaptation
Given a sustained shift (>2 hours) in median active engagement time calculated over the last 21 days vs. prior 21 days When the shift is detected Then the engine updates the learned send windows within 48 hours And future nudges use the updated windows without manual intervention And the change is versioned and auditable with timestamp, old windows, new windows, and detection score
Smart Snooze with Auto-Reschedule
"As a consultant on the go, I want to snooze a reminder and have it come back at a better time automatically so that I don’t lose the task but also avoid disruption."
Description

Enable users to snooze a nudge with context-aware options (later today, tomorrow morning, next Friday, after work) that auto-reschedule based on observed work patterns and quiet hours. The system should learn preferred deferral lengths and avoid rescheduling into blocked times. Snoozed nudges must preserve task context (e.g., which receipts/transactions were queued) and deduplicate if those tasks are completed elsewhere before the snooze fires. Deep links should return users to the original action set across devices.

Acceptance Criteria
Context-Aware Snooze Options Display
Given the user has quiet hours set to 21:00–07:00 and typical work hours 09:30–18:00 on weekdays And it is Wednesday at 16:15 local time When a Habit Nudger reminder fires and the user opens the Snooze menu Then the menu displays the options "Later today", "Tomorrow morning", "Next Friday", and "After work" with concrete scheduled times shown next to each option And no displayed option schedules within 21:00–07:00 or overlaps a blocked calendar event And selecting "Later today" schedules the nudge between 16:30–18:00 the same day And selecting "Tomorrow morning" schedules the nudge between 09:30–10:30 the next workday And selecting "After work" schedules the nudge at 18:05 the same day (or next non-quiet day if 18:05 falls in quiet hours) And selecting "Next Friday" schedules the nudge on the next Friday at the user's learned preferred hour within 09:30–11:00
Smart Snooze Auto-Schedules by Work Patterns
Given the system has at least 14 days of interaction history establishing a high-response window of 10:00–11:00 on Tue–Thu And the user's quiet hours are 21:00–07:00 When the user taps "Snooze" without specifying a time (Smart Snooze) Then the nudge is auto-rescheduled to the next 10:00–11:00 window within the next 24 hours that does not fall in quiet hours or blocked times And the scheduled time respects the user's current timezone and DST And the scheduled time is persisted server-side within 1 second and synced to all signed-in devices within 5 seconds
Adaptive Learning Defaults Snooze Option
Given at least 5 snooze actions have been taken in the last 30 days And the same option (e.g., "Tomorrow morning") was chosen in at least 60% of those actions When the user opens the Snooze menu Then that option appears first with a "Recommended" label and is the default applied by a one-tap snooze action And future recommendations update only after at least 3 additional snooze actions alter the 60% threshold
Task Context Preserved Through Snooze
Given a nudge associated with a specific action set of 11 items (8 receipt photos, 3 bank transactions) When the user snoozes the nudge and later opens it via notification at the scheduled time Then the app opens directly to the action set showing the same 11 items in the same order as when snoozed And any items completed prior to opening are marked done and excluded from the actionable list And no unrelated items created after the snooze are added to this action set view
Deduplication When Tasks Completed Before Snooze Fires
Given a snoozed nudge is linked to N actionable items And prior to the scheduled time, all N items are completed on any device When the scheduled time arrives Then no notification is delivered And an audit event "nudge_canceled_due_to_completion" is recorded with the nudge ID and timestamp And if only K<N items remain, the notification (and badge/count) reflects K items without creating a second nudge
Deep Link Restores Original Action Set Across Devices
Given a snoozed nudge is scheduled and the user is signed in on multiple devices (iOS and Android) When the user taps the notification on a device where the app is closed Then the app launches to the original action set screen linked to that nudge within 2 seconds of cold start And if authentication is required, after successful sign-in the app navigates to the same screen without losing context And the deep link consumes the nudge so that it is not presented again on other devices
Quiet Hours and Calendar Conflict Resolution
Given a snoozed nudge is scheduled for 22:30 and the user's quiet hours are 21:00–07:00 And the user's calendar has a busy event from 07:00–08:00 the next day When the system evaluates the schedule at least 5 minutes before the target time Then the nudge is rescheduled to the next free 30-minute slot after 08:00 and before 24 hours elapse (e.g., 08:15) And if no free slot exists within 24 hours, schedule the earliest available slot after 24 hours and mark the nudge as "delayed" in the audit log And no more than one automatic reschedule occurs within any 12-hour window
Task Bundling Engine (“Friday Five”)
"As a creative with limited time, I want a single bundled reminder of quick wins so that I can make meaningful tax progress in a few minutes."
Description

Create a bundling service that assembles a small set of low-effort, high-impact actions (e.g., categorize 5 uncategorized transactions, attach missing receipts, confirm last month’s mileage) into a single nudge designed for 3–5 minutes of effort. The engine must select tasks using freshness, importance, and estimated effort, avoid duplicates, and ensure all items are actionable on mobile. It should generate a concise summary with progress indicators, and mark items complete in source systems upon action. Integrates with Transactions, Receipts, and Mileage modules; exposes a scoring API for future bundles (e.g., quarterly prep).

Acceptance Criteria
Bundle size and effort fit the Friday Five target
Given a pool of eligible tasks with per-task estimated_duration_secs When the engine assembles a Friday Five bundle Then it selects 3 to 6 tasks And the total estimated effort is between 180 and 300 seconds inclusive And no single task has estimated_duration_secs > 90 And if fewer than 3 eligible tasks exist, it includes all available tasks and sets total_estimated_effort accordingly And if zero eligible tasks exist, it generates no bundle and logs reason "no_eligible_tasks"
Tasks are scored by freshness, importance, and effort
Given each candidate has freshness_days, importance_score in [0,1], and effort_secs When the engine computes scores Then score = 0.5*importance_norm + 0.3*freshness_norm + 0.2*(1 - effort_norm) And freshness_norm = min(freshness_days / 30, 1) And effort_norm = min(effort_secs / 120, 1) And candidates are ordered by score descending before selection And ties are broken by earliest created_at And selected items include score and factor breakdown in telemetry
Duplicates and ineligible tasks are excluded
Given the last 14 days of bundles and current source system states When selecting tasks for a new bundle Then exclude any item already completed in source systems And exclude any item previously bundled in the last 14 days and not yet completed And exclude any item requiring desktop-only flows or unavailable permissions on mobile And ensure no two tasks in the bundle reference the same entity_id (module + item_id)
All tasks are mobile-actionable within two taps
Given a selected task in the bundle When the user opens the nudge on a mobile device Then a deep link opens the in-app action within two taps And the action screen becomes interactive in under 1500 ms at p95 on a 4G connection And the task can be completed without leaving the app or requiring desktop authentication
Concise summary with real-time progress indicators
Given a generated Friday Five bundle When the nudge is created Then the summary displays title, task_count, and total_estimated_time_minutes rounded to the nearest minute And each task shows a label and icon in the preview list And a progress indicator shows 0–100% completion And completing a task updates the progress indicator within 2 seconds at p95 And the notification preview text is <= 180 characters
Completion syncs to source systems reliably
Given a user completes a bundled task When the action is confirmed Then the corresponding source module state is updated within 5 seconds at p95 (Transactions category set, Receipts attachment linked, Mileage trip confirmed) And the bundle item is marked complete only after source update succeeds And completion writes are idempotent; repeated retries do not duplicate effects And any failure triggers a visible retry option and logs an error event with correlation_id
Public scoring API for future bundles
Given authenticated clients with scope bundles:score When POST /v1/bundles/score is called with bundle_type and candidates[] Then the API returns HTTP 200 with a list of candidates including score, reason_codes[], and rank And supports bundle_type values friday_five and quarterly_prep And processes up to 200 candidates with p95 latency <= 300 ms And enforces rate limit of 60 requests/min per client with HTTP 429 on exceed And the response is versioned with header X-API-Version and field api_version
Reminder Preference Center
"As a user who values control, I want to set when and how I’m nudged so that reminders fit my routine without causing fatigue."
Description

Provide a centralized settings UI and backend for users to control nudge frequency, preferred days/times, channels (push, in-app, email), quiet hours, and categories (cleanup, mileage, invoice follow-up). Include presets (Light, Standard, Focused Fridays) and per-category opt-in/out. Preferences must sync across devices, version for rollback, and be enforceable by the scheduling service at send time. Include consent capture for behavioral learning and a simple way to pause all nudges.

Acceptance Criteria
Configure Core Reminder Preferences
Given an authenticated user opens the Reminder Preference Center When the user sets frequency to Weekly, selects Friday, sets time to 17:00 in their account time zone, and selects channels Push and Email, and saves Then the backend persists the exact values and returns 200 OK with a new preference version ID And subsequent GET /preferences returns the same values and version ID within 1 second And the scheduling service computes and stores the next send for the upcoming Friday at 17:00 in the user’s account time zone within 10 seconds of save And a test nudge triggered at send time is delivered only to the selected channels and never to unselected channels And if the user changes the time to 16:00 after scheduling but before send, the nudge is sent at 16:00 using the latest saved preferences
Quiet Hours Enforcement with Time Zone and DST
Given the user sets Quiet Hours from 21:00 to 08:00 in their account time zone and saves When a nudge is scheduled for 22:30 local time Then the scheduling service does not send during quiet hours and reschedules to the next allowed time outside quiet hours while preserving the configured frequency cadence And if the next allowed slot is the following day, the send occurs at the user’s preferred time that day And during DST transitions, quiet hours are enforced by wall‑clock time (21:00–08:00) in the account time zone without duplicate or missing sends And if the device time zone differs from the account time zone, enforcement uses the account time zone And changes to Quiet Hours up to 1 minute before send are enforced at send time
Category Opt‑In/Out and Preset Application
Given the user has category toggles for Cleanup, Mileage, and Invoice Follow‑Up When the user opts out of Mileage and saves Then no Mileage nudges are scheduled or sent, and existing queued Mileage nudges are cancelled within 10 seconds When the user applies the preset “Focused Fridays” and confirms Then the UI displays a preview of the resulting schedule and categories, and the saved preferences match the preset specification And a “Custom” label appears if the user changes any setting after applying a preset And reverting to a previously saved version restores the prior category and preset state
Pause All Nudges and Auto‑Resume
Given a global Pause All toggle with options 24 hours, 7 days, or Until date When the user enables Pause All for 7 days and confirms Then the backend records a pause window with start/end timestamps and returns 200 OK And no nudges of any category are sent via any channel during the pause window And the scheduling service holds or skips sends and recalculates the next eligible send after the pause ends And if the user taps Resume Now, the pause is removed immediately and normal scheduling resumes within 10 seconds
Cross‑Device Sync and Conflict Resolution
Given the user is logged in on Device A and Device B When the user saves preference changes on Device A Then Device B receives and displays the updated preferences within 10 seconds via backend sync When conflicting edits occur within 5 seconds (A saves v12, B saves v11) Then last‑write‑wins by version ID; the older save is rejected with 409 Conflict and the UI prompts to refresh with the latest version And both devices display the same version and values after resolution And scheduling uses the latest committed version at send time
Preference Versioning and Rollback
Given each successful save creates a new immutable version with timestamp and diff When the user opens Version History and selects a prior version to restore Then the system creates a new head version identical to the selected version and returns 200 OK with the new version ID And the scheduling service updates to enforce the restored preferences within 30 seconds And an audit trail records who performed the rollback, from-version, to-version, and reason
Consent Capture for Behavioral Learning
Given a setting “Use my activity to improve reminders (behavioral learning)” is present and off by default When the user opts in and confirms after seeing a plain‑language notice with a link to details Then consent is recorded with user ID, timestamp, policy version, and method, and exposed via GET /consents And withdrawing consent updates the record, halts further behavioral learning for that user, and schedules deletion of derived learning signals within 24 hours And nudges continue to function without learning when consent is off
Cross-Platform Notification Delivery with Deep Links
"As a mobile-first user, I want reminders that open directly to the action I need to take so that I can finish quickly without hunting through menus."
Description

Implement reliable, idempotent delivery of nudges via mobile push (iOS/Android) and in-app inbox, with optional email fallback when push permissions are absent. Each nudge includes deep links to specific quick actions (e.g., open ‘Categorize 5’), supports rich content (summaries, counts), and carries a stable deduplication key to prevent duplicate alerts across channels. Track delivery, open, and action events for analytics and scheduling feedback. Handle token rotation, bounce cleanup, and GDPR/CCPA-compliant data retention.

Acceptance Criteria
Mobile Push Delivery with Cross-Channel Deduplication
Given a nudge N with dedup_key K and both push and inbox channels enabled When the send job executes Then at most one user-facing alert is presented (push OR inbox alert) and exactly one inbox item is created Given any retry or duplicate send request with the same dedup_key K within the configured dedup window W When delivery is attempted Then no additional alerts or inbox items are created and the attempt is logged as suppressed_by_dedup=true Given the dedup window W has elapsed When a nudge with the same dedup_key K is sent Then the nudge is delivered as a fresh message Given partial failures (e.g., push provider timeout) and automatic retries up to R times When delivery completes Then idempotency is preserved such that the user experiences at most one alert and one inbox item for dedup_key K
Deep Link Opens Specific Quick Action on iOS and Android
Given a delivered nudge that includes deep_link targeting action_id=categorize_5 with nudge_id and dedup_key parameters When the user taps the push on iOS or Android from background or terminated state Then the app opens directly to the Quick Action "Categorize 5" with the correct preloaded count and shows the target screen within 2 seconds of foreground Given the same nudge is opened from the in-app inbox When the user taps Open Then the same deep link target is executed and the same screen is shown with identical context Given the app is not installed When the user taps the push Then the system redirects to the appropriate App Store/Play Store listing URL without error
In-App Inbox Delivery and Read-State Sync
Given push is disabled or an alert was suppressed by deduplication When the nudge is created Then exactly one inbox item appears within 10 seconds with title, summary, count, and deep link intact Given the user opens the nudge from any channel (push, inbox, or email) When the content screen is shown Then the corresponding inbox item is marked read and the read state syncs across the user’s devices within 5 seconds Given the user archives or deletes the inbox item When a retry with the same dedup_key K occurs within window W Then no new inbox item or alert is created
Email Fallback When Push Permissions Are Absent
Given the user has not granted push permissions on any device and has a verified email When a nudge is triggered Then an email is sent within 2 minutes containing equivalent title/summary/count and a deep link URL with nudge_id and dedup_key parameters Given the user grants push permission before the send window elapses When the nudge is sent Then the push notification is sent and the email is not sent Given the email hard-bounces When the provider returns a hard-bounce event Then the email channel is auto-suppressed for the user within 24 hours and the bounce is recorded
Rich Content Payload Validation and Rendering
Given a nudge payload with title (required), summary (optional, <= 120 chars), count (integer >= 0), and optional thumbnail_url When the payload is validated server-side Then invalid fields (missing title, non-integer count, overlong summary, invalid URL) are rejected with a descriptive error and are not sent Given a valid payload When rendered as push on iOS and Android Then content is displayed using platform-specific limits with ellipsis truncation and the count is visible in subtitle/badge Given the same payload When rendered in the in-app inbox and email Then title, summary, and count match exactly and missing optional fields degrade gracefully without layout breakage
Event Tracking for Delivery, Open, and Action
Given any delivery attempt across channels When events are emitted Then the system records delivery_attempted, delivered (per channel), open (per channel), action_performed, and suppress_dedup with fields {event_id, user_id, nudge_id, dedup_key, channel, timestamp, device_id(optional), campaign_id(optional)} Given events are produced When analytics ingestion runs Then 99% of events are available for query within 2 minutes and duplicates with the same event_id are ignored Given an action deep link is executed When the target screen loads Then an action_performed event with action_id and source_channel is emitted exactly once
Token Rotation, Bounce Cleanup, and GDPR/CCPA Retention
Given a push send returns APNS status 410 or FCM NotRegistered When processing the response Then the device token is marked invalid immediately and a new token is requested on next app launch; invalid tokens are purged within 24 hours Given email soft-bounce responses When retries occur Then up to 3 retries are attempted with exponential backoff; upon hard-bounce the address is auto-suppressed within 24 hours Given GDPR/CCPA obligations When a user requests deletion Then all nudge-related PII (tokens, email address in messaging tables, payloads, and analytics events linked to user_id) are deleted or irreversibly anonymized within 30 days and exports are provided within 15 days; routine retention keeps detailed payloads for no more than 13 months
Nudge Outcome Tracking and Feedback Loop
"As a user who cares about relevance, I want to give quick feedback on nudges so that future reminders are more useful to me."
Description

Capture structured events for each nudge (delivered, viewed, snoozed, acted, dismissed) and attribute outcomes to the tasks included. Provide lightweight user feedback controls (“more like this,” “less often,” “not relevant”) that adjust future scheduling, bundling, and category selection. Expose metrics for completion rate, time-to-action, and fatigue indicators to product analytics and the adaptive scheduler. Ensure privacy-safe aggregation and guardrails to prevent overfitting on sparse data.

Acceptance Criteria
Nudge Event Lifecycle Capture
Given a nudge is successfully delivered via any channel, When delivery confirmation is received, Then record a single delivered event with fields {nudge_id, schedule_id, user_id_hash, channel, task_ids[], timestamp_utc} and delivery_status=success. Given a delivered nudge, When the user views/opens it, Then record a viewed event once per nudge occurrence with a 60-minute deduplication window per user and nudge_id. Given a delivered nudge, When the user taps Snooze, Then record a snoozed event with fields {snooze_duration, snooze_reason=smart|manual}. Given a delivered nudge, When the user dismisses it without action, Then record a dismissed event with dismissal_surface and timestamp_utc. Given a delivered nudge, When the user completes any included task via deep link or in-app within 48 hours, Then record an acted event once per task_id and nudge occurrence with action_source and timestamp_utc. Then 95% of all events are available to the analytics pipeline within 15 minutes and pass schema validation with 0% required-field nulls.
Outcome Attribution to Included Tasks
Given a nudge occurrence includes task_ids [t1..tn], When the user completes task t_k within 48 hours of the most recent viewed (or delivered if not viewed) event, Then attribute the action to that nudge_id and task_id with attribution_window=48h and attribution_source=nudge. Given multiple nudges include the same task within the window, When attribution is computed, Then attribute to the most recent viewed event (tie-breaker: most recent delivered) and do not double count across nudges. Given a task is completed multiple times, When metrics are aggregated, Then count only the first completion per task_id per nudge occurrence. Then attributed outcomes appear in the Outcomes API and are reflected in completion_rate metrics within 30 minutes p95.
Feedback Control — More Like This
Given an in-app nudge is displayed, Then present a More like this control that is focusable, screen-reader labeled, and visible without scrolling. When the user selects More like this, Then emit a feedback event {type=more_like_this, nudge_id, category, timestamp_utc}. Then increase the scheduler weight for the nudge’s category for this user by at least +0.1 (capped at 1.0) and persist within 5 minutes p95. Then within the next scheduling cycle, If eligible inventory exists, Then the probability of this category being included is higher than prior by ≥10%, and at least one nudge in the next 7 days includes this category. Then show an in-context confirmation within 2 seconds of selection.
Feedback Controls — Less Often and Not Relevant
Given an in-app nudge is displayed, Then present Less often and Not relevant controls adjacent to the content and accessible via keyboard and screen reader. When the user selects Less often, Then emit a feedback event {type=less_often, nudge_id, category} and reduce the per-user cadence for this category by ≥30% and enforce a minimum interval of ≥7 days between same-category nudges, effective next scheduling run. When the user selects Not relevant, Then emit a feedback event {type=not_relevant, nudge_id, category} and suppress this category for the user for 30 days and decrease the category weight by ≥0.5 (floored at 0), effective next scheduling run. Then both selections display confirmation within 2 seconds and persist changes even if the app is restarted.
Metrics Exposure to Analytics and Scheduler
Given nudge lifecycle and outcome events are ingested, When aggregation runs every 15 minutes, Then compute and expose the following per nudge_type and category: completion_rate, median_time_to_action, dismiss_rate, snooze_rate, multi_snooze_streak>=3 rate, and viewed_to_action_rate. Then publish aggregates to the product analytics topic/warehouse and expose a read API to the adaptive scheduler, both updating within 30 minutes p95 of event arrival. Then metric definitions are consistent across surfaces such that analytics validation queries differ from pipeline aggregates by <2% absolute. Then the API returns data with versioned schemas and rejects requests lacking required parameters with 4xx responses.
Privacy-Safe Aggregation and Data Minimization
Given event capture is active, Then do not include PII in events (no names, emails, exact locations); use user_id_hash and coarse device_type only. When publishing aggregates to analytics, Then enforce k-anonymity with k>=20 per reported segment and suppress segments below threshold. Then apply differential privacy noise with daily privacy budget epsilon<=1.0 to cohort-level rates exposed outside the scheduler. Then honor user analytics opt-out by excluding their events from aggregates and per-user scheduler signals within 24 hours. Then encrypt data in transit and at rest and limit retention to 13 months, with access logging enabled for all reads of raw events and aggregates.
Adaptive Scheduler Guardrails Against Sparse-Data Overfitting
Given the scheduler receives feedback and metrics, When updating per-user or per-category parameters, Then require minimum evidence: >=200 deliveries and >=50 acted events for the segment in the last 14 days (or >=500 deliveries if acted<50) and observed uplift >=5% vs a 5–10% holdout with p<=0.1. Then cap parameter change magnitude to <=25% per update and enforce a 14-day cooldown before the same parameter can be updated again. Then maintain a persistent 5–10% holdout group unaffected by feedback-based adjustments for measurement validity. Then automatically rollback to the prior parameter set if completion_rate degrades by >5% for 48 consecutive hours post-update. Then for users with <5 historical interactions in a category, apply default scheduling and honor only hard suppressions (e.g., Not relevant) until the threshold is met.
Behavioral Data Privacy and Controls
"As a privacy-conscious freelancer, I want to understand and control how my behavior data shapes reminders so that I can use the feature confidently."
Description

Offer transparent controls over behavioral data used for nudging (what is collected, why, and for how long), with the ability to opt out of learning while retaining basic scheduling. Provide an in-app explainer for ‘Why this nudge now?,’ data export/deletion options, and enforcement of data minimization and encryption-at-rest/in-transit. Integrate with existing consent and privacy modules to honor user choices across all Habit Nudger services and channels.

Acceptance Criteria
Opt-Out of Behavioral Learning While Retaining Scheduling
Given the user opens Settings > Privacy > Habit Nudger When the user toggles "Behavioral learning" to Off and confirms Then the system stops collecting new behavioral signals for learning within 60 seconds And future nudges use only static schedules (no smart timing or bundling) And existing behavioral models stop querying this user's data And the Schedule screen displays a "Learning off" indicator for affected nudges And the change is synchronized to all devices and channels within 5 minutes And an auditable record of the consent change is stored with timestamp, user ID, and device
In-App "Why This Nudge Now?" Explainer
Given a Habit Nudger notification is delivered When the user taps "Why this nudge?" Then an explainer panel opens within 1 second And it lists the trigger source(s) and features used, the last model update time, and a plain-language reason And it provides a Manage Data & Privacy link to the relevant settings And if behavioral learning is Off, the explainer states "Based on your schedule only" And if insufficient data exists, the explainer states "We didn't use behavioral data" And no third-party identifiers or other users' data are displayed
Behavioral Data Export (Self-Service)
Given the user navigates to Settings > Privacy > Export data When the user requests export of Habit Nudger behavioral data and verifies email Then the system prepares a machine-readable JSON and CSV bundle containing events, nudge history, derived features, and retention metadata And the export is available via a signed download link within 15 minutes And the link expires after 7 days and can be revoked immediately by the user And the export includes a data dictionary and schema version And an audit log entry is created with request time, completion time, and requester
Behavioral Data Deletion and Propagation
Given the user navigates to Settings > Privacy > Delete behavioral data When the user confirms deletion for Habit Nudger Then behavioral events, derived features, and nudge history tied to the user are queued for deletion immediately And production data stores reflect deletion within 24 hours And encrypted backups and analytics stores complete hard deletion within 30 days And all Habit Nudger services stop referencing deleted data and fall back to schedule-only nudges And the user receives an in-app and email confirmation upon completion And an audit trail captures request, scope, and completion status
Data Minimization and Retention Enforcement
Given Habit Nudger services emit behavioral events When events are ingested Then only events on the approved allowlist (documented schema) are stored And disallowed or extraneous fields are dropped and logged as violations And default retention for behavioral data is 13 months and configurable per user between 3 and 24 months And data older than the configured retention is automatically purged daily And the user can view and change retention within the allowed range in Settings And a monthly report summarizes purge counts and any violations
Encryption at Rest and In Transit
Given Habit Nudger stores or transmits behavioral data When data is written to storage or sent between services or to devices Then data at rest is encrypted with AES-256 (or cloud-provider equivalent) and keys are managed by KMS with rotation every 90 days or less And data in transit uses TLS 1.2+ with HSTS enabled; plaintext connections are refused or redirected And mobile local caches use OS secure keystores; if unavailable, behavioral data is not cached locally And database snapshots, logs containing behavioral data, and analytics extracts are encrypted And quarterly automated scans verify protocol/cipher compliance and report failures
Consent Synchronization Across Modules and Channels
Given the user updates consent in the global Consent & Privacy Center When the consent change is saved Then Habit Nudger honors the change across mobile push, email, and SMS within 5 minutes And scheduled nudges that would violate consent are suppressed with a deny reason recorded And the consent state in Habit Nudger UI updates on next app foreground or within 5 minutes And a reconciliation job detects and resolves mismatches, alerting operations on failure And integration tests validate end-to-end propagation on every release

Packet Preview

A live, plain‑English view of your upcoming quarterly packet with a completeness bar. Missing items are flagged with one‑tap fixes (request a W‑9, add a receipt, confirm a category), so you always know what’s left and never scramble at filing time.

Requirements

Live Packet Composition Engine
"As a freelancer, I want a live preview of my quarterly packet so that I can see exactly what will be filed and what’s still missing."
Description

Assemble a real-time preview of the user’s upcoming quarterly tax packet by aggregating income, expense, and document data across invoices, bank feeds, and receipt OCR. Apply quarter cutoffs, business rules, and de-duplication to produce an “as of” packet state with sectioned summaries (income, deductible expenses, mileage, estimated payments, documents). Support incremental recalculation on data changes, maintain a versioned snapshot for auditability, and surface data lineage links back to source items. Handle edge cases (multi-currency, split transactions, refunds) and fail gracefully with clear error states. Expose a performant API endpoint (<200ms P95 for cached views) for the UI and enforce privacy/role checks.

Acceptance Criteria
Quarterly Cutoffs and Sectioned Summaries
Given a user has income, expense, mileage, estimated payment, and document items dated across multiple quarters And the user’s tax profile timezone determines quarter boundaries When the engine assembles the packet preview for a specified quarter as of now Then only items whose effective dates fall within the quarter are included per business rules And sectioned summaries are produced for Income, Deductible Expenses, Mileage, Estimated Payments, and Documents And each section’s total equals the sum of its included line items to the cent And the packet includes quarter label, asOf timestamp, and per‑section item counts And items outside the quarter are excluded from all section totals
De-duplication Across Sources
Given items representing the same real‑world transaction exist across invoices, bank feeds, and receipt OCR When the engine assembles the packet preview Then duplicates are merged into a single canonical line item without double‑counting totals And the canonical item retains references to all merged source IDs and systems And de‑duplication outcomes are deterministic given identical inputs And an audit entry records the de‑dup rule and participating source items And no section contains more than one representation of the same transaction
Incremental Recalculation on Data Mutations
Given a packet state version Vn exists for the current quarter And a single source item relevant to the quarter is created, updated, or deleted When the engine processes the change Then only affected sections are recalculated and unchanged sections retain their prior revisionIds And a new packet state Vn+1 is emitted with updated totals and asOf timestamp And lineage and de‑dup decisions for unaffected items remain unchanged And processing logs indicate incremental recomputation (not full rebuild)
Versioned Snapshot and Data Lineage
Given the packet state changes materially When the engine persists the new state Then an immutable snapshot is stored with versionId, createdAt, quarter, and content checksum And retrieving any versionId returns byte‑identical content to when it was created And every line item includes lineage: sourceSystem, sourceId, linkUri (or equivalent), and transformation notes And lineage links are navigable to the underlying source items subject to access controls
Edge Cases: Multi-currency, Splits, and Refunds
Given the dataset includes multiple currencies, split transactions, and refunds/chargebacks When the engine assembles the packet preview Then all amounts are converted to the user’s base currency using the defined FX rule and a rate timestamp is recorded And split transactions are represented as multiple line items whose amounts sum exactly to the original gross amount And refunds/chargebacks are netted against the original items when linkable, or represented as negative adjustments with references And section totals remain mathematically consistent after conversions, splits, and refunds
API Endpoint Performance and Caching
Given GET /api/packets/preview?quarter=YYYY-Q# is requested with a warm cache When 1000 or more requests are executed under nominal load Then the response P95 latency is <200ms and responses include ETag and Cache-Control headers And the payload contains the asOf timestamp, sectioned summaries, and versionId And the endpoint returns 304 Not Modified when the client presents a valid ETag And uncached responses remain functionally correct and include an indicator of cache status
Privacy/Role Access Controls and Graceful Failure
Given a caller lacks permission to view the tenant’s packet preview When the API is invoked Then the request is denied (403) without leaking tenant identifiers or PII, and the access attempt is audit‑logged with actorId and decision Given a dependency is unavailable or a section computation fails When assembling the preview Then the API returns the last known snapshot with isStale=true and includes per‑section error codes and messages And no partial or unauthorized data from other tenants is included in any response
Completeness Bar & Section Breakdown
"As a mobile-first user, I want a clear completeness bar with section details so that I know what remains to reach 100% before the deadline."
Description

Compute and display a dynamic completeness score for the upcoming packet, broken down by sections (income captured, expenses categorized, receipts attached, documents collected). Define explicit validation rules per section (e.g., all transactions categorized, receipts attached for expenses over threshold, W‑9/W‑8 for paid contractors, bank feed synced within 72 hours). Provide tooltips that explain how the score is calculated and what is required to reach 100%. Update in real time as issues are resolved, and persist the underlying checklist for analytics and historical comparison across quarters.

Acceptance Criteria
Overall and Section Completeness Scores
Given a user opens Packet Preview for the current quarter When the data loads Then the UI displays an Overall Completeness percentage (0–100% with one decimal) and four section percentages labeled Income Captured, Expenses Categorized, Receipts Attached, and Documents Collected And each section percentage equals round(resolved_items_in_section / total_items_in_section * 100, 1) And the overall percentage equals round(total_resolved_items_across_all_sections / total_items_across_all_sections * 100, 1) And the counts resolved/total for each section are available to the UI for display and analytics
Expenses Categorized and Receipts Threshold Enforcement
Given expense transactions exist for the quarter When completeness is computed Then the Expenses Categorized section is 100% only if every expense transaction is assigned to a tax category or explicitly marked Excluded And any expense with amount greater than the configured receipt-required threshold is linked to a receipt file OR is explicitly marked Missing Receipt with a justification note And a linked receipt must have a readable date and amount that match the transaction within ±7 days and ±$1.00 (or configured tolerance) And unresolved items for uncategorized expenses or missing/invalid receipts are listed with one-tap fixes
Income Captured and Bank Feed Freshness
Given invoices and bank feed credits exist for the quarter When completeness is computed Then the Income Captured section is 100% only if all invoices marked Paid have an associated payment record or matched bank credit, and all bank income credits are matched to an invoice or explicitly confirmed as Other Income/Excluded And if the last successful bank feed sync is older than 72 hours, create an unresolved item Sync bank feed and cap Income Captured at <100% until a new sync completes And unmatched or ambiguous income items are surfaced with counts and one-tap resolve actions (Match, Confirm, Exclude)
Contractor Documents Collected (W‑9/W‑8)
Given one or more contractors were paid during the quarter When completeness is computed Then the Documents Collected section is 100% only if each paid contractor has a valid tax form on file (W‑9 for U.S. persons or W‑8 for non‑U.S.) captured prior to or on the first payment date for the quarter And missing or outdated forms generate unresolved items with a one‑tap Request Form action and track request status (Requested, Received, Failed) And receipt of a valid form automatically resolves the item and updates the section percentage
Real‑Time Score Updates on Resolution
Given the Packet Preview is open When a user resolves an unresolved item (e.g., categorizes a transaction, attaches a receipt, requests/receives a W‑9, or completes a bank sync) Then the affected section percentage and overall completeness update within 2 seconds of backend confirmation without requiring a page refresh And multiple rapid resolutions coalesce so that the UI reflects the latest server state within 5 seconds with no intermediate regressions And the resolved item disappears from the unresolved list and the remaining counts decrement accordingly
Tooltips Explain Calculation and Next Steps
Given a user hovers or taps the info icon on the Overall Completeness bar or any section card When the tooltip is shown Then it displays the calculation formula as Resolved/Total items and the resulting percentage (to one decimal) And it lists up to the top 3 unresolved items for that scope with direct action links (e.g., Add Receipt, Categorize, Request W‑9, Sync Bank) And it states explicitly what is required to reach 100% (e.g., 2 receipts missing; 1 contractor form outstanding; bank sync older than 72h) And closing the tooltip or taking an action from it behaves consistently on web and mobile
Checklist Persistence and Historical Comparison
Given checklist items are generated for the quarter When changes occur (create, resolve, reopen) Then each item state change is persisted with a unique ID, quarter, section, timestamps, and actor for analytics And the system can retrieve the full checklist and daily rollups for the current and at least the previous 4 quarters via API And after a quarter is marked Filed, its final completeness snapshot is immutable and available for historical comparison in the Packet Preview trend view
Missing Items Detection & One‑Tap Fixes
"As a freelancer, I want missing items flagged with one‑tap fixes so that I can resolve them quickly and avoid end‑of‑quarter scrambling."
Description

Identify gaps that block packet completion (uncategorized transactions, missing receipts, unlinked invoices, duplicate detections, missing contractor tax forms) and surface them inline in the preview. Provide one‑tap actions to resolve each item: request a W‑9 via email/text with prefilled template, attach or capture a receipt, confirm or change a category, link a bank feed, or dismiss with reason. Pre-fill context (amount, date, vendor) and confirm with a single tap, updating the packet state instantly. Record an audit trail of actions, support undo within 30 minutes, and throttle outbound requests to prevent spam.

Acceptance Criteria
Inline Detection of Missing Items in Packet Preview
Given a user opens Packet Preview with connected data sources When the system analyzes current invoices, bank feeds, and receipts Then it surfaces missing items grouped by type: uncategorized transactions, missing receipts, unlinked invoices, suspected duplicates, and missing contractor W‑9s And each item displays amount, date, and vendor/payee context And each item shows a single one‑tap fix CTA appropriate to the type And each item provides a Dismiss option that requires selecting or entering a reason And group counts and a total outstanding count are shown And the list refreshes to reflect any fix/dismiss within 2 seconds
One‑Tap W‑9 Request with Prefilled Template and Throttling
Given a contractor is flagged as missing a W‑9 in Packet Preview When the user taps Request W‑9 Then a dialog offers Email and Text options with a prefilled message template including contractor name and tax year And the user can confirm send with a single tap And the item status changes to Requested and is removed from blocking totals within 2 seconds And outbound W‑9 requests are limited to a maximum of 2 per contractor per 24 hours; attempts beyond the limit are blocked with a clear message And the action is recorded for audit with timestamp, channel, actor, and contractor id
One‑Tap Attach or Capture Receipt
Given a transaction is flagged Missing Receipt in Packet Preview When the user taps Attach/Capture Receipt Then the app opens camera and file picker options And upon selecting or capturing an image, the system extracts amount, date, and vendor and pre‑fills the confirmation sheet And the user confirms with a single tap Then the receipt is attached to the transaction, the missing item is removed, and the completeness bar updates within 2 seconds
One‑Tap Confirm or Change Category
Given a transaction is flagged Uncategorized in Packet Preview When the user taps Categorize Then a bottom sheet shows a predicted category, the top 3 alternatives, and search When the user confirms a category with one tap Then the transaction is updated, the item is removed from Missing Items, and the completeness bar updates within 2 seconds And the user can Dismiss with Reason instead of categorizing, which removes the item and records the reason
Link Bank Feed from Packet Preview
Given Packet Preview shows Unlinked Invoices or missing transactions due to an unlinked bank account When the user taps Link Bank Feed Then the secure aggregator flow opens and supports successful OAuth/credentials linking And on successful link, an initial sync starts immediately And Missing Items are re‑evaluated and the list/completeness bar updates within 60 seconds of first data arrival And on failure or user cancellation, a clear error/state is shown and the item remains unresolved
Duplicate Detection and One‑Tap Resolution
Given the system identifies suspected duplicates across invoices, bank transactions, and receipts When Packet Preview loads or new data syncs Then suspected duplicates are listed with confidence score and details (amount, date, vendor) And each item provides one‑tap Merge and Not a Duplicate actions When the user taps Merge Then records are combined into a single entry with attachments preserved and sources noted, the item is removed, and completeness updates within 2 seconds When the user taps Not a Duplicate Then the item is dismissed with reason and not re‑flagged for the same pair
Undo Within 30 Minutes and Full Audit Trail
Given any fix or dismiss action is performed from Packet Preview Then an Undo option is shown for 30 minutes from the action time When the user taps Undo within the window Then the system restores the prior state, recalculates completeness within 2 seconds, and records the undo event After 30 minutes, Undo is no longer available and cannot be invoked via API And the audit trail records action type, actor, timestamp, target item id, and before/after values; entries are immutable
Plain‑English Narrative & Annotations
"As a user, I want plain‑English explanations of my packet so that I understand and trust what will be filed."
Description

Render a human-readable summary for each packet section that explains what’s included, why, and any assumptions (e.g., mileage rate applied, recurring categories). Provide expandable annotations linking to source documents and IRS references, with toggles to show/hide technical terms. Ensure language is concise, consistent in tone, and localized for US English initially, with hooks for future i18n. Include inline warnings for potential compliance concerns (e.g., mixed-use purchases) and disclaimers where user confirmation is needed.

Acceptance Criteria
User reads per-section narrative in Packet Preview (mobile)
Given a mobile user opens Packet Preview and taps the Expenses section When the section loads Then a plain-English narrative is rendered within 500 ms after data is ready And the narrative length is between 80 and 180 words And the Flesch-Kincaid Grade Level is <= 9 And the first two sentences state what items are included/excluded and why And totals are displayed with USD currency formatting and item counts And no undefined acronyms or tax codes appear when Technical Terms = Off And tone is second-person, active voice per style guide
Assumptions surfaced for mileage rate and recurring categories
Given the packet contains mileage entries and recurring expense rules When the user views the Mileage or Expenses summary subsection Then an Assumptions list is shown with the mileage rate (e.g., $0.67/mi) and applicable tax year And recurring category rules are summarized with rule name and last modified date And each assumption shows its data source (IRS reference or user rule) and last sync timestamp And the user can expand to view calculation basis and affected transactions And a Change action navigates to the relevant settings screen And all amounts reconcile with packet computations within $0.01
Annotations expand to source docs and IRS references
Given figures in the narrative are derived from invoices, bank feeds, or receipts When the user taps Show annotations Then inline markers appear next to each figure in the narrative And tapping a marker opens a panel listing at least one linked source document with thumbnail, date, and amount And an IRS reference link is shown where applicable and opens to the relevant section in an in-app webview And any broken link is flagged with a retry option and does not crash the view And the expanded/collapsed annotation state persists per section during the current session
Toggle technical terms and definitions
Given the Technical Terms toggle is Off by default for en-US When the user toggles Technical Terms On Then tax terms (e.g., Section 179, safe harbor) appear inline with tappable definitions or footnotes And toggling Off replaces terms with plain-language synonyms And the toggle state persists across sections and app restarts And the toggle has accessible labels and is announced correctly by screen readers And the narrative re-renders within 300 ms of the toggle change
Inline warnings for mixed-use and other compliance risks
Given the system detects mixed-use purchases or threshold anomalies in the section When the narrative is rendered Then an inline warning badge appears adjacent to the relevant statement And the warning text includes risk type, affected amount, and recommended next action And a one-tap Fix action navigates to confirm, split, or recategorize flow And warnings have severity (info/warn) and are deduplicated per section And dismissing a warning requires explicit confirmation and is audit logged with timestamp and user ID
US English localization and i18n hooks
Given the device locale is en-US When the narrative and annotations render Then dates use MM/DD/YYYY and numbers use US separators and USD formatting And spelling and vocabulary are US English And all user-facing strings are sourced from localization keys with placeholders (no hardcoded strings) And switching to a pseudo-locale expands strings without truncation or layout overflow And right-to-left checks show mirrored layouts when an RTL test locale is enabled
Disclaimers and user confirmations gate risky statements
Given the narrative includes statements requiring user confirmation (e.g., home office percent, vehicle business use) When such statements are present Then a disclaimer line with a required confirmation checkbox is displayed And Share Packet and Download actions remain disabled until confirmation is checked And tapping the disclaimer opens details with IRS guidance links And the confirmation is recorded with timestamp, user ID, and packet version And any subsequent data change that affects the statement resets the confirmation and re-prompts the user
Real‑Time Data Sync & Confidence Indicators
"As a user, I want the preview to reflect my latest data with confidence indicators so that I know what needs my review versus what I can trust automatically."
Description

Keep the preview fresh by subscribing to ingestion events (webhooks) and scheduled syncs for bank feeds, invoicing apps, and receipt OCR. Display a freshness timestamp and per-item confidence scores from extraction/classification models. Gate auto-application of low-confidence items behind a quick confirm flow and surface review queues for anything below threshold. Implement backoff and retry policies, handle provider rate limits, and ensure eventual consistency without blocking the UI.

Acceptance Criteria
Webhook and Scheduled Sync Update
Given registered webhooks for bank feeds, invoicing apps, and receipt OCR, When a valid event is received, Then the ingestion job starts within 5 seconds of receipt and acknowledges with 2xx within 1 second. Given scheduled sync windows, When no webhook event is received for 15 minutes, Then a scheduled sync runs and fetches deltas from all providers. Then updates are applied idempotently using provider event IDs; duplicates are not created. Then the Packet Preview reflects new/changed items within 10 seconds after ingestion completes. And the UI remains interactive during all sync operations; no blocking overlay is shown.
Freshness Timestamp Accuracy
Given a successful ingestion batch completion timestamp T, When the Packet Preview is visible, Then a "Last refreshed" timestamp is displayed in local time and an ISO 8601 value in a tooltip, equal to T ± 1 second. Then a relative time label (e.g., "2m ago") auto-updates at least every 60 seconds without a full page refresh. When ingestion is in progress, Then the timestamp area shows "Syncing…" with a spinner and does not display a stale time value. When read by assistive technology, Then the freshness region uses an ARIA live="polite" announcement and meets contrast ratio ≥ 4.5:1.
Per-Item Confidence Scores Display
Given an extracted or classified item, When shown in Packet Preview, Then a numeric confidence score 0.0–100.0 is displayed with one decimal place. Then the score color maps to thresholds: green ≥ 90.0, amber 70.0–89.9, red < 70.0. When the user taps the score, Then a tooltip/modal shows the model name and version and the top contributing factors (max 3) plus the raw parsed fields. When sorting by confidence is selected, Then items are ordered by score descending; ties preserve original order. Given an item missing a score, Then the UI shows "N/A" and flags the item for review.
Low-Confidence Auto-Apply Gate
Given an auto-apply threshold default of 80.0% (configurable 50.0–95.0%), When an item's confidence score is below the threshold, Then it is not auto-applied to the packet. Then the item shows a one-tap "Confirm & Apply" action opening a prefilled confirm sheet. When the user confirms, Then the item is applied and labeled "Applied (user-confirmed)" and an audit log entry records user ID, timestamp, and values changed. When the user dismisses or edits instead of confirming, Then the item is moved to the Review Queue with reason "Below threshold". Given an item at or above threshold, Then it is auto-applied and labeled "Auto-applied" with an audit log entry.
Review Queue Surfacing and Bulk Actions
Given one or more items below threshold or with missing required fields, When Packet Preview loads, Then a Review Queue badge displays the total count and opens the queue on tap. Then newly ingested low-confidence items appear in the queue within 5 seconds of ingestion completion. When performing bulk actions, Then the user can select 2–100 items and Confirm, Assign Category, or Request W‑9 in a single action, and the queue count updates within 2 seconds. Given all items in the queue are resolved, Then the badge hides automatically within 2 seconds.
Backoff, Retry, and Rate Limit Handling
Given a provider returns HTTP 429 with a Retry-After header value R, When syncing, Then the next attempt is delayed by R plus jitter (uniform 0–500 ms) and a rate-limit event is logged. Given transient errors (network timeout or HTTP 5xx), Then exponential backoff is applied starting at 2 s, factor 2, max 5 retries per provider per batch. Given 3 consecutive failures for a provider, Then a circuit breaker opens for 5 minutes; the UI shows a non-blocking banner "Some sources delayed" and a disabled retry until half-open. When a probe succeeds while half-open, Then the circuit closes and syncing resumes. All retries per batch are capped so total retry time does not exceed 60 seconds.
Eventual Consistency and Non-Blocking UI
Given a last-known snapshot S of the packet, When opening Packet Preview, Then S renders within 300 ms on a mid-tier mobile device (P50) and 1 s (P95). When new data arrives after open, Then the view updates incrementally without full reload and preserves scroll position and selection. Given intermittent network failures, Then a "Data may be stale" indicator is shown and local actions remain enabled; on recovery, a silent refresh updates the view. Given duplicate or out-of-order events, Then merges are performed by version/timestamp to produce a single item without duplication. Within 2 minutes of the last successful provider response, Then the preview reflects all available deltas or surfaces an error banner with the affected sources.
Mobile‑First Preview UI & Accessibility
"As a mobile-first user, I want a fast, accessible preview with easy actions so that I can manage tax tasks on the go."
Description

Design a responsive, mobile‑first interface with fast load times, skeleton states, and accessible controls. Provide section tabs, sticky completeness bar, swipeable actionable chips for fixes, and haptic feedback on completion. Meet WCAG AA standards (color contrast, focus order, screen reader labels) and support offline read‑only viewing with queued actions that sync when online. Ensure gestures map to web and native patterns consistently.

Acceptance Criteria
Mobile-first responsive preview with sticky completeness bar
Given a device width from 320px to 768px, when the Packet Preview screen loads, then section tabs render in a single-row, horizontally scrollable tab bar and the completeness bar remains sticky at the top during vertical scroll without overlapping content. Given the user scrolls through sections up to 2000px content height, when the completeness percentage changes due to local state (e.g., confirming a category), then the sticky bar updates within 200ms and remains visible. Given landscape orientation on a 360x640 device, when rotating to portrait and back, then layout reflows without content truncation, with tab labels fully readable (ellipsis allowed) and no horizontal page scroll. Rule: Tap targets for tabs and bar controls are ≥44x44 dp; visual alignment matches an 8dp grid. Rule: No layout shift > 0.1 CLS occurs after initial content paints.
Fast load with skeleton states on mobile network
Given a cold start on a mid-tier Android device (e.g., Moto G7) with emulated 4G (400ms RTT, 1.6 Mbps), when navigating to Packet Preview, then skeleton placeholders appear within 150ms and are replaced progressively. Rule: p75 LCP ≤ 2.5s; p75 TTI ≤ 3.5s; p75 CLS ≤ 0.1 for the Packet Preview route. Rule: Skeletons match final component dimensions within ±8px to minimize shift. Rule: No blocking script > 50KB (compressed) on this route; total route JS ≤ 150KB (compressed) loaded before interaction. Rule: API requests are parallelized; first byte for preview data ≤ 800ms at p75.
Accessible tabs, focus order, and screen reader labels (WCAG AA)
Rule: All text and interactive elements meet WCAG AA contrast (normal text ≥ 4.5:1, large text ≥ 3:1, focus indicators ≥ 3:1 against adjacent colors). Given keyboard navigation from the address bar, when tabbing through the page, then focus order follows the visual order: header > completeness bar > tabs > section content > actionable chips > footer, and is fully reachable without traps. Rule: Tabs implement ARIA roles (tablist, tab, tabpanel) with proper aria-selected and roving tabindex; Enter/Space activates, Arrow keys move focus. Rule: Actionable chips and controls have accessible names/labels that convey action and target (e.g., "Request W‑9 for Client X"). Given the completeness percentage updates, when using a screen reader, then the sticky bar announces changes via aria-live="polite" and does not steal focus.
Swipeable actionable chips with accessible alternatives
Given a list of actionable chips, when the user swipes left on a chip (native) or drags with pointer (web), then context actions (e.g., Request W‑9, Add Receipt, Confirm Category) reveal with a threshold of 24dp and activate on release, with an Undo option visible for 5s. Rule: Each chip has a non-gesture alternative: a visible overflow button (kebab) and keyboard-accessible menu providing the same actions. Rule: Minimum chip hit area ≥ 44x44 dp; actions are operable via keyboard (Enter/Space) and screen reader rotor/Actions. Given VoiceOver/TalkBack is enabled, when a chip gains focus, then available actions are announced and can be invoked without swipe gestures. Rule: Error states for failed actions display inline messages and are announced via aria-live with retry control.
Haptic feedback on fix completion respecting system settings
Given a user completes a fix (e.g., successfully adds a receipt or confirms a category), when the action result is persisted, then a light impact haptic (or platform-equivalent) triggers once within 100ms of success. Rule: Haptics follow system settings; if haptics are disabled or device lacks support, no vibration occurs and no errors are thrown. Rule: No haptic fires on validation errors; error feedback uses visual and auditory/screen reader announcements only. Rule: Batch operations (multi-fix) coalesce haptics to a single feedback per batch.
Offline read-only preview with queued actions and auto-sync
Given the device is offline (e.g., Airplane Mode), when opening Packet Preview, then cached preview content renders read-only with an "Offline" banner and disabled action controls. Given the user attempts an action while offline, when tapping an action, then the action is queued locally with a visible "Queued" state and count badge. Given connectivity is restored, when the app detects online status, then queued actions auto-sync within 5s using exponential backoff (max 3 retries per item) and update UI states; failures surface retriable errors. Rule: Cached preview remains available across app restarts (persisted storage) until replaced by fresher data. Rule: Conflict resolution prefers server state; if client action conflicts, user sees a non-blocking notification with details and a link to resolve.
Consistent gesture-to-action mapping across web and native
Rule: Swipe left reveals primary contextual actions; swipe right reveals secondary/undo where applicable; on web, drag or click overflow mirrors the same action order and labels. Rule: Long-press (native) and right-click (web) open the same context menu as the overflow, with identical shortcuts and iconography. Given a user switches between PWA (web) and native app, when performing the same gesture on actionable chips and tabs, then the resulting actions, animations, and labels are consistent within platform conventions (e.g., Material motion durations 150–200ms). Rule: Gesture affordances include visible hints on first use and are dismissible and cached to not reappear for 30 days. Rule: All gestures have keyboard and screen reader equivalents documented in on-screen help.
Deadline Nudges & Smart Reminders
"As a busy freelancer, I want timely reminders about what’s missing so that I can finish my packet before the deadline without stress."
Description

Proactively notify users of outstanding packet items based on quarter deadlines and personal behavior (e.g., uncategorized transactions older than 7 days, missing receipts over amount threshold). Provide digest and real-time push/email options with deep links into the exact fix action. Suppress reminders once resolved, batch notifications to reduce noise, and respect user preferences and quiet hours. Track effectiveness to tune timing and content.

Acceptance Criteria
Real-Time Reminders for Aging Action Items
Given the user has enabled real-time reminders and current time is outside the user’s quiet hours and channels are enabled When an uncategorized transaction is older than 7 days and amount is greater than or equal to the user-configured threshold Then send exactly one notification via the preferred channel within 15 minutes containing the item count and a deep link to categorize the highest-priority transaction, and record the send And do not send another reminder for the same transaction within 24 hours Given an expense lacks a receipt for at least 3 days and amount is greater than or equal to the user-configured threshold and receipt reminders are enabled When the reminder job runs outside quiet hours Then send a notification with a deep link that opens the Attach Receipt flow for that expense and record the send Given a payee has no W-9 on file more than 7 days after first payment When the reminder job runs Then send a notification with a deep link to the Request W-9 flow prefilled for that payee and record the send
Quarterly Deadline Digest with Completeness Summary
Given unresolved packet items exist and digest notifications are enabled When the quarter due date is 14, 7, or 3 days away in the user’s time zone and current time is within allowed send hours Then send one digest for that day via enabled channel(s) showing completeness percentage, counts by fix type (uncategorized, missing receipts, missing W-9), and deep links to the top 3 fix actions and to Packet Preview And do not send a digest if no unresolved items remain And ensure no more than one digest is sent per scheduled day
Deep Links Navigate to Exact Fix Action
Given the user taps a reminder deep link to categorize a specific transaction When the app or web opens Then navigate directly to that transaction’s Categorize screen with context preloaded and on save return to Packet Preview with completeness updated Given the user taps a deep link to attach a receipt When the app or web opens Then open the Attach Receipt flow prefilled to the expense and on completion mark the receipt status as satisfied Given the user taps a deep link to request a W-9 When the app or web opens Then open the Request W-9 flow with the payee preselected and show confirmation after sending If the native app is not installed on mobile Then open the equivalent responsive web flow for the same action If the item is already resolved at open time Then show a resolved state and do not present the action flow
Auto-Suppress and De-Duplicate Reminders on Resolution
Given a reminder has been sent for an item and that item is resolved (categorized, receipt attached, or W-9 received/sent) When the resolution is saved Then cancel any pending reminders and mark the item as suppressed for future evaluations And do not send further reminders for that item unless it re-enters a needs-action state And update any scheduled digest to exclude the resolved item And mark the previously sent reminder as resolved in tracking within 5 minutes
Batching and Rate Limits for Notifications
Given multiple triggerable items of the same type are detected within a 30-minute batching window When notifications are sent Then group them into a single message per channel with aggregated counts and a deep link to a filtered list of items Enforce a maximum of 1 push and 1 email per user per 6-hour period, except within 24 hours of the quarter deadline when up to 2 pushes and 1 email are allowed Cap total reminders at 8 per user per rolling 7 days; suppress non-critical reminders once the cap is reached
Preferences, Quiet Hours, Time Zone, and Fallback Channel
Given the user has configured notification preferences (channels, digest vs real-time, quiet hours) and a time zone When scheduling and sending reminders Then send only via enabled channel(s), respect quiet hours (default 21:00–08:00 local if unset), and schedule in the user’s time zone If a push fails due to missing/invalid token or two consecutive delivery errors Then fall back to email within 15 minutes if email is enabled When the user snoozes a reminder for 24 hours Then exclude the snoozed item from evaluations until the snooze expires
Effectiveness Tracking and Tuning
Given a reminder is sent When the user opens, clicks, or completes an action via a deep link Then record send time, channel, open, click, and action completion with item identifier and time-to-resolution Compute and store per-template daily metrics: open rate, click-through rate, and resolution rate within 48 hours Expose metrics to the analytics pipeline within 1 hour of event capture and retain raw events for at least 90 days

SettleSense

Automatically bridges the gap between pre-authorizations and final settlements. When amounts shift due to tips, currency holds, or delayed posting, SettleSense links the same purchase across pending and posted transactions so your receipt stays attached, your categories stick, and nothing is left unmatched.

Requirements

Authorization-to-Settlement Link Engine
"As a freelance creative, I want my pending card holds to automatically link to the final posted transaction so that my receipts and categories stay intact without me having to fix mismatches later."
Description

Implement a deterministic-first, probabilistic-fallback matching engine that links pending pre-authorizations to final posted settlements across bank feeds. The engine should correlate transactions using merchant identifiers, masked PAN/last4, authorization codes, timestamps, and amount tolerances, accounting for tips, partial captures, reversals, and delayed postings. It must operate idempotently, handle multiple candidate matches, and maintain a stable transaction identifier so that downstream objects (receipts, categories, notes) remain attached throughout the lifecycle. The engine should expose match confidence scoring, reason codes, and structured deltas (amount and time) for analytics and UI explanation. Integrate with TaxTidy’s ingestion pipeline to run on feed webhooks and scheduled backfills, and write results to a canonical ledger that other services can query.

Acceptance Criteria
Deterministic match on auth code with stable transaction ID preservation
Given a pending authorization A with normalized_merchant_id M, last4 L, authorization_code C, authorized_amount 50.00 USD, and authorized_at T0 And a posted settlement S with normalized_merchant_id M, last4 L, authorization_code C, settled_amount 50.00 USD, and settled_at T1 within 7 days of T0 When the link engine processes S Then it links A to S with match_type "deterministic" And sets confidence = 1.00 and reason_codes = ["AUTH_CODE","MERCHANT_ID","LAST4","AMOUNT_EQUAL","TIME_WINDOW<=7D"] And preserves the stable_transaction_id from A on S And ensures all attachments (receipts, categories, notes) remain associated via the stable_transaction_id And emits structured_deltas {amount_delta: 0.00, time_delta_hours: hours(T1 - T0)} And writes a single link record to the canonical ledger with idempotency_key unique to (A.id, S.id)
Probabilistic match for tip/FX-adjusted settlement within tolerances
Given a pending authorization A with normalized_merchant_id M, last4 L, authorized_amount 40.00 USD, and authorized_at T0 And a posted settlement S for the same card last4 L and merchant name similar to A (normalized_name_similarity >= 0.90 OR same MCC) with settled_at T1 within 7 days of T0 And no deterministic match exists for A And either (S.settled_amount is between 40.00 and 40.00 + min(12.00, 0.30*40.00) USD) OR (if currency differs, the implied FX rate delta vs network_rate_at_auth <= 3%) When the link engine processes S Then it links A to S with match_type "probabilistic" And sets confidence in [0.70, 0.90] and reason_codes includes at least one of ["TIP_DELTA","FX_HOLD","NAME_FUZZY","LAST4"] And preserves the stable_transaction_id from A on S And records structured_deltas including amount_delta = S.amount_in_auth_currency - A.authorized_amount and time_delta_hours = hours(T1 - T0) And writes the result to the canonical ledger
Partial capture from single authorization consolidated correctly
Given a pending authorization A with normalized_merchant_id M, last4 L, authorized_amount 200.00 USD, authorized_at T0 And posted settlements S1 (120.00 USD) and S2 (80.00 USD) share normalized_merchant_id M and last4 L and occur within 7 days of T0 When the link engine processes S1 and S2 Then it links both S1 and S2 to A with reason_codes including "PARTIAL_CAPTURE" And marks A as fully settled when cumulative_captured_amount within $1.00 of A.authorized_amount And assigns a shared group_id for A, S1, and S2 and preserves the stable_transaction_id across the group And ensures all attachments remain associated to the group via the stable_transaction_id And records structured_deltas for each capture (per_capture_amount_delta and cumulative_amount_delta) And writes parent-child relationships to the canonical ledger
Authorization reversal without settlement handled correctly
Given a pending authorization A with normalized_merchant_id M, last4 L, authorized_amount X, and authorized_at T0 And a reversal R for A is received (network reversal or posted amount -X) and no settlements for A arrive within 14 days of T0 When the link engine processes R Then it marks A as reversed and does not auto-link A to any future posted transaction And sets confidence = 1.00 and reason_codes = ["AUTH_REVERSAL"] And preserves the stable_transaction_id on A and R And ensures any existing attachments remain associated to the reversal record via the stable_transaction_id and are not left unmatched And writes the reversal outcome and structured_deltas (amount_delta = -X, time_delta_hours = hours(R.time - T0)) to the canonical ledger
Idempotent reprocessing across webhook retries and backfills
Given a webhook event for settlement S and authorization A is delivered N>=2 times and the same pair (A,S) is also discovered during a scheduled backfill When the link engine processes each occurrence Then it produces exactly one link outcome in the canonical ledger for (A,S) And preserves the same stable_transaction_id on all reprocessings And does not create duplicate attachments or duplicate ledger entries (verified by unique (A.id,S.id) idempotency_key) And only updates the existing record if the new computation yields a higher confidence or additional reason_codes without reducing confidence
Multiple candidate settlements resolved with confidence and ambiguity handling
Given a pending authorization A with at least two candidate settlements S1 and S2 within the time window and sharing merchant/last4 characteristics And the engine computes confidence scores score(S1) and score(S2) When processing A Then if max(score) >= 0.85 and (max(score) - next_best) >= 0.05, it auto-links A to the top candidate and records reason_codes Else it does not auto-link and marks A as "ambiguous", exposing the top 3 candidates with scores and reason_codes via the matching API And writes the ambiguity state to the canonical ledger for review
Pipeline integration and canonical ledger write with analytics fields
Given new bank feed data arrives via webhook and historical data via scheduled backfill When the link engine processes incoming authorizations and settlements Then it runs on both triggers and writes a record per evaluated pair containing stable_transaction_id, match_type, confidence, reason_codes, amount_delta, time_delta_hours, group_id (if applicable), authorization_id, settlement_id, and processing_source {webhook|backfill} And downstream services can query by stable_transaction_id to retrieve the same entity before and after settlement with attachments intact And the API returns the structured_deltas and reason_codes for UI explanation for every linked or ambiguous case
Receipt and Category Persistence
"As a solo consultant, I want my receipt and category to follow the transaction from pending to posted so that my tax records are complete without extra work."
Description

Ensure that user-attached receipts, categories, tags, notes, and memo edits on a pending transaction are automatically and atomically transferred to the matched posted settlement. Preserve edit history and attribution while preventing duplication when feeds emit both reversal and settlement events. Provide safeguards so that if a match is re-evaluated, attachments and classifications move once and only once to the current canonical transaction. Update all references in exports, reports, and tax packet assembly to point to the final record, guaranteeing continuity in IRS-ready documentation.

Acceptance Criteria
Atomic persistence from pending to posted match
Given a pending transaction X with user-attached receipts and user-applied category, tags, notes, and memo edits And a posted settlement Y is matched to X by SettleSense When Y is ingested Then all attachments and classifications from X are transferred atomically to Y in a single committed operation And X is marked non-canonical and hidden from default views And Y is marked canonical with a backlink to X in the audit trail And no duplicate attachments or tags are created And the transfer completes within 2 seconds at p95
Duplicate prevention on reversal + settlement events
Given the bank feed emits both a pending reversal event and a posted settlement for the same authorization When events arrive in any order or are retried at-least-once Then exactly one canonical posted transaction exists And the user's receipts, categories, tags, notes, and memo appear only on the canonical transaction And the reversal and original pending items are archived and excluded from exports and reports And no duplicate files exist in storage for the same receipt (by checksum) And the migration is idempotent across retries
Single-move behavior on re-match to a new canonical
Given pending X initially matches posted Y and data is migrated And SettleSense re-evaluates and selects a new posted Z as the canonical match for X When the re-match occurs Then attachments and classifications move from Y to Z exactly once And Y retains only audit pointers, with no residual user data And all internal references update to Z And repeated re-matches do not create duplicates or data loss
Exports, reports, and tax packet continuity
Given a user generates CSV exports, summary reports, and IRS-ready tax packets after settlement When a pending-to-posted migration or re-match has occurred Then all exported references point to the final canonical transaction ID And amounts and categories reflect the posted settlement values And receipt file links resolve successfully (HTTP 200 or valid file handle) And there are zero dangling references in any artifact And scheduled reports generated within the last 24 hours are backfilled within 15 minutes
Edit history and user attribution preserved
Given user A edits category, tags, notes, or memo and attaches receipts on pending X When data migrates to canonical posted Y Then Y includes an immutable audit trail of all changes with UTC timestamps and user IDs And the audit history shows original and new values for each field And the UI and API can retrieve the last 20 history entries within 300 ms at p95 And audit entries cannot be modified by non-admin users
Concurrency and race-condition safety
Given a user is editing pending X within 200 ms of settlement Y ingestion And the ingestion pipeline may process retries out of order When the migration executes Then either the user's latest saved edits are included in the migration or the migration retries until they are And no partial state is visible externally; the system either shows X as pending or Y as canonical, never both with differing data And the final canonical record is consistent across UI, API, and exports And the error rate for migration operations remains below 0.1% over a rolling 24-hour period
FX, Tip, and Adjustment Handling
"As a freelancer who travels, I want SettleSense to account for tips and currency conversions so that my final amounts are accurate and my expense totals reflect what I actually paid."
Description

Add explicit handling for common settlement adjustments: gratuities added after authorization, currency conversion holds versus final FX rates, partial captures, and interchange-driven amount corrections. Define configurable tolerance bands by merchant MCC and region, and compute and store an adjustment object detailing pre-auth amount, final amount, delta, currency, and reason. Surface adjustments in the UI and API, and update budget/tax totals accordingly while preserving original pre-auth values for audit context.

Acceptance Criteria
Post-Authorization Tip Adjustment Within Tolerance
Given a pending transaction with preAuthAmount=100.00 USD, MCC=5812 (Restaurants), region=US, and a configured tolerance of 25% or $15 (whichever is greater) And a posted transaction from the same merchant within 5 calendar days with finalAmount=115.00 USD and metadata indicating a gratuity of 15.00 USD When SettleSense runs matching Then the pending and posted transactions are auto-linked as the same purchase And an adjustment object is stored with fields: preAuthAmount=100.00, finalAmount=115.00, deltaAmount=+15.00, currency="USD", reason="Gratuity" And the original pre-authorization values remain visible in the audit trail And the existing receipt and category remain attached to the settled transaction And budget and tax totals are recalculated using 115.00 USD And the transaction is not flagged for review
FX Hold vs Final Rate Settlement Adjustment
Given a pending transaction authorized for 100.00 EUR on an account with base currency USD and displayed pending amount 110.00 USD (hold rate 1.10) And a posted transaction settles for 100.00 EUR at a final rate of 1.08, yielding 108.00 USD And the MCC=4511 (Airlines), region=EU, with a tolerance of 5% or $5 (whichever is greater) When SettleSense reconciles pending to posted Then the transactions are auto-linked And an adjustment object is stored with preAuthAmount=110.00, finalAmount=108.00, deltaAmount=-2.00, currency="USD", reason="FX Rate Finalization" And original authorization currency and amount (100.00 EUR) remain preserved in the audit record And budget and tax totals are updated to 108.00 USD
Partial Capture Linking and Adjustment Recording
Given a pending transaction with preAuthAmount=200.00 USD from the same merchant and an authorization code with a capture window of 7 days And a posted transaction references the authorization and captures 120.00 USD within the window When SettleSense processes the settlement Then the posted transaction is linked to the pending as a partial capture regardless of tolerance thresholds And an adjustment object is stored with preAuthAmount=200.00, finalAmount=120.00, deltaAmount=-80.00, currency="USD", reason="Partial Capture" And the receipt and category remain attached And budget and tax totals reflect 120.00 USD, while the original pre-auth amount remains available for audit
Interchange-Driven Amount Correction After Settlement
Given a transaction settled and linked at 50.00 USD with an existing adjustment object And a subsequent issuer/network correction posts for +1.23 USD referencing the same transaction When SettleSense ingests the correction Then a new adjustment object entry is appended with preAuthAmount=50.00, finalAmount=51.23, deltaAmount=+1.23, currency="USD", reason="Interchange Correction" And budget and tax totals increase by 1.23 USD And prior pre-authorization and settlement values remain immutable and viewable in audit history
MCC/Region Tolerance Configuration and Auto-Linking Rules
Given tolerance rules are configured: MCC 5812 (US) = max(25%, $15) and MCC 4511 (EU) = max(5%, $5) When a posted transaction would produce a delta exceeding both percent and absolute thresholds for its MCC/region Then the system does not auto-link the pending and posted transactions and marks the pending transaction status "Needs Review" with reason "Out of Tolerance" And budget and tax totals remain unchanged until user confirmation And upon user confirmation to link, an adjustment object is created with the computed delta and reason="Manual Override", and totals are updated accordingly And when a delta is within the configured tolerance for its MCC/region, auto-linking proceeds and the appropriate reason (e.g., "Gratuity" or "FX Rate Finalization") is assigned
UI and API Surfacing of Adjustment Object and Audit Values
Given a linked transaction with an adjustment object When viewed in the mobile app transaction details Then an "Adjustment" badge is visible, and a details view shows preAuthAmount, finalAmount, deltaAmount, currency, and reason with correct sign and two-decimal formatting for currency And the API GET /transactions/{transactionId}/adjustments returns HTTP 200 with a JSON payload including fields: preAuthAmount, finalAmount, deltaAmount, currency (ISO 4217), reason, mcc, region, matchedPendingId, matchedPostedId, createdAt (ISO 8601) And field values match the stored adjustment object, and amounts are precise to 2 decimal places for USD/EUR And original pre-authorization values remain accessible via an audit endpoint or audit section
Matching Window and Confidence Controls
"As a user, I want transparent and tunable matching rules so that I can trust automatic links and easily understand why a transaction was or wasn’t matched."
Description

Provide configurable time windows, amount tolerances, and matching weights to tailor linking behavior by institution and merchant patterns. Implement merchant name normalization and MCC heuristics to improve accuracy. Expose match confidence scores and human-readable explanations to the client app for transparency. Include guardrails that prevent auto-linking when confidence falls below a threshold, routing those cases to a review queue.

Acceptance Criteria
Institution-specific matching window configuration
Given a default matching_window_days of 7 and an institution override for "Chase" of 14 When evaluating a pending and posted pair on a Chase account with posted_at - pending_at = 10 days Then the pair passes the time-window rule Given the same override of 14 days When posted_at - pending_at = 15 days Then the pair fails the time-window rule and is not auto-linked Given a merchant-level override for "Uber" of 2 days (applies across institutions) When posted_at - pending_at = 3 days Then the pair fails the time-window rule Given an admin updates matching_window_days for "CapitalOne" from 7 to 9 When a new evaluation occurs after the change Then the new window is applied within 5 minutes of the update And previously linked pairs remain unchanged
Amount tolerance rules for tips and FX holds
Given amount_tolerance_abs = $3.00 and amount_tolerance_pct = 20% When pending_amount = $40.00 and posted_amount = $47.00 (tip applied) Then the pair passes the amount rule via percentage tolerance Given amount_tolerance_abs = $3.00 When pending_amount = $40.00 and posted_amount = $42.50 Then the pair passes the amount rule via absolute tolerance Given fx_tolerance_pct = 3% and pending_currency != posted_currency When the normalized percentage difference between pending and posted amounts is <= 3% Then the pair passes the amount rule for FX variance Given both absolute and percentage differences exceed their tolerances When evaluating the pair Then the amount rule fails and the pair is not auto-linked Given posted amounts are rounded to 2 decimals When comparing amounts Then comparisons use bank-reported rounded values
Weighted matching and confidence scoring
Given configured weights: amount=0.40, time=0.20, merchant_similarity=0.30, mcc_match=0.10 (sum=1.00) When candidate feature scores are computed in [0,1] Then confidence = sum(weight_i * score_i) is produced in [0,1] and rounded to 2 decimals Given the default auto_link_threshold = 0.70 When confidence >= 0.70 and tie_margin condition is not met Then the pair is eligible for auto-linking Given two top candidates have confidences 0.74 and 0.73 and tie_margin = 0.02 When evaluating for auto-linking Then the pair is routed to Review instead of auto-linking Given a feature is missing for a candidate When computing the score Then a neutral score of 0.50 is used for that feature unless an override is configured
Merchant name normalization and MCC heuristics
Given the raw merchant name "SQ*JOE'S COFFEE 1234" and candidate "Joe's Coffee" When normalization removes processor prefixes, punctuation, case, stopwords, and numeric tails Then the merchant similarity score is >= 0.90 Given an alias map includes {"UBER TECHNOLOGIES":"UBER"} When comparing "UBER TRIP HELP.UBER.COM" to "Uber" Then the merchant similarity score is >= 0.85 Given MCC codes match between pending and posted When computing confidence Then a +0.05 boost is applied (not exceeding 1.00) Given MCC codes conflict When computing confidence Then a -0.05 penalty is applied (not dropping below 0.00) Given MCC is unknown for either side When computing confidence Then no MCC adjustment is applied
Client-facing confidence score and explanation payload
Given a matched candidate is evaluated When the client fetches transaction details Then the response includes matchConfidence (0.00–1.00 to 2 decimals), matchExplanation[] (factor, contribution, description), timeWindowApplied, amountToleranceApplied, and thresholdUsed Given the same inputs are evaluated twice When the client requests the data repeatedly Then matchConfidence and matchExplanation are identical across calls Given a candidate is below threshold When returning the payload Then matchExplanation contains the reason code "BelowThreshold" with top contributing factors Given privacy constraints When returning explanation data Then no internal rule IDs or PII are exposed; only reason codes and human-readable descriptions
Auto-link guardrails and review queue routing
Given confidence < thresholdUsed When evaluating a candidate Then do not auto-link; enqueue a review item with candidateIds, scores, and top factors Given multiple candidates within tie_margin of the top score When evaluating for auto-linking Then do not auto-link; enqueue to Review with reason code "TieMargin" Given an auto-link is performed When the evaluation is re-run Then the operation is idempotent and no duplicate links or duplicate audit events are created Given a review item is enqueued When delivery occurs Then a correlationId is used to deduplicate any retries
Integrity on link: preserve receipt and category, prevent duplicates
Given a pending transaction with an attached receipt and category When it is linked to its posted counterpart via auto-link Then the receipt remains attached to the resulting purchase and the category remains unchanged Given a link is created When inspecting both transactions Then both reference the same purchaseId and the pending transaction is marked as resolved Given a link is reversed by a reviewer When the unlink operation completes Then receipts and categories revert to their prior associations with no data loss Given linking operations occur under load When monitoring for duplicates Then no duplicate purchase entities are created and an audit log entry with link_id, old_state, and new_state is recorded
Unmatched and Multi-Match Review Queue
"As a busy freelancer, I want a simple review queue for edge cases so that I can resolve mismatches quickly without hunting through my feed."
Description

Create a review workflow for transactions that fail auto-match or have multiple plausible settlements. Provide a compact UI to compare candidates (merchant, time, amount, confidence, reason codes), approve or reject links, and manually link or unlink transactions. All actions must be reversible, fully logged, and trigger re-attachment of receipts and categories. Notify users of pending reviews and auto-resolve items when new feed data raises confidence above the acceptance threshold.

Acceptance Criteria
Queue Population for Unmatched and Multi-Match Transactions
Given auto-matching has completed for a new feed import When a transaction has no acceptable match or yields two or more candidates above the candidate threshold Then the transaction appears in the Review Queue within 2 minutes of import completion And the queue item includes transaction ID, merchant, timestamp, amount, and queue reason (Unmatched or Multi‑Match) And for Multi‑Match items, the candidate count is displayed And the queue de-duplicates identical items across imports And resolving or removing the item decreases the queue count accordingly
Compact Candidate Comparison UI with Confidence and Reason Codes
Given a Review Queue item is opened When candidate rows render Then each candidate displays merchant, timestamp (with timezone), amount (with currency), confidence score (0–100), and reason codes And candidates are sorted by descending confidence by default And differences versus the source transaction (amount/time) are visually highlighted And the UI fits a 375×667 mobile viewport without horizontal scrolling And the first 10 candidates render within 500 ms on a median device
Approve/Reject Candidate Linking and Propagation
Given a Review Queue item with multiple candidates When the user approves a candidate Then the pending and posted transactions are linked And receipts, categories, and notes from the pending/hold transaction are re-attached to the settled transaction within 5 seconds And the queue item is marked resolved and removed And an audit entry is recorded When the user rejects a candidate Then that candidate is hidden for this item and user And it is excluded from future auto-resolve And an audit entry is recorded
Manual Link and Unlink with Search and Validation
Given a user initiates manual linking from a queue item or transaction detail When the user searches by merchant, amount, date range, or last‑4 card Then results return within 1 second for up to 5,000 records When the user selects two transactions to link Then the system validates currency match, date tolerance (±7 days), and amount variance (≤20% unless tip-coded) And on success links them, re-attaches receipts/categories/notes, and removes conflicting links When the user unlinks a previously linked pair Then attachments and categories return to the original pending item (if applicable) And the item re-enters the Review Queue if still unresolved And all changes are logged
Reversible Actions and Full Audit Logging
Given any approve, reject, link, unlink, or edit action is performed When the user taps Undo within 5 minutes or restores from History Then the prior state is fully restored, including attachments and categories, and queue counts update And Redo is available after an Undo within the same session And the audit log records actor, UTC timestamp, action, item IDs, and before/after states And audit entries are immutable and exportable to CSV
Notifications for Pending Reviews
Given the user has pending review items When the app opens or new items are created by import Then a badge with the count appears on the Review tab and a dismissible in‑app banner prompts review And if push/email is enabled, a daily digest is sent only when there is at least 1 pending item, respecting user quiet hours And tapping the notification deep‑links to the first queue item And notifications clear when the queue is empty
Auto-Resolve on Confidence Threshold Increase
Given new feed data updates candidate confidence When any candidate’s confidence meets or exceeds the acceptance threshold (e.g., ≥85) Then the system auto-links the pair, re-attaches receipts/categories/notes, removes the item from the queue, and records an audit entry And an in‑app notice informs the user of the auto-resolution And candidates explicitly rejected by the user are never auto‑resolved And items with a manual hold flag are excluded from auto‑resolve
Audit Trail and Reconciliation Export
"As a user preparing taxes, I want an auditable report of how pending transactions became final so that I can defend my records if I’m questioned."
Description

Record an immutable audit trail for every link decision, including input features, confidence score, rules applied, and user overrides. Provide exportable reconciliation reports (CSV/PDF) that show pre-auth to settlement mappings, adjustments, and attachment continuity for use in IRS documentation and accountant workflows. Integrate with TaxTidy’s tax packet generator so that the final packet reflects settled amounts while preserving evidence of the original authorization.

Acceptance Criteria
Immutable Audit Log for Link Decisions
Given SettleSense automatically links a pre-authorization (preAuthId) to a posted settlement (settlementId) When the link decision is persisted Then an append-only audit record is created containing: decisionId, preAuthId, settlementId, decisionType=auto, timestamp (UTC ISO-8601), actor=system, confidenceScore (0.00–1.00), rulesApplied (names and versions), inputFeatures (keys and normalized values), modelVersion, environment, recordHash (SHA-256), previousHash And the audit record cannot be updated or deleted via any API; attempted mutations return 403 and are themselves logged with actor and timestamp And the audit chain for a 24h partition verifies successfully when hashes are recomputed end-to-end And audit records are retained unaltered for at least 7 years
User Override Logging and Traceability
Given a user manually overrides an automatic link decision (link, unlink, or relink) When the override is saved Then a new audit record is appended with decisionType=override, actor=userId, method (UI/API), reason (optional text up to 500 chars), beforeMapping, afterMapping, timestamp (UTC), recordHash, previousHash, and reference to the superseded decisionId And the original automatic decision record remains intact and queryable And the transaction lineage view returns the full chronological chain of auto and override decisions for the involved transactions And exports flag override rows with userOverrideFlag=true and include overrideUserId and overrideReason
Reconciliation Report Export — CSV
Given a user with export permission requests a reconciliation CSV for a date range and account(s) When the export job is submitted Then the system generates a UTF-8 CSV with LF line endings and comma delimiter within 60 seconds for datasets up to 50k rows And the CSV includes a header row and exactly these columns in fixed order: preAuthId, settlementId, merchantOriginal, merchantNormalized, preAuthAmount, settledAmount, currency, fxRate, adjustmentType, adjustmentAmount, decisionConfidence, rulesApplied, userOverrideFlag, overrideUserId, overrideReason, preAuthDate, settlementDate, categoryBefore, categoryAfter, receiptAttached, attachmentIds, linkStatus, auditRecordHash And each matched link appears as one row; unmatched pre-authorizations and settlements are included with linkStatus=unmatched and nulls for missing fields And numeric values use dot as decimal separator with two fractional digits; dates are UTC ISO-8601 And the download URL is single-use, expires in 24 hours, and requires authentication And the export job and download are logged in the audit trail with actor, timestamp, and reportId
Reconciliation Report Export — PDF
Given a user with export permission requests a reconciliation PDF for a date range and account(s) When the export is generated Then the PDF contains a summary section with totals (count of links, unmatched pre-auths, unmatched settlements, sum of settledAmount, sum of adjustments) and a detailed table mirroring the CSV columns And the PDF includes page numbers, reportId, generation timestamp (UTC), and the filters applied And receipt attachments are referenced by attachmentId with clickable links; where configured, thumbnails are embedded for receipts under 500 KB And totals in the summary equal the aggregates computed from the detailed table And the file name follows: TaxTidy_Recon_{YYYYMMDD-YYYYMMDD}_{AccountAlias}_{ReportId}.pdf
Attachment Continuity Across Pre-Auth to Settlement
Given a pre-authorization has one or more receipt attachments and is later linked to a settlement When the link is created or updated Then all existing attachments remain associated to the settlement record without duplication and remain accessible via their original attachmentIds And the category and memo fields from the pre-authorization persist on the settlement unless explicitly changed by the user And if a link is undone, attachments revert to the pre-authorization and are removed from the settlement And CSV/PDF exports reflect receiptAttached=true and list the correct attachmentIds for the current link state
Tax Packet Integration Reflecting Settled Amounts with Authorization Evidence
Given a user generates a TaxTidy tax packet for a period containing linked transactions When SettleSense provides reconciliation data to the packet generator Then the packet uses settledAmount values for expense totals and line items while displaying original pre-authorization amounts and adjustments in an appendix or footnotes And each line item in the packet includes a reference to decisionId/auditRecordHash enabling trace-back to the audit trail And receipt attachments included at pre-authorization are included or linked from the settled entry in the packet And categories applied on pre-authorization persist to the settled entries unless a user override is present, in which case the override category is shown And packet validation passes: sum(line item settledAmount) equals reported totals; cross-references resolve to existing audit records

GeoPrint Match

Boosts match accuracy using precise time-and-place fingerprints from your phone. Even when bank descriptors are vague (e.g., SQ* or processor codes), GeoPrint cross-references receipt location, device signals, and merchant databases to propose the right transaction first—so you confirm with a single swipe.

Requirements

Secure Location Consent & Controls
"As a privacy-conscious freelancer, I want granular control over how my location is used so that I can improve match accuracy without compromising my privacy."
Description

Implements explicit, OS-native location permission prompts and an in-app privacy control center that lets users opt in/out, select precise vs. approximate location, and set retention windows for location data. Presents a just-in-time rationale explaining why GeoPrint improves transaction matching and how data is protected. Applies data minimization by storing only time-bounded coordinates with accuracy radius and salted hashes where feasible, and encrypts data in transit and at rest. Integrates with TaxTidy account preferences, honors regional compliance (GDPR/CCPA), and provides purge/export mechanisms. Defines graceful fallbacks to non-GeoPrint matching when permission is denied or revoked.

Acceptance Criteria
Just-in-Time Rationale and OS Permission Request Flow
Given the user first attempts a GeoPrint-enabled action that requires location When the consent flow starts Then an in-app, just-in-time rationale is shown explaining GeoPrint’s benefit, data protection, and user controls, with links to the Privacy Policy and Privacy Control Center And when the user taps Continue, the OS-native location permission prompt is invoked with system-appropriate options; when the user taps Not now, the flow cancels without requesting OS permission And after the user’s choice, the app records consent status, selected precision, and timestamp to the account, and will not show the OS prompt again unless permission is reset or revoked And for EU/EEA users, region-specific compliance copy is displayed and no location processing occurs until consent is granted
Privacy Control Center: Opt-In/Out, Precision, and Account Sync
Given the user opens the Privacy Control Center When Location Collection is toggled Off Then GeoPrint location capture stops immediately, no new location requests are made, and non-GeoPrint matching is used for subsequent operations When Location Collection is toggled On Then the app checks OS permission; if insufficient, the user is directed to system settings; if sufficient, capture resumes When the user switches Precision between Precise and Approximate Then the preference is saved to the TaxTidy account and propagates to all logged-in devices within 60 seconds And the screen displays current OS permission state and last consent timestamp, and settings persist across logout, reinstall, and device change
Retention Window Configuration and Automated Enforcement
Given the user sets a location data retention window (30/90/180/365 days or custom 7–730 days) When the window is saved Then only records within the window are retained and records older than the window are purged within 24 hours And purge operations are logged with counts and timestamps and are visible in the Privacy Control Center And backups reflect the same retention, with deleted records removed from backups within 7 days
Data Minimization for Stored Location Events
Given the system persists a location event When the event is stored Then only these fields exist: user_id, timestamp (UTC), latitude, longitude, accuracy_radius_meters, collection_reason, transaction_link_hash (salted), consent_version And no raw device identifiers, merchant names, or continuous trace IDs are stored And collection occurs only on explicit triggers (receipt capture, invoice import confirmation, manual transaction review); no background tracking while idle And if the user selected Approximate, stored coordinates are reduced to a minimum effective accuracy radius of ≥1000 meters; if Precise, accuracy reflects the OS-provided radius
Encryption in Transit and At Rest for Location Data
Given location data is transmitted to backend services When a network request is made Then TLS 1.2+ with HSTS and certificate pinning is enforced; requests over plaintext are blocked Given location data is stored at rest When storage configuration is inspected Then data is encrypted with AES-256 via managed KMS; keys are rotated at least every 90 days; access is least-privileged and audit-logged
User Self-Service Data Export and Purge (GDPR/CCPA)
Given the user requests Export My Location Data for a date range When the request is submitted Then a machine-readable file (JSON or CSV) is delivered to the registered email within 24 hours containing all retained events and field definitions Given the user requests Delete My Location Data When the request is confirmed Then all retained location events are purged within 24 hours and confirmation is shown in-app and via email; backups are purged within 7 days And for GDPR/CCPA accounts, the export includes processing purposes/legal basis, and deletion requests are honored without requiring account cancellation
Graceful Fallback When Location Access Is Denied or Revoked
Given OS-level location permission is Denied or revoked When the user performs an action that would use GeoPrint Then the system uses non-GeoPrint matching without errors, shows a dismissible banner linking to the Privacy Control Center, and makes no location API calls And the app does not automatically re-prompt the OS; only a user-initiated action from the Privacy Control Center opens system settings And median task completion time for the non-GeoPrint path does not degrade by more than 10% compared to baseline
GeoPrint Capture SDK
"As a mobile-first user, I want the app to automatically capture reliable location context during my work so that it can later match receipts and transactions without extra effort."
Description

Delivers an on-device module that captures time-and-place fingerprints during key events such as receipt photo capture, invoice creation, and app foreground sessions near merchants. Samples GPS, Wi‑Fi, and cellular signals with timestamps, accuracy, and motion state, deduplicates readings, applies jitter/obfuscation rules, and batches uploads to minimize battery and data usage. Buffers securely when offline, signs payloads, versions the schema, and exposes a lightweight API for other TaxTidy features to attach context. Provides performance safeguards, power budgeting, and telemetry for capture success rates.

Acceptance Criteria
Event-Triggered GeoPrint Capture
Given location permission is granted and the SDK is enabled When the user captures a receipt photo in the TaxTidy app Then a GeoPrint capture starts within 300 ms of the shutter event and completes within 5 s or times out gracefully Given location permission is granted and the SDK is enabled When the user creates a new invoice and taps Save Then a GeoPrint capture is initiated within 300 ms and associated with the invoice ID Given the app enters foreground within 100 m of a known merchant centroid and permission is granted When the foreground session lasts at least 5 s Then at most one GeoPrint capture occurs per merchant per 10-minute window Given location permission is denied When any of the above triggers occur Then the SDK records capture_attempted with reason=permission_denied and does not start sensors
Sampling Completeness and Metadata
Given a capture has started When sampling sensors Then the SDK attempts GPS first (up to 3 fixes over 5 s), then Wi‑Fi-based location (1 scan), then cellular (1 lookup), stopping early once accuracy <= 50 m is achieved Given a capture completes Then the resulting fingerprint includes: eventId, triggerType, ISO‑8601 UTC timestamps (start, end), locationSource, latitude, longitude, horizontalAccuracyMeters, motionState (stationary|walking|driving|unknown), device locale, device timezone; and totalSamples <= 5 Given no fix is available within 5 s Then the fingerprint is marked hasLocation=false with reason=timeout and includes lastKnownLocationAgeSeconds if present
Deduplication and Privacy Obfuscation
Given multiple sensor readings within the same capture When two readings are within 10 m and 2 s Then only the reading with the lowest horizontalAccuracyMeters is retained Given a fingerprint is ready to upload When preparing the payload Then latitude/longitude are rounded to 4 decimal places and a random offset of 0.0005–0.0015 degrees is applied in a random bearing; the unjittered values are never included in the payload Given two captures occur within 60 s and 25 m with the same triggerType Then only one upload record is produced, with dedupeReason=near_duplicate
Batching, Upload Scheduling, and Power/Data Budget
Given the device is online When buffered fingerprints count >= 20 or oldest age >= 15 min or the app is moving to background Then the SDK uploads in batches of up to 50 fingerprints per request Given uploads are occurring on cellular data and battery level < 20% or Low Power Mode is on Then uploads are deferred until Wi‑Fi or until oldest age >= 60 min Given normal operation on cellular Then total GeoPrint payload data usage stays <= 1 MB per rolling 24 hours; if the projection exceeds this, the SDK throttles to max one batch per hour Given an upload fails with a transient error (HTTP 429/5xx or network error) Then exponential backoff is applied starting at 1 min, doubling to a max delay of 2 hours, with jitter
Offline Buffering and Retry
Given the device is offline When captures occur Then fingerprints are encrypted at rest using AES‑256‑GCM with a key from the OS keystore/Keychain and buffered up to 10,000 records or 20 MB, whichever comes first Given the buffer limit is exceeded Then the oldest records are dropped with dropReason=capacity and a counter is incremented Given connectivity returns Then buffered records are uploaded in FIFO order within 5 min, respecting batching and budget rules
Payload Integrity, Signing, and Schema Versioning
Given a batch is ready to upload When constructing the request Then each payload includes schemaVersion (integer), monotonic eventSequence, and an Ed25519 signature header over the canonical JSON body; unsigned or locally invalid signatures are not sent Given the server responds with HTTP 400 and errorCode=schema_unsupported with requiredVersion > local schemaVersion Then the SDK pauses further uploads, marks affected records status=awaiting_update, and emits a non‑fatal telemetry alert Given device clock skew Then event timestamps include both UTC wall time and monotonic elapsed time to ensure non‑decreasing sequencing
Context Attachment API and Telemetry
Given a TaxTidy feature calls GeoPrint.attachContext(eventId, contextMap) When contextMap size <= 1 KB and keys match ^[a-z0-9_]{1,32}$ Then the context is attached to the fingerprint and included in the next upload; oversize or invalid keys return an error without crashing Given captures are running Then the SDK emits telemetry: capture_attempted, capture_success, capture_success_rate (rolling 24 h), time_to_first_fix_ms p50/p95, samples_per_capture_avg, buffer_fill_ratio, upload_batches, drops_capacity Given an on‑device test in open‑sky conditions with permission granted Then time_to_first_fix_ms p50 <= 2000 and capture_success_rate >= 95% Given battery level < 10% or thermal state is serious/critical Then the SDK suspends new captures and records reason=power_guard
Receipt–Transaction Correlation Engine
"As a busy freelancer, I want the system to suggest the most likely transaction for each receipt so that I can confirm with a single swipe and save time."
Description

Creates a backend service that ingests GeoPrints and bank transactions, normalizes time zones and currencies, and builds candidate matches using configurable time windows and distance thresholds. Parses vague processor descriptors (e.g., SQ*, Stripe, Adyen) and applies a ranking model that combines GeoPrint proximity, time overlap, merchant similarity, and amount tolerance to propose a top match. Supports feedback loops from user confirmations/edits to continuously retrain weights, ensures idempotent processing, and exposes APIs to fetch proposed matches with SLAs and observability (metrics, logs, tracing).

Acceptance Criteria
Normalize Time Zones and Currencies
Given receipts and bank transactions include varying local time zones and currencies When the engine ingests both data sources Then all timestamps are converted to UTC in ISO 8601 with millisecond precision and original timezone is stored as metadata And all monetary amounts are converted to the configured base_currency using the FX rate for the transaction date with banker's rounding to 2 decimals And the FX source, rate, and rate_date are stored alongside the normalized amount And DST boundary cases (e.g., fall-back hour) are normalized with correct UTC offsets and non-overlapping ordering And sample records show deterministic normalization on reprocessing with identical outputs
Configurable Time Window and Distance Thresholds
Given matching configuration defines time_window_minutes = T and distance_threshold_meters = D When the engine builds candidate matches for a receipt GeoPrint Then only transactions where |receipt_time - transaction_time| <= T minutes are included And only transactions with haversine_distance(geo_receipt, geo_tx) <= D meters are included And updates to T or D via config service are applied to new jobs within 60 seconds without service restart And metrics emit candidate_count and filtered_out_by_reason per receipt_id
Parse Vague Processor Descriptors
Given transactions include processor descriptors (e.g., "SQ* COFFEE TRUCK 1234", "Stripe 3DS MERCHANT X", "ADYEN*MERCH-AB12") When the descriptor parser runs Then canonical merchant_name is extracted and normalized (case-folded, diacritics removed, common prefixes/suffixes stripped) And ambiguous processor tokens (e.g., "SQ*", "STRIPE", "ADYEN") are excluded from merchant similarity features And known merchant IDs are resolved via merchant database when confidence >= 0.9 And on a labeled test set of >= 1,000 transactions, merchant extraction achieves precision >= 95% and recall >= 90%
Ranking Model Proposes Top Match
Given a receipt with a set of candidate transactions When the ranker scores candidates using proximity, time overlap, merchant similarity, and amount_tolerance Then each candidate receives a score s in [0,1] and the top_match and top_3 are returned with feature contributions And amount_tolerance is satisfied when abs(amount_delta_normalized) <= max(2.00, 0.02 * receipt_amount) And ties are broken by smallest abs(amount_delta), then smallest time_delta, then smallest distance, then lowest transaction_id lexicographically And on a held-out validation set of >= 10,000 labeled pairs, Top-1 accuracy >= 92% and Top-3 recall >= 98%
Feedback Loop Ingestion and Retraining
Given users confirm or edit proposed matches in the app When feedback events arrive Then events are persisted with user_id, receipt_id, transaction_id, prior_proposal_id, label (confirm/edit), timestamp, and optional reason_code And labeled features are available to the training store within 5 minutes of receipt And the model retrains daily at 02:00 UTC, versioned with reproducible metadata (code hash, data snapshot) And automatic promotion occurs only if Top-1 accuracy improves by >= 0.5% without increasing false positive rate by > 0.2%; otherwise previous model remains active And rollback to the previous model completes within 10 minutes on command
Idempotent Ingestion and Deduplication
Given duplicate deliveries of the same receipt or transaction with a stable idempotency_key When the ingestion pipeline processes events multiple times Then only one set of candidate matches and one proposal record exist per unique (receipt_id, transaction_id) pair And subsequent retries return the same proposal_id and checksum without creating new records And out-of-order events are handled, with late arrivals up to 7 days incorporated without duplications And reprocessing the same batch yields byte-for-byte identical outputs and metrics
Match Proposal API SLAs and Observability
Given a client calls GET /v1/matches with receipt_id or transaction_id When the request is served under normal load (100 RPS in staging, warmed cache) Then p95 latency <= 200 ms and p99 latency <= 500 ms, with 99.9% monthly availability And the response includes correlation_id, top_match, top_3, scores, and feature_breakdown And metrics expose request_count, latency histograms, error_rate by status_code, and top_match_confidence And structured logs include correlation_id across services; distributed traces cover ingress, ranker, datastore, and cache spans And alerts trigger if p95 > 300 ms or error_rate > 1% for 5 minutes
Merchant Knowledge Graph Integration
"As a user dealing with vague bank descriptors, I want accurate merchant names and locations so that I can trust the suggested matches."
Description

Integrates third-party place data and internal merchant registries to unify merchant identities across names, processor IDs, and locations. Maintains geocoded store-level entries with categories and tax-relevant tags, resolves duplicates, and tracks store variants and relocations over time. Caches responses, rate-limits external calls, and provides backfill jobs for historical transactions. Supplies enrichment to the correlation engine and surfaces normalized merchant names and categories to the UI.

Acceptance Criteria
Unify Merchant Identities Across Aliases and Processor IDs
Given a labeled corpus of 1,000 transactions spanning 200 real merchants with known aliases (e.g., SQ*, PAYPAL*, legal names) and processor IDs When the knowledge graph integration runs the identity resolution pipeline Then at least 98% precision and 95% recall are achieved on merchant entity linking And each resolved merchant is assigned a stable merchant_id and source mappings (name variants, processor IDs) And no two distinct ground-truth merchants share the same merchant_id And confidence scores are stored per mapping with a threshold configurable via environment variable
Maintain Geocoded Store-Level Entries With Tax Tags
Given merchants with multiple physical locations and online variants When third-party place data and internal registries are ingested Then each store node has required fields: store_id, merchant_id, normalized_name, category, tax_tags[], full_address, lat, lon, geo_precision, open_hours, data_sources[] And category and tax_tags coverage is ≥ 99% of active stores; missing tags are flagged with reason codes And geo_precision <= 30 meters for 95% of stores with device-verified receipts; fallback precision is recorded And categories conform to TaxTidy taxonomy vX.Y with validation against the schema registry
Duplicate Resolution and Survivorship Rules
Given duplicate store/merchant candidates from multiple sources When the deduplication job executes Then candidates exceeding the duplicate threshold are merged under a single canonical entity using deterministic survivorship priority (internal_registry > verified_third_party > unverified) And all inbound foreign keys (transactions, receipts) are re-pointed atomically to the survivor And merge history (loser->winner mappings, timestamps, rationale, features) is persisted and queryable And no orphaned references remain; referential integrity check passes 100%
Track Store Relocations and Variants Over Time
Given a merchant store that relocates or changes branding When an update is ingested with a new address within a 5 km radius and effective_from date Then a new store_version is created with valid_from/valid_to intervals and carries forward merchant_id And transactions are matched to the correct version based on transaction_date and proximity And UI shows the normalized name and current address while historical transactions retain their historical address And timeline queries return non-overlapping intervals per store_id with 0 violations
Response Caching and External Rate Limiting
Given repeated lookups for the same place/processor ID When the enrichment API is called Then cache hit rate is ≥ 85% for hot keys with a configurable TTL and cache stampede protection enabled And external provider rate limits are never exceeded; 0 occurrences of HTTP 429/QuotaExceeded in production SLO windows And exponential backoff with jitter is applied on transient errors with max retries configurable And observability emits per-provider QPS, error rates, and cache metrics with RED/SLO dashboards
Historical Backfill for Legacy Transactions
Given an account with 36 months of historical transactions without merchant enrichment When the backfill job runs Then it processes at least 50 transactions/sec per worker with horizontal scaling to N workers And the job is idempotent; re-runs do not create duplicate entities or links And progress checkpoints every 1,000 transactions allow resume within 60 seconds after failure And error rate during backfill remains < 0.5% with retries; failures are quarantined with actionable reasons
Enrichment Delivery to Correlation Engine and UI
Given a transaction eligible for GeoPrint Match When enrichment completes Then the correlation engine receives merchant_id, store_id, normalized_name, category, tax_tags, lat, lon, confidence, and source_attribution fields And the mobile UI displays normalized_name and category within 200 ms P95 from local cache or async update within 2 seconds P95 if fetched And missing enrichments are surfaced as “Needs Review” with a retry action; retry success rate ≥ 90% And all emitted fields validate against the public API schema with 0 schema violations in CI
Swipe-to-Confirm Match UX
"As a user on the go, I want to confirm the right match with a single swipe so that I can keep my records clean without slowing down."
Description

Implements a mobile-first interaction that presents the top proposed match with a concise rationale (time delta, distance, normalized merchant name) and a mini-map preview. Enables one-swipe confirmation, quick access to alternate candidates, and an undo/edit action. Handles permission-off states with clear prompts, supports accessibility (VoiceOver/TalkBack, large text), and localizes dates, times, and currencies. Provides deep links from the transactions feed and receipt viewer and logs user actions for learning and audit.

Acceptance Criteria
Top Match Presentation and Rationale
Given a transaction with a proposed GeoPrint top match, When the Swipe-to-Confirm view is opened via any entry point, Then the top match card displays normalized merchant name, time delta, and distance, and shows a mini-map preview centered on the proposed merchant location. Given the bank descriptor contains processor prefixes (e.g., "SQ*" or codes), When the top match is rendered, Then the normalized merchant name replaces the raw descriptor in the prominent title field. Given multiple candidates exist, When the view loads, Then the highest-ranked candidate is preselected and visually marked as "Top match". Given the view is loaded on a modern device, When rendering completes, Then the rationale fields and mini-map appear within 500 ms of view presentation.
One-Swipe Confirmation Flow
Given the top match is visible, When the user performs a single continuous swipe on the confirm control, Then the match is saved, the transaction is linked to the proposed receipt/merchant, and a success confirmation is announced visually and via accessibility. Given the user confirms by swipe, When the save succeeds, Then the state persists across app relaunch and the transaction appears as "Matched" in the feed. Given a network delay, When the user swipes to confirm, Then the UI shows an in-progress state within 100 ms and completes the action within 300 ms locally, queuing server sync if offline. Given the user attempts a second swipe on the same item, When the item is already confirmed, Then the action is idempotent and no duplicate links are created.
Alternate Candidates Quick Access
Given the top match may be incorrect, When the user taps "See other matches" or the candidates control, Then a list of at least 3 alternate candidates is shown ranked by confidence with each item showing normalized merchant name, time delta, and distance. Given the alternate list is open, When the user selects a different candidate, Then the main card updates instantly to reflect the new selection and rationale, and the confirm control targets the newly selected candidate. Given the user changes their mind, When they reopen the candidates list, Then the original top match remains available and can be reselected.
Undo and Edit After Confirmation
Given a match has just been confirmed, When the confirmation toast/snackbar is shown, Then an "Undo" action is available for at least 5 seconds. Given the user taps "Undo" within the window, When processed, Then the link is reverted, the transaction returns to unmatched state, and an accessibility announcement confirms the revert. Given a confirmed match exists, When the user taps "Edit", Then they can change to an alternate candidate and update editable fields (e.g., merchant name note) and save or cancel. Given the user saves edits, When the operation completes, Then the updated selection persists and history records both the original and new values.
Permissions-Off Handling
Given location permission is denied, restricted, or off, When the match view is opened, Then a clear non-blocking prompt explains GeoPrint benefits with options "Enable" and "Continue without location". Given the user chooses "Enable", When the OS dialog is presented, Then the app requests the minimum required permission and returns to the match view preserving context after the user's choice. Given the user chooses "Continue without location", When the view renders, Then geo-dependent rationale fields are replaced with neutral messaging and the feature remains usable with candidates sourced from non-geo signals. Given the user has previously dismissed the prompt in the session, When reopening the view, Then the prompt is not repeatedly shown in the same session.
Accessibility and Localization Support
Given a screen reader is active (VoiceOver/TalkBack), When the match view loads, Then all actionable controls are focusable with descriptive labels, the map is marked as "map preview", and the confirm action is exposed as a single accessible action. Given Dynamic Type or large text is enabled, When the view renders, Then text reflows without truncating critical fields, and touch targets are at least 44x44 pt (iOS) or 48x48 dp (Android). Given WCAG AA contrast requirements, When the UI is displayed, Then all text and interactive elements meet a contrast ratio of at least 4.5:1 against their backgrounds. Given the device locale and region settings, When dates, times, distances, and currencies are shown, Then they are formatted per locale (e.g., 24h vs 12h, km vs mi, currency symbol and grouping), including right-to-left layout support.
Deep Links and Action Logging
Given a deep link from the transactions feed or receipt viewer with valid IDs, When opened, Then the app navigates directly to the match view for the targeted item, preloading the proposed top match and candidates. Given a deep link is invalid or the item is missing, When opened, Then the app shows a friendly error and a path to the parent screen without crashing. Given any user action in the match flow (open, view candidates, select candidate, swipe confirm, undo, edit, permission prompt shown/acted), When the action occurs, Then an event is logged with schema including userId (hashed), transactionId, receiptId (if any), action type, timestamp (UTC), surface (feed/receipt), candidateId, confidence score, time delta, and distance; events pass validation and are queued and retried on transient failures. Given an auditor requests history, When retrieving logs for a transaction, Then the system can reconstruct the sequence of user actions and the final state from the emitted events.
Confidence Score & Explainability
"As a cautious user, I want to see why a match is recommended so that I can trust the system’s decisions."
Description

Generates a 0–100 confidence score with interpretable factors such as distance, time overlap, merchant similarity, and amount variance. Defines policy thresholds for auto-confirm, user review, or manual match required, and A/B tests threshold configurations to balance precision and recall. Stores the score inputs alongside the decision for auditability, displays succinct explanations in the UI, and exposes the score via API to downstream workflows like auto-categorization.

Acceptance Criteria
Compute 0–100 Confidence Score with Core Factors
Given a transaction and receipt with inputs distance=120m, time_overlap=5min, merchant_similarity=0.92, amount_variance_pct=1.2 When the confidence score is calculated Then the score is between 0 and 100 inclusive and the four factor values are returned alongside the score Given the same inputs are processed repeatedly 10 times When the confidence score is calculated each time Then the score varies by no more than ±0.1 across runs Given one or more factor signals are unavailable When the confidence score is calculated Then missing factor values are returned as null, the score is still produced, and a missing-signal note is included in metadata
Enforce Decision Thresholds for Auto-Confirm, Review, Manual
Given thresholds are configured as: auto_confirm ≥ 85, needs_review 60–84.99, manual_match_required < 60 When decisions are made for scores 90, 75, and 40 Then the decisions are auto_confirm, needs_review, and manual_match_required respectively Given threshold values are updated by an admin When a new decision is made after the update Then the new thresholds are applied to that decision Given an existing decision was created under prior thresholds When thresholds are updated Then the prior decision remains unchanged unless explicitly re-evaluated via a re-run action, and the thresholds version used is stored with the decision
A/B Test Threshold Configurations and Metrics
Given an experiment with variants A (auto ≥ 85; review 60–84.99) and B (auto ≥ 80; review 55–79.99) and a 50/50 allocation is active When 10,000 decisions are made Then assignment proportions per variant are each within 45%–55% Given user confirmations/corrections within 14 days serve as ground truth When daily experiment metrics are computed Then precision and recall are reported per variant with 95% confidence intervals and sample sizes Given the experiment is concluded and variant B is selected as winner When rollout is executed Then thresholds switch globally within 10 minutes, the winning configuration is versioned, and the experiment is archived
Persist Score Inputs and Decisions for Auditability
Given any match decision is made When the record is persisted Then the following fields are stored and queryable: decision, score, factor values (distance_m, time_overlap_min, merchant_similarity, amount_variance_pct), thresholds version, experiment variant (if any), timestamp (UTC), receipt_id, transaction_id, user_id Given a request attempts to modify an existing audit record When the update is submitted Then the system rejects in-place mutation and only allows creating a new superseding record linked by previous_record_id Given an admin requests an export for a date range When the export completes Then 100% of decisions in the range are included and the file is available in CSV and JSONL formats
Display Succinct Score Explanations in Mobile UI
Given a proposed match is shown in the GeoPrint Match mobile UI When the user expands "Why this match?" Then the UI displays the 0–100 score and up to three top factors with human-readable labels and values (e.g., "120m away", "5 min overlap", "Name similarity 0.92") Given the explanation panel is opened on a typical 4G connection When time-to-first-render is measured Then it is ≤ 300 ms and all elements are accessible via screen readers with descriptive labels Given any factor value is unavailable When the panel displays Then a clear placeholder (e.g., "Distance unavailable") is shown and the explanation fits within three lines in compact view Given dark mode is enabled When the panel displays Then text and icons meet WCAG AA contrast ratios
Expose Score and Factors via Public API
Given an authenticated client with scope matches:read When GET /v1/matches/{id} is called Then the response includes fields: score (0–100), factors.distance_m, factors.time_overlap_min, factors.merchant_similarity, factors.amount_variance_pct, decision, thresholds.version, experiment.variant, explanation.summary Given the client lacks the required scope When the endpoint is called Then the response is 403 with no confidential fields returned Given a sustained load of 100 RPS in staging When latency is measured over 15 minutes Then p95 ≤ 500 ms and error rate ≤ 0.1% Given the OpenAPI specification is generated When it is validated Then the documented schema includes the above fields with example values and is published to the developer portal
Offline Capture & Backfill Matching
"As a traveler who is often offline, I want the app to backfill matches when I reconnect so that my tax records stay complete without manual work."
Description

Ensures GeoPrints are captured and encrypted locally when offline, with reliable retry and backoff for upload. When bank feeds sync later, replays stored GeoPrints to generate matches within configurable time windows, handling clock skew and device time changes. Deduplicates overlapping GeoPrints, reconciles conflicts when multiple candidates exist, and notifies the user when new high-confidence matches are found. Scales to retain and process up to 90 days of GeoPrints without degrading app performance.

Acceptance Criteria
Offline GeoPrint Capture and Encryption
Given the device has no internet connectivity When a GeoPrint event is triggered by an eligible signal (merchant proximity, receipt capture, or transaction-like tap) Then a GeoPrint record is written to local storage within 500 ms And the record is encrypted at rest with AES-256-GCM using a hardware-backed key And the record includes a monotonic event ID, device UTC timestamp, local timezone offset, and capture version And the encrypted payload cannot be read after app force-close/restart without valid app keys And GeoPrint capture continues across app restarts and OS process kills without data loss (0 lost events in 100 forced-kill test runs)
Resilient Upload with Exponential Backoff
Given there are N pending GeoPrints and the device regains stable connectivity (Wi‑Fi or ≥ 3G) When the app is in foreground or permitted background execution Then the first upload attempt starts within 10 seconds And retries use exponential backoff with jitter (initial 5–10 s, factor 2.0, max interval 15 min) And uploads pause on Low Power Mode or battery < 15% unless charging And on a continuous stable network for 30 minutes, ≥ 99% of pending GeoPrints are uploaded within 15 minutes And retry state survives app restarts and OS reboots without duplicating uploads (idempotent by event ID)
Backfill Matching on Bank Feed Sync
Given a bank feed sync delivers new transactions and there are stored GeoPrints up to 90 days old When replay is triggered Then each transaction is evaluated against GeoPrints within the configurable window (default ±2 h; adjustable 15 min–12 h per account) And candidate matches are scored and the top candidate is proposed with a confidence score [0.00–1.00] And audit logs record transaction ID, GeoPrint ID, window used, and score for each proposal And for a sync of up to 200 transactions and a local set of up to 10,000 GeoPrints, proposal generation completes within 10 seconds And no more than one proposal is created per transaction unless the user dismisses the prior proposal
Clock Skew and Time Change Tolerance
Given the device time is manually shifted by up to ±30 minutes or crosses a DST boundary When matching is executed Then server/network time is used to normalize timestamps when available And the effective matching window compensates for detected skew up to ±30 minutes without reducing precision (no missed matches in test set) And stored GeoPrints retain both device-local and UTC timestamps for audit And no duplicate proposals are created for the same transaction due to time jumps
GeoPrint Deduplication
Given multiple GeoPrints are captured within a 5-minute interval and the same geohash-8 area When building the replay set Then events are deduplicated into a single canonical GeoPrint using deterministic ordering (highest signal quality, then longest dwell, then latest timestamp) And deduplication produces a stable idempotency key so reprocessing yields the same canonical choice And downstream matching uses only the canonical event (zero duplicate proposals in 1,000 synthetic overlap cases)
Conflict Resolution and High-Confidence Notifications
Given a GeoPrint has multiple candidate transactions within the window When candidates are scored Then the highest score is proposed first And if the score delta between top two candidates < 0.05, the user is prompted to review rather than one-swipe confirm And no auto-confirmation occurs; user confirmation is required for all matches And if a proposal has confidence ≥ 0.85, a push/in-app notification is sent within 60 seconds of candidate generation with merchant, amount, and time, deep-linking to one-swipe confirm And when > 5 high-confidence proposals are generated within 10 minutes, a single digest notification is sent instead of individual alerts And notifications respect user opt-in and OS Do Not Disturb/quiet hours settings
90-Day Retention and Performance
Given continuous use for 90 days with GeoPrint capture enabled When the local store reaches 50,000 GeoPrints or 200 MB (whichever first) Then the system retains the most recent ≤ 90 days and purges older or matched items without data corruption And app cold-start time increases by < 150 ms compared to an empty store And steady-state memory overhead attributable to GeoPrint storage/processing stays < 50 MB in foreground And background processing CPU time averages < 5 minutes per day and battery impact < 2% per day attributable to GeoPrint tasks And purge jobs run incrementally and never block the UI thread (no dropped frames > 1% over baseline)

AutoSplit

Turns one receipt into clean, compliant allocations automatically. Detects tips, taxes, and line items to split a single card charge across categories, clients, or reimbursables. You get accurate deductions and project-level clarity without manual math.

Requirements

Smart Line-Item Parsing
"As a freelancer, I want receipt line items to be parsed automatically so that I don’t have to type details and I can split charges accurately in seconds."
Description

Automates extraction of merchant, date, totals, currency, taxes, tip, and individual line items from receipt photos and PDFs, normalizing them into TaxTidy’s transaction schema. Uses OCR with confidence scoring, common POS template heuristics, and fallback strategies when line items are incomplete, ensuring a usable split even on low-quality images. Supports multi-currency and locale formats, handles e-receipts and thermal images, and queues offline captures for later processing on mobile. Integrates with existing TaxTidy document ingestion and categorization pipelines to provide structured, item-level data for downstream splitting and reporting.

Acceptance Criteria
Parse Core Fields from Receipt Photos and PDFs
Given a clear receipt photo (≥300 DPI equivalent, no major occlusion) or vector PDF When the document is processed by Smart Line-Item Parsing Then merchant_name, purchase_date (ISO 8601), total_amount, currency_code (ISO 4217), tax_amount, and tip_amount are present in the normalized TaxTidy transaction schema And all monetary fields conform to the currency minor unit (e.g., 2 decimals for USD, 0 for JPY) And each core field has confidence ≥ 0.85 And parsing_metadata includes parser_version, document_type (photo|pdf), and processing_duration_ms
Extract and Normalize Line Items with Confidence Scoring
Given a receipt that contains itemized lines When the document is processed Then line_items[] is populated with at least description and line_total for each item And quantity and unit_price are populated when present on the source And each line item has a confidence value between 0 and 1, with line-level confidence ≥ 0.80 And the sum(line_items.line_total) + tax_amount + tip_amount matches total_amount within the currency rounding tolerance
Fallback Strategy on Incomplete or Low-Quality Itemization
Given a receipt where itemized lines cannot be reliably parsed (e.g., <70% of subtotal covered or OCR confidence < 0.60) When the document is processed Then a single synthetic line item labeled "Unparsed Items" is created with line_total = total_amount − tax_amount − tip_amount And the synthetic line item is flagged needs_review = true with reason_code = "incomplete_itemization" And core fields (merchant_name, purchase_date, total_amount, currency_code) are still populated if detectable And overall parsing_status = "partial" is emitted in parsing_metadata
Multi-Currency and Locale-Aware Parsing
Given a receipt using locale-specific number formats (e.g., comma decimal separator, thousand separators) and/or currency symbols When the document is processed Then currency_code is detected or inferred and saved as ISO 4217 And numeric amounts are correctly parsed and normalized per locale (e.g., 1.234,56 € → 1234.56 with EUR) And if currency cannot be unambiguously determined, workspace default currency is applied and needs_review = true is set with reason_code = "ambiguous_currency" And all normalized amounts adhere to currency minor units without loss beyond rounding tolerance
E-Receipt (HTML/PDF) and Thermal Image Handling
Given an HTML or PDF e-receipt containing transactional data When the document is processed Then marketing content is ignored and the transactional section is parsed into core fields and line_items And multi-page receipts are fully parsed with fields taken from the transactional page(s) Given a thermal photo with skew ≤ 15° and low contrast When the document is processed Then orientation is corrected, contrast enhanced, and core fields are extracted with confidence ≥ 0.75 within 10 seconds on standard processing tier
Mobile Offline Capture Queue and Deferred Processing
Given a user captures a receipt on mobile while offline When the capture is saved Then the document is queued locally with status = "pending_ocr" and a durable retry policy And upon connectivity restoration, the document uploads automatically and is processed within 15 minutes And if the app is force-closed and relaunched before connectivity is restored, the queued document persists and resumes upload And duplicate prevention is applied using content hash so the same receipt is not processed twice
Pipeline Integration and Idempotent Publication
Given a document is successfully parsed When normalization completes Then a receipt.parsed event is published to the ingestion and categorization pipelines containing structured core fields, line_items, and confidence scores And the event includes an idempotency_key derived from document_id + checksum so replays do not create duplicate transactions And downstream AutoSplit receives item-level data and produces category/client/reimbursable-ready allocations without manual intervention And parsing results are stored with trace_id and are retrievable via the transactions API for auditing
Tip & Sales Tax Auto-Detection
"As a solo consultant, I want tips and taxes detected automatically so that I can categorize expenses correctly without doing manual calculations."
Description

Identifies and separates tip/gratuity and sales tax amounts from the receipt total, labeling them distinctly for categorization, allocation, and reporting transparency. Recognizes common descriptors and positioning on receipts, validates against detected jurisdiction rates when available, and accommodates post-authorization tip adjustments common in hospitality. Exposes these components to the split engine and UI so users can include, exclude, or reassign them per policy without manual math, improving accuracy and compliance-ready documentation.

Acceptance Criteria
Standard Receipt with Explicit Tip and Sales Tax
Given a receipt image with clearly labeled Subtotal, Sales Tax, Tip, and Total When the receipt is processed by OCR and parsing Then Sales Tax and Tip amounts are extracted and labeled as separate components And Subtotal + Sales Tax + Tip equals Total within $0.01 tolerance; otherwise the receipt is marked "Needs Review" And Tip and Sales Tax components are exposed to the split engine and UI with include/exclude and reassign controls
Post-Authorization Tip Adjustment on Settled Card Charge
Given a card transaction where the bank feed shows a settled amount greater than the pre-tip total on the receipt When the system reconciles the bank feed transaction with the receipt Then the difference (settled amount − pre-tip total) is classified as Tip if it is between $0.01 and 30% of pre-tip total; otherwise it remains unclassified and is flagged "Needs Review" And the Tip component is added to the split engine and UI and linked to an audit trail showing pre- and post-authorization amounts
Jurisdiction Rate Validation of Sales Tax
Given merchant location and tax jurisdiction can be determined from receipt or merchant data When a Sales Tax amount is detected Then the system calculates expected tax as taxable subtotal × jurisdiction rate And if |detected − expected| ≤ max($0.05, 0.5% of taxable subtotal) the Sales Tax is marked Verified with jurisdiction code; else it is marked Unverified and flagged "Rate mismatch"
Descriptor and Position Disambiguation for Tip vs Service Charge
Given a receipt containing descriptors such as "Tip", "Gratuity", "Service Charge", or "Convenience Fee" When parsing labeled lines and their positions relative to Subtotal and Total Then values on lines labeled "Tip" or "Gratuity" are classified as Tip And lines labeled "Service Charge" are not classified as Tip unless accompanied by "optional" or "add gratuity" indicators; otherwise classify as Fee (non-tip) And no value labeled "Tax", "Sales Tax", "VAT", or similar may be classified as Tip
UI Exposure and Real-Time Recalculation
Given detected Tip and Sales Tax components for a transaction When a user toggles include/exclude or reassigns category, client, or reimbursable for these components in the AutoSplit UI Then the transaction’s deductible totals and project/client allocations update in real time And changes persist on save and are reflected identically in subsequent views, exports, and reports
No-Sales-Tax or Unlabeled Receipts Handling
Given a receipt from a jurisdiction with 0% sales tax or a receipt that lacks any explicit tax line When the system processes the receipt Then no Sales Tax component is created unless a line labeled "Tax" (or synonym) is present or a jurisdiction rate match is made from subtotal/total math within $0.05 tolerance And if subtotal and total match exactly and no tax label is present, Sales Tax detection returns null and no tax component is exposed to the split engine
Reporting and Export Transparency for Tip and Sales Tax
Given a transaction with detected Tip and Sales Tax components When the user generates an IRS-ready packet or CSV export Then Tip and Sales Tax appear as distinct fields with amounts, inclusion status, category, and client/reimbursable assignment And the receipt PDF is annotated with bounding boxes around the detected Tip and Sales Tax lines, with coordinates stored and visible in the packet
Rule-Based Split Engine
"As a freelance creative, I want charges split automatically according to my rules so that I get accurate deductions and project-level clarity without manual effort."
Description

Calculates compliant splits of a single card charge into categories, clients, and reimbursables based on parsed line items, detected taxes/tips, user-defined rules, and learned preferences. Ensures mathematical integrity (splits always reconcile to the transaction total), supports rounding strategies, and maintains item-to-split traceability for auditability. Provides default mappings to TaxTidy’s Schedule C-aligned categories and allows overrides. Designed as a deterministic, testable service with idempotent operations and versioned rules to guarantee consistent outcomes across platforms.

Acceptance Criteria
Deterministic Splits Reconcile to Transaction Total
Given a single card charge with parsed line items, taxes, and tips And active user rules and learned preferences When the engine computes splits Then the sum of all split amounts equals the original transaction total to the cent And any rounding remainder is distributed according to the selected rounding strategy And no split has a negative amount unless the source line item is a discount/return And reprocessing the same input with the same ruleset version yields identical split amounts and targets
Tax and Tip Detection and Allocation
Given a receipt where tax and tip are detected as separate lines When the engine allocates splits Then tax lines are labeled type=tax and tip lines type=tip in metadata And tax and tip amounts are not double-counted in any other line item And default behavior includes tax/tip with the parent category unless a user rule specifies alternate mapping And if a user rule maps tip to a separate category or reimbursable, the mapping is applied and traceable
User Rule Precedence over Defaults and Preferences
Given conflicting mappings between a user-defined rule, a learned preference, and the default mapping When the engine selects a category, client, or reimbursable target Then the user-defined rule takes precedence And the applied rule identifier is recorded in the split metadata And learned preferences apply only when no user rule matches And defaults apply only when neither a user rule nor a learned preference matches
Default Schedule C Category Mapping with Override Persistence
Given an item with no existing user rule or learned preference When the engine categorizes it Then it maps to the default Schedule C-aligned category set And if the user overrides the mapping, a new user rule is created or updated And subsequent replays of the same transaction and future matching items use the override And the original default choice remains recoverable via rule version history
Rounding Strategy Application and Integrity
Given splits that produce fractional cents And the selected rounding strategy is 'Round Half Up' or 'Bankers' or 'Largest-Remainder' When the engine finalizes amounts Then each split is rounded according to the selected strategy And the post-rounding sum equals the transaction total to the cent And the strategy used and any remainder distribution are recorded in metadata
Idempotent Operations and Versioned Rules
Given an idempotency key and a specific ruleset version When the same request payload is processed multiple times Then the response is identical, including split IDs and amounts And processing is performed at most once server-side, with subsequent attempts returning the original result And the output includes the ruleset version used and a hash of the input used for audit
Item-to-Split Traceability and Auditability
Given any computed split When inspecting its provenance Then it references the source transaction ID and source line item IDs And it lists the rules fired (by ID and version) that contributed to the decision And it exposes a reconciliation trail showing pre-rounding and post-rounding values And the audit record is exportable and immutable once finalized
Client/Project Allocation
"As a freelancer managing multiple clients, I want to allocate parts of a receipt to specific projects so that I can see true project costs and bill accurately."
Description

Enables assignment of items or percentages from a single receipt to specific clients or projects, preserving tags, notes, and budgets for downstream reporting. Suggests likely client/project based on vendor, prior behavior, and calendar context, with quick selection shortcuts optimized for mobile. Stores user preferences securely to auto-allocate recurring vendors and supports multi-client splits in one flow, feeding allocations into TaxTidy’s project dashboards and exports.

Acceptance Criteria
Mobile Quick Allocation with Suggested Client
1) Given an imported receipt with a recognized vendor on a mobile device, When the user opens the allocation screen, Then the top 3 suggested clients/projects are displayed with confidence indicators and the most likely is preselected but not applied. 2) Given the user taps a suggestion, When confirming allocation, Then the selection applies to the entire receipt or selected items as chosen and requires no more than 2 taps to complete. 3) Given limited connectivity, When loading suggestions, Then the allocation screen renders within 2 seconds with a fallback to manual search and suggestions are deferred without blocking entry.
Multi-Client Percentage Split in One Flow
1) Given a receipt total, When the user enters percentage splits across two or more clients/projects, Then the system calculates amounts to two decimal places and Save is disabled until percentages equal exactly 100%. 2) Given a rounding residual of ≤ $0.01, When Save is tapped, Then the residual is added to the largest allocation and the adjustment is logged in the allocation metadata. 3) Given the user switches to amount-based entry, When amounts are modified, Then corresponding percentages auto-update and validation prevents exceeding the receipt total.
Line-Item Allocation with Preservation of Tags and Notes
1) Given itemized receipt lines, When specific line items are allocated to clients/projects, Then receipt-level tags and notes copy to each allocation and can be overridden per allocation. 2) Given overridden tags/notes, When the receipt is reopened, Then per-allocation overrides persist and appear in downstream reports and exports. 3) Given an allocation is removed, When the receipt is saved, Then tags/notes associated only with that allocation are not retained in reporting.
Calendar and Prior Behavior-Based Suggestions
1) Given at least five prior allocations for the same vendor and a connected calendar, When a new receipt from that vendor is imported within four hours of a calendar event, Then the suggestions include the event’s client/project and the historically most frequent client/project with explanatory badges. 2) Given suggestions are shown, When the user taps “Why?”, Then an explanation lists the factors used (vendor match, frequency, calendar proximity) without exposing PII of other clients. 3) Given a suggestion is dismissed three times consecutively, When future receipts from that vendor arrive, Then that suggestion is deprioritized below rank three.
Recurring Vendor Auto-Allocation with Secure Preferences
1) Given the user opts in to auto-allocate for a vendor, When a matching receipt imports and the amount is within ±20% of the vendor’s six-receipt median, Then the saved client/project template and split apply automatically with an “Auto-allocated” badge and an undo option. 2) Given auto-allocation rules exist, When the user views Vendor Preferences, Then rules can be created, edited, paused, or deleted and changes apply only to subsequent imports. 3) Given stored preferences, When evaluated at rest, Then they are encrypted using AES-256 or stronger and access is limited to authorized services as verified by automated security tests.
Budget and Over-Allocation Guardrails
1) Given project budgets are enabled, When a new allocation would exceed the remaining budget for the current period, Then the user is warned with the overage amount and may override by entering a reason before Save. 2) Given allocations in progress, When total allocated amount exceeds the receipt total, Then Save is disabled and an inline error shows the delta to resolve. 3) Given taxes and tips are present, When the user selects “Exclude taxes/tips from client billing,” Then those amounts are left unallocated or assigned to an internal project per settings.
Downstream Dashboards and Export Integrity
1) Given allocations are saved, When the user opens the Project Dashboard, Then totals, budgets, and burn-down reflect the new allocations within 60 seconds. 2) Given allocations are saved, When exporting IRS-ready packets and CSV, Then each line includes client/project identifiers, allocation amount, percentage, tags, and notes. 3) Given an allocation is edited or deleted, When reports and exports are regenerated, Then previous values are superseded without duplicates and an audit trail records the change.
Reimbursable Flagging & Export
"As a solo consultant, I want to flag reimbursable items and export them cleanly so that I can invoice clients and avoid double-counting in my taxes."
Description

Allows users to mark selected items within a receipt as reimbursable, track reimbursement status, and export those items with item-level detail to invoicing/accounting integrations and CSV. Preserves source receipt images and annotations, includes client/project references, and clearly distinguishes reimbursables from business expenses within TaxTidy’s reports and tax packets. Supports batching and re-export safeguards to prevent duplicates.

Acceptance Criteria
Flagging Line Items as Reimbursable in an AutoSplit Receipt
Given a receipt automatically split into line items by AutoSplit and at least one client/project exists When the user selects one or more line items and toggles "Reimbursable" on Then the selected items are marked reimbursable=true and assigned status=Unsubmitted And each flagged item requires a client and project before export; missing values trigger inline validation messages And a "Reimbursable" badge is displayed on each flagged item in the receipt view And non-flagged items remain categorized as business expenses and are not blocked by reimbursable validations And setting a client/project at the receipt level auto-fills those fields on newly flagged items without overwriting existing item-level selections
Reimbursement Status Lifecycle and Audit Trail
Given a reimbursable item exists with current status in {Unsubmitted, Submitted, Partially Paid, Paid, Canceled} When the user or an integration updates the status Then only the following transitions are allowed: Unsubmitted->Submitted, Submitted->Partially Paid, Partially Paid->Paid, Submitted->Paid, Any->Canceled And each status change records changed_by, changed_at (UTC), previous_status, new_status, and optional note And setting Paid requires a payment_date and paid_amount >= 0; Partially Paid requires cumulative_paid_amount < total_amount And attempts to regress status (e.g., Paid->Submitted) are rejected with an error unless the item is "Reopened", which sets status to Submitted and clears paid_amount And list and report views reflect status updates within 5 minutes or immediately on manual refresh
Export to Invoicing Integration with Item-Level Detail
Given an invoicing/accounting integration is connected and a client/project mapping exists for the selected items When the user exports a batch of reimbursable items to the integration Then an invoice/billable expense/expense claim line is created per item with description, quantity, unit price, tax, and tip amounts matching the source item within ±$0.01 And client and project references are populated according to the mapping rules of the target system And each created line stores an external_id and export_batch_id on the item record And the export job returns a summary with counts of succeeded, failed, and skipped items and a downloadable error report for failures And retries with the same export_batch_id are idempotent and do not create duplicates And partial failures are retriable without re-sending successfully exported items
CSV Export of Reimbursables with Source References
Given the user selects reimbursable items and chooses "Export CSV" When the CSV file is generated Then the file is UTF-8 encoded, comma-delimited, includes a header row, and uses double quotes to escape fields And each row includes: receipt_id, item_id, txn_date, merchant, category, amount, tax_amount, tip_amount, reimbursable_flag, status, client, project, annotation_text, image_url, export_batch_id And amounts equal the source item amounts within ±$0.01; sum(amount) across exported rows equals the sum of exported item totals And image_url is a signed, expiring HTTPS link valid for at least 24 hours And only the selected reimbursable items are present; non-reimbursable items from the same receipt are excluded And rows are deterministically ordered by txn_date asc, then merchant asc, then item_id asc
Reports and Tax Packets Clearly Separate Reimbursables
Given reimbursable and non-reimbursable items exist in the workspace When generating business expense reports and tax packets Then reimbursable items are excluded from deductible totals by default and shown in a separate "Reimbursables" section with subtotal by client and project And a toggle "Include reimbursables in totals" is available in on-screen reports but is off by default and does not affect exported tax packets And each reimbursable row is labeled "Reimbursable" and filterable by status (Unsubmitted, Submitted, Partially Paid, Paid, Canceled) And downloadable reports and tax packets include links/references to source receipt images and item annotations for reimbursables
Batch Export with Duplicate Prevention and Re-Export Rules
Given some reimbursable items have already been exported (exported=true with external_id and last_exported_at) When the user initiates a new export without selecting "Re-export updated" Then previously exported items are skipped with reason=AlreadyExported and are not sent to targets or included in CSV And when "Re-export updated" is selected Then only items changed since last_exported_at (amount, tax, tip, client, project, annotation, status) are sent, with a new export_batch_id and prior external_ids retained where supported by the target And the system prevents concurrent duplicate exports by enforcing a unique (workspace_id, export_batch_id) constraint and token-based submission; launching two exports within 2 seconds yields one batch And item export marking is atomic: items are marked exported only after the target confirms creation/update And duplicate detection is maintained per-destination (CSV vs each integration) to allow parallel exports without cross-destination conflicts
Preservation of Receipt Images and Annotations in Exports
Given reimbursable items reference an original receipt image and item-level annotations When exporting to integrations or CSV Then the export includes a durable reference to the immutable original image (file_id and link) and the current annotation text per item And if an image is redacted or replaced before export, the export references the latest version and the audit log records the version change And re-export of unchanged items preserves the same image file_id and does not create a new image asset And target systems that support attachments receive the image or link attached at the invoice/expense line or document level according to their capabilities
Bank Charge Reconciliation
"As a user, I want my receipt splits tied to the actual bank charge so that my books stay accurate even if the final amount changes."
Description

Links the parsed receipt and its computed splits to the corresponding bank transaction, handling pending-to-posted amount changes and tip adjustments. Performs duplicate detection across receipts and charges, resolves partial matches, and updates splits when the final posted amount differs while preserving an audit trail. Ensures each split inherits the transaction’s accounting date, currency, and merchant metadata, keeping ledgers consistent across TaxTidy’s bank feed and documents modules.

Acceptance Criteria
Auto-link Receipt Splits to Bank Transaction
Given a parsed receipt with computed splits and exactly one candidate bank transaction meets the configured date and amount tolerances and match score threshold When reconciliation runs Then the receipt and all its splits are linked to that bank transaction with a persistent transactionId reference And the reconciliation state for the receipt is set to Linked And no duplicate link is created for the same receiptId or transactionId And an audit entry records receiptId, transactionId, matchScore, tolerances used, and timestamp
Pending-to-Posted Amount Change Reconciliation
Given a receipt is linked to a pending bank transaction that later posts with a different final amount When the posted amount change is detected Then if the delta is within the configured auto-adjust tolerance, all linked splits are proportionally scaled so their total equals the posted amount And the accounting date on the splits is updated per configuration (e.g., to the posted date) And an audit entry records previous total, posted total, scaling factor, and reason=PendingToPosted And if the delta exceeds the tolerance, the reconciliation state is set to Needs Review and no auto-scaling occurs
Tip Adjustment Recognition and Allocation
Given the posted amount exceeds the receipt subtotal plus tax by a delta within the configured max tip percentage/amount When reconciliation evaluates the delta Then a distinct tip split is created or updated, categorized to the configured gratuity category, linked to the same transactionId And original itemized splits remain unchanged aside from normalization to ensure the sum of all splits equals the posted amount And an audit entry records tipDetected=true, tipAmount, and detectionMethod=DeltaVsReceiptTotal And if the delta exceeds the max tip threshold, the item is marked Needs Review and no automatic tip split is created
Duplicate Detection Across Receipts and Charges
Given a receipt or bank transaction is already linked When another receipt or transaction would create a duplicate link based on a matching fingerprint (normalized merchant, card last4, currency, amount within tolerance, date within window) Then the system blocks auto-linking and flags the new item as Potential Duplicate And presents the existing linkage details to the user for review And writes an audit record with both resource IDs and the fingerprint hash And enforces that a bank transaction can be linked to at most one receipt unless an authorized user explicitly overrides with confirmation captured in audit
Partial Match Resolution Workflow
Given multiple candidate bank transactions meet the matching criteria for a receipt, or key fields conflict (e.g., merchant similarity below threshold while amount/date are within tolerance) When reconciliation runs Then the system does not auto-link and sets the receipt reconciliation state to Needs Review with reason=PartialMatch And it surfaces a ranked candidate list with match scores and field-level diffs for user selection And upon user confirmation of a candidate, the link is created, splits are associated, and the state transitions to Linked And the decision, selected candidate, and prior candidates list are captured in the audit trail
Metadata Inheritance and Ledger Consistency
Given a receipt is linked to a bank transaction When the link is created or updated Then each split inherits and persists the transaction's accountingDate, currencyCode, merchantName, merchantCategoryCode, and transactionId And split amounts are stored in transaction currency and home currency using the transaction FX rate with rounding per configured precision And totals and metadata are consistent between the bank feed ledger and the documents module ledger And a nightly integrity job flags any discrepancy greater than 0.01 in home currency with an audit entry and alert
Comprehensive Audit Trail on Reconciliation Changes
Given any creation, update, or deletion of a link or split caused by reconciliation When the change is applied Then an immutable audit record is written containing actor (system/user), timestamp, entity IDs (receiptId, transactionId, splitIds), previous values, new values, reason code, and source event And audit records are queryable by receiptId or transactionId and return the latest 100 entries within the configured SLA And audit exports include a reconciliation summary attached to the tax packet for the relevant period
Split Review UI & Audit Trail
"As a mobile-first freelancer, I want an easy review screen with a clear audit trail so that I can approve accurate splits quickly and defend them later if needed."
Description

Provides a mobile-first review screen showing parsed items, detected tips/taxes, and proposed splits with real-time totals and validations. Supports quick edits, add/remove items, category and client pickers, reimbursable toggles, and one-tap approve, with accessibility and offline save. Every change is captured in an immutable audit log with before/after values, timestamps, and actor, and annotations are embedded into exported tax packets to support IRS-ready documentation.

Acceptance Criteria
Real-time Split Totals and Validations
Given a parsed receipt with subtotal, tax, tip, and line items When the system proposes splits Then the sum of all split amounts equals the original charge total within $0.01 tolerance Given the user edits any line amount, category, client, or reimbursable flag When the edit is applied Then totals recalculate and the variance indicator updates within 500 ms Given splits exceed the original total or a negative balance remains When totals are computed Then an error banner appears, invalid fields are highlighted, and Approve is disabled Given taxes and tips are detected When splits are displayed Then tax amounts default to the tax category and tips to the tip category, both editable
Quick Edit of Parsed Items
Given the review screen is open When the user taps Add item Then a new editable line appears with $0.00 amount and requires a category before approval Given a line item exists When the user swipes left and taps Delete Then the line is removed and totals rebalance correctly Given a line item When the user opens Category or Client picker and selects a value Then the selection persists and is reflected in the line chip Given a line item When the user toggles Reimbursable Then the item is flagged reimbursable and included in reimbursable exports Given unsaved changes exist When the user attempts to navigate away Then a confirmation dialog offers Save, Discard, or Cancel
One-Tap Approve and Commit
Given all validations pass and variance is $0.00 When the user taps Approve Then the split is committed, the receipt is marked Approved, and an audit event is recorded Given approval succeeds When the user returns to the list Then the receipt shows Approved status and is no longer flagged for review Given a transient network failure occurs during approval When the user remains on the screen Then the approval is queued and auto-retries; upon reconnect it commits once without duplication and shows success
Offline Save and Sync
Given the device is offline When the user edits splits Then changes are stored locally and a Pending Sync badge is shown on the receipt Given offline edits are saved When connectivity is restored Then changes auto-sync within 10 seconds and the Pending Sync badge clears on success Given the server version changed while the user was offline When syncing Then a conflict view shows local vs server differences, allows choose/merge, and records an audit entry for the resolution
Mobile Accessibility Compliance
Given VoiceOver or TalkBack is enabled When navigating the review screen Then all actionable elements have descriptive labels, roles, and states; reading order matches visual order Given UI contrast is measured When tested Then all text and interactive elements meet WCAG 2.2 AA contrast ratios Given touch targets are tested When measured Then all tappable controls are at least 44x44pt and provide clear focus indicators Given a hardware keyboard is used When navigating Then focus traversal, activation, and shortcuts function for Approve and pickers
Immutable Audit Trail for Split Review
Given any field change on the review screen When the change is saved Then an audit entry is appended with actorId, deviceId, timestamp (UTC ISO 8601), field, beforeValue, afterValue, and optional reason Given the audit log storage When entries are written Then each entry includes previousHash and currentHash forming a hash chain that verifies on recomputation Given the user opens Audit History When viewing entries Then entries are read-only, ordered newest-first, and filterable by field/event type Given an attempt is made to edit or delete an audit entry When the action is attempted Then the system blocks it and records a security event audit entry Given a tax packet export is initiated When the packet is generated Then the receipt’s audit log subset is embedded or linked with an integrity checksum
Exported Tax Packet Contains Split Annotations
Given a split is approved When exporting PDF and JSON tax packets Then each line includes category code, client reference, reimbursable flag, tip/tax flags, and approval metadata (approver, timestamp) Given category and client summaries are generated When the export completes Then per-category and per-client totals equal the sum of included lines and match the original charge total Given attachments are included When the packet is created Then the original receipt image and parsed text are embedded and line items cross-reference the source via index or selector Given IRS documentation needs When audit annotations are included Then a human-readable change log summary for the receipt (timestamps, actors, fields) is present in the packet

DupeShield

Keeps your ledger tidy by spotting duplicate receipt photos, email-forwarded copies, and multi-page scans. DupeShield merges pages into one receipt, prevents double attachments to the same transaction, and flags suspicious repeats before they clutter your books.

Requirements

Content Fingerprinting & Similarity Detection
"As a freelancer, I want the system to automatically spot duplicate receipts across all my sources so that I don’t waste time cleaning up repeats or risk misreporting expenses."
Description

Implement a robust duplicate-detection engine that generates perceptual hashes for images (pHash/dHash/aHash), text-normalized hashes for OCR-extracted content, and normalized metadata signatures (vendor, amount, date) for PDFs and emails. The engine must tolerate rotations, crops, lighting changes, and re-exports, and operate across receipt photos, email attachments, and scans. It assigns a confidence score per potential duplicate and exposes an internal API for other services to query matches. The module runs asynchronously on ingestion with fallbacks for real-time checks on mobile uploads, and writes match candidates to a central dedupe index without blocking the user flow. Expected outcome: high-precision, low-false-positive detection that scales to millions of documents while keeping CPU and storage costs predictable.

Acceptance Criteria
Perceptual Hash Robustness Across Image Transformations
Given five variants of the same receipt (rotated 0–360°, cropped up to 20% edges, exposure ±30%, re-exported JPEG/WebP), when processed, then the engine clusters them together with min pairwise confidence ≥ 0.85 and max ≥ 0.95. Given a set of 200 non-duplicate receipts, when processed at the default threshold, then false positives ≤ 0.5% and no unrelated pair exceeds confidence 0.75. Given a batch of 1,000 images, when fingerprinting runs, then P95 per-image compute time ≤ 150 ms and worker RSS ≤ 40 MB.
Cross-Channel Duplicate Detection (Mobile, Email, PDF Scan)
Given the same receipt submitted via mobile photo, email attachment, and PDF scan, when processed, then all are linked under one duplicate group with group confidence ≥ 0.90. Given a transaction already attached to one document, when a cross-channel duplicate is detected, then a second attachment to the same transaction is blocked and a dedupe flag is propagated to the UI within 2 seconds of ingestion. Given 100 duplicated pairs across channels and 100 unique controls, when processed, then precision ≥ 0.98 and recall ≥ 0.95 at the default threshold.
Multi-Page Scan Consolidation and Double-Attach Prevention
Given a 3-page scanned PDF and the same pages uploaded individually, when processed, then a single canonical receipt entity is created with page references and the individual images are linked as duplicates with confidence ≥ 0.90. Given the canonical entity is attached to a transaction, when a duplicate page is uploaded later, then double-attachment is prevented and an audit entry with reason "duplicate_page" is recorded within 500 ms P95. Given mixed PDFs of 2–10 pages, when processed, then page-order detection accuracy ≥ 0.98 and manual review rate for merges ≤ 1%.
OCR/Text Hash and Metadata Signature Fallbacks
Given OCR confidence < 0.70, when computing text-normalized hashes, then the engine falls back to metadata signature (vendor, amount, date) using amount tolerance ±0.01 and date tolerance ±3 days, and flags duplicates only when ≥ 2 of 3 fields match and confidence ≥ 0.85. Given 1,000 receipts with partial OCR, when processed, then metadata-based matching achieves precision ≥ 0.97 and recall ≥ 0.90 at the default threshold. Given two receipts sharing vendor and date but different amounts, when processed, then no duplicate is flagged at the default threshold.
Confidence Scoring Calibration and Thresholding
Given a labeled validation set of 10,000 receipt pairs (50% duplicates), when scored, then at threshold 0.85 the ROC-AUC ≥ 0.98, precision ≥ 0.98, recall ≥ 0.92, and F1 ≥ 0.95. Given the threshold is raised to 0.92, when rescored, then false positive rate reduces by ≥ 50% while recall remains ≥ 0.85 on the same set. Given a duplicate score returned, when inspected, then contributing signals (pHash, dHash, aHash, textHash, metaSig) and weights sum to 1.0 are included for auditability.
Asynchronous Ingestion with Real-Time Mobile Fallback
Given bank/email ingestion, when documents enter the queue, then dedupe runs asynchronously with enqueue-to-index P95 ≤ 5 seconds and no synchronous UI blocking calls. Given a mobile upload requesting an immediate check, when processed via the real-time path, then the API returns "duplicate"/"no_duplicate"/"needs_review" within 800 ms P95. Given async workers are degraded, when fallback is used, then a "deferred" response is returned within 500 ms and background dedupe completes within 10 minutes P99 with index updated and client notified.
Internal Match API and Dedupe Index Performance & Scalability
Given a documentId or fingerprint, when the internal matches API is queried, then the top 10 candidates with confidence scores and reason codes are returned in ≤ 200 ms P95 with idempotent, paginated responses. Given a dedupe index of 1,000,000 documents, when sustained query load of 10 RPS for 10 minutes is applied, then error rate ≤ 0.1%, P95 latency ≤ 250 ms, and CPU utilization ≤ 60% on the provisioned tier. Given ingestion of 1,000,000 new documents, when indexing completes, then average dedupe entry storage overhead ≤ 4 KB/document, write amplification ≤ 1.5×, and any required backfill completes within 24 hours within budgeted compute.
Cross-Source Correlation & Transaction Matching
"As a user, I want duplicates from different sources to be recognized as the same receipt so that my ledger doesn’t create multiple transactions for one purchase."
Description

Correlate duplicates across ingestion channels (camera uploads, email forwards, cloud drive imports, and scanner PDFs) and align them to the same underlying expense transaction. Combine content fingerprints with normalized financial metadata (total, currency, vendor, last-4 of card, and posting date) to strengthen matches. When a duplicate corresponds to an existing transaction, auto-link it and suppress additional transaction creation. Provide deterministic tie-breakers and confidence thresholds to ensure consistent outcomes and prevent ledger drift.

Acceptance Criteria
Cross-Source Auto-Linking to Existing Transaction
Given an existing expense transaction T with a linked receipt R1 from any source And a new receipt R2 is ingested from a different source And R2's perceptual fingerprint matches R1 within threshold (Hamming distance <= 3) And normalized metadata matches T: total matches within ±0.01 in the same currency, vendor similarity >= 0.90, card_last4 exact match, posting_date within ±3 calendar days When DupeShield processes R2 Then R2 is auto-linked to transaction T And no new transaction is created And R2 is not shown as a standalone item in the receipt inbox And an audit log entry is created with confidence score >= 0.90 and reasons including fingerprint and metadata matches
Prevent Double Attachment on Repeat Ingest
Given transaction T already has a primary receipt R_primary attached And a duplicate receipt R_dup of the same content is ingested from any source When DupeShield evaluates R_dup Then DupeShield does not add a second attachment of the same content to T And R_dup is marked as Suppressed Duplicate referencing R_primary And the audit log records suppression with confidence >= 0.90
Merge Multi-Source Pages into Single Receipt
Given a multi-page receipt where pages are ingested across sources (e.g., page 1 via camera, page 2 via email, page 3 via scanner PDF) And the pages share matching vendor and total across pages or OCR page indicators And page capture timestamps are within 30 minutes of each other When DupeShield processes these pages Then pages are merged into one receipt entity in correct sequence And duplicate pages (same fingerprint) are removed And the merged receipt is linked to a single transaction T And no additional transactions are created
Deterministic Tie-Breakers for Multiple Candidate Transactions
Given a receipt R yields multiple candidate transactions T1..Tn with scores within 0.02 of each other When DupeShield selects a transaction to link Then tie-breakers are applied in order: card_last4 (exact match) > total (exact match) > highest vendor similarity > smallest absolute posting date difference > most recent transaction creation timestamp And the same transaction is selected on repeated runs with identical inputs And the audit log records the applied tie-breaker sequence and final decision
Confidence Threshold Buckets and Actions
Given a receipt R with computed match confidence score S in [0,1] When S >= 0.90 Then R is auto-linked to the top candidate transaction and no new transaction is created When 0.70 <= S < 0.90 Then R is placed in a Needs Review queue and no auto-link is made And automatic transaction creation from R is paused pending review When S < 0.70 Then R is treated as a new receipt candidate with no link to existing transactions And transaction creation follows the standard (non-duplicate) pipeline And each outcome is logged with score S and top contributing features
Non-Match Guardrails for Similar Same-Day Purchases
Given two same-vendor purchases on the same day with similar totals exist for a user And a receipt R matches one transaction by card_last4 and posting_date within ±1 day but not the other When DupeShield processes R Then R is linked only to the transaction with matching card_last4 and date tolerance And no link is made where currency differs or card_last4 mismatches And no existing transaction links are altered
Accuracy and Latency SLAs
Given a validation dataset of at least 1,000 real-world receipts with ground-truth links When DupeShield runs matching end-to-end in staging Then auto-link precision is >= 98.0% and recall is >= 95.0% for items with S >= 0.90 And p95 decision latency from ingestion to decision (link/suppress/review) is <= 5s and p99 is <= 10s And 100% of decisions emit an audit log with fields: receipt_id, candidate_count, selected_transaction_id (if any), score, threshold_bucket, tie_breakers_applied, reasons
Multi-Page Receipt Merge
"As a consultant, I want multi-page receipts to be merged into one clean document so that I can review and share a complete record without manual stitching."
Description

Automatically detect sequential pages of the same receipt or invoice and merge them into a single receipt object with preserved page order. Generate a unified PDF/PNG stack, consolidate extracted fields (totals, taxes, line items) with page-level provenance, and retain originals for auditability. Provide a UI preview and server-side merge service that handles mixed orientations, varying page sizes, and partial page overlaps. Store a merge manifest to enable re-splitting if needed.

Acceptance Criteria
Auto-Detect and Order Sequential Receipt Pages
Given multiple receipt page images belonging to the same invoice captured within a 15-minute window, When DupeShield processes them, Then it groups them into a single merge candidate if vendor/invoice/date signals match above the configurable threshold (default 0.85) and excludes unrelated pages. Given grouped pages containing explicit page markers (e.g., "Page 1/3"), When ordering is computed, Then the final order matches the numeric markers; otherwise it falls back to capture timestamp ascending; ties are resolved deterministically by filename ascending. Given a set including pages from two different invoices of the same vendor, When processing, Then pages with differing invoice IDs or totals are never merged into the same candidate. Given pages with mixed orientations and sizes, When ordered, Then the computed order is persisted exactly in the merge manifest.
Generate Unified PDF/PNG Stack with Mixed Orientations
Given a confirmed merge candidate of up to 10 pages, When the merge is executed, Then a unified PDF is generated with all pages in the manifest order, each page auto-rotated upright using EXIF/visual cues without cropping, and page dimensions preserved per page. Given the merged PDF is created, When preview assets are produced, Then a PNG stack is generated (one PNG per page) with max dimension 2048 px and minimum effective 300 DPI for legibility. Given the server reference environment, When merging up to 10 pages averaging ≤5 MB each, Then the merge completes within 3 seconds and emits file size, page count, and SHA-256 checksums. Given the merge completes, When the client requests the file, Then the merged file is accessible via a download URL and stored under the merged receipt object.
Consolidate Extracted Fields with Page-Level Provenance
Given OCR/field extraction results per page, When merging, Then consolidated fields are computed as: invoice number -> most frequent non-empty value; totals/taxes -> value from the last page summary if present; otherwise sum of line items; currency -> dominant detected currency. Given fields are consolidated, When saving the merged receipt, Then each field includes provenance entries of (page_index, bbox, confidence) for every contributing source. Given conflicting field values exist (e.g., two distinct invoice numbers with confidence ≥0.7), When consolidating, Then the field is marked "conflict" and surfaced for review in the UI with links to source pages. Given multi-page line items, When consolidating, Then line items are concatenated across pages preserving page order and each line item retains page_index provenance.
Retain and Link Original Artifacts for Audit
Given a successful merge, When the merged receipt object is created, Then all original files remain stored immutable and linked via the manifest; their storage URIs and checksums are preserved. Given the merged receipt exists, When the user requests originals, Then a ZIP containing all original files is downloadable from the merged receipt detail screen. Given a user deletes the merged receipt, When no explicit "Delete originals as well" confirmation is given, Then the original files are not deleted. Given any merge action occurs, When audit logs are recorded, Then entries include actor, timestamp, source IDs, checksums, and manifest version.
UI Preview and Manual Reorder Before Merge
Given a merge candidate is detected, When the user opens the preview, Then a thumbnail gallery shows all pages in proposed order with page count and vendor label. Given the preview is open, When the user drags to reorder, rotates by 90° increments, or removes a page, Then the preview updates immediately and the manifest draft reflects the changes. Given at least two pages are present, When the user taps "Confirm Merge," Then the final manifest is persisted and the server-side merge is triggered. Given a typical 4G connection, When loading up to 10 thumbnails, Then the preview loads within 1 second at p75 and tap interactions respond within 100 ms at p75. Given accessibility settings are enabled, When navigating the preview, Then screen readers announce page index and available actions for each page.
Server-Side Merge Handles Overlaps and Size Variance
Given adjacent images that are duplicate captures of the same page, When similarity >0.9 by perceptual hash and overlap area >80%, Then one image is excluded as a duplicate and the exclusion is recorded in the manifest. Given pages with varying sizes and aspect ratios, When rendering the unified PDF, Then each page is centered on a canvas matching its native size; no content is cropped and no page is upscaled beyond 2x; EXIF rotations are applied. Given any input file is unreadable or corrupt, When the merge is attempted, Then a structured error with remediation guidance is returned and no partial merge is stored. Given the same manifest is submitted multiple times, When the service processes it, Then the output checksums are identical (idempotent).
Merge Manifest Enables Re-splitting and Recovery
Given a merged receipt object exists, When a user selects "Re-split," Then the system restores the original items to their pre-merge state (including tags, categories, and transaction links) and archives the merged object. Given a merged receipt is saved, When persisting the manifest, Then it contains: source_file_ids, page_order, per-page rotation, excluded_duplicates, field_provenance, checksums, created_by, created_at, and version. Given API access, When calling GET /receipts/{id}/manifest and POST /receipts/{id}/resplit as the receipt owner, Then the endpoints return 200 with the manifest and perform the re-split respectively. Given a re-split occurs, When the user re-merges the same items, Then a new manifest is created with version incremented and the previous manifest remains retrievable for audit.
Attachment Deduplication Guard (Per-Transaction)
"As a user, I want the app to stop me from attaching the same receipt twice to a transaction so that my records stay clean and accurate."
Description

Prevent the same document (or near-duplicate) from being attached more than once to a single transaction. On attach events (auto or manual), check against the transaction’s attachment set using fingerprints and metadata signatures; block duplicates with a clear, actionable message and offer to view the already-attached file. Provide override options for admins and log all blocks for audit. Ensure minimal latency so user experience remains responsive on mobile and web.

Acceptance Criteria
Block Exact Duplicate On Same Transaction
Given a transaction T with existing attachment A with content_hash H When a user attempts to attach a file F where content_hash(F) == H from any channel (web, mobile, email, API) Then the system blocks the attach And shows a message "This file is already attached to this transaction" And shows a primary action "View existing attachment" And no new attachment record is created or queued And an audit event duplicate_block is written with fields {transaction_id, existing_attachment_id, incoming_source, fingerprint_type: "content_hash", fingerprint_value: H, user_id, timestamp}
Block Near-Duplicate By Perceptual Match And Metadata
Given configured thresholds: pHash_distance <= 6 OR (merchant & amount match AND date within +/- 48h) When a user attempts to attach file F that meets near-duplicate criteria against any attachment on T Then the system blocks with message "Possible duplicate detected" And provides actions "View existing attachment" and "Override (admin only)" And for non-admin users the Override action is disabled Given an admin user When Override is selected and a justification of at least 10 characters is submitted Then the attachment is saved and linked And an audit event duplicate_override is written with fields {transaction_id, existing_attachment_id, new_attachment_id, matcher, match_metrics, justification, admin_user_id, timestamp}
Cross-Channel Deduplication Coverage
Given channels {manual upload (web), camera capture (mobile), email-forward, automated bank-feed OCR attach, public API} When an attachment is attempted from any channel Then duplicate checks (content_hash and near-duplicate rules) are applied before creation And duplicate blocks prevent creation uniformly across channels And the block message text and action labels are consistent across web and mobile And email-forwarded duplicates trigger a notification email to the sender with a link to view the existing attachment
View Existing Attachment From Block State
Given a duplicate is blocked for transaction T When the user selects "View existing attachment" Then the app opens the attachment viewer focused on the matched existing attachment And highlights it within T's attachment list And preserves back navigation to the originating screen And no new attachment is created
Permissions And Override Safeguards
Given a non-admin user When a duplicate is detected Then no override control is shown or enabled And the block cannot be bypassed Given an admin user When a duplicate is detected Then an Override control is visible And selecting it requires entering a justification (min 10 characters) and choosing a reason code from {client-request, legal-requirement, system-error, other} And overriding creates the attachment and links it to T And the audit event includes reason_code and justification
Performance And Latency Targets
Given a transaction with up to 50 existing attachments When an attach attempt is made Then server-side duplicate decision time is <= 120ms p95 and <= 250ms p99 And for transactions with 51–200 attachments, decision time is <= 180ms p95 and <= 350ms p99 And the client displays a success/block state within 800ms p95 on web and 1000ms p95 on mobile (4G reference profile) And no loading indicator persists longer than 2 seconds without explanatory text
Audit Trail And Reporting
Given any duplicate block or admin override occurs Then an immutable audit record is stored with fields {transaction_id, user_id, role, source_channel, fingerprint_types_used, matched_attachment_id, match_metrics, action: block|override, reason_code, justification, timestamp, client_app_version} And audit records are visible in the Admin > Audit Log, filterable by date range, user, transaction, and action And audit records are exportable as CSV and JSON And timestamps are recorded in UTC with millisecond precision
Duplicate Review Queue & Resolution Workflow
"As a busy freelancer, I want a simple queue to review and resolve duplicate suggestions so that I can keep my books tidy with minimal effort."
Description

Introduce a dedicated review queue that lists suspected duplicates with confidence scores, source channels, and suggested actions (merge, link, dismiss, delete). Support bulk operations, keyboard shortcuts, and mobile-friendly triage. When merging, allow selection of canonical document, page order, and field precedence rules. Capture user decisions to improve future matching via feedback loops. Integrate notifications for items exceeding age or confidence thresholds.

Acceptance Criteria
Queue Displays Suspected Duplicates with Metadata
Given a user opens the DupeShield review queue with suspected duplicates present, When the queue loads, Then each item displays a thumbnail, page count, source channel(s), first-seen timestamp, associated transaction (if any), a 0–100 confidence score (1-decimal precision), and suggested actions (Merge, Link, Dismiss, Delete) with the top suggestion preselected. Given suspected duplicates are present, When the user filters by confidence (e.g., >= 80) and/or source channel, Then only items matching the filter appear and the result count updates in real time. Given the queue contains up to 500 items, When the user sorts by Confidence, Age, or Source, Then the sorted list renders within 200 ms on desktop and 400 ms on mobile. Given no suspected duplicates exist, When the user opens the queue, Then an empty state with guidance and a link to import sources is shown.
Bulk Actions and Keyboard Shortcuts for Triage
Given the user selects multiple duplicate groups via checkboxes or Shift-click range selection, When 2–200 groups are selected, Then a bulk action bar appears with Merge, Link, Dismiss, and Delete enabled and shows the count selected. Given 2–200 groups are selected, When the user invokes a bulk action, Then progress is shown, partial failures are reported per group, and successful groups are updated in the UI without page reload. Given the queue has focus, When the user presses M (Merge), L (Link), I (DismIss), or X (Delete), Then the corresponding action is triggered for the focused group; Arrow keys move focus; Ctrl/Cmd+A selects all; ? opens a shortcuts help overlay. Given a bulk action completes, When the user clicks Undo within 10 minutes, Then the operation is reversed for all affected groups and documents, restoring prior state. Given up to 50 groups are bulk merged, When processing completes, Then 95th percentile completion time is ≤ 5 seconds and no group is merged twice (idempotency).
Mobile-Friendly Triage Workflow
Given a device viewport width ≤ 414 px, When the user opens the review queue, Then items render in a single-column mobile layout with tap targets ≥ 44x44 px and a sticky action bar. Given a mobile user views an item, When they swipe right or left, Then right triggers Merge and left triggers Dismiss with haptic feedback and an on-screen confirmation, both with Undo. Given the queue loads on a 4G connection, When the first screen is requested, Then the first 10 items render in ≤ 2 seconds and subsequent items lazy-load as the user scrolls. Given accessibility services are enabled, When navigating the queue, Then all actions are reachable via screen reader, have labels, and meet WCAG 2.1 AA color contrast.
Merge Workflow with Canonical Selection and Page Ordering
Given a duplicate group with ≥ 2 documents, When the user opens Merge, Then they can select a canonical document (default is the highest-confidence document) before confirming. Given documents in the merge dialog, When the user reorders pages via drag-and-drop, Then the new order is reflected in a live preview and persisted in the merged output. Given conflicting fields (date, amount, vendor, category, memo, tax) across documents, When the user selects field precedence rules (Prefer Canonical, Newest Value, Highest OCR Confidence), Then the merged document metadata uses those rules. Given the user confirms Merge, When processing completes, Then a single consolidated receipt (PDF) is created with the chosen page order and fields, linked to the relevant transaction(s), and an audit log records pre-merge IDs, canonical ID, precedence rules, user ID, and timestamp. Given a merge of up to 10 pages, When the operation runs server-side, Then completion time is ≤ 3 seconds and the action is idempotent on retry.
Linking Prevents Double Attachments to the Same Transaction
Given a duplicate document is already linked to Transaction T, When a user attempts to attach an additional near-duplicate to T, Then the system blocks a second attachment and shows options: View Existing, Replace, or Merge as Additional Page. Given the user selects Replace, When confirmation is accepted, Then the previous attachment is detached, the new one is attached, and the audit trail records the replacement with both document IDs. Given the user selects Merge as Additional Page, When confirmation is accepted, Then the documents are merged into a single attachment for T in the specified page order. Given attachment rules are enforced, When any workflow attempts to attach a duplicate to T, Then a uniqueness constraint prevents more than one duplicate attachment per transaction.
Feedback Loop Captures User Decisions for Model Improvement
Given a user performs Merge, Link, Dismiss, or Delete on a duplicate group, When the action completes, Then a feedback event is stored containing user_id, action_type, doc_ids, group_id, model_version, confidence_at_decision, features_snapshot, optional reason, timestamp, and outcome label. Given feedback events are stored, When the nightly pipeline runs, Then events are exported to the training store within 24 hours with PII hashed and retained for ≥ 400 days unless the user opts out. Given a pair or group is Dismissed as Not Duplicate, When a similar group would be queued again, Then a suppression rule prevents resurfacing above the user’s threshold for 180 days unless new evidence increases confidence by ≥ 20 points. Given the model version updates, When evaluating post-update, Then previously dismissed exact pairs do not reappear with confidence > 50.
Notifications for Age and Confidence Thresholds
Given user-configured thresholds exist (confidence default 90, age default 7 days), When a group exceeds either threshold, Then the user receives an in-app badge and is included in the next daily email digest at 08:00 local time; push is sent if enabled. Given a notification is sent, When the user acts on all items in the queue, Then the badge count resets to zero within 5 seconds and no further notifications are sent for cleared items. Given multiple threshold breaches in one day, When compiling digests, Then at most one email and one push are sent per day with de-duplicated content and deep links to the queue. Given quiet hours are set, When a threshold is met during quiet hours, Then notifications are deferred until quiet hours end or included only in the next digest.
Audit Trail, Rollback, and Compliance Export
"As a user preparing for taxes, I want a clear audit trail and the ability to undo dedupe actions so that I can satisfy audits and fix mistakes without data loss."
Description

Maintain an immutable audit log for all dedupe actions (detections, merges, blocks, dismissals) including actor, timestamp, confidence, and before/after state. Support one-click rollback for merges and deletions within a retention window and ensure referential integrity across transactions and tax packets. Provide exportable audit reports (CSV/PDF) suitable for CPA review and IRS substantiation, and surface event hooks to downstream reporting so packets reflect final, de-duplicated documents.

Acceptance Criteria
Immutable Audit Logging for Dedupe Actions
Given a dedupe action of type detect, merge, block, dismiss, or delete completes When the action is committed Then an audit record is appended within 500 ms containing: event_id (UUIDv4), action_type, actor_type, actor_id, timestamp (ISO-8601 UTC), transaction_id (if any), document_id(s), confidence (0.0–1.0), before_state JSON, after_state JSON, reason_code, request_id, hash_prev, hash_self And the audit record is durable and queryable Given the audit store When any attempt is made to update or delete an existing audit record via API or internal service Then the operation is rejected and logged, preserving append-only immutability Given two consecutive audit records When validating their hashes Then record[n].hash_prev equals record[n-1].hash_self, forming a tamper-evident chain for the entire log Given a query by transaction_id or document_id When retrieving audit history Then all related records are returned in chronological order with no gaps, and the count equals the number of committed actions
One-Click Rollback of Merge Within Retention Window
Given a merge action exists within the configured retention window When the user triggers one-click rollback via UI or API Then the pre-merge documents are restored as active records, the merged composite is archived, and all transaction attachments are re-pointed to the restored documents And a rollback audit record is appended referencing the original merge event_id And the user's view and APIs reflect the restored state within 60 seconds Given the same rollback is requested again When the rollback endpoint is called Then the operation is idempotent and returns a no-op response without duplicating audit entries Given a merge action is older than the configured retention window When rollback is attempted Then the operation is blocked with an explicit error (HTTP 403) and a message indicating the retention policy Given integrity checks run after rollback When validating references Then zero broken references exist across transaction->document and tax_packet->document relations
One-Click Restore of Deleted Duplicate Within Retention Window
Given a dedupe-driven deletion (duplicate removal) occurred within the configured retention window When the user triggers one-click restore Then the deleted document is reinstated with its prior metadata and attachment relationships, and any placeholder or tombstone is cleared And a rollback audit record is appended referencing the original delete event_id And integrity checks report zero broken references Given the restore is attempted outside the retention window When the user triggers restore Then the operation is blocked with HTTP 403 and explanatory message referencing policy Given repeated restore requests for the same deletion When the endpoint is called multiple times Then the operation is idempotent and does not create duplicate documents or audit entries
Compliance Audit Export (CSV and PDF) for CPA/IRS Review
Given a user with export permission selects a date range and optional filters (actor, transaction_id, packet_id) When the user requests an audit export Then the system generates both CSV and PDF files within 2 minutes for up to 50,000 audit records And each record in the export includes: event_id, action_type, actor_type, actor_id, timestamp (ISO-8601 UTC), transaction_id, document_id(s), confidence, reason_code, before_state summary (diff), after_state summary (diff), related_event_ids (e.g., rollback_of) And the CSV uses UTF-8 with headers and RFC 4180 quoting; the PDF includes a cover page, table of contents, and page numbers And each file is accompanied by a SHA-256 checksum and creation metadata (export_id, generated_at) And the download links are secured, audit-logged, and expire in 24 hours Given the export is opened by a CPA When cross-checking totals for actions in the date range Then counts per action_type in the cover summary match the number of detailed records
Downstream Event Hooks Propagate Dedupe Outcomes to Reporting and Tax Packets
Given any dedupe action is committed When the action completes Then an event is emitted to the reporting stream within 1 second containing schema_version, event_id, correlation_id (request_id), action_type, actor_type, actor_id, transaction_id, document_id(s), confidence, before_state hash, after_state hash, and occurred_at (UTC) And event delivery is at-least-once with an idempotency key so downstream consumers can safely de-duplicate Given the packet builder service subscribes to the stream When it receives a merge, delete, or rollback event Then the affected tax packets are updated to reflect the final de-duplicated documents within 60 seconds And the packet version is incremented and recorded in the audit log with linkage to the triggering event_id Given a delivery failure to a subscriber When retries are exhausted Then the event is placed on a dead-letter queue and an alert is emitted; the original event remains available for manual reprocessing
Audit Log Retention, Legal Hold, and Access Control
Given the system retention policy is configured When audit records age beyond the retention window and are not under legal hold Then records are purged from hot storage and retained only per policy (e.g., cold archive) with an audit of the purge operation Given a legal hold is applied to a scope (user, transaction, date range) When retention would otherwise purge records Then those records are preserved until the hold is cleared, and the hold is visible in administrative reports Given an authenticated request to read audit records or exports When the requester lacks the required role or scope Then access is denied with HTTP 403 and the attempt is audit-logged without exposing record contents Given an administrator queries audit retention status When retrieving policy and metrics Then the system reports current retention window, volume by tier (hot/cold), and count of records under legal hold

Offline SnapSync

Snap receipts anywhere—planes, subways, spotty cafés. Offline SnapSync stores a secure fingerprint (time, location, total) and auto-matches to your bank feed once you’re back online, notifying you when the pair is confirmed and categorized.

Requirements

On-Device Offline Receipt Capture
"As a freelance consultant, I want to snap receipts when I’m offline so that I don’t lose expenses captured on planes or subways."
Description

Enable users to capture receipt photos without connectivity and securely cache the originals plus essential metadata (timestamp, coarse location when permitted, device ID, and a thumbnail) on-device. Store all assets encrypted at rest using OS keystore/secure enclave, generate an image hash for deduplication, and queue items for background sync. Provide clear UI states (offline badge, queued count, storage usage warnings) and graceful handling of low storage, app termination, and intermittent connectivity. Compress images, correct orientation, and minimize battery impact. Integrate with the existing receipt ingestion pipeline by emitting a standardized payload for sync, and ensure privacy by collecting only necessary metadata and honoring system permissions.

Acceptance Criteria
Offline Receipt Photo Captured and Encrypted On-Device
Given the device has no network connectivity and the user captures a receipt photo When the capture completes Then the original image is saved encrypted at rest with a key sourced from the OS keystore/secure enclave And a 256px longest-edge thumbnail is generated and stored encrypted And metadata saved includes timestamp_utc (ISO 8601), coarse_location (if permission granted), and an app-scoped device_instance_id And the files are not readable as plaintext via the device file browser or adb without the app process And the assets persist after force-closing the app and after a device reboot
Metadata Collection Honors System Permissions
Given location permission is denied When a receipt is captured offline Then no location coordinates are stored and location fields are absent in metadata Given location permission is granted When a receipt is captured offline Then only coarse_location is stored as latitude/longitude rounded to 2 decimal places and no raw GPS or EXIF geotags are retained And the device identifier stored is a random app-scoped UUID (not hardware-tied) And metadata is limited to: timestamp_utc, coarse_location (optional), device_instance_id, image_hash, orientation, compression_level, thumbnail_size
Image Compressed and Orientation Corrected Offline
Given a receipt photo up to 12 MP is taken offline When the app processes the image Then the image is downscaled so the longer edge is <= 2048 px And JPEG compression is applied so the file size is <= 800 KB And EXIF orientation is applied so the image displays upright And all EXIF location tags are stripped And processing completes within 1500 ms on a mid-tier device and does not exceed 30% CPU for >2 seconds And no network radios are woken during offline processing
Image Hash Generated and Duplicate Prevention
Given a receipt is captured offline When the image is normalized Then a SHA-256 hash of the normalized image bytes is generated and stored Given a second capture produces the same hash while an item with that hash is queued or unsynced within 30 days When the user attempts to save Then a duplicate alert is shown and the second capture is not enqueued unless the user selects "Save anyway" And if overridden, both items are enqueued with a duplicate flag
Queued Sync with Intermittent Connectivity
Given at least one offline-captured item is queued When connectivity is restored Then queued items are uploaded in capture timestamp order And on network failure, retries use exponential backoff up to 5 attempts per item before marking error and remaining queued And partial uploads resume from the last successful chunk without creating duplicate server records And sync defers when system Data Saver is on or battery < 15% unless the user manually initiates sync And upon successful server acknowledgment, local records are marked synced and eligible for encrypted cache cleanup rules
Offline UI Indicators and Storage Warnings
Given the device goes offline When the user opens the capture screen Then an Offline badge is visible within 1 second Given items are captured offline When each capture completes Then the queued count increments within 1 second Given app cache reaches 80% of its configured on-device quota When the user opens the capture screen Then a storage usage warning is shown with an action to manage space Given free disk space is < 50 MB When the user attempts to capture Then the capture is blocked with an explanatory message and no partial data is stored
Standardized Payload Emitted for Ingestion Pipeline
Given a queued item is ready to sync When it is sent to the ingestion pipeline Then the payload conforms to schema version v3 with fields: image_hash (SHA-256 hex), timestamp_utc (ISO 8601), coarse_location (lat/lon rounded to 2 decimals, optional), device_instance_id (UUIDv4), orientation, compression (format, quality), and references to encrypted original and thumbnail And the receiving endpoint returns HTTP 200 and creates a server receipt record ID And the app stores the server record ID and maps it to the local item
On-Device OCR Total Extraction
"As a freelance creative, I want the app to pull the total from the receipt photo offline so that I don’t have to retype it later."
Description

Perform lightweight, on-device OCR to extract the receipt total and currency while offline, producing a confidence score and capturing supporting fields (tax, tip, subtotal) when available. Preprocess images (denoise, deskew, contrast) to improve accuracy under low light or motion blur. Support regional number formats and multiple currencies, choose the paid total when subtotal/tip/total are present, and allow quick manual correction if confidence is low or OCR fails. Package the model for offline use with incremental model updates via optional assets; avoid any network calls while offline. Persist extracted values and confidence into the offline record so they can be included in the fingerprint and used for matching upon sync.

Acceptance Criteria
Offline Extraction of Total and Currency
Given the device is offline and a receipt image (8–12 MP, JPEG/HEIC) is captured via Offline SnapSync When on-device OCR runs Then it extracts a numeric total amount and currency symbol/code And returns a confidence score between 0.00 and 1.00 And completes in ≤ 3 seconds on the reference device profile And makes zero network calls during the operation
Preprocessing Improves OCR Under Poor Capture
Given a receipt photo with low light (EV ≤ 3), motion blur (~1/15s), or skew ≤ 15° When preprocessing runs (denoise, deskew, contrast normalization) Then OCR proceeds without crash And the extracted total has confidence ≥ 0.60 on the provided test set And skew correction reduces tilt to ≤ 2° in the processed image
Support for Regional Number Formats and Multiple Currencies
Given receipts with the following printed totals: "$1,234.56", "1.234,56 €", "¥1,234", "CHF 1’234.50" When OCR runs offline Then the parsed total numeric values are 1234.56, 1234.56, 1234.00, and 1234.50 respectively And the currency is identified as USD, EUR, JPY, and CHF respectively And decimal/grouping separators are correctly interpreted per locale
Prioritize Paid Total Over Subtotal and Tip
Given a receipt image that contains Subtotal, Tip, and Total lines with amounts When OCR extracts line-items and amounts Then the selected total equals the "Total" line amount And if no "Total" line is present but "Subtotal" and "Tip" are present, the selected total equals Subtotal + Tip within ±0.01 of the displayed sum And supporting fields Subtotal, Tax, and Tip are captured when present
Manual Correction Flow for Low Confidence or OCR Failure
Given OCR confidence for total is < 0.70 or total/currency is not detected When the user opens the captured receipt while offline Then a compact correction UI is shown with editable fields for Total, Currency, Subtotal, Tax, Tip And the user can correct the value in ≤ 3 taps without network connectivity And upon save, the corrected values are persisted, flagged as user-corrected, and confidence is set to 1.00
Persist Offline OCR Results into Fingerprint for Matching
Given an offline capture where OCR has produced values (total, currency, confidence, optional subtotal/tax/tip) When the offline record is written to storage Then the fingerprint includes timestamp, location (if permitted), total, currency, confidence, and optional fields And upon next sync, the matching module receives these fields from the record And the values are unchanged from what was persisted offline
Offline-Only Operation and Incremental Model Updates
Given the device is offline (Airplane Mode) When multiple OCR extractions are performed Then no outbound network requests are initiated or queued by the OCR pipeline And a base OCR model bundled with the app is used Given the device later goes online and an optional model asset update is available When the asset is downloaded Then subsequent offline OCR uses the updated model version And if the asset is missing or corrupted, the system falls back to the bundled model without failure
Secure Metadata Fingerprint & Tamper Protection
"As a self-employed user, I want my offline captures to be trusted and verifiable so that my tax records hold up during audits."
Description

Generate a cryptographic fingerprint that binds the receipt image hash, extracted total, currency, capture timestamp, and coarse location into a tamper-evident record signed with a device-held key. Store the fingerprint alongside the offline record and transmit it first during sync to enable early matching. Ensure keys are managed via platform secure storage with rotation support. Include versioning for fingerprint schema, handle clock skew via server reconciliation, and maintain a minimal audit trail linking the fingerprint to the final matched transaction. Conform to privacy regulations by minimizing PII in the fingerprint and enabling data deletion on request.

Acceptance Criteria
Device-Signed Fingerprint Creation Offline
Given the device is offline and a receipt photo is captured, When total and currency are extracted, Then the app computes a SHA-256 hash of the original image bytes and constructs a canonical payload {imageHash,total,currency,captureTimestamp,coarseLocation,schemaVersion,deviceKeyId}. Given a hardware-backed private key exists in platform secure storage, When the payload is signed using ECDSA P-256, Then a signature is produced with the non-exportable key and the fingerprint+signature are stored encrypted alongside the offline record.
Tamper-Evident Verification
Given any field in the fingerprint payload or the associated image bytes are altered after capture, When signature verification is performed client-side and server-side, Then verification fails, the record is flagged as tampered, and it is excluded from matching. Given the payload and image are unmodified, When verification is performed, Then verification succeeds consistently and the verification result is logged with timestamp and deviceKeyId. Given a fingerprint is replayed from a different device, When the server validates deviceKeyId against the account's registered public keys, Then the request is rejected.
Fingerprint-First Sync and Early Match Trigger
Given network connectivity resumes, When sync starts, Then the client uploads fingerprint envelopes before images and rich OCR data. Given the server receives a valid fingerprint, When early match is attempted, Then the server responds within 2 seconds with status pending|matched|rejected and queues a push/in-app notification upon matched. Given constrained bandwidth conditions, When sync runs, Then at least 95% of fingerprints are transmitted within 5 seconds of connectivity regain.
Key Management and Rotation with Backward Verification
Given platform secure storage (iOS Keychain/Secure Enclave, Android Keystore) is available, When generating the signing key, Then the key is hardware-backed, non-exportable, user-locked, labeled with deviceKeyId and creation date. Given a time-based (>=12 months) or manual rotation event occurs, When a new key is generated and activated, Then new fingerprints include the new deviceKeyId, and the server retains prior public keys to verify historical fingerprints. Given any attempt to export or read private key material, When the operation is invoked, Then the platform denies access and no key material leaves secure storage.
Fingerprint Schema Versioning
Given every fingerprint includes schemaVersion (major.minor), When the server receives version 1.x, Then it is accepted; when it receives an unknown major version, it is rejected with HTTP 400 and a descriptive error. Given schema evolution adds optional fields, When an older client submits v1.0 fingerprints, Then the server processes them successfully and defaults missing fields without data loss. Given canonicalization rules are defined, When two clients sign identical logical data, Then the serialized payload bytes are identical prior to signing.
Clock Skew Reconciliation
Given the device captureTimestamp differs from server time by more than ±5 minutes, When the fingerprint syncs, Then the server sets canonicalTimestamp = serverReceivedAt and stores skewDelta (in seconds), and matching uses canonicalTimestamp. Given skew is within ±5 minutes, When the fingerprint syncs, Then the device captureTimestamp remains canonical. Given extreme skew (>24 hours), When the fingerprint syncs, Then it is accepted but flagged for review and excluded from auto-matching until user confirms the time.
Privacy Minimization and Minimal Audit Trail with Deletion
Given coarse location is recorded, When a fingerprint is created, Then location precision is limited to a radius >= 5 km (e.g., geohash length 5 or lat/lon rounded to 2 decimals) and no precise GPS, merchant name, or card PAN is embedded. Given an audit trail is written on match, When a fingerprint links to a bank transaction, Then the trail stores only {fingerprintId, transactionId, createdAt, matchedAt, verifyStatus, schemaVersion} and excludes raw images and PII. Given a user submits a data deletion request, When the request is processed, Then fingerprints, signatures, and audit links are removed from client and server within 30 days, and deletion is reflected in user data export.
Resilient Background Sync & Retry
"As a busy freelancer, I want receipts to sync automatically when I’m back online so that I don’t have to remember to upload them."
Description

Implement a background sync service that detects connectivity restoration and uploads queued receipts and metadata using encrypted transport. Support exponential backoff, resumable/chunked uploads, and idempotent operations to avoid duplicates. Respect battery saver, data saver, and metered network settings; pause/resume intelligently and continue after app restarts. Provide user-visible progress and error states, automatic deduplication via image/hash checks, and robust error logging/telemetry for failure analysis. Integrate with the ingestion pipeline to ensure items transition from queued to synced states reliably.

Acceptance Criteria
Auto Sync on Connectivity Restoration
- Given the device has at least one receipt in the local queue and no active connectivity, When connectivity is restored to an allowed network (Wi‑Fi or permitted cellular) and remains stable for 5 seconds, Then the background sync service starts within 10 seconds without requiring the app to be in the foreground. - Given an upload is initiated, Then all API calls use TLS 1.2+ with no plaintext fallback; if secure transport cannot be established, Then the upload is aborted and marked "Error: Secure Transport" with no payload transmitted. - Given a receipt enters sync, Then its state transitions from "Queued" -> "Syncing" -> "Uploaded" are persisted locally and survive process kills or device reboot.
Exponential Backoff & Retry with Telemetry
- Given a transient error (HTTP 408/429/5xx, timeout, DNS failure), When a receipt upload fails, Then retries use exponential backoff with jitter: initial delay 2s, factor 2.0, ±20% full jitter, max delay 5 minutes. - Given HTTP 429 or a Retry-After header is present, Then the next retry delay honors Retry-After if greater than the calculated backoff. - Given 8 consecutive retry attempts fail, Then the item is marked "Error: Retry Exhausted" and the user is notified with an actionable message; no further automatic retries occur until connectivity changes or the user taps "Retry". - Given any retry occurs, Then telemetry records event sync_retry with fields: item_id, attempt_number, error_code, network_type, power_state, data_saver, metered, timestamp; PII is excluded.
Resumable Chunked Uploads with Integrity Verification
- Given a receipt image or attachment larger than 1 MB or when on unreliable connectivity, Then the upload is performed in chunks of 512 KB with a server-issued session id. - Given connectivity loss mid-upload, When connectivity is restored, Then the client resumes from the last server-acknowledged chunk within 15 seconds without re-uploading completed chunks. - Then the server validates a final SHA-256 checksum of the full payload; If checksum mismatch, Then only the mismatched chunks are retransmitted; on success the item transitions to "Uploaded".
Idempotent Operations & Deduplication
- Given an upload request is resent due to retry or resume, Then an idempotency key derived from a stable hash (SHA-256 of canonicalized image bytes + metadata fingerprint) is included, resulting in exactly one server-side receipt record regardless of duplicate submissions. - Given the same receipt image is captured multiple times, Then perceptual hash and normalized metadata (timestamp ±2 minutes, total ±$0.01, location radius 100 m) are compared; When a probable duplicate is detected (≥95% similarity), Then the item is deduplicated or linked rather than duplicated, and UI shows "Merged duplicate". - Given idempotency prevents duplication, Then the server returns HTTP 200 with the existing resource id instead of creating a new record; the client updates local mapping accordingly.
Policy-Aware Sync on Battery/Data Saver and Metered Networks
- Given OS Battery Saver is ON, Then background sync pauses within 5 seconds and displays "Paused: Battery Saver"; it auto-resumes within 10 seconds after Battery Saver turns OFF. - Given OS Data Saver is ON, Then uploads occur only on unmetered Wi‑Fi; the client refrains from starting uploads on metered networks and displays "Waiting for Wi‑Fi". - Given network is metered and Data Saver is OFF, Then uploads proceed but concurrency is limited to 1 item and chunk size is reduced to 256 KB; telemetry records metered=true for these uploads.
Persistence and Recovery Across App Restarts
- Given there are queued or in-progress uploads, When the app is force-closed or the device reboots, Then the sync service restarts on next opportunity and resumes all jobs within 30 seconds without user action. - Given an upload had completed N chunks before interruption, Then upon resume it revalidates session and continues at chunk N+1; previously acknowledged chunks are not re-sent. - Given any in-flight state is restored, Then no duplicate UI entries are created and the item retains the same local id and server id mapping.
Ingestion Pipeline State Transitions, Progress, and Notifications
- Given an item is uploaded, Then it transitions through states: Uploaded -> Ingested -> Matched -> Categorized; the app reflects each state within 60 seconds via push or polling. - Given per-item sync is in progress, Then the UI displays a progress bar with percent complete and remaining item count; upon errors, the UI shows a concise error code and a "Retry" action. - Given a bank-feed match is confirmed and the item is categorized, Then the user receives a notification within 60 seconds with receipt total, merchant, and category; tapping opens the matched transaction.
Bank Feed Auto-Match Engine (Fingerprint-Aware)
"As a solo consultant, I want receipts I snapped offline to auto-attach to the right bank transactions so that categorization is hands-off."
Description

Create a matching service that uses the offline fingerprint to pair receipts to bank feed transactions based on amount, currency, time-window proximity, and optional location/merchant similarity. Support configurable matching windows and thresholds, currency conversion when bank and receipt currencies differ, and safeguards against false positives with confidence scoring. When a high-confidence match is found, automatically attach the receipt, apply categorization rules, and annotate the transaction; surface low-confidence candidates for review. Learn from user corrections to improve future matches. Ensure performance and scalability for near-real-time processing and maintain idempotency across retries.

Acceptance Criteria
High-Confidence Match Auto-Attach and Categorize
Given a receipt fingerprint (amount, currency, timestamp, optional location/merchant) and an ingested bank transaction When the computed match confidence is >= the configured HighConfidenceThreshold Then the receipt is automatically attached to the bank transaction And categorization rules are applied to the transaction And the transaction is annotated with the match rationale, features used, and confidence score And the user receives an in-app and push notification of the attachment And an audit log entry is recorded with actor=system and the configuration version used
Configurable Matching Window and Thresholds
Given tenant-level configuration values (timeWindowHours, highConfidenceThreshold, lowConfidenceThreshold, amountTolerancePercent) When these values are updated via admin UI or API Then the new values take effect for all new evaluations within 1 minute without service downtime And candidate matches are only considered if bank timestamp is within ±timeWindowHours of the receipt timestamp And amount difference after currency normalization must be <= amountTolerancePercent of the bank amount And confidence >= highConfidenceThreshold triggers auto-attach; lowConfidenceThreshold <= confidence < highConfidenceThreshold queues for review; confidence < lowConfidenceThreshold discards the candidate And the effective configuration version is captured in the match annotation and audit log
Currency Conversion for Cross-Currency Matches
Given a receipt in currency A and a bank transaction in currency B and an FX provider configuration When evaluating a match where A != B Then the engine converts the receipt amount to currency B using the rate for the bank transaction date from the primary FX provider And the converted amount is rounded using currency B precision And the match is valid only if |convertedAmount − bankAmount| <= configured amountTolerancePercent of bankAmount And the FX rate value, source provider, and rate timestamp are stored in the match annotation And if the primary FX provider is unavailable, the engine uses the configured fallback; if all providers fail, the candidate is not auto-attached and is queued with reason=FXUnavailable
Location and Merchant Similarity Weighting
Given a receipt with optional GPS coordinates and merchant text and a bank transaction with merchant text When computing match confidence Then normalized merchant similarity >= 0.85 increases confidence by the configured weight; similarity < 0.5 decreases confidence by the configured penalty And geo distance <= 250 meters between receipt location and merchant location (if available) increases confidence by the configured weight; distance > 5 km decreases confidence by the configured penalty And absence of location or merchant data does not prevent matching; only present signals influence the score And all similarity scores and distances used are recorded in the match annotation
Low-Confidence Review Queue and Notifications
Given a candidate match with confidence such that lowConfidenceThreshold <= confidence < highConfidenceThreshold When the evaluation completes Then a review task is created containing the top 3 candidate bank transactions sorted by confidence with key features (amounts, currencies, timestamps, merchant, location distance, confidence) And the user receives an in-app notification within 10 seconds linking to the review screen And approving a candidate attaches the receipt, applies categorization, and records the decision and actor; rejecting a candidate suppresses that exact pair and records the reason And items unresolved for 7 days trigger a reminder; items auto-expire after 30 days with reason=ExpiredNoDecision
Learning From User Corrections to Improve Future Matches
Given the user corrects matches (relinks receipts or adjusts categories) that share a consistent pattern (e.g., merchant alias, typical time offset, rounding variance) When at least 3 similar corrections occur within a 30-day window Then the engine updates per-tenant weights/aliases within 1 hour and records the learned rule and scope And the next evaluated candidate that matches the learned pattern reflects an adjusted confidence score by at least +0.10 in the intended direction (or −0.10 to demote false positives) And users can view and roll back learned adjustments; rollbacks take effect within 1 hour and are audited
Reliable, Idempotent, Near-Real-Time Processing at Scale
Given duplicate deliveries of the same receipt fingerprint and/or the same bank event When the engine processes these events with retries Then exactly one attachment and one categorization occur; duplicate notifications are not sent; all side effects are idempotent via deterministic keys And after a transient failure and retry, the final state is consistent with a single successful run And under a load of 10,000 receipts and 10,000 bank transactions ingested within 10 minutes, the 95th percentile time from event ingestion to match decision is <= 4 seconds And after a controlled service restart, processing resumes without data loss and maintains ordering guarantees within the configured lookback window And events that fail 3 consecutive attempts are sent to a dead-letter queue with an alert; operational metrics (latency p50/p95, throughput, error rate) are exposed
Confirmation Notifications & Activity Timeline
"As a mobile-first user, I want to be notified when my offline receipts are matched so that I can trust everything is handled."
Description

Send concise push and in-app notifications when receipts are synced, matched, and categorized, with batching to reduce noise and deep links to the matched transaction. Provide an activity timeline that shows each event (captured offline, synced, matched, categorized) with timestamps, statuses, and any required user actions. Include settings to control notification types, an offline status banner, and accessibility-compliant UI. Integrate with existing notification services and audit logs so users can verify that their offline captures were processed end-to-end.

Acceptance Criteria
Push Notifications on Sync, Match, and Categorize
Given a receipt is captured offline with a stored fingerprint and push notifications are enabled When connectivity is restored and the receipt is synced Then a push notification labeled "Receipt synced" is delivered within 30 seconds and includes amount, local timestamp, and a deep link to the receipt timeline Given the synced receipt is matched to a bank transaction When matching completes Then a push notification labeled "Receipt matched" is delivered within 30 seconds and includes amount, merchant (if available), and a deep link to the matched transaction detail Given the matched transaction is categorized When categorization completes Then a push notification labeled "Categorized: <Category>" is delivered within 30 seconds and includes the selected category and a deep link to review; if model confidence < 90%, the notification includes "Review suggested" Rule: Push payload size is <= 4KB and is accepted by APNs/FCM without error; failed deliveries are retried up to 3 times with exponential backoff
Notification Batching Digest
Given batching is enabled and 3 or more receipt events (synced, matched, categorized) occur within a 5-minute window When the window elapses Then a single digest push is sent that summarizes counts per event type and the most recent event time, and deep links to the in-app notification inbox Given events are included in a digest When the digest is sent Then no individual pushes for those events are sent Given 10 or more events occur within the batch window When the digest is generated Then the inbox shows the first 10 items and a "View all" control for the remainder; the push body displays "10+ updates" Given batching is disabled in settings When events occur Then each event generates an individual notification (subject to DND/mute settings) Given a Do Not Disturb schedule is active When events occur during the DND window Then pushes are suppressed and events are delivered to the in-app inbox only
Deep Link Opens Matched Transaction Detail
Given a push or in-app notification includes a deep link to a matched transaction When the user taps the notification Then the app opens to the Transaction Detail screen within 2 seconds on warm start (4 seconds on cold start) and displays amount, merchant, category, receipt image thumbnail, and match status "Confirmed" with access to the receipt timeline Given the deep link token is invalid or expired When the user taps the notification Then the app opens the Notifications Inbox with an inline error banner and provides a search entry prefilled with the receipt total and date Rule: An analytics event "notification_deeplink_opened" is logged with notification_type, receipt_id, and masked transaction_id; failures log "notification_deeplink_failed" with reason
Activity Timeline End-to-End Lifecycle with Audit Correlation
Given a receipt was captured offline When the user opens its Activity Timeline Then events are listed in chronological order: Captured Offline, Synced, Matched, Categorized (and any Corrections), each with status icon, human-readable label, and an ISO 8601 timestamp rendered in the user's locale/time zone Rule: "Captured Offline" displays device local time and location (with accuracy in meters if available); "Matched" displays bank source and masked account (••••1234) and transaction date; "Categorized" displays category and ruleset/model origin Rule: Events are append-only and immutable; updates generate a new "Correction" event referencing the prior event Rule: Events that require user action display an actionable chip (e.g., "Confirm match", "Choose category") that navigates to the appropriate screen via deep link Rule: Each event exposes an "Audit ref" that copies a reference ID and reveals the last 5 related audit log entries inline; reference IDs resolve in backend audit logs Performance: Initial timeline render completes within 1 second on a warm view; loading skeletons are shown until data arrives
Notification Preferences and In-App Inbox
Given the user opens Settings > Notifications When they toggle Synced, Matched, Categorized, and Digest types Then the preferences save to the account, sync across devices within 60 seconds, and immediately control push delivery behavior Given the user configures a Do Not Disturb schedule When the schedule is active Then pushes are suppressed and all events route to the in-app inbox; if batching is enabled, a single digest push is sent at the end of the DND window summarizing the suppressed events Given the user mutes all push notifications When new events occur Then no pushes are sent and the in-app inbox still receives entries with badges Rule: The in-app inbox retains 90 days of notifications, supports pagination (20 items per page), and supports swipe to open the deep link and long-press to mark as read
Offline Status Banner Behavior
Given the device is offline When the user opens the app or the capture screen Then an offline banner appears within 1 second reading "Offline — SnapSync active" and shows the last sync attempt time; it does not block capture or navigation Given receipts are captured while offline When additional captures occur Then the banner updates to show the count of unsynced items and no network calls are attempted When connectivity is restored and the first successful sync completes Then the banner auto-dismisses and a non-intrusive toast confirms "<n> receipts synced" with a deep link to the Activity Timeline Accessibility: The banner is announced by screen readers on entry, has focusable actions, and meets 4.5:1 contrast; it is dismissible via keyboard/gesture and reappears only when offline is detected again
Accessibility Compliance for Notifications and Timeline
Rule: Notifications and timeline UI meet WCAG 2.1 AA — text contrast >= 4.5:1, touch targets >= 44x44 px, and supports Dynamic Type up to 200% Rule: All icons and buttons have descriptive accessibility labels; timeline items are navigable in a logical order; notifications expose "Open" and "Mark as read" actions to assistive technologies Rule: Respect OS accessibility settings — Reduce Motion eliminates non-essential animations; no flashing content > 3 Hz Validation: Automated accessibility tests (mobile axe/Detox or equivalent) pass with zero critical violations, and manual QA with VoiceOver/TalkBack confirms correct reading order and actionable controls

MatchScore

A clear confidence meter with explainable signals—Time, Location, Amount, Merchant. Review why a match is suggested, accept or correct in a tap, and watch the system learn from your choices to surface better matches and fewer edge-case reviews over time.

Requirements

Confidence Meter UI
"As a mobile-first freelancer, I want to instantly see how confident the system is about a match so that I can decide quickly whether to accept it or take a closer look."
Description

Implement a mobile-first confidence meter that visualizes each proposed document-to-transaction match as a normalized 0–100 score, mapped to accessible tiers (e.g., High/Medium/Low with color-safe indicators). The meter should be rendered inline wherever matches appear (review screens, transaction detail, receipts) and expand on tap to show quick context. Include configurable thresholds per workspace to control auto-accept vs. needs-review behavior, with sensible defaults for new users. Ensure sub-100ms render latency and offline-safe caching for previously computed scores. Provide fallback and “insufficient data” states when signals are missing, and guard against misleading displays by indicating data gaps. The component must be WCAG AA compliant, reusable across web and mobile, and instrumented for analytics (impressions, expands, and action rates) to guide iteration. Outcome: users instantly gauge match certainty, reducing cognitive load and speeding review decisions.

Acceptance Criteria
Inline Rendering Across Screens and Platforms
- Given matches are present on Review, Transaction Detail, and Receipt screens on mobile and web, When the screen renders, Then each match displays an inline confidence meter with a numeric score (0–100) and a tier label. - Given a score exists for a match, When viewing the same match on web and mobile, Then the displayed score value, tier label, and iconography are identical. - Given a list with up to 100 matches, When the list renders, Then no meter overlaps or truncates adjacent UI and all meters are visible within the viewport of their list items. - Given localized number formats, When rendering the score, Then the numeric value is rounded to the nearest integer and formatted per locale without altering the underlying 0–100 value.
Accessible Tiers and WCAG AA Compliance
- Given a 0–100 score, When mapping to tiers, Then defaults are High (≥85), Medium (60–84), Low (<60) unless workspace overrides are set. - Given a tier is displayed, Then the meter shows: (a) a textual tier label, (b) a non-color cue (icon/pattern), and (c) color that meets WCAG AA contrast (text ≥4.5:1; non-text UI ≥3:1). - Given color is unavailable or user has color-contrast settings enabled, When viewing the meter, Then tier differentiation remains perceivable via label and icon. - Given screen reader focus on the meter, When announced, Then it reads “Match confidence {score} out of 100, {tier}.” - Given keyboard or switch input, When navigating, Then the meter is focusable and its primary action is operable via Enter/Space.
Tap-to-Expand Explainable Signals
- Given the user taps/clicks the meter, When expanded, Then a panel shows Time, Location, Amount, and Merchant signals with their values and match statuses. - Given expansion is triggered, When opening, Then the expanded panel becomes visible within 200 ms and can be closed via tap outside, Back, or Esc. - Given any signal is missing, When viewing the expanded panel, Then the signal is labeled “Not available” and excluded from the contribution summary. - Given assistive technologies, When focus enters the expanded panel, Then focus is trapped inside until closed and the close control is the last focusable element. - Given the user collapses the panel, When closed, Then focus returns to the originating meter button.
Workspace Thresholds and Auto-Accept Behavior
- Given a new workspace, When initializing the component, Then default thresholds are set to Auto-Accept = 85 and Needs-Review = 60. - Given an admin edits thresholds, When saving, Then Auto-Accept must be greater than Needs-Review; invalid values block save with an inline error message. - Given a match score ≥ Auto-Accept, When evaluated, Then the match is auto-accepted and the meter shows tier High with an “Auto-accepted” indicator. - Given a match score between Needs-Review and Auto-Accept − 1, When evaluated, Then the match is marked Needs Review. - Given a match score < Needs-Review, When evaluated, Then the match is marked Low Confidence and requires manual review. - Given thresholds are updated, When returning to any screen with meters, Then meters reflect the new thresholds without app restart and within one render cycle.
Performance and Offline Caching
- Given a previously computed score is available locally, When the meter mounts, Then first visual render of the meter completes within 100 ms on a mid-tier device, measured at the component boundary. - Given the device is offline, When viewing matches with cached scores, Then meters display the last known score and tier with an offline indicator and without errors. - Given the underlying document or transaction data changes, When detected, Then cached scores for affected matches are invalidated and recomputed before display. - Given a fresh score arrives while a meter is visible, When updating, Then the meter updates within one render cycle without flicker or duplicate announcements to screen readers.
Fallback and Insufficient Data States
- Given critical signals are missing preventing score computation, When rendering, Then the meter shows an “Insufficient data” state with neutral styling and no numeric score. - Given the “Insufficient data” state is shown, When expanded, Then the missing signals are enumerated with labels and short reasons (e.g., “Location: Not available”). - Given an item is in “Insufficient data,” When evaluating automation, Then auto-accept is disabled and the item is routed to manual review. - Given partial signal availability, When rendering, Then the meter displays a data-gap indicator and does not over-represent confidence for absent signals.
Analytics Instrumentation
- Given the meter is ≥50% in viewport for ≥500 ms, When visible, Then an impression event is logged with workspace_id (hashed), match_id, screen, score, tier, and timestamp. - Given the user expands the meter, When the panel opens, Then an expand event is logged with the same identifiers and source screen. - Given the user accepts or corrects a match, When the action completes, Then an action event is logged with action_type (accept/correct), pre_score, pre_tier, and post_tier (if corrected). - Given events are generated while offline, When connectivity resumes, Then events are flushed within 60 seconds with ≤1% loss rate in QA tests. - Given analytics schemas, When events are sent, Then they validate against the defined schema and exclude PII fields by design.
Explainable Signal Breakdown
"As a user reviewing a suggested match, I want to see which signals influenced it and how, so that I can trust the recommendation or correct it confidently."
Description

Provide an explainability panel that breaks down the MatchScore by core signals—Time, Location, Amount, Merchant—with clear deltas (e.g., time difference from invoice date, distance between merchant location and receipt GPS, amount variance with currency normalization, merchant name similarity) and per-signal contributions/weights. Each signal must link to source evidence (invoice ID, bank transaction, receipt image snippet with redaction) and show reason codes (e.g., Amount exact match, Merchant alias recognized) to justify the recommendation. Support degraded operation when some signals are unavailable and transparently display what was used. Integrate with TaxTidy’s document store, bank feed normalizer, and receipt OCR pipeline to fetch evidence efficiently with prefetch hints. Outcome: users understand why a match is suggested, increasing trust and facilitating quick corrections when needed.

Acceptance Criteria
Signal Breakdown Completeness and Consistency
Given a suggested match with Time, Location, Amount, and Merchant signals available When the user opens the Explainability panel Then each signal displays a delta in canonical units (Time: hours; Location: km; Amount: currency-normalized variance; Merchant: similarity %) And each signal displays its contribution as a percentage And the sum of displayed contributions equals 100% ± 0.1% And the displayed MatchScore equals the weighted sum of signal scores within ±1% (or ±0.5 points) And numeric formatting is: hours/distance to 1 decimal, amounts to 2 decimals with currency code, percentages as whole numbers And Amount deltas are computed after currency normalization using the transaction date FX rate and source is indicated
Evidence Linking and Redaction
Given the Explainability panel is open When the user taps the Time signal evidence link Then the app opens the linked invoice (document store) and transaction (bank feed normalizer) records showing dates used in the calculation When the user taps the Location signal evidence link Then the app opens a receipt image snippet (receipt OCR) highlighting the location data used and the merchant location from the bank feed When the user taps the Amount signal evidence link Then the app opens invoice line totals and bank transaction amount with the FX rate and date applied When the user taps the Merchant signal evidence link Then the app opens merchant name strings, alias mapping, and matched tokens And all evidence views mask sensitive data per policy (e.g., account numbers last-4 only, email partially masked, GPS rounded to 3 decimals) And each evidence view loads within 1.2 s P95 and shows the source record IDs
Reason Codes Display and Mapping
Given a suggested match is rendered When the Explainability panel displays signals Then each signal shows at least one reason code from the approved catalog And the reason code text is human-readable and ≤ 80 characters And reason codes align with data: "Amount exact match" only if variance == 0 post-normalization; "Amount within tolerance" only if |variance| ≤ configured threshold; "Merchant alias recognized" only if alias mapping exists; "Location within 2 km" only if distance ≤ 2 km; "Time within 24h" only if |hours| ≤ 24 And tapping a reason code shows a one-line explanation without leaving the panel And the system logs signal inputs and selected reason codes with the match ID for audit
Degraded Operation and Transparency for Missing Signals
Given a suggested match where one or more signals are unavailable (e.g., no receipt GPS, missing invoice date, merchant not parsed) When the user opens the Explainability panel Then unavailable signals are labeled Unavailable with a cause (e.g., No receipt GPS, No invoice date) and are not counted as zero And the MatchScore excludes unavailable signals and re-normalizes remaining contributions to sum to 100% ± 0.1% And a banner lists which signals were used to compute the score And the panel renders without error within 1.5 s P95 And telemetry records which signals were unavailable and their causes
Prefetch and Performance Targets
Given the user navigates to the Match Review list When the top N suggested matches are displayed Then the app issues prefetch hints for evidence to the document store, bank feed normalizer, and receipt OCR for those N items And when the user opens an Explainability panel for a prefetched item, the breakdown renders in ≤ 800 ms P95 and evidence thumbnails in ≤ 1.8 s P95 on 4G And for non-prefetched items, breakdown renders in ≤ 1.4 s P95 and evidence thumbnails in ≤ 2.5 s P95 on 4G And instrumentation reports median latency improvement ≥ 30% with prefetch enabled over a 7-day window And total backend calls per panel open are ≤ 3 due to batched requests and caching, with cache hit rate ≥ 70% P50 within a session
Quick Accept/Correct With Consistent Explainability
Given the Explainability panel is open for a suggested match When the user taps Accept Then the match is confirmed, the panel closes, and an event is recorded containing current signal deltas, contributions, and reason codes When the user taps Correct Then the user can select an alternative pairing and the panel recomputes the breakdown for the new selection within 1.2 s P95 And the updated breakdown reflects the new evidence (e.g., Amount variance = 0 yields "Amount exact match") and shows revised contributions that still sum to 100% ± 0.1% And a match_correction event is emitted including before/after IDs and signal inputs for learning pipelines
One-Tap Accept/Correct Actions
"As a busy freelancer, I want to accept or correct a suggested match in one tap so that I can clear my review queue quickly without losing accuracy."
Description

Enable frictionless review with primary actions: Accept, Reject, and Correct in a single tap, optimized for thumb reach on mobile. Accept will finalize the link and lock the evidence; Reject will dismiss and record feedback; Correct opens a fast picker to reassign the transaction to a better document or create a new entry. Provide undo (5–10s) and batch actions for multi-select lists. Ensure idempotent, resilient APIs with optimistic UI, offline queuing, and conflict resolution on sync. Log all actions to an immutable audit trail for IRS-readiness and to power the learning loop. Outcome: users resolve matches in seconds, reducing review time and increasing throughput.

Acceptance Criteria
One-Tap Accept Locks Evidence and Finalizes Match
Given a suggested match is visible with MatchScore signals When the user taps "Accept" Then the row transitions to an Accepted state within 300ms with actions disabled And the linked evidence documents are marked read-only (locked) in the UI immediately And an idempotent finalize-match request is sent or queued with a unique idempotency key And duplicate taps or retries do not create duplicate links due to idempotency And an audit entry is written with userId, action=ACCEPT, matchId, linkId, evidenceIds, timestamp, deviceId, idempotencyKey And if the server call fails online, an error appears within 2s and the UI reverts; if offline, the item shows Pending until sync
One-Tap Reject Dismisses Suggestion and Captures Feedback
Given a suggested match is visible When the user taps "Reject" Then the suggestion is removed from the queue within 300ms And a reason sheet appears with default "Not a match" and optional note (0–140 chars); if dismissed within 5s, the default reason is saved And an idempotent reject request is sent or queued And the same transaction–document pair is not resurfaced unless upstream data changes And an audit entry is recorded with action=REJECT, reason, timestamp, userId, idempotencyKey And a learning event is emitted to improve future recommendations
One-Tap Correct with Fast Picker and New Entry Creation
Given a suggested match is visible When the user taps "Correct" Then a fast picker opens within 300ms showing top alternatives and search And selecting an alternative and confirming updates the link with a single confirm tap And choosing "Create New Entry" creates the document with required fields and links it in ≤ 3 taps total And the original link (if any) is replaced, and evidence locks are updated to the new link And audit entries are recorded for UNLINK and LINK with a shared correlationId And requests use idempotency keys to prevent duplicates; on failure, an error shows within 2s and previous state remains
Time-Bound Undo for Accept/Reject/Correct
Given the user has performed Accept, Reject, or Correct When the action completes or is queued Then an Undo snackbar appears for 7 seconds And tapping Undo within 7 seconds reverses the action locally and remotely (or cancels the queued item) and restores prior UI state and list position And an audit UNDO entry is appended referencing the original action via correlationId And if Undo fails to sync, the UI surfaces an error and offers retry; if offline, Undo is queued for sync
Batch Multi-Select Actions with Partial Failure Handling
Given multi-select mode is enabled in the review list When the user selects N items (1 ≤ N ≤ 100) and taps Accept, Reject, or Correct Then the app applies the batch with optimistic UI and a progress indicator And per-item outcomes are shown; partial failures are highlighted with retry affordances And for N ≤ 50 while online, all successful items finalize within 3s excluding server latency; when offline, all selected items enter Pending within 200ms And each item uses a distinct idempotency key; audit entries include a shared batchId And operations are atomic per item (no cross-item dependence)
Offline Queue with Optimistic UI and Conflict Resolution
Given the device is offline or loses connectivity during review When the user performs Accept, Reject, or Correct (single or batch) Then the UI shows a Pending state within 200ms and actions are enqueued with exponential backoff retries up to 24h And on reconnect, the client syncs and resolves conflicts: if the transaction or document changed or was linked elsewhere, the item is marked Conflict and the user is prompted to choose; no duplicate links are created And idempotency keys prevent duplicate side effects on retry And outcomes (Success/Conflict/Failed) are surfaced to the user; Pending badges clear on success And the queue persists across app restarts and is encrypted at rest
Immutable Audit Trail for IRS-Ready Logging
Given any Accept, Reject, Correct, Undo, or batch action occurs When the action is initiated and when it completes Then an append-only audit entry is written to WORM storage with fields: auditId, correlationId/batchId, actionType, actorUserId, deviceId, timestamp (UTC ISO-8601), entityIds (transactionId, documentId), priorState, newState, reason (if any), idempotencyKey, offlineFlag, result And audit entries are visible in admin within 5s of completion and exportable in an IRS-ready packet And attempts to modify or delete audit entries are prevented and logged as separate security events
Personalized Learning from Feedback
"As a returning user, I want the system to learn from my past decisions so that future match suggestions are more accurate for my workflow."
Description

Capture user accept/reject/correct actions as labeled outcomes to adapt MatchScore per user and workspace. Implement a feedback ingestion service that aggregates events, applies basic quality filters, and updates a per-user weighting profile (e.g., tighter amount tolerances for some users, greater reliance on location for others). Support safe online updates with caps, decay, and rollback; maintain model/version lineage and enable A/B comparisons against a global baseline. Respect privacy by limiting cross-user leakage and allowing users to reset their profile. Provide monitoring for drift and guardrails to prevent confidence inflation. Outcome: the system learns from each user’s behavior, surfacing better matches and reducing edge-case reviews over time.

Acceptance Criteria
Capture and Label Feedback Events
Given an authenticated user in a workspace acts on a suggested match (Accept, Reject, or Correct) When the action is submitted with network connectivity Then a feedback event is created with fields: event_id (UUID), user_id, workspace_id, suggestion_id, action_type, timestamp (UTC), original_match_id, corrected_match_id (nullable), signals_snapshot {time, location, amount, merchant}, client_version And the event is persisted with p95 latency ≤ 1s and is idempotent (duplicate event_id is ignored) And the event is available to the learning pipeline within 5 minutes (p95) And the API responds 2xx with event_id
Apply Quality Filters to Feedback
Given a stream of feedback events When events are processed by the ingestion service Then events older than 30 days, missing required fields, or duplicates by (user_id, suggestion_id, action_type) within 24h are rejected with reason codes And events with outlier corrections (e.g., amount variance > 10× workspace median) are flagged and excluded from learning but retained for audit And ≥95% of valid events pass filters; rejection rate and reasons are exposed via metrics
Update Per-User Weighting Profile with Caps and Decay
Given validated feedback for a specific user and workspace When the learner updates the weighting profile Then parameter updates are bounded per update: amount_tolerance Δ ≤ ±5%, location_weight Δ ≤ ±0.05, time_weight Δ ≤ ±0.05, merchant_weight Δ ≤ ±0.05 And cumulative daily change magnitude is capped at 20% And parameters are constrained: weights ∈ [0,1], sum normalized to 1 And exponential decay with half-life 30 days is applied to historical feedback And the new profile version is activated within 10 minutes and logged with profile_version, parent_version, changelog
Safe Online Updates with Rollback and Version Lineage
Given a newly activated profile version When precision@1 drops by ≥3 percentage points or rejection rate increases by ≥5 percentage points over a 24h window versus the prior version Then the system automatically rolls back to the last good version within 5 minutes And all profile versions maintain lineage: profile_version_id, created_at, training_event_range, parameters_hash, triggering_events_count And manual rollback via admin API is available and completes within 2 minutes
A/B Comparison Against Global Baseline
Given an experiment that assigns workspaces 50/50 to Personalized (treatment) and Global Baseline (control) with sticky assignment When at least 500 suggestions per cohort accumulate over a 14-day window Then the treatment cohort shows ≥10% reduction in edge-case review rate and no degradation in precision@1 worse than 2 percentage points at 95% confidence And if criteria are not met, treatment workspaces are automatically reverted to baseline within 24 hours
Privacy Controls and Profile Reset
Given per-user and per-workspace learning When updating a profile Then only that user’s workspace feedback contributes to the profile; cross-workspace or cross-user feedback is excluded except for immutable global baseline parameters And no raw PII (e.g., receipt images, full merchant addresses) is stored in the profile; only derived, non-identifying statistics And when a user triggers “Reset MatchScore Learning” for their workspace Then the profile and its feedback history are deleted or cryptographically tombstoned, the active model reverts to the global baseline, and confirmation is returned within 60 seconds
Monitoring, Drift Detection, and Confidence Guardrails
Given live operation When confidence inflation is detected (mean confidence increases by ≥10% while precision@1 over the last 7 days drops by ≥2 percentage points) Then confidence outputs are capped to baseline or prior version until retraining completes And alerts are sent to on-call within 5 minutes, and dashboards expose precision, recall, acceptance/rejection rates, confidence distribution, and data drift for time, location, amount, and merchant signals And guardrails prevent any individual signal weight from exceeding 0.7 or falling below 0.1 without manual approval
Edge-Case Review Triage
"As a user with limited time, I want the riskiest or most impactful mismatches prioritized so that I can review fewer items while maintaining compliance."
Description

Create a dedicated review queue that prioritizes items by low MatchScore and potential tax impact (amount, category sensitivity, quarter close proximity). Provide grouping of similar items (e.g., recurring merchants) and quick filters (low score, missing receipt, high amount). Include snooze, remind, and timeboxing to keep reviews under a set daily budget. Integrate notifications (push/email) for pending high-impact items and expose a “Why in queue” tag to clarify inclusion. Sample a small set of medium-score items for quality control to detect drift. Outcome: users spend time only where it matters, further cutting review effort.

Acceptance Criteria
Queue prioritization by low score and tax impact
- Given a set of review items with MatchScore (0.0–1.0), Amount, Category Sensitivity (High/Medium/Low), and Days to Quarter Close, when the review queue loads, then items are ordered by: 1) MatchScore ascending; 2) Amount descending; 3) Category Sensitivity High > Medium > Low; 4) Days to Quarter Close ascending; 5) CreatedAt ascending. - Given two items where one has a MatchScore at least 0.05 lower than the other, when both are in the queue, then the lower-score item appears above regardless of other fields. - Given two items with MatchScores within 0.05 and Amount differs by at least $50, then the higher-amount item appears above. - Given items are updated (score change, amount correction, category change, close date), when the user refreshes or 5 seconds elapse, then ordering recalculates within 300 ms and reflects the new priority. - Given 1,000 items, when the queue loads, then first paint occurs within 2 seconds and scroll remains responsive (>60 FPS).
Grouping of similar items (recurring merchants)
- Given multiple items with the same normalized merchant (case/whitespace/punctuation-insensitive) and amount variance ≤ 15%, when grouping is enabled, then they appear under a single collapsible group labeled with merchant name, item count, and total amount. - Given a group, when expanded, then all items are visible and individually selectable; when collapsed, only the group header remains. - Given the user selects a group and chooses Accept all or Correct all, then the action applies to all items currently in the group and a confirmation displays the affected count. - Given an item is mis-grouped, when the user selects Move out of group, then it is ungrouped immediately and persists ungrouped across reloads. - Given quick filters are active, when groups are displayed, then group contents and counts reflect only items matching the filters.
Quick filters — low score, missing receipt, high amount
- Given the review queue, when the user taps Low Score, then only items with MatchScore ≤ 0.40 are shown and the filter chip displays the result count. - Given the user taps Missing Receipt, then only items without an attached receipt or OCR data are shown. - Given the user taps High Amount, then items with Amount ≥ the configured threshold (default $500 or top 10% by amount, whichever is lower) are shown. - Given multiple quick filters are active, then the results are the intersection (AND) of the selected filters. - Given no items match, then an empty state with Clear filters appears; selecting it removes all quick filters. - Given the user returns within 24 hours, then the last-used quick filters persist unless manually cleared.
Snooze, remind, and daily timeboxing
- Given a daily review budget (minutes) set by the user (default 10), when a session starts, then a visible countdown timer appears and can be paused/resumed. - Given the timer reaches zero, then opening additional items is blocked and options to End session or Extend by 5 minutes (once per day) are offered. - Given an item is snoozed for 1 day, 3 days, 1 week, or a custom date/time, then it is hidden from the queue until expiration and reappears with recalculated priority. - Given a snoozed item reaches its reminder time, then an in-app reminder badge appears and a notification is sent if enabled. - Given an item is snoozed, then it is excluded from notifications and QC sampling until the snooze expires.
High-impact item notifications (push/email)
- Given an item with MatchScore < 0.50 and Amount ≥ $1,000, or within 14 days of quarter close with Category Sensitivity = High, when it enters the queue, then a high-impact notification is queued within 15 minutes. - Given push notifications are enabled, then send push; otherwise send email; if both are enabled, send push and suppress duplicate email. - Given Do Not Disturb hours are configured, then notifications are deferred until the next allowed window. - Given the user taps the notification, then the app opens to the triage queue with the high-impact filter active and the item highlighted. - Given multiple high-impact items occur within 1 hour, then a single batched notification summarizing the count is sent.
"Why in queue" tag visibility and explanation
- Given any item in the triage queue, when viewed in list or detail, then a Why in queue tag is shown with up to three reasons, each including the triggering threshold and current value (e.g., Low MatchScore 0.32 ≤ 0.40). - Given the user expands the tag, then a full ordered list of reasons is shown, covering score, amount, receipt status, and quarter proximity. - Given underlying data changes (e.g., a receipt is attached), then the tag updates within 5 seconds to reflect the new state. - Given a screen reader is active, then the tag and reasons are announced with descriptive, accessible labels.
Medium-score sampling for drift detection (QC)
- Given items with MatchScore between 0.50 and 0.70, when daily sampling runs at 02:00 local time, then 5% of eligible items are added as QC sample, capped at 50 per week per user. - Given sampling, then selection is random and stratified by merchant and category; items already acted on are excluded. - Given an item is marked QC sample, then it displays a QC badge and cannot be snoozed beyond 7 days. - Given the user completes reviews on sampled items, then weekly QC metrics report acceptance/rejection rates and drift indicators. - Given sampled items are cleared, then they are not eligible for re-sampling for 30 days.
Signal Extraction & Normalization
"As a user, I want the system to accurately interpret time, location, amount, and merchant details from my documents so that match suggestions are dependable."
Description

Build robust pipelines to extract and normalize the four core signals across sources: Time (UTC-normalized with timezone resolution and invoice/payment gap handling), Location (receipt GPS, merchant geocode, and user geofences with distance calculation), Amount (currency normalization, tax-inclusive/exclusive handling, and tolerance rules), and Merchant (OCR cleanup, alias dictionary, bank descriptor normalization, and fuzzy matching). Provide confidence per signal, propagate missing-data flags, and cache canonicalized entities. Integrate with existing OCR, bank feed parsers, and invoice ingestion; expose a shared service API for MatchScore and explainability to consume. Outcome: high-quality, consistent signals that drive reliable scoring and explanations.

Acceptance Criteria
Time Signal: UTC Normalization & Timezone Resolution with Payment Gaps
- Given a receipt timestamp with explicit IANA timezone, when normalized, then the UTC value is computed correctly, time.missing=false, and time.confidence>=0.95. - Given a bank timestamp that is DST-ambiguous (e.g., local 01:30 at fall-back), when normalized, then the service resolves the ambiguity deterministically (fold=1 policy), records reason dst_disambiguated=true, and sets time.confidence between 0.70 and 0.90. - Given a date with no timezone and no location context, when normalized, then the service applies the account default timezone, sets time.tz_inferred=true, time.confidence<=0.60, and time.missing=false. - Given an invoice date and a payment timestamp, when processed together, then gap_days is returned as an integer, long_gap=true if gap_days>45, and both timestamps are UTC-normalized.
Location Signal: Multi-source Resolution & Distance Calculation
- Given receipt GPS lat/lon with accuracy<=50m, when normalized, then location.source="gps", distance_to_merchant_m is computed using Haversine, location.missing=false, and location.confidence>=0.95. - Given no GPS but a merchant address that geocodes successfully, when normalized, then location.source="merchant_geocode", distance_to_merchant_m is returned, and location.confidence between 0.70 and 0.90. - Given user geofences, when the event point lies within 100m of a geofence center, then location.geofence_match=true and geofence_name is set; otherwise false. - Given neither GPS nor geocode are available, when normalized, then location.missing=true, reasons include "missing", and location.confidence=0.0.
Amount Signal: Currency Normalization & Tax Handling with Tolerances
- Given an invoice amount with ISO currency and tax-inclusive flag with tax_rate%, when normalized for a specific as_of date, then subtotal, tax_value, and total are computed consistently, currency is preserved, and amount.confidence>=0.95. - Given a multi-currency context (source currency!=account currency), when normalized, then conversion uses the as_of FX rate for the event date (fallback to daily average if intraday missing), fx_rate_id is returned, rounding follows bankers' rounding to 2 decimals, and amount.confidence>=0.90. - Given two amounts to compare (e.g., bank vs invoice), when evaluated, then within_tolerance=true if abs(diff)<=max(0.50, 0.01*max(amounts)), else false; tolerance_basis and tolerance_applied are returned in reasons. - Given currency cannot be determined, when normalized, then amount.missing=true, reasons include "missing_currency", and amount.confidence=0.0.
Merchant Signal: Canonicalization via OCR Cleanup, Aliases, and Fuzzy Match
- Given OCR text and a bank descriptor, when normalized, then text is cleaned (case-folding, punctuation/stopword removal), alias dictionary is applied first, and if an alias hit occurs the canonical merchant_id and display_name are returned with merchant.confidence>=0.95. - Given no alias hit, when fuzzy matching is applied, then a canonical merchant is returned only if score>=0.85; reasons include algo="fuzzy" and score; otherwise merchant.missing=true and merchant.confidence<0.85. - Given a user updates the alias dictionary, when the next normalization occurs, then the new alias is honored within 1 minute and any previously cached canonicalization for affected keys is invalidated.
Per-Signal Confidence Scores and Explainable Reasons
- For each signal (time, location, amount, merchant), when returned, then confidence is a float in [0,1] with max two decimal places, reasons is a non-empty list of key:value tags when confidence>0, and missing is a boolean present on each signal. - When data is inferred (e.g., tz_inferred, currency_inferred), then the corresponding reasons include an *_inferred tag and confidence<=0.80. - When a signal is missing, then missing=true, reasons includes "missing" with a specific subtype (e.g., missing_timezone, missing_gps), and confidence=0.0.
Shared Service API with Canonical Cache & Missing-Data Propagation
- Given POST /v1/signals/normalize with valid invoice/bank/receipt payload, when called, then the response contains time, location, amount, and merchant objects each with value (or structured fields), confidence, reasons[], and missing, plus a request_id. - Given repeated identical payloads within TTL, when called, then responses are served from cache with cache_hit=true in reasons and p95 latency<=200ms; cold path p95 latency<=500ms. - Given alias dictionary or FX rates update events, when they occur, then affected cache entries are invalidated within 2 minutes and subsequent requests reflect updated canonicalization or rates. - Given invalid input, when validated, then the API returns HTTP 422 with field-level errors; given upstream OCR/bank parser timeouts, then HTTP 503 is returned with a retriable=true hint; idempotency-key is honored to deduplicate within 24h.
Match Performance Analytics
"As a product owner, I want clear metrics on MatchScore performance so that we can improve accuracy and reduce user review time over releases."
Description

Deliver a metrics layer and dashboards tracking acceptance rate, correction rate, average review time, precision/recall from offline validation sets, and uplift from personalized learning. Segment by source (invoice, bank, receipt), merchant, and user cohort; surface trend alerts (e.g., sudden drop in acceptance for a source). Provide exports and privacy-preserving aggregation. Instrument key funnels (view → expand → action) and feed insights back into product tuning (thresholds, UI tweaks). Outcome: the team can monitor efficacy, identify regressions, and iterate deliberately to hit the 60% time-saved goal.

Acceptance Criteria
Metrics Coverage, Accuracy, and Freshness
Given a selected time window (Last 24h, 7d, 30d), when acceptance rate, correction rate, and average review time are displayed, then values match a warehouse recomputation within ±0.5 percentage points for rates and ±0.1 minutes for time for the same window. Given live data ingestion, when the dashboard loads, then a data freshness timestamp is shown and p95 data lag is ≤15 minutes; if lag exceeds 15 minutes, a freshness warning banner is displayed.
Segmentation by Source, Merchant, and Cohort
Given filters for source (invoice, bank, receipt), merchant, and user cohort, when any single or combined filter is applied, then all displayed metrics recompute correctly and render in ≤2 seconds, and totals equal the sum of visible segments. Given records with missing merchant, when metrics are computed, then those records appear under “Unknown” and are included in overall totals. Given selected filters, when the dashboard is refreshed or shared, then the same filters persist via URL parameters.
Trend Alerts for Performance Changes
Given a 7-day rolling baseline per source, when the current 2-hour rolling acceptance rate for any source drops by ≥5 percentage points from baseline, then a single alert is sent to Slack and email containing segment, magnitude, timeframe, and a deep link to the dashboard. Given correction rate per source, when it increases by ≥5 percentage points over baseline for 2 consecutive hours, then an alert is sent with the same details. Given an alert has fired, when the condition persists, then suppress repeat alerts for 24 hours or until the metric recovers within 2 percentage points of baseline.
Model Performance and Learning Uplift Reporting
Given the weekly offline validation set, when results are published, then precision, recall, and F1 are reported by source and top 20 merchants with 95% confidence intervals and sample sizes, and values match the offline job within ±0.5 percentage points. Given current and prior models, when deltas are displayed, then changes in precision/recall/F1 are shown in percentage points and directionally match the offline comparison job. Given a randomized holdout or counterfactual evaluation, when personalized uplift is computed for users with ≥50 labeled actions in the last 30 days, then uplift in acceptance rate is displayed with 95% CI and n, and matches the evaluation job within ±0.5 percentage points.
Funnel Instrumentation and Conversion Reporting
Given a user session that views a suggestion, expands signals, and takes an action, when events are ingested, then events view_suggestion, expand_signals, and take_action are recorded in order with the same suggestion_id and are queryable within 60 seconds at p95. Given a selected date range, when the funnel is displayed, then view→expand→action conversion and step drop-offs are shown and match backend event counts within ±1%.
Privacy-Preserving Aggregation and Data Export
Given any aggregate segment, when counts are calculated, then segments with n < 10 are suppressed or merged into “Other,” and no raw PII (name, email, exact timestamp, card last4, address) appears in dashboards or exports. Given a 30-day window, when an export is requested, then CSV and JSON aggregate exports are generated in ≤2 minutes, delivered via a signed URL that expires in 7 days, and the download is audit-logged with user and timestamp.
Actionable Insights and Threshold Tuning Loop
Given observed precision/recall trade-offs, when a threshold change proposal is created in the Analytics UI, then the system simulates expected impact on acceptance rate and average review time using the last 7 days and displays projected deltas with 95% CI before enabling Apply. Given a feature-flagged rollout of a threshold change, when the experiment runs in production, then guardrails are auto-evaluated (acceptance rate not down by >2 pp and average review time not up by >10% versus baseline); if violated, an alert is sent and a rollback toggle is available. Given a change is applied, when audit logs are opened, then user, timestamp, old/new values, affected segments, and links to before/after dashboards are present.

Payout Stitcher

Automatically reconciles Stripe, PayPal, and FreshBooks payouts to your bank deposits. Groups the underlying invoices, fees, and refunds under each deposit so you confirm matches in one tap. Delivers clean income records per client and project without spreadsheet gymnastics.

Requirements

Unified Payout Connectors
"As a freelancer using Stripe, PayPal, and FreshBooks, I want TaxTidy to automatically import my payouts and related charges/fees/refunds so that my income can be reconciled without manual file uploads."
Description

Build and maintain OAuth-based connectors for Stripe, PayPal, and FreshBooks to ingest payout batches and their underlying items (charges/invoices, fees, refunds) via REST APIs and webhooks. Normalize data to a unified schema with stable external IDs, currency, timestamps, and client/project metadata. Implement incremental sync with stateful cursors, automatic backfill, retry/backoff, and idempotency. Schedule periodic pulls and process inbound webhooks to keep data fresh. Integrate with TaxTidy’s account-linking flow and trigger downstream reconciliation jobs upon new or updated payout data.

Acceptance Criteria
OAuth connection flow for Stripe, PayPal, and FreshBooks
- Given a user initiates linking for a provider (Stripe, PayPal, FreshBooks) in TaxTidy, When the user completes OAuth successfully, Then the system stores an encrypted refreshable token, provider account ID, and marks connector status as Connected. - Given a connector is Connected, When a scope validation check runs, Then the connector has only the required read scopes for payouts/transactions/fees/refunds and no write scopes. - Given token exchange completes, When a test API call is made to the provider, Then the call succeeds and returns the provider account ID within 5 seconds. - Given logging is enabled, When secrets are handled, Then tokens and secrets are masked in logs and never persisted in plaintext.
Initial backfill and normalization to unified payout schema
- Given a connector is Connected, When initial backfill runs, Then payout/deposit batches and underlying items since the earlier of 24 months or provider data availability are fetched. - Given data is fetched, When records are written, Then each payout and item conforms to the unified schema with required fields populated: external_id, provider, currency (ISO 4217), gross_amount, fee_amount, net_amount, status, occurred_at (UTC ISO 8601), client_id, project_id (if available). - Given normalization occurs, When duplicate fetches happen, Then records are upserted by stable external_id without creating duplicates. - Given backfill completes, When validation runs, Then critical write failures are 0% and retriable failures are <1%, with retries queued.
Incremental sync with stateful cursors and periodic pulls
- Given a connector has an established cursor, When the scheduled pull runs every 15 minutes, Then only new or updated payouts and items since the cursor are fetched. - Given items are processed, When processing succeeds, Then the cursor advances atomically to the last processed event or timestamp. - Given at-least-once delivery from providers, When duplicate items are encountered, Then idempotency keys prevent duplicate records by external_id and version. - Given new provider data appears, When a webhook arrives earlier than the scheduled pull, Then data is ingested and visible within 10 minutes; otherwise within 20 minutes via scheduled pull.
Webhook signature verification and idempotent processing
- Given webhook endpoints are configured, When a webhook arrives, Then the signature is verified and invalid signatures are rejected with HTTP 4xx and no processing. - Given a valid event with event_id arrives, When it is processed, Then it is deduplicated by event_id for at least 24 hours and enqueued within 30 seconds. - Given a valid event is queued, When processing occurs, Then the corresponding payout/items are upserted and linked within 2 minutes. - Given processing fails with a transient error, When retries occur, Then exponential backoff is applied up to 5 attempts before quarantining the event.
Retry, backoff, and connector health monitoring
- Given a provider API returns 429 or 5xx, When a request is retried, Then exponential backoff is applied starting at 1s doubling to a max of 120s, with up to 6 retries and respect for Retry-After headers. - Given repeated failures occur, When failures exceed 5 consecutive attempts for a connector, Then the connector status becomes Degraded and a user-facing alert with error code and guidance is recorded. - Given a circuit breaker opens, When 10 minutes of successful health checks occur, Then the connector status automatically returns to Connected. - Given metrics are emitted, When observing over a 15-minute window, Then p95 provider API latency < 3s and error rate < 5%; otherwise an ops alert is triggered.
Reconciliation trigger on new or updated payout data
- Given a new or updated payout or item is persisted, When change detection runs, Then an enqueue_reconciliation message is published exactly once with payout_id, provider, change_type, and time_range. - Given messages are published, When a reconciliation worker reads the message, Then the job starts within 5 minutes and marks affected client/project income records updated. - Given multiple changes occur to the same payout within 1 minute, When enqueueing, Then only one reconciliation job is queued for that payout.
Bank Deposit Reconciliation Engine
"As a user, I want my processor payouts to be matched to corresponding bank deposits automatically so that I can verify income without manual cross-checking."
Description

Create a rules- and score-based engine that matches imported processor payouts to bank deposits from TaxTidy’s bank feed. Support exact and fuzzy matching by net amount, date windows, and currency, including cases where multiple payouts combine into one deposit or a single payout splits across deposits. Handle time-zone differences, weekends/holidays, and processor-specific settlement patterns. Provide deterministic linking, re-reconciliation when late data arrives, and manual override with full undo. Persist reconciliation links and confidence scores for each match.

Acceptance Criteria
Exact Match: Single Payout to Single Deposit
Given the bank feed contains a single deposit D and the processor feed contains a single payout P in the same currency And net_amount(D) equals net_amount(P) to the cent And date(D) equals settlement_date(P) When the reconciliation engine runs Then it links P to D with confidence_score = 1.00 and match_type = "1:1" And it persists the link and confidence_score And rerunning the engine without data changes produces the same link and score
Many-to-One: Multiple Payouts Combined into One Deposit
Given payouts P1..Pn exist in the same currency and a bank deposit D exists And amount(D) equals sum(net_amount(Pi) for i=1..n) within $0.01 And settlement_date(Pi) fall within a 2-business-day window ending on date(D) When the reconciliation engine runs Then it links {P1..Pn} to D with match_type = "n:1" And it computes confidence_score >= 0.95 for the grouped match and persists the link and score And no other payouts or deposits are linked to D
One-to-Many: Single Payout Split Across Multiple Deposits
Given a payout P exists and deposits D1..Dm exist in the same currency And net_amount(P) equals sum(amount(Di) for i=1..m) within $0.01 And date(Di) fall within 3 business days of settlement_date(P) When the reconciliation engine runs Then it links P to {D1..Dm} with match_type = "1:n" And it allocates amounts exactly to each Di and persists the link and a confidence_score >= 0.95 And P is not linked to any deposit outside {D1..Dm}
Fuzzy Match: Amount Tolerance, Date Window, Currency Enforcement
Given no exact match exists for a target deposit or payout When the engine evaluates candidate matches Then it only considers candidates with the same currency code And it applies an amount tolerance of max($1.00, 0.5% of the target amount) And it applies a date window of ±3 business days adjusted for weekends/holidays and processor settlement rules after timezone normalization And it calculates a confidence_score in [0,1] from amount proximity and date proximity And it creates and persists a link only if confidence_score >= 0.80; otherwise the item remains unmatched
Deterministic Linking and Idempotent Re-runs
Given a fixed set of deposits and payouts When the engine runs multiple times or with inputs provided in varying orders Then it produces identical links, match_type values, and confidence_scores on each run And persisted links and scores remain unchanged across service restarts And a re-run with no data changes does not modify existing links or scores
Late Data Re-reconciliation Without Duplicates
Given a deposit or payout is initially unmatched due to missing counterpart data And the missing counterpart arrives in a subsequent import When the engine re-runs automatically or on-demand Then it evaluates the new candidates and creates the highest-confidence valid link meeting acceptance thresholds And it persists the new link and confidence_score And no duplicate or overlapping links remain for the affected records
Manual Override with Full Undo and Audit Trail
Given a user selects a deposit and payout(s) and invokes Manual Link or Unlink When the user confirms the action Then the system records a manual reconciliation that supersedes any auto link And it persists the manual link with user_id, timestamp, and reason And future auto-runs do not alter manual links unless the user explicitly Undoes them And the user can Undo to restore the previous state in a single step, with the prior auto/manual link reinstated
Line-Item Grouping and Attribution
"As a freelancer, I want each deposit to show the specific invoices, fees, and refunds it contains so that I can see accurate income by client and project."
Description

For each reconciled deposit, group and display its underlying items: invoices/charges, processor fees, refunds/chargebacks, and reserves/releases. Compute gross, fees, refunds, and net totals, and attribute income to clients and projects using invoice metadata, naming rules, or saved mappings. Flag items with missing attribution and provide quick assignment. Persist attribution rules so future items auto-classify. Feed finalized records into TaxTidy’s income ledger and client/project reporting.

Acceptance Criteria
Group and Display Underlying Items per Deposit
Given a bank deposit is reconciled to one or more processor payouts When the user opens the deposit detail view Then the UI shows grouped sections for Invoices/Charges, Processor Fees, Refunds/Chargebacks, and Reserves/Releases And each item displays source processor, transaction date, amount, currency, and reference ID And each group total equals the sum of its items And items appear only under deposits they contribute to, even when multiple payouts map to the same deposit
Compute and Present Totals for Gross, Fees, Refunds, and Net
Given a reconciled deposit with underlying items When totals are computed Then Gross Total equals the sum of invoice/charge amounts included in the deposit And Fees Total equals the sum of processor fees included in the deposit And Refunds/Chargebacks Total equals the sum of refund/chargeback amounts included in the deposit And Reserves/Releases Net equals releases minus reserves included in the deposit And Net Total equals Gross Total - Fees Total - Refunds/Chargebacks Total + Reserves/Releases Net And Net Total equals the reconciled bank deposit amount within 0.01 of the base currency And all monetary values display with the correct currency symbol and two decimal places
Auto-Attribution via Metadata, Saved Mappings, and Naming Rules
Given underlying items include invoice metadata for client and project When attribution runs Then those items are attributed to the specified client and project Given remaining items match an existing saved mapping When attribution runs Then those items are attributed per the saved mapping Given remaining items match configured naming rules When attribution runs Then those items are attributed per the naming rules And attribution precedence is: invoice metadata, then saved mappings, then naming rules And attributed items display client and project on the deposit detail
Flag Missing Attribution and Quick Assignment Flow
Given one or more items lack client or project attribution When the deposit detail loads Then those items are flagged with a "Needs Attribution" indicator and counted in a summary badge When the user taps a flagged item Then an in-context assignment sheet opens without navigation away When the user selects a client and project and saves Then the item immediately shows the new attribution and the summary count updates When the user enables "Save as rule" during assignment Then a saved mapping is created for future auto-classification
Persist Attribution Rules for Future Auto-Classification
Given a saved attribution rule exists When new items with a matching signature are imported or a deposit is reprocessed Then the items auto-attribute to the rule's client and project before the user opens the deposit And auto-attributed items are not flagged as needing attribution And users can override auto-attribution on an item without altering the saved rule
Feed Finalized Records into Income Ledger and Reporting
Given all items under a deposit are attributed or the user confirms finalize When the user finalizes the deposit Then income records are written to the TaxTidy income ledger with fields: date, client, project, processor, deposit ID, gross, fees, refunds, reserves/releases net, and net And client and project reports include these records in the next reporting refresh And ledger totals per client for the deposit equal the sum of their attributed items And finalized ledger entries remain unchanged unless the deposit is explicitly re-finalized
One-Tap Match Confirmation UX
"As a busy mobile user, I want to confirm payout-to-deposit matches in one tap so that I can reconcile quickly on the go."
Description

Provide a mobile-first review experience that surfaces candidate matches as cards with summary amounts, confidence badges, and grouped line-items. Enable single-tap confirm, bulk confirm for similar matches, quick edit for client/project attribution, and instant undo. Include a review queue for exceptions, keyboard and screen-reader support, and latency under 200 ms for card actions. Record user actions to the audit log and use confirmations to improve future matching via heuristics.

Acceptance Criteria
Card Summaries with Grouped Line Items and Confidence Badges
Given a candidate match for a bank deposit on a mobile viewport 7414 px width When the card is rendered Then it displays deposit date, source platform, client (if known), and deposit amount without horizontal scrolling And invoices, fees, and refunds are grouped with counts and subtotals And subtotal(invoices)  subtotal(fees)  subtotal(refunds) equals the deposit amount within $0.01 And a confidence badge shows one of {High, Medium, Low} with a numeric score 08100 And cards are sorted by confidence score descending
One-Tap Confirm and Instant Undo (3200 ms)
Given a candidate match card is enabled When the user taps Confirm Then the UI shows a confirmed state within 3200 ms of tap and prevents duplicate taps And the deposit is linked to the grouped invoices/fees/refunds and marked reconciled And an Undo control appears for 10 seconds When the user taps Undo within 10 seconds Then the linkage is fully reverted, the card returns to candidate state, and feedback appears within 3200 ms
Bulk Confirm for Similar Matches
Given two or more candidate matches share similar patterns and have score 85 When the user selects Bulk Confirm Then all selected matches are confirmed; any failures are reported inline without affecting successful confirmations And each card shows confirmation feedback within 3200 ms and a completion summary lists counts of confirmed, skipped, failed When Bulk Undo is triggered within 10 seconds Then prior confirmations are reverted for all included items or failures are reported per item without partial corruption
Quick Edit for Client/Project Attribution
Given a candidate match card When the user opens Quick Edit Then client and project fields are focusable, searchable with autocomplete from existing values, and dismissible via Esc When the user saves Then the chosen client/project is persisted on the underlying income records and reflected on the card within 3200 ms And future bulk confirm suggestions inherit the edited attribution for similar patterns
Exceptions Review Queue
Given a deposit with no high-confidence match or amount mismatch > $0.01 When the system triages matches Then the deposit appears in the Exceptions queue with a reason tag of No Match, Low Confidence, or Amount Mismatch When the user applies filters (reason, platform, date) Then the queue updates within 3200 ms and shows an accurate count When the user resolves an item (confirm or edit) Then the item is removed from the queue and the count decrements accordingly
Accessibility: Keyboard and Screen-Reader Support
Given the review screen When navigating with keyboard only Then all interactive elements are reachable in logical tab order; Enter activates Confirm; Space toggles selection; Esc exits Quick Edit; focus is visibly indicated Given a screen reader is active When a card gains focus Then its accessible name announces client (if any), deposit amount, source platform, confidence level and score, and status (candidate/confirmed) And all buttons/links expose role, name, and state; color contrast for text and badges is 81.5:1 (WCAG 2.1 AA)
Audit Logging and Heuristic Learning
Given any Confirm, Undo, Bulk Confirm, or Quick Edit action completes When the action result is committed Then an audit log entry is written with user ID, timestamp (UTC), action type, affected record IDs, before/after values, and correlation ID Given a controlled test dataset of recurring payouts and invoices When a user confirms matches and edits attribution Then subsequent similar candidates show an increased confidence score by 810 points or prefilled client/project attribution compared to baseline
Edge Case Handling: Refunds, Disputes, and Reserves
"As a user, I want TaxTidy to correctly handle refunds, disputes, and reserves so that my income records remain accurate over time."
Description

Support complex scenarios including negative payouts, partial refunds spanning multiple settlement cycles, rolling reserve holds and releases, disputes/chargebacks and reversals, currency conversions, and adjustments. Reconcile late-arriving events by reopening and rebalancing prior deposits with clear user notifications and diffs. Maintain correct historical income after adjustments and prevent double-counting. Surface unresolved anomalies to the review queue with suggested fixes.

Acceptance Criteria
Negative Payout from Chargeback Exceeding Same-Day Sales
Given a payment processor posts a payout with a net amount < 0 due to chargeback(s) and fees When the bank feed shows a matching withdrawal within 3 business days and the event is ingested Then within 5 minutes the system creates a negative deposit record linked to the dispute(s), fees, and original invoice(s) And the negative amount is excluded from recognized income and recorded as an adjustment on the payout date And the bank transaction is auto-matched and marked Pending Confirm And an audit log entry is created with processor payout ID, bank transaction ID, component breakdown, and timestamps And processing is idempotent so duplicate processor events do not create additional adjustments
Partial Refund Spanning Multiple Settlement Cycles
Given a refund is issued that relates to invoices settled across two or more prior payouts When the refund event is ingested Then the system reopens each affected prior deposit, allocates the refund pro‑rata by original invoice amounts, and rebalances totals And a per‑deposit diff is displayed showing before/after invoice totals, fees, and net change And users are notified within 5 minutes and can confirm the rebalancing in one tap And historical income reports reflect reduced income on the original invoice dates with no double‑counting or negative leakage And processing is idempotent so retried webhook events do not alter totals after confirmation
Rolling Reserve Holds and Releases
Given the processor applies a rolling reserve percentage to payouts When a payout containing a reserve hold is imported Then the hold is recorded as a non‑income component excluded from recognized income and bank matching And the UI displays Reserve Hold amount, hold start date, and expected release date When a reserve release event is imported Then the released funds are recognized as income on the release date and allocated back to originating clients/projects proportionally And cross‑links to original payouts are maintained, preventing double‑counting before and after release And reports and reconciliation reflect the hold and release within 5 minutes of event ingestion
Disputes, Chargebacks, and Reversals Lifecycle
Given a dispute is opened on a previously recognized invoice When the processor withdraws funds for a chargeback Then the withdrawal is linked to the original invoice and deposit, the invoice status is set to Disputed, and income is reduced accordingly And dispute fees are recorded as expense components tied to the dispute When the dispute is reversed and funds are returned Then returned principal and reversed fees are allocated back to the original invoice and affected deposit(s), restoring income And all affected deposits display diffs and a complete audit trail of state changes And all steps complete within 5 minutes of each event ingestion and are idempotent across retries
Currency Conversions and FX Adjustments
Given payouts are received in currency C1 and the bookkeeping base currency is C2 When a payout and its components (fees, refunds, reserves) are imported Then original C1 amounts and the applied FX rate are stored, and C2 values are computed to 4‑decimal precision And per‑deposit rounding differences in C2 do not exceed ±0.01; any remainder is posted to FX Rounding Adjustment When a late FX adjustment is received Then prior deposits are reopened, C2 values recomputed using the new rate, diffs displayed, and reports updated without duplicating amounts And all recalculations complete within 5 minutes and are logged with the FX source and rate
Late‑Arriving Events Reopen and Rebalance Prior Deposits
Given an adjustment, fee, or refund arrives after a deposit has been reconciled When the event is ingested Then the affected deposit(s) are reopened, component totals recalculated, and the bank match updated And the user receives a notification within 5 minutes with a human‑readable diff and Accept/Undo actions And the audit log records before/after totals, component list, event IDs, timestamps, and the confirming user And if the bank transaction is locked in the ledger, a non‑destructive revision is created preserving the original record And event handling is idempotent, so duplicate or out‑of‑order events do not change final totals once confirmed
Anomaly Detection and Review Queue with Suggested Fixes
Given an event cannot be fully reconciled automatically (e.g., missing counterpart, amount mismatch > $0.50, unknown currency) When reconciliation fails Then the item appears in the Review Queue within 2 minutes with severity, root‑cause category, and suggested actions (e.g., split deposit, merge payouts, wait for reserve release) And the item includes links to affected invoices, payouts, and bank transaction(s) And after a suggested fix is applied, the system re‑evaluates and removes the item if balanced, otherwise updates the suggestion And metrics capture time‑to‑resolution and auto‑vs‑manual rates for reporting
Audit Trail and Exportable Income Ledger
"As a taxpayer, I want an auditable ledger and exports of reconciled income so that I can substantiate my filings and share records with my accountant."
Description

Maintain a tamper-evident audit trail for every reconciliation: source system IDs, timestamps, pre/post values, user confirmations, and rationale. Persist a versioned, filterable income ledger that can be exported (CSV/JSON) by date range, client, project, or processor, and consumed by the tax packet builder. Include data lineage links back to invoices and bank transactions. Provide error logs and health metrics to monitor connector and reconciliation integrity.

Acceptance Criteria
Tamper-Evident Audit Trail on Reconciliation Confirmation
Given a user one-tap confirms a payout reconciliation, When the confirmation is saved, Then an audit record is created containing reconciliation_id, deposit_id, processor (Stripe|PayPal|FreshBooks), source_record_ids[], user_id, user_email, action="confirm", rationale (non-empty), pre_state_hash, post_state_hash, created_at (UTC ISO-8601), device_fingerprint, ip_address, app_version, and version. Given an audit record is written, When the system persists it, Then content_hash and previous_record_hash fields form a verifiable hash chain per reconciliation and any mutation causes chain verification to fail. Given an existing audit record, When an update or delete is attempted via API or UI, Then the operation is rejected with HTTP 409 and message "Audit trail is immutable" and no data is changed.
Versioned Income Ledger Persistence
Given a reconciliation is confirmed or edited, When the ledger is updated, Then a new ledger_version is created with monotonically increasing version, previous_version, change_type (create|update|void), and diff_summary, and all prior versions remain queryable. Given concurrent updates, When two writes occur within 50 ms, Then the ledger commits atomically so only one version increment per transaction is observed and no partial entries exist. Given ledger version N exists, When requesting version N-1 via API, Then the system returns the exact prior state and computed totals (gross, fees, net) match the prior state within 0.01 of currency units.
Filterable Ledger Views and API Performance
Given ledger entries span multiple clients, projects, and processors, When filters are applied (date_range, client_id[], project_id[], processor in [Stripe, PayPal, FreshBooks], amount_min/max, reconciliation_status), Then only matching entries are returned and response includes total_count, sum_gross, sum_fees, sum_net. Given a dataset of up to 50,000 entries, When the above filters are applied, Then the API responds in ≤500 ms at p95 and pagination cursors are provided. Given a user saves a filter set, When it is recalled later, Then the same parameters are reapplied and results are identical to the original save (barring underlying data changes).
Export Income Ledger to CSV and JSON
Given a user selects a date range and optional filters, When exporting to CSV, Then the file includes columns: entry_id, entry_date, client_name, client_id, project_name, project_id, processor, deposit_id, invoice_ids, gross_amount, fees_amount, net_amount, refund_amount, currency, reconciliation_status, ledger_version, lineage_links[], export_generated_at, and downloads within 10 seconds for up to 100,000 rows. Given the same selection, When exporting to JSON, Then the payload is UTF-8 JSON array with schema_version="1.0" and fields equivalent to the CSV, and the endpoint returns HTTP 200 with Content-Type application/json. Given an export is generated from a filtered view, When comparing UI totals to the export, Then gross, fees, and net totals match exactly within 0.01 currency units.
Data Lineage Links to Invoices and Bank Transactions
Given a ledger entry references invoices and a bank transaction, When the entry detail is opened, Then each lineage link resolves to the source record (processor invoice URL or internal resource) and a HEAD/GET to the source returns HTTP 200. Given a source record has been removed or is unavailable upstream, When resolving lineage, Then the UI shows status "Source missing" and preserves stored snapshot fields (amount, date, payer, processor_id) without breaking audit chain verification. Given lineage links exist, When exporting the ledger, Then invoice_ids and bank_transaction_id are included and resolvable via API endpoints documented for lineage.
Error Logs and Connector Health Metrics
Given connectors run on a schedule, When viewing the health dashboard, Then for each connector the UI shows last_success_at, last_attempt_at, 7d success_rate, avg_latency_ms, error_rate, backlog_count, unmatched_deposits, and metrics refresh at least every 5 minutes. Given a connector failure occurs, When inspecting error logs, Then each entry includes timestamp (UTC), connector, severity, error_code, message, request_id, redacted_request, response_status, retry_count, and reconciliation_id (if applicable), with PII fields masked. Given error logs exist for a period, When exporting logs by date range, Then CSV and JSON exports are available and maintain the same masking rules as the UI.
Tax Packet Builder Consumption Contract
Given the tax packet builder requests income data for a filing period, When it calls the ledger API endpoint, Then the API responds within 2 seconds (p95) for up to 10,000 entries with schema_version, period start/end, per-client/project totals (gross, fees, net), and an entries array including lineage links. Given the ledger schema version changes, When the builder calls with an older accepted version, Then the API negotiates a compatible version or returns HTTP 426 with an upgrade URL and does not return partial data. Given entries have changed since a prior export, When the builder requests with an as_of_version parameter, Then the API returns data frozen at that ledger version and includes as_of_version in the response metadata.

Fee Unbundle

Peels processor fees off gross invoice amounts and categorizes them instantly. Your dashboards show true net income, per‑project profitability, and accurate deductible merchant fees—no manual math or rule tinkering required.

Requirements

Multi-Processor Fee Detection Engine
"As a freelancer, I want payment processor fees automatically separated from my invoice payments so that my net earnings are accurate without manual math."
Description

Automatically ingest transaction data from Stripe, PayPal, Square, and similar processors, parse fee components (processing, fixed per-transaction, cross‑border, currency conversion, platform/marketplace fees), and produce structured fee line items linked to the originating invoice. Compute net amounts (gross minus all fee components) in both original and reporting currency, using processor-provided exchange rates when available and a consistent fallback FX source when not. Ensure idempotent processing, retry/backoff handling for rate limits, webhook/event ordering tolerance, and sub‑second throughput to keep dashboards current. Emit normalized fee objects with transaction_id, processor, currency, amount, type, and timestamps, and store them in TaxTidy’s ledger for downstream reconciliation, categorization, and dashboards.

Acceptance Criteria
Multi-Processor Fee Parsing and Normalization
Given a batch of transactions from Stripe, PayPal, and Square containing fee breakdowns, When the engine ingests them, Then it emits one normalized fee object per fee component with fields: transaction_id, processor, currency (ISO 4217), amount, type in {processing, fixed, cross_border, fx_conversion, platform}, event_timestamp, processed_timestamp, and persists them to the ledger. Given processor-specific fee nomenclature, When normalized, Then all fee types are mapped to the canonical set and any unmapped component is emitted as type "unknown" with amount and currency preserved. Given 1,000 labeled transactions per processor, When validated, Then parsing accuracy for fee amounts and types is ≥ 99.5%. Given a normalized fee object, When validated, Then amount is stored as a negative number and currency codes are uppercase ISO 4217.
Invoice Linkage and Multi-Fee Association
Given an ingested transaction with an originating invoice reference (transaction_id/invoice_id/metadata), When processing fees, Then each fee object stores a link to the originating invoice in the ledger. Given multiple fee components for a single transaction, When persisted, Then all fee objects reference the same invoice id. Given a transaction is received before its invoice record, When the invoice arrives later, Then the linkage is completed automatically without manual intervention within 5 minutes. Given no invoice can be resolved after 24 hours, When reporting, Then the fee objects are flagged for reconciliation.
Net Amount and FX Computation
Given a transaction with gross amount and associated fee components in original currency, When computing net, Then net_original = gross_original - sum(fee.amount_original) with exact arithmetic. Given processor-provided FX rate exists for the transaction, When computing reporting currency, Then that rate is used; otherwise, the configured fallback FX source is used with rate as-of the transaction date. Given computed reporting amounts, When persisted, Then fields include reporting_currency, fx_source ("processor" or "fallback"), fx_rate, fx_timestamp, and rounding is to 2 decimals using bankers rounding. Given recomposition, When validating, Then |(gross_reporting - sum(fee_reporting)) - net_reporting| ≤ 0.01 in reporting currency.
Idempotency and Duplicate Event Handling
Given the same webhook/event is delivered multiple times (same processor and event id), When processed, Then no duplicate fee objects or net computations are created in the ledger. Given a historical backfill reprocesses the same time window, When executed, Then the total count and sums of fee objects remain unchanged. Given idempotency keys derived from (processor, transaction_id, fee type, amount, currency), When concurrent workers process overlapping batches, Then only one record per unique fee component is persisted. Given a suite that replays the same 1,000 events 10 times, When inspected, Then delta in record count is 0 and sums match exactly.
Rate Limit Retries and Backoff
Given processor API responses with HTTP 429 or rate-limit headers, When encountered, Then the engine applies exponential backoff with jitter (initial 200ms, factor 2.0, max 30s) for up to 7 attempts. Given a temporary rate limit, When retried, Then overall success rate after retries is ≥ 99% in load tests with induced 429s. Given retries are exhausted, When handling, Then the job is parked in a retry queue with visibility timeout and an alert metric is emitted; no partial writes occur. Given backoff is active, When observing metrics, Then processor-specific rate-limit gauges and retry counts are exported.
Out-of-Order Events and Dashboard Freshness
Given fee events arrive before or after their corresponding charge/invoice events, When processed, Then final ledger linkage and computed net amounts are correct based on event timestamps, regardless of arrival order. Given new or updated fee objects are persisted, When measured end-to-end from webhook receipt to ledger write, Then p50 latency ≤ 300ms and p95 ≤ 800ms at 50 TPS sustained. Given ledger updates occur, When observed from the dashboard layer, Then fee totals and net income metrics reflect changes within 1 second for 95% of events. Given out-of-order arrivals, When eventual consistency is measured, Then linkage completion time is ≤ 120 seconds for 99% of cases.
Payout and Bank Reconciliation
"As a user, I want fees matched to payouts and bank transactions so that my accounts reconcile and I can trust my dashboards."
Description

Match fee line items to the corresponding gross payments, batched payouts, and bank feed deposits to verify that net = gross − fees at the payout level. Reconcile across time gaps and partial captures, handling edge cases including refunds, partial refunds, disputes/chargebacks, reversals, and adjustments. Implement a reconciliation state machine (unmatched, partially matched, reconciled, exception) with an exceptions queue for human review. De‑duplicate events, tolerate delayed or missing webhooks by polling as fallback, and surface reconciliation status per transaction and per payout in-app. Persist reconciliation links in the ledger to drive trustworthy dashboards and exports.

Acceptance Criteria
Batched Payout Net Equals Gross Minus Fees
Given a processor payout P with gross transactions T1..Tn, associated fee line items F1..Fm, and a single bank deposit D for P When automated reconciliation runs Then sum(T.gross_amount) − sum(F.fee_amount) equals D.amount within a tolerance of $0.01 And reconciliation links (Ti↔P), (Fj↔P), (P↔D) are persisted in the ledger And P.state = "reconciled", each Ti.state = "reconciled", and D.state = "reconciled" And a reconciliation_id and completed_at timestamp are stored Given the absolute delta between (sum(T.gross) − sum(F.fee)) and D.amount exceeds $0.01 When reconciliation runs Then P.state = "exception" and an exceptions queue item is created with reason_code = "NET_MISMATCH" and delta_amount recorded Given a payout P without a matching bank deposit When reconciliation runs Then P.state = "partially_matched" if Ti and Fj are linked but D is missing; otherwise P.state = "unmatched"
Cross-Period Partial Capture Reconciliation
Given an authorization A with partial captures C1..Ck that settle across different dates and payouts When each capture appears in processor payouts and corresponding bank deposits are ingested Then each Ci is linked to its payout Pi and deposit Di, and marked reconciled individually And A.aggregate_state = "partially_matched" until sum(Ci.amount) equals A.amount or the capture_grace_period elapses (default 30 days) Given the capture_grace_period elapses and residual amount remains unmatched When reconciliation runs Then A.aggregate_state = "exception" with reason_code = "PARTIAL_CAPTURE_TIMEOUT" and residual_amount recorded And all matched captures remain reconciled without regression
Refunds and Partial Refunds Adjustment
Given a settled transaction T linked to payout P1 and bank deposit D1 And a refund R (full or partial) posts and is included in payout P2 (and deposit D2) When reconciliation runs Then R is linked to T, P2, and D2, and any fee reversals are linked to their original fee line items And net amounts for affected payouts are recomputed so that for each payout Pi: net(Pi) = sum(gross in Pi) − sum(fees in Pi) − sum(refunds in Pi) within $0.01 And T.state reflects the aggregate across original sale and refunds: "reconciled" when amounts fully balance; otherwise "partially_matched" Given the recomputed nets do not balance within $0.01 after refund linkage When reconciliation runs Then an exception is created with reason_code = "REFUND_MISMATCH" and delta_amount recorded
Disputes/Chargebacks and Reversals Handling
Given a dispute or chargeback event E referencing an original transaction T that has been settled When E is received Then an adjustment entry is created and linked to T and any related payout(s) And T.state = "exception" and an exceptions queue item is created with reason_code = "DISPUTE_OPENED" When a dispute resolution event is received Then if outcome = "won", the adjustment is reversed and T.state returns to its prior reconciliation state And if outcome = "lost", the negative adjustment is finalized, associated payout(s) nets are recomputed within $0.01, and T.state = "reconciled" And all state changes are audit-logged with timestamps and source (webhook/poll)
Reconciliation State Machine and Exceptions Queue
Rule: Allowed states are {"unmatched","partially_matched","reconciled","exception"} Rule: Allowed transitions are - unmatched → partially_matched | exception - partially_matched → reconciled | exception | unmatched - reconciled → exception (only via downstream adjustment: refund, dispute, reversal) - exception → unmatched | partially_matched | reconciled (upon human resolution or new data) Rule: Each transition writes an immutable audit log with {entity_id, entity_type, from_state, to_state, reason_code?, actor, occurred_at} Rule: Exceptions queue items include {entity_id, entity_type, reason_code, delta_amount?, first_seen_at, last_updated_at, links:{transaction_id?, fee_id?, payout_id?, bank_entry_id?}} Rule: Newly detected exceptions appear in the queue within 60 seconds of detection and are filterable by reason_code and entity_type Rule: Resolving an exception updates the entity state per allowed transitions and records resolver and resolution_note
Webhook Deduplication and Polling Fallback
Given duplicate or retried webhooks with identical processor event_id When processed Then only one reconciliation mutation occurs (idempotent handling) and no duplicate ledger links are created Given out-of-order webhooks (e.g., payout before transactions) When processed Then reconciliation defers until prerequisites are available (max wait 2 minutes) and then proceeds without error Given a webhook type expected but not received within 10 minutes of a related event When the polling job runs (every 5 minutes) Then missing transactions, fees, refunds, disputes, payouts, and deposits are fetched via API and reconciliation is attempted And metrics are recorded: webhook_deduplicated_count, webhook_out_of_order_count, poll_catchup_count, reconciliation_retry_success_count
In-App Status and Ledger Persistence
Given any transaction, payout, or bank deposit entity When viewed in-app Then the current reconciliation state and (if applicable) exception reason_code are displayed consistently on the entity detail and list views And totals shown on dashboards reflect ledger-derived links so that Net Income = sum(reconciled payout nets) and Deductible Merchant Fees = sum(reconciled fee amounts) And CSV/JSON exports include fields {reconciliation_state, reconciliation_id, payout_id?, bank_entry_id?} for each transaction Rule: Ledger persists reconciliation_link records with fields {transaction_id?, fee_id?, refund_id?, dispute_id?, payout_id?, bank_entry_id?, link_type, created_at, source} Rule: Reconciliation is deterministic: re-running the process with identical inputs produces identical links and totals
Auto-Classification to Deductible Merchant Fees
"As a taxpayer, I want processor fees auto-categorized as deductible expenses so that my tax packet is accurate and audit-ready."
Description

Automatically categorize detected processor fees under a standard deductible category (Merchant Processing Fees) aligned with Schedule C and TaxTidy’s taxonomy. Maintain a mapping table per processor fee type to the internal chart of accounts and to external systems (QuickBooks, Xero) for sync/export. Allocate fees to projects/jobs when an invoice is project‑tagged so per‑project profitability reflects true net. Allow user overrides and category locks without breaking future automation. Include multi‑currency handling so expense category amounts reflect reporting currency while retaining source currency for audit.

Acceptance Criteria
Auto‑Categorize Processor Fees on Ingestion
Given a supported processor transaction with identifiable fee metadata is ingested When ingestion completes Then the fee component is split from the gross and categorized to TaxTidy "Merchant Processing Fees" (Schedule C aligned) within 60 seconds And the transaction is labeled Auto‑Classified with processor name and fee type captured And no manual rules are required to achieve the classification
Processor Fee Mapping Table Resolution
Given a mapping table entry exists for processor_id + fee_type_code -> internal_account_id When a fee with that processor_id and fee_type_code is detected Then the internal account used is the mapped account and the user‑visible category is "Merchant Processing Fees" And when a mapping is updated Then new classifications created after the change use the updated mapping and existing posted transactions are not changed automatically And when no fee_type_code mapping exists Then the processor‑level default mapping to Merchant Processing Fees is applied and an informational log is recorded
External Accounting Sync to QuickBooks/Xero
Given QuickBooks or Xero is connected and category/account mappings are configured When a fee expense is exported Then it posts to the configured external expense account for Merchant Processing Fees with correct class/tracking category for the related project (if any) And the export uses the reporting‑currency amount and includes source currency, source amount, and FX rate in the memo And retries are idempotent and do not create duplicate entries (same external_id reused)
Per‑Project Fee Allocation and Profitability
Given an invoice is tagged to Project Alpha and its associated processor fee is detected When the fee is recorded Then the fee amount is allocated to Project Alpha And the project profitability report shows Net Revenue = Gross Invoice − Allocated Fees within 5 minutes of ingestion And per‑project totals reconcile to global totals for the same period
User Override and Category Lock Behavior
Given a user changes a fee transaction’s category and enables Lock When automated reclassification runs or similar new transactions are ingested Then the locked transaction’s category remains unchanged And future similar transactions (same processor and fee type) default to the user’s chosen category unless a lock exists on those transactions And the user can unlock and reclassify, with an audit log capturing user, timestamp, old category, and new category
Multi‑Currency Fee Recording and Reporting
Given a fee is charged in a source currency (e.g., EUR) and the workspace reporting currency differs (e.g., USD) When the transaction is recorded Then the system stores source amount and currency, FX rate (with date and source), and the computed reporting‑currency amount And reports/dashboards display only reporting‑currency amounts while transaction detail shows source currency context And rounding differences are ≤ 0.01 in reporting currency per transaction and period totals reconcile within ±0.01
Unbundling and De‑Duplication Across Sources
Given both a bank feed net payout and a processor statement/invoice are ingested for the same settlement When unbundling runs Then the system links the records and recognizes the fee once without duplication And Gross − Fees = Net holds for each settlement; discrepancies are flagged for review and not auto‑exported And linkage is traceable via a settlement identifier visible in the transaction audit trail
Historical Backfill and Dashboard Recompute
"As a user, I want past transactions backfilled and dashboards recomputed so that historical profitability and taxes are correct."
Description

Upon enabling Fee Unbundle, backfill up to 24 months of processor data, detect and store historical fee line items, and recompute net income, per‑project profitability, and deductible totals. Run as an incremental, resumable background job with progress reporting and safe throttling. Version calculations so previously exported tax packets remain reproducible, while dashboards reflect the latest computations. Update caches and indexes atomically to prevent transient inconsistencies, and record all recalculation metadata for traceability.

Acceptance Criteria
Enable Fee Unbundle Triggers 24‑Month Backfill
Given a user enables Fee Unbundle for a connected processor When the backfill job is started Then the system requests transactions from the earlier of 24 months prior or the processor connection date And only transactions eligible for fee unbundling are queued And transactions already unbundled are skipped And an initial queue size and time range are recorded And idempotency is enforced using processor_transaction_id + account_id so no duplicates are created And the backfill can be initiated only once per account until it is completed or explicitly canceled
Resumable Backfill with Throttling and Progress Visibility
Given a backfill job is running When a worker restarts or crashes Then processing resumes within 5 minutes from the last committed checkpoint without reprocessing committed batches And checkpoints persist at least every 500 transactions or 60 seconds, whichever comes first Given processor API rate limits are approached When 429 or rate-limit signals are observed Then the job applies exponential backoff to keep 429s under 1% and average request rate at or below the configured limit Given a progress endpoint is queried When the client requests status Then it returns discovered, processed, skipped, failed counts, percent complete, current phase, and ETA updated at least every 30 seconds Given a pause or cancel request is issued When the job receives the request Then it quiesces within 60 seconds and persists state for later safe resume
Accurate Historical Fee Detection and Storage
Given fetched transactions may contain embedded or separate processor fee records When parsing and normalizing fees Then each fee line item is stored with amount, currency, date, source_ids, and linked to its gross transaction and project And amounts reconcile to processor statements within ±0.01 in source currency (or currency minor unit) And refunds, reversals, and disputes produce offsetting fee entries with correct signs And duplicate ingestion is prevented via idempotency keys; reprocessing yields no additional fee rows And all stored fees pass schema validation and precision rules for the currency And a per-batch reconciliation report records totals by day, currency, and processor
Recompute Net Income, Per‑Project Profitability, and Deductible Totals
Given historical fee entries are stored When recomputation runs for an account Then net income per transaction equals gross minus processor fees minus taxes withheld (if present) with currency-consistent rounding And per‑project profitability and period dashboards update within 5 minutes of batch commit And deductible merchant fee totals increase by the sum of detected fees for the selected period(s) And unrelated metrics remain unchanged (tolerance = 0.00 in native currency) And all recomputed entities are stamped with a new calc_version And recomputation is skipped for transactions already consistent with the latest calc_version
Versioned Calculations Preserve Export Reproducibility
Given one or more tax packets were exported before Fee Unbundle backfill When recomputation completes Then previously exported packets remain byte‑identical and reference their original calc_version And new exports default to the latest calc_version And users can optionally re‑export using a prior calc_version without mutation of historical packets And an audit log entry records user, calc_version_from, calc_version_to, timestamp, and affected ranges And calc_versions are immutable and queryable for any transaction or summary
Atomic Cache and Index Publication Prevents Transient Inconsistencies
Given recompute results are ready to publish When writing to analytics caches and search indexes Then readers observe either the old version or the new version for any entity, never a mix And publication is all‑or‑nothing; on failure the system rolls back to the prior state And read‑after‑write consistency for the account is achieved within 60 seconds And cache warmup for affected dashboards completes within 2 minutes And monitoring alerts on partial reads or version skew are absent during steady state
Comprehensive Traceability and Audit Metadata
Given a recomputation run starts When it progresses and completes (or fails) Then metadata is recorded: job_id, account_id, processors, time_range, discovered/processed/skipped/failed counts, calc_version_from/to, throttle/backoff events, error summaries, and checkpoint cursors And this metadata is queryable via admin UI/API and exportable as JSON/CSV And retention is at least 24 months with PII redacted per policy And each mutation to fees or recomputed values references the originating job_id for end‑to‑end traceability
Exceptions and Overrides UI (Mobile-First)
"As a mobile-first user, I want to quickly resolve exceptions and override fee matches so that edge cases don’t block accurate reporting."
Description

Provide an in‑app, mobile‑optimized workflow to review and resolve reconciliation exceptions (unmatched fees, ambiguous matches, suspected duplicates). Allow manual link of a fee to an invoice/payout, split or adjust fee allocations, mark fees as non‑deductible, and lock decisions. Include inline guidance, bulk actions, undo/rollback, and full accessibility support. All changes write to the ledger with who/when/why annotations and do not block ongoing automation.

Acceptance Criteria
Resolve Unmatched Processor Fee on Mobile
Given I am on the Exceptions screen on a mobile device and an Unmatched Fee exists When I open the exception, review suggested matches, search by amount/date/processor reference, and select an invoice or payout, then tap Link Then the fee is linked to the selected target, the exception count decreases by 1, a success message appears within 1 second, the ledger records who/when/why with source identifiers, and background automation continues without pause And the linked invoice/payout detail reflects the associated fee within 5 seconds of confirmation
Split Fee Across Multiple Invoices or Payouts
Given a processor fee requires allocation across more than one invoice or payout When I choose Split and enter allocations by amount or percentage for 2–10 targets Then the UI validates that totals equal 100% or the exact fee amount, enforces 0.01 currency precision with rounding to the largest remainder, and blocks save until valid And upon save, per-split ledger entries are created with who/when/why annotations and category Merchant Fees; all affected invoices/payouts display their share within 5 seconds; automation continues
Mark Fee as Non‑Deductible and Lock Decision
Given I am viewing a fee that should not be deducted for taxes When I select Mark Non‑Deductible, provide a reason (chosen from list or custom note min 5 characters), and toggle Lock Then the fee is categorized as Non‑Deductible, a lock icon is shown in list and detail, re-imports and auto-rules do not override this classification, and the ledger records who/when/why including reason and lock status And the action is reversible only via explicit Unlock with confirmation and audit trail
Bulk Resolve Ambiguous Matches with Confidence Threshold
Given a list of Ambiguous Match exceptions is displayed When I multi-select up to 50 items and apply Auto‑link with a confidence threshold of 80%, then confirm the summary dialog Then items with suggested matches at or above 80% are linked; items below remain unchanged with per-item explanation; partial failures are reported inline without aborting the batch And batch execution completes within 5 seconds for 50 items on a median mobile device; all changes create individual ledger entries with who/when/why; an Undo option is available for the batch for 10 minutes
Undo or Rollback of Overrides
Given I have applied overrides (link, split, non‑deductible, dismiss) within the last 60 minutes When I tap Undo on a specific override from the Activity log Then the system restores the prior state (including allocations and categorizations), creates a reversal ledger entry referencing the original, and updates affected dashboards on next refresh And Undo is disabled with explanatory message if the affected period is locked by a finalized tax packet
Mobile Accessibility Compliance for Exceptions UI
Rule: The Exceptions and Overrides UI conforms to WCAG 2.2 AA on mobile Rule: All actionable elements have accessible names and roles; focus order follows visual order; error messages are programmatically associated Rule: Minimum touch target size is at least 44x44 dp; color contrast is at least 4.5:1; supports Dynamic Type 80–200% without clipping or loss of functionality Rule: Screen readers announce exception type, status, and action outcomes (including toasts); all actions are operable via an external keyboard; no interaction relies solely on motion or color
Resolve Suspected Duplicate Fees
Given two or more fees are flagged as Suspected Duplicates When I open the duplicate group, compare details side‑by‑side, and choose Merge as Duplicate or Not a Duplicate Then Merge keeps a single primary record, prevents double counting, and writes ledger close-out entries for merged items with who/when/why referencing the primary; Not a Duplicate dismisses the exception with annotation And future imports that match the dismissed pattern are auto‑dismissed with the prior rationale; background automation continues throughout
Audit Trail and Exportable Annotations
"As a user, I want an audit trail and exportable annotations for each fee so that I can substantiate deductions to the IRS if questioned."
Description

Capture and display complete provenance for each fee: source processor event IDs, raw payload pointers, parsed components, exchange rate source and timestamp, reconciliation steps and outcomes, and user override history. Include immutable checksums for exported documents, export options (CSV, JSON, PDF) with line-item notes, and embed annotations in TaxTidy’s IRS‑ready packet. Enforce data retention policies and minimize stored PII while preserving evidentiary value for audits.

Acceptance Criteria
Display Fee Provenance in Fee Details
Given a fee is unbundled from a processor transaction, when a user opens the Fee Details view for that fee, then the following fields are present and non-empty: fee_id, processor_name, processor_event_id, raw_payload_pointer (URI), parsed_components.amount, parsed_components.currency, parsed_components.type, capture_timestamp (ISO 8601 UTC). Given the original transaction involved currency conversion, when the fee is displayed, then exchange_rate.source, exchange_rate.timestamp (ISO 8601 UTC), and exchange_rate.rate are present; and when no conversion occurred, then exchange_rate fields are null. Given the gross and net amounts of the source transaction, when the fee is calculated, then parsed_components.amount equals (gross - net - other_fees) within ±0.01 of the fee currency. Given the raw_payload_pointer, when the link is resolved with a valid session, then it returns 200 via a time-limited, access-controlled URL; and when accessed without authorization, then it returns 403.
Reconciliation Steps Logged for Fee Unbundling
Given a fee is unbundled and reconciliation runs, when the audit trail is retrieved, then it contains an ordered list of steps with fields: step_id, step_type, input_refs, actor (system|user), outcome.status (success|failure), outcome.reason_code (if failure), started_at, ended_at. Given the fee is successfully matched, when the final step is recorded, then outcome.status is "success" and links include invoice_id and/or bank_tx_id where applicable. Given a reconciliation failure occurs, when the audit trail is inspected, then there is a failure step with a non-empty reason_code and a subsequent retry step if a retry was attempted, both timestamped in ISO 8601 UTC. Given any audit trail record, when fields are validated, then timestamps are monotonic non-decreasing across steps for the same fee.
User Override History Captured
Given a user edits a fee’s category, note, exchange rate, or amount, when they save the change, then a history entry is created with fields: change_id, field_name, before_value, after_value, user_id, timestamp (ISO 8601 UTC), and optional reason_note (required when amount changes). Given multiple edits occur, when the fee history is viewed, then entries are listed in chronological order and no previous entry is mutated. Given an override exists, when the current fee representation is shown, then it reflects the latest values and displays an "overridden" indicator with count of overrides.
Export Annotations with Immutable Checksums
Given a user requests an export for a date range, when the export completes, then CSV, JSON, and PDF files are generated and available for download, and each includes line-item annotations/notes for every fee. Given the export files are generated, when checksums are produced, then a SHA-256 checksum is generated per file, stored in an append-only audit log with file_name, checksum, generated_at, and included as a .sha256 sidecar; the PDF embeds the checksum in document metadata. Given any downloaded export file, when its SHA-256 is computed client-side, then it matches the published checksum exactly. Given the CSV and JSON exports, when a record is inspected, then it includes fields: fee_id, processor_event_id, raw_payload_pointer, parsed_components, exchange_rate.source, exchange_rate.timestamp, exchange_rate.rate, reconciliation_status, override_count, line_item_note.
Embed Annotations in IRS-Ready Packet
Given a user generates the IRS-ready packet for a tax year, when the PDF packet is produced, then it contains a section or appendix listing fee annotations per line item, including fee_id, processor_event_id, reconciliation_status, exchange_rate.source (if any), and the line-item note. Given a fee line shown in the packet, when the cross-reference is followed, then it points to the corresponding fee_id present in the in-app audit trail or export. Given the packet is regenerated for the same period with unchanged data, when the packet’s embedded checksum manifest is compared, then checksums for unchanged sections remain identical.
Data Retention and PII Minimization Enforcement
Given a data retention policy is configured to retain audit evidence for N years and purge/minimize PII after M days, when the scheduled retention job executes, then PII fields (payer_name, payer_email, card_last4, bank_account_last4, raw_payload_body) are purged or redacted while preserving non-PII evidentiary fields (processor_event_id, timestamps, amounts, checksums, outcome logs, raw_payload_pointer). Given PII has been purged per policy, when any audit artifact is retrieved, then no full PII values are returned; pointers remain resolvable only via time-limited, access-controlled URLs and return 403 when expired. Given the retention job runs, when a retention report is generated, then it lists counts of purged/redacted records and confirms no deletion of checksum or audit trail records required for evidentiary value.

Client Resolver

Unifies duplicate client profiles across platforms using smart matching on names, emails, and invoice metadata. Keep a single, clean client record for totals, tags, and notes so 1099 tracking and project reporting stay accurate.

Requirements

Fuzzy Client Matching Engine
"As a freelancer, I want TaxTidy to automatically detect when two client entries are the same so that my reports and 1099 totals aren’t split across duplicates."
Description

Build a multi-strategy matching engine that ingests client records from invoices, bank feeds, email contacts, and receipt metadata, then normalizes and compares them using deterministic keys (email, EIN/SSN when available), tokenized/phonetic name matching, domain similarity, billing address overlap, invoice/project code correlation, and fuzzy string algorithms. Produce a confidence score with configurable thresholds for auto-merge vs. manual review, and expose human-readable match explanations. Support incremental real-time matching on new imports and scheduled batch reconciliation, enforce strict tenant boundaries, handle international names/UTF-8, and operate under PII-safe constraints. Provide metrics and alerts for match quality, and ensure the engine is extensible with pluggable rules and weights.

Acceptance Criteria
Matching Strategies and Confidence Scoring
Given client records ingested from invoices, bank feeds, email contacts, and receipt metadata within the same tenant And deterministic identifiers (normalized email OR hashed EIN/SSN) are equal When matching runs with auto_threshold=0.90 and review_threshold=0.70 Then the confidence score is 1.00 and the records are auto-merged Given client records within the same tenant have no deterministic match And name tokens and phonetics similarity >= 0.85, domain similarity >= 0.80, billing address overlap >= 0.75, and invoice/project code correlation >= 0.70 When matching runs with configured weights Then a confidence score between 0.00 and 1.00 is computed with two-decimal precision And if score >= auto_threshold the records auto-merge And if review_threshold <= score < auto_threshold the pair is queued for manual review And if score < review_threshold the pair is neither merged nor queued Given two records belong to different tenants When matching runs Then a score may be computed but no merge or review is created across tenants
Human-Readable Match Explanations
Given any match candidate is produced (auto-merge or manual-review) When requesting match details via API or viewing in UI Then an explanation is returned containing: decision (auto-merge/manual/none), overall score, top 5 contributing signals with names, normalized values compared, and per-signal score contributions And the explanation is human-readable in English and <= 800 characters for the UI summary And field names are stable and documented And for auto-merge decisions the explanation is persisted with the merge audit record
Real-Time Incremental Matching and Scheduled Batch Reconciliation
Given a new client-like record is imported When the event is processed Then matching completes within 500 ms p95 and 1 s p99 per record And the operation is idempotent (replaying the same event does not create duplicate merges) And concurrency controls prevent double-merge under parallel ingestion Given the nightly batch job runs at a configurable schedule (default 02:00 UTC) When there are unmatched or stale records Then the job evaluates all candidates and yields identical decisions to real-time rules And progress, counts, and durations are logged and exported as metrics
International and UTF-8 Name Handling
Given records contain names with diacritics or non-Latin scripts (e.g., “José García”, “Müller GmbH”, “株式会社アカメ”) When normalization and matching run Then Unicode normalization (NFKC) and casefolding are applied without data loss And diacritic-insensitive and transliterated comparisons are used for fuzzy signals And locale test suites across at least three languages achieve >= 0.85 similarity for true positives and <= 0.05 for clear negatives Given mixed-script or emoji in free-text fields When matching runs Then such characters do not crash processing and are ignored for similarity without reducing the true-positive rate below target
PII Safety and Logging Controls
Given EIN/SSN values are present When matching runs Then comparisons use salted hashes and no plaintext EIN/SSN is persisted or logged And logs, metrics, and explanations redact PII fields And access to PII-derived signals requires role-based authorization And a configuration flag allows disabling PII-based matching per tenant
Match Quality Metrics and Alerts
Given the engine processes matches When weekly evaluation runs on a labeled sample of at least 500 pairs per tenant cohort Then estimated precision >= 0.98 and false-merge rate <= 0.5% And recall >= 0.90 for top-100 clients by volume And metrics (precision, recall, false_merge_rate, review_queue_size, score_distributions) are exported to monitoring at 1-min resolution And an alert is fired within 5 min if precision drops below 0.95 or review_queue_size exceeds 2x the 30-day baseline
Extensible Pluggable Rules and Weights
Given a new matching-rule plugin is added and configured When the engine reloads configuration Then the rule becomes active without service restart and is versioned (e.g., ruleset vX.Y) And weights can be changed per tenant or globally with effective-from timestamps And A/B buckets can assign different weights to at least 10% of traffic And if a plugin errors, it is isolated, contributes zero score, and does not block other rules And all changes are audit-logged with who/when/what
Merge Preview & Field-Level Conflict Resolver
"As a user, I want to review and resolve conflicts before merging duplicates so that I keep the most accurate client information."
Description

Offer an interactive merge preview that surfaces candidate duplicates side-by-side with field-level provenance (which source provided each value) and a clear pre-/post-merge diff. Allow users to select winning values per field (name, email(s), phone(s), address, notes), merge arrays (contacts, tags) with de-duplication, and preserve non-chosen values as secondary/alternate fields where appropriate. Enforce safety checks (e.g., block merges with conflicting tax IDs), display attachment counts and recent activity, and honor source-of-truth priorities with override capability. Provide accessible, mobile-friendly controls, keyboard shortcuts, and confirmation with a summary of impacts before execution.

Acceptance Criteria
Side-by-Side Preview with Provenance, Diff, and Activity
Given two or more candidate duplicate client records are selected for merge When the merge preview loads Then both records are displayed side-by-side with rows for name, emails, phones, address, notes, contacts, tags, and tax ID And each displayed value shows a provenance label including source system name and last-updated timestamp (if available) And a pre-/post-merge diff panel shows the proposed post-merge value for every field before any user changes And fields with conflicting values are visually highlighted And attachment count and a recent-activity summary (e.g., last invoice date, last note date) are shown for each candidate And the preview renders in ≤ 2 seconds for records with up to 200 total array items across fields
Field-Level Winner Selection and Alternate Preservation
Given the merge preview is open And multiple values exist for a field When the user selects a winning value for a single-valued field (name, address, notes, tax ID) Then the selected value becomes the post-merge value and non-selected values are not applied And blank values cannot override non-blank values unless explicitly selected by the user When the user selects a winning value for a multi-valued field (emails, phones) Then non-selected unique values are preserved as alternates in the resulting record with provenance retained And the post-merge diff updates immediately upon each selection
Array Merge and De-duplication for Contacts and Tags
Given both candidates contain contacts and tags When the merge is executed Then the resulting contacts array is the union of unique contacts, de-duplicated by normalized email (case-insensitive) and/or E.164-normalized phone And no more than one contact is marked primary after merge; if multiple primaries existed, the primary from the higher-priority source is retained unless the user chose otherwise And the resulting tags list is the case-insensitive de-duplicated union of all tags And array item provenance is retained for each merged contact and tag
Safety Check: Block Merge on Conflicting Tax IDs
Given candidate records both contain a non-empty tax ID (EIN/SSN) value And the normalized tax IDs are not equal When the user views the merge preview or attempts to confirm merge Then the Merge action is disabled and a blocking error explains that merges with conflicting tax IDs are not allowed And the UI identifies the conflicting field(s) inline without revealing full sensitive values (masking applies) And the merge can proceed only after the conflict is resolved by editing to a single matching or empty value
Source-of-Truth Priorities with User Override
Given source-of-truth priorities are configured for connected systems When the merge preview opens Then default winning values are auto-selected per field according to the configured priority; ties are broken by most recent last-updated timestamp And an indicator shows when a value was auto-selected due to source priority When the user manually selects a different value Then the manual override is applied for that field and clearly marked as overridden And auto-selection does not prevent user override except where a separate safety check blocks merging
Accessible, Mobile-Friendly Controls and Shortcuts
Given the merge preview is open on desktop When navigating with keyboard only Then all actionable elements are reachable in a logical order, have visible focus states, and support Enter/Space activation And keyboard shortcuts exist: M to open merge preview (from duplicate list), Arrow keys to move selection between candidates/fields, and Cmd/Ctrl+Enter to open confirmation; shortcuts are discoverable via a help hint Given the merge preview is open on a mobile device (≤ 414px width) Then layout stacks vertically with sticky field headers, controls are finger-friendly (tap targets ≥ 44x44px), and horizontal scrolling is not required for primary actions And the UI meets WCAG 2.1 AA for color contrast and screen reader labels on field names, provenance, and diff
Pre-Merge Confirmation Summary and Execution
Given field selections are complete When the user clicks Merge Then a confirmation dialog summarizes impacts: number of fields updated, number of emails/phones/contacts/tags added or de-duplicated, count of overrides vs. auto-selected values, and which record will be retained as the survivor And the dialog lists any blocked items (none if proceeding) and requires explicit confirmation before execution When the user confirms Then the merge executes and the resulting client record reflects all selected winners, preserved alternates, and merged arrays, with provenance retained
Referential Re-linking of Historical Records
"As a user, I want all past invoices and expenses to roll up under a single client after a merge so that totals and tax packets are accurate."
Description

Upon merge, reassign all historical references (invoices, receipts, bank transactions, projects, time entries, notes, and attachments) from deprecated client IDs to the canonical client record. Recompute aggregates and indexes (lifetime totals, outstanding balances, project summaries, tags), regenerate 1099-related figures, and update caches and search indexes without downtime. Execute as an idempotent background job with progress reporting, chunked processing, and automatic retries. Maintain an alias map from old IDs to the canonical client to prevent re-creation and to preserve deep links. Guarantee consistency so no double counting or orphaned records occur.

Acceptance Criteria
Reassign Historical Records to Canonical Client
Given a merge request combining two or more client IDs under the same account And the canonical client ID is selected When the re-linking job executes Then 100% of invoices, receipts, bank transactions, projects, time entries, notes, and attachments previously referencing deprecated IDs are updated to reference the canonical ID And verification queries find zero records referencing deprecated IDs in those entity tables And the job emits a summary of reassigned counts per entity type
Aggregate Recompute Without Double Counting or Orphans
Given pre-merge datasets with overlapping entities across merged clients When aggregates and indexes are recomputed Then lifetime totals, outstanding balances, project summaries, and tag aggregates equal the distinct union of underlying records without duplication And no report or dashboard displays the same invoice or expense more than once And referential integrity checks detect zero orphaned child records
Regenerate 1099 Figures for Canonical Client
Given the merged client is 1099-eligible When 1099 figures are regenerated after re-linking Then year-to-date totals for each applicable tax year equal the sum of unique reportable payments across all merged IDs, excluding non-reportable categories And regenerated exports and forms reference the canonical client's identifiers and details And prior 1099 exports for affected years are marked superseded with a pointer to the new export And an audit log entry records inputs, old IDs, new ID, and totals before/after
Zero-Downtime Cache and Search Index Update
Given production read/write traffic is ongoing When caches and search indexes are updated during the merge job Then read and write operations continue without service downtime And, upon job completion, searching by any former client name/email/invoice metadata returns only the canonical client record And cache entries for deprecated IDs are invalidated or aliased to the canonical ID before the job reports 100% complete
Idempotent Job with Retries and Deterministic Outcome
Given the merge job is triggered multiple times for the same set of client IDs When it executes concurrently or sequentially Then results are deterministic with no duplicate reassignment or aggregate inflation And transient failures are retried automatically with backoff, up to the configured attempt limit And the job can safely resume from the last successful checkpoint after a crash or timeout
Chunked Processing with Progress Reporting
Given a merged client with a large history (e.g., >100k related records) When the job runs Then records are processed in batches not exceeding the configured batch size And a progress endpoint/report returns total records discovered, records processed, percentage complete, current phase, and estimated time remaining And progress updates occur at least once per configured reporting interval during active processing
Alias Map Prevents Re-Creation and Preserves Deep Links
Given deprecated client IDs exist after merge When any API call, deep link, or internal reference uses a deprecated ID Then the system resolves it to the canonical client via the alias map without error And attempts to create or import a client matching a deprecated ID are blocked or merged into the canonical record using the alias map And HTTP routes for deprecated IDs respond with a permanent redirect to the canonical route And the alias map is queryable for audit and includes timestamp and source merge job ID
Integration-Aware Sync & Duplicate Suppression
"As a user, I want merges in TaxTidy to stay in sync with my external apps so that duplicates don’t reappear after the next import."
Description

Detect and honor external system capabilities (e.g., QuickBooks vendor/customer merge, Stripe customer merge). When supported, propagate merges via API/webhooks; when not, bind multiple external IDs to the canonical client and suppress duplicate re-creation on future imports. Implement idempotent sync operations, rate limiting, exponential backoff, and a reconciliation view for partial failures. Map and backfill external references, prevent circular updates, and maintain a per-integration capabilities matrix. Ensure merges in TaxTidy do not regress into duplicates after the next sync cycle.

Acceptance Criteria
Propagate Merge to Supported Integrations
Given an integration in the capabilities matrix has mergeSupport = true And Client A (canonical) and Client B are linked to external IDs extA and extB for that integration When the user merges Client B into Client A in TaxTidy Then TaxTidy issues a single merge request to the integration to merge extB into extA And upon receiving a success callback or webhook, TaxTidy marks Client B as merged and inactive And the external reference map retains extA as primary and archives extB under Client A And subsequent syncs do not create a new client in the external system or in TaxTidy for extB
Bind External IDs When Merge Not Supported
Given an integration in the capabilities matrix has mergeSupport = false And Client A and Client B are linked to external IDs extA and extB for that integration When the user merges Client B into Client A in TaxTidy Then TaxTidy binds both extA and extB to Client A in the external reference map And TaxTidy does not call any merge endpoint for that integration And future imports referencing extB are attributed to Client A And no duplicate client is created in TaxTidy or in the external system on subsequent syncs
Idempotent Sync and Duplicate Suppression
Given a previous merge of Client B into Client A exists and mappings for extA/extB are stored And an idempotency key is generated for the merge propagation job When the sync job is retried due to transient errors Then no additional merge or update requests are executed beyond the first successful attempt And no new client records are created internally or externally And audit logs show a single logical operation correlated by the idempotency key
Rate Limiting and Exponential Backoff
Given an external API responds with HTTP 429 and a Retry-After header When TaxTidy processes merge or mapping updates for clients Then TaxTidy applies exponential backoff with jitter honoring Retry-After where present (initial delay ≥ 1s, max delay ≤ 60s) And retries the request up to 5 times before marking a partial failure And limits concurrent requests per integration to 5 in-flight operations And records throttle events and retry counts in the audit log
Reconciliation View for Partial Failures
Given one or more merge or mapping operations failed after all retries for any integration When a user opens the Reconciliation view Then the list displays client name, integration, operation type, external IDs involved, error code/message, retry count, and last-attempt timestamp And the user can trigger individual retry or bulk retry And successful retries remove items from the list automatically And the view provides deep links to the external records where applicable
External Merge Webhook Mapping and Backfill
Given an external system merges two client records and emits a webhook or is detected on import When TaxTidy ingests the event indicating extB merged into extA Then TaxTidy binds extA and extB to the canonical client without creating a new client And stops outbound updates that would recreate extB (prevents circular updates via source-of-truth flag) And backfills historical transactions referencing extB to the canonical client within 5 minutes And updates the capabilities-aware mapping table accordingly
No Duplicate Regression After Next Sync
Given a merge has been completed in TaxTidy and external mappings are current When the next scheduled sync runs across QuickBooks, Stripe, and CSV importer Then all inbound records referencing former external IDs map to the canonical client And no new internal duplicates are created And client totals, tags, notes, and 1099 reporting reflect the unified client And automated regression tests for these integrations pass
Audit Trail & One-Click Undo
"As a user, I want a clear history and a safe way to undo a merge so that I can fix mistakes without losing data."
Description

Create an immutable audit log for every merge containing initiator, timestamp, match evidence, fields changed, before/after snapshots, and the list of relinked records. Provide a reversible window (e.g., 14 days) for one-click undo that safely rolls back field values, referential links, and integration calls when possible, with safeguards if intervening changes exist. Support role-based access, exportable logs for compliance, redaction of sensitive PII in UI views, and retention policies aligned with tax record requirements.

Acceptance Criteria
Immutable Merge Audit Log Captures Required Fields
Given a user with permission merges two client profiles in Client Resolver When the merge is confirmed Then the system writes an immutable audit event for the merge And the event contains: unique event ID, initiator user ID and role, timestamp (ISO 8601 UTC), match evidence (matched names, emails, invoice metadata references and similarity scores), fields changed with before/after values, list of relinked record IDs grouped by type (invoices, bank transactions, receipts), and any integration calls triggered with target system and result And the event stores before/after snapshots of the merged client profiles And the audit event cannot be edited or deleted by any role via UI or API And any attempt to modify or delete the event is rejected with HTTP 403 and is itself logged as an access violation
One-Click Undo Within 14-Day Window Fully Restores State
Given a merge audit event exists and its timestamp is less than 14 days old And the requester has Undo permission When the user clicks Undo Merge Then the system atomically restores the pre-merge client records, including all field values and identifiers And re-establishes original referential links for invoices, bank transactions, and receipt images And issues compensating calls to connected integrations that support unmerge/rollback And records an Undo audit event linked to the original merge event And updates totals, 1099 tracking, and project reporting to reflect the restored state And the Undo action completes within 10 seconds for merges affecting up to 5,000 linked records And the Undo control is not shown for events older than 14 days
Safeguards and Conflict Handling on Undo with Intervening Changes
Given changes occurred to the merged client record or its linked records after the merge When the user initiates Undo Then the system performs a preflight analysis to detect conflicts (changed fields, new/deleted links, new external references) And presents a conflict summary indicating items that cannot be safely rolled back And allows the user to cancel the Undo without changes And if the user proceeds, the system performs a safe partial rollback without data loss, preserving intervening changes and annotating preserved items And blocks the Undo if it would orphan data or violate referential integrity, showing a clear message with next steps And the outcome (full, partial, or blocked) and rationale are recorded in the Undo audit event
Role-Based Access to Audit Log and Undo
Given workspace role assignments exist When users access audit logs or attempt to undo a merge Then only Admins may execute Undo And Admins and Compliance Auditors may view full, unredacted audit details And Contributors may view redacted audit details and cannot export unredacted data And users may only access audit entries for clients within their workspace scope And unauthorized attempts return HTTP 403 and generate an access audit entry linked to the user
UI Redaction of Sensitive PII in Audit Views
Given an authorized user views audit details in the UI When sensitive PII fields are present (e.g., SSN/TIN, bank account numbers, full email addresses, phone numbers) Then the UI masks PII values (e.g., last 4 digits only, email local-part masked, phone partially masked) And redaction applies consistently across detail pages, lists, search results, and tooltips And copying masked values yields masked content And users with Compliance Auditor or Admin roles cannot disable redaction in UI but may obtain unredacted values only via authorized export And redaction does not alter stored audit data
Exportable, Filterable Audit Logs for Compliance
Given a user with export permission requests an audit log export When filters are applied (date range, client, initiator, event type) Then the system produces a downloadable export in CSV and JSON formats And the export includes required fields: event ID, timestamp, initiator, event type (merge/undo), match evidence summary, fields changed, before/after snapshots, relinked record IDs, integration call outcomes And exports respect access scope and include unredacted data only for authorized roles And large exports (up to 1,000,000 events) are processed asynchronously and the user is notified upon completion And each export is watermarked with requester, generation timestamp, and filter parameters
Retention Policy and Automated Purge
Given the organization has a retention policy configured When the retention period elapses for audit events Then the system purges expired audit events from primary storage and search indexes And retains a minimal tombstone (event ID, purge timestamp, retention policy ID) to evidence purge And default retention is 7 years, configurable between 3 and 10 years by Admins And legal hold prevents purge for events under hold until the hold is removed And purge operations run daily and produce a purge audit entry with counts purged And expired events are excluded from exports and UI after purge
Mobile Review Queue & Notifications
"As a mobile-first freelancer, I want to quickly approve duplicate merges from my phone so that I keep my client list clean without sitting at a desktop."
Description

Deliver a mobile-first review queue that lists suspected duplicates with confidence scores, key fields, and quick actions (approve, compare, dismiss). Enable one-tap merges, push notifications when new high-confidence matches are found or when manual review is needed, and offline-friendly state with queued actions that sync on reconnect. Optimize for responsiveness and accessibility, provide localized copy, and include safeguards to prevent accidental merges (e.g., hold-to-confirm).

Acceptance Criteria
Mobile Review Queue Item Display
- Given a logged-in user with suspected duplicate clients, When they open the Review Queue, Then each item displays both client display names, primary emails, a confidence score as a percentage with one decimal (e.g., 92.3%), source platform badges, and the last invoice date. - Given a listed suspected duplicate, When the item is rendered, Then Approve, Compare, and Dismiss quick-action buttons are visible and enabled if the item has no prior action. - Given no suspected duplicates exist, When the user opens the Review Queue, Then an empty state with localized guidance is displayed.
Quick Actions Behavior and State
- Given a listed suspected duplicate, When the user taps Approve, Then the item moves to an Approved state, is removed from the Unreviewed list, displays an "Approved" badge in history, and a server update is attempted. - Given a listed suspected duplicate, When the user taps Dismiss, Then the item moves to a Dismissed state, is removed from the Unreviewed list, displays a "Dismissed" badge in history, and a server update is attempted. - Given a listed suspected duplicate, When the user taps Compare, Then a side-by-side compare view opens showing key fields (names, emails, invoice counts, totals, tags, notes) for both records. - Given any quick action is in progress, Then its button shows a loading indicator, ignores further taps, and returns to enabled/disabled based on outcome. - Given a network error occurs during Approve or Dismiss, Then the UI reverts the optimistic change, an error message is shown, and the item remains actionable. - Given an item has already been actioned (Approved or Dismissed), Then quick-action buttons are disabled and the state is clearly indicated; repeated taps do not create duplicate server requests (idempotent).
Hold-to-Confirm One-Tap Merge
- Given the compare view is open, When the user presses and holds the Merge button continuously for 1 second, Then a visual countdown completes and the merge is confirmed; releasing before completion cancels the merge. - Given a successful merge, Then a single unified client record is created consolidating invoices, totals, tags, and notes; source records are archived or soft-deleted; an audit log entry includes user ID, timestamp, and source record IDs. - Given a merge is executing, Then the merge button is disabled and duplicate submissions are prevented; the operation is idempotent on the server. - Given merge preconditions are not met or a server conflict is detected, Then no data is changed and a localized error message explains what must be resolved before retrying.
Push Notifications for Matches
- Given a new suspected match with confidence ≥ 90% is detected, When notifications are permitted by the OS for the app, Then a push notification is sent within 60 seconds containing both client names, the confidence score, and a deep link directly to the compare view for that pair. - Given a new suspected match with confidence between 60% and 89.9%, When notifications are permitted, Then a push notification is sent indicating "Needs review" with a deep link to the Review Queue focused on that item; notifications are rate-limited to a maximum of 3 per hour per user. - Given OS-level notifications for the app are disabled, Then no push notifications are sent; the detection event is logged for analytics. - Given the user taps a push notification, Then the app opens to the target view; if the item no longer exists, a localized "No longer available" message is displayed with navigation back to the Review Queue.
Offline Actions Queue and Sync
- Given the device is offline, When the user taps Approve or Dismiss on a suspected duplicate, Then the action is enqueued locally with a timestamp and deterministic ID, the UI shows a "Queued" state, and no error is shown. - Given queued actions exist, Then they persist across app restarts and device reboots and are stored encrypted at rest. - Given connectivity is restored, When sync begins, Then queued actions are sent to the server in FIFO order; on success the corresponding items update/remove and the "Queued" state clears. - Given a server-side conflict is detected during sync (e.g., item already processed), Then the local queued action is skipped, a localized "Already processed" message is shown, and remaining queue items continue syncing. - Given a sync attempt fails due to server/network error, Then the action remains queued with exponential backoff retries and a manual "Retry" control is available.
Localization and Accessibility
- Given the app language is set to English (en-US) or Spanish (es-ES), Then all Review Queue, Compare, action controls, empty states, and notification texts display the correct localized strings with no placeholder keys. - Given a screen reader (VoiceOver/TalkBack) is enabled, Then each list item exposes client names, confidence score, and action buttons as separate, properly labeled focusable elements; focus order matches visual order. - Given a user cannot perform a press-and-hold gesture, Then an alternative accessible action (e.g., "Confirm merge" accessibility action) is available to complete the merge with an explicit confirmation prompt. - Given the UI renders, Then text and essential icons meet WCAG AA contrast (≥ 4.5:1), touch targets are ≥ 44x44 points, and Dynamic Type/Font Scaling does not clip critical information (ellipsis allowed) with access to full details. - Given localization is switched between en-US and es-ES, Then the app updates strings without requiring restart and maintains state on the current screen.
Mobile Performance and Responsiveness
- Given a mid-tier device (e.g., iPhone 12 / Pixel 5) on a 10 Mbps, 150 ms RTT connection, When opening the Review Queue cold, Then the first 10 items render in ≤ 1.2 s P50 and ≤ 2.0 s P95. - Given the Review Queue is visible with 200 items available, When the user scrolls continuously for 10 seconds, Then jank frames are < 2% and average frame rate is ≥ 55 fps. - Given the user taps Approve or Dismiss, Then the UI reflects the state change within 150 ms and server-confirmed status within 2.0 s P95 (or "Queued" within 100 ms if offline). - Given the user taps Compare on an item, Then the compare view appears within ≤ 500 ms P95 if data is cached, or ≤ 1.2 s P95 with a network fetch. - Given a 10-minute session on the Review Queue, Then the app remains crash-free and the feature’s memory footprint remains ≤ 200 MB P95 on both iOS and Android.

Project Auto‑Link

Reads invoice line items, tags, and metadata to attach each payment to the right project automatically. Learns from your edits to build rules, giving you real‑time project P&L and cleaner reimburseables vs. true expense separation.

Requirements

Invoice Line Item Parsing
"As a freelancer, I want TaxTidy to reliably read detailed line items from my invoices and receipts so that projects can be auto-linked without manual data entry."
Description

Extracts and normalizes invoice, receipt, and bank memo details into structured line items that include client, project identifiers (codes, tags, PO numbers), service descriptions, quantities, rates, dates, and currency. Supports PDFs, email attachments, and receipt photos via OCR with tolerance to noisy scans, deduplication against existing documents, and cross-checks against bank feed totals. Persists source-document links, ensures idempotent re-ingestion, and emits validation errors for incomplete or ambiguous documents, enabling downstream auto-linking to operate reliably across heterogeneous inputs in a mobile-first workflow.

Acceptance Criteria
PDF Invoice Line‑Item Extraction and Normalization
Given a single PDF invoice with 3 line items containing client "Acme Co.", project code "PRJ-042", PO "PO-7788", service descriptions, quantities, rates, service dates, and USD currency, When the file is ingested, Then the system produces 3 structured line items with fields client, project_code, po_number, description, quantity, rate, service_date, and currency populated and normalized to schema types. And Then the computed line totals (quantity × rate) and invoice subtotal/tax/total match the extracted monetary fields within ±$0.01 rounding. And Then each line item includes invoice_id and a source_document_id linking back to the PDF.
Noisy Receipt Photo OCR to Structured Line Item
Given a smartphone photo of a tilted, low‑contrast receipt showing merchant "Blue Bottle", purchase date "2025-03-14", total "€18.50" and VAT line "VAT €3.08", When uploaded via the mobile app, Then OCR corrects orientation and extracts merchant, date, currency "EUR", tax amount, and total into a single normalized line item. And Then if client or project identifiers are not present on the receipt, a validation error with code "VAL_MISSING_ID" is emitted referencing fields ["client","project"]. And Then the parsed total equals the sum of itemized amounts plus tax within ±€0.01 when itemization is present, otherwise equals the receipt total.
Email Attachment Ingestion with Source Link Persistence
Given an email forwarded to the user's ingestion address with a single PDF invoice attachment, When processing completes, Then each produced line item includes a source_document_link that opens the exact attached PDF via API and mobile UI. And Then the source_document_link remains associated to the line items after user sign‑out/sign‑in and after re‑forwarding the same email, without creating duplicate line items.
Idempotent Re‑ingestion and Cross‑Channel Deduplication
Given the same invoice document is ingested multiple times via PDF upload, email forward, and mobile camera scan, When each attempt is processed, Then the system creates line items only once and subsequent attempts update source linkage history without duplicating items. And Then a document content fingerprint is stored and used to prevent duplicates across channels and sessions. And Then re‑ingesting an updated version of the same document (changed pixels but identical semantic content) does not create duplicates and preserves the original line item IDs.
Bank Feed Amount Cross‑Check
Given a parsed invoice with total 1250.00 USD and a bank feed transaction in USD whose memo contains the invoice number, When cross‑checking runs, Then the system verifies the bank transaction amount equals the invoice total within ±$0.01 and marks the line items as bank‑verified. And Then if no matching bank feed transaction is found or the amount mismatches, a validation error "VAL_BANK_MISMATCH" is emitted and the line items are not marked bank‑verified.
Ambiguous or Incomplete Metadata Validation
Given a document where the client name maps to multiple saved clients or the project identifier is missing or conflicting, When parsed, Then the system emits a validation error "VAL_AMBIGUOUS_METADATA" listing the conflicting fields and candidate values. And Then no client_id or project_id is committed to line items until the ambiguity is resolved by the user, and the items are flagged as "needs_review".
Bank Memo Normalization to Line Items
Given a bank feed transaction with memo "ACME STUDIO — PRJ-042 — 3h Design @ $120/h — Inv 7788", When ingested, Then the system parses client "ACME STUDIO", project_code "PRJ-042", description "Design", quantity 3, rate 120, currency "USD", and invoice reference "7788" into a single structured line item. And Then the line item amount equals quantity × rate and matches the bank transaction amount within ±$0.01.
Project Auto-Matching Engine
"As a consultant, I want payments and expenses to auto-attach to the correct project with a clear confidence score so that I can trust my project financials at a glance."
Description

Automatically links income and expense transactions to the correct project using a hybrid of deterministic rules (exact tag, PO, client mapping) and probabilistic signals (fuzzy text matches, date proximity, amount patterns). Handles partial payments, multi-project splits, and multi-currency normalization. Produces an explainable match with a confidence score, falls back to ranked suggestions when below threshold, and updates links in real time as new data arrives. Designed for low latency on mobile, idempotent reprocessing, and safe retries to ensure consistent project assignment across all sources.

Acceptance Criteria
Exact Deterministic Mapping Auto-Link
Given a transaction whose tag, PO number, or client is mapped 1:1 to a project When the engine processes the transaction Then it links the transaction to that project with confidence = 1.0 And it records an explanation citing the exact matched field and rule identifier And no alternative suggestions are generated And the operation is completed with p95 latency ≤ 2 seconds on mobile intake
High-Confidence Probabilistic Match with Explainability
Given a transaction with fuzzy text, date proximity, and amount pattern signals to a single project When the engine computes a confidence score ≥ the configured threshold (default 0.85) Then it auto-links the transaction to that project And it records an explanation listing the top 3 contributing signals with weights And the confidence score is persisted and retrievable via API/UI
Ranked Suggestions and Learning from User Edits
Given a transaction whose computed confidence is below the threshold When the engine generates suggestions Then it returns up to 5 project suggestions ranked by confidence, including an Unassigned option And selecting a suggestion or manually assigning creates an auditable user edit And the engine converts consistent user edits into a deterministic rule after ≥ 3 identical confirmations, effective for future matches
Partial Payment Allocation for Invoices
Given an invoice for a project that receives a partial payment When the payment is processed Then the payment amount is linked to the project and the remaining balance is tracked And when subsequent payments arrive, the outstanding balance is reduced and links updated without duplication And for invoices spanning multiple projects, partial payments are allocated proportionally to line‑item project amounts
Multi-Project Transaction Splitting
Given a transaction covering multiple projects (e.g., expense with multiple tagged line items) When the engine processes it Then it creates per-project split links whose signed amounts sum to the original amount within ±$0.01 rounding tolerance And each split has its own explanation and confidence And the UI/API exposes the splits as child links of the source transaction
Multi-Currency Normalization for Project P&L
Given a transaction in currency C different from the project base currency B When the engine normalizes amounts Then it uses the transaction-date FX rate from the configured provider to convert to B And it records the rate, provider, and timestamp in the explanation And normalized amounts are rounded to 2 decimals and used in project P&L and splits
Idempotent Reprocessing, Safe Retries, and Real-Time Updates
Given the same input data is reprocessed or a retry occurs after a transient failure When the engine runs again Then the resulting project links and confidence scores are identical to the prior successful run And no duplicate links or suggestions are created And transient failures are retried up to 5 times with exponential backoff and jitter, with a single correlation ID in the audit log And when new related data arrives (e.g., a matching invoice or payment), affected links are recalculated and the UI reflects updates within 5 seconds
Reimbursable vs True Expense Classification
"As a creative, I want reimbursable costs separated from true expenses so that both my taxes and project profitability stay accurate."
Description

Differentiates reimbursable, client-billable costs from true business expenses at the line-item level and supports splitting mixed transactions. Uses tags, memo patterns, vendor history, and linked invoice context to auto-flag reimbursables, with clear UI indicators and override options. Ensures reimbursements are excluded from taxable income and prevents double counting by coordinating with revenue recognition and export mappings. Propagates the classification to project P&L, accounting exports, and the IRS-ready packet to maintain accurate profitability and tax treatment.

Acceptance Criteria
Tag‑Based Reimbursable Classification
Given a line item includes the tag "client-billable" or "reimbursable" When Project Auto‑Link processes the source (invoice, bank transaction, or receipt) Then the line item is classified as Reimbursable and displays a "Reimbursable" indicator on the line And the classification is applied within 5 seconds of ingestion And the classification reason lists the triggering tag
Memo/Vendor Pattern Classification with Linked Context
Given a line item memo matches a saved reimbursable pattern (e.g., "reimbursable", "bill to client") or the vendor is marked Pass‑Through And the source document is linked to a project/client When the item is processed Then the line item is classified as Reimbursable And the classification reason cites the matched pattern or vendor rule and the linked project name
Mixed Transaction Splitting and Independent Classification
Given a bank transaction includes multiple expense purposes in one charge When the user splits the transaction into separate line items Then each split line supports independent classification as Reimbursable or True Expense And the sum of split amounts equals the original transaction total within $0.01 tolerance And each split line can be exported and mapped independently to the accounting system
Clear UI Indicators and Override Controls
Given a user views transaction or invoice details When a line item is auto‑classified Then the line shows a visible pill "Reimbursable" or "True Expense" and a "Why?" tooltip with signals used And the user can change the classification via a toggle or dropdown on the line And the override persists, updates totals immediately, and is visible on refresh and in list views
Rule Learning from Repeated Overrides
Given the user manually reclassifies two or more similar line items (same vendor and memo pattern) within 30 days When the second override is saved Then the system prompts to create a rule that auto‑classifies future matches as Reimbursable or True Expense And upon user acceptance, subsequent matching items are auto‑classified with reason "Learned Rule" And the user can edit or disable the rule from Classification Rules settings
No Double Counting in Revenue and Export Mappings
Given a reimbursable expense is linked to a client invoice and a payment is recorded When exporting to the connected accounting system Then the reimbursable amount is not recognized as taxable income nor as deductible expense (net zero effect) And the export posts to configured clearing or contra accounts without duplicate entries And the export job includes a validation note showing zero net impact for reimbursables
Propagation to Project P&L and IRS‑Ready Packet
Given line items are classified as Reimbursable or True Expense When viewing a project's P&L Then reimbursables appear in a separate section and are excluded from expense totals used for project margin And when generating the IRS‑ready packet, reimbursables are excluded from deductible totals and listed in an "Excluded Reimbursables" appendix with amounts and references And totals in the packet reconcile to the accounting export within $0.01
Learning-from-Edits Rules
"As a busy freelancer, I want the system to learn from my corrections so that future transactions are auto-linked according to my preferences."
Description

Captures user corrections to auto-linked transactions and converts them into safe, versioned rules that adapt future matches. Supports conditions across fields (contains/equals/regex on memo, vendor, tags, amounts, client, PO) with precedence, scoping per user, and rollback. Simulates impact before activation and records rule provenance for explainability. Syncs across devices, respects privacy boundaries, and provides gradual rollout with confidence thresholds to reduce false positives while accelerating automation over time.

Acceptance Criteria
Create versioned rule from a user correction
Given an auto-linked transaction is corrected by the user to a different project, When the user selects "Learn from this edit," Then a draft rule is generated with pre-populated conditions from the corrected transaction's memo, vendor, tags, amount, client, and PO fields. Given a draft rule, When the user activates it, Then the system saves it as version v1 with scope "User" and records created_by, created_at, and source_transaction_id. Given an existing rule, When the user edits and re-saves it, Then a new version v(n+1) is created, the prior version is retained and marked "Superseded," and a diff is stored. Given the rule is Active, When a new matching transaction is ingested, Then it is auto-linked to the target project within 5 seconds and labeled with the rule id and version. Given the rule is Active, Then no historical transactions are modified unless the user explicitly selects "Apply to existing" during activation.
Support multi-field condition operators
Given the rule editor, When the user configures memo contains "retainer", Then matching is case-insensitive and returns true for "Monthly Retainer - April" and false for "Monthly Retain". Given the rule editor, When the user configures vendor equals "Acme LLC", Then only transactions with vendor exactly "Acme LLC" match; "Acme, LLC" and "ACME" do not. Given the rule editor, When the user configures memo regex "^(Design|Editing) Invoice #[0-9]+$", Then transactions with matching memos evaluate true; the regex is validated with a 2 KB pattern limit and 100 ms evaluation timeout. Given the rule editor, When the user configures amount equals 1500.00 and client equals "Contoso", Then only transactions matching both conditions are selected. Given the rule editor, When the user configures tag contains "reimbursable", Then transactions with that tag (any case) match even if multiple tags are present.
Rule precedence and conflict resolution
Given multiple rules match a transaction, When priorities are evaluated, Then the rule with the highest explicit priority applies; if equal, the rule with greater specificity (more conditions) applies; if still equal, the most recently activated applies. Given a lower-priority rule and a higher-priority rule match the same transaction, Then only the higher-priority rule executes and the decision is logged with the suppressed rule ids. Given a transaction was linked by precedence, When the user views "Why was this linked?", Then the UI shows the winning rule, priority, matched conditions, and any competing rules that were bypassed.
Simulate impact before activation
Given a draft rule, When the user clicks Simulate, Then the system computes and displays the number of historical matches over the last 12 months, the affected projects, and 20 sample transactions, without modifying any data. Given a draft rule, When simulation results are shown, Then the counts equal the actual number of transactions that would be affected if the rule were activated immediately. Given an account with ≤10,000 transactions, When the user runs a simulation, Then results are returned within 3 seconds at the 95th percentile.
Rollback rule to a prior version
Given a rule at version v3 is Active, When the user selects Rollback to v2, Then v2 becomes Active, v3 is marked "Rolled Back," and all future matches use v2. Given a rollback, When the user does not select "Reapply to existing," Then no previously linked transactions are changed; when selected, Then affected historical transactions are re-evaluated and updated. Given a rollback occurs, Then an audit event is recorded with rule id, from_version, to_version, actor, timestamp, and reason.
Rule provenance and explainability
Given any rule, Then the system stores provenance fields: created_by, created_at, source_transaction_id, activation_method (auto/manual), and change_history. Given a transaction auto-linked by a rule, When the user opens the explanation panel, Then it displays the rule id, version, matched conditions, evaluation time, and a link to the originating correction or edit. Given export is requested for rules activity within a date range, Then a CSV containing rule ids, versions, actions (create/edit/activate/rollback/apply), actors, and timestamps is generated.
Gradual rollout with confidence thresholds
Given a rule with confidence score ≥ the user's threshold (default 0.80), When a matching transaction arrives, Then it is auto-applied; otherwise it is sent to a Review queue requiring user approval. Given 5 consecutive approvals with no denials for a rule below threshold, Then the system increases the rule's confidence by 0.05 (max 0.99) and notifies the user. Given a rule generates a denial in the Review queue, Then the confidence decreases by 0.10 (min 0), and the system suggests editing or pausing the rule. Given weekly evaluation, When the observed false-positive rate for auto-applied rules exceeds 2%, Then the system lowers the default threshold by 0.05 and prompts the user to review.
Rule Builder & Conflict Resolution UI
"As a mobile-first user, I want an easy way to review and adjust the auto-linking rules so that I can resolve edge cases without digging into complex settings."
Description

Provides a mobile-first interface to view, create, edit, and prioritize auto-linking rules with inline previews of affected transactions. Detects conflicts, shows which rule would apply and why, and allows users to test, bulk-apply, and undo changes safely. Includes search, filtering, and draft/publish states to minimize accidental regressions and supports accessibility and responsive layouts. Integrates tightly with the matching engine for real-time feedback and transparent decision making.

Acceptance Criteria
Create Rule with Inline Preview on Mobile
Given a user on a 375px-wide mobile device viewing a transaction, when they tap “Create rule,” then a bottom-sheet rule form opens covering at least 90% viewport height and a preview panel shows a paginated list of ≥10 predicted affected transactions. Given the user edits any rule field (condition or action), when the field loses focus or debounced input fires, then the preview list and count badge update within 500 ms and reflect the backend match results. Given required fields (rule name, at least one condition, action target project) are incomplete, then the Publish button is disabled and an accessible helper text states which fields are missing. Given the condition “invoice line item contains ‘retainer’” and action “link to project Acme Site,” then the preview contains only transactions that match and each item shows the rule name, version, and the specific condition matched. Given the user taps “Save as draft,” then the rule persists with state=draft, does not alter any transactions, and appears in the rule list with a “Draft” chip within 1 second.
Edit Rule with Real-time Affected Transactions Update
Given an existing published rule is opened for edit, when a condition value is changed, then the preview recalculates within 700 ms and displays delta counters for Added and Removed matches. Given the edit would change outcomes for ≥1 transactions, then a non-blocking warning banner appears summarizing the exact count before saving, and the Save button label includes the count (e.g., “Save (42 changes)”). When the user saves, then a new immutable rule version is created, the previous version is retained, and the audit log records user, timestamp, and a diff of changed fields. After save, a snackbar with “Undo” is visible for 10 seconds; tapping Undo restores the prior version and reverts any applied previewed changes. If the user cancels edits, then no version is created and the preview reverts to the last saved state immediately.
Conflict Detection and Priority Ordering
Given at least two rules match the same transaction, when the user opens the Conflict view, then the UI lists all matching rules ordered by priority with the winning rule highlighted and a textual “why” explanation including priority and matched conditions. When the user reorders rules via drag-and-drop, then the predicted winner recalculates and the preview updates within 700 ms to reflect the new priority. Given the user saves the new priority order, then the order persists, and no historical transactions change unless the user explicitly runs Bulk Apply. Given no conflicts exist for current filters, then an empty state is displayed with guidance to adjust filters and a link to the full rules list. Conflicts list supports search by rule name and project; filtering reduces the set using AND logic and updates counts in under 400 ms for up to 200 rules.
Draft vs Publish Workflow with Safe Rollout
Draft rules are non-executing: they never affect live matching or historical data until published. When the user taps Publish, then a confirmation modal summarizes scope: project target, matched future transactions, and count of historical matches if “Apply to past” is enabled. If “Apply to past” is selected, then a backfill process starts with a progress indicator updating at least every 2 seconds, supports Cancel, and on cancel leaves already-processed transactions updated. Upon publish, the rule appears in the Published tab within 1 second and is inserted at the default priority position (configurable: top or bottom) reflected in the list immediately. All publish and backfill actions are recorded in the audit log with user, timestamp, counts, and rule version.
Bulk Apply and Undo Changes
Given a preview shows ≥1 pending changes, when the user taps Bulk Apply, then the system applies the changes idempotently, skipping duplicates and reporting skipped counts in the result summary. Bulk apply of up to 1,000 transactions completes within 30 seconds or shows a live progress indicator that updates every 2 seconds until completion. After completion, a persistent banner offers Undo for 10 minutes; Undo reverts all changes in the batch, and partial failures (if any) list affected transaction IDs. Each bulk operation writes an audit record containing batch ID, rule version, user, counts applied/failed/skipped, and duration. If network loss occurs during apply, then the operation resumes on reconnect or is safely resumable by re-triggering, guaranteed by the batch IDempotency key.
Search and Filter Rules and Transactions
Given the user enters a query, when searching by rule name, condition keyword, project name, or author, then results return within 400 ms for datasets up to 500 rules and display total count. Filters include state (Draft/Published), has conflicts (Yes/No), project, and last modified range; combining filters applies AND logic and updates the list and count consistently. Clearing search restores the prior unfiltered list and preserves the previous scroll position. No-results state displays a clear message and offers one-click actions to remove the last-applied filter or reset all. Search and filter changes announce updated result counts via an ARIA live region for screen readers.
Accessibility and Responsive Layout Compliance
All tap targets are at least 44x44 pt on mobile; primary CTAs are reachable with one hand in bottom-sheet layouts. The UI meets WCAG 2.1 AA: color contrast ≥ 4.5:1, full keyboard navigation, visible focus states, proper labels, and roles on interactive and dynamic elements (including the preview list as a polite live region). Screen readers announce preview updates and conflict-winning rule changes via ARIA live regions without stealing focus. Responsive behavior supports widths 320–1440 px: ≤414 px uses a bottom sheet for the form; ≥768 px shows split-pane with form and live preview side-by-side; components reflow without horizontal scroll. All error messages are programmatically associated to inputs and are read by screen readers when they appear.
Real-Time Project P&L Sync
"As a project owner, I want real-time P&L for each project so that I can make timely decisions and bill clients accurately."
Description

Continuously updates each project’s revenue, costs, and margin as transactions are auto-linked or reclassified, with instant recalculation of reimbursables versus true expenses. Supports time filters, multi-currency rollups, and cached summaries for fast mobile loads. Surfaces anomalies (e.g., unlinked deposits, unusually high costs) and provides drill-through to the underlying transactions and rules used, ensuring actionable, current P&L for day-to-day decisions and accurate billing.

Acceptance Criteria
Instant P&L Recalc on Auto‑Link
Given an incoming transaction is auto‑linked to Project A and was not previously included in Project A When the link is applied Then Project A revenue, cost totals, reimbursables vs true expense subtotals, and margin % update within 2 seconds and display without page reload And Then the transaction appears in the appropriate drill‑through list with the applied rule visible
Real‑Time Reclassification Impact
Given a user edits a transaction linked to Project B to change its category, reimbursable flag, or project assignment When the edit is saved Then Project B P&L totals and margin % recalculate within 2 seconds and the UI reflects the change immediately And Then any cached summary for Project B is invalidated and refreshed within 2 seconds
Accurate Time‑Filtered P&L
Given Project C has transactions across multiple months and time zones When the user applies a date range filter (start and end dates inclusive) based on the user’s profile timezone Then only transactions whose transaction date falls within the range are included in revenue, costs, margin, and reimbursables subtotals And Then preset filters (This Month, Last Month, YTD) equal the totals of their equivalent custom ranges And Then exported P&L for the same filter matches on‑screen totals exactly
Multi‑Currency Rollup to Base Currency
Given Project D contains transactions in multiple currencies When viewing P&L in base currency USD Then each foreign transaction is converted using the FX rate for its transaction date from the configured provider, totals are rounded to 2 decimals, and cumulative rounding error per line item grouping does not exceed $0.01 And Then a tooltip or detail view shows the rate and date used for any converted amount And Then changing the base currency updates all totals within 3 seconds
Fast Mobile Summary via Caching
Given a user opens Project E P&L summary on a mobile device over a typical 4G connection after initial sync When the P&L view loads Then the summary (revenue, costs, margin, reimbursables subtotals) renders from cache in under 500 ms And Then a freshness indicator shows the last updated timestamp (or “< 15 min ago”) and a pull‑to‑refresh is available And Then background refresh completes within 2 seconds and updates the UI if newer data exists without blocking interaction
Anomaly Detection and Surfacing
Given the system evaluates transactions and P&L trends hourly When anomalies are present Then unlinked deposit anomalies are created for deposits ≥ $50 not linked to any project within 24 hours of ingestion, with a banner showing count and a drill‑through to review and link And Then unusually high cost anomalies are created when a project’s daily cost for a category exceeds 3× its 30‑day moving median and the absolute increase is ≥ $100, with a banner and drill‑through And Then marking an anomaly as reviewed hides it for 7 days unless its state changes; resolving the root cause (e.g., linking the deposit) removes the anomaly immediately and updates P&L within 2 seconds
Drill‑Through to Transactions and Rules
Given a user taps any P&L line item for Project F When the drill‑through opens Then it lists all and only the transactions contributing to that line for the active time filter, sorted by date descending, with pagination or infinite scroll And Then each transaction displays the rule or heuristic that linked it, with a link to view rule details And Then navigating back restores the prior P&L scroll position and filter state
Decision Audit Trail & Exportable Annotations
"As a user preparing taxes, I want an audit trail of auto-link decisions so that I can justify categorizations if I’m audited."
Description

Maintains an immutable history of how each auto-link decision was made, including rule IDs, confidence scores, and key fields considered. Allows users to add notes and attach supporting documents, and exposes this metadata in dashboards and exports (CSV/PDF) for the IRS-ready packet. Supports re-evaluation when rules change, with clear before/after diffs and safeguards for locked periods to protect filed records, enabling trust, compliance, and rapid issue resolution.

Acceptance Criteria
Immutable Audit Log Creation on Auto-Link
Given an invoice payment is auto-linked to a project by the system When the decision is saved Then an audit record is created containing decision_id, invoice_id, project_id, rule_ids[], model_version, confidence (0.00–1.00), considered_fields[] (names and values or hashes), actor=system, and timestamp in UTC ISO 8601 And the audit record includes content_hash and previous_hash linking to the prior audit entry for the same decision chain And any attempt to modify a stored field via UI or API returns 403 and creates a new audit entry of type="tamper_attempt" without altering the original record And retrieving the audit trail and verifying the hash chain detects no integrity breaks
User Notes and Supporting Document Attachments
Given a user with Editor role views an auto-link decision When they add a note (<= 2,000 characters) and optional attachments (PDF/JPG/PNG; up to 10 files; each <= 10 MB) Then the note and attachments are stored as append-only entries with user_id, timestamp (UTC ISO 8601), and content_hash And all attachments pass antivirus scanning; infected or unsupported files are rejected with an error And thumbnails/previews render for images and PDFs within 2 seconds for files <= 5 MB And users cannot delete or overwrite existing notes/attachments; they may add a new note referencing the prior entry And Viewer role can read notes/attachments but cannot add them; access is enforced at the project level
Dashboard Display of Decision Metadata
Given a user opens the Project Auto-Link detail view for a transaction When the panel loads Then the UI displays rule_ids, model_version, confidence, considered_fields (names and values or redacted hashes for PII), and any user notes/attachments And the list view supports filtering by rule_id, confidence range, has_notes (true/false), and last_updated date range And sorting by confidence and last_updated is available And for a dataset of 5,000 decisions, initial load time is <= 2 seconds and filter response time is <= 1 second at p95
CSV/PDF Export with Audit Metadata and Annotations
Given a user initiates an export for a project or date range When CSV export completes Then the CSV includes columns: decision_id, invoice_id, project_id, rule_ids, model_version, confidence, considered_fields, actor, timestamp, locked_period_flag, and notes_count And when PDF export completes Then the PDF packet includes a summary per decision with the same fields and an appendix that lists user notes and embeds or links attachments with filenames and SHA-256 hashes And exports are deterministic: identical inputs produce byte-identical files (excluding export_id and timestamp headers) And a batch export of 10,000 decisions completes within 60 seconds at p95 and streams partial results within 5 seconds
Rule Change Re-evaluation with Before/After Diffs
Given a rule set change is published or a user triggers re-evaluation When the system recomputes project links for affected decisions Then a diff is generated per decision showing before: {project_id_old, confidence_old, rule_ids_old} and after: {project_id_new, confidence_new, rule_ids_new} And the user can batch Accept or Reject proposed changes, with an optional note required on Reject And on Accept, a new audit entry is appended with type="re_evaluation", linking to the prior decision via previous_hash and recording approver user_id and timestamp And re-evaluation does not run on decisions within locked periods; those are marked skipped with reason="locked_period"
Locked Period Safeguards and Behavior
Given an accounting period is locked When a user attempts to change a decision affecting that period or re-evaluation proposes a change Then the system prevents applying the change, returns 423 Locked, and records an audit entry of type="blocked_change" with reason="locked_period" And exports for locked periods are frozen to the state at lock time and include a header noting the lock period and lock timestamp And users may still add notes/attachments post-lock; these are tagged post_filing=true and included in exports under a Post-Filing Notes section without altering financial calculations

1099 Watchlist

Live, client‑by‑client totals with threshold alerts so you know who’s likely to issue you a 1099‑NEC. At tax time, reconcile received forms against your tracked totals to spot over/under‑reporting in seconds.

Requirements

Live Payer Totals Dashboard
"As a freelancer, I want to see live per-client income totals by tax year so that I know which clients are likely to send me a 1099‑NEC."
Description

Real-time aggregation of income by payer across invoices, bank feeds, and payment platforms, producing a per-client total for the selected tax year. The dashboard surfaces each payer’s progress toward the 1099‑NEC threshold with visual indicators and supports mobile-first card and list views, search, and filters (tax year, business profile, tags). Totals update automatically as new transactions and invoices are ingested, and reflect currency normalization, refunds/credits netting, and manual adjustments with audit notes. Integrates with existing data ingestion and categorization pipelines, leveraging TaxTidy’s income classifications to ensure only eligible payments are counted. Exposes an API for downstream reconciliation and export.

Acceptance Criteria
Real-Time Aggregation and Auto-Refresh of Payer Totals
- Given a tax year is selected and eligible income exists across invoices, bank feeds, and payment platforms, When the dashboard loads, Then per-payer totals equal the sum of all 1099-eligible transactions for that year with no duplicates across sources. - Given new eligible income is ingested and final-categorized, When ingestion completes, Then the affected payer’s total updates on the dashboard within 60 seconds without manual refresh. - Given a payer appears across multiple sources, When totals are computed, Then duplicates are eliminated using unique transaction identifiers and linkage rules, and a dedup count is available in diagnostics. - Given non-eligible classifications or internal transfers exist, When computing totals, Then exclude them according to TaxTidy’s income classifications. - Then initial dashboard render completes within P95 ≤ 2s and P99 ≤ 4s for up to 500 payers.
1099-NEC Threshold Progress Indicators and Alerts
- Given the U.S. 1099-NEC threshold of $600 for the selected tax year and profile, When computing per-payer totals, Then display a progress indicator with states: <80% = Neutral, 80–99% = Approaching, ≥100% = Reached. - When a payer’s total first crosses 80% of the threshold from below, Then show a non-blocking in-app alert and badge the payer card; if notifications are enabled, send a push notification within 60 seconds. - When refunds/credits reduce a payer below a threshold state, Then update the state within 60 seconds and clear any prior “Reached” badges. - Indicators provide numeric totals and percentages, meet WCAG 2.1 AA contrast, and include text labels so color is not the sole indicator.
Mobile Card and List Views
- On viewports ≤ 414px width, default to Card view showing payer name, total (base currency), progress bar, and transaction count; user can toggle to List view via a visible control. - The selected view persists per user and profile across sessions until changed. - Infinite scroll/pagination loads the next page within 500 ms (P95) after reaching 90% scroll depth, with skeleton placeholders displayed during fetch. - Pull-to-refresh triggers a data refresh and updates visible totals and states on completion. - All interactive elements are reachable via keyboard and screen reader with correct roles, names, and focus order.
Search and Filters for Tax Year, Business Profile, and Tags
- Users can filter by tax year (required), business profile (required when multiple exist), and tags (multi-select); filters combine with a free-text search. - Search matches payer name, email, EIN, and platform handle, is case- and accent-insensitive, and returns results within 300 ms (P95) after submit on a 4G mobile connection. - Applied filters are visible as removable chips; Clear All resets filters and search to defaults. - Result counts, totals, and progress indicators reflect the filtered set exactly and update in place without a full page reload.
Currency Normalization and Refund/Credit Netting
- For multi-currency transactions, amounts are normalized to the business profile’s base currency using the FX rate on the transaction date (fallback to end-of-month average if daily is unavailable), and the applied rate and source are stored in the audit log. - Refunds and credits posted within the selected tax year reduce the payer’s total for that year; negative amounts are handled correctly and cannot invert sign due to double application. - Display of each payer total includes a breakdown tooltip/modal showing gross, refunds/credits, manual adjustments, and net total, with all components summing exactly to the displayed total to the cent (2 decimal places).
Manual Adjustments with Audit Notes
- Users with Accountant or Admin roles can add positive or negative manual adjustments per payer per tax year; adjustments require an audit note of at least 10 characters. - Adjustments are recorded with user, timestamp, amount, note, and version; edits create a new version and preserve history; deletions are soft (revertible) only. - Totals reflect a new or edited adjustment immediately in the UI (≤1s after save) and adjustments are flagged in exports and API responses. - The audit trail view lists all adjustments chronologically and supports filtering by payer and tax year.
Reconciliation and Export API
- Provide authenticated GET endpoint /api/v1/payers/totals supporting parameters: tax_year (required), profile_id, tags[], q, view=summary|detail, page, per_page; responses are JSON and include payer identifiers, total, base currency, threshold_state, transaction_count, gross, refunds, adjustments. - API totals match the dashboard for identical filters within 0.01 in base currency and include an ETag for caching; support 200, 401, 403, 422, and 429 status codes. - Rate limit is enforced at ≥60 requests/min per token with standard Retry-After headers; summary view responds in ≤500 ms P95 and detail view in ≤1,000 ms P95 for up to 10,000 transactions via pagination.
Threshold Alerts & Configuration
"As a mobile‑first freelancer, I want alerts when a client approaches or crosses the 1099‑NEC threshold so that I can collect a W‑9 and verify my records."
Description

Configurable alert system that monitors each payer’s cumulative nonemployee compensation against a default $600 IRS threshold, with customizable thresholds per payer and per year. Supports proactive alert triggers at configurable checkpoints (e.g., 75%, 100%, 120%), delivered via in-app notifications, email, and mobile push, with per-user preferences, quiet hours, and snooze/dismiss controls. Alerts deep-link to the payer detail view showing recent contributing transactions. Admin controls ensure rate-limiting and prevent duplicate alerts. Integrates with TaxTidy’s notification service and permission model; stores alert history for auditability.

Acceptance Criteria
Default $600 Threshold Checkpoint Alerts
Given a payer without a custom threshold for tax year Y and a cumulative nonemployee compensation total below 75% of $600 When a new contributing transaction is ingested that brings the cumulative total to or above 75% of $600 Then the system generates exactly one 75% checkpoint alert for that payer and tax year within 5 minutes and includes payer, tax year, checkpoint (75%), threshold ($600), and current cumulative total in the payload Given a payer without a custom threshold for tax year Y and a cumulative total below 100% of $600 When a new contributing transaction is ingested that brings the cumulative total to or above 100% of $600 Then the system generates exactly one 100% checkpoint alert for that payer and tax year within 5 minutes and includes the same metadata Given a payer without a custom threshold for tax year Y and a cumulative total below 120% of $600 When a new contributing transaction is ingested that brings the cumulative total to or above 120% of $600 Then the system generates exactly one 120% checkpoint alert for that payer and tax year within 5 minutes and includes the same metadata
Custom Threshold Overrides per Payer-Year
Given a user sets a custom threshold T for payer P for tax year Y where no custom threshold existed When the setting is saved Then subsequent checkpoint calculations for payer P in year Y use T and checkpoints at 75%, 100%, and 120% of T Given a cumulative total C for payer P in year Y and no prior alerts for one or more checkpoints based on new threshold T When T is saved and C is already at or above any of those checkpoint amounts Then the system emits exactly one alert per newly satisfied checkpoint within 5 minutes and marks them as "backfilled" Given an existing custom threshold T1 with alerts already sent for certain checkpoints When the threshold is changed to T2 Then the system does not resend alerts for checkpoints already sent under T1, and only evaluates and sends alerts for checkpoints not yet satisfied under T2
Multi-Channel Delivery, Preferences, Quiet Hours, Snooze/Dismiss
Given a user has in-app and email enabled and push disabled in notification preferences When a checkpoint alert is generated for that user Then the alert is delivered via in-app and email only and no push notification is sent Given a user has quiet hours configured from 22:00 to 07:00 in their timezone When an alert is generated at 23:30 local time Then in-app notification is created immediately and email/push deliveries are deferred until quiet hours end and are dispatched within 10 minutes after 07:00 Given a user snoozes alerts for payer P in tax year Y for 7 days When any new alert for payer P in year Y is generated during the snooze window Then the system suppresses outbound delivery for all channels, records the alert event with status "snoozed", and does not notify the user Given a user dismisses a specific checkpoint alert (e.g., 100%) for payer P in year Y When subsequent transactions occur without crossing a new checkpoint for that payer and year Then the system does not re-notify for the dismissed checkpoint
Alert Deep-Link to Payer Detail with Contributing Transactions
Given an alert exists for payer P in tax year Y with a deep link When the user opens the alert from any channel Then the app navigates directly to the payer P detail view scoped to tax year Y Given the payer detail view is opened from an alert When the view loads Then it displays a "Recent Contributing Transactions" section listing the last up to 10 transactions that contributed to crossing the checkpoint, including date, amount, and source for each, and the sum of listed amounts equals the delta that caused the checkpoint crossing Given the alert shows a cumulative total T and threshold Th When the payer detail view is opened Then the view displays the current cumulative total and threshold and, if they differ from the alert values due to subsequent activity, shows a badge "Updated since alert"
Admin Rate Limiting and Duplicate Prevention
Given an admin-configured rate limit of one alert per payer–checkpoint–tax year per user per 24 hours per channel When multiple transactions or processes attempt to send the same alert within the rate-limit window Then the system sends at most one delivery per channel and suppresses additional deliveries as duplicates while recording suppression in alert history Given the system processes a burst of transactions that cross multiple checkpoints for the same payer and year (e.g., from below 75% to above 120%) When evaluating alerts Then the system generates at most one alert event per unique checkpoint (75%, 100%, 120%) and ensures idempotency using a deduplication key of payerId+taxYear+checkpoint Given an alert for a specific payer–checkpoint–tax year was already sent When the same event is replayed or retried Then no additional alert is delivered for that deduplication key
Alert History and Audit Log
Given any alert lifecycle event occurs (generated, delivered, deferred, suppressed, snoozed, dismissed, failed, retried) When the event is processed Then the system writes an immutable history record containing alertId, timestamp (UTC), userId, payerId, taxYear, checkpoint, threshold, cumulative total, channel, outcome status, and actor (system/user) and the record is queryable via audit API Given a user opens their alert history for payer P in tax year Y When querying the audit API Then the results include all alert events for that payer and year, ordered by timestamp descending, and exclude events for payers the user is not permitted to view Given an admin queries alert history for compliance review When filtering by date range, payer, checkpoint, channel, or outcome Then the API returns matching records within 2 seconds for up to 10,000 events
Notification Service Integration and Permissions
Given the notification service is available When an alert is generated Then the system enqueues delivery tasks to the TaxTidy Notification Service with idempotency keys and retries failed deliveries up to 3 times with exponential backoff, logging failures after final retry Given a user lacks the permission to view the 1099 Watchlist for payer P When an alert for payer P would be generated Then the system does not deliver the alert to that user and does not expose the alert in their in-app feed Given a user's access to payer P is revoked When future alerts for payer P are generated Then the user does not receive those alerts and any existing in-app deep links to payer P return an access denied state
Payer Identity Normalization
"As a user, I want TaxTidy to merge different names for the same client into one payer profile so that my 1099 watchlist totals are accurate."
Description

Identity resolution that unifies multiple representations of the same payer (invoice client names, bank memo strings, platform descriptors) into a single canonical Payer Profile with legal name, FEIN/EIN (when available), address, and contact info. Implements deterministic and probabilistic matching rules, manual review/merge flows, and undo capabilities to maintain accurate aggregation. Stores alias mappings and source provenance for traceability. Integrates with W‑9 capture to enrich profiles and improves the accuracy of watchlist totals and alerts.

Acceptance Criteria
Deterministic EIN-Based Merge Across Sources
Given two or more payer records from any sources (invoices, bank feeds, platforms) share the same FEIN/EIN value after punctuation/whitespace normalization And none of the records have conflicting non-empty EIN values When the records are ingested or reprocessed Then the system creates/updates a single canonical Payer Profile containing that EIN And the merge reason is recorded as "deterministic_ein" And all distinct names from the source records are stored as aliases mapped to the canonical profile And all source provenance (source type, raw identifier, timestamps) is persisted And watchlist totals aggregate across the merged sources within 60 seconds And no other profile exists in the workspace with the same EIN
Probabilistic Name/Address Matching with Thresholds
Given two payer records lack a usable EIN and have similar names/addresses When the computed match score is >= 0.88 and postal code matches Then the records are auto-linked to the same canonical Payer Profile And the merge reason is recorded as "probabilistic_auto" And the stored confidence score reflects the computation Given 0.70 <= score < 0.88 or postal code is missing but city+state match When the match is evaluated Then the pair is placed into the Review Queue within 10 seconds And no auto-merge occurs Given score < 0.70 When the match is evaluated Then no link is suggested nor created
Manual Review: Merge, Field Selection, and Audit Log
Given two or more candidate profiles are in the Review Queue When a user selects Merge and confirms Then the system presents a field-by-field preview allowing selection of canonical legal name, address, and contact info And upon confirmation, profiles are merged into one canonical Payer Profile And all non-selected values are preserved as aliases or secondary fields And an immutable audit record is stored with actor, timestamp, selected fields, and decision rationale And watchlist totals recompute and reflect across dashboards within 60 seconds And the canonical profile ID remains stable across subsequent ingestions
Undo Merge (Unmerge) Restores Prior State
Given a merge occurred within the past 30 days When a user triggers Undo Merge on the canonical profile Then the system restores the original profiles with their prior IDs, fields, aliases, and provenance And watchlist totals and alerts are recalculated accordingly within 60 seconds And the audit log records the undo action linking to the original merge event And no data from unrelated profiles is affected
Alias Mapping and Source Provenance Retrieval
Given any canonical Payer Profile When requesting its alias map Then the system returns all known aliases including source type, raw values (e.g., bank memo strings, invoice client names, platform descriptors), and first/last seen timestamps And lookups by any alias resolve to the canonical profile within 200 ms at the 95th percentile And exporting the profile includes the alias mapping and provenance details
W-9 Capture Enriches and Locks Canonical Identity
Given a W-9 is captured and verified for a payer When the W-9 contains a valid TIN format and legal name Then the canonical Payer Profile is updated with legal name and TIN and flagged trust_level="w9_verified" And any profiles with matching TIN are auto-merged deterministically And pending review suggestions for that payer are re-scored and updated within 5 minutes And access to the W-9 document is role-restricted and TIN is masked in UI except last 4 digits
Watchlist Totals and Alerts Use Canonical Profiles
Given multiple transactions and invoices across alias sources belong to the same payer When normalization completes Then the 1099 Watchlist displays a single payer row with aggregated YTD total equal to the sum of underlying items within ±$0.01 And threshold alerts trigger once per canonical payer according to configured rules (e.g., approaching $600, reached $600) And received 1099 forms can be matched to the canonical payer by EIN and legal name with a one-click reconcile that flags over/under-reporting beyond ±$1 And deduplicated totals persist across sessions and after data refresh
1099-Eligible Income Logic
"As a freelancer, I want the watchlist to count only income that belongs on a 1099‑NEC and exclude 1099‑K and reimbursements so that my thresholds are correct."
Description

Rules engine that determines which payments count toward 1099‑NEC totals, excluding amounts reported on 1099‑K (e.g., card processor payouts) and non-compensation items (reimbursements, sales tax collected) according to IRS guidance. Supports platform-specific heuristics (Stripe/PayPal marketplace payouts), bank-transaction pattern recognition, and user overrides with justification notes. Applies year-based cutoffs (cash-basis date logic) and handles currency conversions on transaction date. Provides explainability: each total displays included/excluded line items and reasons to reduce disputes and double‑counting.

Acceptance Criteria
Exclude 1099‑K and Marketplace Payouts from 1099‑NEC
Given a transaction identified as a card-processor/marketplace payout (e.g., Stripe/PayPal/Shopify) by descriptor, MCC, or platform metadata When 1099‑NEC totals are calculated for a client and year Then the transaction amount is excluded from 1099‑NEC and the reason "1099‑K eligible payout" is recorded and shown in explainability Given a transfer labeled as Stripe Connect/PayPal Payouts (platform-specific heuristics matched) When eligibility is evaluated Then the transfer is excluded from 1099‑NEC and linked to the platform source in the audit trail Given an ACH/Zelle/wire payment received directly from a client (not via a 1099‑K platform) When eligibility is evaluated Then the amount is included in 1099‑NEC for that client and year
Exclude Non‑Compensation Amounts (Reimbursements & Sales Tax)
Given a transaction or invoice split with a category tagged Reimbursement When 1099‑NEC eligibility is computed Then the reimbursed amount is excluded and the reason "Non‑compensation: reimbursement" is displayed Given a receipt/invoice with detected sales tax (via OCR or itemization) or a split line categorized as Sales Tax Collected When eligibility is computed Then the sales tax portion is excluded and the reason "Non‑compensation: sales tax" is displayed Given a transaction containing both compensation and non‑compensation splits When totals are computed Then only the net compensation amounts are included and each excluded portion is itemized in the explainability view
Cash‑Basis Year Cutoff by Posted Date and Timezone
Given the user’s tax year and timezone are set When a transaction’s posted/cleared date falls within the tax year in the user’s timezone Then the amount is considered for that year’s 1099‑NEC computation Given a transaction initiated in one year but posted in the next When eligibility is computed under cash‑basis rules Then the transaction is included in the year it posted, not the initiation year Given backdated edits to source documents (e.g., invoice date changes) When totals recalc Then the posted bank date remains the determinant for cash‑basis inclusion and a note is added to the audit trail
Accurate Currency Conversion on Transaction Date
Given a foreign‑currency transaction included for 1099‑NEC When converting to USD for totals Then the system applies the FX rate from the transaction posted date; if unavailable, uses the most recent prior business day rate, and stores the rate source and timestamp Given rounding rules for USD When totals are computed Then line items are converted to USD with bankers’ rounding to the nearest cent and the rounded values are displayed in explainability Given the same transaction is recalculated When FX data has not changed Then the converted USD amount remains deterministic and unchanged
User Override with Justification and Auditability
Given a user selects a transaction line and chooses Include or Exclude override When the user provides a justification note of at least 10 characters Then the override is saved, the 1099‑NEC total updates within 5 seconds, and the explainability view shows "Override: include/exclude" with the note Given an existing override When the user reverts to system‑determined status Then the total recalculates and the audit log records the revert with timestamp and user id Given an export of the client’s 1099 detail When overrides exist Then the export includes override status, justification note, timestamp, and user id for each affected line
Explainability: Included/Excluded Line‑Item Drilldown
Given a client’s 1099 Watchlist entry When the user opens the detail view Then the system lists all included and excluded line items with source (bank/invoice/receipt), rule or override reason, original currency amount, USD amount, and inclusion status Given a user clicks on a line item When the detail panel opens Then the applied rule (e.g., "1099‑K payout exclusion", "Sales tax exclusion") is shown with the matched evidence (descriptor match, category tag, OCR field) and a link to the source document Given the user exports detail When CSV or PDF is generated Then the exported file contains the same columns and reasons as displayed in the drilldown
De‑duplication Across Sources to Prevent Double‑Counting
Given a bank deposit matches an issued invoice payment via amount, date window, and client identity heuristics When eligibility is computed Then the two records are linked and counted once toward 1099‑NEC totals Given multiple receipts/invoices map to a single consolidated bank deposit When eligibility is computed Then only the compensation portions are summed once and mapped to that deposit with traceable links in explainability Given conflicting matches (two possible invoices for one deposit) When the confidence score is below threshold Then the system flags the item for review, excludes it by default from totals pending resolution, and surfaces a "needs review" reason
Year-End 1099 Reconciliation Workspace
"As a taxpayer, I want to compare my tracked totals to the 1099‑NECs I receive so that I can quickly spot and resolve over‑ or under‑reporting."
Description

Dedicated workflow to reconcile received 1099‑NEC forms against TaxTidy’s tracked payer totals. Supports importing forms via photo/OCR, PDF upload, or data import (CSV, accounting integrations), auto-matching by payer profile, EIN, and amount, and highlighting discrepancies beyond a configurable tolerance. Provides side-by-side comparison, drill‑down to underlying transactions, adjustment entries, and note-taking. Allows marking payers as reconciled, partially reconciled, or disputed, and generates a discrepancy summary for the user or CPA. Integrates with the Evidence Pack Export.

Acceptance Criteria
Multi-Source 1099-NEC Import and OCR Capture
- Given I am in the Year-End 1099 Reconciliation Workspace When I import a 1099-NEC via photo, PDF, or CSV (<= 25 MB; JPG/PNG/PDF/CSV) Then the file is accepted, parsed, and an import summary shows counts of forms imported, duplicates skipped, and errors within 5 seconds for up to 100 forms - Given a photo/PDF import When OCR runs Then payer name, EIN, recipient name, recipient TIN (if present), tax year, and Box 1 amount are extracted with >=95% field-level accuracy on clean scans and confidence scores are displayed; fields with confidence <90% are flagged for review - Given a CSV or integration import When column mapping is required Then a mapping screen enforces required fields (payer name or EIN, tax year, amount) and prevents import until all required fields are mapped and validated - Given duplicate detection is enabled When a form with the same payer EIN, recipient TIN, tax year, and amount is imported Then it is flagged as a duplicate and not re-created; the import summary lists duplicates
Auto-Match 1099 Forms to Payer Records
- Given a 1099 form is imported When auto-matching runs Then the system matches to a payer record by exact EIN first; if EIN is missing, by normalized payer name with a fuzzy score >= 0.92 and amount within tolerance; a confidence score is shown - Given multiple candidate matches When confidence scores tie within 0.02 Then no automatic link occurs and the user is prompted to choose; the form remains in "Needs review" until resolved - Given a user manually links or unlinks a form to a payer When the action is saved Then the link state updates immediately and is recorded in the audit log with user, timestamp, and optional rationale
Configurable Discrepancy Tolerance and Highlighting
- Given the default discrepancy tolerance is $50 or 1%, whichever is greater When calculating differences between 1099 Box 1 and TaxTidy tracked totals for the same payer and tax year Then differences at or below tolerance are marked "within tolerance" and not flagged; differences above tolerance are flagged in red with the amount and percentage - Given the user opens settings or a payer override When they change tolerance (absolute, percent, or both) Then the new tolerance applies immediately to all comparisons in the workspace and is persisted per user and per payer override - Given a flagged discrepancy When the user hovers or taps the flag Then an explanation tooltip shows the calculation, tolerance used, and last recalculation time
Side-by-Side Comparison With Drill-Down to Transactions
- Given a matched payer for a selected tax year When viewing the reconciliation panel Then the left side shows imported 1099 form fields (payer name, EIN, Box 1 amount, form ID) and the right side shows TaxTidy total, date range used, number of transactions, and adjustments applied - Given the user clicks the TaxTidy total When the transaction drawer opens Then the underlying transactions list loads within 2 seconds, supports filter by date, category, and source, and totals equal the displayed TaxTidy total within $0.01 - Given the user selects a transaction When drilling further Then the original source document (invoice, bank line, receipt) is viewable, if available
Posting Adjustments and Notes With Audit Trail
- Given a discrepancy exists When the user posts an adjustment Then they must enter amount, direction (increase/decrease), reason, and optional attachment; the adjustment updates the TaxTidy total and is labeled "Adjustment" in the ledger - Given an adjustment is saved When viewing history Then an immutable audit entry records user, timestamp, before/after totals, reason, and attachment link - Given an adjustment would bring the discrepancy within tolerance When saved Then the payer status can be set to "Reconciled"; if no note is supplied for adjustments > $100 or >2% of total, saving is blocked with a prompt - Given a user voids an adjustment When confirmed Then the original totals are restored and a void entry is recorded; voiding requires a note
Reconciliation Status Management and Validation
- Given a payer comparison When the discrepancy is within tolerance (considering adjustments) Then the system allows status "Reconciled" and prevents "Disputed" without a note - Given the discrepancy exceeds tolerance When attempting to set "Reconciled" Then the action is blocked with guidance to post an adjustment or mark "Disputed" with a required note - Given multiple forms or unmatched items remain for a payer When setting status Then "Partially Reconciled" is available and requires selecting pending items; status shows a progress indicator (e.g., 2 of 3 forms matched) - Given statuses are updated When returning to the workspace Then statuses persist, are filterable (All/Reconciled/Partial/Disputed), and are included in exports
Discrepancy Summary Generation and Evidence Pack Integration
- Given at least one payer has a discrepancy above tolerance When the user clicks "Generate Summary" Then a summary PDF and CSV are produced within 10 seconds for up to 200 payers, listing payer name, EIN, TaxTidy total, 1099 total, difference, tolerance, status, notes, and links to source docs - Given the summary is generated When exporting to Evidence Pack Then the summary and all referenced source documents (forms, adjustments, transaction exports) are attached and indexed under "1099 Reconciliation" with cross-links back to the workspace - Given the user shares the summary When creating a CPA share link Then a time-bound, read-only link valid for 14 days is created with access logs; access can be revoked at any time
Evidence Pack Export
"As a freelancer, I want to export a comprehensive packet of my 1099 watchlist and reconciliation details so that I can share clean, defensible records with my CPA."
Description

Export generator that produces an IRS‑ready, annotated packet including per‑payer totals, included/excluded transactions with reasons, reconciliation status and notes, and copies of relevant 1099‑NECs, invoices, and W‑9s. Supports PDF for review and CSV for import into tax software, with selectable date ranges and payer filters. Embeds source provenance and timestamps for audit defense. Integrates with TaxTidy’s existing export service and mobile share sheet for quick sending to a CPA.

Acceptance Criteria
IRS-Ready PDF Evidence Pack Generation
Given a workspace with at least one payer and transactions within the selected date range When the user selects Export > Evidence Pack and chooses PDF Then the system generates a single PDF containing: a cover summary with per-payer income totals and adjustments; sections listing included transactions and excluded transactions with explicit exclusion reasons; per-payer reconciliation status and any user notes; appended copies of relevant 1099-NECs, invoices, and W-9s And the PDF is text-searchable and preserves original document image quality at 300 DPI or better And the PDF footer on every page displays a unique Export ID, generated-at timestamp (ISO 8601, UTC), and the workspace identifier And for datasets up to 2,000 transactions and 150 attached documents totaling ≤150 MB, the PDF is generated within 60 seconds And the generated file name follows TaxTidy_EvidencePack_[YYYYMMDD]_[dateStart-dateEnd]_[payerCount]payers.pdf
CSV Export for Tax Software Import
Given the user selects Export > Evidence Pack and chooses CSV with a valid date range and payer filter When the export completes Then a UTF-8 (no BOM), RFC 4180-compliant CSV is downloaded with the header: export_id,payer_id,payer_name,payer_tax_id,transaction_id,transaction_date,amount,currency,included,exclusion_reason,category,source_type,source_system,source_identifier,import_timestamp,reconciliation_status,reconciliation_note And transaction_date values are ISO 8601 (YYYY-MM-DD) in workspace timezone, import_timestamp is ISO 8601 UTC And amount values use two decimals and currency is ISO 4217 And included is TRUE or FALSE; exclusion_reason is empty when included is TRUE And reconciliation_status is one of Match, OverReported, UnderReported, NoFormOnFile And the number of data rows equals the count of transactions matching the selected filters And the generated file name follows TaxTidy_EvidencePack_[YYYYMMDD]_[dateStart-dateEnd]_[payerCount]payers.csv
Date Range and Payer Filter Application
Given the user opens Export > Evidence Pack When the user sets a custom date range (start and end) and selects one or more payers Then only transactions with transaction_date between start and end (inclusive) AND belonging to the selected payers are included in both PDF and CSV And the pre-export summary displays the number of transactions and total amount that will be exported And if the selection yields zero transactions, the Export buttons are disabled and a message states “No results for the chosen date range and payers.” And changing filters updates the preview counts within 1 second
Reconciliation Summary and Discrepancy Flagging
Given one or more 1099-NEC documents are attached to payers for the selected tax year/date range When the user generates the Evidence Pack Then for each payer the export computes Tracked Total, Reported Total (sum of attached 1099-NECs), and Difference = Reported − Tracked to the nearest cent And reconciliation_status is set per payer: Match (|Difference| < $0.01), OverReported (Difference > = $0.01), UnderReported (Difference <= −$0.01), NoFormOnFile (no 1099-NEC linked) And any reconciliation notes saved in the app are included alongside the payer’s reconciliation section in PDF and as reconciliation_note in CSV And if multiple 1099-NECs exist for a payer, the export lists each form with identifier and amount and shows the aggregated Reported Total
Embedded Source Provenance and Timestamps
Given transactions originate from invoices, bank feeds, or receipt photos When the Evidence Pack is generated Then each transaction entry in PDF and CSV includes provenance fields: source_type (invoice|bank|receipt), source_system, source_identifier, and import_timestamp (ISO 8601 UTC) And each attached document (1099-NEC, invoice, W-9) in the PDF appendix lists original filename, source_system, capture/import timestamp, and a SHA-256 checksum of the file And the export includes a unique Export ID and generated-at (ISO 8601 UTC) prominently on the cover page and in CSV export_id column And all timestamps reflect UTC with timezone explicitly noted
Export via Existing Service and Mobile Share Sheet
Given the device is online and the user is authenticated When the user initiates an Evidence Pack export Then the request is routed through TaxTidy’s existing export service with the same authorization and rate limits And upon successful generation on iOS and Android, the native share sheet opens within 3 seconds with the exported file attached and a prefilled subject “TaxTidy Evidence Pack — [dateStart–dateEnd]” And the export is stored in the user’s TaxTidy account under Exports with the Export ID, file type, size, and generated-at timestamp And if share is canceled, the file remains accessible in Exports and via a Copy Link action respecting workspace permissions

Refund Reconcile

Tracks refunds, partial credits, and chargebacks back to their original invoices, updating client totals, project P&L, and 1099 Watchlist automatically. Keeps year‑to‑date income honest—even when payments don’t go as planned.

Requirements

Refund Event Ingestion & Idempotency
"As a freelancer, I want TaxTidy to automatically capture refund and chargeback events from my banks and processors so that I don’t miss adjustments that affect my taxes."
Description

Continuously capture refund, partial credit, and chargeback events from bank feeds and payment processors via polling and webhooks, normalize disparate schemas, and persist raw payloads. Deduplicate events using idempotency keys and external IDs, maintain stable mappings between transactions and invoices, and implement resilient retries with backoff. Support multi-currency, timezone normalization, and categorization of event types (refund, reversal, fee). This foundation ensures complete, reliable input data for downstream reconciliation and reporting within TaxTidy’s data pipeline.

Acceptance Criteria
Signed Webhook Ingestion and Raw Payload Persistence
Given a valid signed webhook event from a supported processor for a refund, partial credit, chargeback, or fee When the event is received by the webhook endpoint Then the signature is verified and invalid signatures are rejected with HTTP 401 and no data is persisted And the raw JSON payload is stored losslessly within 1 second of receipt with a stable raw_payload_id And a normalized event record is created referencing the raw_payload_id And the endpoint returns a 2xx response within 2 seconds upon successful processing
Scheduled Polling for Bank Feeds with Catch-Up
Given bank feeds that do not support webhooks When the polling job runs every 5 minutes Then transactions of type refund, credit, or chargeback since the last successful checkpoint (with a 24-hour lookback) are fetched And raw payloads are persisted before normalization And the checkpoint is advanced atomically only after successful normalization and deduplication And rerun polling cycles do not create duplicate normalized records for already ingested transactions
Idempotent Deduplication Across Webhooks and Polling
Given events may arrive multiple times or via both webhook and polling for the same refund/chargeback When an incoming event shares the same dedup identity (processor, external_id or idempotency_key, amount_minor, currency) within a 30-day window Then no additional normalized event is created and no side effects (mappings, metrics) are duplicated And the existing normalized record is returned for downstream processing And deduplication state persists across service restarts and deployments
Normalization and Event Type Categorization
Given disparate schemas from processors and bank feeds When events are normalized Then the normalized record contains: event_type ∈ {refund, partial_refund, chargeback, reversal, fee}, amount_minor (integer), currency (ISO 4217), processor (enumeration), external_id, occurred_at_utc (ISO 8601 UTC), source_channel ∈ {webhook, polling}, original_timezone, raw_payload_id And partial refunds are identified and flagged with refundable_remaining_minor calculated per payment/invoice context And unrecognized types are captured with event_type=unknown while preserving raw payload and flagged for review without failing the pipeline
Multi-Currency and Timezone Normalization
Given events may be in various currencies and local timezones When storing monetary and temporal fields Then amounts are stored in minor units with ISO 4217 currency codes in uppercase And occurred_at_utc is normalized to UTC with millisecond precision while preserving original timezone metadata And if an organization base currency is configured, a base_converted_amount_minor is computed using the FX rate as of occurred_at_utc and stored with fx_rate and fx_source metadata
Stable Transaction-to-Invoice Mapping
Given a refund, partial refund, chargeback, or fee that references a payment or invoice via metadata (invoice_id, payment_intent_id, bank memo) or deterministic matching rules When normalization completes Then the event is linked to the correct invoice/payment record using deterministic matching with documented tie-breakers And once established, the mapping remains stable across reprocessing and retries unless an explicit correction event is received And split payments and multiple partial refunds map to the same invoice and update net invoice impact accordingly And an audit trail records mapping inputs, rule applied, and final link id for traceability
Resilient Retry with Exponential Backoff and Dead-Letter Queue
Given transient failures such as HTTP 5xx, rate limits, or timeouts during ingestion or normalization When an operation fails Then it is retried up to 5 attempts with exponential backoff starting at 1s and doubling up to a max delay of 30s And retries are idempotent and do not create duplicate normalized records or mappings And after max retries, the event is moved to a dead-letter queue with failure reason, attempt count, and raw_payload_id for later reprocessing And successful manual or automated reprocessing from the dead-letter queue marks the item as resolved and produces exactly one normalized event
Refund Auto-Matching Engine
"As a freelancer, I want refunds to be auto-linked to the original invoice so that my income and client records stay accurate without manual matching."
Description

Automatically link refund, partial credit, and chargeback transactions to their originating invoices using deterministic identifiers (invoice IDs, processor payment intents) and probabilistic signals (amount similarity, date windows, merchant descriptors). Handle multiple partial refunds against a single invoice, net-of-fee discrepancies, and fee line items. Produce a confidence score with states (auto-matched, needs review, no match) and expose match rationale for transparency. Integrate with TaxTidy’s transaction graph to keep matching fast and incremental as new events arrive.

Acceptance Criteria
Deterministic Identifier Auto-Match
Given a refund, partial credit, or chargeback includes a processor payment_intent or invoice_id equal to an existing paid invoice When the engine processes the transaction Then it links the transaction to that invoice with state=auto-matched, confidence >= 0.99, and rationale lists the matched identifier keys and values And processing time is <= 150 ms p95 per transaction on a dataset of up to 100k transactions
Probabilistic Matching Without Identifiers
Given a refund/credit/chargeback lacks deterministic identifiers When candidate invoices are evaluated using amount similarity (within ±$1 or ±1%, whichever is greater), date window (within 30 days of original payment), and merchant descriptor similarity (>= 0.80) Then the engine computes a confidence score in [0,1] and assigns state: auto-matched if score >= 0.85; needs review if 0.60 <= score < 0.85; no match if score < 0.60 And the rationale lists the top 3 contributing signals with their weights
Multiple Partial Refunds On Single Invoice
Given multiple partial refunds relate to the same invoice When each refund is processed Then the engine links each to the invoice, maintains a running refunded_total, and prevents cumulative refunds from exceeding the settled amount And reprocessing the same refund (same processor refund_id) is idempotent and does not double count
Net-of-Fee and Fee Line Items Handling
Given a refund/chargeback is net of fees or includes dispute/processor fee line items When the engine matches the transaction Then it reconciles using gross and net amounts, creates/links fee line items, and updates project P&L and client totals to reflect -refund and +fee expense separately And matching tolerates a net discrepancy equal to the identified fee(s) ± $0.01
Chargeback Matching and Accounting Adjustments
Given a chargeback is received for a previously settled payment When the engine processes it Then it links to the original invoice using deterministic or probabilistic rules, records a negative income adjustment and any dispute fees, and sets state per the computed confidence And the invoice, client totals, and related project P&L are updated within 1 second p95
Confidence Score and Rationale Exposure
Given any match decision is produced When the result is retrieved via API Then the response includes fields: state, confidence (0.00–1.00), matched_invoice_id, reasons[] (signal_name, contribution), and candidates[] (top 3 with scores for needs review) And the UI displays state and the top 3 reasons for transparency
Incremental Transaction Graph Integration
Given new transactions arrive or existing ones are updated When the engine runs incrementally Then only affected nodes and edges in the transaction graph are re-evaluated, end-to-end propagation (matching + updates to invoice balance, client totals, project P&L, and 1099 Watchlist) completes within 1 second p95, and results are versioned with audit trail entries
Invoice & Client Totals Adjustment
"As a freelancer, I want my invoice statuses, client totals, project P&L, and 1099 tracking to update when refunds occur so that my year-to-date income reflects reality."
Description

Upon confirmed match, update the source invoice status (refunded/partially refunded), adjust client lifetime and year-to-date income, recalculate project P&L, and update the 1099 Watchlist thresholds. Reflect changes on cash-basis income reports and dashboards, honoring effective dates, rounding rules, and multi-currency conversions. Sync adjustments to connected accounting systems (e.g., as credit memos or negative payments) to keep ledgers aligned. Trigger downstream recalculations and summaries used in IRS-ready tax packets.

Acceptance Criteria
Full Refund — Single Invoice, Same Currency
Given a paid invoice in the same currency as the refund and a confirmed one-to-one match When the refund amount equals the invoice gross total and the effective date is set Then set the invoice status to Refunded And decrease the client lifetime income and year-to-date income by the refund amount using the refund effective date And update cash-basis income reports and dashboards to reflect the decrease within 60 seconds And recalculate the associated project P&L to include the refund as negative income And append an audit log entry capturing user/source, timestamp, effective date, amounts before/after, and invoice/refund IDs
Partial Refund — Line-Level Allocation
Given a paid invoice with multiple line items mapped to categories/projects and a confirmed partial refund match When an allocation is provided with the refund (line-level or category-level) Then allocate the refund to the specified lines/categories and mark invoice status as Partially Refunded And adjust each impacted project P&L and category totals by the allocated amounts And reduce client lifetime and year-to-date income by the total refund amount according to the refund effective date And display the remaining invoice balance and cumulative refunded amount with proper rounding to currency minor units And record the allocation method and details in the audit trail Given a confirmed partial refund match without explicit allocation details When processing the refund Then allocate proportionally across invoice line items by their original gross amounts And apply the same adjustments and audit logging as above
Chargeback — Backdated to Prior Year (Cash Basis)
Given a confirmed chargeback matched to a prior paid invoice and an effective date in a previous tax year When processing the chargeback Then decrease client lifetime income by the chargeback amount And do not change the current year-to-date income totals And update prior-year cash-basis income reports to reflect the chargeback on the effective date And leave current-year dashboards unchanged unless the date filter includes the chargeback effective date And set invoice status to Partially Refunded or Refunded based on total recovered amount And capture an audit entry labeled Backdated with effective date, amounts, and affected reporting periods
Multi-Currency Refund — Conversion & Rounding
Given base currency is set (e.g., USD) and an invoice exists in a foreign currency (e.g., EUR) with a confirmed refund When the refund is processed with an effective date T Then convert the refund to base currency using the configured FX rate for date T and store the FX rate used And round both source and base amounts to the correct minor units for each currency And adjust client lifetime and year-to-date income in base currency using the converted amount And ensure the sum of line-level allocations equals the refund total after rounding by distributing any 0.01 residual to the highest-value line And reflect converted adjustments on cash-basis reports and dashboards within 60 seconds
1099 Watchlist — Threshold Re-evaluation
Given a client’s year-to-date payments to the freelancer are at or above the 1099-NEC threshold and a refund is confirmed effective in the current tax year When the refund is processed Then recompute the client-to-freelancer year-to-date total immediately And update the 1099 Watchlist status to Below Threshold if the new total falls under the threshold, otherwise keep Above/At Threshold with updated amount And show the Watchlist update in the UI within 60 seconds and in exports on next generation And append an audit note referencing the threshold change, old total, new total, and refund reference
External Accounting Sync — Credit Memo vs Negative Payment (Idempotent)
Given an accounting system (e.g., QuickBooks/Xero) connection is active and sync is enabled When a refund or chargeback is processed and matched to an invoice Then create a credit memo applied to the original invoice for merchant refunds or a negative payment linked to the original payment for chargebacks, as supported by the target system And include a reference to the TaxTidy Refund ID and original Invoice ID in the external memo/description And complete the sync within 2 minutes of processing with confirmation of external record IDs And ensure idempotency so that retries or duplicate webhooks do not create duplicate external records (matched by external transaction key) And surface sync failures with retriable status, error code/message, and notify the user with next automatic retry time
Downstream Recalculations — Project P&L, Dashboards, Tax Packet Summaries
Given a refund affects one or more projects and reporting periods used by IRS-ready tax packets When the refund is processed Then recalculate impacted project P&L metrics (revenue, margin) within 60 seconds And refresh dashboards and cash-basis income summaries to include the adjustment within 60 seconds And regenerate or flag as Needs Refresh all affected tax packet schedules for the impacted periods And emit an income.adjusted event containing invoice ID, refund ID, effective date, amounts (source/base), and impacted aggregates And coalesce multiple adjustments for the same invoice within a 30-second window to avoid redundant recalculations
Partial Allocation & Categorization
"As a freelancer, I want partial refunds to be allocated to the correct line items and categories so that my reports and deductions remain precise."
Description

Allocate partial refunds across invoice line items proportionally by amount or via user-specified splits. Preserve and adjust original tax categories, tags, and project associations so that category rollups remain accurate. Support splitting refunds across multiple projects and handling sales tax portions, ensuring accurate treatment in reports and exports. Provide deterministic rules to prevent orphaned balances and zero out invoices only when fully reconciled.

Acceptance Criteria
Proportional Allocation Across Invoice Line Items
Given an invoice with multiple line items and a partial refund amount R And each line item has original category, tags, and project associations When the user selects Allocate proportionally by original line amounts and confirms Then the system allocates R to each line item proportionally to two decimal places And the sum of allocations equals exactly R after deterministic rounding (any residual cent assigned to the highest-amount line, then earliest line position) And no line item allocation exceeds its original line amount or remaining balance And original categories, tags, and project associations are preserved on refund entries And line and invoice balances are updated without creating negative balances
User-Specified Splits Across Items and Projects
Given an invoice with line items and a partial refund amount R And the user opts to enter manual splits by amount or percentage When the user assigns allocations per line item and optionally adds multiple project splits for a line item Then validation enforces total allocations equal exactly R and no allocation is negative or exceeds the line’s remaining balance And category and tag values default from the original line but can be explicitly overridden per split And saving creates discrete refund allocation entries per item/project with two-decimal precision And save is blocked until any unallocated remainder is resolved to $0.00
Sales Tax Portion Handling
Given an invoice that includes a sales tax line and a partial refund When allocating the refund Then if the refund includes a tax component, the tax portion is allocated to the sales tax line up to its original tax amount, with the remainder allocated to items And allocations to the sales tax line use the Sales Tax category and do not affect income/expense rollups And if the refund excludes tax, the sales tax line is not adjusted And exports and reports reflect tax allocations in tax liability totals, not income
Deterministic Reconciliation and Zero-Out Rules
Given an invoice with payments and one or more refunds When refund allocations are saved Then the invoice status reflects Partially Refunded or Refunded as appropriate and only becomes Closed/Zeroed when payments minus refunds equals the invoice total to the cent And the allocation process is idempotent: reprocessing the same refund transaction ID does not create duplicate allocations And rounding rules prevent orphaned $0.01 balances by assigning any residual cent via deterministic tie-breaker (highest original line amount, then earliest line position) And the system prevents negative balances at both line and invoice levels
Reporting Updates and 1099 Watchlist Adjustments
Given a refund has been allocated and saved When dashboards and reports refresh Then client YTD and lifetime income decrease by the sum of allocations to income categories for that client And affected project P&L reflects the allocations on the associated projects dated as of the refund date And category rollups update within 60 seconds and match the sum of underlying allocations And the 1099 Watchlist reduces the payer’s reportable amount by allocations mapped to 1099-reportable income categories
Accurate Exports and Audit Trail for Allocations
Given refund allocations exist for an invoice When the user exports CSV reports or the IRS-ready annotated tax packet Then exports include refund allocation lines with original invoice ID, refund/chargeback transaction ID, allocation amounts per category and project, and split method (Proportional or Manual) And exported category totals equal in-app rollups to the cent And an audit trail entry records actor, timestamp, before/after amounts, and the allocation method And re-exporting after changes reflects the latest allocation state
Bank Feed Refund/Chargeback Allocation Flow
Given a refund or chargeback transaction is imported from a bank feed And the system detects one or more candidate invoices with matching client and amount context When the user opens the transaction and selects a target invoice Then the system proposes a default proportional allocation that the user can switch to manual splits And the allocation cannot be saved until an invoice is selected and the sum of splits equals the refund amount And unmatched refunds remain Pending and do not impact balances or reports until allocated And upon save, invoice and line balances update and the transaction is marked Reconciled with a link to the allocation
Chargeback Lifecycle Management
"As a freelancer, I want chargebacks and their fees tracked through their lifecycle so that I understand what income is at risk and what was finally reversed."
Description

Model the full lifecycle of chargebacks, including provisional debits, representments, outcomes (won/lost), and associated fees. Mark impacted income as "at risk" until final resolution, separate temporary reversals from final refunds, and automatically update amounts and statuses as processor webhooks change. Surface key timelines and reason codes, and ensure final outcomes propagate to invoices, client totals, and reports without double counting.

Acceptance Criteria
Provisional Chargeback Creation and Linking
Given a settled payment linked to an invoice and client When a processor sends a 'chargeback initiated' webhook containing case ID, original transaction ID, disputed amount, currency, and reason code Then the system creates a Chargeback record in status 'Provisional Debit' linked to the original payment, invoice, project (if any), and client And the disputed amount supports partial amounts and multiple chargeback cases per invoice And the record stores processor/scheme, acquirer case ID (if provided), reason code, initiated timestamp, and dispute currency And the invoice and payment detail views display a Chargeback badge with the disputed amount and reason code
Income At-Risk Flagging and Reporting Adjustments
Given a Chargeback record exists in any non-final status (e.g., Provisional Debit, Represented, Under Review) When calculating client totals, project P&L, YTD income, and the 1099 Watchlist Then the disputed amount is excluded from recognized income and marked as 'At Risk' while still tracked under gross income And invoice and client summary show an 'At Risk' subtotal equal to the sum of open chargeback disputed amounts And removing or resolving the chargeback automatically clears the 'At Risk' flag and recalculates metrics within 1 minute of the triggering webhook
Representment Status Updates and SLA Timeline Surfacing
Given a Chargeback in status 'Provisional Debit' When a 'representment submitted' or equivalent webhook/event is received Then the chargeback status updates to 'Represented' and the system records submission timestamp and evidence reference (if provided) And the UI surfaces reason code, scheme (Visa/MC/Amex/etc.), and computed SLA milestones (e.g., response due, expected decision window) based on scheme-specific rules And subsequent webhooks update status progression (e.g., 'Under Review', 'Arbitration') without losing prior timestamps, while maintaining an audit trail of changes
Final Outcome Propagation Without Double Counting
Given a Chargeback with linked invoice and client totals When a final outcome webhook is received with outcome 'Won' or 'Lost' Then the chargeback status becomes 'Resolved—Won' or 'Resolved—Lost' and the 'At Risk' flag is cleared And amounts propagate exactly once to invoice balance, client totals, project P&L, and reports without double counting even if multiple partial disputes exist And if 'Won', provisional debit entries are reversed and recognized income is restored for the disputed amount And if 'Lost', a final refund entry is recorded for the disputed amount (distinct from any temporary reversals) And all affected reports (YTD income, P&L, 1099 Watchlist) reflect the net outcome within 1 minute
Temporary Reversals vs Final Refunds Separation
Given a Chargeback in progress with a previously recorded provisional debit When the processor sends a 'temporary reversal/conditional credit' webhook Then the system records a Temporary Reversal linked to the chargeback without marking the case as resolved or restoring recognized income And the 'At Risk' designation remains until a final outcome is received And when a final 'refund/credit posted' or 'final decision' webhook arrives, the system converts the temporary reversal to final settlement as appropriate and updates income recognition accordingly And the audit trail shows distinct entries for provisional debit, temporary reversal(s), and final settlement
Chargeback Fee Accounting to P&L
Given a Chargeback case with processor-issued fees (initiation fee, representment fee, arbitration fee) delivered via webhooks or fee line items When fees are received Then the system records fee expenses mapped to 'Processor/Bank Fees', linked to the chargeback, invoice, project (if any), and client And fees are excluded from 1099 income but included in project P&L and expense reports with correct signs and currency And if the case is won and fees are refunded, the system records a fee reversal that nets to zero across reports without double counting
Webhook Idempotency and Out-of-Order Event Reconciliation
Given processor webhooks may arrive duplicated or out of chronological order When receiving any chargeback-related webhook (initiation, representment, temp reversal, outcome, fees) Then the system deduplicates by provider event ID and chargeback case ID to avoid duplicate records or double financial impact And it reconciles the chargeback to the correct latest status using authoritative fields (e.g., final outcome flag), updating timestamps without regressing status And late-arriving interim events are recorded in the audit log but do not alter final financial outcomes And an integration health metric flags orphaned events (no matching payment) for manual resolution
Reconciliation Review & Overrides
"As a freelancer, I want an easy review and override screen for low-confidence matches so that I can quickly fix edge cases and keep working on mobile."
Description

Provide a mobile-first review queue for low-confidence matches with quick actions to confirm, reassign to another invoice, split across invoices, or ignore. Display contextual details (original invoice, client, amounts, fees, dates, confidence, and event timeline) and support search, filters, and bulk approvals. Capture user notes and reason codes; all actions trigger recalculations and updates to dashboards and exports. Enforce role-based permissions for teams and maintain responsive performance on mobile.

Acceptance Criteria
Low-Confidence Refund Queue Visibility
Given I am a signed-in user with access to Refund Reconcile on a mobile device And there exist refund, partial credit, or chargeback events with confidence < 0.80 When I open the Reconciliation Review queue Then I see only those low-confidence events in a mobile-optimized list And each item shows: original invoice ID (tap-to-open), client name, project, gross amount, fees, net amount, type (refund/credit/chargeback), event date, invoice date, confidence score, and status And each item exposes an expandable event timeline showing payment, refund, and adjustment events in chronological order And tapping an item opens a detail drawer with the same fields plus chargeback reason (if applicable)
Quick Actions: Confirm, Reassign, Split, Ignore
Given I am viewing a queue item When I tap Confirm Match Then the item is marked Confirmed, removed from the queue, and a confirmation toast appears within 300 ms And the note is optional and the reason code defaults to Confirmed When I tap Reassign to Another Invoice, search/select a target invoice by invoice ID, client, date, or amount, and submit with a mandatory note and reason code Then the refund is linked to the selected invoice, the item is removed from the queue, and a success toast appears When I tap Split Across Invoices, allocate exact amounts to multiple invoices (minimum 2) such that the sum equals the absolute refund amount to the cent, specify fee allocation, and provide a mandatory note and reason code Then save is enabled only when totals match exactly and, on save, links are created per allocation and the item is removed from the queue When I tap Ignore and provide a mandatory note and reason code Then the item is excluded from income calculations and moved to the Ignored tab with the captured reason
Recalculations and Downstream Updates
Given a Confirm, Reassign, Split, or Ignore action is saved When the system processes recalculations Then client YTD and lifetime totals, project P&L, and the 1099 Watchlist reflect the change within 60 seconds And affected exports (tax packet, CSV, 1099) are flagged stale and regenerated automatically on next export request with timestamps later than the override And the event timeline records the override with user, timestamp (UTC), note, and reason code And the UI shows Processing within 300 ms of save and Completed on finish or an error banner on failure And repeated submission of the same action does not duplicate effects (idempotent)
Search, Filters, and Bulk Approvals
Given the review queue contains 50 or more items When I search by invoice ID, client name, amount range, or date range Then results return and render within 2 seconds and highlight matched terms When I apply filters for confidence band (0–50, 50–80, 80–90, 90+), type (refund/credit/chargeback), date range, client, project, status (New/Processing/Ignored), and amount range Then only matching items are shown and active filter chips are visible When I select multiple items and choose Bulk Confirm Then up to 100 eligible items are confirmed in one action, items requiring split or reassign are excluded with a warning, and an optional note/reason code can be applied to all And upon completion I see a summary of successes and failures, and failed items remain with actionable error messages
Role-Based Permissions and Audit Trail
Given team roles Viewer, Reviewer, and Admin are configured When a Viewer accesses the queue Then they can view items and details but cannot perform confirm, reassign, split, ignore, or bulk actions (buttons disabled and API returns 403 if invoked) When a Reviewer accesses the queue Then they can perform confirm, reassign, split, and ignore on single items with required notes/reason codes where applicable When an Admin accesses the queue Then they can perform all Reviewer actions plus bulk approvals And every action writes an audit record containing action type, item ID, before/after invoice associations and amounts, note, reason code, timestamp (UTC), actor user ID, and device info, retrievable by Admins via Audit Log view
Mobile Performance and Responsiveness
Given a mid-tier mobile device (2019 or newer) on 4G (400 ms RTT, 1.5 Mbps) When I open the Reconciliation Review queue with at least 100 items Then time-to-first-actionable list (first 20 items, controls interactive) is ≤ 2.5 seconds And scrolling the list maintains ≥ 55 fps with no frame drops > 100 ms And tapping any quick action opens its sheet/drawer within 300 ms and shows immediate touch feedback within 100 ms And the layout remains usable on viewports as small as 360×640 without horizontal scrolling
Concurrency and Conflict Handling
Given I open an item in the review queue And another user or background job modifies or resolves the same item before I save my action When I attempt to save Confirm, Reassign, Split, or Ignore Then the system detects a version conflict, prevents applying my changes, shows a Conflict Detected modal with options to View Latest and Refresh, and logs the conflict event And after Refresh the item reflects the latest state or is removed if already resolved And no duplicate or partial updates are applied
Compliance Audit Trail & Export
"As a freelancer, I want an audit trail and export of all refund-related adjustments so that I can substantiate my records for the IRS or my accountant."
Description

Record an immutable audit trail for each refund and chargeback, including timestamps, actor, source payload references, pre/post balances, allocation decisions, and attached evidence. Generate annotated, IRS-ready exports (PDF and CSV) that summarize adjustments per client, invoice, and project, highlighting YTD impacts and 1099-relevant totals. Provide API access to the audit artifacts and ensure data retention policies align with tax record-keeping requirements.

Acceptance Criteria
Immutable Audit Trail on Refund Creation
Given a refund is recorded against an invoice via UI or API with idempotency key K and attached evidence E When the refund is saved Then an audit entry is created with fields: audit_id (UUID), event_type=refund.created, timestamp (UTC ISO 8601), actor (user_id/service_id), source_payload_id or hash, original_invoice_id, client_id, project_id, currency, pre_balance.invoice, post_balance.invoice, pre_balance.project_pnl, post_balance.project_pnl, pre_balance.client_ytd_income, post_balance.client_ytd_income, allocation[{line_item_id, amount, rationale}], evidence_ids, and 1099_impact_delta And the audit entry is immutable; any direct update attempt returns 409 and no data is altered And a content_hash and previous_hash are stored to form a tamper-evident chain And creating the same refund with the same idempotency key K results in a single audit entry (no duplicates) And the audit entry is queryable by invoice_id and client_id within 5 seconds of creation
Partial Credit Allocation and P&L Recalculation
Given a partial credit of amount X is applied to invoice I with line-level allocations across multiple project line items and the workspace has USD base currency with FX rate R for the transaction date When the credit is saved Then project P&L, invoice balance, client YTD income, and 1099 watchlist totals are recalculated using allocation amounts and FX rate R with rounding to 2 decimals (bankers rounding) And pre/post balances and allocation decisions are recorded in the audit entry with rationale text limited to 280 characters per allocation And the sum of allocation amounts equals X; otherwise the save is rejected with HTTP 422 and a structured error payload And the audit entry references the superseded balances via previous_entry_id when reallocations occur later
Chargeback Ingestion via Bank Feed Webhook
Given a chargeback notification is received from the bank feed webhook with provider_ref PR that matches a settled payment tied to invoice I When the webhook is processed Then a chargeback audit entry is created with event_type=chargeback.created, actor=system:webhook/bank, source_payload_id=PR, and a normalized reason_code And the entry links to invoice I and original payment_id and adjusts post balances and YTD/1099 deltas accordingly And duplicate webhook deliveries with the same PR are idempotently ignored (no duplicate audit entries) And evidence retrieval status (requested/pending/received) is tracked and updated via subsequent audit entries And the processing time from receipt to persisted audit entry is under 10 seconds for at least 95% of events
IRS-Ready Annotated Export (PDF and CSV)
Given a user with Export permissions requests an Audit Export for date range D, format=PDF and CSV, with filters by client C and project P When the export job completes Then the CSV contains one row per adjustment with required columns: audit_id, event_type, timestamp, actor, client_id, project_id, invoice_id, amount_delta, currency, ytd_income_delta, is_1099_relevant, allocation_summary, evidence_count, source_payload_ref, pre_invoice_balance, post_invoice_balance And the PDF includes a per-client, per-invoice, and per-project summary with YTD impacts and 1099 totals highlighted, plus an evidence index referencing audit_ids And CSV header names match the documented schema exactly and values are UTF-8, comma-delimited, RFC 4180 compliant And totals in the export tie out to internal ledger totals for the same filters within ±0.01 per currency unit And files are named TaxTidy_AuditExport_{workspace}_{D_start}_{D_end}_{timestamp}.{pdf|csv} and are downloadable within 60 seconds for up to 10,000 audit events And each export is itself logged as an audit event with event_type=audit_export.created
Audit Artifacts API Access and Filtering
Given an API client with scope audit:read and role permissions for workspace W requests GET /audit/v1/events with filters (date range, event_type in [refund.created, chargeback.created], client_id, invoice_id) and pagination (limit, cursor) When the request is processed Then the response is 200 with a JSON array of events conforming to the published schema, including evidence_signed_urls expiring in 15 minutes or less, and a next_cursor when more results exist And unauthorized requests return 401; insufficient permissions return 403; malformed filters return 400; requesting a non-existent id returns 404 And results are sorted by timestamp desc by default and are consistent under pagination (no duplicates or skips across pages) And responses include ETag headers and support If-None-Match with 304 for unchanged results within a 60-second cache window And the API enforces a rate limit of at least 60 requests per minute per token and returns 429 with retry-after when exceeded
Data Retention, Legal Hold, and Deletion
Given workspace W has default audit retention set to 7 years and a legal hold can be toggled per client or tax year When an audit event reaches its retention expiration without legal hold Then the event and associated evidence files are hard-deleted, and a non-content tombstone record with audit_id and deletion_timestamp is retained for chain continuity And legal hold prevents deletion and is itself auditable with event_type=legal_hold.applied and legal_hold.removed And an admin can export a complete set of audit artifacts for a given tax year even if associated invoices were archived And all audit artifacts are encrypted at rest with AES-256 and in transit via TLS 1.2 or higher, with encryption keys rotated at least annually

Exhibit Index

Auto‑builds an indexed table of exhibits that ties every deduction back to its receipt, bank transaction, and invoice in one glance. Each exhibit gets a consistent label (A1, A2…) and cross‑references that match the PDF’s page anchors, so auditors and clients can jump straight to the exact proof without hunting. Saves review time and makes your packet feel professional and court‑ready.

Requirements

Exhibit ID Schema & Re-indexing
"As a freelancer preparing taxes, I want every exhibit to have a clean, consistent label that stays correct when I add or remove items so that my packet looks professional and I don’t have to manually renumber things."
Description

Define a deterministic alphanumeric labeling scheme (e.g., A1, A2, …) that assigns a unique, human-readable Exhibit ID to every deduction’s evidence bundle and remains stable across edits. The system must automatically re-index and resolve collisions when exhibits are added, removed, or reordered, preserving cross-references wherever possible. Labels are derived from a canonical sort (e.g., date, amount, vendor) to ensure repeatable output, and mappings are stored so regenerated packets reproduce the same labels unless content materially changes. The labeling service integrates with TaxTidy’s document store and expense records, supports batch operations, and exposes an API for the PDF generator and UI to request current labels. Outcome: professional, consistent labels that don’t require manual renumbering and won’t drift during packet updates.

Acceptance Criteria
Deterministic Canonical Sort and Label Pattern
Given a packet with N deductions and associated evidence bundles When labels are generated Then exhibits are ordered by: (1) transaction_date (UTC) ascending, (2) absolute_amount descending, (3) vendor_name case-insensitive ascending, (4) exhibit_uuid ascending And each exhibit receives a unique label matching the pattern ^[A-Z][0-9]+$ with default prefix 'A' and numeric sequence starting at 1 with no gaps within the packet And the label-to-exhibit mapping includes label, exhibit_id, sort_index, and version_id
Label Stability Under Non‑Material Edits
Given labels have been generated for a packet When fields not used in the canonical sort (e.g., memo, category, note text, tags) are modified for any exhibit Then 0 existing labels change and version_id increments And when secondary evidence files are added without changing canonical sort keys Then the label for that exhibit remains unchanged
Re‑indexing on Insert/Delete/Reorder with Cross‑Reference Preservation
Given a packet labeled A1..A10 When a new exhibit whose canonical sort position falls between A3 and A4 is added Then only labels A4..A10 are renumbered to A5..A11 and A1..A3 remain unchanged And all cross-references in the UI and generated PDF anchors update atomically to the new labels with no broken links And when an exhibit is deleted at the end of the sequence Then no other labels change And when an exhibit’s canonical sort keys change Then only labels in the affected contiguous range are renumbered
Collision Handling and Deterministic Tie‑Breakers
Given two or more exhibits share identical canonical sort keys (same date, amount, vendor) When labels are generated Then their relative order is resolved by ascending exhibit_uuid as a deterministic tie-breaker And labels remain stable across regenerations until one of the tie-breaker inputs changes And no duplicate labels exist within a packet
Mapping Persistence and Regeneration Consistency
Given a stored mapping for packet P at version V When labels are regenerated with the same exhibits and unchanged canonical keys Then labels and exhibit_id associations exactly match version V (checksum identical) And when a material change occurs (add, delete, or change to canonical sort keys) Then only changed or new exhibits receive different labels; unchanged exhibits retain their labels And a change log lists old_label -> new_label for all affected exhibits And given the same inputs and configuration across environments Then the same labels are produced
API and Batch Operations for Label Retrieval
Given API consumers request current labels for a packet When GET /v1/labels?packetId={id} is called Then the service returns 200 with fields: label, exhibit_id, sort_index, cross_refs {receipt_id, bank_txn_id, invoice_id}, pdf_anchor, and version_id And responses include ETag; when If-None-Match matches current ETag Then the service returns 304 with no body And when POST /v1/labels/batch is called with up to 10,000 exhibits Then labeling completes idempotently within 5 seconds p95 and concurrent calls return the same mapping without race conditions And all operations are logged with correlation IDs
PDF Anchors & Internal Cross-Links
"As an auditor reviewing a TaxTidy packet, I want each index entry to jump to the exact receipt page so that I can verify deductions without hunting."
Description

Embed named destinations and bookmarks within the generated PDF so each exhibit label (A1, A2, …) maps to a precise page anchor. The Exhibit Index table must include clickable internal links to these anchors and show accurate page numbers. Cross-links from index entries to original artifacts (receipt image, bank transaction, invoice) must be available as URL hyperlinks that open the corresponding item in TaxTidy, with graceful behavior offline. Links must be robust across PDF viewers (desktop, mobile, web), meet accessibility guidelines (readable link text, logical order), and be validated during export. Outcome: one-click navigation from the index to the exact proof page for auditors and clients.

Acceptance Criteria
One-click index link navigates to exact exhibit page anchor
Given a generated PDF with an Exhibit Index, When the user clicks the index entry for label "A1", Then the viewer navigates to the exact page anchor named for A1 and displays the exhibit heading within the initial viewport. Given any exhibit index entry, When its link is activated, Then the destination page number matches the page number shown in the index entry. Given multiple exhibit entries, When each index link is tested, Then 100% of links resolve to their corresponding named destinations without error.
Exhibit Index shows correct page numbers and consistent labels
Given exhibits labeled sequentially (A1…An), When the PDF is generated, Then labels in the index, page anchors, and bookmarks are identical and unique. Given each index row, When pagination is finalized, Then the page number displayed equals the 1-based page of the named destination in the final PDF output. Given the packet is regenerated after content changes, When the index is rebuilt, Then all page numbers update accordingly with zero mismatches found by the export validator.
Bookmarks and named destinations are embedded and work across common viewers
Given the exported PDF, When opened in Adobe Acrobat DC, Apple Preview (macOS), Chrome PDF viewer (desktop), iOS Files viewer, and a common Android PDF viewer, Then index links and bookmarks navigate to the correct destinations in all tested viewers. Given the PDF outline, When expanded, Then it contains a top-level "Exhibit Index" and child entries for each exhibit label (A1, A2, …) in logical order. Given the PDF is preflighted with a standards validator, When checked, Then named destinations and bookmarks use standards-compliant syntax with no validation errors related to internal links.
Cross-links to source artifacts open correct items in TaxTidy
Given an index entry with artifact links (Receipt, Bank Transaction, Invoice), When the user clicks a "Receipt" hyperlink, Then the default browser opens a TaxTidy HTTPS deep link that displays the exact receipt detail. Given the user is authenticated to TaxTidy, When the deep link is opened, Then the artifact detail loads successfully within 2 seconds on a stable broadband connection. Given the user is not authenticated, When the deep link is opened, Then the user is prompted to sign in and, after successful authentication, is redirected to the intended artifact detail. Given the artifact is missing or access is denied, When its deep link is opened, Then a friendly error page is shown with a reference ID and a link to support.
Graceful offline behavior for artifact hyperlinks
Given the device is offline, When an artifact hyperlink is activated, Then the browser displays an offline page that preserves the target URL and offers a Retry action. Given intermittent connectivity, When a deep link request fails within 10 seconds, Then the client retries once and, if still failing, shows a clear retry option without breaking the PDF link. Given connectivity is restored, When the user retries, Then the original artifact page opens without requiring the user to reselect the link in the PDF.
Accessibility of index links and reading order
Given the Exhibit Index, When read with a screen reader, Then each link has descriptive text that includes the exhibit label and artifact type (e.g., "A1 receipt link") so purpose is clear without surrounding context. Given the PDF tag structure, When tested with an accessibility checker, Then the index is tagged as a table with TH/TD cells, links are tagged as <Link>/<Reference>, and the reading order matches the visual order. Given linked text styling, When contrast is measured, Then link text meets WCAG 2.1 AA contrast (≥4.5:1) and links are visually distinguishable by more than color (e.g., underline).
Export validation prevents broken anchors and links
Given the export process, When generating the PDF, Then it verifies that each index link maps to an existing named destination and that each artifact URL responds with a valid status (200/302 or auth redirect) via a HEAD/GET check. Given any missing destination or failing URL, When detected during validation, Then the export fails with an error report listing the specific labels and issues, and no PDF is delivered. Given validation passes, When export completes, Then a validation summary (counts of anchors, bookmarks, internal links, external links) is logged and embedded in document metadata for diagnostics.
Evidence Mapping Engine
"As a solo consultant, I want TaxTidy to automatically pair each deduction with its receipt, bank transaction, and invoice so that my exhibits are complete without manual matching."
Description

Automatically assemble and maintain a complete mapping between each deduction and its evidentiary components: receipt photo(s), bank transaction(s), and invoice(s). Support 1:N relationships, split transactions, multi-currency normalization, and de-duplication using content hashes and heuristics (amount, date, vendor). Enforce required fields (date, amount, vendor/category) and flag mismatches or missing items. Persist mappings with referential integrity in TaxTidy’s datastore and expose them via a query API for index generation, PDF export, and UI. Provide reconciliation logic to suggest likely matches from bank feeds and invoices and surface unmatched items for user review. Outcome: every exhibit is complete and cross-referenced, enabling accurate index rows and links.

Acceptance Criteria
Reconciliation Suggestions and Unmatched Surfacing
Given a deduction with amount, date, and vendor/category populated and bank feed/invoice data imported When the reconciliation job runs Then the engine returns up to 5 candidate evidence bundles (receipts, bank transactions, invoices) per deduction with a confidence_score in [0,1] and only includes candidates with confidence_score ≥ 0.70 And each candidate includes matching_features: amount_delta, date_delta_days, vendor_similarity And candidates must satisfy: |date_delta_days| ≤ 7 and amount_delta ≤ max(1.00, 0.01 × deduction_amount) And all unmatched items (receipts, bank transactions, invoices) are surfaced with reason codes in {NO_VENDOR_MATCH, AMOUNT_VARIANCE, DATE_OUT_OF_WINDOW, NO_CORRESPONDING_DOCUMENT} And the reconciliation completes in ≤ 5 seconds for a batch of 500 deductions on reference hardware
Required Fields Enforcement and Mismatch Flagging
Given a request to create or update a deduction mapping When the deduction is missing any of {date, amount, vendor_or_category} Then validation fails and the deduction is marked invalid with error codes in {MISSING_DATE, MISSING_AMOUNT, MISSING_VENDOR_OR_CATEGORY} Given a valid deduction and one or more linked evidence items When the mapping is saved Then the engine validates: (a) sum(linked_evidence.amount_normalized) ≈ deduction.amount within tolerance max(1.00, 0.01 × deduction.amount); (b) each evidence date within ±7 days of deduction date; (c) vendor/category consistency with vendor_similarity ≥ 0.70 or an explicit approved alias And any violation creates flags with codes in {AMOUNT_MISMATCH, DATE_MISMATCH, VENDOR_MISMATCH} And flagged mappings remain saved but marked needs_review = true
1:N Relationships and Split Transactions
Given a single bank transaction T amount = 300.00 When T is split into three deductions D1, D2, D3 of 100.00 each and mappings are saved Then the datastore persists T -> {D1, D2, D3} and sum(amount(D1..D3)) = amount(T) within 0.01 And each Dn retains a reverse link to T And reconciliation does not suggest T for other deductions beyond any remaining unallocated amount = 0.00 Given a deduction D linked to multiple evidence items {R1, R2 receipts} and {I1 invoice} When the mapping is saved Then the mapping persists D -> {R1, R2, I1} including per-evidence allocated_amounts whose sum equals D.amount within 0.01 And the query API returns the relationship types and allocated_amounts
De-duplication via Content Hash and Heuristics
Given two uploaded receipts with identical file hash or perceptual_hash_distance ≤ 5 When receipts are ingested Then the system retains a single canonical receipt record, links duplicates to the canonical, and prevents duplicate attachment to any mapping And heuristics mark items as potential duplicates when {amount, date, vendor} all match within tolerances; such items are merged automatically if confidence ≥ 0.90, otherwise flagged DUPLICATE_CANDIDATE for review And deduplication is idempotent: re-ingesting the same artifacts does not create new receipt records And an audit trail is stored with dedup decision, timestamp, actor, and inputs used
Multi-currency Normalization
Given a deduction in base currency USD and linked evidence in a non-USD currency When the mapping is saved Then each evidence item stores original_currency, original_amount, fx_rate, fx_source, fx_date and computes amount_normalized_usd = round_half_even(original_amount × fx_rate, 2) And fx_rate is sourced from the configured provider (e.g., ECB) for the evidence date or the most recent prior business day And the sum of amount_normalized_usd across evidence equals the deduction amount within tolerance max(1.00, 0.01 × deduction.amount) And if an FX rate is unavailable, the mapping is saved with flag FX_UNAVAILABLE and needs_review = true
Persistence and Referential Integrity
Given a mapping M referencing deduction D and evidence items E ∈ {receipts, bank_transactions, invoices} When M is saved Then all foreign keys must exist; otherwise the operation fails with no partial writes and returns a validation error And the save operation is atomic (ACID) within a single transaction And when a referenced item is soft-deleted, M is marked ORPHAN_REFERENCE and excluded from index generation until resolved And when a referenced item is hard-deleted with cascade enabled, M and its join records are removed consistently with no dangling references
Query API Completeness and Performance
Given a request GET /mappings?deduction_id={id} When executed Then the response is 200 OK with JSON containing: deduction, receipts[], bank_transactions[], invoices[], allocated_amounts, normalized_amounts, currencies, fx metadata, validation_flags, dedup_canonical_ids, and created_at/updated_at timestamps And results are ordered by evidence_type ASC then evidence_date ASC and support pagination via limit and cursor with total_count And for a payload of 100 mappings, p95 latency ≤ 300 ms and for 5,000 mappings, p95 latency ≤ 1.5 s on reference hardware And the endpoint is idempotent and returns ETag/Last-Modified headers for caching
Exhibit Index Table Renderer
"As a freelancer, I want a clean, readable table of exhibits at the top of my packet so that I can present organized proof of every deduction in one glance."
Description

Generate a formatted, readable Exhibit Index at the front of the packet and an equivalent in-app view. Include columns for Exhibit ID, date, vendor, amount, category, page number(s), and artifact links. Ensure consistent typography, compact layout for one-glance scanning, and responsive behavior for mobile. Support sorting (ID/date/amount), pagination for large sets, footnotes for split transactions, and stable page numbering synchronized with PDF anchors. Integrate with branding and export profiles (PDF/A when required). Optimize for performance on packets with hundreds of exhibits. Outcome: a professional, court-ready table that summarizes and links every deduction’s proof.

Acceptance Criteria
Front-of-Packet PDF Exhibit Index Generation & Branding Compliance
Given a packet with 10+ exhibits and export profile "Standard" When exporting to PDF Then the Exhibit Index appears immediately after the cover page and before any exhibits And the table includes columns exactly: Exhibit ID, Date, Vendor, Amount, Category, Page No(s), Artifact Links And each row has a unique Exhibit ID in the format A<number> starting at A1 and incrementing by 1 with no gaps within the packet And currency amounts are formatted to two decimals with currency symbol per locale And the table typography uses brand tokens (body font family, 11pt; header 12–13pt bold); Amount is right-aligned; other columns are left-aligned And row height is compact (<= 28pt) producing at least 36 rows per US Letter page at 100% scale And column widths ensure Exhibit ID and Amount are never truncated; long Vendor names truncate with ellipsis and expose full value via tooltip/alt text And the PDF uses active brand theme colors and embeds the brand logo per export profile When export profile "PDF/A-2b" is selected Then the exported PDF validates as PDF/A-2b with 0 errors in VeraPDF, with all fonts and color profiles embedded, and hyperlinks preserved
In-App Exhibit Index View Parity & Artifact Linking
Given the same dataset used for the PDF When opening the in-app Exhibit Index Then the row count equals the PDF index and columns appear in the order: Exhibit ID, Date, Vendor, Amount, Category, Page No(s), Artifact Links And each row shows up to three artifact links: Receipt, Bank Transaction, Invoice; missing artifacts display a disabled icon with tooltip "Not available" When clicking a Receipt link Then the receipt preview opens in an in-app panel within 300 ms When clicking a Bank Transaction or Invoice link Then the respective record opens in its detail view within 500 ms And Exhibit ID, Date (per profile format), Vendor, Amount, Category, and Page No(s) values match the PDF exactly for all rows And all artifact links resolve successfully (HTTP 200/OK or in-app navigation success); failures surface a non-blocking error toast and retain context
Sorting and Pagination Controls at Scale
Given 500 exhibits in the index When the user sorts by Exhibit ID ascending/descending Then rows reorder correctly within 300 ms on desktop and 500 ms on mid-tier mobile When the user sorts by Date ascending/descending and by Amount ascending/descending Then rows reorder correctly within the same latency thresholds And sort is stable; for equal primary values, Exhibit ID ascending is used as a deterministic tie-breaker And the active sort column displays a visible indicator with direction arrow Given pagination with default page size 50 When navigating Next/Prev/First/Last Then the correct rows are displayed with no duplicates or omissions, and the page indicator shows "Page X of Y" And the selected sort order persists across page navigation
Footnotes for Split Transactions
Given an exhibit derived from a split bank transaction When rendering the Exhibit Index (PDF and in-app) Then a superscript footnote marker appears next to the Exhibit ID And a Footnotes section appears at the end of the index (or bottom of the view in-app) listing for each marker: parent transaction ID, original amount, split amount applied to this exhibit, and the other categories/exhibits receiving splits And footnote markers are unique and sequential (1, 2, 3...) and link from marker to footnote and back (PDF and in-app) And if no split transactions are present Then no footnote section is rendered
Stable Page Numbering and PDF Anchor Integrity
Given exhibits that include single- and multi-page artifacts When exporting to PDF Then the Page No(s) column shows a single page number for single-page proofs or a range (e.g., 12–14) for multi-page proofs And each Page No(s) value is a clickable link that navigates to the first page of the corresponding artifact in the PDF And the PDF contains named anchors using the pattern exhibit_a<number> that match the Exhibit ID and remain unchanged across exports with identical data and ordering And re-exporting the packet with the same dataset yields identical Page No(s) values for all rows And a 20-item random sample of index entries navigates 100% to the correct proof pages
Responsive Mobile Layout for One-Glance Scanning
Given a mobile device viewport width ≤ 414 px When viewing the in-app Exhibit Index Then the layout switches to a stacked two-line per row format: Line 1 shows Exhibit ID • Vendor • Amount; Line 2 shows Date • Category • Page No(s); artifact icons align right And a sticky summary bar replaces the full header; horizontal scrolling is not required to see these fields And tap targets for artifact links and row interactions are ≥ 44×44 px with a visible pressed state And text remains legible with minimum 11pt equivalent; truncation uses ellipses with accessible tooltips for full values And the mobile view renders within 1.5 s on a mid-tier device and maintains ≥ 50 FPS while scrolling
Rendering Performance on Large Packets
Given a packet containing 1,000 exhibits and 1,500 total artifacts When generating the in-app Exhibit Index Then first contentful paint occurs within 1.2 s on desktop and within 2.0 s on mid-tier mobile, and sort/page interactions respond within 300 ms When exporting the same packet to PDF (Standard profile) Then the PDF generation completes within 10 s server-side without timeouts, and the index contributes ≤ 2% to the total file size And peak server memory for index generation remains below 1 GB And no client UI thread block exceeds 50 ms during rendering as measured by performance instrumentation
Audit Preview & Integrity Checks
"As a user about to send my packet to a client or auditor, I want to see and fix any broken links or missing proofs so that I’m confident everything will check out."
Description

Provide a pre-export audit mode that validates the Exhibit Index and its references: detect broken or missing links, unmatched deductions, amount mismatches between receipt/transaction/invoice, duplicate exhibits, and stale page anchors. Present actionable warnings and auto-fix suggestions, with deep links to edit the offending items. Block export on critical failures and include an optional Validation Report section in the packet. Run checks automatically on export and incrementally on changes to keep the index trustworthy. Outcome: users ship error-free, audit-ready packets with confidence.

Acceptance Criteria
Broken or Missing Exhibit Links Detected in Audit Preview
Given an Exhibit Index where each exhibit must reference a receipt, a bank transaction, and (when applicable) an invoice When Audit Preview runs Then any exhibit with a missing required reference is flagged as Critical with issue code LINK_MISSING and shows the exhibit label and missing reference type And any exhibit whose reference points to a deleted or inaccessible record is flagged as Critical with issue code LINK_BROKEN And the issues summary displays the total count of link issues and affected exhibits And clicking an issue highlights the corresponding exhibit in the Audit Preview list
Amount Consistency Across Receipt, Transaction, and Invoice
Given exhibits with monetary amounts sourced from receipt totals, bank transaction amounts (or split amounts), and invoice paid amounts When Audit Preview runs Then any absolute difference greater than 0.01 between the exhibit amount and any linked source is flagged as Critical with issue code AMOUNT_MISMATCH and shows the compared values and delta And if a bank transaction is split, the sum of referenced splits must equal the exhibit amount within 0.01, else flag as Critical with issue code SPLIT_MISMATCH And currency codes across linked sources must match, else flag as Critical with issue code CURRENCY_MISMATCH And exhibits with no monetary source found are flagged as Critical with issue code AMOUNT_SOURCE_MISSING
Duplicate Exhibits and Label Collisions
Given a generated labeling scheme (A1, A2, …) where each label must be unique per packet When Audit Preview runs Then any duplicate label is flagged as Critical with issue code LABEL_DUPLICATE and shows the conflicting exhibit IDs And if two or more exhibits reference the same set of underlying documents (receipt, transaction, invoice), flag as Warning with issue code EXHIBIT_DUPLICATE_CONTENT and list duplicates And an auto-fix suggestion “Re-label conflicts” is available that assigns the next available label sequence without gaps
Stale PDF Page Anchors Detection and Regeneration
Given each cross-reference includes a page anchor target within the assembled PDF When Audit Preview runs Then anchors that do not resolve to a valid page/element are flagged as Critical with issue code ANCHOR_STALE And a single-click auto-fix “Regenerate Anchors” is available and re-computes anchors for all affected exhibits And after regeneration, test navigation from the index to each target succeeds for 100% of exhibits And verifying 1,000 anchors completes in under 3 seconds on desktop and 5 seconds on mobile (p95)
Actionable Warnings with Deep-Link Fix Flow
Given an issue is listed in Audit Preview with a suggested fix When the user taps “Fix” or “Review” Then the app deep-links to the precise editor view for the offending item, pre-filtered and focused within 2 seconds (p95) And after applying a fix, only affected checks re-run and the issue list updates within 1 second (p95) And where an auto-fix is available, the user can preview the change and confirm/undo; applied auto-fixes are logged and visible in the Validation Report
Export Blocking on Critical Validation Failures
Given the user initiates Export Packet When Critical issues exist Then export is blocked and a modal lists all Critical issues with counts and links to fix; no override is offered And when only Warnings remain, export proceeds And the user can toggle “Include Validation Report” before export; when enabled, a Validation Report section is appended to the packet with issue summary and resolutions
Automatic and Incremental Validation with Optional Report
Given the user edits any exhibit, receipt, bank transaction, invoice, label, or attachment When the change is saved Then incremental validation runs for impacted exhibits only and completes within 500 ms per change (p95) for up to 5,000 exhibits And starting an export always triggers a full validation pass before generation And the Validation Report includes timestamp, check versions, counts by severity, and detailed rows (exhibit label, issue code, fields, before/after values for auto-fixes)
Incremental Update Sync & Versioning
"As a mobile-first user, I want the exhibit index to stay up to date as I add items throughout the week so that I don’t need to rebuild the whole packet."
Description

Keep the Exhibit Index, labels, page anchors, and links up to date as users add receipts or edit transactions without requiring a full packet rebuild. Perform incremental regeneration, preserving stable IDs/URLs when possible and updating only affected sections. Maintain versioned exports with timestamps and allow comparison of index changes between versions. Ensure concurrency safety across devices, show progress, and notify users upon completion. Outcome: always-fresh indexes for mobile-first workflows with minimal waiting and no broken references.

Acceptance Criteria
Add Receipt on Mobile — Incremental Exhibit Index Refresh
Given an existing Exhibit Index with labeled exhibits (e.g., A1…A200) and stable URLs/anchors When the user uploads a new receipt via the mobile app and it is auto-matched to a transaction Then only the affected exhibit entry/entries and any impacted summary sections are regenerated (no full packet rebuild) And unaffected exhibit labels and URLs remain unchanged (≥99.5% retention for unaffected items) And the new exhibit receives the next correct label according to ordering rules and appears in the index within 5 seconds for datasets ≤2,000 exhibits (≤12 seconds for ≤10,000 exhibits) And all new/updated index links navigate to the correct PDF anchors with zero broken links in automated link-check validation
Edit Bank Transaction Category — Preserve Labels and Update Links
Given an exhibit already ties a receipt to a bank transaction and invoice with label A37 and stable URL When the user edits the transaction’s category and memo on any device Then the Exhibit Index reflects the new category/memo in the row without changing the exhibit label or URL And the PDF anchor for A37 remains valid; if pagination shifts, the anchor is updated and link still resolves correctly And no additional exhibits are relabeled or reordered And automated validation shows 0 broken references for all links associated with A37
Cross‑Device Concurrent Updates — Concurrency Safety and Merge
Given the same user is signed in on two devices (mobile and web) with the index up to date When Device A adds a new receipt and Device B simultaneously edits a transaction within a 60‑second window Then changes are merged without data loss or duplicate exhibits And label assignment remains globally unique and deterministic (no collisions, no gaps beyond expected ordering) And the resolved state propagates to both devices within 10 seconds of last sync completion And any true conflicts (same field edited differently) present a single clear resolution prompt; user choice is applied atomically with no broken links
Versioned Export — Timestamped PDFs with Index Change Diff
Given a previously exported packet version v1 exists with ISO‑8601 timestamp and immutable share URL When the user exports a new packet after incremental updates Then a new version v2 is created with a unique version ID and timestamp, and v1 remains accessible And a Compare view lists index changes between v1 and v2 (added/removed/modified exhibits) including label, page, and link deltas And users can download any prior version; links inside each version remain immutable and valid And the version history records who initiated the export and the source device
Stable IDs/URLs — No Broken References After Incremental Regeneration
Given public/ sharable exhibit URLs and in‑PDF anchors are in use by clients/auditors When incremental regeneration occurs due to data edits or new receipts Then all unchanged exhibits retain the same IDs, labels, and URLs And any changed anchors provide automatic redirects from prior URLs to new targets for at least 12 months And automated link‑check across the packet confirms 100% of index entries resolve (HTTP 200/OK or valid in‑PDF jump) with 0 broken references
Progress & Notifications — Background Sync with User Feedback
Given incremental regeneration may run in the background on mobile and web When an update is triggered (manual or auto) Then a non‑blocking progress indicator displays percentage and current stage (e.g., indexing, anchoring, linking) And users can continue normal app use without degraded responsiveness beyond 10% UI latency increase And users receive a completion or failure notification (push on mobile, in‑app toast on web); failures include retriable action and error code And progress and notifications are accessible (screen reader labels, sufficient contrast)
Performance Threshold — Incremental vs Full Rebuild Timing
Given representative datasets of 500, 2,000, and 10,000 exhibits When a single receipt is added or a single transaction is edited Then incremental regeneration completes in ≤2s (500), ≤5s (2,000), and ≤12s (10,000) on a typical broadband connection And the incremental path uses ≤25% of the CPU time and ≤30% of the network I/O compared to a full rebuild for the same change And no full rebuild is invoked unless structural invariants are violated; such fallbacks are logged with reason codes

TraceLink QR

Embeds a tamper‑aware QR and deep link on each exhibit and summary page. Scanning opens the exact receipt image, ledger entry, and source metadata in TaxTidy for instant verification—no inbox digging. Works offline with a verification code printed in the packet, giving auditors confidence and you fewer back‑and‑forth emails.

Requirements

Tamper-Aware QR Generation
"As a freelancer compiling my tax packet, I want each page to include a secure QR tied to its exact contents so that anyone can instantly verify the right receipt and entry without manual searching."
Description

Generate a unique, tamper-aware QR for every exhibit and summary page in a TaxTidy packet. Each QR encodes a short deep link plus a signed payload containing packet ID, exhibit ID, content hashes (receipt image and ledger entry), timestamp, and version. Use high-error-correction QR with vector rendering for print/PDF, ensure legibility at 300–600 DPI, and include a human-readable short code adjacent to the QR. Provide SDK/service to produce the signature using asymmetric keys and support key rotation and payload versioning.

Acceptance Criteria
Unique QR Generation per Exhibit and Summary Page
Given a packet containing N exhibit pages and M summary pages When the packet is generated Then each exhibit page and each summary page contains exactly one QR And no two QRs in the packet share the same signed payload And each QR payload references the correct packetId and the page's exhibitId (or summaryId)
Signed Payload Composition Completeness
Given QR code data decoded from any page When the payload is parsed Then it contains: packetId, exhibitId (or summaryId), receiptImageHash (SHA-256), ledgerEntryHash (SHA-256), timestamp (ISO 8601 UTC), payloadVersion, signature And it embeds a short deep link URL And no additional PII fields are present And the total encoded payload length is ≤ 1024 bytes
Cryptographic Signature Verification and Tamper Detection
Given a payload and signature produced by the signing service with the current private key When verification is performed using the corresponding public key Then verification succeeds When any single bit of the payload or deep link is modified Then verification fails And verification fails when using a non-corresponding public key
Vector QR Rendering and Print Scannability (300–600 DPI)
Given the generated packet PDF for print When inspecting any page's QR Then the QR is embedded as vector (PDF paths/SVG) without rasterization And error correction level is set to H And the quiet zone is at least 4 modules And minimum printed size is ≥ 24 mm × 24 mm at 300 DPI And when printed at 300–600 DPI on standard paper, the QR scans successfully on iOS and Android test devices with ≥ 95% success over 20 attempts per device per page
Deep Link Resolution to Exact Assets and Integrity Indication
Given a valid QR is scanned with network connectivity When the deep link is opened Then TaxTidy loads the exact receipt image, ledger entry, and source metadata for the referenced packetId and exhibitId (or summaryId) And the displayed assets' computed hashes match the hashes in the payload And the viewer indicates "Signature Verified" when verification passes and "Tamper Detected" when it fails And access is scoped read-only to the referenced exhibit or summary
Human-Readable Short Code Adjacent to QR
Given any page containing a QR When viewing the printed or PDF page Then a human-readable short code (8–10 alphanumeric characters, grouped e.g., XXXX-XXXX) is printed adjacent to the QR And the short code maps 1:1 to the same deep link target And the short code is rendered at ≥ 9 pt with contrast ratio ≥ 4.5:1 And manual entry of the short code in the resolver loads the same assets and verification state as scanning the QR
SDK Signing/Verification with Key Rotation and Payload Versioning
Given publicly documented SDK/service endpoints for sign and verify When sign is called with packetId, exhibitId (or summaryId), receiptImageHash, ledgerEntryHash, timestamp, payloadVersion Then it returns a signed payload, signature, and keyId of the signing key And verify accepts payload and signature and returns verification status and payloadVersion And after rotating keys from K0 to K1, new signatures are created with K1 while existing K0 signatures continue to verify with the published K0 public key And when payload schema changes, payloadVersion increments and previous versions remain verifiable
Deep Link Resolver & Context Loader
"As a user reviewing a packet, I want scanning the QR to open the precise receipt and ledger context so that I don’t waste time navigating or matching entries."
Description

Implement a resolver endpoint that accepts QR deep links, validates the signed payload, and loads the exact receipt image, ledger entry, and source metadata in TaxTidy (web and mobile). Support context anchoring (highlighting the matched transaction), graceful errors for expired/invalid links, and environment awareness (prod/sandbox). If the user is authenticated, load directly; if not, hand off to Auditor Access or prompt login based on link scope.

Acceptance Criteria
Authenticated Resolution to Context-Loaded View
Given a QR deep link containing a signed payload with receipt_id, ledger_id, environment=prod, scope=user, and a future expiry timestamp And the user is authenticated in the prod environment and authorized for the referenced workspace When the user opens the link via browser or app Then the resolver validates the signature with the current prod public key and confirms the payload is unexpired And loads the exact receipt image for receipt_id and displays source metadata (source system, timestamp, amount, merchant) And loads the ledger view anchored to ledger_id, auto-scrolls to the entry, and visually highlights it And the receipt and ledger amounts match and are displayed side-by-side And no unrelated records are highlighted And the URL/app route reflects workspace, receipt_id, and ledger_id for refresh/deeplink persistence
Unauthenticated Auditor-Scope Link Handoff
Given a QR deep link with scope=auditor and a valid signed, unexpired payload And the user is not authenticated When the link is opened Then the resolver routes to the Auditor Access flow carrying the link token/context And no receipt image, ledger data, or PII is rendered before successful Auditor Access verification And upon successful Auditor Access verification, the system loads the exact receipt, ledger anchor, and metadata as in the authenticated case And upon failed verification, the system shows an access denied message without revealing sensitive details
Unauthenticated Private-Scope Link Prompts Login
Given a QR deep link with scope=user and a valid signed, unexpired payload And the user is not authenticated When the link is opened Then the resolver presents the login screen with post-login return to the deep link context And after successful authentication and authorization to the workspace, the system loads the exact receipt, ledger anchor, and metadata And if authentication succeeds but authorization fails, a no-access message is shown without revealing sensitive content
Tampered or Expired Link Shows Graceful Error
Given a QR deep link with an invalid signature or an expiry timestamp in the past When the link is opened Then the resolver does not fetch or render any referenced resources And shows a branded error page stating Link expired or invalid with error code TL-EXPIRED or TL-TAMPERED and a timestamp And provides a single action to request a new link from the packet owner And returns HTTP 410 for expired and 400 for invalid signature in web context
Environment-Aware Resolution and Redirect
Given a QR deep link whose payload environment is sandbox (or prod) When the link is opened on a different host/app namespace than the payload environment Then the resolver redirects to the correct environment host/app namespace preserving payload and state And displays an environment banner indicating Sandbox or Prod And prevents cross-environment data access; no prod data is queried using a sandbox payload and vice versa
Mobile App Deep Link With Web Fallback
Given a valid, unexpired deep link opened on iOS or Android When the TaxTidy mobile app is installed and supports the universal/app link Then the OS opens the app directly to the context-loaded screen with receipt, ledger highlight, and metadata And if the app is not installed, the link opens the responsive web view with the same context And in both cases, the deep link token is cleared from the address bar/app route after resolution to prevent reuse leakage
Revoked or Deleted Resource Handling
Given a valid, unexpired deep link whose referenced receipt or ledger entry has been deleted or access revoked since link creation When the link is opened by an authenticated user or after Auditor Access verification Then the resolver displays a Resource unavailable message with code TL-NOTFOUND or TL-REVOKED And no partial data (thumbnails, amounts, merchant names) is displayed And for resource owners, a CTA to restore/replace is shown; for non-owners, guidance to contact the owner is shown
Offline Verification Code
"As an auditor without scanning capability, I want a printed code I can type to verify an exhibit so that I can confirm authenticity even without QR scanning."
Description

Print a short, checksum-protected verification code next to each QR that deterministically derives from the signed payload. Allow users/auditors to manually enter the code at a verify URL to retrieve the same verification result when scanning isn’t possible. Include guidance text in the packet explaining how to use the code and ensure codes are unique within a packet, human-friendly, and resilient to transcription errors.

Acceptance Criteria
Deterministic Code From Signed Payload
Given a signed payload S for an exhibit When the verification code is generated multiple times across environments Then the resulting code is identical each time Given two distinct signed payloads S1 and S2 When verification codes are generated for each Then the codes differ Given a corpus of 100,000 distinct signed payloads When verification codes are generated Then zero collisions are observed
Code Length, Alphabet, and Readability
Given any generated verification code Then its length is between 8 and 12 characters inclusive And it uses an alphabet that excludes visually confusable characters (0, O, 1, I, l) And it is formatted for print in groups of 4 characters separated by a hyphen (e.g., XXXX-XXXX) And in the PDF packet it appears adjacent to its QR with minimum 10pt font and contrast ratio ≥ 7:1
Uniqueness Within a Packet
Given a packet containing N exhibits and summary pages (N ≤ 1,000) When verification codes are generated for the packet Then all codes are unique within that packet And each printed code is the code for the same payload as its adjacent QR And regenerating the same packet from the same inputs produces the same code per item
Manual Verification via Verify URL
Given a valid verification code from a packet When the code is entered at the verify URL Then the system returns the same verification result that would be shown when scanning the corresponding QR deep link (including signature validity and source metadata summary) And no additional personally identifiable information beyond the exhibit’s verification details is exposed And the verification response is returned within 2 seconds at the 95th percentile under expected load
Entry Resilience and Normalization
Given a valid verification code entered with arbitrary casing, extra spaces, or hyphens in any positions When the code is submitted at the verify URL Then the system normalizes input and validates successfully Given a code with leading or trailing whitespace When submitted Then validation is not affected Given an invalid or malformed code When submitted Then the system displays a clear, non-revealing error and prompts the user to re-check characters, without indicating proximity to a valid code
Checksum Tamper Detection
Given a valid verification code When any single character is substituted Then validation fails Given a valid verification code When any two adjacent characters are transposed Then validation fails Given a valid verification code When a character is omitted or an extra character is added Then validation fails with an error that does not disclose packet contents
Guidance Text in Packet
Given any page in the packet that contains a TraceLink QR and verification code Then guidance text is present and includes: the verify URL, a one-sentence instruction to enter the printed code if scanning isn’t possible, and a formatted example code And the guidance text and code remain legible in grayscale printing and pass a contrast ratio ≥ 7:1 And the guidance appears within 2 cm of the QR/code block on each applicable page
Auditor Read-Only Access
"As an auditor, I want a secure, limited view of the specific documents linked from the packet so that I can verify evidence without needing full account access."
Description

Provide time-bound, scoped, read-only access for third-party auditors who arrive via QR/deep link. Generate a constrained session token that reveals only artifacts tied to the packet (receipt image, ledger entry, metadata, annotations) with PII minimization per settings. Enforce download controls, watermarking, and revocation. Log access for audit trail and display verification state (verified/mismatch) prominently.

Acceptance Criteria
Scoped Token via QR/Deep Link
Given an auditor opens a valid TraceLink QR/deep link for Packet P When the access endpoint is called Then the system issues a session token with claims role=auditor_readonly, scope=packet:P, and expires_at set within the configured TTL (default 7 days, max 14) And any request using the token to resources outside scope returns 403 and is logged with reason=out_of_scope And the deep link resolves directly to the targeted artifact within Packet P
Read-Only Enforcement & PII Minimization
Given an active auditor_readonly session When the auditor attempts to view or modify any artifact Then the UI presents view-only controls; edit/delete/annotate/export actions are not rendered And all write or mutation API calls return 403 with code=read_only_enforced and no data is changed And fields configured as PII-minimized are masked per owner settings (e.g., account numbers last4 only; emails partially obfuscated; addresses redacted if enabled) And the API/HTML responses do not contain unmasked PII values for masked fields (verified via response inspection)
Artifact Access Set Completeness
Given an auditor_readonly session scoped to Packet P When navigating the packet Then the auditor can open for each exhibit: the receipt image(s), the linked ledger entry, the source metadata, and any owner annotations And following a deep link to an exhibit opens that exact exhibit without additional search steps And requests for artifacts not belonging to Packet P return 404 with code=not_in_packet and are logged
Download Controls & Watermarking
Given owner download setting is Disabled for Packet P When the auditor attempts to download or print Then download/print controls are hidden in the UI and direct asset GET requests return 403 code=download_disabled And browser print-to-PDF is deterred via no-print styles and appropriate headers Given owner download setting is Enabled for Packet P When the auditor downloads or prints an artifact Then every page/image is watermarked with “TaxTidy • Packet P • AccessID {token_id} • UTC timestamp • Verification: {state}” And the watermark appears on all pages at 15–30% opacity and cannot be toggled off by the auditor
Revocation & Expiration Behavior
Given the packet owner revokes auditor access to Packet P When an auditor with an active session performs any request Then the session is invalidated within 30 seconds and subsequent requests return 401 code=revoked and redirect to an “Access revoked” screen And scanning the QR or opening the deep link after revocation shows the revoked screen Given a session token has expired When it is used Then the server returns 401 code=expired and displays instructions to request renewed access from the owner
Offline Verification Code Flow
Given an auditor scans the TraceLink QR while the device is offline When the scanner displays the QR contents Then a human-readable verification code V is visible and matches the printed code on the packet And instructions indicate that V can be entered at the TaxTidy verification portal to open the packet when online Given the auditor enters V online When V is valid and unrevoked Then the same Packet P opens in auditor_readonly view And if V is invalid or mismatched, the viewer shows “Mismatch” and no artifacts are revealed
Access Logging & Verification State Display
Given any auditor access event for Packet P When the event occurs Then an audit log entry is recorded with timestamp (UTC), packet_id, token_id, artifact_id (if applicable), action, outcome, IP (per policy), and user agent And log entries are immutable and viewable/exportable by the packet owner from the audit trail screen Given an artifact is opened in the auditor viewer When integrity verification succeeds Then a prominent “Verified” state with green indicator is shown in the header And if verification fails, a prominent red “Mismatch” state is shown, downloads are blocked, and the failure is logged
Cryptographic Integrity Verification
"As a compliance-focused user, I want cryptographic proof that exhibits haven’t been altered so that I can satisfy audit requirements confidently."
Description

Sign each exhibit’s canonical data (image hash, ledger hash, exhibit ID, packet ID, timestamp) using an asymmetric key. On scan or code entry, validate the signature and recompute hashes to detect tampering or mismatches. Surface clear states (verified, mismatch, unknown version) and reasons. Manage key storage (HSM/KMS), rotation, and backward compatibility with older packets. Provide a public verification endpoint and documentation of the signing scheme.

Acceptance Criteria
Canonical Data Signing on Packet Generation
Given an exhibit is finalized into a packet When the system generates the TraceLink QR payload Then it constructs canonical_data with fields [image_hash, ledger_hash, exhibit_id, packet_id, timestamp, signing_scheme_version] in the documented canonical order and encoding And computes the digest(s) and produces an asymmetric signature using the active key in HSM/KMS per Signing Scheme v1.x And embeds {signature, key_id, signing_scheme_version} into the QR/deeplink payload and prints the short verification code as specified And persists an audit record {exhibit_id, packet_id, key_id, scheme_version, signed_at, canonical_digest} And verification of the signature with the corresponding public key succeeds
QR Scan Verification and Status Surfacing
Given a user scans the TraceLink QR while online When the deep link opens in TaxTidy Then the app fetches the exhibit and recomputes image_hash and ledger_hash from the stored sources And verifies the signature over canonical_data using the published public key for key_id And surfaces status 'verified' when signature is valid and both hashes match And surfaces status 'mismatch' when the signature is invalid or any hash differs, including reason codes [REASON_IMAGE_HASH_MISMATCH, REASON_LEDGER_HASH_MISMATCH, REASON_BAD_SIGNATURE] And surfaces status 'unknown_version' when signing_scheme_version is unsupported, with reason [REASON_SCHEME_VERSION_UNSUPPORTED] And the verification endpoint responds within 2s p95 and logs verification outcomes with request_id
Manual Verification via Printed Code (Offline-Friendly)
Given an auditor enters the printed verification code from the packet When the device is offline Then the app validates the code format and checksum against the QR/deeplink payload and displays 'code_valid_offline' or 'code_invalid_offline' And when connectivity is restored, the app automatically performs full verification without re-entry and updates status to 'verified', 'mismatch', or 'unknown_version' with reasons
Key Storage, Rotation, and Auditability
Given a scheduled key rotation event When a new signing key is activated in HSM/KMS Then new signatures use the new key_id and previous packets remain verifiable using retained public keys And private keys never leave HSM/KMS; signing operations require scoped IAM permissions and are logged with key_id and request_id And rotation, activation time, and rollback are recorded; exportable audit logs include who/when and counts of signatures per key_id
Backward Compatibility and Unknown Version Handling
Given a QR payload indicates a signing_scheme_version When verification runs Then if version <= current, verification follows that version's rules and succeeds/fails accordingly And if version > current, status is 'unknown_version' with reason [REASON_SCHEME_VERSION_UNSUPPORTED] and instructions to update And the verification endpoint returns HTTP 200 with {status:'unknown_version', scheme_version} (not a transport error)
Public Verification Endpoint and Documentation
Given a third party submits a QR payload or printed code to the public verification endpoint When the request is valid Then the endpoint returns 200 with JSON {status, reasons[], scheme_version, key_id, exhibit_id, packet_id, verified_at} And rejects malformed input with 400 and non-leaking errors; applies rate limits (e.g., 60 rpm per IP) with 429 on excess And /docs/signing-scheme publishes canonicalization rules, algorithms, key identifiers, test vectors, and sample code sufficient to reproduce signatures and verifications
Tamper Scenarios Produce Mismatch
Given any of the following were altered post-signing: receipt image bytes, ledger entry data, exhibit_id, packet_id, or timestamp When verification runs Then status is 'mismatch' and reasons[] includes specific mismatch codes for each altered component And the QR/deeplink still resolves but never displays 'verified' for altered data
Packet Builder Integration & Layout Rules
"As a user exporting my packet, I want the QR and code to appear cleanly and consistently on every page so that the packet looks professional and is easy to verify."
Description

Integrate QR and code placement into the packet generation pipeline for exhibits and summary pages. Define consistent placement, sizing, and safe margins that survive common print workflows and duplex settings. Support dark/light backgrounds, ensure accessibility contrast, and fallbacks when space is constrained. Add regression tests for various page templates and locales to guarantee consistent rendering.

Acceptance Criteria
Consistent QR Placement and Sizing Across Page Types
Given a packet is generated for exhibits and summary pages with duplex printing When rendering any odd-numbered page (A4 or Letter) Then place the QR block at the bottom outer corner with: - QR size 26mm ±0.5mm, ECC level Q, module size ≥0.6mm - Quiet zone ≥2mm on all sides - Distance from page trim ≥10mm - Distance from inner gutter ≥15mm And mirror placement to the bottom outer corner on even-numbered pages (bottom-left if binding is on the left) And render the verification code directly below the QR in 10pt monospace, baseline offset 2–3mm And ensure no page content overlaps the QR block bounding box
Print Workflow Resilience and Decodability
Given the packet is printed and scanned under common office conditions When a 200-page packet is printed at 90–110% scale, grayscale, duplex, and scanned at 300 dpi with ±5° rotation and mild noise Then ≥99% of QR codes decode successfully to a valid payload And each decoded payload maps to the correct receipt and ledger entry And QR remains decodable under toner-saver mode and grayscale conversion
Background Adaptation and Accessibility Contrast
Given page backgrounds may be dark or light When rendering the QR on any background with relative luminance < 0.7 Then draw a white knockout plate with 2mm bleed behind the QR and code text And ensure QR module-to-background contrast ratio ≥ 7:1 And ensure verification code text contrast ratio ≥ 4.5:1 (WCAG AA) And include accessible alt text "TraceLink QR: open original receipt and ledger entry" in the PDF tag
Space-Constrained Fallback Behavior
Given the target placement area after margins provides < 30mm in height or width for the QR block When rendering the page Then switch to fallback: render verification code only (no QR) with a short deep link, 10pt monospace And place the fallback block in the same corner within margins; if still insufficient, move to the top outer corner And emit a structured warning event "qr_fallback_applied" with page number and template ID And ensure the deep link and code resolve to the same resources as the QR would
Deep Link Resolution and Tamper-Aware Verification
Given a user scans the QR or clicks the deep link hotspot in the PDF When the payload is resolved by TaxTidy Then open the exact receipt image, ledger entry, and source metadata And validate a signature (Ed25519) over {version, doc_id, receipt_id, ledger_id, ts, sha256(content)}; if invalid or expired (> 90 days), show an "Invalid/Expired TraceLink" page without exposing raw IDs And allow offline verification by entering the printed code to retrieve the same records once connectivity is available
Regression Coverage Across Templates and Locales
Given the CI regression suite renders all supported templates and locales When generating packets for locales {en-US, es-ES, fr-FR, ar-SA} and paper sizes {Letter, A4} in {portrait, landscape} Then QR placement coordinates differ from the baseline by ≤ 1pt and size by ≤ 0.5mm And visual diff per page against golden PDFs is ≤ 0.25% And no overlaps are detected between the QR block bounding box and other page elements
Accessible PDF Tagging and Clickable Hotspot
Given the generated PDF is viewed with assistive technologies When navigating via keyboard or screen reader Then the QR is tagged as a Figure with alt text and the deep link is an adjacent tagged Link with accessible name "Open source record in TaxTidy" And the hotspot area is keyboard focusable and appears in reading order after the page header And the document passes PDF/UA checks for these elements in the accessibility audit
Universal/App Links & Fallbacks
"As a mobile-first user, I want QR scans to open the right screen in the app automatically so that verification is fast and seamless on my phone."
Description

Configure iOS Universal Links and Android App Links so scans open the TaxTidy app directly when installed, falling back to a responsive web verifier when not. Handle deep link parameters securely, support multi-tenant domains, and present a lightweight verification screen optimized for mobile. Implement rate limiting and bot protections on public endpoints.

Acceptance Criteria
iOS Universal Link launches TaxTidy to exact receipt
- Given an iOS 15+ device with TaxTidy installed and a valid Universal Link for a receipt in tenant T, when the user taps/scans the link, then the app opens directly (no browser interstitial) and navigates to the Receipt Detail for that resource in tenant T within 2 seconds of app launch. - Given the deep link contains tenantId, resourceType, resourceId, signature, and expiresAt, when the app processes it, then the signature validates and expiresAt is in the future; otherwise a non-sensitive error screen is shown and no resource is loaded. - Given an unrecognized or unsupported path, when opened via Universal Link, then the app shows a safe fallback screen and does not crash, and the event is logged without PII.
Android App Link launches TaxTidy to exact receipt
- Given an Android 10+ device with TaxTidy installed and verified App Links for the link domain, when the user taps/scans the link, then the app opens directly (no chooser) and navigates to the Receipt Detail for the resource in tenant T within 2 seconds of app launch. - Given App Links verification fails or the domain is unverified, when the user taps the link, then the system opens the browser to the web verifier fallback URL for the same resource. - Given an unrecognized or unsupported path, when opened via App Link, then the app shows a safe fallback screen and does not crash, and the event is logged without PII.
Fallback to responsive web verifier when app not installed
- Given a device without the TaxTidy app, when the Universal/App Link is opened, then the HTTPS web verifier loads with LCP ≤ 2.5s and CLS < 0.1 on a Fast 3G profile (Moto G4 class) and displays the receipt image, ledger entry, and source metadata matching the backend record. - Given a 360×640 viewport, when the verifier renders, then primary actions have tap targets ≥ 44×44 px and content reflows without horizontal scroll. - Given an invalid or expired link, when opened, then the verifier shows an "Invalid or expired link" state with a retry/support CTA and does not expose internal IDs. - Given a transient network error, when retried, then the page offers a retry mechanism and succeeds within 2 attempts under normal conditions.
Correct tenant routing across multi-tenant domains
- Given a link generated for tenant T under its assigned domain or path, when opened (app or web), then the content resolves in the context of tenant T only, and a user cannot access content from tenant U using T’s link (cross-tenant requests are denied). - Given a cross-tenant tampering attempt (altered tenantId/host), when verified, then the request is rejected with HTTP 403 on web and a safe error in-app; no PII is leaked; the event is audit-logged. - Given tenants T1, T2, and T3, when test links are generated, then iOS/Android linking and the web verifier resolve correctly for all three tenants and reflect tenant branding where applicable.
Secure deep link parameter handling and validation
- Given any deep link, when its HMAC signature is recomputed server-side, then it matches; otherwise the request is rejected with HTTP 401 (web) or a safe error screen (app) and no resource data is rendered. - Given an expiresAt timestamp in the past, when processed, then the link is rejected and the user is prompted to request a new link; expired links are not served from cache. - Given the URL, then it contains only minimally required identifiers (tenantId, resourceType, opaque resourceId) and no PII (e.g., no emails, names) appears in the path or query string. - Given extra or malformed query parameters, when processed, then they are ignored and do not alter routing or expand access scope.
Rate limiting and bot protections on public verifier endpoints
- Given the /verify public endpoints, when a client exceeds 100 requests per IP per 15 minutes with a burst of 20, then subsequent requests return HTTP 429 with a Retry-After header; limits are configurable via environment. - Given anomalous traffic (e.g., failed signature rate ≥ 80% over the last 50 requests from an IP), when new requests arrive, then a CAPTCHA challenge is required before content is served. - Given an allowlist of auditor IP ranges, when those IPs access /verify, then they bypass CAPTCHA but remain subject to rate limits. - Given bot protections are enabled, then the 95th percentile added latency is < 100 ms at 500 rps sustained, measured in staging load tests. - Given receipt images are served, then they use signed URLs expiring ≤ 10 minutes and are not directly hotlinkable without a valid verifier session.
Mobile-optimized verification screen performance and accessibility
- Given initial load on a Fast 3G profile, then total transfer size for HTML+CSS+JS ≤ 150 KB gzipped (excluding receipt image), and JavaScript ≤ 50 KB; no render‑blocking third-party scripts are present. - Given the page renders, then it meets accessibility: WCAG 2.1 AA contrast, semantic landmarks, labeled controls, and correct reading order for screen readers; critical actions are keyboard accessible. - Given devices iOS Safari 14+, Chrome Android 96+, and Firefox 96+, when links are opened, then the verifier functions without layout or functional defects; if JavaScript is disabled, core content renders server-side with the receipt image and metadata visible. - Given a mid-tier device, then Time to Interactive ≤ 3 seconds and all interactive tap targets are ≥ 44×44 px with a visible focus state.

Policy Footnotes

Adds plain‑English notes and IRS citations next to each claim (e.g., mixed‑use percentage, mileage method, home‑office rules). Color‑coded flags (pass, caution, needs doc) show compliance at a glance, while footnotes explain why an item qualifies. You get credible, readable packets that answer policy questions before they’re asked.

Requirements

Rule Citation Mapping
"As a freelancer, I want each deduction to show the exact IRS rule it relies on so that I can confidently defend my claims if questioned."
Description

Associate each claim with authoritative IRS sources (e.g., Publication sections, Code, Regs, Rev. Procs, Notices) and applicable tax year. Support multiple citations per item, short inline cite labels, and tap-to-expand full references. Maintain a category-to-citation crosswalk for common scenarios (mileage methods, mixed-use assets, home office) and update automatically when the policy library changes. Provide an API that returns structured citation data for UI rendering and export, ensuring consistent, credible references across mobile and desktop.

Acceptance Criteria
Associate Multiple IRS Citations with Claim and Tax Year
Given a claim has a category and taxYear When citations are added automatically or manually Then the system shall allow 1..N citations of types {Publication, IRC, Regulation, Revenue Procedure, Notice} And each citation must include {type, identifier, section (optional), taxYear (YYYY), shortLabel (<=30 chars), fullReference, sourceUrl} And validation rejects citations missing required fields, with invalid taxYear format, or shortLabel exceeding 30 chars And duplicate citations (by type+identifier+section+taxYear) are de-duplicated And the claim persists the citations array in the canonical schema and is retrievable unchanged via the API
Tap-to-Expand Full Citation from Short Inline Label
Given a claim renders short inline citation labels When a user taps/clicks a label Then the UI reveals a panel with fullReference, title, section, issuing authority, taxYear, and sourceUrl within 300 ms And the panel is keyboard navigable (Tab/Shift+Tab) and screen-reader labeled; Esc closes the panel And on mobile and desktop, the expanded content is identical and truncation does not remove required fields
Auto-Apply Category-to-Citation Crosswalk on Claim Categorization
Given a claim is saved with a category and taxYear that exist in the crosswalk When the claim is created or re-categorized (e.g., mileage method, mixed-use, home office) Then the system auto-attaches the mapped citations for that category and taxYear And if the category/method changes, the prior auto-applied citations are removed and the new mapped citations are attached And user-added manual citations persist across re-categorizations unless explicitly removed by the user And crosswalk rules are versioned and the applied rule version is recorded on the claim
Auto-Update Citations on Policy Library Change with Versioning
Given a policy library update adds/modifies/deprecates citations When the synchronization job runs Then affected crosswalk entries and claims are re-mapped within 10 minutes And claims in locked/archived packets are not mutated; reopening creates a new version with updated citations and a visible change log And an audit record captures before/after citation sets, library version, timestamp, and actor And users receive an in-app notification summarizing impacted items
Citation API Returns Structured Data for UI and Export
Given the API endpoint /citations is requested with claimId or {category, taxYear} When the request is valid Then respond 200 with JSON containing {claimId, taxYear, citations:[{type, identifier, section, shortLabel, fullReference, sourceUrl, status(current|deprecated), effectiveFrom, effectiveTo, version}]} And p95 latency <= 300 ms for responses with <= 10 citations; include ETag and Cache-Control headers And invalid claimId returns 404; malformed parameters return 400; submitting deprecated-only citations returns 409 with guidance And the schema and field names are identical across mobile and desktop clients
Consistent Inline Labels and Footnotes in UI and Export
Given a user previews and exports a tax packet containing claims with citations When the export completes (PDF/CSV/JSON) Then each claim line displays shortLabel inline and a footnotes section enumerates fullReference entries in order of appearance And numbering/order in preview matches all export formats exactly And each footnote includes taxYear and sourceUrl; no missing or duplicate footnote numbers across the packet
Validation and Error Handling for Invalid or Deprecated Citations
Given a claim contains a citation that is missing required fields, is not applicable to the claim’s taxYear, or is deprecated without replacement When validation runs (save, export, or API submit) Then the claim is flagged with a machine-readable error specifying the citation and reason And user-facing status is set to caution or needs-doc based on severity; save/export is blocked only on errors marked fail And the system suggests current replacement citations where available And the API responds 422 with per-citation error details on invalid submissions
Plain-English Rationale Templates
"As a solo consultant, I want simple explanations next to each claim so that I understand the rationale without needing to read tax code."
Description

Generate readable footnotes that explain why an item qualifies, using template-driven text with placeholders for category, method, percentage, limits, and edge conditions. Enforce a 6th–8th grade reading level, avoid legalese, and include optional examples. Handle special cases (mixed-use assets, business meals, commuting vs. business travel) and surface the chosen method (e.g., standard mileage vs. actual). Provide localization-ready strings (US English initially) and support live preview in the authoring tool and mobile UI.

Acceptance Criteria
Template Rendering with Required Placeholders in Authoring Preview
Given I am editing a mileage footnote template in the authoring tool When I include placeholders {category}, {method}, {percentage}, {limit}, and {edge_condition} and click Preview with sample data Then all placeholders resolve using values from the policy engine And the chosen method label is shown (e.g., "Standard Mileage") And no unresolved tokens (e.g., {unknown}) appear in the output And the preview displays without runtime errors
Plain-English Output Meets Readability and Legalese Rules
Given a footnote is generated from any template When the text is evaluated for readability and banned terms Then the Flesch-Kincaid Grade Level is between 6.0 and 8.9 inclusive And average sentence length is ≤ 18 words And none of the banned legalese terms list is present And the optional example section renders only when include_example=true
Mixed-Use Asset Footnote Shows Percentages, Limits, and Edge Notes
Given an expense is marked mixed-use with business_use_percentage and optional limit When the footnote is generated Then the text states the business-use percentage from data And includes the applicable limit value if provided And adds an edge-condition note explaining partial personal use when present And the text passes the readability and legalese rules
Business Meals Footnote States Deduction Basis and Documentation Needs
Given an expense is categorized as meals with context tags (e.g., client_meeting, travel, event) When the meals footnote is generated Then the deduction percentage is pulled from the policy engine and stated And the qualifying condition is stated (e.g., business discussion occurred) And a compliance flag indicates missing required documentation (e.g., attendees, receipt) when absent And the text passes the readability and legalese rules
Commuting vs Business Travel Footnote Surfaces Method and Qualifies Deductibility
Given a transportation expense has travel_type set (commuting or business_travel) and a method selected (Standard Mileage or Actual) When the footnote is generated Then the chosen method name appears in the text And the text plainly states whether the trip is deductible based on travel_type And a brief rationale tied to a policy reference ID is included And the text passes the readability and legalese rules
Localization-Ready Strings with US English Default and Fallback
Given the app locale is set to en-US When a footnote renders Then all user-visible strings originate from i18n resource keys (no hard-coded literals) And number, date, and currency formats follow en-US conventions And switching to a pseudo-locale (e.g., en-XA) expands strings without truncation or layout break in preview And missing translations fall back to en-US
Live Preview Works in Authoring Tool and Mobile UI
Given the template editor or mobile preview screen is open When the author edits template text or toggles the include_example option Then the preview updates within 400 ms of the last change And placeholders resolve using sample data when real values are unavailable And preview output matches the production renderer (layout and text) And unresolved token errors are shown inline with a clear message
Compliance Flagging Engine
"As a freelancer, I want clear pass or caution indicators on my deductions so that I can fix issues before sending my packet to my tax preparer."
Description

Evaluate each claim against rules to assign color-coded flags: Pass, Caution, or Needs Documentation. Implement a rules engine with parameterized thresholds (limits, substantiation requirements, safe harbors) and return machine-readable reasons, confidence scores, and recommended actions. Include accessibility-safe colors and iconography, allow accountant overrides with notes, and run evaluations in real time during categorization and at packet compile-time.

Acceptance Criteria
Rule-Based Flagging per Claim
Given a claim meets all applicable rules and required substantiation is present When the engine evaluates the claim Then the flag is Pass and at least one passing reason is included Given any applicable rule indicates missing required substantiation When the engine evaluates the claim Then the flag is Needs Documentation and the reasons include the missing document types Given one or more applicable rules produce caution conditions without missing substantiation When the engine evaluates the claim Then the flag is Caution Given multiple rules yield different severities for the same claim When the engine aggregates results Then the final flag is the highest severity according to Needs Documentation > Caution > Pass Given a configured cautionBandPercent exists for a limit L and a claim’s actual value is within that band below L When the engine evaluates the claim Then the flag is Caution and the reason includes actual and L values
Parameterized Rule Thresholds with Effective Dating
Given a threshold T has versions with effectiveDate values When evaluating a claim at time Teval Then the engine applies the latest version of T whose effectiveDate ≤ Teval Given an admin updates a threshold value T and publishes a new effectiveDate When evaluating previously compiled packets with compileTime < new effectiveDate Then the engine continues using the prior version of T Given a rule compares claim.amount to a configured limit L When amount > L Then a violation reason is produced including ruleId, actual amount, limit L, and units Given a safeHarbor toggle is enabled for a rule When the engine evaluates claims governed by that rule Then the applied limit and reason reference reflect the safe harbor configuration Given thresholds include units and precision When values are updated Then evaluations reflect units correctly and comparisons are numerically precise to at least two decimal places
Machine-Readable Reasons, Confidence, and Recommendations Schema
Given a claim is evaluated When the engine returns a result Then the payload contains: flag ∈ {PASS, CAUTION, NEEDS_DOCUMENTATION}, confidenceScore ∈ [0.0,1.0], reasons[], recommendedActions[], evaluatedAt (ISO-8601), ruleVersion Given reasons[] items are returned When inspected Then each item includes: reasonId, ruleId, title, message, citation (IRS reference), severity, triggered (boolean), actual, limit (if applicable), missingFields/missingDocs (if applicable) Given recommendedActions are returned When validated Then each action is one of {UPLOAD_RECEIPT, ADD_MILEAGE_LOG, CONFIRM_MIXED_USE_PERCENT, SET_HOME_OFFICE_SQFT, REVIEW_POLICY_NOTE, REQUEST_ACCOUNTANT_REVIEW} Given the result payload is validated against JSON Schema v1.0 When malformed Then validation fails and the engine surfaces a schema_error with details; otherwise validation passes Given the same inputs and ruleVersion When evaluated multiple times Then the output payload is identical (idempotent)
Accessibility-Safe Colors and Iconography
Given a flag value When the engine provides presentation tokens Then it returns colorToken ∈ {flag.pass, flag.caution, flag.needsDoc}, iconToken ∈ {checkCircle, warningTriangle, documentExclamation}, and a11yLabel ∈ {"Pass","Caution","Needs documentation"} Given the UI renders the returned tokens on light and dark themes When contrast is measured Then text contrast is ≥ 4.5:1 and icon/adjacent shape contrast is ≥ 3:1 per WCAG 2.1 AA Given color is unavailable or user has color-vision deficiency When only icon and label are presented Then the status remains unambiguous (color is not the sole indicator) Given a screen reader user focuses a flagged item When labels are announced Then the a11yLabel and reason.title are read in order without decorative icons announced
Accountant Overrides with Notes and Audit Trail
Given a user with role ACCOUNTANT selects Override on a claim with current flag Caution and enters a note ≥ 10 characters When saved Then the claim flag becomes Pass (Overridden), the note is stored, and an audit record is created with userId, timestamp, priorFlag, newFlag, and note Given a non-accountant attempts to override a flag When action is submitted Then the system denies the request with 403 Forbidden and no state change occurs Given an override exists on a claim When the packet is compiled Then the overridden flag and note appear in the packet output and footnotes identify the override Given an accountant selects Revert Override on a claim with an override When saved Then the original engine-evaluated flag is restored and an audit record of the revert is created
Real-Time and Compile-Time Evaluations
Given a user edits category or amount on a claim in the categorization screen When the field loses focus or 300ms of inactivity elapse Then the engine re-evaluates and returns a result within 300ms p95 and 800ms p99 Given a packet contains 1,000 claims When compile is initiated Then 100% of claims are evaluated and aggregated within 30 seconds p95 and 60 seconds p99 Given a claim is evaluated during categorization and later at compile-time with unchanged inputs and ruleVersion When compared Then the flags and payloads are identical Given evaluations exceed the time budget When measured Then a performance event is logged with correlationId and timing details for diagnostics
Error Handling and Fallback Behavior
Given a transient rule service error occurs When evaluating a claim Then the engine retries up to 3 times with exponential backoff starting at 200ms before failing Given all retries fail When returning a result Then the engine returns flag=Needs Documentation with reasonId=ENGINE_ERROR, includes error details suitable for support (no PII), and recommendedActions includes REQUEST_ACCOUNTANT_REVIEW Given claim input fails validation (e.g., missing required fields) When evaluation is requested Then the engine returns 400 with errorCode=CLAIM_INVALID and does not mutate state Given a claim has partial data (e.g., bank feed pending payee) When evaluated Then the engine returns flag=Caution with reasons.missingFields listing the incomplete fields
Evidence Linkage & Checklist
"As a freelance creative, I want each footnote to link directly to the required documents so that I can quickly complete any missing proof."
Description

Attach and surface supporting evidence per claim (receipts, mileage logs, home-office calculation worksheets) and display a completion checklist driven by the rule’s substantiation requirements. Enable mobile capture, duplicate detection, and secure storage. Reflect evidence status in the footnote and flag engine (e.g., Needs Documentation if missing). Provide deep links from footnotes to the specific document preview and allow users to upload or reassign documents inline.

Acceptance Criteria
Attach Evidence to Expense Claim
Given an expense claim exists without evidence When the user uploads a receipt (PDF/JPG/PNG) via web or mobile Then the file uploads successfully and is linked to that claim And a preview thumbnail is generated And the claim’s evidence count increments by 1 And the “Receipt” item in the checklist is marked complete Given a mileage claim exists When the user links a mileage log (CSV/PDF) Then the “Mileage Log” checklist item is marked complete And the evidence appears with file name, upload date, and source Given a home-office claim exists When the user links a calculation worksheet (XLSX/PDF) Then the “Home-Office Worksheet” checklist item is marked complete
Generate Rule-Driven Evidence Checklist
Given a claim type (expense, mileage, home-office, mixed-use) When the claim is opened Then a checklist is generated from the current IRS rule configuration listing required and optional substantiation items for that claim type Given required items are incomplete When evidence is added or removed Then the checklist updates in real time reflecting item statuses and remaining count Given all required items are present When the checklist is evaluated Then the checklist shows 100% completion for required items
Footnote Flag Reflects Evidence Status
Given a claim has at least one missing required checklist item When viewing the footnote Then the flag displays “Needs Documentation” with the needs-doc color and lists missing item names Given all required items are present and evidence metadata matches claim details within configured tolerances When viewing the footnote Then the flag displays “Pass” with the pass color Given evidence is present but mismatches claim details beyond configured tolerances (e.g., date or amount) When viewing the footnote Then the flag displays “Caution” with the caution color and a brief reason
Deep Link from Footnote to Document Preview
Given a footnote references specific evidence When the footnote deep link is clicked or tapped Then the app navigates to the document preview for that evidence and focuses the correct file Given the user lacks permission to view the referenced document When the deep link is used Then access is denied with a secure error state and no document content is exposed Given the deep link is opened on mobile When the app is installed Then it opens the in-app preview; otherwise it opens the secure web preview
Inline Upload and Reassignment from Footnote
Given a footnote indicates missing documentation When the user selects “Add Document” inline Then an upload or camera control is shown based on device And on successful upload the document links to the underlying claim And the footnote and checklist update to reflect the new status Given a footnote shows an attached document that belongs to a different claim When the user selects “Reassign” inline and chooses another claim Then the document linkage moves to the selected claim And both claims’ checklists and flags update accordingly Given the user cancels an inline upload or reassignment When the action is aborted Then no changes are persisted
Duplicate Evidence Detection and Handling
Given a document is being uploaded When its content hash matches an existing document in the same workspace Then the user is notified of a duplicate and offered to link the existing document or cancel And no new duplicate file is stored if the user chooses to link existing Given a duplicate is detected and the user chooses to link existing When the link is created Then the claim’s evidence list and checklist update as if a new upload occurred Given the uploaded file name matches an existing file but the content hash differs When the upload completes Then the system treats it as a new document with no duplicate warning
Secure Storage and Access Controls
Given any evidence document is uploaded When it is stored Then it is encrypted at rest and retrievable only via authenticated, time-limited URLs, and all access is logged Given evidence is viewed or downloaded When the audit log is queried Then entries include user, timestamp, action (view/download/link/reassign/delete), and claim ID Given a user without permission attempts to view evidence When they request access Then the system denies access and does not reveal file contents or metadata
Versioned Policy Library & Release Management
"As a tax preparer, I want policy updates to flow safely into packets with version tracking so that I can rely on consistent, current guidance."
Description

Maintain a versioned repository of IRS rules, thresholds, and citations with effective dates and tax-year mappings. Support editorial workflow (draft, review, publish), hotfixes, and deprecation, with automatic propagation to mapping, templates, and the flag engine. Persist which policy version was used per packet and provide change logs for transparency. Cache frequently used rules for mobile performance and fall back gracefully if updates are unavailable.

Acceptance Criteria
Publish Policy Version with Effective Dates & Tax-Year Mapping
Given a complete policy set with effective_date ranges and tax_year mappings When a publisher submits the set for publish Then the system assigns an immutable version ID and records effective_date_start and effective_date_end And querying by any calendar date within the range returns that version And querying by tax_year returns the mapped version And prior versions remain retrievable by version ID And each rule includes required fields: unique key, citations, thresholds (with units), description, and change_reason
Editorial Workflow: Draft → Review → Publish with Permissions & Validation
Given a policy set in Draft created by Author A When Author A requests Review Then automated validation passes for schema, citation format, duplicate rule keys, and overlapping effective_date ranges And a Reviewer (not Author A) with review permission approves or rejects with mandatory comment When approved and Publish is initiated Then the policy set moves to Published and is locked from further edits except via new version And an audit trail records user, timestamp, and validation results for each state transition
Hotfix Release Without Breaking SemVer Boundaries
Given a Published policy version X.Y.Z When a Hotfix is created Then the new version is X.Y.(Z+1) And allowed hotfix changes are restricted to: citation text/links, note clarifications, typo fixes, and metadata corrections (no rule key changes, no effective_date backdating, no field removals) And the system blocks disallowed changes with an explicit error And dependent caches are invalidated and rebuilt And a hotfix changelog entry is generated and attached to both versions
Deprecation and Sunset of Policies with Forward Compatibility
Given a rule is marked Deprecated with a sunset_date and optional replacement_rule_id When the sunset_date is reached Then the rule is no longer selectable for new packets And existing packets referencing the rule continue to resolve successfully And API and UI surface deprecation status and replacement guidance And reports include deprecation metadata for traceability
Automatic Propagation to Mapping, Templates, and Flag Engine
Given a policy set is Published or Hotfixed When the release event is committed Then mapping tables are regenerated, document templates recompiled, and the flag engine rule graph rebuilt And all three propagation jobs report success within 2 minutes or trigger an automatic rollback to the prior version And footnotes rendered for a sample packet reflect the new version and citations And telemetry captures duration, success/failure, and affected artifact counts
Packet-Level Policy Version Persistence and Transparent Change Log
Given a tax packet is generated When policy rules are applied Then the packet stores policy_version_id and publish_timestamp immutably And the exported packet (PDF/JSON) displays the policy version and a link to its changelog When a newer policy version is released Then the existing packet remains pinned to its original version until an explicit Upgrade is requested And the Upgrade flow shows a diff summary of impacted rules and footnotes before confirmation
Mobile Caching & Offline Fallback for Policy Rules
Given a mobile client with network connectivity When the user signs in or opens the Policy Footnotes view Then the client fetches and caches the top 200 most-used rules with ETag/If-None-Match and a 24-hour TTL And subsequent lookups resolve from cache with <200 ms p95 latency When the client is offline or the update endpoint is unavailable Then the app falls back to the last-known published version, surfaces a "Policy data may be stale" banner, and retries in the background And publish/edit actions are disabled on mobile while offline
Annotated Packet Export (PDF and Share Link)
"As a freelancer, I want to export a packet that shows footnotes and citations clearly so that my CPA and any reviewer can understand each claim without follow-up questions."
Description

Render IRS-ready packets with inline footnotes beside each line item, a legend for flags, and a bibliography of citations. Ensure selectable, accessible text, clickable links to evidence and sources, and consistent typography. Support export settings (include/exclude attachments, redactions), secure share links with expiry, and watermarking for drafts. Optimize for mobile initiation with server-side rendering for reliability and speed.

Acceptance Criteria
Inline Footnotes and Flag Legend with Evidence Links
Given a packet with line items that have footnotes, flags (pass, caution, needs doc), and evidence URLs When the user exports the packet to PDF Then each line item shows a superscript footnote marker and the corresponding footnote text beside the item on the same page And color-coded flags render next to line items and match the legend colors and labels And a single legend explaining all flag statuses appears once in the packet And evidence links in footnotes are clickable hyperlinks that open the referenced evidence in the app or browser per user permissions And if a line item spans pages, its footnote appears with the portion of the item on the same page with a continued marker
Bibliography and Clickable Source Links
Given footnotes include IRS citations When the packet is exported to PDF Then a Bibliography section is appended listing each unique citation once with title, code/notice number, and year And each citation reference in footnotes is a clickable internal link to its Bibliography entry And each Bibliography entry contains a clickable external link to the official IRS source URL And duplicate citations are de-duplicated and numbered consistently
Selectable and Accessible Text in PDF Export
Given the exported PDF is opened in a standard PDF viewer Then all textual content (line items, footnotes, legend, bibliography) is selectable and copyable as text And the PDF contains tags with a logical reading order: line item followed by its footnote, section headings before content And the document primary language metadata is set to en-US And no page is rasterized; full-text search finds line item and footnote words And embedded fonts are present; no missing font warnings; body text size is 11 pt ±0.5, footnotes 9 pt ±0.5, headings 14–18 pt
Export Settings: Attachments and Redactions
Given the user sets Include Attachments = On and Redact Sensitive Data = Off When exporting Then an Attachments section is appended containing all selected receipt images and documents in original resolution Given the user sets Include Attachments = Off When exporting Then no attachments are present in the PDF Given the user sets Redact Sensitive Data = On When exporting Then sensitive fields (SSNs, full bank account numbers, email addresses) are irreversibly redacted throughout the PDF and cannot be revealed by copy/paste or search And redactions apply to both main content and attachments when included
Secure Share Links with Expiry and Revocation
Given the user creates a share link with an expiry date/time and optional passcode When an unauthenticated recipient opens the link before expiry and enters the correct passcode Then the packet renders and is downloadable according to the share settings When the link is opened after expiry or after the owner revokes it Then the recipient sees an expired link message and cannot access the packet And audit logs record link creation, access (timestamp, IP), and revocation And the share token has at least 128 bits of entropy
Draft Watermarking
Given the packet status is Draft When exporting to PDF or creating a share link Then every page displays a diagonal “DRAFT” watermark with 10–15% opacity and the export date And underlying text remains fully selectable and passes copy/paste without including watermark text Given the packet status is Final When exporting or sharing Then no watermark is present
Mobile-Initiated Server-Side Rendering Performance and Reliability
Given a user starts an export from a mobile device on a typical cellular network When the export is requested Then the server acknowledges job creation within 3 seconds And the export completes within 20 seconds at the 95th percentile for packets up to 50 pages and 25 attachments And progress/status updates are shown in-app; the export continues if the app is backgrounded And the user receives a push or email with the download/share link on completion And failed render attempts automatically retry up to 3 times before surfacing an error
Audit Trail & Footnote Provenance
"As a solo filer, I want an audit trail for each explanation so that I can prove how the decision was made if I’m audited."
Description

Record the inputs, rule versions, evaluation results, and rendered text for each footnote, including timestamps and user overrides. Provide an immutable, exportable audit summary that explains the decision path (“show your work”) and ensures PII-safe logging. Expose a read-only viewer for accountants and support retention policies aligned with tax record-keeping requirements.

Acceptance Criteria
Log Footnote Generation Event
Given a footnote is generated for a transaction, invoice, or receipt When the rule engine evaluates the item Then the system writes an immutable audit entry containing footnote_id, source_ids, input_snapshot, rule_ids_with_versions, condition_outcomes, compliance_flag, rendered_text, started_at, completed_at, run_id, actor_id, actor_type And the audit entry is content-hashed (SHA-256) and addressable by its hash and footnote_id And any attempt to modify the audit entry after write is rejected with 409 and leaves the entry unchanged And the audit entry is retrievable via API by footnote_id within 300 ms p95
Capture User Override with Provenance
Given a user with permission overrides a footnote’s compliance flag or text When they submit an override Then the system records override_id, user_id, role, reason_code, note, timestamp, diff_original_to_override, prior_audit_hash And a new audit entry version is created and linked via parent_version_id while preserving the original And the UI and API show the full override history ordered by timestamp And override requests without reason_code are rejected with 400
Export Tamper-Evident Audit Summary
Given a request to export an audit summary for one or more footnotes When the export is generated Then the export includes input_snapshot, rule_versions, decision_path, evaluation_outcomes, overrides_history, timestamps, and all related hashes And the export is available in JSON and PDF with a top-level SHA-256 and detached signature And verifying the signature and recomputing the hash succeeds using the published public key And exporting 1 footnote completes in <= 1s p95; exporting 500 footnotes completes in <= 30s p95
PII-Safe Logging and Redaction
Given audit logging of footnotes When sensitive fields are processed Then full SSNs, full bank or card numbers, CVV, full DOB, and receipt images are not stored in the audit entry And allowed identifiers are masked (e.g., SSN last4 only, account last4 only, emails SHA-256 hashed with salt) And references to originals use secure object URLs without embedding content And automated PII scan on a sample of 1000 entries reports 0 Critical and 0 High findings
Read-Only Accountant Viewer
Given an invited accountant opens the audit viewer via a time-limited link When they browse footnotes Then they can search, filter, and view provenance, decision path, and overrides but cannot edit or delete And all mutation attempts return 403 and are logged with user_id, attempted_action, timestamp, and ip And the link enforces tenant isolation and expires at or before its configured TTL And viewer access events are visible in the workspace access log
Retention and Deletion Policy Enforcement
Given a workspace with a 7-year retention policy When an audit entry’s retention period elapses and no legal hold exists Then the system permanently deletes or anonymizes the audit entry and related exports within 30 days And a deletion certificate with entry_ids, timestamps, actor=system, method, and hash is generated and retained And if a legal hold is active, deletion is deferred and the hold is recorded and visible And backups and replicas purge the deleted data within 30 days of primary deletion
Rule Version Change Auditability
Given a rule set is revised from version X to version Y When a footnote is re-evaluated after the change Then a new audit entry is created capturing rule_version=Y and decision deltas versus X And existing entries with rule_version=X remain immutable and retrievable And the UI and API can compare outcomes between versions X and Y for the same item And the decision path explanation references the exact rule code commit or artifact ID used

Provenance Timeline

Renders a one‑page, visual timeline for any expense or income item—invoice issued → payment received → bank settlement → receipt captured → categorization applied. Each step shows timestamps, devices, and approvers, providing end‑to‑end traceability from source to ledger without decoding logs.

Requirements

Unified Provenance Event Schema
"As a freelancer, I want each step of an expense or income to be represented consistently so that I can trust and understand the timeline across different sources."
Description

Define a normalized event model for TaxTidy that represents each provenance step (invoice issued, payment received, bank settlement, receipt captured, categorization applied) with consistent fields: source and normalized timestamps, actor/role, device and channel, source system, approver, amount/currency, linkage identifiers, attachment pointers, and integrity metadata (hash/signature). Ensure backward-compatible extensibility and map all existing invoice, bank feed, and receipt ingestion pipelines to this schema to enable end-to-end traceability and consistent rendering in the Provenance Timeline.

Acceptance Criteria
Normalize and Validate Timestamps Across Sources
Given source events with heterogeneous time zones and formats When they are ingested into the unified schema Then each event includes source_timestamp (original zone) and normalized_timestamp (UTC, RFC3339) And normalized_timestamp is deterministically derived from source_timestamp and timezone rules And events sort deterministically by normalized_timestamp, then event_id for ties And time_skew_seconds is recorded and falls within -86400..86400 or skew_flag=true is set
Required Core Fields and Type Validation
Given any unified provenance event When validated against schema v1.0 Then it contains: event_id (UUIDv4), schema_version (semver), event_type ∈ {invoice_issued, payment_received, bank_settled, receipt_captured, categorization_applied}, actor.id, actor.role ∈ {user, system, approver, bank, vendor}, device.type ∈ {mobile, web, server, bank_connector}, channel ∈ {mobile, web, api, bank_feed, email_import}, source_system (non-empty) And approver.id is present iff actor.role=approver And amount.value (number or integer minor_units) and amount.currency (ISO 4217) are present for monetary events; else amount is null And attachment_pointers[ ] ≥ 1 iff event_type ∈ {receipt_captured, invoice_issued}; else 0..n And integrity.hash (SHA-256 hex) is present for all events; integrity.signature is optional but, if present, includes algorithm and signer_id
Linkage Identifiers Enable End-to-End Traceability
Given a target identifier (invoice_id, payment_id, bank_txn_id, receipt_id, or category_id) When querying the unified events API by that identifier Then the response includes a chain of events sharing a common trace_id covering all related steps And each adjacent event pair shares at least one linkage_id that connects them And completeness_status=complete for chains with ≥4 distinct steps; otherwise completeness_status=incomplete And orphan_rate=0% for production queries over a 24h window
Backward-Compatible Extensibility via Versioning
Given consumers built against schema v1.0 When schema v1.1 introduces only optional fields and extensions under extensions.* Then v1.0 consumer contract tests pass with zero breaking changes And every event includes schema_version and min_consumer_version And a down-conversion process can emit valid v1.0 events by dropping unknown fields without altering required semantics And validation rejects missing required fields but accepts unknown fields in extensions.*
Pipeline Mapping Coverage and Emission SLAs
Given invoice ingestion, bank feed, and receipt capture pipelines are live When processing production traffic over a rolling 24h window Then ≥99.9% of source records produce a valid unified event; the remainder are in a dead-letter queue with error_code and retryable flag And unified event emission latency is ≤60s at p95 and ≤5m at p99 for each pipeline And field mapping accuracy for required fields is ≥99.5% verified by sampled cross-checks against source records And monitoring exposes per-pipeline success_rate, p95/p99_latency, and dead_letter_rate metrics
Integrity and Tamper-Evidence
Given any persisted unified event When recomputing the hash over the canonicalized payload Then the result equals integrity.hash And if integrity.signature is present, signature verification using the signer public key succeeds And any attempted mutation after persistence causes hash/signature mismatch and the write is rejected with error_code=INTEGRITY_VIOLATION
Consistent Timeline Rendering From Unified Events
Given a unified event chain for an expense or income item When rendered in the Provenance Timeline Then each step displays normalized_timestamp, actor.role, approver.id (if present), device.type, channel, amount.value/amount.currency (if applicable), and attachment thumbnails (if present) And steps are ordered by normalized_timestamp ascending with event_id tie-breaker And no renderer-specific adapters are required; rendering is driven solely by the unified schema fields
Cross-Source Correlation Engine
"As a user, I want the system to automatically connect my invoice, payment, bank entry, receipt, and category so that I don’t have to piece them together manually."
Description

Implement an engine that automatically links disparate events into a single item’s Provenance Timeline using deterministic keys (invoice IDs, bank transaction IDs) and heuristic matching (amount and currency, date proximity, payee/payer similarity, memo tokens), producing a confidence score. Support late-arriving events, re-correlation, and conflict resolution with full audit logging and manual override hooks in the UI. Integrate with TaxTidy’s ingestion services and ensure idempotent processing and retry semantics.

Acceptance Criteria
Deterministic Linking via IDs
Given an invoice ingestion event with invoice_id=X and a payment event referencing invoice_id=X When the engine processes both events Then it creates a deterministic link with confidence 1.0 and link_reason deterministic_invoice_id within 1 second Given a bank transaction with bank_tx_id=B and a settlement event referencing bank_tx_id=B When processed Then a deterministic link is created with confidence 1.0 and link_reason deterministic_bank_tx_id Given events with mismatched IDs When processed Then no deterministic link is created and confidence remains unchanged
Heuristic Matching with Confidence Scoring
Given two events without shared deterministic keys and matching amount and currency and date difference <= 3 days and payee or payer similarity >= 0.85 and at least one shared memo token When processed Then the engine links them with confidence >= 0.90 and link_reason heuristic and features used are recorded Given two events with matching amount and currency and date difference <= 10 days and payee or payer similarity between 0.70 and 0.84 When processed Then the engine assigns confidence between 0.60 and 0.80 and marks the link needs_review if confidence < 0.80 Given multiple candidate matches for one event When processed Then the engine selects the highest confidence unique pairing and leaves other candidates unlinked and if there is a tie in confidence the engine does not auto link and marks the event conflict
Late-Arriving Event Re-correlation
Given an item linked by heuristic with confidence < 1.0 and a late arriving event provides a deterministic key match When reprocessing occurs Then the engine replaces the heuristic link with a deterministic link within 15 minutes and the Provenance Timeline order is updated Given a late arriving event that increases the best heuristic confidence by >= 0.10 When processed Then the engine updates the link to the higher confidence match and emits a correlation_update event and preserves the prior link in the audit log Given no new evidence improves confidence by >= 0.05 When processed Then the engine retains the current link and records the evaluation decision in the audit log
Conflict Resolution and Audit Logging
Given two candidate links above the auto link threshold for the same event When processed Then the engine applies the precedence policy deterministic over heuristic then higher confidence then most recent and if still tied it withholds auto linking and marks status conflict Given any link is created updated or removed When processed Then the engine writes an immutable audit log entry capturing prior_state new_state features_considered algorithm_version timestamp actor correlation_id and a content hash and the entry is queryable within 5 seconds Given a manual override or conflict resolution is applied When processed Then the audit log records the reason_code and user_id and the previous state is preserved
Idempotent Processing and Safe Retries
Given the same event payload is delivered multiple times with the same idempotency_key When processed Then the resulting links and timeline are unchanged and the engine returns status unchanged Given a transient downstream failure occurs during correlation When retried with the same idempotency_key Then exactly once semantics are maintained and at most one link exists and if retries exceed the limit the event is moved to a dead letter queue and state remains consistent Given concurrent processing of related events When processed Then race conditions are prevented and the final persisted state matches some serial ordering of those events
Manual Override Hook Integration in UI
Given a user with permission Edit Correlations selects two events and submits Link Manually When processed Then the engine creates a link with confidence manual and reason manual_override and updates the timeline within 3 seconds and writes an audit entry with user_id and justification Given a user removes an auto link When processed Then the engine unlinks the events and suppresses auto relinking unless new evidence increases confidence by >= 0.15 or a deterministic key appears and the suppression has a default expiry of 90 days Given a manual override conflicts with a deterministic rule and the user does not have admin Force Override When processed Then the deterministic rule prevails and the attempted override is rejected and logged
Ingestion Integration and Performance
Given events arrive from invoice bank and receipt pipelines using schema version v1 with required fields event_id source amount currency date payee_or_payer memo device_id approver_id When validated Then well formed events are accepted and malformed events are rejected with an error code and no state change Given a typical workload of 10000 events per day and a peak of 50 events per second When processed Then p95 correlation latency is <= 2 seconds and p99 correlation latency is <= 5 seconds and no data loss occurs Given a timeline is requested for an item with up to 10 related events When queried after the last event is processed Then the correlation result is complete and consistent within 1 second
Timeline UI Component (Web & Mobile)
"As a mobile-first freelancer, I want a clear visual timeline for each item so that I can see where it stands at a glance and drill into details when needed."
Description

Build a responsive, one-page visual timeline component that orders steps chronologically with recognizable icons, labels, status badges, timestamps (with time zone awareness), device and approver chips, and confidence indicators. Provide expand/collapse details, deep links to source artifacts, loading skeletons, empty/error states, and accessibility compliance (WCAG 2.1 AA). Optimize for mobile-first interactions (swipe, tap targets, haptics) and support dark mode to align with TaxTidy’s design system.

Acceptance Criteria
Chronological Rendering with Step Metadata and Time Zone-Aware Timestamps
Given an item with 5 provenance steps and server timestamps in UTC, When the timeline renders on web or mobile, Then steps appear in ascending chronological order by timestamp with no misordering. And each step displays a step-type icon from the design system, a human-readable label, a status badge, a timestamp formatted in the user’s profile time zone as "MMM D, YYYY, HH:mm z", a device chip (device type and OS), an approver chip (display name), and a confidence indicator (percentage with semantic color). And hovering (web) or long-pressing (mobile) the timestamp reveals a tooltip/sheet showing the original UTC ISO-8601 value and the user’s time zone name. And if the user has no profile time zone, timestamps use the device time zone; if unavailable, UTC is used and the "UTC" suffix is displayed. And confidence indicators map as: ≥90% = success style, 70–89% = warning style, <70% = info style, and include an accessible text label of the value.
Expand/Collapse Step Details
Given the timeline is visible, When a user taps/clicks a step’s summary row, Then the step expands inline to show details (raw data summary, notes, approver comment) within 200 ms. And tapping/clicking the same step again collapses it. And multiple steps can be expanded simultaneously. And expanded/collapsed state persists when navigating to a deep-linked artifact and returning via back navigation within the same session. And the expanded container is focus-trapped only when opened via keyboard, and ESC closes it; aria-expanded reflects state; focus returns to the triggering control on close.
Deep Links to Source Artifacts
Given a step has a source artifact URL provided by the API, When the user activates "View Source", Then on web it opens in a new tab with the correct URL and UTM tracking parameters; on mobile it opens an in-app web view with a visible close control. And if the link returns 401/403, an inline message prompts the user to re-authenticate with a "Reconnect" CTA; if 404/410, show "Artifact unavailable" with a "Report issue" CTA; if timeout >10 s, show a retry option. And all artifact URLs are validated against an allowlist of hosts before navigation; invalid URLs are blocked and surfaced with an error message. And activating a deep link logs an analytics event with itemId, stepType, and artifactHost.
Loading Skeletons, Empty and Error States
Given the component mounts, When data has not arrived within 150 ms, Then a timeline skeleton placeholder is shown until data resolves or errors. And when data resolves, skeletons are replaced with content without layout shift greater than CLS 0.1. And if the API returns an empty steps array, an empty state appears with copy explaining no provenance is available and a CTA to connect sources. And if the request fails (network error or 4xx/5xx), an inline error state shows with a retry button; selecting retry re-issues the request and replaces the error on success. And a max of one global error banner is shown at a time; errors are announced to screen readers via role="alert".
Accessibility Compliance WCAG 2.1 AA
Given a keyboard-only user, When navigating the timeline, Then all interactive elements (step summary, expand controls, deep links, retry/CTAs) are reachable in a logical order and operable via Enter/Space; ESC collapses details; visible focus indicators meet 3:1 contrast. And color is not the sole means of conveying status or confidence; textual labels are present for each. And text and controls maintain at least 4.5:1 contrast in both light and dark modes; content reflows without loss at 200% zoom and in 320 px width. And screen readers announce each step summary including type, status, timestamp (localized), device, approver, and confidence; expanded regions expose aria-expanded and proper headings/landmarks. And motion is reduced when prefers-reduced-motion is enabled; no flashing content violates WCAG. And automated audits (axe-core/Lighthouse) report zero Critical or Serious violations on the component.
Mobile Interactions and Haptics
Given a mobile device (≤768 px width), When the timeline renders, Then all tap targets are at least 44x44 pt with 8 pt spacing and no horizontal scroll is required. And a single tap on a step expands/collapses it; pull-to-refresh at the top triggers a data reload and haptic success feedback on completion. And haptic feedback is provided on expand/collapse and on error states (if device supports haptics), and is disabled when system haptics are off. And orientation changes preserve scroll position and expanded state. And all gestures and animations respect system reduced motion settings.
Dark Mode and Design System Alignment
Given the OS theme changes, When the user has not overridden app theme, Then the timeline switches between light and dark mode without flicker and persists correct theme across sessions. And when the user sets a manual theme in app settings, that theme overrides OS and persists. And all colors, icons, badges, and confidence indicators use TaxTidy design tokens; contrast in dark mode remains ≥4.5:1 for text and ≥3:1 for UI components. And images/illustrations used in empty/error states provide dark-mode variants or are appropriately tinted. And visual regression tests for light and dark modes show no diffs above threshold for the component’s baseline snapshots.
Evidence Attachments & Integrity Verification
"As a user, I want to view the original documents behind each timeline step so that I can verify accuracy and defend deductions if audited."
Description

Attach and display evidence for each timeline step (invoice PDFs, receipt images, and bank transaction snapshots) with secure storage references. Compute and store content hashes and verification metadata, show integrity status in the UI, and maintain an immutable audit log preventing post-facto tampering. Provide in-app preview, download, and redaction capabilities, and expose evidence pointers to the export pipeline for IRS-ready packets.

Acceptance Criteria
Attach Evidence to Timeline Step
Given a logged-in user with edit permission on an item When they attach a supported file type (PDF/JPG/PNG/HEIC) up to 25 MB to a specific timeline step Then the file is uploaded, a secure opaque storage reference is stored with the step, and metadata (filename, size, MIME type, uploader ID, upload timestamp, step ID) is persisted And a SHA-256 hash is queued for computation and the UI shows the attachment entry with filename and size And if an evidence with identical SHA-256 already exists on the same step, the system links to the existing evidence instead of storing a duplicate and surfaces a "Duplicate linked" notice
Compute and Display Content Hash Integrity
Given an evidence upload completes When the server computes the SHA-256 hash and stores it with algorithm and computed-at timestamp Then the evidence shows an Integrity status of "Verified" after a successful verification pass Given an evidence is accessed in the UI When an integrity check compares the stored hash to the content retrieved from storage Then on match the UI displays "Verified" with last-verified timestamp, and on mismatch it displays "Integrity Mismatch", blocks preview/download, and raises an alert event And all verification outcomes are stored with timestamp and actor/process ID
Immutable Audit Log for Evidence Lifecycle
Given any evidence lifecycle event occurs (attach, view, preview, download, redact, replace, export-pointer-read) When the event is processed Then an append-only audit entry is created capturing evidence ID, step ID, action, actor ID (or system), timestamp (UTC), client device ID, IP (hashed), prior hash and new hash where applicable And each audit entry includes a cryptographic chain reference to the prior entry (prev_entry_hash) to ensure tamper-evidence And the audit log cannot be edited or deleted via any API or admin UI Given an audit export request When the chain is validated Then the export returns a complete, chronologically ordered log with a passing chain integrity check
In-App Preview and Secure Download
Given an evidence file is of a previewable type (PDF/JPEG/PNG) When a user with view permission opens it from the timeline Then an in-app preview renders successfully within 2 seconds for files ≤10 MB and provides zoom and rotate controls Given a non-previewable type or files >10 MB When opened Then the UI shows metadata and a Download action only Given a user initiates a download When the system generates access Then a time-bound signed URL (valid ≤10 minutes) is issued and logged, and after expiry any access attempt returns 403 And download/preview attempts without permission return 403 and are logged
Redaction Workflow with Original Preservation
Given a user with redact permission opens a PDF or image evidence When they apply redactions and save Then a new redacted derivative is created and linked to the original with its own metadata and SHA-256 hash And the original remains immutable and visible only to the owner and admins while the redacted version is the default for collaborators And the UI labels the evidence as "Redacted" and allows toggling between Original and Redacted (subject to permissions) And the audit log records the redaction action, actor, timestamps, and hashes of both versions And integrity verification passes independently for both versions
Expose Evidence Pointers in IRS Export
Given a user generates an IRS-ready export for a selected item or date range When the export pipeline assembles the packet Then each timeline step in the export contains evidence pointers including filename, size, MIME, storage opaque URI, SHA-256 hash, hash algorithm, and last-verified timestamp And the export passes schema validation and contains either embedded copies or external pointers according to export settings And any evidence flagged "Integrity Mismatch" is excluded from embedding, listed in an Exceptions section with reason, and the export process returns a warning status
Access Control and Opaque References
Given role-based access is configured (Owner, Collaborator, Accountant) When an authorized user attempts to view, preview, or download evidence for an item they can access Then the action succeeds and is logged with actor and timestamp Given an unauthorized user attempts access via direct link or guessed reference When the request is made Then the request is denied with 403, logged, and no existence-leaking details are returned And all storage references are opaque, non-sequential identifiers that cannot be enumerated, and signed URLs are scoped to the requesting user/context
Approvals & Annotations Capture
"As a consultant, I want to see who approved each step and any notes so that I have clear accountability and context."
Description

Capture approver identity, role, and timestamp per step from source systems or manual actions within TaxTidy. Allow authorized users to add approvals, notes, and tags with full audit trails and role-based permissions. Render approver chips and annotations inline on the timeline and expose this data to exports and sharing flows for accountability and context.

Acceptance Criteria
Ingest source approvals metadata into timeline steps
Given a supported source provides approver identity, role, and timestamp for a step When the item is imported or refreshed in TaxTidy Then the approver chip displays the correct identity, role, and timestamp on the corresponding timeline step And the audit log stores the imported approval with source="system" and a unique reference And any missing field is labeled "Unknown" and the step is flagged "Approval metadata incomplete"
Manual approval with role-based permissions
Given a user with Owner or Manager role views a timeline step without an approval When they add an approval with identity, role, and timestamp Then the approval saves, is attributed to the actor, and an approver chip appears inline within 1 second And the audit trail records the action with actor, role, timestamp, and reason And users with Preparer or Viewer roles cannot add approvals and receive a permission error
Notes and tags with full auditability
Given an authorized user adds, edits, or deletes a note or tag on a timeline step When they save the change Then the note or tag renders inline with author and timestamp And an append-only audit entry is created capturing before/after values, actor, role, timestamp, and reason And previous versions remain viewable in the history; content is never hard-deleted, only superseded
Inline rendering of approver chips and annotations
Given a timeline with steps that have approvals, notes, and tags When the timeline is loaded or updated Then each step shows approver chips with avatar/initials, role label, and timestamp, plus indicators for notes/tags if present And selecting a chip or indicator opens a details drawer within 300 ms showing full metadata and audit history And rendering completes within 800 ms for timelines up to 50 steps on a mid-tier mobile device
Exports and shared links include approvals and annotations
Given a user exports the item to PDF, CSV, or JSON or creates a share link When the export or share is generated Then approvals (identity, role, timestamp, source) and annotations (notes, tags, authors, timestamps) are included per step and match on-screen data And recipients of share links can view but cannot modify approvals or annotations And exports include an audit appendix listing all approval and annotation events chronologically
Editing, corrections, and conflict controls
Given a step already has an approval recorded When an Admin corrects the approver identity or role Then the prior value remains accessible in history with reason "Correction" and the updated value is effective for all views and exports And non-admin users cannot edit existing approvals but may append additional approvals if authorized And simultaneous edits are prevented with optimistic locking and a clear user message on conflict
Gaps, Errors, and Remediation Prompts
"As a user, I want the timeline to highlight missing pieces and tell me how to fix them so that my records stay complete and audit-ready."
Description

Detect missing or inconsistent steps (e.g., payment without receipt, unmatched bank transaction) and surface inline warnings, placeholders, and confidence indicators on the timeline. Provide guided remediation actions—attach receipt, link/unlink events, adjust categorization—with progress tracking and dismiss/resolve controls. Allow configuration of detection thresholds and rules to maintain audit readiness.

Acceptance Criteria
Missing Receipt on Paid Expense
Given an expense item has Payment Received and Bank Settlement steps recorded and no receipt attached for longer than the configured grace period (default 7 days) When the Provenance Timeline is opened for that item Then an inline warning "Receipt missing" is shown at the Receipt step position with a placeholder step and primary action "Attach receipt" And the confidence indicator "Documentation completeness" displays below the configured threshold and the progress tracker shows less than 100% When the user uploads a receipt via "Attach receipt" Then the receipt is stored, OCR processed, and linked to the item; the Receipt step replaces the placeholder with timestamp, device, and user; the warning is removed; the progress tracker updates to 100%; and the issue status becomes Resolved with an audit log entry
Unmatched Bank Transaction Link/Unlink
Given a bank transaction exists within ±10 days of an issued invoice or payment event and is not linked to the item, or a payment event exists without a linked bank transaction When the timeline is opened Then an inline warning "Unmatched bank transaction" is displayed with a suggestions list including candidate matches, each showing counterparty, amount, date delta, and a confidence score And actions "Link" and "Dismiss" are available When the user selects "Link" on a suggested match at or above the configured confidence threshold Then the bank transaction is associated; the Payment and Bank Settlement steps display the link; the warning resolves; and an audit log entry records the link operation with user and timestamp When the user unlinks a previously linked transaction Then the association is removed; the warning reopens; and an audit log entry records the unlink operation
Categorization Conflict Review
Given an expense has a current category that conflicts with vendor history or rule-engine output such that the categorization confidence is below the configured threshold When the timeline is opened Then an inline warning "Categorization may be incorrect" is displayed with a suggested category and confidence score and actions "Accept suggestion" and "Override" When the user selects "Accept suggestion" Then the expense category is updated; a Categorization step is appended with timestamp, device, user, and old→new category; the warning resolves; and ledger postings are updated accordingly When the user selects "Override" and chooses a category (reason required if configured) Then the category is set to the selected value; the warning resolves with state "Overridden"; and an audit log entry includes the override reason
Dismiss and Reopen Gap with Reason
Given any open warning on the timeline (missing receipt, unmatched bank transaction, categorization conflict) When the user selects "Dismiss" and provides a mandatory reason from a configurable list or free text (minimum 10 characters) Then the warning status changes to Dismissed; it is hidden from the default view; counters and progress reflect the dismissal; and an audit log entry captures the dismissal reason, user, and timestamp When the timeline filter is set to include Dismissed Then the dismissed warning appears with status badge "Dismissed" and action "Reopen" When the user selects "Reopen" Then the warning returns to Open state and reappears in default view; counters and progress update accordingly And any material data change relevant to the warning (e.g., new receipt attached) automatically re-evaluates the dismissal and updates state within 60 seconds
Configurable Detection Thresholds and Rules
Given the user opens Settings > Audit Rules for the Provenance Timeline When the user updates one or more of the following: Receipt grace period (days), Match confidence threshold (%), Categorization confidence threshold (%), Vendor variance rule (amount or %) Then the changes are validated (ranges enforced), versioned, saved, and applied And a preview shows the number of affected items before save And re-evaluation of existing items runs and updates warnings and progress within 5 minutes of saving When the user restores a previous rules version Then the prior configuration is reapplied and a version-change audit entry is recorded
Audit Readiness Summary and Progress
Given a timeline has zero open warnings and any dismissed warnings have recorded reasons When the timeline header renders Then an "Audit readiness" badge displays "Ready" with a timestamp of attainment and the progress tracker shows 100% Given a timeline has N open warnings When the timeline header renders Then the badge displays "Issues: N" and the progress tracker reflects the proportion of resolved steps When the user selects "Resolve all" Then the system launches the first unresolved warning’s remediation flow and advances sequentially until all warnings are Resolved or Dismissed with reason, after which the badge updates within 2 seconds
Secure Sharing & Export
"As a freelancer, I want to share a clean, secure timeline packet with my accountant or an auditor so that they can review provenance without accessing my whole account."
Description

Enable secure sharing and export of a single item’s Provenance Timeline as a tokenized link or PDF, with selectable redactions (PII, account numbers), expiration controls, watermarking, and access logging. Include an IRS-ready appendix listing steps, timestamps, approvers, integrity checks, and evidence references. Ensure permission checks align with TaxTidy’s account roles and privacy settings.

Acceptance Criteria
Tokenized Link Share with Expiration and Access Log
Given a user with Share permission on a specific item’s Provenance Timeline And the user sets link expiration to 72 hours and max views to 3 When the user generates a tokenized share link Then the system issues a unique, URL-safe token with at least 128 bits of entropy And the link becomes inaccessible exactly after 72 hours or after 3 successful views, whichever occurs first And each access creates an immutable access log entry with UTC timestamp, IP, user agent, and viewer identity if authenticated And any access after expiration or max views returns HTTP 410 Gone without exposing content and is logged
PDF Export with Selectable Redactions and Watermark
Given a user with Export permission selects PDF export And the user enables redaction for PII and account numbers When the PDF is generated Then all SSNs/TINs and bank/account numbers are redacted to last 4 digits only across body and appendix And a visible watermark (share ID, exporter email, UTC timestamp) appears on every page footer And embedded PDF metadata excludes redacted fields And the PDF is generated within 5 seconds for timelines up to 200 events
Permission Check per Account Roles and Privacy Settings
Given workspace policy requires Owner or Admin to share/export And the item is marked Private to team members without explicit access When a Member without required role attempts to share or export via UI or API Then Share/Export UI controls are disabled or hidden And API requests return HTTP 403 with error code PERMISSION_DENIED and no artifact is created And a security audit log records the denied action with actor, reason, and timestamp
IRS-Ready Appendix Completeness and Formatting
Given a Provenance Timeline includes invoice issued, payment received, bank settlement, receipt captured, and categorization applied steps When the user views a shared link or exports a PDF Then the appendix lists for each step: step name, UTC ISO 8601 timestamp, device/source, approver identity, integrity hash, and evidence reference URI or file ID And the appendix includes an overall checksum for the timeline payload and an appendix version and schema ID labeled IRS-Ready And the appendix validates against the schema with 0 errors
Redaction Rules Applied to All Delivery Channels
Given redactions are enabled for a share link or PDF When the timeline is viewed via web share, downloaded as PDF, or printed Then all PII and account numbers remain redacted consistently across body, thumbnails, and appendices And evidence reference URIs are masked to non-sensitive identifiers (e.g., file-UUID) and do not expose paths or bucket names And no client-side control reveals redacted data; view-source or DOM inspection does not include unredacted values
Revocation and Immediate Enforcement
Given an active share link exists When the owner revokes the link Then the link is removed from active shares and becomes inaccessible within 5 seconds And subsequent access attempts return HTTP 410 Gone and are logged with reason REVOKED And previously downloaded PDFs remain watermarked with share ID to enable traceability

Packet Seal

Applies a cryptographic signature and checksum to the exported PDF and CSV audit trail, producing a verification sheet inside the packet. Anyone can confirm integrity with a public verifier, making alterations obvious. Builds trust in remote audits and safeguards you against claims of post‑fact changes.

Requirements

PAdES-Signed PDF with Verification Sheet
"As a freelancer undergoing a remote audit, I want my exported tax packet PDF to include a verifiable digital signature and verification sheet so that auditors can independently confirm it has not been altered."
Description

Implements standards-based digital signing of the exported tax packet PDF using PAdES with long-term validation, embedding OCSP/CRL responses to enable offline verification over time. Inserts a verification sheet at the front of the PDF summarizing signer identity, trusted timestamp, packet ID and version, SHA-256 hash of the PDF, and checksums for associated exports (e.g., CSV audit trail), plus a QR code and short link to the public verifier. Sealing is executed only after all export transformations (OCR, pagination, compression, annotations) are complete to ensure a stable byte stream; any subsequent byte change invalidates the signature. The feature integrates with the export service, stores signature metadata with the packet record, and ensures common PDF viewers display the signature status without proprietary plugins.

Acceptance Criteria
PAdES LTV Seal on Finalized PDF
Given the packet PDF’s OCR, pagination, compression, and annotation steps have completed and the byte stream is stable When the sealing job executes Then the PDF is signed with PAdES-B-LT (SHA-256) including embedded OCSP/CRL responses and a trusted timestamp (TSA) And the digital signature covers the entire document and validates as "Signed and all signatures are valid" in Adobe Acrobat Reader DC without plugins And the signed file’s SHA-256 remains identical across subsequent downloads of the same packet version
Verification Sheet Contents and Accuracy
Given sealing completes When the verification sheet is inserted as the first page Then it displays signer common name and organization, certificate issuer CN, trusted timestamp, packet ID, packet version, SHA-256 of the signed PDF, and checksums for associated exports (e.g., CSV audit trail), plus a QR code and short link to the public verifier And the QR code decodes to the same HTTPS URL as the short link And recomputing the hashes of the delivered PDF and CSV audit trail exactly matches the values printed on the verification sheet
Offline Long-Term Validation
Given the device has no network connectivity When the signed PDF is validated in a PAdES LTV–compliant validator Then the signature and timestamp validate using only embedded OCSP/CRL/timestamp data with no online fetches And the validation time is anchored to the embedded trusted timestamp (TSA)
Public Verifier Resolution and Consistency
Given internet access When the QR code on the verification sheet is scanned or the short link is opened Then the public verifier loads over HTTPS and shows packet ID, packet version, signer CN, trusted timestamp, PDF SHA-256, and CSV checksum And the verifier’s displayed values exactly match those on the verification sheet and those recomputed from the downloaded artifacts And uploading or referencing a modified PDF results in a clear "invalid/altered" status
Tamper Detection After Sealing
Given a sealed PDF is altered by changing any byte (e.g., metadata edit or page append) When opened in a PDF signature validator Then the signature status is "invalid" with a reason indicating the document has been modified after signing And recomputing the PDF’s SHA-256 no longer matches the value printed on the verification sheet
Sealing Gated by Transform Completion; No Post-Seal Mutations
Given export transform jobs are tracked for OCR, pagination, compression, and annotations When any transform remains pending Then the sealing job must not start When the sealing job completes Then the system prevents any further modification of the signed byte stream; any requested change triggers a new packet version and a new signature
Signature Metadata Persisted with Packet Record
Given sealing succeeds When the packet record is queried via the internal API Then signature metadata is present and immutable for that version, including: signature ID, signer subject DN, certificate serial number and issuer, TSA timestamp and serial, embedded OCSP/CRL payloads or digests, PDF SHA-256, CSV checksums, packet ID, packet version, and sealing software version And audit logs record the sealing event with timestamp and operator/service ID
CSV Audit Trail Integrity Manifest
"As an auditor, I want an integrity manifest and signature for the CSV audit trail so that I can verify its contents match what the system produced without trusting the sender."
Description

Generates a cryptographic integrity manifest describing the CSV audit trail (filename, size, SHA-256 checksum, packet ID, version) and signs it using a detached CMS/PKCS#7 signature. Exports the CSV alongside its manifest and signature and references their checksums on the PDF’s verification sheet to bind the set as one sealed packet. The manifest supports streaming-safe hashing for large files and protects against partial or reordered row tampering. Verifiers re-compute the CSV hash, validate the signature, and surface clear pass/fail results, enabling recipients to trust the audit trail without relying on the sender’s environment.

Acceptance Criteria
Export outputs CSV, integrity manifest, and detached signature sidecars
Given a user exports a sealed packet with the CSV audit trail enabled When the export completes Then the output includes three files adjacent to the PDF: - audit.csv - audit.csv.manifest.json - audit.csv.sig And the manifest JSON contains fields with exact values and formats: - filename equals the CSV basename (e.g., audit.csv) - size equals the CSV byte size - sha256 is a 64-character lowercase hex SHA-256 of the CSV bytes - packetId is a valid UUID v4 - version matches SemVer MAJOR.MINOR.PATCH And audit.csv.sig is a CMS/PKCS#7 detached signature over the manifest bytes using SHA-256
Public verifier Pass for untampered set
Given the exported CSV, manifest, and signature are unmodified When they are submitted to the public verifier Then the verifier recomputes the CSV SHA-256 and it equals manifest.sha256 And the CMS/PKCS#7 detached signature over the manifest validates with an intact, unexpired signer certificate chain And the verifier returns a clear Pass result displaying packetId, version, filename, size, and sha256
Tamper detection: truncation, cell edit, row reordering cause Fail
Given three modified variants of the original CSV: (a) truncated rows, (b) one cell edited, (c) two rows reordered When each variant is verified against the original manifest and signature Then verification fails for each with a Fail result and a reason containing "hash mismatch" And the verifier identifies the file under test and does not produce a Pass result for any modified variant
Streaming-safe hashing for large CSVs (>=2 GB)
Given a generated CSV audit trail of at least 2 GB When the manifest is created Then the SHA-256 is computed in streaming mode without loading the entire file into memory And peak memory usage during hashing is less than or equal to 128 MB And the resulting sha256 equals an independently computed reference SHA-256 And the operation completes successfully without out-of-memory errors
PDF verification sheet binds CSV, manifest, and signature by checksums
Given the packet PDF is generated with a verification sheet When the verification sheet is opened Then it lists for CSV, manifest, and signature: filename, size, and SHA-256 (where applicable), plus packetId and version And the listed values exactly match the actual exported files and manifest And altering any sidecar file causes a mismatch when cross-checked against the PDF values, indicating the packet is no longer sealed
Filename and size binding prevents wrong-file substitution
Given a CSV different from the one described in the manifest is provided during verification (renamed or substituted) When verification is attempted Then the verifier checks that manifest.filename equals the provided CSV basename and manifest.size equals the CSV byte size And verification fails with a Fail result and descriptive reason if filename or size differ or if the SHA-256 does not match
Public Web Verifier and QR Validation
"As a tax preparer, I want a public verification page and QR code to confirm a packet’s authenticity so that I can trust documents received from clients without special software."
Description

Provides a public, privacy-preserving verification page where anyone can upload the sealed PDF or paste a packet ID/hash to validate authenticity and integrity without an account. The verifier checks the PDF’s PAdES signature, embedded OCSP/CRL, trusted timestamp, and recomputes SHA-256 checksums for companion files such as the CSV when supplied. The QR code and short link printed on the verification sheet resolve to this verifier with a non-identifying reference. The service returns clear pass/fail status and diagnostics, supports client-side hashing where feasible, rate-limits abuse, and publishes CLI and offline verification instructions for audit firms with restricted environments.

Acceptance Criteria
Upload Sealed PDF: PAdES Signature and Timestamp Validation
Given I am on the public verifier without being logged in When I upload a sealed TaxTidy PDF ≤ 25MB Then the verifier validates the PAdES signature against a trusted CA store and shows Pass with signer CN, signing time (UTC), and certificate chain details Given the uploaded PDF has an invalid or missing signature, a revoked certificate via OCSP/CRL, or a timestamp outside validity When verification runs Then the verifier shows Fail with explicit reason codes (e.g., SIGNATURE_MISSING, CERT_REVOKED, TIMESTAMP_INVALID) and a human-readable summary Given a valid packet When verification completes Then the result is returned within 5 seconds at p95 on a typical broadband connection and no account is required Given any upload When verification completes Then no file contents are stored beyond transient processing and no PII from the file is logged
Manual Entry: Packet ID or Hash Validation Without Account
Given I have a packet short ID (8–12 chars) or a full 64-hex SHA-256 hash When I paste it into the verifier and submit Then the verifier resolves the reference without requiring an account and returns a clear Pass/Fail with the same reason codes as file upload Given the input format is invalid When I submit Then I receive inline validation guidance and no backend call is made Given a valid reference When verification completes Then no identifying information about the owner is displayed and only non-identifying metadata (packet creation date, algorithm, version) is shown
QR Code and Short Link: Resolve to Public Verifier with Non-identifying Reference
Given I scan the QR code or visit the short link on a TaxTidy verification sheet When the page opens Then it loads the public verifier over HTTPS with the packet reference prefilled and contains no PII in the URL query Given the prefilled reference is invalid or expired When I load the page Then I see a safe error and instructions to upload the PDF or enter a hash Given a mobile device When the QR is scanned Then the verifier is mobile-friendly and completes verification with the same outcomes as desktop
Companion Files: SHA-256 Checksum Recalculation and Match
Given a sealed packet PDF lists companion files (e.g., CSV audit trail) and their SHA-256 checksums When I also supply the companion CSV Then the verifier recomputes the checksum client-side when feasible, compares it to the embedded value, and shows Pass only if all listed files match Given one or more companion files are missing or mismatched When verification runs Then the verifier shows Fail with a list of missing/mismatched filenames and their expected vs actual hashes Given no companion files are supplied When verification runs Then the verifier shows a companion files not supplied notice but still validates the PDF signature and returns Pass/Fail accordingly
Client-Side Hashing: Browser-Only Verification Path
Given my browser supports Web Crypto and the file size ≤ 50MB When I choose Privacy Mode (local hashing) Then the verifier computes SHA-256 locally, only transmits the hash and signature metadata, and renders the same Pass/Fail outcome as server-side processing Given the environment lacks required capabilities or the file exceeds the limit When I attempt Privacy Mode Then the UI clearly falls back to server-side processing with a notice Given Privacy Mode is used When verification completes Then no file bytes are uploaded and the network traffic contains only hash/reference metadata
Abuse Protection: Rate Limiting and Error Responses
Given an IP exceeds 60 verification requests per minute or 500 per hour When limits are breached Then the verifier responds with HTTP 429, includes a Retry-After header, preserves normal performance for in-limit users, and logs the event Given repeated abuse over a 24-hour window When thresholds are met Then temporary blocking is applied and a CAPTCHA challenge is presented on subsequent attempts, without blocking legitimate QR/short-link resolutions within normal use patterns Given a legitimate user under limit When they verify Then no additional friction (e.g., CAPTCHA) is shown
Audit Firm Support: CLI and Offline Verification Instructions
Given a restricted environment with no web access When an auditor opens the offline guide Then it provides step-by-step commands using standard tools (e.g., pdfsig/openssl/sha256sum) to validate the PAdES signature, embedded OCSP/CRL status, trusted timestamp, and companion file checksums Given the CLI instructions and sample assets When followed against a provided test packet Then the results reproduce the same Pass/Fail outcomes and reason codes as the web verifier Given the guide and any helper CLI are published When accessed Then they are publicly accessible without an account, downloadable as PDF/text, versioned, and accompanied by SHA-256 checksums (and code signatures where applicable)
HSM-Backed Key Management and Trusted Timestamping
"As a security-conscious user, I want TaxTidy to use managed signing keys and trusted timestamps so that my sealed packets remain defensible and time-bound."
Description

Uses a hardware security module or cloud KMS to generate and protect signing keys with role-based access controls, environment isolation, and periodic rotation. Applies RFC 3161 trusted timestamps to every signature to anchor the seal to an independent, auditable time source. Embeds OCSP/CRL material to enable long-term validation and configures automated certificate renewal before expiry. Defines operational runbooks for key rollover and compromise response, and updates verifier trust stores seamlessly during rotations. All key usage is audited and alerts are emitted on anomalous activity.

Acceptance Criteria
Non-Exportable Signing Keys Enforced in HSM/KMS with RBAC and Environment Isolation
Given a production deployment, when a signing key is created, then the key is generated inside the HSM/KMS as non-exportable and scoped to production only. Given a user without the Signer role, when they attempt to use or export the signing key, then the operation is denied and an audit log entry is recorded with user ID and reason. Given privileged operations (key create/destroy/policy change), when requested, then dual-approval (2 distinct approvers) and MFA are required and enforced by policy. Given dev and staging environments, when listing keys, then production keys are not visible or usable due to account/project isolation. Given a packet is sealed, when the key is used, then an immutable audit record is stored with key ID, requestor service identity, operation type, timestamp, and outcome (success/failure).
Periodic Key Rotation with Zero-Downtime and Backward Verification
Given a 90-day rotation policy, when a signing key reaches 90 days of age, then a new key pair is provisioned and activated for new signatures within 15 minutes and the old key is retired from signing but retained for verification. Given a rotation event, when verifying packets signed before the rotation, then verification succeeds using the embedded chain and updated verifier trust store without user action. Given the rotation cutover, when signatures are produced, then no signing outage exceeds 60 seconds and any queued requests are processed within 5 minutes. Given two consecutive rotations, when verifying packets across three key generations, then all verifications pass. Given a rotation failure, when detected, then an alert is emitted within 5 minutes and the system automatically rolls back to the prior active key without data loss.
RFC 3161 Trusted Timestamps Applied to Every Packet Seal
Given a packet sealing request, when the signature is created, then a RFC 3161 timestamp token from the configured TSA is attached to the signature. Given the TSA is unreachable, when sealing is attempted, then the operation fails within 10 seconds with a user-visible error and no packet is exported. Given system clock skew, when verifying a sealed packet, then the verifier relies on the TSA time from the timestamp token, not the local system time. Given a sealed packet, when verified with the public verifier, then both the signature and the RFC 3161 timestamp validate against the TSA certificate chain. Given any post-seal modification to the packet, when verification is attempted, then validation fails and indicates tampering.
Embedded OCSP/CRL for Long-Term Offline Validation (LTV)
Given a PDF and CSV audit trail are sealed, when exporting the packet, then OCSP responses or CRLs for the signer and TSA chains are embedded to enable LTV. Given the verifier is offline, when validating the packet, then verification succeeds using only the embedded revocation information. Given fresh revocation data is required, when sealing, then embedded OCSP responses are no older than 24 hours; otherwise sealing fails with a clear error. Given the signer’s certificate is revoked after sealing, when validating the packet, then verification still succeeds if the RFC 3161 timestamp predates the revocation. Given the packet is validated 3 years later, when the verifier runs, then the packet verifies successfully without fetching network revocation data.
Automated Certificate Renewal and Trust Store Updates
Given a certificate has 30 days until expiry, when the renewer runs, then a new certificate is issued, deployed, and starts being used for new signatures within 24 hours with no downtime. Given renewal deployment completes, when the public verifier is used, then its trust store is updated automatically to include the new chain and continues to validate older packets. Given a renewal attempt fails, when detected, then an alert is emitted within 5 minutes and retries are performed with exponential backoff for up to 12 hours. Given staging preflight, when a canary renewal is executed, then end-to-end signing and verification pass before promoting to production. Given a CA chain change occurs during renewal, when verification runs, then packets validate with the new chain and no pinning errors are raised.
Comprehensive Audit Logging and Anomaly Alerting for Key Usage
Given any key operation (create, sign, rotate, destroy), when it occurs, then a tamper-evident log entry is written with timestamp, actor/service ID, key ID, operation, request origin, and result. Given logs are produced, when ingested by the SIEM, then they are available for query within 2 minutes and integrity is ensured via hash chaining. Given an anomaly (e.g., usage outside approved hours, spike >3x baseline hourly rate, access from new geo), when detected, then a high-severity alert pages on-call within 5 minutes and includes correlated context. Given a log tampering attempt or gap is detected in the hash chain, when verification runs, then an alert is emitted and the affected window is quarantined for investigation. Given retention requirements, when queried, then logs for key usage are available for at least 7 years.
Key Compromise Response and Verifier Trust Store Update Runbook
Given a simulated key compromise drill, when the runbook is executed, then within 60 minutes the compromised key is revoked, a new key is provisioned and activated, verifier trust stores are updated, and user communications are sent. Given active compromise indicators, when detected, then new signing is halted within 2 minutes, pending requests are rejected with a clear error, and containment actions are initiated. Given packets sealed prior to compromise with valid RFC 3161 timestamps, when verified after the incident, then they continue to validate successfully. Given incident closure, when postmortem is produced, then MTTD is under 5 minutes and MTTR is under 60 minutes with corrective actions tracked to completion. Given the runbook changes, when updated, then changes are versioned, approved by security, and rehearsed in staging within 14 days.
Export Pipeline Sealing Orchestration
"As a user exporting my records, I want sealing to run automatically at the end of the export process with clear versioning so that I know which copy is final and tamper-evident."
Description

Orchestrates sealing as the terminal step of the export pipeline to guarantee immutability of sealed artifacts. Produces a deterministic packet version, writes sealed outputs to append-only object storage, and blocks any post-seal mutation. Re-exports create a new version and seal while preserving prior versions for reference. The pipeline exposes asynchronous job status with progress, retries idempotently on transient errors, and emits in-app events and webhooks when sealing completes or fails, allowing clients to update UI state and notify users.

Acceptance Criteria
Terminal Sealing Step Enforces Immutability
Given the export pipeline reaches the terminal sealing step When sealing completes successfully Then all sealed artifacts are marked immutable and any mutation attempt returns 409 with an audit log entry Given a sealed version exists When a client attempts to modify artifacts in place (overwrite or delete) Then the operation is blocked and the system instructs to create a new version instead Given storage layer controls When an overwrite of an existing sealed object key is attempted Then the request fails with a precondition or permission error and no data is changed
Deterministic Versioning and Re-Export Behavior
Given identical inputs and export configuration When a re-export is initiated Then a new sequential version_number is created and artifact checksums remain identical to the prior version Given multiple exports over time When listing packet versions Then all prior versions are present, ordered by created_at, and individually retrievable Given inputs change between exports When sealing completes Then artifact checksums differ and version metadata records predecessor_version and created_at timestamps Given a sealed version When requesting version metadata Then it returns version_number (integer), content_hash (SHA-256 hex), created_at (ISO 8601), and source_config_fingerprint (string)
Append-Only Object Storage Write and Preservation
Given a new sealing operation starts When uploading artifacts to object storage Then objects are written with unique keys and conditional writes prevent overwrites of existing sealed objects Given a sealed object exists When a delete or overwrite is requested via application credentials Then the request is denied (403/409) and an audit event is recorded Given lifecycle rules on the bucket When evaluating retention Then sealed objects are exempt from deletion or rewrite policies for the defined retention period
Asynchronous Job Status and Progress API
Given a client creates an export When polling the job status endpoint with job_id Then status transitions only among [queued, running, sealing, uploading, completed, failed] and progress is monotonically non-decreasing from 0 to 100 Given a running job When requested Then the response includes status, progress (0–100), current_step, and estimated_steps_remaining (optional) Given a completed job When requested Then the response includes version_number, artifact_urls, content_hashes, created_at, and a terminal status of completed (progress=100) Given a failed job When requested Then the response includes failure_code, failure_message, retryable (boolean), and no artifact urls
Idempotent Retries on Transient Failures
Given a transient failure occurs (e.g., network timeout, 5xx) When the client retries with the same idempotency key Then only one sealed version is produced and all responses reference the same job_id Given duplicate submissions arrive concurrently with the same idempotency key When processed Then the server returns the existing job resource without creating duplicates Given a non-transient validation error When retried with the same idempotency key Then the job remains failed and no sealed artifacts are created
Event and Webhook Emission on Completion and Failure
Given sealing completes successfully When events are emitted Then in-app and webhook payloads include event_id, job_id, account_id, user_id, version_number, artifact_urls, content_hashes, created_at, and signature_reference Given webhook delivery When the subscriber does not return 2xx Then deliveries are retried with exponential backoff for up to 24 hours and include the same event_id for deduplication Given sealing fails When events are emitted Then payloads include failure_code, failure_message, retryable, and no artifact urls; delivery and retry semantics are identical to success events Given a webhook secret is configured When payloads are sent Then each request is HMAC-SHA256 signed and receivers can verify signatures; invalid signatures result in 401 and are retried
Integrity Verification Sheet and Artifact Checksums
Given a sealed packet is downloaded When the PDF is opened Then a verification sheet is present showing version_number, created_at, checksum algorithm, and checksums for each artifact Given the sealed PDF and CSV audit trail When SHA-256 is computed locally Then the resulting hashes match the values in the verification sheet and job metadata Given the public verifier tool When the packet is submitted unaltered Then it reports the signature as valid; if any artifact bits are changed, it reports tampered
Seal Event Audit Logging and Retention
"As a compliance manager, I want detailed seal event logs and retained proofs so that I can produce evidence for audits and investigations."
Description

Captures immutable, tamper-evident logs for each sealing event, including user and workspace identifiers, packet ID and version, document and CSV hashes, signer key ID, TSA token, timestamp, IP address, and verifier link. Stores signature metadata and validation proofs under a defined retention policy with optional WORM storage. Exposes read-only audit entries to admins and includes them in data exports to support SOC 2 evidence and IRS audit requests. Provides search and export tools for investigators to rapidly assemble proof of integrity.

Acceptance Criteria
Seal Event Log Completeness
Given a user seals a packet containing a PDF and CSV audit trail When the sealing process completes successfully Then the system writes exactly one immutable audit record containing: event_id, user_id, workspace_id, packet_id, packet_version, pdf_sha256, csv_sha256, signer_key_id, tsa_token (raw and serial), sealing_timestamp (UTC ISO-8601, ms precision), source_ip, verifier_link (https), signature_algorithm, signature_value, certificate_chain, validation_proofs (OCSP/CRL) And the stored hashes match the computed hashes of the exported PDF and CSV And the verifier_link embedded in the packet resolves to the same event_id And the record is queryable within 5 seconds of sealing
Tamper-Evident Storage and WORM Enforcement
Given the audit log store is configured When a seal event is recorded Then the record is append-only with an entry_hash and prev_entry_hash (hash chain) and a store_signature And any attempt to update or delete an existing record via API or DB results in HTTP 403 and is itself logged as a security event And a daily integrity check recomputes the chain and returns status "OK" with the expected chain_root when no tampering is present And when WORM is enabled for the workspace, deletion and update operations are technically blocked by storage controls and retention cannot be shortened below the configured policy
Admin Read-Only Audit Access Control
Given a workspace admin is authenticated When they list or view seal audit records in their workspace Then they can read full record contents and download validation proofs And any create/update/delete endpoints are not exposed in the UI and return HTTP 403 if called directly And a non-admin in the same workspace receives HTTP 403 for the same endpoints And a user in a different workspace cannot access records outside their tenant (HTTP 404/403)
Investigator Search and Filter
Given at least 100,000 seal audit records exist in a workspace When an investigator searches by any combination of date range, packet_id, packet_version, user_id, signer_key_id, tsa_serial, source_ip, event_id, and hash prefix (first 8+ chars) Then results are correctly filtered, sorted by sealing_timestamp desc by default, and paginated And the first page (size 50) returns within 2 seconds at the 95th percentile under nominal load And total result count and applied filters are included in the response
Audit Export for SOC 2 and IRS
Given an admin selects a date range or a set of packet_ids When they request an audit export Then the system produces downloadable CSV and JSON files containing all matching audit records with all fields and embedded validation proofs And the export includes a manifest.json with file checksums (SHA-256) and record counts, plus a detached signature to verify export integrity And exports up to 100,000 records complete within 60 seconds and are retained for download for 7 days via pre-signed URLs And the exported records exactly match the search criteria and counts
Retention Policy, Legal Hold, and Deletion Logging
Given a workspace retention policy (default 7 years) is configured When records exceed the retention period Then they are purged automatically within 24 hours unless tagged with a legal_hold And when legal_hold is applied, records are preserved regardless of retention until the hold is cleared by an authorized admin And all purge actions (counts, ids, timestamps) are written to a separate immutable retention_audit log And retention cannot be shortened while WORM is enabled; attempts to change are rejected and logged
Verifier Link and TSA Validation
Given a sealed packet’s verification sheet includes a verifier_link When the link is opened without authentication Then the public verifier displays the event_id, packet_id/version, hash values, and validation status as "Valid" when the signature, certificate chain, OCSP/CRL, and TSA token are all verified And if any element fails validation or the content hashes differ from what is stored, the status is "Tampered" with a clear reason code And the verifier response includes a timestamp, and a copy of the validation proofs for download
In-App Seal Status and Sharing UX
"As a mobile-first freelancer, I want clear in-app seal status and easy sharing of verification links so that I can confidently send sealed packets from my phone."
Description

Surfaces sealing status across mobile and web with clear states (Pending, Sealed, Failed) and a details view that shows signer, timestamp, packet ID, version, and content hashes. Provides one-tap actions to copy the verification link, download the verification sheet, or share via the system share sheet. Educates users on what sealing means and warns when upstream changes require a new export. Presents actionable errors with retry options and sends notifications when sealing completes, ensuring a smooth mobile-first workflow.

Acceptance Criteria
Cross-Platform Seal Status Indicators
- Given a user views the Packets list on mobile or web When a packet’s sealing job state is pending Then the packet displays status "Pending" and share/copy/download actions are disabled - Given a user views the Packets list or packet header in details When the packet’s sealing job state is sealed Then the packet displays status "Sealed" with a success indicator and share/copy/download actions are enabled - Given a user views the Packets list or packet header in details When the packet’s sealing job state is failed Then the packet displays status "Failed" and a "View details" link to the error is available
Sealed Packet Details Metadata
- Given a sealed packet When the user opens the Seal Details view Then the view shows signer name, signing timestamp in the device’s local timezone, packet ID, packet version, PDF SHA-256, and CSV SHA-256 - Given the user compares the displayed hashes to the verification sheet When they open the downloaded verification sheet for the same packet version Then the PDF and CSV hashes exactly match the ones shown in the Seal Details view - Given the public verifier is opened via the verification link When the verifier loads Then the packet ID and version in the verifier match the Seal Details values
Share, Copy Link, and Download Verification Sheet
- Given a sealed packet When the user taps "Share" Then the native system share sheet opens with a prefilled verification link to the public verifier for the packet’s ID and version - Given a sealed packet When the user taps "Copy verification link" Then the link is placed on the clipboard and a confirmation toast is shown - Given a sealed packet When the user taps "Download verification sheet" Then a PDF verification sheet is provided; on mobile the system share sheet opens with the PDF attached; on web the PDF downloads; the file name contains the packet ID and version - Given the packet status is Pending or Failed When the user attempts to use share, copy, or download actions Then the action is prevented and the user is informed of the current status
Upstream Change Warning and Re-export Prompt
- Given a previously sealed packet When upstream source data included in the packet changes after export (e.g., an expense edit or new transaction) Then the packet details show a warning banner that the packet is outdated and a primary CTA "Export new version" - Given an outdated packet When the user taps share, copy link, or download Then a modal explains that a new export is recommended and offers "Export new version" and "Continue anyway" options, defaulting to re-export - Given the user chooses "Export new version" When the export begins Then the packet status updates to "Pending" for the new version and the banner clears upon successful sealing
Actionable Error Handling and Retry
- Given a packet with sealing status Failed When the user opens the details view Then an error panel shows a human-readable reason (mapped from error code), a "Retry seal" action, and a link to troubleshooting - Given a packet with sealing status Failed When the user taps "Retry seal" Then the status transitions to Pending and, on success, to Sealed; on repeat failure, the error panel updates with the latest error - Given transient network loss during retry When connectivity is restored within the session Then the retry resumes automatically or presents the "Retry seal" action again without losing context
Sealing Completion Notifications
- Given a user initiates an export that triggers sealing When sealing completes successfully while the app is backgrounded on mobile Then the user receives a push notification "Packet sealed" that, when opened, deep-links to the packet’s details view - Given sealing fails while the app is backgrounded on mobile When the failure is recorded Then the user receives a push notification "Sealing failed" with a "Retry" deep link to the error panel - Given the user is active in-app on mobile or web when sealing completes When the status transitions to Sealed Then an in-app toast is shown with "View details" and no duplicate push notification is sent
Sealing Education and Verification Guidance
- Given a user views the seal status or details for any packet When the user taps "What does sealing mean?" Then an in-app explainer appears describing sealing, integrity verification, and a short 3-step guide to verify using the public verifier - Given the explainer is shown When the user taps "Open verifier" Then the public verifier opens with the packet ID and version prefilled - Given the user has dismissed the explainer once When they view other sealed packets Then the education entry point remains available but is not auto-expanded

Auditor Portal

Generates a read‑only, time‑boxed link to the exact packet with scoped access to supporting evidence. Auditors can search exhibits, filter by policy flag, and leave item‑level questions that thread directly to the page. You stay in control with expiry dates and no exposure of balances or unrelated clients.

Requirements

Scoped Read-Only Packet Link
"As a freelancer under audit, I want to share a read-only link to a specific tax packet so that the auditor can review exactly what’s relevant without altering anything or seeing other data."
Description

Implement backend and UI to generate unique, signed URLs that open a read-only, auditor-facing view of a specific IRS-ready tax packet and its linked exhibits. Scope is enforced server-side via token claims (tenant, packet ID, permissions) validated on every request, preventing navigation outside the packet and blocking any edits or uploads. Links are non-guessable, compatible with mobile and desktop, and render a minimal interface focused on packet sections and evidence. Outcome: auditors get precise, controlled access to the relevant packet without risking data leakage or modification.

Acceptance Criteria
Generate Signed, Scoped Read-Only Link for Specific Packet
Given I am an authenticated tenant admin on packet P within tenant T, When I request "Create Auditor Link" with expiry set to 14 days, Then the API responds 201 with {url, expiresAt, packetId:P, tenantId:T, permissions:["read"]}. Given the generated URL, When the token is inspected by the server, Then the claims include tenantId=T, packetId=P, permissions contains "read", and exp is in the future. Given two links are generated for the same packet, When I compare the URLs, Then each URL is unique. Given the generated URL, When inspecting the token length, Then the token is at least 43 base64url characters (>=128 bits entropy) and non-sequential. Given a single character in the token is altered, When I attempt to load the link, Then access is denied with 401/403 and no packet identifiers are revealed in the response body.
Validate Token Claims on Every Request
Given an auditor opens the link, When the app loads HTML, exhibits, or API data, Then each request is authorized by validating signature, exp, tenantId, packetId, and permissions from the token. Given the token is expired, When the auditor attempts to load the page or any resource, Then access is denied and the UI displays "Link expired" without exposing packet metadata. Given the token tenantId or packetId does not match a requested resource, When the request is made, Then the server responds 403 and returns no data. Given the token permissions do not include "read", When a GET request is made, Then access is denied with 403.
Enforce Read-Only Permissions and Block Mutations
Given an auditor link, When the auditor attempts any POST, PUT, PATCH, or DELETE operation, Then the server responds 403 and no data is changed. Given an auditor link, When the interface renders, Then all edit, upload, delete, or annotate controls are hidden or disabled. Given any attempt to request an upload URL for exhibits, When called with the auditor token, Then the server does not issue an upload URL and responds 403.
Prevent Navigation Outside Scoped Packet and Tenant
Given an auditor link for packet P in tenant T, When the auditor alters the URL or parameters to target packet Q or a different tenant, Then the server responds 404/403 and returns no data. Given the auditor view, When following any internal link, Then navigation remains within packet P with no access to dashboards, other clients, or packet listings. Given arbitrary query parameters referencing other resource IDs, When the request is made, Then the server ignores them and uses the token's claims to scope the response.
Minimal Auditor Interface With No Sensitive Leakage
Given an auditor link is opened, When the page renders, Then only packet title, filing period, sections, and linked exhibits are displayed; balances, bank account numbers, unrelated client names, and global navigation are not shown. Given network responses from the auditor view, When inspecting payloads, Then fields for unrelated clients, global balances, and user PII not required by the packet are absent. Given the auditor view is served, When response headers and meta tags are inspected, Then Cache-Control is private and robots directives include noindex.
Cross-Device Compatibility for Read-Only Auditor View
Given the auditor link, When opened on the latest two versions of Chrome, Safari, and Edge (desktop) and Safari (iOS) and Chrome (Android), Then the view loads successfully, packet sections render, and exhibits open without errors. Given a mobile viewport of 320px width, When the auditor view is loaded, Then there is no horizontal overflow, text is readable, and controls are tappable with a minimum 44px touch target. Given a PDF or image exhibit is opened on mobile, When the user views the file, Then it is readable with zoom/pan via the built-in viewer and no edit/upload options are exposed.
Link Expiry and Owner Revocation
Given an auditor link with expiresAt timestamp, When current time passes expiresAt (±60s clock skew), Then the link denies access and the UI shows "Link expired". Given the packet owner revokes the link before expiry, When the auditor uses the revoked URL, Then access is denied and the UI shows "Link revoked". Given the owner generates a new link after revocation, When the auditor uses the new link, Then access is granted via the new URL while the revoked URL remains invalid.
Time-Boxed Access & Revocation
"As an account owner, I want to set expiry dates and revoke auditor links so that I can control when access ends and limit exposure during and after the audit."
Description

Provide controls to set an access window (start/end date-time), optional view/download limits, and manual revoke for auditor links. Expired or revoked links immediately become invalid and display a clear expiration message. Allow link regeneration with new tokens while preserving prior links’ logs. Send owner notifications on first access, impending expiration, and post-expiry attempts. Outcome: owners remain in full control of when and how long auditors can access materials.

Acceptance Criteria
Access Window Enforcement (Start/End)
Given an auditor link with a defined start date-time and end date-time When the link is visited before the start date-time Then access is denied, a message states "This link is not available until <start date-time>", no packet content is displayed, and the attempt is logged Given the same link When the link is visited at or after the start date-time and before the end date-time Then access is granted and the visit is logged Given the same link When the link is visited at or after the end date-time Then access is denied within 5 seconds, a message states "This link has expired", no packet content is displayed, and the attempt is logged
Manual Revocation of Auditor Link
Given an active auditor link When the owner selects Revoke and confirms Then the link becomes invalid across all devices within 5 seconds, no packet content is accessible, and a message states "This link has been revoked by the owner" And a revocation event with timestamp and actor is recorded in the audit log And subsequent access attempts remain blocked and are logged
View and Download Limits Enforcement
Given an auditor link with an optional view limit V and download limit D (unset means unlimited) When an auditor successfully loads the packet view Then the view count increments and remaining views = V - used (if V is set) And if remaining views reaches 0, further views are blocked with message "View limit reached" and attempts are logged Given the same link When an auditor successfully downloads a file Then the download count increments and remaining downloads = D - used (if D is set) And if remaining downloads reaches 0, further downloads are blocked with message "Download limit reached" while views remain allowed if V is not exhausted Given intermittent failures When a view or download does not complete (non-200 or client abort before content render/transfer threshold) Then the limit counters do not increment
Link Regeneration with Token Rotation and Log Preservation
Given an existing auditor link token A When the owner selects Regenerate Link Then a new token B is issued, token A immediately becomes invalid, and all historical logs for token A remain accessible And the owner may set a new access window for token B; otherwise, the previous window is retained And attempts to use token A show a message "This link is no longer valid" and are logged as attempts on a retired token And the audit log records a regeneration event linking token A to token B
Owner Notifications for Access Events
Given an auditor link When the first successful access occurs for that link Then the owner receives a notification within 2 minutes containing link identifier, timestamp (UTC), and originating IP/user-agent (if available), and the notification is logged Given an active link with an end date-time When the current time is 24 hours before the end date-time Then the owner receives an impending-expiration notification once, containing the scheduled end date-time and link identifier, and the notification is logged Given an expired or revoked link When any access attempt occurs Then the owner receives a post-expiry attempt notification within 5 minutes containing the attempt timestamp and link identifier, and the attempt is logged
Invalid Link Messaging and Data Non-Disclosure
Given a link that is expired, revoked, not-yet-started, or has exceeded view/download limits When an auditor visits the link Then a clear reason-specific message is displayed and no packet content, file names, exhibit lists, balances, client names, or other metadata are shown And modifying URL parameters or headers does not bypass the invalid state And all such attempts are logged with reason codes (expired, revoked, not-yet-started, view-limit, download-limit)
Exhibit Search & Policy Filters
"As an auditor, I want to search exhibits and filter by policy flags and metadata so that I can efficiently locate the evidence needed to validate specific deductions and entries."
Description

Enable full-text and metadata search across packet exhibits (receipts, invoices, bank-matched items) with filters for policy flags (e.g., missing receipt, potential personal expense), date range, category, vendor, amount range, and match status. Reuse TaxTidy’s OCR/extraction index to power fast, scoped queries. Results display key metadata and open directly to the relevant page/line within the exhibit viewer. Outcome: auditors can quickly find supporting evidence aligned to compliance policies, improving review speed and accuracy.

Acceptance Criteria
Full-Text and Metadata Search Across Exhibits
Given an auditor with an active, unexpired portal link scoped to packet X When they submit a search query between 2 and 64 characters Then the system returns only exhibits within packet X whose OCR text or indexed metadata contains the query terms Given the query contains a quoted phrase When the search is executed Then only exhibits containing the exact phrase are returned Given multiple space-separated terms without operators When the search is executed Then the terms are matched using AND by default; OR and "-" operators are supported Given a search is executed When measuring end-to-end response time on a packet with up to 5,000 exhibits Then the 95th percentile latency is ≤ 1500 ms Given no exhibits match When the search is executed Then a no-results state is shown and no exhibit metadata outside packet X is revealed
Policy Flag Filtering
Given one or more policy flags are selected in Filters When the search is executed Then only exhibits tagged with any of the selected flags are returned Given no policy flags are selected When the search is executed Then results are not restricted by policy flags Given multiple policy flags are selected When combined with other filter types Then flag selections are ORed within flags and ANDed with other filter types Given a policy flag is cleared When the search is executed Then the cleared flag no longer affects the results and the URL reflects the current filter state
Date, Amount, Category, Vendor, and Match Status Filters
Given a date range is set When the search is executed Then only exhibits with exhibit date within the inclusive start and end dates are returned Given an amount range is set When the search is executed Then only exhibits with amounts within the inclusive min and max are returned using the packet currency Given one or more categories or vendors are selected When the search is executed Then results include only exhibits matching any selected category and any selected vendor respectively Given a match status is selected (Matched or Unmatched) When the search is executed Then results include only exhibits with the selected status Given any filter field has invalid input (e.g., min > max, bad date) When the user attempts to apply filters Then validation prevents execution and displays an inline error Given filters from different types are set When the search is executed Then results satisfy all selected filter types (logical AND)
Result List Metadata, Highlighting, and Deep Link into Viewer
Given search results are returned When rendering each result row Then it displays exhibit type, date, vendor, amount with currency, category, policy flags, and match status Given a result contains text matches When rendered Then a snippet shows up to two matched terms highlighted Given a result is opened When the auditor clicks it Then the exhibit viewer opens at the exact page and region of the first match and navigation allows moving to next/previous matches Given a result is opened for a file ≤ 10 MB When measured end-to-end Then the 95th percentile time from click to viewer ready is ≤ 800 ms Given the deep-linked URL is reloaded When the page loads Then the same exhibit, page, and highlight state are restored
Scoped Access, Read-Only, and Link Expiry
Given an active auditor link scoped to packet X When search or filters are used Then only exhibits from packet X are queried and shown and balances or unrelated client data are never displayed Given a request attempts to access an exhibit outside packet X or a non-searchable field When processed by the API Then a 403 or 400 is returned with no sensitive metadata in the body Given the auditor portal is used When any write action (create, update, delete) is attempted Then the API returns 405 and no data is changed Given the auditor link has expired When the auditor attempts to load search or results Then a 401/expired state is shown and no prior results or metadata are accessible
Pagination, Sorting, and Result Counts
Given more than 25 results exist When results are displayed Then they are paginated in pages of 25 with total count and Next/Previous controls Given a sort option is selected (Relevance default; Date asc/desc; Amount asc/desc) When the search is executed Then results are ordered accordingly and the sort is reflected in the URL Given filters or query change When results are displayed Then the total result count updates to reflect the current query and filters Given pagination state exists When the URL is shared within the valid link window Then the recipient sees the same query, filters, sort, and page
Item-Level Q&A Threads
"As an auditor, I want to ask item-level questions and receive threaded responses so that I can resolve issues precisely where they occur without separate email chains."
Description

Allow auditors to leave questions anchored to specific items (transaction lines, receipt regions, or invoice entries) with threaded replies, statuses (open, needs info, resolved), and attachments for clarifications. Notify the owner (and optional accountant collaborator) of new questions and replies, and permit responding directly in-app or via email reply parsing. Preserve an immutable timeline of Q&A activity without granting edit rights to the underlying records. Outcome: streamlines audit communication tied directly to the exact line items in question.

Acceptance Criteria
Anchor Question to Transaction Line
Given an auditor is viewing a packet via a valid, unexpired read-only link, When they select a transaction line and click Ask, Then a new question is created anchored to that line's stable ID and the question status defaults to 'open'. Given a question is anchored to a line item, When the item is recategorized or updated outside the portal, Then the thread remains linked to the same stable ID and the UI shows an 'updated item' indicator. Given an anchor target no longer exists in the scoped packet, When the thread is loaded, Then the thread remains accessible with a 'reference missing' badge and no data beyond scope is exposed.
Anchor Question to Receipt Region
Given an auditor opens a receipt image, When they drag to select a rectangular region and submit a question, Then the thread stores normalized coordinates relative to the image and displays a pin/outline at that region on all devices. Given a thread has a receipt-region anchor, When any participant opens the thread, Then clicking the anchor scrolls/zooms the image to the saved region.
Threaded Replies and Status Transitions
Given a question thread exists, When auditor, owner, or accountant posts a reply in-app, Then the reply appears in chronological order with author, timestamp, and attachments indicator. Given a question with status 'open', When an auditor requests more documents, Then they may set status to 'needs info'. Given a question in any status, When owner or accountant sets status, Then allowed transitions are: open->needs info, open->resolved, needs info->resolved, needs info->open, resolved->open; all status changes are logged with actor and timestamp. Given any posted message, When a participant attempts to edit or delete it, Then the system rejects the action and logs the attempt.
Attachments on Questions and Replies
Given a compose area for a question or reply, When a user attaches files of types pdf,jpg,jpeg,png,csv,xlsx up to 25 MB each (max 10 per message), Then the files upload successfully and are virus-scanned before becoming visible. Given an attachment is present, When a participant views the thread, Then supported previews render (images, pdf first page) and all attachments are downloadable; originals remain unmodified.
Notifications to Owner and Accountant
Given an auditor posts a new question or reply, When the event is saved, Then the owner and any designated accountant receive an in-app notification immediately and an email within 60 seconds containing a secure deep link to the thread. Given user notification preferences disable email, When an event occurs, Then only in-app notifications are sent. Given notifications are sent, When viewed by the recipient, Then they include item context (type and identifier) without exposing balances or unrelated client data.
Respond via Email Reply Parsing
Given the owner or accountant receives a notification email with a reply-to token, When they reply to that email, Then the plain-text portion before the quoted thread is parsed and posted to the correct thread as that user within 60 seconds. Given the email reply includes attachments meeting allowed types and size limits, When processed, Then the attachments are added to the new reply and scanned. Given parsing fails (invalid token or expired link), When the system receives the email, Then it does not post a reply and sends a failure notice to the sender with instructions to reply in-app.
Immutable Q&A Timeline and Read-Only Records
Given any Q&A activity occurs, When recorded, Then an append-only audit log entry is created with server timestamp, actor, action (post, status change, attachment), and hash of content; no entries can be updated or deleted by any role. Given an auditor uses the portal, When viewing items through Q&A, Then they cannot edit underlying transactions, receipts, or invoices; any attempt returns 403 and no data is changed. Given the packet is exported, When the export is generated, Then the full Q&A timeline is included as a read-only appendix with references to anchored items.
Evidence Viewer & Redaction Controls
"As an owner, I want to preview and optionally redact exhibits and control download permissions so that I can protect sensitive details while still satisfying auditor requests."
Description

Provide an in-portal viewer for PDFs and images with page thumbnails, zoom, text selection (if OCR), and highlights for flagged policies. Allow owners to apply optional redactions before sharing (e.g., mask account numbers or personal addresses), and configure per-exhibit download permissions (view-only, watermarked PDF, or original). Disable bulk downloads unless explicitly enabled. Ensure previews strip sensitive metadata (e.g., EXIF) and render consistently on mobile. Outcome: auditors can review high-quality evidence while owners minimize exposure of unrelated PII.

Acceptance Criteria
Inline Evidence Viewer: PDF & Image Support
Given an auditor opens an exhibit in the Auditor Portal, When the exhibit is a PDF or image (PDF, JPG, PNG, HEIC, TIFF), Then it renders inline without requiring download. Given a multi-page document, When the viewer loads, Then page thumbnails appear and selecting a thumbnail navigates to that page within 100 ms on desktop and 200 ms on mobile. Given a rendered page, When the user adjusts zoom controls or pinch-to-zoom, Then the zoom range supports 50%–400% and 200% maintains legible quality for 300 DPI sources without artifacts. Given a document has policy-flagged regions, When the page is visible, Then those regions are overlaid with highlights matching a legend that shows policy codes and color mapping.
OCR Text Selection in Viewer
Given an exhibit with OCR text, When the auditor drags to select text, Then the correct text is selectable and copyable preserving spaces and line breaks. Given an exhibit without OCR text, When the auditor attempts to select text, Then selection is disabled and a non-blocking hint indicates that no selectable text is available. Given mixed Unicode content, When text is copied from the viewer, Then UTF‑8 characters are preserved without substitution or loss.
Owner Redaction Controls Pre-Share
Given an owner is preparing an auditor link, When Redaction Mode is enabled, Then they can draw rectangular redaction boxes and apply pattern-based redactions for SSNs, routing/account numbers, phone numbers, and postal addresses. Given a redaction is saved, When a shareable preview or watermarked download is generated, Then the redacted content is permanently removed (pixels/text burned out) and cannot be revealed via selection, copy, or image adjustments. Given redactions are applied, When the owner reviews activity, Then an audit log entry records user, timestamp (UTC), page number, and redaction reason. Given an exhibit has redactions, When the owner sets permission mode to Original, Then a warning explains redactions do not apply to originals and requires explicit confirmation before saving.
Per-Exhibit Download Permission Modes
Given an exhibit, When permission is set to View-Only, Then the download action is hidden/disabled and only the inline viewer is available to auditors. Given an exhibit, When permission is set to Watermarked PDF, Then auditors can download a flattened PDF watermarked on every page with auditor email, exhibit ID, UTC timestamp, and link fingerprint. Given an exhibit, When permission is set to Original, Then auditors can download the original file byte-for-byte without transformation. Given any permission mode, When an auditor attempts a direct URL download for a disallowed mode, Then access is denied and the attempt is logged with IP, auditor ID, and timestamp.
Bulk Download Disabled by Default
Given an auditor session, When multiple exhibits are selected, Then no bulk download option is available unless the owner has explicitly enabled Bulk Download for the portal. Given Bulk Download is enabled, When an auditor requests a bulk download, Then only exhibits with Watermarked PDF or Original modes are included per their mode, and View-Only exhibits are excluded. Given a bulk archive is generated, When the ZIP is created, Then it includes a manifest listing each exhibit ID, filename, permission mode, SHA-256 checksum, and generation UTC timestamp. Given rate limiting is configured, When an auditor initiates more than 2 bulk downloads within 10 minutes, Then subsequent requests receive HTTP 429 with a Retry-After header.
Preview Sanitization Removes Sensitive Metadata
Given image exhibits (JPG, PNG, HEIC, TIFF), When generating in-portal previews and watermarked downloads, Then EXIF, IPTC, and XMP metadata including GPS, device model, serials, author, and software tags are stripped. Given PDF exhibits, When generating viewable previews and watermarked downloads, Then document metadata (Author, Title, Creator, Producer), embedded XMP, JavaScript, and file attachments are removed or disabled. Given sanitized outputs, When an auditor inspects metadata with standard tools, Then no GPS coordinates, camera identifiers, author names, or embedded scripts are present. Given Original mode is enabled, When downloading originals, Then original metadata remains intact and the UI displays a notice that originals may contain metadata.
Mobile Viewer Performance & Consistency
Given a modern mobile device (iOS Safari 16+ or Android Chrome 114+), When opening a 25 MB, 20-page PDF over 4G, Then the first page renders within 2.5 seconds and scrolling maintains ≥45 FPS. Given a mobile viewport 360–430 px wide, When using pinch-to-zoom and swipe, Then gestures function reliably; thumbnail strip scrolls horizontally; and interactive controls have minimum 44x44 px tap targets. Given device rotation, When switching between portrait and landscape, Then the viewer preserves page position and zoom level with no layout shift exceeding 50 px. Given mobile accessibility, When a screen reader is enabled, Then thumbnails and controls expose descriptive ARIA labels and are navigable via hardware keyboard.
Data Scope Isolation (No Balances)
"As a security-conscious user, I want auditors to see only packet-specific information and masked identifiers so that unrelated balances or clients are never exposed."
Description

Enforce strict data minimization in the auditor portal: expose only documents, annotations, and fields belonging to the selected packet; hide account balances, cross-client records, dashboards, or unrelated totals. Apply field-level masking for sensitive identifiers (e.g., last 4 of accounts) and validate all API responses and queries for packet scoping. Add automated tests to prevent scope regressions. Outcome: auditors see only what’s necessary for the audit, aligning with least-privilege and reducing risk.

Acceptance Criteria
Packet-Scoped Document Visibility
Given an authenticated auditor using a read-only link for packet P When they open the Documents view Then only documents with packet_id = P are listed And no navigation links exist to other packets or clients And document previews and metadata reflect only items in P
No Balances or Global Totals in Auditor Portal
Given an auditor portal session for packet P When any portal page or sidebar is rendered Then no account balances, revenue/expense totals, cash summaries, or dashboards are displayed And network/API responses omit balance and totals fields And attempting to access a summary endpoint returns 403 Forbidden
Field-Level Masking of Sensitive Identifiers
Given documents or annotations containing account, card, or tax identifiers When rendered in UI, exported, or returned via API Then identifiers are masked to last 4 digits (e.g., **** **** **** 1234) And full identifiers are absent from DOM, PDFs, downloads, logs, and network payloads And masked values are used in search results and question threads
Packet-Scoped Search and Filters
Given an auditor link for packet P When the auditor searches by keyword or filters by tag/policy flag Then only results from packet P are returned And the result count equals the number of matches within P And forcing a different packet via URL or query param yields 403 Forbidden with no data
API Scope Enforcement and Response Whitelisting
Given any API request initiated from the auditor portal When the request omits packet P or references a different packet/client Then the server responds 401/403 with no data payload And successful responses include only auditor-allowed fields belonging to packet P And responses are schema-validated to exclude forbidden fields (balances, cross-client identifiers, unrelated totals)
Automated Scope Regression Tests in CI
Given the CI pipeline for the auditor portal When scope tests execute (unit, integration, e2e) Then tests fail if any API response includes forbidden fields or cross-packet data And tests fail if UI snapshots include balances, dashboards, or unrelated totals And the pipeline blocks merge on any scope regression
Scoped Item-Level Questions
Given an auditor views document D in packet P When they create or reply to an item-level question Then the thread attaches only to D within P And notifications/webhooks contain only masked identifiers and a deep link to D in P And the thread view never exposes unrelated documents, balances, or other client data
Access Logs & Activity Export
"As an owner, I want detailed access logs and an exportable activity trail so that I can demonstrate compliance and investigate any suspicious auditor behavior."
Description

Record all significant events for auditor links, including creation, revocation, access attempts, exhibit views, downloads, searches, and Q&A actions, with timestamps, link ID, IP, and user agent. Provide an owner-facing activity dashboard with filters and an export (CSV/PDF) for compliance records. Trigger alerts for anomalous patterns (e.g., rapid downloads, foreign IPs). Outcome: transparent visibility into auditor activity that supports compliance, security review, and dispute resolution.

Acceptance Criteria
Auditor Link Lifecycle Events Logged
- For each auditor link creation, revocation, and natural expiry, the system records a log entry with fields: event_type ∈ {link_create, link_revoke, link_expire}, link_id, actor_role ∈ {owner, system}, timestamp_utc (ISO 8601), ip, user_agent, and reason (for expire). - Lifecycle events are written append-only and cannot be edited or deleted through the UI. - Each lifecycle event becomes visible in the owner activity dashboard within 30 seconds of the action. - Revoked and expired links no longer accept access; corresponding deny outcomes are observable as separate access_attempt events.
Access Attempts Tracked with Outcome
- Every auditor access attempt produces an access_attempt event with fields: link_id, timestamp_utc, ip, user_agent, outcome ∈ {success, expired, revoked, invalid_token}, and http_status. - Successful access attempts also include session_id and first_byte_ms; unsuccessful attempts do not expose packet metadata in the response. - Consecutive attempts are recorded individually and are filterable by outcome in the dashboard. - access_attempt events are visible in the dashboard within 30 seconds and exportable.
Exhibit Views and Downloads Logged
- Viewing any exhibit records an exhibit_view event with fields: link_id, exhibit_id, timestamp_utc, ip, user_agent. - Downloading an exhibit or the full packet records a download event with fields: link_id, scope ∈ {exhibit, packet}, exhibit_id (when scope=exhibit), file_count, byte_count, timestamp_utc, ip, user_agent. - Packet ZIP generation is logged once at initiation and once on completion with outcome ∈ {success, fail}. - Repeated views/downloads are logged separately and are countable per link and per exhibit.
Search Activity Logged
- Each search action records a search event with fields: link_id, timestamp_utc, ip, user_agent, query_text (UTF-8, truncated to 256 chars), filters_applied (JSON), and result_count. - Clearing filters or changing filter chips without text query records a search event with empty query_text and updated filters_applied. - Search events are visible in the dashboard within 30 seconds and are included in exports. - Logged search data must not include balances or unrelated client identifiers.
Q&A Actions Logged
- Creating a question records qa_create with fields: link_id, question_id, exhibit_id, actor_role ∈ {auditor}, timestamp_utc, ip, user_agent. - Owner replies record qa_reply with actor_role=owner; resolving or reopening records qa_resolve or qa_reopen respectively with the same base fields. - Log entries do not store full message bodies; only IDs and event metadata are stored. - Q&A events are visible in the dashboard within 30 seconds and are included in exports.
Owner Activity Dashboard Filters and Export
- The owner-facing activity dashboard lists events with columns: timestamp_utc, event_type, link_id, outcome (when applicable), exhibit_id, question_id, ip, user_agent. - Dashboard supports server-side filters: date_range, event_type (multi-select), link_id, outcome, ip, exhibit_id, question_id; applied filters affect counts and exports. - Exporting produces files for the current filtered set: CSV and PDF, each containing all matching rows (not just the current page), generated_at_utc, and a filters_summary header. - Exports of up to 50,000 rows complete within 60 seconds; larger jobs are queued, and the owner receives an in-app notification when ready. - Dashboard and exports never display balances or unrelated client identifiers; access is limited to the packet owner and workspace admins.
Anomaly Detection Alerts
- If >100 file downloads or >200 MB is downloaded from the same IP within 5 minutes for a single link, raise alert_rapid_downloads: send in-app and email alerts to the owner; log alert_raised with reason=rapid_downloads, link_id, ip, timestamp_utc. - If an access_attempt is from a country different from the workspace country, raise alert_foreign_ip: send alerts and log alert_raised with reason=foreign_ip, country, ip, link_id, timestamp_utc. - Alerts are deduplicated: no more than one alert per rule per link within a 30-minute window; dedup decisions are logged as alert_suppressed with reason. - Owner actions to acknowledge or revoke from the alert UI are logged as alert_acknowledged or link_revoke with actor_role=owner.

Product Ideas

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

Passkey Lockbox

Passwordless, device-bound sign-in using hardware passkeys and encrypted session tokens. Offers offline recovery codes and per-device revoke from a safety dashboard.

Idea

DelegateSafe Approvals

Invite VAs and bookkeepers with scoped permissions, approval queues, and tamper-proof activity logs. Hide balances while allowing uploads and categorization.

Idea

1099 E-File Checkout

One-click 1099-NEC e-filing inside TaxTidy with per-form checkout and payer/payee validation. Flags missing W-9s and calculates state thresholds automatically.

Idea

1099 Starter Coach

Guided setup that teaches deductions as you go with plain-language tips and a 7-day habit checklist. Preloads categories and creates your first quarterly packet.

Idea

Snap-to-Transaction

Matches receipt photos to bank transactions using time, location, and amount fingerprints. Confirm with a swipe; TaxTidy auto-categorizes and reconciles instantly.

Idea

Invoice Siphon

Pull invoices from Stripe, PayPal, and FreshBooks, then link payments and expenses to projects. Auto-summarizes 1099 payer totals by client.

Idea

Chain-of-Proof Packets

Assemble IRS-ready packets with receipt-to-ledger traceability, policy checks, and human-readable annotations. Export a single, indexed PDF plus CSV audit trail.

Idea

Press Coverage

Imagined press coverage for this groundbreaking product concept.

P

TaxTidy Launches Chain‑of‑Proof Packets to Make Freelancers Audit‑Ready in Minutes

Imagined Press Article

San Francisco, CA — September 8, 2025 — TaxTidy, the mobile‑first tax and bookkeeping companion for freelancers and solo consultants, today announced the general availability of Chain‑of‑Proof Packets, a new suite that produces IRS‑ready, annotated tax packets with end‑to‑end traceability. Built for creative independents and consultants who juggle receipts, bank feeds, and invoices, the release stitches every deduction and income item to its original evidence so audits and reviews move from stressful to straightforward. Chain‑of‑Proof Packets combine several innovations that work together behind the scenes to turn scattered records into a single, credible narrative: Exhibit Index auto‑builds a clean, numbered table of exhibits; TraceLink QR embeds a tamper‑aware QR and deep link for instant verification; Policy Footnotes adds plain‑English explanations and IRS citations; Provenance Timeline visualizes each item’s journey from source to ledger; Packet Seal applies a cryptographic signature and checksum; and Auditor Portal provides scoped, read‑only access for third‑party reviewers without exposing unrelated data. “Freelancers deserve audit‑grade documentation without hiring a full‑time back office,” said Maya Chen, co‑founder and CEO of TaxTidy. “Chain‑of‑Proof Packets transform the way independent professionals present their books: every claim is backed by evidence, every exception is explained, and integrity is verifiable in seconds.” Unlike generic exports, TaxTidy’s packets are designed to answer the most common reviewer questions up front. Each exhibit includes consistent labeling (A1, A2, etc.), direct cross‑references to the PDF’s page anchors, and color‑coded policy flags that signal pass, caution, or needs documentation. Scanning any TraceLink QR opens the exact receipt image, ledger entry, and source metadata inside TaxTidy, even if the packet is printed. For offline audits, a verification code on the packet’s verification sheet confirms integrity against the Packet Seal checksum. Privacy and safety are central to the experience. Selective Redaction masks balances, account numbers, client rates, and personally identifiable information by role and context across the UI, exports, and notifications. Scoped Sessions and Role Blueprints ensure that assistants and bookkeepers can help compile packets without over‑exposure, while Audit Ledger captures an append‑only, hash‑chained trail of who did what, when, and from which device. “As a security‑minded consultant, I treat financial data like client secrets,” said Piper Hayes, an early TaxTidy user and privacy advocate. “With Chain‑of‑Proof Packets, I can share exactly what an auditor needs and nothing more. The QR verification and redaction controls lower the temperature of every review call.” The new suite integrates seamlessly with the workflows freelancers already use in TaxTidy. As you snap receipts on mobile, connect bank feeds, and import invoices from Stripe, PayPal, and FreshBooks, TaxTidy matches documents using GeoPrint Match and MatchScore, splits tips and taxes with AutoSplit, and reconciles settlements with SettleSense. Packet Preview shows a live completeness bar for upcoming quarterly packets and flags missing items with one‑tap fixes, like requesting a W‑9 or confirming a category. For creatives and consultants who plan around estimated quarterly payments, Chain‑of‑Proof Packets slot directly into Quarterly Tax Planner workflows. Each quarter, TaxTidy compiles deductible summaries, links exhibits, and packages documents into an IRS‑ready folder, so year‑end is just a final export rather than a scramble. Audit‑Ready Sticklers will appreciate Provenance Timeline’s one‑page visual that ties together invoice issued, payment received, bank settlement, receipt captured, and categorization applied—no decoding logs required. Early users report fewer back‑and‑forth emails and faster resolution times when questions arise. By presenting explanations, evidence, and verifiable integrity up front, freelancers save hours chasing attachments and clarifying edge cases. When corrections are needed, Correction Flow lets you adjust only the changed fields and re‑issue an updated packet with a preserved audit trail. Chain‑of‑Proof Packets are available today on TaxTidy Pro and Business plans, with Packet Preview included on Starter. Auditor Portal is included at no extra cost during an introductory period; advanced export options and longer portal timeboxes are available on Business. All features work with TaxTidy’s passkey‑based sign‑in, Login Freeze, and Two‑Key Recovery for added account safety, and Travel Mode prevents lockouts while you’re on the road. Getting started is simple: connect your accounts, snap or forward receipts as usual, and open Packet Preview to see progress. When you’re ready, export a signed PDF and CSV audit trail with a single tap, or share a read‑only Auditor Portal link scoped to the exact packet. TraceLink QR and Packet Seal are applied automatically, and Selective Redaction follows your role settings to keep sensitive fields masked. About TaxTidy TaxTidy automatically collects, categorizes, and stores freelancers’ tax documents from invoices, bank feeds, and receipt photos, producing IRS‑ready, annotated tax packets. Freelance creatives and solo consultants using mobile‑first workflows rely on TaxTidy to extract tax data, match expenses, and cut tax‑prep time by roughly 60%, saving hours each quarter. Media Contact Elena Brooks, Communications Lead press@taxtidy.com +1 (415) 555‑0134 www.taxtidy.com/press

P

New Mobile Snap Intelligence Cuts Receipt Work by 60% With SettleSense, GeoPrint, and AutoSplit

Imagined Press Article

San Francisco, CA — September 8, 2025 — TaxTidy today introduced a major upgrade to its mobile experience that helps freelancers turn everyday paperwork into accurate, IRS‑ready records with less effort. The new Mobile Snap Intelligence release brings SettleSense, GeoPrint Match, AutoSplit, DupeShield, Offline SnapSync, and an explainable MatchScore together so receipts attach themselves to the right transactions, tips and taxes are separated correctly, and edge cases resolve in a tap—even when you’re offline. For Mobile Snap‑Capturers who live out of their camera roll and Bank‑Feed Reconcilers who spend evenings matching descriptors to reality, the update delivers speed and confidence. SettleSense bridges the gap between pre‑authorizations and final settlements by linking the same purchase across pending and posted transactions. When amounts shift due to tips, currency holds, or delayed posting, your receipt stays attached, your categories stick, and nothing is left unmatched. GeoPrint Match boosts matching accuracy using precise time‑and‑place fingerprints from your phone. Even when bank descriptors are vague or use processor codes, TaxTidy cross‑references your on‑device location signals, receipt metadata, and merchant databases to propose the right transaction first. You confirm with a swipe, and TaxTidy learns from your choices to reduce reviews over time. “Freelancers don’t want to become bookkeepers. They want a system that does the right thing by default and asks for help only when it matters,” said Jordan Rios, Head of Product at TaxTidy. “With Mobile Snap Intelligence, we’ve turned the phone into a reconciliation engine. It’s less busywork, fewer second guesses, and cleaner packets at quarter‑end.” AutoSplit turns one receipt into clean, compliant allocations automatically. It detects tips, taxes, and line items, splitting a single card charge across categories, clients, or reimbursables. The result is accurate deductions and project‑level clarity without manual math. For those juggling multiple clients, client‑level tagging and Project Auto‑Link inherit from your invoice metadata so expenses align to the right project automatically. DupeShield keeps your ledger tidy by spotting duplicate receipt photos, forwarded copies, and multi‑page scans. It merges pages into a single receipt, prevents double attachments to the same transaction, and flags suspicious repeats before they clutter your books. If you’re offline—in a plane, subway, or spotty café—Offline SnapSync stores a secure fingerprint (time, location, total) and auto‑matches to your bank feed once you’re back online. A push notification lets you know when the pair is confirmed and categorized. Transparency matters as much as automation. MatchScore introduces a clear, explainable confidence meter with signals like Time, Location, Amount, and Merchant. You can review why a match is suggested, accept or correct in a tap, and watch the system adapt to your preferences. If an item falls outside your rules, Smart Queues batch similar items and auto‑route them to the right approver or back to you for a fast, focused review. For creators and consultants who split their workflows among assistants or bookkeepers, the upgrade plays nicely with TaxTidy’s delegation controls. Scoped Sessions let a VA’s tablet upload receipts and categorize transactions without exposing balances or tax totals. Role Blueprints and JIT Access provide prebuilt permissions and just‑in‑time elevation for tasks like exporting CSVs or editing rules, while Selective Redaction masks sensitive fields across the app and exports. “I used to keep a backlog of 200 photos and a note that said ‘fix later,’” said Alicia Kim, a freelance photographer who tested the release. “Now the matches show up while I’m still on the train, and AutoSplit sorts tips and sales tax before I sit down at my desk. I’m spending more time with clients and less time in spreadsheets.” The Mobile Snap Intelligence features are available today on iOS and Android. To get the most out of GeoPrint Match, enable location permissions; TaxTidy respects your settings and uses your device signals to improve matching only for receipts you capture. DupeShield and AutoSplit are on by default, and SettleSense applies automatically as settlements post. For those new to TaxTidy, Snap Coach provides guided practice on your first five receipts with instant feedback so you can see OCR in action and learn how matches form in real time. Security remains foundational. TaxTidy supports passkey‑based sign‑in, Risk Guard adaptive checks, Login Freeze for misplaced phones, and Two‑Key Recovery. Travel Mode prevents lockouts while you’re on the move, and Access Radar helps owners monitor delegate activity and tighten scopes where needed. Every action is recorded in the Audit Ledger, an append‑only, hash‑chained log that exports with your packets if required. About TaxTidy TaxTidy automatically collects, categorizes, and stores freelancers’ tax documents from invoices, bank feeds, and receipt photos, producing IRS‑ready, annotated tax packets. Freelance creatives and solo consultants using mobile‑first workflows rely on TaxTidy to extract tax data, match expenses, and cut tax‑prep time by roughly 60%, saving hours each quarter. Media Contact Elena Brooks, Communications Lead press@taxtidy.com +1 (415) 555‑0134 www.taxtidy.com/press

P

TaxTidy Debuts Integrated 1099 Filing Suite With W‑9 AutoChase and State e‑File Relay

Imagined Press Article

San Francisco, CA — September 8, 2025 — TaxTidy today announced its Integrated 1099 Filing Suite, bringing W‑9 collection, TIN validation, state rule handling, error prevention, corrections, and recipient delivery into a single, mobile‑first workflow. Built for invoice‑driven creatives and consultants who manage many clients and payment channels, the suite replaces last‑minute scrambles with a guided, compliant e‑file process from first invoice to final acknowledgment. The 1099 Filing Suite includes: - W‑9 AutoChase to request, collect, and validate W‑9s before you file. Send a secure mobile‑friendly link to payees, auto‑remind until complete, and attach the signed W‑9 to the payee record. - TIN Match Sync to pre‑flight name/TIN checks with guided fixes, reducing IRS B‑Notices and rejections. Run instant or batch validations and apply suggested corrections before you hit e‑file. - State e‑File Relay so you file once and we route everywhere that applies. TaxTidy automatically determines state thresholds, handles CF/SF participation and exceptions, and tracks acknowledgments alongside your federal submission. - Reject Shield to catch common errors before the IRS does, surfacing one‑tap fixes for amount rounding, address formats, TCC/Payer data, and more. - Correction Flow to make corrections without chaos. Select a filed form, mark it corrected, adjust only the changed fields, and re‑file with a preserved audit trail. - Recipient Vault to deliver recipient copies securely with e‑consent. Payees get a self‑serve portal to download forms, update addresses, and choose e‑delivery, with optional print‑and‑mail fulfillment. “Independent professionals told us the worst part of tax season is chasing paperwork and navigating multiple portals,” said Renée Patel, VP of Compliance at TaxTidy. “We built the 1099 Filing Suite to be end‑to‑end and error‑aware. It reduces avoidable rejections, centralizes state requirements, and keeps payees informed without exposing your books.” For Invoice‑Driven Creatives, the suite is tightly integrated with income tracking. TaxTidy’s Payout Stitcher reconciles Stripe, PayPal, and FreshBooks payouts to your bank deposits, grouping underlying invoices, fees, and refunds under each deposit for one‑tap confirmation. Fee Unbundle peels processor fees off gross amounts to reveal true net income and deductible merchant fees. Client Resolver unifies duplicate client profiles across platforms, and Project Auto‑Link attaches payments to the right project automatically, giving you clean totals that flow directly into your 1099 Watchlist. 1099 Watchlist provides live, client‑by‑client totals with threshold alerts so you know who’s likely to issue you a 1099‑NEC and where you may need your own copies for reconciliation. At tax time, reconcile received forms against your tracked totals to spot over‑ or under‑reporting in seconds. If refunds, partial credits, or chargebacks occur, Refund Reconcile updates client totals and project P&L automatically, keeping year‑to‑date income honest. “Last year I juggled spreadsheets, email threads, and a state portal that crashed on deadline day,” said David Park, an agency subcontractor who piloted the suite. “This year TaxTidy chased W‑9s for me, flagged a TIN mismatch before I filed, and relayed the state copy after the federal went through. The Recipient Vault eliminated ‘I never got it’ messages from vendors.” The filing experience is designed to be approachable without sacrificing rigor. Deduction Drills and Category Quiz teach relevant concepts as you go, while Packet Preview shows a live completeness bar and one‑tap fixes for missing items. Reject Shield runs business‑rule and schema checks, and Risk Guard steps up authentication if something about your session looks unusual. The Audit Ledger and Packet Seal provide post‑filing confidence for you and your recipients. From a security and privacy perspective, the suite inherits TaxTidy’s protections: passkey‑based sign‑in, QR Handoff for quick desktop sessions, Login Freeze to block new device enrollments from lost phones, and Two‑Key Recovery for resilient account access. Selective Redaction masks balances, account numbers, and client rates in recipient communications and exports. Role Blueprints and JIT Access let owners delegate prep work to a VA or bookkeeper while retaining final approval. Availability and pricing The Integrated 1099 Filing Suite is available today to TaxTidy Pro and Business customers in the United States. Federal 1099‑NEC e‑file is supported at launch, with additional 1099 form types and expanded state coverage rolling out ahead of year‑end deadlines. W‑9 AutoChase, TIN Match Sync, and Recipient Vault are included; e‑file fees may apply per form based on plan. Print‑and‑mail fulfillment is optional. Customers can start from the 1099 hub in TaxTidy or by connecting Stripe, PayPal, and FreshBooks to auto‑import payees and payments. Getting started Open TaxTidy, connect your payment platforms and bank accounts, invite any delegates with Role Blueprints, and visit the 1099 hub to review your Watchlist. Send W‑9 requests with a tap, run batch TIN validations, fix flagged fields, and proceed to e‑file. Track federal and state acknowledgments in one place, deliver recipient copies via the Recipient Vault, and use Correction Flow if updates are needed. About TaxTidy TaxTidy automatically collects, categorizes, and stores freelancers’ tax documents from invoices, bank feeds, and receipt photos, producing IRS‑ready, annotated tax packets. Freelance creatives and solo consultants using mobile‑first workflows rely on TaxTidy to extract tax data, match expenses, and cut tax‑prep time by roughly 60%, saving hours each quarter. Media Contact Elena Brooks, Communications Lead press@taxtidy.com +1 (415) 555‑0134 www.taxtidy.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.