Files
sure/test/fixtures/users.yml
Juan José Mata 5b0ddd06a4 Add post-trial inactive Family cleanup with data archival (#1199)
* Add post-trial inactive family cleanup with data archival

Families that expire their trial without subscribing now get cleaned up
daily. Empty families (no accounts) are destroyed immediately after a
14-day grace period. Families with meaningful data (12+ transactions,
some recent) get their data exported as NDJSON/ZIP to an ArchivedExport
record before deletion, downloadable via a token-based URL for 90 days.

- Add InactiveFamilyCleanerJob (scheduled daily at 4 AM, managed mode only)
- Add ArchivedExport model with token-based downloads
- Add inactive_trial_for_cleanup scope and requires_data_archive? to Family
- Extend DataCleanerJob to purge expired archived exports
- Add ArchivedExportsController for unauthenticated token downloads

https://claude.ai/code/session_01LR3Vo83R5s5SczYe6T33dQ

* Fix Brakeman redirect warning in ArchivedExportsController

Use rails_blob_path instead of redirecting directly to the ActiveStorage
attachment, which avoids the allow_other_host: true open redirect.

https://claude.ai/code/session_01LR3Vo83R5s5SczYe6T33dQ

* Update schema.rb with archived_exports table

Add the archived_exports table definition to schema.rb to match
the pending migration, unblocking CI tests.

https://claude.ai/code/session_01LR3Vo83R5s5SczYe6T33dQ

* Fix broken CI tests for ArchivedExports and InactiveFamilyCleaner

- ArchivedExportsController 404 test: use assert_response :not_found
  instead of assert_raises since Rails rescues RecordNotFound in
  integration tests and returns a 404 response.
- InactiveFamilyCleanerJob test: remove assert_no_difference on
  Family.count since the inactive_trial fixture gets cleaned up by
  the job. The test intent is to verify the active family survives,
  which is checked by assert Family.exists?.

https://claude.ai/code/session_01LR3Vo83R5s5SczYe6T33dQ

* Wrap ArchivedExport creation in a transaction

Ensure the ArchivedExport record and its file attachment succeed
atomically. If the attach fails, the transaction rolls back so no
orphaned record is left without an export file.

https://claude.ai/code/session_01LR3Vo83R5s5SczYe6T33dQ

* Store only a digest of the download token for ArchivedExport

Replace plaintext download_token column with download_token_digest
(SHA-256 hex). The raw token is generated via SecureRandom on create,
exposed transiently via attr_reader for use in emails/logs, and only
its digest is persisted. Lookup uses find_by_download_token! which
digests the incoming token before querying.

https://claude.ai/code/session_01LR3Vo83R5s5SczYe6T33dQ

* Remove raw download token from cleanup job logs

Log a truncated digest prefix instead of the raw token, which is the
sole credential for the unauthenticated download endpoint.

https://claude.ai/code/session_01LR3Vo83R5s5SczYe6T33dQ

* Fix empty assert_no_difference block in cleaner job test

Wrap the perform_now call with both assertions so the
ArchivedExport.count check actually exercises the job.

https://claude.ai/code/session_01LR3Vo83R5s5SczYe6T33dQ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-14 20:14:18 +01:00

103 lines
2.4 KiB
YAML

empty:
family: empty
first_name: User
last_name: One
email: user1@example.com
password_digest: $2a$12$XoNBo/cMCyzpYtvhrPAhsubG21mELX48RAcjSVCRctW8dG8wrDIla
onboarded_at: <%= 3.days.ago %>
role: admin
ai_enabled: true
show_sidebar: true
show_ai_sidebar: true
ui_layout: dashboard
sure_support_staff:
family: empty
first_name: Support
last_name: Admin
email: support@sure.am
password_digest: $2a$12$XoNBo/cMCyzpYtvhrPAhsubG21mELX48RAcjSVCRctW8dG8wrDIla
role: super_admin
onboarded_at: <%= 3.days.ago %>
ai_enabled: true
show_sidebar: true
show_ai_sidebar: true
ui_layout: dashboard
family_admin:
family: dylan_family
first_name: Bob
last_name: Dylan
email: bob@bobdylan.com
password_digest: $2a$12$XoNBo/cMCyzpYtvhrPAhsubG21mELX48RAcjSVCRctW8dG8wrDIla
role: admin
onboarded_at: <%= 3.days.ago %>
ai_enabled: true
show_sidebar: true
show_ai_sidebar: true
ui_layout: dashboard
family_member:
family: dylan_family
first_name: Jakob
last_name: Dylan
email: jakobdylan@yahoo.com
password_digest: $2a$12$XoNBo/cMCyzpYtvhrPAhsubG21mELX48RAcjSVCRctW8dG8wrDIla
onboarded_at: <%= 3.days.ago %>
role: member
ai_enabled: true
show_sidebar: true
show_ai_sidebar: true
ui_layout: dashboard
new_email:
family: empty
first_name: Test
last_name: User
email: user@example.com
unconfirmed_email: new@example.com
password_digest: $2a$12$XoNBo/cMCyzpYtvhrPAhsubG21mELX48RAcjSVCRctW8dG8wrDIla
onboarded_at: <%= Time.current %>
role: member
ai_enabled: true
show_sidebar: true
show_ai_sidebar: true
ui_layout: dashboard
intro_user:
family: empty
first_name: Intro
last_name: User
email: intro@example.com
password_digest: $2a$12$XoNBo/cMCyzpYtvhrPAhsubG21mELX48RAcjSVCRctW8dG8wrDIla
onboarded_at: <%= 1.day.ago %>
role: guest
ai_enabled: true
show_sidebar: false
show_ai_sidebar: false
ui_layout: intro
inactive_trial_user:
family: inactive_trial
first_name: Inactive
last_name: User
email: inactive@example.com
password_digest: $2a$12$XoNBo/cMCyzpYtvhrPAhsubG21mELX48RAcjSVCRctW8dG8wrDIla
role: admin
onboarded_at: <%= 90.days.ago %>
ai_enabled: true
show_sidebar: true
show_ai_sidebar: true
ui_layout: dashboard
# SSO-only user: created via JIT provisioning, no local password
sso_only:
family: empty
first_name: SSO
last_name: User
email: sso-user@example.com
password_digest: ~
role: admin
onboarded_at: <%= 1.day.ago %>
ai_enabled: true