Skip to content
Draft
488 changes: 488 additions & 0 deletions .claude/skills/convert-email-mjml/SKILL.md

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions .claude/skills/convert-email-mjml/template.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<mjml>
<mj-head>
<mj-include path="../../components/head.mjml" />
</mj-head>

<mj-body>
<!-- For standard emails with logo -->
<mj-include path="../../components/logo.mjml" />

<!-- For Provider emails with blue header, use instead: -->
<!-- <mj-bw-simple-hero /> -->
<!-- OR -->
<!-- <mj-bw-hero /> -->

<!-- Main content area -->
<mj-wrapper background-color="#fff" border="1px solid #e9e9e9"
css-class="border-fix" padding="0">
<mj-section>
<mj-column>

<!-- Text blocks -->
<mj-text padding="0 0 10px">
Your text content here with {{HandlebarsVariables}}
</mj-text>

<!-- Conditional text (inside mj-text, no mj-raw needed) -->
<mj-text>
{{#if SomeCondition}}
Conditional text here
{{else}}
Alternative text
{{/if}}
</mj-text>

<!-- Buttons -->
<mj-button href="{{{Url}}}" border-radius="5px">
Button Text
</mj-button>

</mj-column>
</mj-section>

<!-- Conditional structural elements (wrap with mj-raw) -->
<mj-raw>{{#unless SomeCondition}}</mj-raw>
<mj-section>
<mj-column>
<mj-text>
This entire section is conditional
</mj-text>
</mj-column>
</mj-section>
<mj-raw>{{/unless}}</mj-raw>

</mj-wrapper>

<!-- Learn More footer -->
<mj-wrapper>
<mj-bw-learn-more-footer />
</mj-wrapper>

<!-- Standard footer -->
<mj-include path="../../components/footer.mjml" />
</mj-body>
</mjml>
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,21 @@
using Bit.Core.Billing.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Mail.Provider.ProviderUpdatePaymentMethod;
using Bit.Core.Platform.Mail.Mailer;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Stripe;

namespace Bit.Commercial.Core.AdminConsole.Providers;

public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProviderCommand
{
private readonly IEventService _eventService;
private readonly IMailService _mailService;
private readonly IMailer _mailer;
private readonly IGlobalSettings _globalSettings;
private readonly IOrganizationRepository _organizationRepository;
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IStripeAdapter _stripeAdapter;
Expand All @@ -34,7 +39,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv

public RemoveOrganizationFromProviderCommand(
IEventService eventService,
IMailService mailService,
IMailer mailer,
IGlobalSettings globalSettings,
IOrganizationRepository organizationRepository,
IProviderOrganizationRepository providerOrganizationRepository,
IStripeAdapter stripeAdapter,
Expand All @@ -45,7 +51,8 @@ public RemoveOrganizationFromProviderCommand(
IPricingClient pricingClient)
{
_eventService = eventService;
_mailService = mailService;
_mailer = mailer;
_globalSettings = globalSettings;
_organizationRepository = organizationRepository;
_providerOrganizationRepository = providerOrganizationRepository;
_stripeAdapter = stripeAdapter;
Expand Down Expand Up @@ -176,10 +183,16 @@ private async Task ResetOrganizationBillingAsync(
await _subscriberService.RemovePaymentSource(organization);
}

await _mailService.SendProviderUpdatePaymentMethod(
organization.Id,
organization.Name,
provider.Name!,
organizationOwnerEmails);
await _mailer.SendEmail(new ProviderUpdatePaymentMethodMail
{
ToEmails = organizationOwnerEmails,
View = new ProviderUpdatePaymentMethodMailView
{
OrganizationId = organization.Id.ToString(),
OrganizationName = CoreHelpers.SanitizeForEmail(organization.Name),
ProviderName = CoreHelpers.SanitizeForEmail(provider.Name!),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Mail.Billing.BusinessUnitConversionInvite;
using Bit.Core.Platform.Mail.Mailer;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.DataProtection;
Expand All @@ -30,7 +31,7 @@ public class BusinessUnitConverter(
IDataProtectionProvider dataProtectionProvider,
GlobalSettings globalSettings,
ILogger<BusinessUnitConverter> logger,
IMailService mailService,
IMailer mailer,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IPricingClient pricingClient,
Expand Down Expand Up @@ -319,7 +320,19 @@ private async Task SendInviteAsync(
var token = _dataProtector.Protect(
$"BusinessUnitConversionInvite {organization.Id} {providerAdminEmail} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");

await mailService.SendBusinessUnitConversionInviteAsync(organization, token, providerAdminEmail);
var mail = new BusinessUnitConversionInviteMail
{
ToEmails = [providerAdminEmail],
View = new BusinessUnitConversionInviteMailView
{
OrganizationId = organization.Id.ToString(),
Email = System.Net.WebUtility.UrlEncode(providerAdminEmail),
Token = System.Net.WebUtility.UrlEncode(token),
WebVaultUrl = globalSettings.BaseServiceUri.VaultWithHash
}
};

await mailer.SendEmail(mail);
}

private async Task<(Subscription, Provider, ProviderOrganization, ProviderUser)> ValidateFinalizationAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
using Bit.Core.Billing.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Mail.Provider.ProviderUpdatePaymentMethod;
using Bit.Core.Platform.Mail.Mailer;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Mocks;
Expand Down Expand Up @@ -123,12 +125,9 @@ await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
await sutProvider.GetDependency<IEventService>().Received(1)
.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);

await sutProvider.GetDependency<IMailService>().Received(1)
.SendProviderUpdatePaymentMethod(
organization.Id,
organization.Name,
provider.Name,
Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == "a@example.com"));
await sutProvider.GetDependency<IMailer>().Received(1)
.SendEmail(Arg.Is<ProviderUpdatePaymentMethodMail>(mail =>
mail.ToEmails.FirstOrDefault() == "a@example.com"));

await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
.UpdateCustomerAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
Expand Down Expand Up @@ -186,12 +185,9 @@ await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
await sutProvider.GetDependency<IEventService>().Received(1)
.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);

await sutProvider.GetDependency<IMailService>().Received(1)
.SendProviderUpdatePaymentMethod(
organization.Id,
organization.Name,
provider.Name,
Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == "a@example.com"));
await sutProvider.GetDependency<IMailer>().Received(1)
.SendEmail(Arg.Is<ProviderUpdatePaymentMethodMail>(mail =>
mail.ToEmails.FirstOrDefault() == "a@example.com"));
}

[Theory, BitAutoData]
Expand Down Expand Up @@ -275,12 +271,9 @@ await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
await sutProvider.GetDependency<IEventService>().Received(1)
.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);

await sutProvider.GetDependency<IMailService>().Received(1)
.SendProviderUpdatePaymentMethod(
organization.Id,
organization.Name,
provider.Name,
Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == "a@example.com"));
await sutProvider.GetDependency<IMailer>().Received(1)
.SendEmail(Arg.Is<ProviderUpdatePaymentMethodMail>(mail =>
mail.ToEmails.FirstOrDefault() == "a@example.com"));
}

[Theory, BitAutoData]
Expand Down Expand Up @@ -364,12 +357,9 @@ await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
await sutProvider.GetDependency<IEventService>().Received(1)
.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);

await sutProvider.GetDependency<IMailService>().Received(1)
.SendProviderUpdatePaymentMethod(
organization.Id,
organization.Name,
provider.Name,
Arg.Is<IEnumerable<string>>(emails => emails.FirstOrDefault() == "a@example.com"));
await sutProvider.GetDependency<IMailer>().Received(1)
.SendEmail(Arg.Is<ProviderUpdatePaymentMethodMail>(mail =>
mail.ToEmails.FirstOrDefault() == "a@example.com"));
}

private static Subscription GetSubscription(string subscriptionId, string customerId) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Mail.Billing.BusinessUnitConversionInvite;
using Bit.Core.Platform.Mail.Mailer;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Test.Billing.Mocks;
using Bit.Core.Utilities;
Expand All @@ -34,7 +35,7 @@ public class BusinessUnitConverterTests
private readonly IDataProtectionProvider _dataProtectionProvider = Substitute.For<IDataProtectionProvider>();
private readonly GlobalSettings _globalSettings = new();
private readonly ILogger<BusinessUnitConverter> _logger = Substitute.For<ILogger<BusinessUnitConverter>>();
private readonly IMailService _mailService = Substitute.For<IMailService>();
private readonly IMailer _mailer = Substitute.For<IMailer>();
private readonly IOrganizationRepository _organizationRepository = Substitute.For<IOrganizationRepository>();
private readonly IOrganizationUserRepository _organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
private readonly IPricingClient _pricingClient = Substitute.For<IPricingClient>();
Expand All @@ -50,7 +51,7 @@ public class BusinessUnitConverterTests
_dataProtectionProvider,
_globalSettings,
_logger,
_mailService,
_mailer,
_organizationRepository,
_organizationUserRepository,
_pricingClient,
Expand Down Expand Up @@ -280,10 +281,11 @@ await _providerUserRepository.Received(1).CreateAsync(
argument.Status == ProviderUserStatusType.Invited &&
argument.Type == ProviderUserType.ProviderAdmin));

