diff --git a/resources/views/email_services/options/smtp.blade.php b/resources/views/email_services/options/smtp.blade.php
index f064bb0a..2ab3727c 100644
--- a/resources/views/email_services/options/smtp.blade.php
+++ b/resources/views/email_services/options/smtp.blade.php
@@ -2,4 +2,4 @@
-
\ No newline at end of file
+
diff --git a/src/Exceptions/MessageLimitReachedException.php b/src/Exceptions/MessageLimitReachedException.php
new file mode 100644
index 00000000..83592e31
--- /dev/null
+++ b/src/Exceptions/MessageLimitReachedException.php
@@ -0,0 +1,10 @@
+campaigns = $campaigns;
- $this->quotaService = $quotaService;
}
/**
@@ -56,11 +48,6 @@ public function send(CampaignDispatchRequest $request, int $id): RedirectRespons
$campaign->tags()->sync($request->get('tags'));
- if ($this->quotaService->exceedsQuota($campaign->email_service, $campaign->unsent_count)) {
- return redirect()->route('sendportal.campaigns.edit', $id)
- ->withErrors(__('The number of subscribers for this campaign exceeds your SES quota'));
- }
-
$scheduledAt = $request->get('schedule') === 'scheduled' ? Carbon::parse($request->get('scheduled_at')) : now();
$campaign->update([
diff --git a/src/Interfaces/QuotaServiceInterface.php b/src/Interfaces/QuotaServiceInterface.php
index f347f538..d387ce35 100644
--- a/src/Interfaces/QuotaServiceInterface.php
+++ b/src/Interfaces/QuotaServiceInterface.php
@@ -6,5 +6,5 @@
interface QuotaServiceInterface
{
- public function exceedsQuota(EmailService $emailService, int $messageCount): bool;
+ public function hasReachedMessageLimit(EmailService $emailService): bool;
}
diff --git a/src/Listeners/MessageDispatchHandler.php b/src/Listeners/MessageDispatchHandler.php
index e70233e4..915dfe34 100644
--- a/src/Listeners/MessageDispatchHandler.php
+++ b/src/Listeners/MessageDispatchHandler.php
@@ -6,11 +6,15 @@
use Exception;
use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Queue\InteractsWithQueue;
use Sendportal\Base\Events\MessageDispatchEvent;
+use Sendportal\Base\Exceptions\MessageLimitReachedException;
use Sendportal\Base\Services\Messages\DispatchMessage;
class MessageDispatchHandler implements ShouldQueue
{
+ use InteractsWithQueue;
+
/** @var string */
public $queue = 'sendportal-message-dispatch';
@@ -27,6 +31,10 @@ public function __construct(DispatchMessage $dispatchMessage)
*/
public function handle(MessageDispatchEvent $event): void
{
- $this->dispatchMessage->handle($event->message);
+ try {
+ $this->dispatchMessage->handle($event->message);
+ } catch (MessageLimitReachedException $e) {
+ $this->release();
+ }
}
}
diff --git a/src/Services/Messages/DispatchMessage.php b/src/Services/Messages/DispatchMessage.php
index 584cdaf4..c60321b8 100644
--- a/src/Services/Messages/DispatchMessage.php
+++ b/src/Services/Messages/DispatchMessage.php
@@ -6,6 +6,8 @@
use Exception;
use Illuminate\Support\Facades\Log;
+use Sendportal\Base\Exceptions\MessageLimitReachedException;
+use Sendportal\Base\Interfaces\QuotaServiceInterface;
use Sendportal\Base\Models\Campaign;
use Sendportal\Base\Models\CampaignStatus;
use Sendportal\Base\Models\EmailService;
@@ -30,18 +32,23 @@ class DispatchMessage
/** @var MarkAsSent */
protected $markAsSent;
+ /** @var QuotaServiceInterface */
+ protected $quotaService;
+
public function __construct(
MergeContentService $mergeContentService,
MergeSubjectService $mergeSubjectService,
ResolveEmailService $resolveEmailService,
RelayMessage $relayMessage,
- MarkAsSent $markAsSent
+ MarkAsSent $markAsSent,
+ QuotaServiceInterface $quotaService
) {
$this->mergeContentService = $mergeContentService;
$this->mergeSubjectService = $mergeSubjectService;
$this->resolveEmailService = $resolveEmailService;
$this->relayMessage = $relayMessage;
$this->markAsSent = $markAsSent;
+ $this->quotaService = $quotaService;
}
/**
@@ -58,11 +65,13 @@ public function handle(Message $message): ?string
$message = $this->mergeSubject($message);
$mergedContent = $this->getMergedContent($message);
-
$emailService = $this->getEmailService($message);
-
$trackingOptions = MessageTrackingOptions::fromMessage($message);
+ if ($this->quotaService->hasReachedMessageLimit($emailService)) {
+ throw new MessageLimitReachedException;
+ }
+
$messageId = $this->dispatch($message, $emailService, $trackingOptions, $mergedContent);
$this->markSent($message, $messageId);
diff --git a/src/Services/QuotaService.php b/src/Services/QuotaService.php
index a5fe6361..90d6ae1e 100644
--- a/src/Services/QuotaService.php
+++ b/src/Services/QuotaService.php
@@ -12,11 +12,11 @@
class QuotaService implements QuotaServiceInterface
{
- public function exceedsQuota(EmailService $emailService, int $messageCount): bool
+ public function hasReachedMessageLimit(EmailService $emailService): bool
{
switch ($emailService->type_id) {
case EmailServiceType::SES:
- return $this->exceedsSesQuota($emailService, $messageCount);
+ return $this->exceedsSesQuota($emailService);
case EmailServiceType::SENDGRID:
case EmailServiceType::MAILGUN:
@@ -34,7 +34,7 @@ protected function resolveMailAdapter(EmailService $emailService): BaseMailAdapt
return app(MailAdapterFactory::class)->adapter($emailService);
}
- protected function exceedsSesQuota(EmailService $emailService, int $messageCount): bool
+ protected function exceedsSesQuota(EmailService $emailService): bool
{
$mailAdapter = $this->resolveMailAdapter($emailService);
@@ -60,8 +60,6 @@ protected function exceedsSesQuota(EmailService $emailService, int $messageCount
$sent = Arr::get($quota, 'SentLast24Hours');
- $remaining = (int)floor($limit - $sent);
-
- return $messageCount > $remaining;
+ return $sent >= $limit;
}
}
diff --git a/src/Traits/MocksSesMailAdapter.php b/src/Traits/MocksSesMailAdapter.php
new file mode 100644
index 00000000..b0e14bfc
--- /dev/null
+++ b/src/Traits/MocksSesMailAdapter.php
@@ -0,0 +1,38 @@
+ $quota,
+ 'SentLast24Hours' => $sent,
+ ];
+ }
+
+ $sesClient = $this->getMockBuilder(SesClient::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $sesClient->method('__call')->willReturn(collect($sendQuota));
+
+ $aws = $this->getMockBuilder(Sdk::class)->getMock();
+ $aws->method('createClient')->willReturn($sesClient);
+
+ $this->app->singleton('aws', function () use ($aws) {
+ return $aws;
+ });
+ }
+}
diff --git a/tests/Feature/Messages/MessagesControllerTest.php b/tests/Feature/Messages/MessagesControllerTest.php
index 98bf0b78..998b48d5 100644
--- a/tests/Feature/Messages/MessagesControllerTest.php
+++ b/tests/Feature/Messages/MessagesControllerTest.php
@@ -8,12 +8,21 @@
use Sendportal\Base\Facades\Sendportal;
use Sendportal\Base\Models\Campaign;
use Sendportal\Base\Models\Message;
+use Sendportal\Base\Traits\MocksSesMailAdapter;
use Tests\TestCase;
class MessagesControllerTest extends TestCase
{
+ use MocksSesMailAdapter;
use RefreshDatabase;
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->mockSesMailAdapter();
+ }
+
/** @test */
public function the_index_of_sent_messages_is_accessible_to_an_authenticated_user()
{
diff --git a/tests/Unit/Services/QuotaServiceTest.php b/tests/Unit/Services/QuotaServiceTest.php
index 96b5b06e..09117f2d 100644
--- a/tests/Unit/Services/QuotaServiceTest.php
+++ b/tests/Unit/Services/QuotaServiceTest.php
@@ -4,15 +4,19 @@
namespace Tests\Unit\Services;
-use Aws\Sdk;
-use Aws\Ses\SesClient;
+use Sendportal\Base\Exceptions\MessageLimitReachedException;
use Sendportal\Base\Interfaces\QuotaServiceInterface;
+use Sendportal\Base\Models\Campaign;
use Sendportal\Base\Models\EmailService;
use Sendportal\Base\Models\EmailServiceType;
+use Sendportal\Base\Models\Message;
+use Sendportal\Base\Traits\MocksSesMailAdapter;
use Tests\TestCase;
class QuotaServiceTest extends TestCase
{
+ use MocksSesMailAdapter;
+
/** @var QuotaServiceInterface */
protected $quotaService;
@@ -24,75 +28,79 @@ public function setUp(): void
}
/** @test */
- public function fewer_subscribers_than_quota_available()
+ public function ses_message_limit_has_not_been_reached()
{
// given
$emailService = EmailService::factory()->create(['type_id' => EmailServiceType::SES]);
- $this->mockMailAdapter(2);
+ $this->mockSesMailAdapter(2);
// then
- self::assertFalse($this->quotaService->exceedsQuota($emailService, 1));
+ self::assertFalse($this->quotaService->hasReachedMessageLimit($emailService));
}
/** @test */
- public function more_subscribers_than_quota_available()
+ public function ses_message_limit_has_been_reached()
{
// given
$emailService = EmailService::factory()->create(['type_id' => EmailServiceType::SES]);
- $this->mockMailAdapter(1);
+ $this->mockSesMailAdapter(1, 1);
// then
- self::assertTrue($this->quotaService->exceedsQuota($emailService, 2));
+ self::assertTrue($this->quotaService->hasReachedMessageLimit($emailService));
}
/** @test */
- public function send_quota_not_available()
+ public function ses_message_limit_quota_not_available()
{
// given
$emailService = EmailService::factory()->create(['type_id' => EmailServiceType::SES]);
- $this->mockMailAdapter();
+ $this->mockSesMailAdapter();
// then
- self::assertFalse($this->quotaService->exceedsQuota($emailService, 1));
+ self::assertFalse($this->quotaService->hasReachedMessageLimit($emailService));
}
/** @test */
- public function unlimited_quota()
+ public function ses_message_limit_unlimited_quota()
{
// given
$emailService = EmailService::factory()->create(['type_id' => EmailServiceType::SES]);
- $this->mockMailAdapter(-1);
+ $this->mockSesMailAdapter(-1);
// then
- self::assertFalse($this->quotaService->exceedsQuota($emailService, 1));
+ self::assertFalse($this->quotaService->hasReachedMessageLimit($emailService));
}
- protected function mockMailAdapter(int $quota = null): void
+ /** @test */
+ public function a_message_limit_reached_exception_is_thrown()
{
- $sendQuota = [];
+ // given
+ $emailService = EmailService::factory()->create(['type_id' => EmailServiceType::SES]);
- if ($quota) {
- $sendQuota = [
- 'Max24HourSend' => $quota,
- 'SentLast24Hours' => 0,
- ];
- }
+ $campaign = Campaign::factory()->create(
+ [
+ 'email_service_id' => $emailService->id,
+ 'workspace_id' => $emailService->workspace_id,
+ 'content' => 'test',
+ ]
+ );
- $sesClient = $this->getMockBuilder(SesClient::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $message = Message::factory()->create(
+ [
+ 'source_id' => $campaign->id,
+ 'workspace_id' => $emailService->workspace_id,
+ ]
+ );
- $sesClient->method('__call')->willReturn(collect($sendQuota));
+ $this->mockSesMailAdapter(1, 1);
- $aws = $this->getMockBuilder(Sdk::class)->getMock();
- $aws->method('createClient')->willReturn($sesClient);
+ $this->withoutExceptionHandling()
+ ->expectException(MessageLimitReachedException::class);
- $this->app->singleton('aws', function () use ($aws) {
- return $aws;
- });
+ $this->post(route('sendportal.messages.send'), ['id' => $message->id]);
}
}