mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 03:54:08 +00:00
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>
This commit is contained in:
70
test/models/archived_export_test.rb
Normal file
70
test/models/archived_export_test.rb
Normal file
@@ -0,0 +1,70 @@
|
||||
require "test_helper"
|
||||
|
||||
class ArchivedExportTest < ActiveSupport::TestCase
|
||||
test "downloadable? returns true when not expired and file attached" do
|
||||
archive = ArchivedExport.create!(
|
||||
email: "test@example.com",
|
||||
family_name: "Test",
|
||||
expires_at: 30.days.from_now
|
||||
)
|
||||
archive.export_file.attach(
|
||||
io: StringIO.new("test content"),
|
||||
filename: "test.zip",
|
||||
content_type: "application/zip"
|
||||
)
|
||||
|
||||
assert archive.downloadable?
|
||||
end
|
||||
|
||||
test "downloadable? returns false when expired" do
|
||||
archive = ArchivedExport.create!(
|
||||
email: "test@example.com",
|
||||
family_name: "Test",
|
||||
expires_at: 1.day.ago
|
||||
)
|
||||
archive.export_file.attach(
|
||||
io: StringIO.new("test content"),
|
||||
filename: "test.zip",
|
||||
content_type: "application/zip"
|
||||
)
|
||||
|
||||
assert_not archive.downloadable?
|
||||
end
|
||||
|
||||
test "downloadable? returns false when file not attached" do
|
||||
archive = ArchivedExport.create!(
|
||||
email: "test@example.com",
|
||||
family_name: "Test",
|
||||
expires_at: 30.days.from_now
|
||||
)
|
||||
|
||||
assert_not archive.downloadable?
|
||||
end
|
||||
|
||||
test "expired scope returns only expired records" do
|
||||
expired = ArchivedExport.create!(
|
||||
email: "expired@example.com",
|
||||
family_name: "Expired",
|
||||
expires_at: 1.day.ago
|
||||
)
|
||||
active = ArchivedExport.create!(
|
||||
email: "active@example.com",
|
||||
family_name: "Active",
|
||||
expires_at: 30.days.from_now
|
||||
)
|
||||
|
||||
results = ArchivedExport.expired
|
||||
assert_includes results, expired
|
||||
assert_not_includes results, active
|
||||
end
|
||||
|
||||
test "generates download_token automatically" do
|
||||
archive = ArchivedExport.create!(
|
||||
email: "test@example.com",
|
||||
family_name: "Test",
|
||||
expires_at: 30.days.from_now
|
||||
)
|
||||
|
||||
assert archive.download_token.present?
|
||||
end
|
||||
end
|
||||
@@ -25,4 +25,86 @@ class Family::SubscribeableTest < ActiveSupport::TestCase
|
||||
@family.update!(stripe_customer_id: "")
|
||||
assert_not @family.can_manage_subscription?
|
||||
end
|
||||
|
||||
test "inactive_trial_for_cleanup includes families with expired paused trials" do
|
||||
inactive = families(:inactive_trial)
|
||||
results = Family.inactive_trial_for_cleanup
|
||||
|
||||
assert_includes results, inactive
|
||||
end
|
||||
|
||||
test "inactive_trial_for_cleanup excludes families with active subscriptions" do
|
||||
results = Family.inactive_trial_for_cleanup
|
||||
|
||||
assert_not_includes results, @family
|
||||
end
|
||||
|
||||
test "inactive_trial_for_cleanup excludes families within grace period" do
|
||||
inactive = families(:inactive_trial)
|
||||
inactive.subscription.update!(trial_ends_at: 5.days.ago)
|
||||
|
||||
results = Family.inactive_trial_for_cleanup
|
||||
|
||||
assert_not_includes results, inactive
|
||||
end
|
||||
|
||||
test "inactive_trial_for_cleanup includes families with no subscription created long ago" do
|
||||
old_family = Family.create!(name: "Abandoned", created_at: 90.days.ago)
|
||||
|
||||
results = Family.inactive_trial_for_cleanup
|
||||
|
||||
assert_includes results, old_family
|
||||
|
||||
old_family.destroy
|
||||
end
|
||||
|
||||
test "inactive_trial_for_cleanup excludes recently created families with no subscription" do
|
||||
recent_family = Family.create!(name: "New")
|
||||
|
||||
results = Family.inactive_trial_for_cleanup
|
||||
|
||||
assert_not_includes results, recent_family
|
||||
|
||||
recent_family.destroy
|
||||
end
|
||||
|
||||
test "requires_data_archive? returns false with few transactions" do
|
||||
inactive = families(:inactive_trial)
|
||||
assert_not inactive.requires_data_archive?
|
||||
end
|
||||
|
||||
test "requires_data_archive? returns true with 12+ recent transactions" do
|
||||
inactive = families(:inactive_trial)
|
||||
account = inactive.accounts.create!(
|
||||
name: "Test", currency: "USD", balance: 0, accountable: Depository.new, status: :active
|
||||
)
|
||||
|
||||
trial_end = inactive.subscription.trial_ends_at
|
||||
15.times do |i|
|
||||
account.entries.create!(
|
||||
name: "Txn #{i}", date: trial_end - i.days, amount: 10, currency: "USD",
|
||||
entryable: Transaction.new
|
||||
)
|
||||
end
|
||||
|
||||
assert inactive.requires_data_archive?
|
||||
end
|
||||
|
||||
test "requires_data_archive? returns false with 12+ transactions but none recent" do
|
||||
inactive = families(:inactive_trial)
|
||||
account = inactive.accounts.create!(
|
||||
name: "Test", currency: "USD", balance: 0, accountable: Depository.new, status: :active
|
||||
)
|
||||
|
||||
# All transactions from early in the trial (more than 14 days before trial end)
|
||||
trial_end = inactive.subscription.trial_ends_at
|
||||
15.times do |i|
|
||||
account.entries.create!(
|
||||
name: "Txn #{i}", date: trial_end - 30.days - i.days, amount: 10, currency: "USD",
|
||||
entryable: Transaction.new
|
||||
)
|
||||
end
|
||||
|
||||
assert_not inactive.requires_data_archive?
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user