diff --git a/app/controllers/account/cancellations_controller.rb b/app/controllers/account/cancellations_controller.rb
new file mode 100644
index 0000000000..4bc21de62f
--- /dev/null
+++ b/app/controllers/account/cancellations_controller.rb
@@ -0,0 +1,18 @@
+class Account::CancellationsController < ApplicationController
+ before_action :ensure_owner
+
+ def create
+ Current.account.cancel
+ redirect_to account_settings_path, notice: "Your account is scheduled for deletion."
+ end
+
+ def destroy
+ Current.account.reactivate
+ redirect_to account_settings_path, notice: "Account deletion has been canceled."
+ end
+
+ private
+ def ensure_owner
+ head :forbidden unless Current.user.owner?
+ end
+end
diff --git a/app/jobs/account/incinerate_job.rb b/app/jobs/account/incinerate_job.rb
new file mode 100644
index 0000000000..2c0a6b9cba
--- /dev/null
+++ b/app/jobs/account/incinerate_job.rb
@@ -0,0 +1,14 @@
+class Account::IncinerateJob < ApplicationJob
+ include ActiveJob::Continuable
+
+ queue_as :incineration
+
+ def perform
+ step :incineration do |step|
+ Account.up_for_incineration.find_each(start: step.cursor) do |account|
+ account.incinerate
+ step.advance! from: account.id
+ end
+ end
+ end
+end
diff --git a/app/models/account.rb b/app/models/account.rb
index f9b2589c07..216adc9b95 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -1,7 +1,7 @@
class Account < ApplicationRecord
- include Account::Storage, Entropic, MultiTenantable, Seedeable
+ include Account::Storage, Cancellable, Entropic, MultiTenantable, Seedeable
- has_one :join_code
+ has_one :join_code, dependent: :destroy
has_many :users, dependent: :destroy
has_many :boards, dependent: :destroy
has_many :cards, dependent: :destroy
@@ -36,6 +36,10 @@ def system_user
users.find_by!(role: :system)
end
+ def incinerate
+ Incineration.new(self).perform
+ end
+
private
def assign_external_account_id
self.external_account_id ||= ExternalIdSequence.next
diff --git a/app/models/account/cancellable.rb b/app/models/account/cancellable.rb
new file mode 100644
index 0000000000..7f93b7b5ad
--- /dev/null
+++ b/app/models/account/cancellable.rb
@@ -0,0 +1,48 @@
+module Account::Cancellable
+ extend ActiveSupport::Concern
+
+ INCINERATION_GRACE_PERIOD = 30.days
+
+ included do
+ has_one :cancellation, dependent: :destroy
+
+ scope :up_for_incineration, -> { joins(:cancellation).where(cancellations: { created_at: ...INCINERATION_GRACE_PERIOD.ago }) }
+ end
+
+ def cancel(**attributes)
+ with_lock do
+ if cancellable? && active?
+ create_cancellation!(**attributes)
+ pause_subscription
+ end
+ end
+ end
+
+ def reactivate
+ with_lock do
+ if cancelled?
+ resume_subscription
+ cancellation.destroy
+ end
+ end
+ end
+
+ def cancelled?
+ cancellation.present?
+ end
+
+ private
+ def active?
+ !cancelled?
+ end
+
+ def cancellable?
+ Account.accepting_signups?
+ end
+
+ def pause_subscription
+ end
+
+ def resume_subscription
+ end
+end
diff --git a/app/models/account/cancellation.rb b/app/models/account/cancellation.rb
new file mode 100644
index 0000000000..277fdc3dda
--- /dev/null
+++ b/app/models/account/cancellation.rb
@@ -0,0 +1,3 @@
+class Account::Cancellation < ApplicationRecord
+ belongs_to :account
+end
diff --git a/app/models/account/incineration.rb b/app/models/account/incineration.rb
new file mode 100644
index 0000000000..e1740927d5
--- /dev/null
+++ b/app/models/account/incineration.rb
@@ -0,0 +1,16 @@
+class Account::Incineration
+ attr_reader :account
+
+ def initialize(account)
+ @account = account
+ end
+
+ def perform
+ cancel_subscription
+ account.destroy
+ end
+
+ private
+ def cancel_subscription
+ end
+end
diff --git a/app/views/account/settings/_cancellation.html.erb b/app/views/account/settings/_cancellation.html.erb
new file mode 100644
index 0000000000..f39955bb4e
--- /dev/null
+++ b/app/views/account/settings/_cancellation.html.erb
@@ -0,0 +1,37 @@
+<% if Current.user.owner? %>
+
+
Delete account
+
Permanently delete your Fizzy account and all its data.
+
+
+ <% if Current.account.cancelled? %>
+
+
+ Account scheduled for deletion
+ Your account will be permanently deleted in <%= distance_of_time_in_words_to_now(Current.account.cancellation.created_at + Account::Cancellable::INCINERATION_GRACE_PERIOD) %>.
+
+
+
+ <%= button_to "Cancel deletion", account_cancellation_path, method: :delete, class: "btn", data: { turbo_confirm: "Are you sure you want to cancel the account deletion?" } %>
+ <% else %>
+
+
+
+
+
+ <% end %>
+<% end %>
diff --git a/app/views/account/settings/show.html.erb b/app/views/account/settings/show.html.erb
index fc5e799b38..74e2744148 100644
--- a/app/views/account/settings/show.html.erb
+++ b/app/views/account/settings/show.html.erb
@@ -18,6 +18,7 @@