await _mailService.Received(1).SendBusinessUnitConversionInviteAsync(
organization,
token,
user.Email);
await _mailer.Received(1).SendEmail(
Arg.Is<BusinessUnitConversionInviteMail>(m =>
m.ToEmails.Contains(user.Email) &&
m.View.OrganizationId == organization.Id.ToString() &&
m.View.Token == System.Net.WebUtility.UrlEncode(token)));
}

[Theory, BitAutoData]
Expand Down Expand Up @@ -348,10 +350,11 @@ public async Task ResendConversionInvite_ConversionInProgress_Succeeds(

await businessUnitConverter.ResendConversionInvite(organization, providerAdminEmail);

await _mailService.Received(1).SendBusinessUnitConversionInviteAsync(
organization,
token,
providerAdminEmail);
await _mailer.Received(1).SendEmail(
Arg.Is<BusinessUnitConversionInviteMail>(m =>
m.ToEmails.Contains(providerAdminEmail) &&
m.View.OrganizationId == organization.Id.ToString() &&
m.View.Token == System.Net.WebUtility.UrlEncode(token)));
}

[Theory, BitAutoData]
Expand All @@ -365,10 +368,8 @@ public async Task ResendConversionInvite_NoConversionInProgress_DoesNothing(

await businessUnitConverter.ResendConversionInvite(organization, providerAdminEmail);

await _mailService.DidNotReceiveWithAnyArgs().SendBusinessUnitConversionInviteAsync(
Arg.Any<Organization>(),
Arg.Any<string>(),
Arg.Any<string>());
await _mailer.DidNotReceiveWithAnyArgs().SendEmail(
Arg.Any<BusinessUnitConversionInviteMail>());
}

#endregion
Expand Down
Loading
Loading