feat: Add account suspension metadata in Super Admin (#15158)

## Description

Super Admins can now record a category and reason when suspending an
account, review the complete suspension history on the account details
page, and correct the latest suspension metadata without losing its
original timestamp. Suspension events are stored internally on the
account without changing customer-facing account API payloads.

## Closes

-
[CW-7653](https://linear.app/chatwoot/issue/CW-7653/ability-to-add-notes-while-suspending-an-acocunt)
- [Implementation
plan](https://linear.app/chatwoot/document/super-admin-account-suspension-metadata-implementation-plan-e7e4eb79d078)

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## What changed

- Require a suspension category and a reason of up to 256 characters
when an active account is suspended.
- Store append-only suspension events in `accounts.internal_attributes`,
while preserving unrelated internal metadata.
- Allow corrections to the latest event for an already suspended account
without changing its timestamp.
- Show the full suspension history, newest first, on the Super Admin
account details page.
- Add visual dividers between top-level sections on the Super Admin
account edit page.
- Keep legacy suspended-account edits and new-account creation behavior
unchanged.

## How to test

1. Open an active account in Super Admin and choose **Suspended**.
2. Confirm the category and reason controls appear, reject incomplete or
invalid values, and enforce the 256-character reason limit.
3. Suspend the account with each supported category and confirm the
event appears on the details page.
4. Reactivate and suspend the account again; confirm prior history is
retained and a new event is added.
5. Edit a suspended account's latest category or reason; confirm its
original timestamp is preserved.
6. Confirm a legacy suspended account without history can still be
edited without supplying suspension metadata.

## Checklist

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Sony Mathew
2026-07-24 14:48:14 +05:30
committed by GitHub
parent afa6421809
commit 7c1711170b
12 changed files with 316 additions and 18 deletions

View File

@@ -2,15 +2,15 @@
<%= f.label field.attribute %>
</div>
<div class="field-unit__field">
<p class="text-gray-400 text-xs italic mb-4"><%= t('super_admin.captain_model_overrides.form.helper_text') %></p>
<div class="field-unit__field max-w-5xl">
<p class="mb-3 text-xs italic text-n-slate-10"><%= t('super_admin.captain_model_overrides.form.helper_text') %></p>
<div class="grid grid-cols-1 gap-4">
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
<% field.feature_rows.each do |feature| %>
<div class="p-3 bg-white rounded-lg shadow-sm outline outline-1 outline-n-container">
<div class="mb-2">
<div class="text-sm font-medium text-slate-700"><%= feature[:name] %></div>
<div class="text-xs text-slate-500"><%= feature[:key] %></div>
<div class="rounded-md border border-n-weak bg-white p-2.5">
<div class="mb-1.5">
<div class="text-xs font-medium text-n-slate-12"><%= feature[:name] %></div>
<div class="text-[11px] text-n-slate-10"><%= feature[:key] %></div>
</div>
<%= select_tag(
@@ -19,7 +19,7 @@
[[t('super_admin.captain_model_overrides.form.use_default', model: feature[:default_model], model_id: feature[:default_model_id]), '']] + feature[:options],
feature[:selected_override]
),
class: 'block w-full rounded-md border-slate-300 text-sm'
class: 'block w-full rounded-md border-n-strong py-1.5 pl-2 pr-8 text-xs'
) %>
</div>
<% end %>

View File

@@ -7,29 +7,29 @@
</summary>
<div class="mt-3 w-full">
<div class="grid grid-cols-1 gap-3">
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
<% field.feature_rows.each do |feature| %>
<div class="p-3 bg-white rounded-md outline outline-n-container outline-1 shadow-sm">
<div class="flex items-center justify-between gap-4">
<div class="rounded-md border border-n-weak bg-white p-2.5">
<div class="flex items-start justify-between gap-2">
<div>
<div class="text-sm font-medium text-n-slate-12"><%= feature[:name] %></div>
<div class="text-xs text-n-slate-11"><%= feature[:key] %></div>
<div class="text-xs font-medium text-n-slate-12"><%= feature[:name] %></div>
<div class="text-[11px] text-n-slate-10"><%= feature[:key] %></div>
</div>
<span class="<%= feature[:source] == :account_override ? 'bg-green-400 text-white' : 'bg-slate-50 text-slate-800' %> rounded-full px-2 py-1 text-xs">
<span class="<%= feature[:source] == :account_override ? 'bg-green-400 text-white' : 'bg-slate-50 text-slate-800' %> shrink-0 rounded-full px-2 py-1 text-[11px]">
<%= feature[:source_label] %>
</span>
</div>
<div class="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<div class="mt-2 grid grid-cols-1 gap-2 text-xs">
<div>
<div class="text-xs uppercase text-n-slate-10"><%= t('super_admin.captain_model_overrides.show.provider') %></div>
<div class="text-[11px] uppercase text-n-slate-10"><%= t('super_admin.captain_model_overrides.show.provider') %></div>
<div class="text-n-slate-12">
<%= feature[:provider] %>
<span class="text-n-slate-10">(<%= feature[:provider_id] %>)</span>
</div>
</div>
<div>
<div class="text-xs uppercase text-n-slate-10"><%= t('super_admin.captain_model_overrides.show.model') %></div>
<div class="text-[11px] uppercase text-n-slate-10"><%= t('super_admin.captain_model_overrides.show.model') %></div>
<div class="text-n-slate-12">
<%= feature[:model] %>
<span class="text-n-slate-10">(<%= feature[:model_id] %>)</span>