create([ 'source' => 'local', 'name' => 'Benevoles', 'slug' => 'benevoles', ]); $first = $this->createContact('Alice', 'Alpha'); $duplicate = $this->createContact('Bob', 'Beta'); $optOut = $this->createContact('Claire', 'Refus', smsOptOut: true); $fixed = $this->createContact('Denis', 'Fixe'); $invalid = $this->createContact('Eva', 'Invalide'); $missing = $this->createContact('Fatima', 'Sansphone'); $group->contacts()->attach([$first->id, $optOut->id, $fixed->id, $invalid->id, $missing->id]); $first->phoneNumbers()->create([ 'raw_number' => '06 11 11 11 11', 'normalized_number' => '+33611111111', 'country' => 'FR', 'is_valid' => true, 'is_mobile' => true, 'is_primary' => true, ]); $duplicate->phoneNumbers()->create([ 'raw_number' => '+33 6 11 11 11 11', 'normalized_number' => '+33611111111', 'country' => 'FR', 'is_valid' => true, 'is_mobile' => true, 'is_primary' => true, ]); $optOut->phoneNumbers()->create([ 'raw_number' => '06 22 22 22 22', 'normalized_number' => '+33622222222', 'country' => 'FR', 'is_valid' => true, 'is_mobile' => true, 'is_primary' => true, ]); $fixed->phoneNumbers()->create([ 'raw_number' => '01 44 44 44 44', 'normalized_number' => '+33144444444', 'country' => 'FR', 'is_valid' => true, 'is_mobile' => false, 'is_primary' => true, ]); $invalid->phoneNumbers()->create([ 'raw_number' => '123', 'normalized_number' => null, 'country' => 'FR', 'is_valid' => false, 'is_mobile' => false, 'is_primary' => true, 'validation_error' => 'Numero invalide.', ]); $result = app(RecipientResolver::class)->resolve([$duplicate->id], [$group->id]); $summary = $result->summary(); $this->assertSame(1, $summary['ready_numbers']); $this->assertSame('+33611111111', $result->recipients[0]['number']); $this->assertSame(1, $summary['duplicate']); $this->assertSame(1, $summary['opt_out']); $this->assertSame(1, $summary['non_mobile']); $this->assertSame(1, $summary['invalid']); $this->assertSame(1, $summary['missing_phone']); } public function test_resolver_trusts_normalized_mobile_flag_instead_of_raw_regex(): void { config(['relais_sms.default_settings.mobile_phone_regex' => '/^never-matches$/']); $contact = $this->createContact('Alice', 'Mobile'); $contact->phoneNumbers()->create([ 'raw_number' => '06 11 11 11 11', 'normalized_number' => '+33611111111', 'country' => 'FR', 'is_valid' => true, 'is_mobile' => true, 'is_primary' => true, ]); $result = app(RecipientResolver::class)->resolve([$contact->id], []); $this->assertSame(1, $result->summary()['ready_numbers']); $this->assertSame([], $result->excluded); } public function test_resolver_accepts_stale_rows_when_normalized_number_is_a_french_mobile(): void { $contact = $this->createContact('Yveline', 'Mobile'); $contact->phoneNumbers()->create([ 'raw_number' => '064-201-4207', 'normalized_number' => '+33642014207', 'country' => 'FR', 'is_valid' => true, 'is_mobile' => false, 'is_primary' => true, ]); $result = app(RecipientResolver::class)->resolve([$contact->id], []); $this->assertSame(1, $result->summary()['ready_numbers']); $this->assertSame('+33642014207', $result->recipients[0]['number']); $this->assertSame([], $result->excluded); } public function test_authenticated_user_can_preview_recipient_selection(): void { $user = User::factory()->create(); $group = ContactGroup::query()->create([ 'source' => 'local', 'name' => 'Membres', 'slug' => 'membres', ]); $contact = $this->createContact('Marie', 'Durand'); $contact->groups()->attach($group); $contact->phoneNumbers()->create([ 'raw_number' => '07 55 55 00 04', 'normalized_number' => '+33755550004', 'country' => 'FR', 'is_valid' => true, 'is_mobile' => true, 'is_primary' => true, ]); $this->actingAs($user) ->get('/recipients') ->assertOk() ->assertSee('Composer') ->assertSee('Pour'); $this->actingAs($user) ->getJson('/recipients/search?q=Durand') ->assertOk() ->assertJsonFragment([ 'type' => 'contact', 'label' => 'Marie Durand', ]); $this->actingAs($user) ->getJson('/recipients/search?q=Membres') ->assertOk() ->assertJsonFragment([ 'type' => 'group', 'label' => 'Membres', ]); $this->actingAs($user) ->post('/recipients/preview', [ 'group_ids' => [$group->id], 'contact_ids' => [$contact->id], ]) ->assertOk() ->assertSee('Liste finale') ->assertSee('+33755550004') ->assertSee('Numeros prets'); } public function test_composer_shows_configured_sms_header_footer_and_complete_message(): void { AppSetting::putString('sms_message_header', 'AFPMA'); AppSetting::putString('sms_message_footer', 'Envoyer STOP SMS pour ne plus etre contacte.'); $user = User::factory()->create(); $contact = $this->createContact('Marie', 'Durand'); $contact->phoneNumbers()->create([ 'raw_number' => '07 55 55 00 04', 'normalized_number' => '+33755550004', 'country' => 'FR', 'is_valid' => true, 'is_mobile' => true, 'is_primary' => true, ]); $this->actingAs($user) ->post('/recipients/preview', [ 'contact_ids' => [$contact->id], 'message' => 'Bonjour', 'include_sms_header' => '1', 'include_sms_footer' => '1', ]) ->assertOk() ->assertSee('SMS complet') ->assertSee("AFPMA\nBonjour\nEnvoyer STOP SMS pour ne plus etre contacte."); } private function createContact(string $firstName, string $lastName, bool $smsOptOut = false): Contact { return Contact::query()->create([ 'source' => 'local', 'first_name' => $firstName, 'last_name' => $lastName, 'sms_opt_out' => $smsOptOut, ]); } }