diff --git a/src/Http/Controllers/Api/SubscribersController.php b/src/Http/Controllers/Api/SubscribersController.php index f3302229..320f0b38 100644 --- a/src/Http/Controllers/Api/SubscribersController.php +++ b/src/Http/Controllers/Api/SubscribersController.php @@ -45,7 +45,7 @@ public function index(int $workspaceId): AnonymousResourceCollection */ public function store(SubscriberStoreRequest $request, int $workspaceId): SubscriberResource { - $subscriber = $this->apiService->store($workspaceId, collect($request->validated())); + $subscriber = $this->apiService->storeOrUpdate($workspaceId, collect($request->validated())); $subscriber->load('segments'); diff --git a/src/Repositories/Subscribers/BaseSubscriberTenantRepository.php b/src/Repositories/Subscribers/BaseSubscriberTenantRepository.php index d93e0e5c..363db2d4 100644 --- a/src/Repositories/Subscribers/BaseSubscriberTenantRepository.php +++ b/src/Repositories/Subscribers/BaseSubscriberTenantRepository.php @@ -27,8 +27,8 @@ public function store($workspaceId, array $data) $subscriber = $this->executeSave($workspaceId, $instance, Arr::except($data, ['segments'])); - // only sync segments if its actually present. This means that users's must - // pass through an empty segments array if they want to delete all segments + // Only sync segments if its actually present. This means that users must + // pass through an empty segments array if they want to delete all segments. if (isset($data['segments'])) { $this->syncSegments($instance, Arr::get($data, 'segments', [])); } @@ -60,8 +60,8 @@ public function update($workspaceId, $id, array $data) $subscriber = $this->executeSave($workspaceId, $instance, Arr::except($data, ['segments', 'id'])); - // only sync segments if its actually present. This means that users's must - // pass through an empty segments array if they want to delete all segments + // Only sync segments if its actually present. This means that users must + // pass through an empty segments array if they want to delete all segments. if (isset($data['segments'])) { $this->syncSegments($instance, Arr::get($data, 'segments', [])); } diff --git a/src/Services/Subscribers/ApiSubscriberService.php b/src/Services/Subscribers/ApiSubscriberService.php index e1be9a7c..1acee0a7 100644 --- a/src/Services/Subscribers/ApiSubscriberService.php +++ b/src/Services/Subscribers/ApiSubscriberService.php @@ -4,6 +4,7 @@ namespace Sendportal\Base\Services\Subscribers; +use Exception; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Sendportal\Base\Events\SubscriberAddedEvent; @@ -21,21 +22,24 @@ public function __construct(SubscriberTenantRepositoryInterface $subscribers) } /** - * @param int $workspaceId - * @param \Illuminate\Support\Collection $data + * The API provides the ability for the "store" endpoint to both create a new subscriber or update an existing + * subscriber, using their email as the key. This method allows us to handle both scenarios. * - * @return \Sendportal\Base\Models\Subscriber - * @throws \Exception + * @throws Exception */ - public function store(int $workspaceId, Collection $data): Subscriber + public function storeOrUpdate(int $workspaceId, Collection $data): Subscriber { - $subscriber = $this->subscribers->store($workspaceId, $data->except(['segments'])->toArray()); + $existingSubscriber = $this->subscribers->findBy($workspaceId, 'email', $data['email']); - event(new SubscriberAddedEvent($subscriber)); + if (!$existingSubscriber) { + $subscriber = $this->subscribers->store($workspaceId, $data->toArray()); - $this->handleSegments($data, $subscriber); + event(new SubscriberAddedEvent($subscriber)); - return $subscriber; + return $subscriber; + } + + return $this->subscribers->update($workspaceId, $existingSubscriber->id, $data->toArray()); } public function delete(int $workspaceId, Subscriber $subscriber): bool @@ -45,13 +49,4 @@ public function delete(int $workspaceId, Subscriber $subscriber): bool return $this->subscribers->destroy($workspaceId, $subscriber->id); }); } - - protected function handleSegments(Collection $data, Subscriber $subscriber): Subscriber - { - if (!empty($data['segments'])) { - $subscriber->segments()->sync($data['segments']); - } - - return $subscriber; - } } diff --git a/tests/Feature/API/SubscribersControllerTest.php b/tests/Feature/API/SubscribersControllerTest.php index 5fc68b35..8ef1ffbc 100644 --- a/tests/Feature/API/SubscribersControllerTest.php +++ b/tests/Feature/API/SubscribersControllerTest.php @@ -8,6 +8,7 @@ use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Support\Arr; use Sendportal\Base\Models\Segment; +use Sendportal\Base\Models\Subscriber; use Tests\TestCase; class SubscribersControllerTest extends TestCase @@ -18,57 +19,57 @@ class SubscribersControllerTest extends TestCase /** @test */ public function the_subscribers_index_is_accessible_to_authorised_users() { + // given $user = $this->createUserWithWorkspace(); $subscriber = $this->createSubscriber($user); - $route = route('sendportal.api.subscribers.index', [ + // when + $response = $this->get(route('sendportal.api.subscribers.index', [ 'workspaceId' => $user->currentWorkspace()->id, 'api_token' => $user->api_token, - ]); - - $response = $this->get($route); + ])); + // then $response->assertStatus(200); - $expected = [ + $response->assertJson([ 'data' => [ Arr::only($subscriber->toArray(), ['first_name', 'last_name', 'email']) ], - ]; - - $response->assertJson($expected); + ]); } /** @test */ public function a_single_subscriber_is_accessible_to_authorised_users() { + // given $user = $this->createUserWithWorkspace(); $subscriber = $this->createSubscriber($user); - $route = route('sendportal.api.subscribers.show', [ + // when + $response = $this->get(route('sendportal.api.subscribers.show', [ 'workspaceId' => $user->currentWorkspace()->id, 'subscriber' => $subscriber->id, 'api_token' => $user->api_token, - ]); - - $response = $this->get($route); + ])); + // then $response->assertStatus(200); - $expected = [ + $response->assertJson([ 'data' => Arr::only($subscriber->toArray(), ['first_name', 'last_name', 'email']), - ]; - - $response->assertJson($expected); + ]); } /** @test */ public function a_subscriber_can_be_created_by_authorised_users() { + // given $user = $this->createUserWithWorkspace(); + // when $route = route('sendportal.api.subscribers.store', $user->currentWorkspace()->id); $request = [ @@ -79,6 +80,7 @@ public function a_subscriber_can_be_created_by_authorised_users() $response = $this->post($route, array_merge($request, ['api_token' => $user->api_token])); + // then $response->assertStatus(201); $this->assertDatabaseHas('subscribers', $request); $response->assertJson(['data' => $request]); @@ -87,10 +89,12 @@ public function a_subscriber_can_be_created_by_authorised_users() /** @test */ public function a_subscriber_can_be_updated_by_authorised_users() { + // given $user = $this->createUserWithWorkspace(); $subscriber = $this->createSubscriber($user); + // when $route = route('sendportal.api.subscribers.update', [ 'workspaceId' => $user->currentWorkspace()->id, 'subscriber' => $subscriber->id, @@ -105,6 +109,7 @@ public function a_subscriber_can_be_updated_by_authorised_users() $response = $this->put($route, $request); + // then $response->assertStatus(200); $this->assertDatabaseMissing('subscribers', $subscriber->toArray()); $this->assertDatabaseHas('subscribers', $request); @@ -114,18 +119,19 @@ public function a_subscriber_can_be_updated_by_authorised_users() /** @test */ public function a_subscriber_can_be_deleted_by_authorised_users() { + // given $user = $this->createUserWithWorkspace(); $subscriber = $this->createSubscriber($user); - $route = route('sendportal.api.subscribers.destroy', [ + // when + $response = $this->delete(route('sendportal.api.subscribers.destroy', [ 'workspaceId' => $user->currentWorkspace()->id, 'subscriber' => $subscriber->id, 'api_token' => $user->api_token, - ]); - - $response = $this->delete($route); + ])); + // then $response->assertStatus(204); } @@ -140,7 +146,6 @@ public function a_subscriber_in_a_segment_can_be_deleted() $subscriber->segments()->attach($segment->id); // when - $this->withoutExceptionHandling(); $response = $this->delete(route('sendportal.api.subscribers.destroy', [ 'workspaceId' => $user->currentWorkspace()->id, 'subscriber' => $subscriber->id, @@ -154,4 +159,96 @@ public function a_subscriber_in_a_segment_can_be_deleted() 'subscriber_id' => $subscriber->id ]); } + + /** @test */ + public function the_store_endpoint_can_update_subscriber_based_on_email_address() + { + // given + $user = $this->createUserWithWorkspace(); + + $subscriber = $this->createSubscriber($user); + + // when + $route = route('sendportal.api.subscribers.store', $user->currentWorkspace()->id); + + $updateData = [ + 'first_name' => $this->faker->firstName, + 'last_name' => $this->faker->lastName, + 'email' => $subscriber->email, + ]; + + $response = $this->post($route, array_merge($updateData, ['api_token' => $user->api_token])); + + // then + $response->assertStatus(200); + + $this->assertDatabaseHas('subscribers', array_merge($updateData, ['id' => $subscriber->id])); + $this->assertDatabaseCount('subscribers', 1); + + $response->assertJson(['data' => $updateData]); + } + + /** @test */ + public function the_store_endpoint_allows_segments_to_be_added_with_the_subscriber() + { + // given + $user = $this->createUserWithWorkspace(); + + $segment = $this->createSegment($user); + + // when + $route = route('sendportal.api.subscribers.store', $user->currentWorkspace()->id); + + $request = [ + 'first_name' => $this->faker->firstName, + 'last_name' => $this->faker->lastName, + 'email' => $this->faker->email, + 'segments' => [$segment->id] + ]; + + $response = $this->post($route, array_merge($request, ['api_token' => $user->api_token])); + + // then + $response->assertStatus(201); + + $this->assertDatabaseHas('subscribers', ['email' => $request['email']]); + + $subscriber = Subscriber::with('segments')->where('email', $request['email'])->first(); + + self::assertContains($segment->id, $subscriber->segments->pluck('id')); + } + + /** @test */ + public function the_store_endpoint_allows_subscriber_segments_to_be_updated() + { + // given + $user = $this->createUserWithWorkspace(); + + $segment1 = $this->createSegment($user); + $segment2 = $this->createSegment($user); + + $subscriber = $this->createSubscriber($user); + $subscriber->segments()->save($segment1); + + // when + $route = route('sendportal.api.subscribers.store', $user->currentWorkspace()->id); + + $request = [ + 'first_name' => $this->faker->firstName, + 'last_name' => $this->faker->lastName, + 'email' => $subscriber->email, + 'segments' => [$segment2->id] + ]; + + $response = $this->post($route, array_merge($request, ['api_token' => $user->api_token])); + + // then + $response->assertStatus(200); + + $subscriber = $subscriber->fresh(); + $subscriber->load('segments'); + + self::assertContains($segment2->id, $subscriber->segments->pluck('id')); + self::assertNotContains($segment1->id, $subscriber->segments->pluck('id')); + } }