## Description
Adds an API-only branded email layout feature for Email inbox replies.
Administrators can configure an account-level fallback layout and
per-email-inbox overrides with Liquid HTML using `{{ content_for_layout
}}`, and eligible outbound email replies/transcripts render through the
scoped layout when the account feature flag `branded_email_templates` is
enabled.
The feature is disabled by default and is manually controlled through
the normal account feature flag mechanism.
Fixes
https://linear.app/chatwoot/issue/CW-7514/branded-html-email-templates-per-inboxbrand
## Type of change
- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update
## How to test
1. Start Chatwoot locally and sign in as an administrator.
2. Enable the account feature flag for the account you are testing:
```ruby
account = Account.find(<account_id>)
account.enable_features!(:branded_email_templates)
```
3. Create or pick an Email inbox, then note the `account_id` and
`inbox_id`.
4. Configure an account-level fallback layout through the API using
authenticated admin headers:
```http
PATCH /api/v1/accounts/:account_id/branded_email_layout
Content-Type: application/json
{
"branded_email_layout": "<html><body><header>Account Brand</header>{{
content_for_layout }}<footer>Account footer</footer></body></html>"
}
```
5. Confirm `GET /api/v1/accounts/:account_id/branded_email_layout`
returns the saved account layout.
6. Configure an inbox-level override for the Email inbox:
```http
PATCH /api/v1/accounts/:account_id/inboxes/:inbox_id
Content-Type: application/json
{
"branded_email_layout": "<html><body><header>Inbox Brand</header>{{
content_for_layout }}<footer>Inbox footer</footer></body></html>"
}
```
7. Confirm `GET /api/v1/accounts/:account_id/inboxes/:inbox_id` returns
the inbox `branded_email_layout`.
8. Send an Email inbox reply and verify the outbound email body is
wrapped with the inbox layout around the generated reply content.
9. Clear the inbox layout by sending a blank value, then send another
reply and verify it falls back to the account layout:
```http
PATCH /api/v1/accounts/:account_id/inboxes/:inbox_id
Content-Type: application/json
{
"branded_email_layout": ""
}
```
10. Clear the account layout with a blank value and verify Email replies
return to the existing no-layout behavior.
11. Verify validation behavior:
- Updating either API with a layout that omits `{{ content_for_layout
}}` returns `422`.
- Updating either API with invalid Liquid returns `422`.
- Updating a non-Email inbox with `branded_email_layout` returns `422`.
- Disabling `branded_email_templates` and updating a layout returns
`422`.
## How Has This Been Tested?
Validation:
- `bundle exec rspec spec/models/email_template_spec.rb
spec/controllers/api/v1/accounts/branded_email_layouts_controller_spec.rb
spec/controllers/api/v1/accounts/inboxes_controller_spec.rb
spec/lib/email_templates/db_resolver_service_spec.rb
spec/mailers/conversation_reply_mailer_spec.rb`
- `bundle exec rspec
spec/enterprise/services/enterprise/billing/handle_stripe_event_service_spec.rb
spec/enterprise/services/internal/reconcile_plan_config_service_spec.rb`
- `bundle exec rubocop` on changed Ruby files, excluding generated
`db/schema.rb`
- `git diff --check` and `git diff --cached --check`
- YAML parsing for changed config/Swagger files
- `bundle exec rails routes -g branded_email_layout`
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have made corresponding changes to the documentation
- [x] 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
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] My changes generate no new warnings
- [ ] Any dependent changes have been merged and published in downstream
modules
146 lines
5.8 KiB
Ruby
146 lines
5.8 KiB
Ruby
require 'rails_helper'
|
|
|
|
describe EmailTemplates::DbResolverService do
|
|
subject(:resolver) { described_class.using(EmailTemplate, {}) }
|
|
|
|
describe '#find_templates' do
|
|
after do
|
|
Current.reset
|
|
end
|
|
|
|
context 'when template does not exist in db' do
|
|
it 'return empty array' do
|
|
expect(resolver.find_templates('test', '', false, [])).to eq([])
|
|
end
|
|
end
|
|
|
|
context 'when installation template exist in db' do
|
|
it 'return installation template' do
|
|
email_template = create(:email_template, name: 'test', body: 'test')
|
|
handler = ActionView::Template.registered_template_handler(:liquid)
|
|
template_details = {
|
|
locals: [],
|
|
format: Mime['html'].to_sym,
|
|
virtual_path: 'test'
|
|
}
|
|
|
|
expect(
|
|
resolver.find_templates('test', '', false, []).first.inspect
|
|
).to eq(
|
|
ActionView::Template.new(
|
|
email_template.body,
|
|
"DB Template - #{email_template.id}", handler, **template_details
|
|
).inspect
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'when account template exists in db' do
|
|
let(:account) { create(:account) }
|
|
let!(:installation_template) { create(:email_template, name: 'test', body: 'test') }
|
|
let!(:account_template) { create(:email_template, name: 'test', body: 'test2', account: account) }
|
|
|
|
it 'return account template for current account' do
|
|
Current.account = account
|
|
handler = ActionView::Template.registered_template_handler(:liquid)
|
|
template_details = {
|
|
locals: [],
|
|
format: Mime['html'].to_sym,
|
|
virtual_path: 'test'
|
|
}
|
|
|
|
expect(
|
|
resolver.find_templates('test', '', false, []).first.inspect
|
|
).to eq(
|
|
ActionView::Template.new(
|
|
account_template.body,
|
|
"DB Template - #{account_template.id}", handler, **template_details
|
|
).inspect
|
|
)
|
|
end
|
|
|
|
it 'return installation template when current account dont have template' do
|
|
Current.account = create(:account)
|
|
handler = ActionView::Template.registered_template_handler(:liquid)
|
|
template_details = {
|
|
locals: [],
|
|
format: Mime['html'].to_sym,
|
|
virtual_path: 'test'
|
|
}
|
|
|
|
expect(
|
|
resolver.find_templates('test', '', false, []).first.inspect
|
|
).to eq(
|
|
ActionView::Template.new(
|
|
installation_template.body,
|
|
"DB Template - #{installation_template.id}", handler, **template_details
|
|
).inspect
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'when inbox template exists in db' do
|
|
let(:account) { create(:account) }
|
|
let(:inbox) { create(:inbox, :with_email, account: account) }
|
|
let!(:inbox_template) { create(:email_template, :layout, account: account, inbox: inbox, body: 'inbox {{ content_for_layout }}') }
|
|
let!(:installation_template) { create(:email_template, :layout, body: 'global {{ content_for_layout }}') }
|
|
|
|
it 'returns inbox template when branded email templates feature is enabled' do
|
|
account.enable_features!(:branded_email_templates)
|
|
Current.account = account
|
|
Current.inbox = inbox
|
|
|
|
expect(resolver.find_templates('base', 'layouts/mailer', false, { locale: [:en] }).first.source).to eq(inbox_template.body)
|
|
end
|
|
|
|
it 'skips account template when branded email templates feature is disabled' do
|
|
account_template = create(:email_template, :layout, account: account, body: 'account {{ content_for_layout }}')
|
|
Current.account = account
|
|
Current.inbox = inbox
|
|
|
|
resolved_template = resolver.find_templates('base', 'layouts/mailer', false, { locale: [:en] }).first
|
|
expect(resolved_template.source).to eq(installation_template.body)
|
|
expect(resolved_template.source).not_to eq(account_template.body)
|
|
end
|
|
|
|
it 'returns account template when current inbox is not email and feature is enabled' do
|
|
account_template = create(:email_template, :layout, account: account, body: 'account {{ content_for_layout }}')
|
|
account.enable_features!(:branded_email_templates)
|
|
Current.account = account
|
|
Current.inbox = create(:inbox, account: account)
|
|
|
|
resolved_template = resolver.find_templates('base', 'layouts/mailer', false, { locale: [:en] }).first
|
|
expect(resolved_template.source).to eq(account_template.body)
|
|
expect(resolved_template.source).not_to eq(installation_template.body)
|
|
end
|
|
|
|
it 'skips account template when current inbox is not email and feature is disabled' do
|
|
account_template = create(:email_template, :layout, account: account, body: 'account {{ content_for_layout }}')
|
|
Current.account = account
|
|
Current.inbox = create(:inbox, account: account)
|
|
|
|
resolved_template = resolver.find_templates('base', 'layouts/mailer', false, { locale: [:en] }).first
|
|
expect(resolved_template.source).to eq(installation_template.body)
|
|
expect(resolved_template.source).not_to eq(account_template.body)
|
|
end
|
|
|
|
it 'skips account template without an inbox when feature is disabled' do
|
|
account_template = create(:email_template, :layout, account: account, body: 'account {{ content_for_layout }}')
|
|
Current.account = account
|
|
|
|
resolved_template = resolver.find_templates('base', 'layouts/mailer', false, { locale: [:en] }).first
|
|
expect(resolved_template.source).to eq(installation_template.body)
|
|
expect(resolved_template.source).not_to eq(account_template.body)
|
|
end
|
|
|
|
it 'falls back to english when requested locale does not have a template' do
|
|
account.enable_features!(:branded_email_templates)
|
|
Current.account = account
|
|
Current.inbox = inbox
|
|
|
|
expect(resolver.find_templates('base', 'layouts/mailer', false, { locale: [:fr] }).first.source).to eq(inbox_template.body)
|
|
end
|
|
end
|
|
end
|
|
end
|