'other']); $message = 'Reunion https://zoom.us/j/123'; $provider = $this->mock(SmsProviderInterface::class); $provider->shouldReceive('send')->once()->withArgs(fn (SmsCampaign $campaign, array $recipients): bool => $campaign->message === $message) ->andReturn(new SmsSendResult(serviceName: 'other', providerJobId: '1', requestPayload: ['message' => $message], responsePayload: [])); $campaign = $this->app->make(SmsCampaignSender::class)->send(User::factory()->create(), $message, []); $this->assertSame($message, $campaign->message); } public function test_history_keeps_the_message_actually_transmitted_by_ovh(): void { config([ 'relais_sms.ovh.sms_short_code' => true, 'relais_sms.ovh.sms_sender' => '', 'relais_sms.ovh.sms_service_name' => 'sms-test', 'relais_sms.ovh.application_key' => 'test', 'relais_sms.ovh.application_secret' => 'test', 'relais_sms.ovh.consumer_key' => 'test', ]); $client = new Client(['handler' => HandlerStack::create(new MockHandler([ new Response(200, [], '123456'), new Response(200, [], '{"ids":[]}'), ]))]); $this->app->instance(SmsProviderInterface::class, new OvhSmsProvider($client)); $campaign = $this->app->make(SmsCampaignSender::class)->send(User::factory()->create(), 'Reunion https://zoom.us/j/123 demain', []); $this->assertSame('Reunion demain', $campaign->message); $this->assertSame('Reunion demain', $campaign->request_payload['message']); } public function test_authenticated_user_can_send_sms_campaign_and_history_is_recorded(): void { $this->app->bind(SmsProviderInterface::class, fn (): SmsProviderInterface => new class implements SmsProviderInterface { public function send(SmsCampaign $campaign, array $recipients): SmsSendResult { return new SmsSendResult( serviceName: 'sms-test', providerJobId: 'job-123', requestPayload: ['receivers' => array_column($recipients, 'number')], responsePayload: ['ids' => ['message-123']], providerMessageIds: ['message-123'], ); } }); $user = User::factory()->create(); $contact = $this->createSmsContact(); $response = $this->actingAs($user) ->post(route('sms-campaigns.store'), [ 'contact_ids' => [$contact->id], 'message' => 'Message de test', ]); $response->assertRedirect(); $this->assertDatabaseHas('sms_campaigns', [ 'message' => 'Message de test', 'status' => 'sent', 'recipients_count' => 1, 'sent_count' => 1, 'provider_service_name' => 'sms-test', 'provider_job_id' => 'job-123', ]); $this->assertDatabaseHas('sms_deliveries', [ 'contact_id' => $contact->id, 'number' => '+33611111111', 'status' => 'sent', 'provider_message_id' => 'message-123', ]); $this->actingAs($user) ->get(route('sms-campaigns.index')) ->assertOk() ->assertSee('Message de test'); } public function test_sms_campaign_message_uses_configured_header_and_footer_when_selected(): void { AppSetting::putString('sms_message_header', 'AFPMA'); AppSetting::putString('sms_message_footer', 'Envoyer STOP SMS pour ne plus etre contacte.'); $this->app->bind(SmsProviderInterface::class, fn (): SmsProviderInterface => new class implements SmsProviderInterface { public function send(SmsCampaign $campaign, array $recipients): SmsSendResult { return new SmsSendResult( serviceName: 'sms-test', providerJobId: 'job-headers', requestPayload: ['message' => $campaign->message], responsePayload: ['ids' => ['message-headers']], providerMessageIds: ['message-headers'], ); } }); $user = User::factory()->create(); $contact = $this->createSmsContact(); $this->actingAs($user) ->post(route('sms-campaigns.store'), [ 'contact_ids' => [$contact->id], 'message' => 'Message de test', 'include_sms_header' => '1', 'include_sms_footer' => '1', ]) ->assertRedirect(); $this->assertDatabaseHas('sms_campaigns', [ 'message' => "AFPMA\nMessage de test\nEnvoyer STOP SMS pour ne plus etre contacte.", 'provider_job_id' => 'job-headers', ]); } public function test_sms_campaign_message_can_remove_configured_footer_in_composition(): void { AppSetting::putString('sms_message_header', 'AFPMA'); AppSetting::putString('sms_message_footer', 'Envoyer STOP SMS pour ne plus etre contacte.'); $this->app->bind(SmsProviderInterface::class, fn (): SmsProviderInterface => new class implements SmsProviderInterface { public function send(SmsCampaign $campaign, array $recipients): SmsSendResult { return new SmsSendResult( serviceName: 'sms-test', providerJobId: 'job-no-footer', requestPayload: ['message' => $campaign->message], responsePayload: ['ids' => ['message-no-footer']], providerMessageIds: ['message-no-footer'], ); } }); $user = User::factory()->create(); $contact = $this->createSmsContact(); $this->actingAs($user) ->post(route('sms-campaigns.store'), [ 'contact_ids' => [$contact->id], 'message' => 'Message de test', 'include_sms_header' => '1', 'include_sms_footer' => '0', ]) ->assertRedirect(); $this->assertDatabaseHas('sms_campaigns', [ 'message' => "AFPMA\nMessage de test", 'provider_job_id' => 'job-no-footer', ]); } public function test_authenticated_user_can_schedule_sms_campaign_without_sending_immediately(): void { Carbon::setTestNow(Carbon::parse('2026-06-12 10:00:00', 'UTC')); $provider = new class implements SmsProviderInterface { public int $calls = 0; public function send(SmsCampaign $campaign, array $recipients): SmsSendResult { $this->calls++; return new SmsSendResult( serviceName: 'sms-test', providerJobId: 'job-123', requestPayload: ['receivers' => array_column($recipients, 'number')], responsePayload: ['ids' => ['message-123']], providerMessageIds: ['message-123'], ); } }; $this->app->instance(SmsProviderInterface::class, $provider); $user = User::factory()->create(); $contact = $this->createSmsContact(); $response = $this->actingAs($user) ->post(route('sms-campaigns.store'), [ 'contact_ids' => [$contact->id], 'message' => 'Message programme', 'send_mode' => 'scheduled', 'scheduled_at' => '2026-06-12T12:05', ]); $response->assertRedirect(); $this->assertSame(0, $provider->calls); $this->assertDatabaseHas('sms_campaigns', [ 'message' => 'Message programme', 'status' => 'scheduled', 'recipients_count' => 1, 'sent_count' => 0, ]); $this->assertDatabaseHas('sms_deliveries', [ 'contact_id' => $contact->id, 'number' => '+33611111111', 'status' => 'queued', ]); } public function test_scheduled_campaign_can_be_canceled_before_processing(): void { Carbon::setTestNow(Carbon::parse('2026-06-12 10:00:00', 'UTC')); $provider = new class implements SmsProviderInterface { public int $calls = 0; public function send(SmsCampaign $campaign, array $recipients): SmsSendResult { $this->calls++; return new SmsSendResult('sms-test', 'job-123', [], []); } }; $this->app->instance(SmsProviderInterface::class, $provider); $user = User::factory()->create(); $contact = $this->createSmsContact(); $this->actingAs($user) ->post(route('sms-campaigns.store'), [ 'contact_ids' => [$contact->id], 'message' => 'Message annulable', 'send_mode' => 'scheduled', 'scheduled_at' => '2026-06-12T12:05', ]); $campaign = SmsCampaign::query()->firstOrFail(); $this->actingAs($user) ->patch(route('sms-campaigns.cancel', $campaign)) ->assertRedirect(route('sms-campaigns.show', $campaign)); $this->assertSame(0, $provider->calls); $this->assertDatabaseHas('sms_campaigns', [ 'id' => $campaign->id, 'status' => 'canceled', ]); $this->assertDatabaseHas('sms_deliveries', [ 'sms_campaign_id' => $campaign->id, 'status' => 'canceled', ]); } public function test_due_scheduled_campaign_is_sent_by_scheduler_command(): void { Carbon::setTestNow(Carbon::parse('2026-06-12 10:00:00', 'UTC')); Log::spy(); $provider = new class implements SmsProviderInterface { public int $calls = 0; public function send(SmsCampaign $campaign, array $recipients): SmsSendResult { $this->calls++; return new SmsSendResult( serviceName: 'sms-test', providerJobId: 'job-456', requestPayload: ['receivers' => array_column($recipients, 'number')], responsePayload: ['ids' => ['message-456']], providerMessageIds: ['message-456'], ); } }; $this->app->instance(SmsProviderInterface::class, $provider); $user = User::factory()->create(); $contact = $this->createSmsContact(); $this->actingAs($user) ->post(route('sms-campaigns.store'), [ 'contact_ids' => [$contact->id], 'message' => 'Message automatique', 'send_mode' => 'scheduled', 'scheduled_at' => '2026-06-12T12:05', ]); Carbon::setTestNow(Carbon::parse('2026-06-12 10:05:00', 'UTC')); $this->artisan('sms-campaigns:send-due') ->expectsOutput('1 campagne(s) SMS programmee(s) prise(s) en charge.') ->assertExitCode(0); $this->assertSame(1, $provider->calls); $this->assertDatabaseHas('sms_campaigns', [ 'message' => 'Message automatique', 'status' => 'sent', 'sent_count' => 1, 'provider_job_id' => 'job-456', ]); $this->assertDatabaseHas('sms_deliveries', [ 'number' => '+33611111111', 'status' => 'sent', 'provider_message_id' => 'message-456', ]); Log::shouldHaveReceived('info') ->once() ->with('SMS campaign status changed from scheduled to sending.', \Mockery::type('array')); } private function createSmsContact(): Contact { $contact = Contact::query()->create([ 'source' => 'local', 'first_name' => 'Marie', 'last_name' => 'Durand', ]); $contact->phoneNumbers()->create([ 'raw_number' => '06 11 11 11 11', 'normalized_number' => '+33611111111', 'country' => 'FR', 'is_valid' => true, 'is_mobile' => true, 'is_primary' => true, ]); return $contact; } }