From d94336a2e5f63735c471ce816baf2a7bded645ff Mon Sep 17 00:00:00 2001 From: Tomas van Rijsse Date: Thu, 11 Dec 2025 10:16:07 +0100 Subject: [PATCH 1/2] add Collection postfix to requests --- generator/JsonApiPestTestGenerator.php | 8 ++++++++ generator/JsonApiRequestGenerator.php | 6 ++++++ generator/JsonApiResourceGenerator.php | 8 ++++++++ generator/generate.php | 2 +- ...getsRequest.php => GetBudgetsCollectionRequest.php} | 2 +- ...p => GetBudgetTimeSpentTotalsCollectionRequest.php} | 2 +- ...Request.php => GetBudgetTypesCollectionRequest.php} | 2 +- ...ngesRequest.php => GetChangesCollectionRequest.php} | 2 +- ...rsRequest.php => GetCustomersCollectionRequest.php} | 2 +- ...est.php => GetDailyProgressesCollectionRequest.php} | 2 +- ...riesRequest.php => GetEntriesCollectionRequest.php} | 2 +- ...st.php => GetEntrySuggestionsCollectionRequest.php} | 2 +- ....php => GetBudgetsExportMailsCollectionRequest.php} | 2 +- ...tsRequest.php => GetIncidentsCollectionRequest.php} | 2 +- .../{GetMesRequest.php => GetMesCollectionRequest.php} | 2 +- ...esRequest.php => GetOvertimesCollectionRequest.php} | 2 +- ...tTeamsRequest.php => GetTeamsCollectionRequest.php} | 2 +- ...est.php => GetTimeSpentTotalsCollectionRequest.php} | 2 +- ...tUsersRequest.php => GetUsersCollectionRequest.php} | 2 +- ...etUserCustomerHoursAggregatesCollectionRequest.php} | 2 +- tests/Factories/MockingWithFactoriesTest.php | 10 +++++----- tests/Requests/BudgetTest.php | 10 +++++----- tests/Requests/BudgetTimeSpentTotalTest.php | 10 +++++----- tests/Requests/BudgetTypeTest.php | 10 +++++----- tests/Requests/CustomerTest.php | 10 +++++----- tests/Requests/DailyProgressTest.php | 10 +++++----- tests/Requests/EntrySuggestionTest.php | 10 +++++----- tests/Requests/EntryTest.php | 10 +++++----- tests/Requests/OvertimeTest.php | 10 +++++----- tests/Requests/TeamTest.php | 10 +++++----- tests/Requests/TimeSpentTotalTest.php | 10 +++++----- tests/Requests/UserCustomerHoursAggregateTest.php | 10 +++++----- tests/Requests/UserTest.php | 10 +++++----- 33 files changed, 104 insertions(+), 82 deletions(-) rename src/Requests/Budget/{GetBudgetsRequest.php => GetBudgetsCollectionRequest.php} (92%) rename src/Requests/BudgetTimeSpentTotal/{GetBudgetTimeSpentTotalsRequest.php => GetBudgetTimeSpentTotalsCollectionRequest.php} (90%) rename src/Requests/BudgetType/{GetBudgetTypesRequest.php => GetBudgetTypesCollectionRequest.php} (90%) rename src/Requests/Change/{GetChangesRequest.php => GetChangesCollectionRequest.php} (83%) rename src/Requests/Customer/{GetCustomersRequest.php => GetCustomersCollectionRequest.php} (91%) rename src/Requests/DailyProgress/{GetDailyProgressesRequest.php => GetDailyProgressesCollectionRequest.php} (90%) rename src/Requests/Entry/{GetEntriesRequest.php => GetEntriesCollectionRequest.php} (92%) rename src/Requests/EntrySuggestion/{GetEntrySuggestionsRequest.php => GetEntrySuggestionsCollectionRequest.php} (90%) rename src/Requests/ExportMail/{GetBudgetsExportMailsRequest.php => GetBudgetsExportMailsCollectionRequest.php} (90%) rename src/Requests/Incident/{GetIncidentsRequest.php => GetIncidentsCollectionRequest.php} (82%) rename src/Requests/Me/{GetMesRequest.php => GetMesCollectionRequest.php} (83%) rename src/Requests/Overtime/{GetOvertimesRequest.php => GetOvertimesCollectionRequest.php} (91%) rename src/Requests/Team/{GetTeamsRequest.php => GetTeamsCollectionRequest.php} (90%) rename src/Requests/TimeSpentTotal/{GetTimeSpentTotalsRequest.php => GetTimeSpentTotalsCollectionRequest.php} (91%) rename src/Requests/User/{GetUsersRequest.php => GetUsersCollectionRequest.php} (91%) rename src/Requests/UserCustomerHoursAggregate/{GetUserCustomerHoursAggregatesRequest.php => GetUserCustomerHoursAggregatesCollectionRequest.php} (90%) diff --git a/generator/JsonApiPestTestGenerator.php b/generator/JsonApiPestTestGenerator.php index 077aa2a..39e403d 100644 --- a/generator/JsonApiPestTestGenerator.php +++ b/generator/JsonApiPestTestGenerator.php @@ -166,11 +166,19 @@ protected function getTestFunctionStubPath(Endpoint $endpoint): string /** * Add "Request" suffix to match JsonApiRequestGenerator behavior + * For collection requests, add "Collection" before "Request" */ protected function getRequestClassName(Endpoint $endpoint): string { + // Use inline collection detection (can't access JsonApiRequestGenerator's method) + $isCollection = $endpoint->method->isGet() && empty($endpoint->pathParameters); + $className = NameHelper::requestClassName($endpoint->name ?: NameHelper::pathBasedName($endpoint)); + if ($isCollection) { + $className .= 'Collection'; + } + if (! str_ends_with($className, 'Request')) { $className .= 'Request'; } diff --git a/generator/JsonApiRequestGenerator.php b/generator/JsonApiRequestGenerator.php index c18fe24..1f189bf 100644 --- a/generator/JsonApiRequestGenerator.php +++ b/generator/JsonApiRequestGenerator.php @@ -41,11 +41,17 @@ protected function shouldIncludeEndpoint(Endpoint $endpoint): bool /** * Hook: Add "Request" suffix to class names + * For collection requests, add "Collection" before "Request" */ protected function getRequestClassName(Endpoint $endpoint): string { $className = parent::getRequestClassName($endpoint); + // For collection requests, add "Collection" suffix + if ($this->isCollectionRequest($endpoint)) { + $className .= 'Collection'; + } + if (! str_ends_with($className, 'Request')) { $className .= 'Request'; } diff --git a/generator/JsonApiResourceGenerator.php b/generator/JsonApiResourceGenerator.php index e89ae70..438fadb 100644 --- a/generator/JsonApiResourceGenerator.php +++ b/generator/JsonApiResourceGenerator.php @@ -23,11 +23,19 @@ protected function shouldIncludeEndpoint(Endpoint $endpoint): bool /** * Hook: Add "Request" suffix to request class names + * For collection requests, add "Collection" before "Request" */ protected function getRequestClassName(Endpoint $endpoint): string { + // Use inline collection detection + $isCollection = $endpoint->method->isGet() && empty($endpoint->pathParameters); + $className = parent::getRequestClassName($endpoint); + if ($isCollection) { + $className .= 'Collection'; + } + if (! str_ends_with($className, 'Request')) { $className .= 'Request'; } diff --git a/generator/generate.php b/generator/generate.php index 81da13a..56aab1f 100644 --- a/generator/generate.php +++ b/generator/generate.php @@ -55,7 +55,7 @@ ); foreach ($files as $fileinfo) { - if (! $fileinfo->isFile() || $fileinfo->getExtension() === 'php') { + if (! $fileinfo->isFile() || $fileinfo->getExtension() !== 'php') { continue; } diff --git a/src/Requests/Budget/GetBudgetsRequest.php b/src/Requests/Budget/GetBudgetsCollectionRequest.php similarity index 92% rename from src/Requests/Budget/GetBudgetsRequest.php rename to src/Requests/Budget/GetBudgetsCollectionRequest.php index a08d3a7..685dbd6 100644 --- a/src/Requests/Budget/GetBudgetsRequest.php +++ b/src/Requests/Budget/GetBudgetsCollectionRequest.php @@ -15,7 +15,7 @@ /** * getBudgets */ -class GetBudgetsRequest extends Request implements Paginatable +class GetBudgetsCollectionRequest extends Request implements Paginatable { use HasFilters; diff --git a/src/Requests/BudgetTimeSpentTotal/GetBudgetTimeSpentTotalsRequest.php b/src/Requests/BudgetTimeSpentTotal/GetBudgetTimeSpentTotalsCollectionRequest.php similarity index 90% rename from src/Requests/BudgetTimeSpentTotal/GetBudgetTimeSpentTotalsRequest.php rename to src/Requests/BudgetTimeSpentTotal/GetBudgetTimeSpentTotalsCollectionRequest.php index b4d56ce..a31c76e 100644 --- a/src/Requests/BudgetTimeSpentTotal/GetBudgetTimeSpentTotalsRequest.php +++ b/src/Requests/BudgetTimeSpentTotal/GetBudgetTimeSpentTotalsCollectionRequest.php @@ -15,7 +15,7 @@ /** * getBudgetTimeSpentTotals */ -class GetBudgetTimeSpentTotalsRequest extends Request implements Paginatable +class GetBudgetTimeSpentTotalsCollectionRequest extends Request implements Paginatable { use HasFilters; diff --git a/src/Requests/BudgetType/GetBudgetTypesRequest.php b/src/Requests/BudgetType/GetBudgetTypesCollectionRequest.php similarity index 90% rename from src/Requests/BudgetType/GetBudgetTypesRequest.php rename to src/Requests/BudgetType/GetBudgetTypesCollectionRequest.php index bbaa904..3646018 100644 --- a/src/Requests/BudgetType/GetBudgetTypesRequest.php +++ b/src/Requests/BudgetType/GetBudgetTypesCollectionRequest.php @@ -14,7 +14,7 @@ /** * getBudgetTypes */ -class GetBudgetTypesRequest extends Request implements Paginatable +class GetBudgetTypesCollectionRequest extends Request implements Paginatable { protected $model = BudgetType::class; diff --git a/src/Requests/Change/GetChangesRequest.php b/src/Requests/Change/GetChangesCollectionRequest.php similarity index 83% rename from src/Requests/Change/GetChangesRequest.php rename to src/Requests/Change/GetChangesCollectionRequest.php index 407c19d..be5a0e6 100644 --- a/src/Requests/Change/GetChangesRequest.php +++ b/src/Requests/Change/GetChangesCollectionRequest.php @@ -11,7 +11,7 @@ /** * getChanges */ -class GetChangesRequest extends Request implements Paginatable +class GetChangesCollectionRequest extends Request implements Paginatable { protected Method $method = Method::GET; diff --git a/src/Requests/Customer/GetCustomersRequest.php b/src/Requests/Customer/GetCustomersCollectionRequest.php similarity index 91% rename from src/Requests/Customer/GetCustomersRequest.php rename to src/Requests/Customer/GetCustomersCollectionRequest.php index d472cbd..621b517 100644 --- a/src/Requests/Customer/GetCustomersRequest.php +++ b/src/Requests/Customer/GetCustomersCollectionRequest.php @@ -15,7 +15,7 @@ /** * getCustomers */ -class GetCustomersRequest extends Request implements Paginatable +class GetCustomersCollectionRequest extends Request implements Paginatable { use HasFilters; diff --git a/src/Requests/DailyProgress/GetDailyProgressesRequest.php b/src/Requests/DailyProgress/GetDailyProgressesCollectionRequest.php similarity index 90% rename from src/Requests/DailyProgress/GetDailyProgressesRequest.php rename to src/Requests/DailyProgress/GetDailyProgressesCollectionRequest.php index 3503173..209870b 100644 --- a/src/Requests/DailyProgress/GetDailyProgressesRequest.php +++ b/src/Requests/DailyProgress/GetDailyProgressesCollectionRequest.php @@ -14,7 +14,7 @@ /** * getDailyProgresses */ -class GetDailyProgressesRequest extends Request implements Paginatable +class GetDailyProgressesCollectionRequest extends Request implements Paginatable { protected $model = DailyProgress::class; diff --git a/src/Requests/Entry/GetEntriesRequest.php b/src/Requests/Entry/GetEntriesCollectionRequest.php similarity index 92% rename from src/Requests/Entry/GetEntriesRequest.php rename to src/Requests/Entry/GetEntriesCollectionRequest.php index 10f1c06..e4671b7 100644 --- a/src/Requests/Entry/GetEntriesRequest.php +++ b/src/Requests/Entry/GetEntriesCollectionRequest.php @@ -15,7 +15,7 @@ /** * getEntries */ -class GetEntriesRequest extends Request implements Paginatable +class GetEntriesCollectionRequest extends Request implements Paginatable { use HasFilters; diff --git a/src/Requests/EntrySuggestion/GetEntrySuggestionsRequest.php b/src/Requests/EntrySuggestion/GetEntrySuggestionsCollectionRequest.php similarity index 90% rename from src/Requests/EntrySuggestion/GetEntrySuggestionsRequest.php rename to src/Requests/EntrySuggestion/GetEntrySuggestionsCollectionRequest.php index 53db1dd..5bf1a74 100644 --- a/src/Requests/EntrySuggestion/GetEntrySuggestionsRequest.php +++ b/src/Requests/EntrySuggestion/GetEntrySuggestionsCollectionRequest.php @@ -15,7 +15,7 @@ /** * getEntrySuggestions */ -class GetEntrySuggestionsRequest extends Request implements Paginatable +class GetEntrySuggestionsCollectionRequest extends Request implements Paginatable { use HasFilters; diff --git a/src/Requests/ExportMail/GetBudgetsExportMailsRequest.php b/src/Requests/ExportMail/GetBudgetsExportMailsCollectionRequest.php similarity index 90% rename from src/Requests/ExportMail/GetBudgetsExportMailsRequest.php rename to src/Requests/ExportMail/GetBudgetsExportMailsCollectionRequest.php index 746f472..3565bbc 100644 --- a/src/Requests/ExportMail/GetBudgetsExportMailsRequest.php +++ b/src/Requests/ExportMail/GetBudgetsExportMailsCollectionRequest.php @@ -14,7 +14,7 @@ /** * getBudgetsExportMails */ -class GetBudgetsExportMailsRequest extends Request implements Paginatable +class GetBudgetsExportMailsCollectionRequest extends Request implements Paginatable { protected $model = ExportMail::class; diff --git a/src/Requests/Incident/GetIncidentsRequest.php b/src/Requests/Incident/GetIncidentsCollectionRequest.php similarity index 82% rename from src/Requests/Incident/GetIncidentsRequest.php rename to src/Requests/Incident/GetIncidentsCollectionRequest.php index 0eb3a09..3327690 100644 --- a/src/Requests/Incident/GetIncidentsRequest.php +++ b/src/Requests/Incident/GetIncidentsCollectionRequest.php @@ -11,7 +11,7 @@ /** * getIncidents */ -class GetIncidentsRequest extends Request implements Paginatable +class GetIncidentsCollectionRequest extends Request implements Paginatable { protected Method $method = Method::GET; diff --git a/src/Requests/Me/GetMesRequest.php b/src/Requests/Me/GetMesCollectionRequest.php similarity index 83% rename from src/Requests/Me/GetMesRequest.php rename to src/Requests/Me/GetMesCollectionRequest.php index a53715c..a8e1823 100644 --- a/src/Requests/Me/GetMesRequest.php +++ b/src/Requests/Me/GetMesCollectionRequest.php @@ -11,7 +11,7 @@ /** * getMes */ -class GetMesRequest extends Request implements Paginatable +class GetMesCollectionRequest extends Request implements Paginatable { protected Method $method = Method::GET; diff --git a/src/Requests/Overtime/GetOvertimesRequest.php b/src/Requests/Overtime/GetOvertimesCollectionRequest.php similarity index 91% rename from src/Requests/Overtime/GetOvertimesRequest.php rename to src/Requests/Overtime/GetOvertimesCollectionRequest.php index a5ae85d..4575e61 100644 --- a/src/Requests/Overtime/GetOvertimesRequest.php +++ b/src/Requests/Overtime/GetOvertimesCollectionRequest.php @@ -15,7 +15,7 @@ /** * getOvertimes */ -class GetOvertimesRequest extends Request implements Paginatable +class GetOvertimesCollectionRequest extends Request implements Paginatable { use HasFilters; diff --git a/src/Requests/Team/GetTeamsRequest.php b/src/Requests/Team/GetTeamsCollectionRequest.php similarity index 90% rename from src/Requests/Team/GetTeamsRequest.php rename to src/Requests/Team/GetTeamsCollectionRequest.php index 80abe50..fd9b901 100644 --- a/src/Requests/Team/GetTeamsRequest.php +++ b/src/Requests/Team/GetTeamsCollectionRequest.php @@ -14,7 +14,7 @@ /** * getTeams */ -class GetTeamsRequest extends Request implements Paginatable +class GetTeamsCollectionRequest extends Request implements Paginatable { protected $model = Team::class; diff --git a/src/Requests/TimeSpentTotal/GetTimeSpentTotalsRequest.php b/src/Requests/TimeSpentTotal/GetTimeSpentTotalsCollectionRequest.php similarity index 91% rename from src/Requests/TimeSpentTotal/GetTimeSpentTotalsRequest.php rename to src/Requests/TimeSpentTotal/GetTimeSpentTotalsCollectionRequest.php index 0a003c8..03fb0fc 100644 --- a/src/Requests/TimeSpentTotal/GetTimeSpentTotalsRequest.php +++ b/src/Requests/TimeSpentTotal/GetTimeSpentTotalsCollectionRequest.php @@ -15,7 +15,7 @@ /** * getTimeSpentTotals */ -class GetTimeSpentTotalsRequest extends Request implements Paginatable +class GetTimeSpentTotalsCollectionRequest extends Request implements Paginatable { use HasFilters; diff --git a/src/Requests/User/GetUsersRequest.php b/src/Requests/User/GetUsersCollectionRequest.php similarity index 91% rename from src/Requests/User/GetUsersRequest.php rename to src/Requests/User/GetUsersCollectionRequest.php index aa03e93..f0d3291 100644 --- a/src/Requests/User/GetUsersRequest.php +++ b/src/Requests/User/GetUsersCollectionRequest.php @@ -15,7 +15,7 @@ /** * getUsers */ -class GetUsersRequest extends Request implements Paginatable +class GetUsersCollectionRequest extends Request implements Paginatable { use HasFilters; diff --git a/src/Requests/UserCustomerHoursAggregate/GetUserCustomerHoursAggregatesRequest.php b/src/Requests/UserCustomerHoursAggregate/GetUserCustomerHoursAggregatesCollectionRequest.php similarity index 90% rename from src/Requests/UserCustomerHoursAggregate/GetUserCustomerHoursAggregatesRequest.php rename to src/Requests/UserCustomerHoursAggregate/GetUserCustomerHoursAggregatesCollectionRequest.php index f75df89..e5e89d8 100644 --- a/src/Requests/UserCustomerHoursAggregate/GetUserCustomerHoursAggregatesRequest.php +++ b/src/Requests/UserCustomerHoursAggregate/GetUserCustomerHoursAggregatesCollectionRequest.php @@ -15,7 +15,7 @@ /** * getUserCustomerHoursAggregates */ -class GetUserCustomerHoursAggregatesRequest extends Request implements Paginatable +class GetUserCustomerHoursAggregatesCollectionRequest extends Request implements Paginatable { use HasFilters; diff --git a/tests/Factories/MockingWithFactoriesTest.php b/tests/Factories/MockingWithFactoriesTest.php index 953545c..6adac82 100644 --- a/tests/Factories/MockingWithFactoriesTest.php +++ b/tests/Factories/MockingWithFactoriesTest.php @@ -3,7 +3,7 @@ use Saloon\Http\Faking\MockClient; use Saloon\Http\Faking\MockResponse; use Timatic\Dto\Budget; -use Timatic\Requests\Budget\GetBudgetsRequest; +use Timatic\Requests\Budget\GetBudgetsCollectionRequest; use Timatic\Requests\Budget\PostBudgetsRequest; use Timatic\TimaticConnector; @@ -14,7 +14,7 @@ ])->make(); $mockClient = new MockClient([ - GetBudgetsRequest::class => MockResponse::make([ + GetBudgetsCollectionRequest::class => MockResponse::make([ 'data' => [$budget->toJsonApi()], ], 200), ]); @@ -22,7 +22,7 @@ $connector = new TimaticConnector; $connector->withMockClient($mockClient); - $response = $connector->send(new GetBudgetsRequest); + $response = $connector->send(new GetBudgetsCollectionRequest); $dtos = $response->dto(); expect($dtos)->toBeInstanceOf(\Illuminate\Support\Collection::class); @@ -37,7 +37,7 @@ $budgets = Budget::factory()->withId()->count(3)->make(); $mockClient = new MockClient([ - GetBudgetsRequest::class => MockResponse::make([ + GetBudgetsCollectionRequest::class => MockResponse::make([ 'data' => $budgets->map(fn ($budget) => $budget->toJsonApi())->toArray(), ], 200), ]); @@ -45,7 +45,7 @@ $connector = new TimaticConnector; $connector->withMockClient($mockClient); - $response = $connector->send(new GetBudgetsRequest); + $response = $connector->send(new GetBudgetsCollectionRequest); $dtos = $response->dto(); expect($dtos)->toHaveCount(3); diff --git a/tests/Requests/BudgetTest.php b/tests/Requests/BudgetTest.php index 4bb2b22..49447d3 100644 --- a/tests/Requests/BudgetTest.php +++ b/tests/Requests/BudgetTest.php @@ -8,7 +8,7 @@ use Saloon\Laravel\Facades\Saloon; use Timatic\Requests\Budget\DeleteBudgetRequest; use Timatic\Requests\Budget\GetBudgetRequest; -use Timatic\Requests\Budget\GetBudgetsRequest; +use Timatic\Requests\Budget\GetBudgetsCollectionRequest; use Timatic\Requests\Budget\PatchBudgetRequest; use Timatic\Requests\Budget\PostBudgetsRequest; @@ -16,9 +16,9 @@ $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getBudgets method in the Budget resource', function () { +it('calls the getBudgetsCollection method in the Budget resource', function () { Saloon::fake([ - GetBudgetsRequest::class => MockResponse::make([ + GetBudgetsCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'budgets', @@ -64,14 +64,14 @@ ], 200), ]); - $request = (new GetBudgetsRequest(include: 'test string')) + $request = (new GetBudgetsCollectionRequest(include: 'test string')) ->filter('customerId', 'customer_id-123') ->filter('budgetTypeId', 'budget_type_id-123') ->filter('isArchived', true); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetBudgetsRequest::class); + Saloon::assertSent(GetBudgetsCollectionRequest::class); // Verify filter query parameters are present Saloon::assertSent(function (Request $request) { diff --git a/tests/Requests/BudgetTimeSpentTotalTest.php b/tests/Requests/BudgetTimeSpentTotalTest.php index e94b8d4..f43ee9f 100644 --- a/tests/Requests/BudgetTimeSpentTotalTest.php +++ b/tests/Requests/BudgetTimeSpentTotalTest.php @@ -6,15 +6,15 @@ use Saloon\Http\Faking\MockResponse; use Saloon\Http\Request; use Saloon\Laravel\Facades\Saloon; -use Timatic\Requests\BudgetTimeSpentTotal\GetBudgetTimeSpentTotalsRequest; +use Timatic\Requests\BudgetTimeSpentTotal\GetBudgetTimeSpentTotalsCollectionRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getBudgetTimeSpentTotals method in the BudgetTimeSpentTotal resource', function () { +it('calls the getBudgetTimeSpentTotalsCollection method in the BudgetTimeSpentTotal resource', function () { Saloon::fake([ - GetBudgetTimeSpentTotalsRequest::class => MockResponse::make([ + GetBudgetTimeSpentTotalsCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'budgetTimeSpentTotals', @@ -42,12 +42,12 @@ ], 200), ]); - $request = (new GetBudgetTimeSpentTotalsRequest) + $request = (new GetBudgetTimeSpentTotalsCollectionRequest) ->filter('budgetId', 'budget_id-123'); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetBudgetTimeSpentTotalsRequest::class); + Saloon::assertSent(GetBudgetTimeSpentTotalsCollectionRequest::class); // Verify filter query parameters are present Saloon::assertSent(function (Request $request) { diff --git a/tests/Requests/BudgetTypeTest.php b/tests/Requests/BudgetTypeTest.php index 244249b..96292a6 100644 --- a/tests/Requests/BudgetTypeTest.php +++ b/tests/Requests/BudgetTypeTest.php @@ -4,15 +4,15 @@ use Saloon\Http\Faking\MockResponse; use Saloon\Laravel\Facades\Saloon; -use Timatic\Requests\BudgetType\GetBudgetTypesRequest; +use Timatic\Requests\BudgetType\GetBudgetTypesCollectionRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getBudgetTypes method in the BudgetType resource', function () { +it('calls the getBudgetTypesCollection method in the BudgetType resource', function () { Saloon::fake([ - GetBudgetTypesRequest::class => MockResponse::make([ + GetBudgetTypesCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'budgetTypes', @@ -48,11 +48,11 @@ ], 200), ]); - $request = (new GetBudgetTypesRequest); + $request = (new GetBudgetTypesCollectionRequest); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetBudgetTypesRequest::class); + Saloon::assertSent(GetBudgetTypesCollectionRequest::class); expect($response->status())->toBe(200); diff --git a/tests/Requests/CustomerTest.php b/tests/Requests/CustomerTest.php index b23aba8..0c851cb 100644 --- a/tests/Requests/CustomerTest.php +++ b/tests/Requests/CustomerTest.php @@ -7,7 +7,7 @@ use Saloon\Laravel\Facades\Saloon; use Timatic\Requests\Customer\DeleteCustomerRequest; use Timatic\Requests\Customer\GetCustomerRequest; -use Timatic\Requests\Customer\GetCustomersRequest; +use Timatic\Requests\Customer\GetCustomersCollectionRequest; use Timatic\Requests\Customer\PatchCustomerRequest; use Timatic\Requests\Customer\PostCustomersRequest; @@ -15,9 +15,9 @@ $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getCustomers method in the Customer resource', function () { +it('calls the getCustomersCollection method in the Customer resource', function () { Saloon::fake([ - GetCustomersRequest::class => MockResponse::make([ + GetCustomersCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'customers', @@ -43,12 +43,12 @@ ], 200), ]); - $request = (new GetCustomersRequest) + $request = (new GetCustomersCollectionRequest) ->filter('externalId', 'external_id-123'); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetCustomersRequest::class); + Saloon::assertSent(GetCustomersCollectionRequest::class); // Verify filter query parameters are present Saloon::assertSent(function (Request $request) { diff --git a/tests/Requests/DailyProgressTest.php b/tests/Requests/DailyProgressTest.php index 3d515bd..6ea676e 100644 --- a/tests/Requests/DailyProgressTest.php +++ b/tests/Requests/DailyProgressTest.php @@ -5,15 +5,15 @@ use Carbon\Carbon; use Saloon\Http\Faking\MockResponse; use Saloon\Laravel\Facades\Saloon; -use Timatic\Requests\DailyProgress\GetDailyProgressesRequest; +use Timatic\Requests\DailyProgress\GetDailyProgressesCollectionRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getDailyProgresses method in the DailyProgress resource', function () { +it('calls the getDailyProgressesCollection method in the DailyProgress resource', function () { Saloon::fake([ - GetDailyProgressesRequest::class => MockResponse::make([ + GetDailyProgressesCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'dailyProgresses', @@ -37,11 +37,11 @@ ], 200), ]); - $request = (new GetDailyProgressesRequest); + $request = (new GetDailyProgressesCollectionRequest); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetDailyProgressesRequest::class); + Saloon::assertSent(GetDailyProgressesCollectionRequest::class); expect($response->status())->toBe(200); diff --git a/tests/Requests/EntrySuggestionTest.php b/tests/Requests/EntrySuggestionTest.php index 4741297..a11e1c9 100644 --- a/tests/Requests/EntrySuggestionTest.php +++ b/tests/Requests/EntrySuggestionTest.php @@ -7,15 +7,15 @@ use Saloon\Laravel\Facades\Saloon; use Timatic\Requests\EntrySuggestion\DeleteEntrySuggestionRequest; use Timatic\Requests\EntrySuggestion\GetEntrySuggestionRequest; -use Timatic\Requests\EntrySuggestion\GetEntrySuggestionsRequest; +use Timatic\Requests\EntrySuggestion\GetEntrySuggestionsCollectionRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getEntrySuggestions method in the EntrySuggestion resource', function () { +it('calls the getEntrySuggestionsCollection method in the EntrySuggestion resource', function () { Saloon::fake([ - GetEntrySuggestionsRequest::class => MockResponse::make([ + GetEntrySuggestionsCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'entrySuggestions', @@ -49,12 +49,12 @@ ], 200), ]); - $request = (new GetEntrySuggestionsRequest) + $request = (new GetEntrySuggestionsCollectionRequest) ->filter('date', 'test value'); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetEntrySuggestionsRequest::class); + Saloon::assertSent(GetEntrySuggestionsCollectionRequest::class); // Verify filter query parameters are present Saloon::assertSent(function (Request $request) { diff --git a/tests/Requests/EntryTest.php b/tests/Requests/EntryTest.php index 2a29ada..e4f0b58 100644 --- a/tests/Requests/EntryTest.php +++ b/tests/Requests/EntryTest.php @@ -7,7 +7,7 @@ use Saloon\Http\Request; use Saloon\Laravel\Facades\Saloon; use Timatic\Requests\Entry\DeleteEntryRequest; -use Timatic\Requests\Entry\GetEntriesRequest; +use Timatic\Requests\Entry\GetEntriesCollectionRequest; use Timatic\Requests\Entry\GetEntryRequest; use Timatic\Requests\Entry\PatchEntryRequest; use Timatic\Requests\Entry\PostEntriesRequest; @@ -16,9 +16,9 @@ $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getEntries method in the Entry resource', function () { +it('calls the getEntriesCollection method in the Entry resource', function () { Saloon::fake([ - GetEntriesRequest::class => MockResponse::make([ + GetEntriesCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'entries', @@ -86,14 +86,14 @@ ], 200), ]); - $request = (new GetEntriesRequest(include: 'test string')) + $request = (new GetEntriesCollectionRequest(include: 'test string')) ->filter('userId', 'user_id-123') ->filter('budgetId', 'budget_id-123') ->filter('startedAt', '2025-01-15T10:30:00Z'); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetEntriesRequest::class); + Saloon::assertSent(GetEntriesCollectionRequest::class); // Verify filter query parameters are present Saloon::assertSent(function (Request $request) { diff --git a/tests/Requests/OvertimeTest.php b/tests/Requests/OvertimeTest.php index 30a6482..ed7f7a2 100644 --- a/tests/Requests/OvertimeTest.php +++ b/tests/Requests/OvertimeTest.php @@ -6,15 +6,15 @@ use Saloon\Http\Faking\MockResponse; use Saloon\Http\Request; use Saloon\Laravel\Facades\Saloon; -use Timatic\Requests\Overtime\GetOvertimesRequest; +use Timatic\Requests\Overtime\GetOvertimesCollectionRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getOvertimes method in the Overtime resource', function () { +it('calls the getOvertimesCollection method in the Overtime resource', function () { Saloon::fake([ - GetOvertimesRequest::class => MockResponse::make([ + GetOvertimesCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'overtimes', @@ -48,14 +48,14 @@ ], 200), ]); - $request = (new GetOvertimesRequest) + $request = (new GetOvertimesCollectionRequest) ->filter('startedAt', '2025-01-15T10:30:00Z') ->filter('endedAt', '2025-01-15T10:30:00Z') ->filter('isApproved', true); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetOvertimesRequest::class); + Saloon::assertSent(GetOvertimesCollectionRequest::class); // Verify filter query parameters are present Saloon::assertSent(function (Request $request) { diff --git a/tests/Requests/TeamTest.php b/tests/Requests/TeamTest.php index 708ca34..2fe02aa 100644 --- a/tests/Requests/TeamTest.php +++ b/tests/Requests/TeamTest.php @@ -7,7 +7,7 @@ use Saloon\Laravel\Facades\Saloon; use Timatic\Requests\Team\DeleteTeamRequest; use Timatic\Requests\Team\GetTeamRequest; -use Timatic\Requests\Team\GetTeamsRequest; +use Timatic\Requests\Team\GetTeamsCollectionRequest; use Timatic\Requests\Team\PatchTeamRequest; use Timatic\Requests\Team\PostTeamsRequest; @@ -15,9 +15,9 @@ $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getTeams method in the Team resource', function () { +it('calls the getTeamsCollection method in the Team resource', function () { Saloon::fake([ - GetTeamsRequest::class => MockResponse::make([ + GetTeamsCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'teams', @@ -39,11 +39,11 @@ ], 200), ]); - $request = (new GetTeamsRequest); + $request = (new GetTeamsCollectionRequest); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetTeamsRequest::class); + Saloon::assertSent(GetTeamsCollectionRequest::class); expect($response->status())->toBe(200); diff --git a/tests/Requests/TimeSpentTotalTest.php b/tests/Requests/TimeSpentTotalTest.php index 305c152..497ddbc 100644 --- a/tests/Requests/TimeSpentTotalTest.php +++ b/tests/Requests/TimeSpentTotalTest.php @@ -6,15 +6,15 @@ use Saloon\Http\Faking\MockResponse; use Saloon\Http\Request; use Saloon\Laravel\Facades\Saloon; -use Timatic\Requests\TimeSpentTotal\GetTimeSpentTotalsRequest; +use Timatic\Requests\TimeSpentTotal\GetTimeSpentTotalsCollectionRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getTimeSpentTotals method in the TimeSpentTotal resource', function () { +it('calls the getTimeSpentTotalsCollection method in the TimeSpentTotal resource', function () { Saloon::fake([ - GetTimeSpentTotalsRequest::class => MockResponse::make([ + GetTimeSpentTotalsCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'timeSpentTotals', @@ -44,13 +44,13 @@ ], 200), ]); - $request = (new GetTimeSpentTotalsRequest) + $request = (new GetTimeSpentTotalsCollectionRequest) ->filter('teamId', 'team_id-123') ->filter('userId', 'user_id-123'); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetTimeSpentTotalsRequest::class); + Saloon::assertSent(GetTimeSpentTotalsCollectionRequest::class); // Verify filter query parameters are present Saloon::assertSent(function (Request $request) { diff --git a/tests/Requests/UserCustomerHoursAggregateTest.php b/tests/Requests/UserCustomerHoursAggregateTest.php index 368bf61..4aa38da 100644 --- a/tests/Requests/UserCustomerHoursAggregateTest.php +++ b/tests/Requests/UserCustomerHoursAggregateTest.php @@ -5,15 +5,15 @@ use Saloon\Http\Faking\MockResponse; use Saloon\Http\Request; use Saloon\Laravel\Facades\Saloon; -use Timatic\Requests\UserCustomerHoursAggregate\GetUserCustomerHoursAggregatesRequest; +use Timatic\Requests\UserCustomerHoursAggregate\GetUserCustomerHoursAggregatesCollectionRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getUserCustomerHoursAggregates method in the UserCustomerHoursAggregate resource', function () { +it('calls the getUserCustomerHoursAggregatesCollection method in the UserCustomerHoursAggregate resource', function () { Saloon::fake([ - GetUserCustomerHoursAggregatesRequest::class => MockResponse::make([ + GetUserCustomerHoursAggregatesCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'userCustomerHoursAggregates', @@ -41,14 +41,14 @@ ], 200), ]); - $request = (new GetUserCustomerHoursAggregatesRequest) + $request = (new GetUserCustomerHoursAggregatesCollectionRequest) ->filter('startedAt', '2025-01-15T10:30:00Z') ->filter('endedAt', '2025-01-15T10:30:00Z') ->filter('teamId', 'team_id-123'); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetUserCustomerHoursAggregatesRequest::class); + Saloon::assertSent(GetUserCustomerHoursAggregatesCollectionRequest::class); // Verify filter query parameters are present Saloon::assertSent(function (Request $request) { diff --git a/tests/Requests/UserTest.php b/tests/Requests/UserTest.php index 6fdea90..4edda58 100644 --- a/tests/Requests/UserTest.php +++ b/tests/Requests/UserTest.php @@ -7,7 +7,7 @@ use Saloon\Laravel\Facades\Saloon; use Timatic\Requests\User\DeleteUserRequest; use Timatic\Requests\User\GetUserRequest; -use Timatic\Requests\User\GetUsersRequest; +use Timatic\Requests\User\GetUsersCollectionRequest; use Timatic\Requests\User\PatchUserRequest; use Timatic\Requests\User\PostUsersRequest; @@ -15,9 +15,9 @@ $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getUsers method in the User resource', function () { +it('calls the getUsersCollection method in the User resource', function () { Saloon::fake([ - GetUsersRequest::class => MockResponse::make([ + GetUsersCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'users', @@ -39,12 +39,12 @@ ], 200), ]); - $request = (new GetUsersRequest) + $request = (new GetUsersCollectionRequest) ->filter('externalId', 'external_id-123'); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetUsersRequest::class); + Saloon::assertSent(GetUsersCollectionRequest::class); // Verify filter query parameters are present Saloon::assertSent(function (Request $request) { From 6ecb2f14dad5008811d4db9154032f76a7456299 Mon Sep 17 00:00:00 2001 From: Tomas van Rijsse Date: Mon, 15 Dec 2025 22:23:07 +0100 Subject: [PATCH 2/2] feat: get all items as DTO's --- README.md | 75 +++++----- src/Pagination/JsonApiPaginator.php | 9 +- tests/Pagination/JsonApiPaginatorTest.php | 164 ++++++++++++++++++++++ 3 files changed, 204 insertions(+), 44 deletions(-) create mode 100644 tests/Pagination/JsonApiPaginatorTest.php diff --git a/README.md b/README.md index 78ded13..8383ad6 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,12 @@ TIMATIC_API_TOKEN=your-api-token-here The SDK connector is automatically registered in Laravel's service container, making it easy to inject into your controllers, commands, and other classes: ```php +use Timatic\Requests\Budget\GetBudgetsCollectionRequest; +use Timatic\Requests\Budget\GetBudgetRequest; +use Timatic\Requests\Budget\PostBudgetsRequest; +use Timatic\Requests\BudgetType\GetBudgetTypesCollectionRequest; + use Timatic\TimaticConnector; -use Timatic\Requests\BudgetType\GetBudgetTypeCollection; class BudgetController extends Controller { @@ -53,15 +57,20 @@ class BudgetController extends Controller public function index() { - // Using resource methods - $budgets = $this->timatic->budget()->getBudgets()->dto(); - - // Using direct send() with dtoOrFail() for automatic DTO conversion + // fetch one or more items, limited by the default page size from the api $budgetTypes = $this->timatic - ->send(new GetBudgetTypeCollection()) - ->dtoOrFail(); - - return view('budgets.index', compact('budgets', 'budgetTypes')); + ->send(new GetBudgetTypesCollectionRequest()) + ->dto(); + + $defaultBudget = $this->timatic->send( + new \Timatic\Requests\Budget\GetBudgetRequest(id: '1337') + )->dtoOrFail(); + + // fetch all DTO's + $budgets = $this->timatic->paginate(new GetBudgetsCollectionRequest()) + ->dtoCollection(); + + return view('budgets.index', compact('budgets', 'budgetTypes', 'defaultBudget')); } public function store(Request $request) @@ -72,7 +81,7 @@ class BudgetController extends Controller ]); $created = $this->timatic - ->send(new \Timatic\Requests\Budget\PostBudgets($budget)) + ->send(new PostBudgetsRequest($budget)) ->dtoOrFail(); return redirect()->route('budgets.show', $created->id); @@ -83,14 +92,17 @@ class BudgetController extends Controller **In Console Commands:** ```php +use \Timatic\Requests\Budget\GetBudgetsCollectionRequest; use Timatic\TimaticConnector; class SyncBudgetsCommand extends Command { public function handle(TimaticConnector $timatic): int { - $budgets = $timatic->budget()->getBudgets()->dto(); - + $budgets = $timatic->paginate( + new GetBudgetsCollectionRequest() + )->dtoCollection() + foreach ($budgets as $budget) { // Process budgets } @@ -111,6 +123,7 @@ use Timatic\TimaticConnector; use Timatic\Dto\Budget; use Timatic\Dto\BudgetType; use Timatic\Requests\Budget\GetBudgetsRequest; +use Timatic\Requests\Budget\PostBudgetsRequest; use Timatic\Requests\BudgetType\GetBudgetTypesRequest; use Saloon\Http\Faking\MockClient; use Saloon\Http\Faking\MockResponse; @@ -121,7 +134,7 @@ test('it displays budgets and budget types', function () { $budgetType = BudgetType::factory()->state(['id' => '1'])->make(); // Create mock responses using factory-generated data - $mockClient = new MockClient([ + $mockClient = MockClient::global([ GetBudgetsRequest::class => MockResponse::make([ 'data' => [$budget->toJsonApi()], ], 200), @@ -130,11 +143,6 @@ test('it displays budgets and budget types', function () { ], 200), ]); - // Bind mock to container - $connector = new TimaticConnector(); - $connector->withMockClient($mockClient); - $this->app->instance(TimaticConnector::class, $connector); - // Make request $response = $this->get(route('budgets.index')); @@ -152,16 +160,12 @@ test('it creates a new budget', function () { 'totalPrice' => '5000.00', ])->make(); - $mockClient = new MockClient([ + $mockClient = MockClient::global([ PostBudgetsRequest::class => MockResponse::make([ 'data' => $budget->toJsonApi(), ], 201), ]); - $connector = new TimaticConnector(); - $connector->withMockClient($mockClient); - $this->app->instance(TimaticConnector::class, $connector); - $response = $this->post(route('budgets.store'), [ 'title' => 'New Budget', 'total_price' => 5000.00, @@ -184,35 +188,22 @@ test('it sends a POST request to create a budget using the SDK', function () { 'customerId' => 'customer-123', ])->make(); - $mockClient = new MockClient([ + $mockClient = MockClient::global([ PostBudgetsRequest::class => MockResponse::make([ 'data' => $createdBudget->toJsonApi(), ], 201), ]); - $connector = new TimaticConnector(); - $connector->withMockClient($mockClient); - - $response = $connector->send(new PostBudgetsRequest($budgetToCreate)); + artisan('sync:budgets')->assertOk(); // Assert the request body was sent correctly - $mockClient->assertSent(function (\Saloon\Http\Request $request) { + $mockClient->assertSent(function (PostBudgetsRequest $request) { $body = $request->body()->all(); return $body['data']['attributes']['title'] === 'New Budget' && $body['data']['attributes']['totalPrice'] === '5000.00' && $body['data']['attributes']['customerId'] === 'customer-123'; }); - - // Assert response - expect($response->status())->toBe(201); - - $dto = $response->dto(); - expect($dto) - ->toBeInstanceOf(Budget::class) - ->id->toBe('created-456') - ->title->toBe('New Budget') - ->totalPrice->toBe('5000.00'); }); ``` @@ -221,7 +212,7 @@ test('it sends a POST request to create a budget using the SDK', function () { Every DTO in the SDK has a corresponding factory class with the following methods: ```php -// Create a single model with random data +// Create a single model with random data, without an ID $budget = Budget::factory()->make(); // Create multiple models with unique UUID IDs @@ -267,7 +258,7 @@ class BudgetController extends Controller } // Or collect all items at once - $allBudgets = $paginator->collect(); + $allBudgets = $paginator->dtoCollection(); } } ``` @@ -282,7 +273,7 @@ The paginator: All responses are instances of `TimaticResponse` which extends Saloon's Response with JSON:API convenience methods: ```php -$response = $timatic->budget()->getBudgets(); +$response = $timatic->send(new GetBudgetsCollectionRequest()); // Get the first item from a collection $firstBudget = $response->firstItem(); diff --git a/src/Pagination/JsonApiPaginator.php b/src/Pagination/JsonApiPaginator.php index 6abd8ab..d5b1442 100644 --- a/src/Pagination/JsonApiPaginator.php +++ b/src/Pagination/JsonApiPaginator.php @@ -4,12 +4,18 @@ namespace Timatic\Pagination; +use Illuminate\Support\Collection; use Saloon\Http\Request; use Saloon\Http\Response; use Saloon\PaginationPlugin\Paginator; class JsonApiPaginator extends Paginator { + public function dtoCollection(): Collection + { + return parent::collect()->collect(); + } + protected function isLastPage(Response $response): bool { return $response->json('links.next') === null; @@ -17,8 +23,7 @@ protected function isLastPage(Response $response): bool protected function getPageItems(Response $response, Request $request): array { - // Return the 'data' array from JSON:API response - return $response->json('data', []); + return $response->dto()->toArray(); } protected function applyPagination(Request $request): Request diff --git a/tests/Pagination/JsonApiPaginatorTest.php b/tests/Pagination/JsonApiPaginatorTest.php new file mode 100644 index 0000000..00347b0 --- /dev/null +++ b/tests/Pagination/JsonApiPaginatorTest.php @@ -0,0 +1,164 @@ +timaticConnector = new TimaticConnector; +}); + +it('returns DTOs instead of raw JSON:API fields when paginating', function () { + // Mock the first page response with pagination links + Saloon::fake([ + GetEntriesCollectionRequest::class => MockResponse::make([ + 'data' => [ + [ + 'type' => 'entries', + 'id' => 'entry-1', + 'attributes' => [ + 'ticketId' => 'ticket-123', + 'ticketNumber' => 'TICKET-001', + 'ticketTitle' => 'First Entry', + 'description' => 'First entry description', + 'minutesSpent' => 60, + 'startedAt' => '2025-12-15T10:00:00.000Z', + 'endedAt' => '2025-12-15T11:00:00.000Z', + ], + ], + [ + 'type' => 'entries', + 'id' => 'entry-2', + 'attributes' => [ + 'ticketId' => 'ticket-456', + 'ticketNumber' => 'TICKET-002', + 'ticketTitle' => 'Second Entry', + 'description' => 'Second entry description', + 'minutesSpent' => 120, + 'startedAt' => '2025-12-15T12:00:00.000Z', + 'endedAt' => '2025-12-15T14:00:00.000Z', + ], + ], + ], + 'links' => [ + 'next' => null, + ], + ], 200), + ]); + + $request = new GetEntriesCollectionRequest; + $paginator = $this->timaticConnector->paginate($request); + + // Get items from first page (convert Generator to array) + $items = $paginator->dtoCollection(); + + expect($items)->toHaveCount(2); + + // Verify first item is a proper Entry DTO with hydrated properties + expect($items->first()) + ->toBeInstanceOf(Entry::class) + ->ticketId->toBe('ticket-123') + ->ticketNumber->toBe('TICKET-001') + ->ticketTitle->toBe('First Entry') + ->description->toBe('First entry description') + ->minutesSpent->toBe(60); + + // Verify second item is also a proper Entry DTO + expect($items[1]) + ->toBeInstanceOf(Entry::class) + ->ticketId->toBe('ticket-456') + ->ticketNumber->toBe('TICKET-002') + ->ticketTitle->toBe('Second Entry') + ->description->toBe('Second entry description') + ->minutesSpent->toBe(120); +}); + +it('correctly follows pagination using links.next URL', function () { + // Mock multiple pages with proper pagination links using sequence + Saloon::fake([ + + // First page + MockResponse::make([ + 'data' => [ + [ + 'type' => 'entries', + 'id' => 'entry-1', + 'attributes' => [ + 'ticketNumber' => 'PAGE-1-ITEM-1', + 'minutesSpent' => 10, + ], + ], + [ + 'type' => 'entries', + 'id' => 'entry-2', + 'attributes' => [ + 'ticketNumber' => 'PAGE-1-ITEM-2', + 'minutesSpent' => 20, + ], + ], + ], + 'links' => [ + 'next' => 'https://api.example.com/entries?page[number]=2', + ], + ], 200), + // Last page (last page, no next link) + MockResponse::make([ + 'data' => [ + [ + 'type' => 'entries', + 'id' => 'entry-3', + 'attributes' => [ + 'ticketNumber' => 'PAGE-2-ITEM-1', + 'minutesSpent' => 50, + ], + ], + ], + 'links' => [ + 'next' => null, + ], + ], 200), + + ]); + + $request = new GetEntriesCollectionRequest; + $paginator = $this->timaticConnector->paginate($request); + + // Collect all items across all pages using items() method + $allItems = $paginator->dtoCollection(); + + // Verify we got all items from all 3 pages + expect($allItems)->toHaveCount(3); + + // Verify items from each page + expect($allItems[0]->ticketNumber)->toBe('PAGE-1-ITEM-1'); + expect($allItems[1]->ticketNumber)->toBe('PAGE-1-ITEM-2'); + expect($allItems[2]->ticketNumber)->toBe('PAGE-2-ITEM-1'); +}); + +it('applies pagination query parameters correctly', function () { + Saloon::fake([ + GetEntriesCollectionRequest::class => MockResponse::make([ + 'data' => [], + 'links' => ['next' => null], + ], 200), + ]); + + $paginator = $this->timaticConnector + ->paginate(new GetEntriesCollectionRequest) + ->setPerPageLimit(123); + + $paginator->dtoCollection(); + + // Verify the request had the correct query parameters + Saloon::assertSent(function (GetEntriesCollectionRequest $request) { + $query = $request->query()->all(); + + expect($query)->toHaveKey('page[number]', 1); + expect($query)->toHaveKey('page[size]', 123); + + return true; + }); +});