diff --git a/README.md b/README.md index 629472c..78ded13 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,8 @@ 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\SDK\TimaticConnector; -use Timatic\SDK\Requests\BudgetType\GetBudgetTypeCollection; +use Timatic\TimaticConnector; +use Timatic\Requests\BudgetType\GetBudgetTypeCollection; class BudgetController extends Controller { @@ -66,13 +66,13 @@ class BudgetController extends Controller public function store(Request $request) { - $budget = new \Timatic\SDK\Dto\Budget([ + $budget = new \Timatic\Dto\Budget([ 'title' => $request->input('title'), 'totalPrice' => $request->input('total_price'), ]); $created = $this->timatic - ->send(new \Timatic\SDK\Requests\Budget\PostBudgets($budget)) + ->send(new \Timatic\Requests\Budget\PostBudgets($budget)) ->dtoOrFail(); return redirect()->route('budgets.show', $created->id); @@ -83,7 +83,7 @@ class BudgetController extends Controller **In Console Commands:** ```php -use Timatic\SDK\TimaticConnector; +use Timatic\TimaticConnector; class SyncBudgetsCommand extends Command { @@ -100,13 +100,155 @@ class SyncBudgetsCommand extends Command } ``` +### Testing + +When testing code that uses the Timatic SDK, you can mock the connector and its responses using factories. The SDK includes factory classes for all DTOs that make it easy to generate test data. + +Here's an example of testing the `BudgetController` from the example above: + +```php +use Timatic\TimaticConnector; +use Timatic\Dto\Budget; +use Timatic\Dto\BudgetType; +use Timatic\Requests\Budget\GetBudgetsRequest; +use Timatic\Requests\BudgetType\GetBudgetTypesRequest; +use Saloon\Http\Faking\MockClient; +use Saloon\Http\Faking\MockResponse; + +test('it displays budgets and budget types', function () { + // Generate test data using factories + $budget = Budget::factory()->state(['id' => '1'])->make(); + $budgetType = BudgetType::factory()->state(['id' => '1'])->make(); + + // Create mock responses using factory-generated data + $mockClient = new MockClient([ + GetBudgetsRequest::class => MockResponse::make([ + 'data' => [$budget->toJsonApi()], + ], 200), + GetBudgetTypesRequest::class => MockResponse::make([ + 'data' => [$budgetType->toJsonApi()], + ], 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')); + + // Assert + $response->assertOk(); + $response->assertViewHas('budgets'); + $response->assertViewHas('budgetTypes'); +}); + +test('it creates a new budget', function () { + // Generate test data with specific attributes + $budget = Budget::factory()->state([ + 'id' => '2', + 'title' => 'New Budget', + 'totalPrice' => '5000.00', + ])->make(); + + $mockClient = new MockClient([ + 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, + ]); + + $response->assertRedirect(route('budgets.show', '2')); +}); + +test('it sends a POST request to create a budget using the SDK', function () { + $budgetToCreate = Budget::factory()->state([ + 'title' => 'New Budget', + 'totalPrice' => '5000.00', + 'customerId' => 'customer-123', + ])->make(); + + $createdBudget = Budget::factory()->state([ + 'id' => 'created-456', + 'title' => 'New Budget', + 'totalPrice' => '5000.00', + 'customerId' => 'customer-123', + ])->make(); + + $mockClient = new MockClient([ + PostBudgetsRequest::class => MockResponse::make([ + 'data' => $createdBudget->toJsonApi(), + ], 201), + ]); + + $connector = new TimaticConnector(); + $connector->withMockClient($mockClient); + + $response = $connector->send(new PostBudgetsRequest($budgetToCreate)); + + // Assert the request body was sent correctly + $mockClient->assertSent(function (\Saloon\Http\Request $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'); +}); +``` + +#### Factory Methods + +Every DTO in the SDK has a corresponding factory class with the following methods: + +```php +// Create a single model with random data +$budget = Budget::factory()->make(); + +// Create multiple models with unique UUID IDs +$budgets = Budget::factory()->withId()->count(3)->make(); // Returns Collection + +// Override specific attributes +$budget = Budget::factory()->state([ + 'title' => 'Q1 Budget', + 'totalPrice' => '10000.00', +])->make(); + +// Chain state calls for complex scenarios +$budget = Budget::factory() + ->state(['customerId' => $customerId]) + ->state(['budgetTypeId' => $budgetTypeId]) + ->make(); +``` + +For more information on mocking Saloon requests, see the [Saloon Mocking Documentation](https://docs.saloon.dev/testing/faking-responses). + ### Pagination The SDK supports automatic pagination for all collection endpoints using Saloon's pagination plugin: ```php -use Timatic\SDK\TimaticConnector; -use Timatic\SDK\Requests\Budget\GetBudgets; +use Timatic\TimaticConnector; +use Timatic\Requests\Budget\GetBudgets; class BudgetController extends Controller { diff --git a/composer.json b/composer.json index 32d726c..a0a361c 100644 --- a/composer.json +++ b/composer.json @@ -5,13 +5,14 @@ "license": "Elastic-2.0", "autoload": { "psr-4": { - "Timatic\\SDK\\": "src/", - "Timatic\\SDK\\Generator\\": "generator/" + "Timatic\\": "src/", + "Timatic\\Generator\\": "generator/", + "Timatic\\Factories\\": "factories/" } }, "autoload-dev": { "psr-4": { - "Timatic\\SDK\\Tests\\": "tests/" + "Timatic\\Tests\\": "tests/" } }, "require": { @@ -21,6 +22,7 @@ "saloonphp/pagination-plugin": "^2.0" }, "require-dev": { + "fakerphp/faker": "^1.23", "laravel/boost": "^1.8", "phpunit/phpunit": "^10.0|^11.0", "pestphp/pest": "^2.0|^3.0", @@ -49,10 +51,10 @@ "extra": { "laravel": { "providers": [ - "Timatic\\SDK\\Providers\\TimaticServiceProvider" + "Timatic\\Providers\\TimaticServiceProvider" ], "aliases": { - "Timatic": "Timatic\\SDK\\Facades\\Timatic" + "Timatic": "Timatic\\Facades\\Timatic" } } }, diff --git a/example.php b/example.php index 14add1e..7a7c59b 100644 --- a/example.php +++ b/example.php @@ -2,7 +2,7 @@ require_once __DIR__.'/vendor/autoload.php'; -use Timatic\SDK\TimaticConnector; +use Timatic\TimaticConnector; // Initialize the Timatic SDK $timatic = new TimaticConnector; @@ -42,7 +42,7 @@ // Example 5: Create a new entry (commented out to prevent accidental execution) echo "Creating a new entry...\n"; -$response = $timatic->entry()->postEntries(new \Timatic\SDK\Dto\Entry([ +$response = $timatic->entry()->postEntries(new \Timatic\Dto\Entry([ 'user_id' => 1, 'customer_id' => 1, 'date' => date('Y-m-d'), diff --git a/factories/ApproveFactory.php b/factories/ApproveFactory.php new file mode 100644 index 0000000..05e2aa2 --- /dev/null +++ b/factories/ApproveFactory.php @@ -0,0 +1,32 @@ + $this->faker->uuid(), + 'overtimeTypeId' => $this->faker->uuid(), + 'startedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'endedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'percentages' => $this->faker->word(), + 'approvedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'approvedByUserId' => $this->faker->uuid(), + 'exportedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'createdAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'updatedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + ]; + } + + protected function modelClass(): string + { + return Approve::class; + } +} diff --git a/factories/BudgetFactory.php b/factories/BudgetFactory.php new file mode 100644 index 0000000..0fdbad5 --- /dev/null +++ b/factories/BudgetFactory.php @@ -0,0 +1,38 @@ + $this->faker->uuid(), + 'customerId' => $this->faker->uuid(), + 'showToCustomer' => $this->faker->boolean(), + 'changeId' => $this->faker->uuid(), + 'contractId' => $this->faker->uuid(), + 'title' => $this->faker->sentence(), + 'description' => $this->faker->sentence(), + 'totalPrice' => $this->faker->word(), + 'startedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'endedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'initialMinutes' => $this->faker->numberBetween(15, 480), + 'isArchived' => $this->faker->boolean(), + 'renewalFrequency' => $this->faker->word(), + 'createdAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'updatedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'supervisorUserId' => $this->faker->uuid(), + ]; + } + + protected function modelClass(): string + { + return Budget::class; + } +} diff --git a/factories/BudgetTimeSpentTotalFactory.php b/factories/BudgetTimeSpentTotalFactory.php new file mode 100644 index 0000000..31e918c --- /dev/null +++ b/factories/BudgetTimeSpentTotalFactory.php @@ -0,0 +1,27 @@ + Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'end' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'remainingMinutes' => $this->faker->numberBetween(15, 480), + 'periodUnit' => $this->faker->word(), + 'periodValue' => $this->faker->numberBetween(1, 100), + ]; + } + + protected function modelClass(): string + { + return BudgetTimeSpentTotal::class; + } +} diff --git a/factories/BudgetTypeFactory.php b/factories/BudgetTypeFactory.php new file mode 100644 index 0000000..7d279a5 --- /dev/null +++ b/factories/BudgetTypeFactory.php @@ -0,0 +1,30 @@ + $this->faker->sentence(), + 'isArchived' => $this->faker->boolean(), + 'hasChangeTicket' => $this->faker->boolean(), + 'renewalFrequencies' => $this->faker->word(), + 'hasSupervisor' => $this->faker->boolean(), + 'hasContractId' => $this->faker->uuid(), + 'hasTotalPrice' => $this->faker->boolean(), + 'ticketIsRequired' => $this->faker->boolean(), + 'defaultTitle' => $this->faker->sentence(), + ]; + } + + protected function modelClass(): string + { + return BudgetType::class; + } +} diff --git a/factories/CorrectionFactory.php b/factories/CorrectionFactory.php new file mode 100644 index 0000000..5f2fe34 --- /dev/null +++ b/factories/CorrectionFactory.php @@ -0,0 +1,24 @@ + Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'updatedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + ]; + } + + protected function modelClass(): string + { + return Correction::class; + } +} diff --git a/factories/CustomerFactory.php b/factories/CustomerFactory.php new file mode 100644 index 0000000..25b5fe8 --- /dev/null +++ b/factories/CustomerFactory.php @@ -0,0 +1,28 @@ + $this->faker->uuid(), + 'name' => $this->faker->name(), + 'hourlyRate' => number_format($this->faker->randomFloat(2, 50, 150), 2, '.', ''), + 'accountManagerUserId' => $this->faker->uuid(), + 'createdAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'updatedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + ]; + } + + protected function modelClass(): string + { + return Customer::class; + } +} diff --git a/factories/DailyProgressFactory.php b/factories/DailyProgressFactory.php new file mode 100644 index 0000000..a2dfa19 --- /dev/null +++ b/factories/DailyProgressFactory.php @@ -0,0 +1,25 @@ + $this->faker->uuid(), + 'date' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'progress' => $this->faker->word(), + ]; + } + + protected function modelClass(): string + { + return DailyProgress::class; + } +} diff --git a/factories/EntryFactory.php b/factories/EntryFactory.php new file mode 100644 index 0000000..1338443 --- /dev/null +++ b/factories/EntryFactory.php @@ -0,0 +1,49 @@ + $this->faker->uuid(), + 'ticketNumber' => $this->faker->word(), + 'ticketTitle' => $this->faker->sentence(), + 'ticketType' => $this->faker->word(), + 'customerId' => $this->faker->uuid(), + 'customerName' => $this->faker->company(), + 'hourlyRate' => number_format($this->faker->randomFloat(2, 50, 150), 2, '.', ''), + 'hadEmergencyShift' => $this->faker->boolean(), + 'budgetId' => $this->faker->uuid(), + 'isPaidPerHour' => $this->faker->boolean(), + 'minutesSpent' => $this->faker->numberBetween(15, 480), + 'userId' => $this->faker->uuid(), + 'userEmail' => $this->faker->safeEmail(), + 'userFullName' => $this->faker->name(), + 'createdByUserId' => $this->faker->uuid(), + 'createdByUserEmail' => $this->faker->safeEmail(), + 'createdByUserFullName' => $this->faker->company(), + 'entryType' => $this->faker->word(), + 'description' => $this->faker->sentence(), + 'isInternal' => $this->faker->boolean(), + 'startedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'endedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'invoicedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'isInvoiced' => $this->faker->word(), + 'createdAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'updatedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'isBasedOnSuggestion' => $this->faker->boolean(), + ]; + } + + protected function modelClass(): string + { + return Entry::class; + } +} diff --git a/factories/EntrySuggestionFactory.php b/factories/EntrySuggestionFactory.php new file mode 100644 index 0000000..26fe97a --- /dev/null +++ b/factories/EntrySuggestionFactory.php @@ -0,0 +1,32 @@ + $this->faker->uuid(), + 'ticketNumber' => $this->faker->word(), + 'customerId' => $this->faker->uuid(), + 'userId' => $this->faker->uuid(), + 'date' => $this->faker->word(), + 'ticketTitle' => $this->faker->sentence(), + 'ticketType' => $this->faker->word(), + 'createdAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'updatedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'budgetId' => $this->faker->uuid(), + ]; + } + + protected function modelClass(): string + { + return EntrySuggestion::class; + } +} diff --git a/factories/EventFactory.php b/factories/EventFactory.php new file mode 100644 index 0000000..d9818a6 --- /dev/null +++ b/factories/EventFactory.php @@ -0,0 +1,37 @@ + $this->faker->uuid(), + 'budgetId' => $this->faker->uuid(), + 'ticketId' => $this->faker->uuid(), + 'sourceId' => $this->faker->uuid(), + 'ticketNumber' => $this->faker->word(), + 'ticketType' => $this->faker->word(), + 'title' => $this->faker->sentence(), + 'description' => $this->faker->sentence(), + 'customerId' => $this->faker->uuid(), + 'eventTypeId' => $this->faker->uuid(), + 'startedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'endedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'createdAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'updatedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'isInternal' => $this->faker->word(), + ]; + } + + protected function modelClass(): string + { + return Event::class; + } +} diff --git a/factories/ExportMailFactory.php b/factories/ExportMailFactory.php new file mode 100644 index 0000000..2a7a6a3 --- /dev/null +++ b/factories/ExportMailFactory.php @@ -0,0 +1,21 @@ +faker = FakerFactory::create(); + } + + public static function new(): static + { + return new static; + } + + /** + * Set the number of models to generate. + */ + public function count(int $count): static + { + $this->count = $count; + + return $this; + } + + /** + * Set custom attribute overrides. + */ + public function state(array $attributes): static + { + $this->states = array_merge($this->states, $attributes); + + return $this; + } + + /** + * Generate models with unique UUIDs as IDs. + */ + public function withId(): static + { + $this->withId = true; + + return $this; + } + + /** + * Generate one or more model instances. + * + * @return Model|Collection + */ + public function make(): Model|Collection + { + if ($this->count === 1) { + return $this->makeOne(); + } + + return Collection::times($this->count, fn () => $this->makeOne()); + } + + /** + * Alias for make() for API consistency with Laravel factories. + * + * @return Model|Collection + */ + public function create(): Model|Collection + { + return $this->make(); + } + + /** + * Generate a single model instance. + */ + protected function makeOne(): Model + { + $modelClass = $this->modelClass(); + $model = new $modelClass; + + $attributes = array_merge($this->definition(), $this->states); + + // Auto-generate UUID if withId() was called + if ($this->withId && ! isset($attributes['id'])) { + $attributes['id'] = $this->faker->uuid(); + } + + foreach ($attributes as $key => $value) { + $model->{$key} = $value; + } + + return $model; + } + + /** + * Define the default attributes for the model. + */ + abstract protected function definition(): array; + + /** + * Get the model class name. + */ + abstract protected function modelClass(): string; +} diff --git a/factories/MarkAsExportedFactory.php b/factories/MarkAsExportedFactory.php new file mode 100644 index 0000000..a5a5e59 --- /dev/null +++ b/factories/MarkAsExportedFactory.php @@ -0,0 +1,32 @@ + $this->faker->uuid(), + 'overtimeTypeId' => $this->faker->uuid(), + 'startedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'endedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'percentages' => $this->faker->word(), + 'approvedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'approvedByUserId' => $this->faker->uuid(), + 'exportedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'createdAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'updatedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + ]; + } + + protected function modelClass(): string + { + return MarkAsExported::class; + } +} diff --git a/factories/MarkAsInvoicedFactory.php b/factories/MarkAsInvoicedFactory.php new file mode 100644 index 0000000..bf9e2f2 --- /dev/null +++ b/factories/MarkAsInvoicedFactory.php @@ -0,0 +1,49 @@ + $this->faker->uuid(), + 'ticketNumber' => $this->faker->word(), + 'ticketTitle' => $this->faker->sentence(), + 'ticketType' => $this->faker->word(), + 'customerId' => $this->faker->uuid(), + 'customerName' => $this->faker->company(), + 'hourlyRate' => number_format($this->faker->randomFloat(2, 50, 150), 2, '.', ''), + 'hadEmergencyShift' => $this->faker->boolean(), + 'budgetId' => $this->faker->uuid(), + 'isPaidPerHour' => $this->faker->boolean(), + 'minutesSpent' => $this->faker->numberBetween(15, 480), + 'userId' => $this->faker->uuid(), + 'userEmail' => $this->faker->safeEmail(), + 'userFullName' => $this->faker->name(), + 'createdByUserId' => $this->faker->uuid(), + 'createdByUserEmail' => $this->faker->safeEmail(), + 'createdByUserFullName' => $this->faker->company(), + 'entryType' => $this->faker->word(), + 'description' => $this->faker->sentence(), + 'isInternal' => $this->faker->boolean(), + 'startedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'endedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'invoicedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'isInvoiced' => $this->faker->word(), + 'createdAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'updatedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'isBasedOnSuggestion' => $this->faker->boolean(), + ]; + } + + protected function modelClass(): string + { + return MarkAsInvoiced::class; + } +} diff --git a/factories/OvertimeFactory.php b/factories/OvertimeFactory.php new file mode 100644 index 0000000..48b9376 --- /dev/null +++ b/factories/OvertimeFactory.php @@ -0,0 +1,32 @@ + $this->faker->uuid(), + 'overtimeTypeId' => $this->faker->uuid(), + 'startedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'endedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'percentages' => $this->faker->word(), + 'approvedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'approvedByUserId' => $this->faker->uuid(), + 'exportedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'createdAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'updatedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + ]; + } + + protected function modelClass(): string + { + return Overtime::class; + } +} diff --git a/factories/TeamFactory.php b/factories/TeamFactory.php new file mode 100644 index 0000000..6a1ff35 --- /dev/null +++ b/factories/TeamFactory.php @@ -0,0 +1,26 @@ + $this->faker->uuid(), + 'name' => $this->faker->name(), + 'createdAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'updatedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + ]; + } + + protected function modelClass(): string + { + return Team::class; + } +} diff --git a/factories/TimeSpentTotalFactory.php b/factories/TimeSpentTotalFactory.php new file mode 100644 index 0000000..1c7fbd9 --- /dev/null +++ b/factories/TimeSpentTotalFactory.php @@ -0,0 +1,28 @@ + Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'end' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'internalMinutes' => $this->faker->numberBetween(15, 480), + 'billableMinutes' => $this->faker->numberBetween(15, 480), + 'periodUnit' => $this->faker->word(), + 'periodValue' => $this->faker->numberBetween(1, 100), + ]; + } + + protected function modelClass(): string + { + return TimeSpentTotal::class; + } +} diff --git a/factories/UserCustomerHoursAggregateFactory.php b/factories/UserCustomerHoursAggregateFactory.php new file mode 100644 index 0000000..de87f41 --- /dev/null +++ b/factories/UserCustomerHoursAggregateFactory.php @@ -0,0 +1,26 @@ + $this->faker->uuid(), + 'userId' => $this->faker->uuid(), + 'internalMinutes' => $this->faker->numberBetween(15, 480), + 'budgetMinutes' => $this->faker->numberBetween(15, 480), + 'paidPerHourMinutes' => $this->faker->numberBetween(15, 480), + ]; + } + + protected function modelClass(): string + { + return UserCustomerHoursAggregate::class; + } +} diff --git a/factories/UserFactory.php b/factories/UserFactory.php new file mode 100644 index 0000000..a7c7c77 --- /dev/null +++ b/factories/UserFactory.php @@ -0,0 +1,23 @@ + $this->faker->uuid(), + 'email' => $this->faker->safeEmail(), + ]; + } + + protected function modelClass(): string + { + return User::class; + } +} diff --git a/generator/JsonApiConnectorGenerator.php b/generator/JsonApiConnectorGenerator.php index c2941d3..2d17c2c 100644 --- a/generator/JsonApiConnectorGenerator.php +++ b/generator/JsonApiConnectorGenerator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Timatic\SDK\Generator; +namespace Timatic\Generator; use Crescat\SaloonSdkGenerator\Data\Generator\ApiSpecification; use Crescat\SaloonSdkGenerator\Generators\ConnectorGenerator; @@ -11,8 +11,8 @@ use Nette\PhpGenerator\PhpFile; use Saloon\Http\Request; use Saloon\PaginationPlugin\Contracts\HasPagination; -use Timatic\SDK\Pagination\JsonApiPaginator; -use Timatic\SDK\Responses\TimaticResponse; +use Timatic\Pagination\JsonApiPaginator; +use Timatic\Responses\TimaticResponse; class JsonApiConnectorGenerator extends ConnectorGenerator { diff --git a/generator/JsonApiDtoGenerator.php b/generator/JsonApiDtoGenerator.php index 5057ed8..ca72a04 100644 --- a/generator/JsonApiDtoGenerator.php +++ b/generator/JsonApiDtoGenerator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Timatic\SDK\Generator; +namespace Timatic\Generator; use cebe\openapi\spec\Reference; use cebe\openapi\spec\Schema; @@ -13,9 +13,9 @@ use Illuminate\Support\Str; use Nette\PhpGenerator\ClassType; use Nette\PhpGenerator\PhpFile; -use Timatic\SDK\Hydration\Attributes\DateTime; -use Timatic\SDK\Hydration\Attributes\Property; -use Timatic\SDK\Hydration\Model; +use Timatic\Hydration\Attributes\DateTime; +use Timatic\Hydration\Attributes\Property; +use Timatic\Hydration\Model; class JsonApiDtoGenerator extends Generator { diff --git a/generator/JsonApiFactoryGenerator.php b/generator/JsonApiFactoryGenerator.php new file mode 100644 index 0000000..27bbf70 --- /dev/null +++ b/generator/JsonApiFactoryGenerator.php @@ -0,0 +1,214 @@ +components) { + foreach ($specification->components->schemas as $className => $schema) { + $dtoClassName = NameHelper::dtoClassName(NameHelper::safeClassName($className)); + $this->generateFactoryClass($dtoClassName); + } + } + + return $this->generated; + } + + protected function generateFactoryClass(string $dtoClassName): PhpFile + { + $factoryName = $dtoClassName.'Factory'; + + $classType = new ClassType($factoryName); + $classFile = new PhpFile; + $namespace = $classFile->addNamespace("{$this->config->namespace}\\Factories"); + + // Extend base Factory + $classType->setExtends(Factory::class); + + // Add imports + $namespace->addUse(Factory::class); + $dtoFullClass = "{$this->config->namespace}\\{$this->config->dtoNamespaceSuffix}\\{$dtoClassName}"; + $namespace->addUse($dtoFullClass); + + // Get DTO properties + $properties = $this->getDtoProperties($dtoFullClass); + + // Add definition() method + $definitionMethod = $classType->addMethod('definition') + ->setReturnType('array') + ->setProtected(); + + $definitionBody = $this->generateDefinitionBody($properties, $namespace); + $definitionMethod->setBody($definitionBody); + + // Add modelClass() method + $modelClassMethod = $classType->addMethod('modelClass') + ->setReturnType('string') + ->setProtected(); + + $modelClassMethod->setBody("return {$dtoClassName}::class;"); + + $namespace->add($classType); + + $this->generated[$factoryName] = $classFile; + + return $classFile; + } + + /** + * Get DTO properties using reflection + * + * @return array + */ + protected function getDtoProperties(string $dtoFullClass): array + { + if (! class_exists($dtoFullClass)) { + return []; + } + + $reflection = new ReflectionClass($dtoFullClass); + $properties = []; + + foreach ($reflection->getProperties() as $property) { + // Skip 'id' and 'type' properties from Model base class + if (in_array($property->getName(), ['id', 'type'])) { + continue; + } + + // Skip static and private properties + if ($property->isStatic() || $property->isPrivate()) { + continue; + } + + // Check if property has DateTime attribute + $isDateTime = ! empty($property->getAttributes(DateTime::class)); + + // Get property type + $type = $property->getType(); + $typeName = null; + if ($type) { + $typeName = $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type; + } + + $properties[] = [ + 'name' => $property->getName(), + 'type' => $typeName, + 'isDateTime' => $isDateTime, + ]; + } + + return $properties; + } + + /** + * Generate the body of the definition() method + */ + protected function generateDefinitionBody(array $properties, $namespace): string + { + $lines = ['return [']; + + foreach ($properties as $property) { + $propertyName = $property['name']; + $fakerCall = $this->generateFakerCall($propertyName, $property['type'], $property['isDateTime']); + + $lines[] = " '{$propertyName}' => {$fakerCall},"; + } + + $lines[] = '];'; + + // Add Carbon import if we have datetime properties + $hasDateTime = collect($properties)->contains('isDateTime', true); + if ($hasDateTime) { + $namespace->addUse('Carbon\\Carbon'); + } + + return implode("\n", $lines); + } + + /** + * Generate appropriate Faker call for a property + */ + protected function generateFakerCall(string $propertyName, ?string $propertyType, bool $isDateTime): string + { + $lowerName = strtolower($propertyName); + + // Handle DateTime properties + if ($isDateTime || $propertyType === 'Carbon\\Carbon') { + return 'Carbon::now()->subDays($this->faker->numberBetween(0, 365))'; + } + + // Handle specific property names (case-insensitive) + if (str_contains($lowerName, 'email')) { + return '$this->faker->safeEmail()'; + } + + if (str_ends_with($propertyName, 'Id') || str_ends_with($lowerName, '_id')) { + return '$this->faker->uuid()'; + } + + if ($lowerName === 'hourlyrate' || $lowerName === 'hourly_rate' || str_contains($lowerName, 'rate')) { + return "number_format(\$this->faker->randomFloat(2, 50, 150), 2, '.', '')"; + } + + if (str_contains($lowerName, 'description')) { + return '$this->faker->sentence()'; + } + + if (str_contains($lowerName, 'title')) { + return '$this->faker->sentence()'; + } + + // Handle by property type + if ($propertyType) { + $baseType = ltrim($propertyType, '?\\'); + + if ($baseType === 'bool' || $baseType === 'boolean') { + return '$this->faker->boolean()'; + } + + if ($baseType === 'int' || $baseType === 'integer') { + // Special cases for specific property names + if (str_contains($lowerName, 'minute')) { + return '$this->faker->numberBetween(15, 480)'; + } + + return '$this->faker->numberBetween(1, 100)'; + } + + if ($baseType === 'float' || $baseType === 'double') { + return '$this->faker->randomFloat(2, 1, 1000)'; + } + } + + // Handle by property name patterns + if (str_ends_with($propertyName, 'Name') && ! str_starts_with($lowerName, 'user')) { + return '$this->faker->company()'; + } + + if (str_contains($lowerName, 'name')) { + return '$this->faker->name()'; + } + + if (str_contains($lowerName, 'number')) { + return '$this->faker->word()'; + } + + // Default to word for strings + return '$this->faker->word()'; + } +} diff --git a/generator/JsonApiPestTestGenerator.php b/generator/JsonApiPestTestGenerator.php index 5292cf9..077aa2a 100644 --- a/generator/JsonApiPestTestGenerator.php +++ b/generator/JsonApiPestTestGenerator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Timatic\SDK\Generator; +namespace Timatic\Generator; use Crescat\SaloonSdkGenerator\Data\Generator\ApiSpecification; use Crescat\SaloonSdkGenerator\Data\Generator\Config; @@ -12,11 +12,11 @@ use Crescat\SaloonSdkGenerator\Generators\PestTestGenerator; use Crescat\SaloonSdkGenerator\Helpers\NameHelper; use Nette\PhpGenerator\PhpFile; -use Timatic\SDK\Generator\TestGenerators\CollectionRequestTestGenerator; -use Timatic\SDK\Generator\TestGenerators\DeleteRequestTestGenerator; -use Timatic\SDK\Generator\TestGenerators\MutationRequestTestGenerator; -use Timatic\SDK\Generator\TestGenerators\SingularGetRequestTestGenerator; -use Timatic\SDK\Generator\TestGenerators\Traits\DtoHelperTrait; +use Timatic\Generator\TestGenerators\CollectionRequestTestGenerator; +use Timatic\Generator\TestGenerators\DeleteRequestTestGenerator; +use Timatic\Generator\TestGenerators\MutationRequestTestGenerator; +use Timatic\Generator\TestGenerators\SingularGetRequestTestGenerator; +use Timatic\Generator\TestGenerators\Traits\DtoHelperTrait; class JsonApiPestTestGenerator extends PestTestGenerator { diff --git a/generator/JsonApiRequestGenerator.php b/generator/JsonApiRequestGenerator.php index 67898a3..c18fe24 100644 --- a/generator/JsonApiRequestGenerator.php +++ b/generator/JsonApiRequestGenerator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Timatic\SDK\Generator; +namespace Timatic\Generator; use Crescat\SaloonSdkGenerator\Data\Generator\ApiSpecification; use Crescat\SaloonSdkGenerator\Data\Generator\Endpoint; @@ -13,10 +13,10 @@ use Nette\PhpGenerator\PhpFile; use Saloon\Http\Response; use Saloon\PaginationPlugin\Contracts\Paginatable; -use Timatic\SDK\Concerns\HasFilters; -use Timatic\SDK\Generator\TestGenerators\Traits\DtoHelperTrait; -use Timatic\SDK\Hydration\Facades\Hydrator; -use Timatic\SDK\Hydration\Model; +use Timatic\Generator\TestGenerators\Traits\DtoHelperTrait; +use Timatic\Hydration\Facades\Hydrator; +use Timatic\Hydration\Model; +use Timatic\Requests\HasFilters; class JsonApiRequestGenerator extends RequestGenerator { @@ -100,7 +100,7 @@ protected function customizeConstructor($classConstructor, ClassType $classType, $namespace->addUse(Model::class); $dataParam = new Parameter( - type: '\\Timatic\\SDK\\Hydration\\Model|array|null', + type: '\\Timatic\\Hydration\\Model|array|null', nullable: true, name: 'data', description: 'Request data', @@ -111,7 +111,7 @@ protected function customizeConstructor($classConstructor, ClassType $classType, $classType->addMethod('defaultBody') ->setProtected() ->setReturnType('array') - ->addBody('return $this->data ? $this->data->toJsonApi() : [];'); + ->addBody("return \$this->data ? ['data' => \$this->data->toJsonApi()] : [];"); } /** @@ -192,7 +192,7 @@ protected function addHydrationSupport(ClassType $classType, $namespace, Endpoin // Add imports $namespace->addUse(Hydrator::class); $namespace->addUse(Response::class); - $namespace->addUse("Timatic\\SDK\\Dto\\{$dtoClassName}"); + $namespace->addUse("Timatic\\Dto\\{$dtoClassName}"); // Add $model property - use the imported class name with ::class $classType->addProperty('model') diff --git a/generator/JsonApiResourceGenerator.php b/generator/JsonApiResourceGenerator.php index 1c68f83..e89ae70 100644 --- a/generator/JsonApiResourceGenerator.php +++ b/generator/JsonApiResourceGenerator.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace Timatic\SDK\Generator; +namespace Timatic\Generator; use Crescat\SaloonSdkGenerator\Data\Generator\Endpoint; use Crescat\SaloonSdkGenerator\Data\Generator\Parameter; use Crescat\SaloonSdkGenerator\Generators\ResourceGenerator; use Crescat\SaloonSdkGenerator\Helpers\NameHelper; use Nette\PhpGenerator\Method; -use Timatic\SDK\Hydration\Model; +use Timatic\Hydration\Model; class JsonApiResourceGenerator extends ResourceGenerator { diff --git a/generator/TestGenerators/CollectionRequestTestGenerator.php b/generator/TestGenerators/CollectionRequestTestGenerator.php index 20f6b67..3b2f02a 100644 --- a/generator/TestGenerators/CollectionRequestTestGenerator.php +++ b/generator/TestGenerators/CollectionRequestTestGenerator.php @@ -2,17 +2,17 @@ declare(strict_types=1); -namespace Timatic\SDK\Generator\TestGenerators; +namespace Timatic\Generator\TestGenerators; use Crescat\SaloonSdkGenerator\Data\Generator\ApiSpecification; use Crescat\SaloonSdkGenerator\Data\Generator\Endpoint; use Crescat\SaloonSdkGenerator\Data\Generator\GeneratedCode; use Crescat\SaloonSdkGenerator\Helpers\NameHelper; -use Timatic\SDK\Generator\TestGenerators\Traits\DtoAssertions; -use Timatic\SDK\Generator\TestGenerators\Traits\DtoHelperTrait; -use Timatic\SDK\Generator\TestGenerators\Traits\MockJsonDataTrait; -use Timatic\SDK\Generator\TestGenerators\Traits\ResourceTypeExtractorTrait; -use Timatic\SDK\Generator\TestGenerators\Traits\TestDataGeneratorTrait; +use Timatic\Generator\TestGenerators\Traits\DtoAssertions; +use Timatic\Generator\TestGenerators\Traits\DtoHelperTrait; +use Timatic\Generator\TestGenerators\Traits\MockJsonDataTrait; +use Timatic\Generator\TestGenerators\Traits\ResourceTypeExtractorTrait; +use Timatic\Generator\TestGenerators\Traits\TestDataGeneratorTrait; class CollectionRequestTestGenerator { diff --git a/generator/TestGenerators/DeleteRequestTestGenerator.php b/generator/TestGenerators/DeleteRequestTestGenerator.php index b60e791..c6b93b7 100644 --- a/generator/TestGenerators/DeleteRequestTestGenerator.php +++ b/generator/TestGenerators/DeleteRequestTestGenerator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Timatic\SDK\Generator\TestGenerators; +namespace Timatic\Generator\TestGenerators; use Crescat\SaloonSdkGenerator\Data\Generator\ApiSpecification; use Crescat\SaloonSdkGenerator\Data\Generator\Endpoint; diff --git a/generator/TestGenerators/MutationRequestTestGenerator.php b/generator/TestGenerators/MutationRequestTestGenerator.php index 6189367..8135d4a 100644 --- a/generator/TestGenerators/MutationRequestTestGenerator.php +++ b/generator/TestGenerators/MutationRequestTestGenerator.php @@ -2,16 +2,16 @@ declare(strict_types=1); -namespace Timatic\SDK\Generator\TestGenerators; +namespace Timatic\Generator\TestGenerators; use Crescat\SaloonSdkGenerator\Data\Generator\ApiSpecification; use Crescat\SaloonSdkGenerator\Data\Generator\Endpoint; use Crescat\SaloonSdkGenerator\Data\Generator\GeneratedCode; use Crescat\SaloonSdkGenerator\Helpers\NameHelper; -use Timatic\SDK\Generator\TestGenerators\Traits\DtoAssertions; -use Timatic\SDK\Generator\TestGenerators\Traits\DtoHelperTrait; -use Timatic\SDK\Generator\TestGenerators\Traits\ResourceTypeExtractorTrait; -use Timatic\SDK\Generator\TestGenerators\Traits\TestDataGeneratorTrait; +use Timatic\Generator\TestGenerators\Traits\DtoAssertions; +use Timatic\Generator\TestGenerators\Traits\DtoHelperTrait; +use Timatic\Generator\TestGenerators\Traits\ResourceTypeExtractorTrait; +use Timatic\Generator\TestGenerators\Traits\TestDataGeneratorTrait; class MutationRequestTestGenerator { @@ -94,16 +94,47 @@ protected function generateMethodArguments(Endpoint $endpoint): string } /** - * Generate DTO instantiation code with sample data + * Generate DTO instantiation code with sample data using factory */ protected function generateDtoInstantiation(Endpoint $endpoint): string { $dtoClassName = $this->getDtoClassName($endpoint); - $properties = $this->generateDtoProperties($endpoint); + $stateArray = $this->generateFactoryStateArray($endpoint); $lines = []; - $lines[] = " \$dto = new \\Timatic\\SDK\\Dto\\{$dtoClassName};"; - $lines[] = $properties; + $lines[] = " \$dto = \\Timatic\\Dto\\{$dtoClassName}::factory()->state(["; + $lines[] = $stateArray; + $lines[] = ' ])->make();'; + + return implode("\n", $lines); + } + + /** + * Generate factory state array with test data + */ + protected function generateFactoryStateArray(Endpoint $endpoint): string + { + $filteredProperties = $this->getFilteredPropertiesForTest($endpoint); + + $lines = []; + + foreach ($filteredProperties as $propInfo) { + $propName = $propInfo['name']; + + if ($propInfo['isDateTime']) { + // Generate Carbon::parse() for DateTime fields + $dateString = $this->generateValue($propName, $propInfo['type']); + $lines[] = " '{$propName}' => \\Carbon\\Carbon::parse('{$dateString}'),"; + } else { + $value = $this->formatAsCode($this->generateValue($propName, $propInfo['type'])); + $lines[] = " '{$propName}' => {$value},"; + } + } + + // Fallback if no properties after filtering + if (empty($lines)) { + return " 'name' => 'test value',"; + } return implode("\n", $lines); } diff --git a/generator/TestGenerators/SingularGetRequestTestGenerator.php b/generator/TestGenerators/SingularGetRequestTestGenerator.php index a0655a9..28b2128 100644 --- a/generator/TestGenerators/SingularGetRequestTestGenerator.php +++ b/generator/TestGenerators/SingularGetRequestTestGenerator.php @@ -2,16 +2,16 @@ declare(strict_types=1); -namespace Timatic\SDK\Generator\TestGenerators; +namespace Timatic\Generator\TestGenerators; use Crescat\SaloonSdkGenerator\Data\Generator\ApiSpecification; use Crescat\SaloonSdkGenerator\Data\Generator\Endpoint; use Crescat\SaloonSdkGenerator\Data\Generator\GeneratedCode; -use Timatic\SDK\Generator\TestGenerators\Traits\DtoAssertions; -use Timatic\SDK\Generator\TestGenerators\Traits\DtoHelperTrait; -use Timatic\SDK\Generator\TestGenerators\Traits\MockJsonDataTrait; -use Timatic\SDK\Generator\TestGenerators\Traits\ResourceTypeExtractorTrait; -use Timatic\SDK\Generator\TestGenerators\Traits\TestDataGeneratorTrait; +use Timatic\Generator\TestGenerators\Traits\DtoAssertions; +use Timatic\Generator\TestGenerators\Traits\DtoHelperTrait; +use Timatic\Generator\TestGenerators\Traits\MockJsonDataTrait; +use Timatic\Generator\TestGenerators\Traits\ResourceTypeExtractorTrait; +use Timatic\Generator\TestGenerators\Traits\TestDataGeneratorTrait; class SingularGetRequestTestGenerator { diff --git a/generator/TestGenerators/Traits/DtoAssertions.php b/generator/TestGenerators/Traits/DtoAssertions.php index a70578c..3c05636 100644 --- a/generator/TestGenerators/Traits/DtoAssertions.php +++ b/generator/TestGenerators/Traits/DtoAssertions.php @@ -1,6 +1,6 @@ isDir() ? 'rmdir' : 'unlink'); - $todo($fileinfo->getRealPath()); + if (! is_dir($folder)) { + throw new Exception('cant clean folder: '.$folder); + } + + // Recursively scan for PHP files + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($folder, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($files as $fileinfo) { + if (! $fileinfo->isFile() || $fileinfo->getExtension() === 'php') { + continue; } - rmdir($folder); - echo ' โœ“ Removed '.basename($folder)."\n"; + // Read first 200 bytes to check for marker + $handle = fopen($fileinfo->getRealPath(), 'r'); + $header = fread($handle, 200); + fclose($handle); + + if (str_contains($header, AUTO_GENERATED_MARKER)) { + unlink($fileinfo->getRealPath()); + } } } @@ -68,7 +80,7 @@ // Create config $config = new Config( connectorName: 'TimaticConnector', - namespace: 'Timatic\\SDK', + namespace: 'Timatic', resourceNamespaceSuffix: 'Resource', requestNamespaceSuffix: 'Requests', dtoNamespaceSuffix: 'Dto', @@ -78,10 +90,10 @@ echo "๐Ÿ—๏ธ Generating SDK with JSON:API models and tests...\n"; $generator = new CodeGenerator( config: $config, - connectorGenerator: new JsonApiConnectorGenerator($config), - dtoGenerator: new JsonApiDtoGenerator($config), requestGenerator: new JsonApiRequestGenerator($config), resourceGenerator: new JsonApiResourceGenerator($config), + dtoGenerator: new JsonApiDtoGenerator($config), + connectorGenerator: new JsonApiConnectorGenerator($config), postProcessors: [new JsonApiPestTestGenerator] // Generate tests in same run ); @@ -91,11 +103,16 @@ $outputDir = __DIR__.'/../src'; // Helper function to write files -function writeFile($file, $outputDir, $namespace) +function writeFile($file, $outputDir, $namespace = null) { - $relativePath = str_replace($namespace, '', array_values($file->getNamespaces())[0]->getName()); $className = array_values($file->getClasses())[0]->getName(); - $filePath = $outputDir.str_replace('\\', '/', $relativePath).'/'.$className.'.php'; + + if (! is_null($namespace)) { + $relativePath = str_replace($namespace, '', array_values($file->getNamespaces())[0]->getName()); + $filePath = $outputDir.str_replace('\\', '/', $relativePath).'/'.$className.'.php'; + } else { + $filePath = $outputDir.'/'.$className.'.php'; + } // Create directory if it doesn't exist $dir = dirname($filePath); @@ -103,7 +120,11 @@ function writeFile($file, $outputDir, $namespace) mkdir($dir, 0755, true); } - file_put_contents($filePath, (string) $file); + // Prepend auto-generated marker + $content = (string) $file; + $content = str_replace('generate($specification); + +// Write factories to /factories directory instead of src/Factories +$factoriesDir = __DIR__.'/../factories'; +foreach ($factoryFiles as $factoryFile) { + $path = writeFile($factoryFile, $factoriesDir); + echo ' โœ“ '.basename($path)."\n"; +} diff --git a/src/Dto/Approve.php b/src/Dto/Approve.php index 10ba1fe..aef906e 100644 --- a/src/Dto/Approve.php +++ b/src/Dto/Approve.php @@ -1,10 +1,12 @@ hydrateCollection(string $model, array $data, array|null $included = null) @@ -16,6 +16,6 @@ class Hydrator extends Facade { protected static function getFacadeAccessor() { - return \Timatic\SDK\Hydration\Hydrator::class; + return \Timatic\Hydration\Hydrator::class; } } diff --git a/src/Concerns/HasAttributes.php b/src/Hydration/HasAttributes.php similarity index 95% rename from src/Concerns/HasAttributes.php rename to src/Hydration/HasAttributes.php index 858eb2b..d6438ff 100644 --- a/src/Concerns/HasAttributes.php +++ b/src/Hydration/HasAttributes.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Timatic\SDK\Concerns; +namespace Timatic\Hydration; use ReflectionClass; -use Timatic\SDK\Hydration\Attributes\Property; +use Timatic\Hydration\Attributes\Property; trait HasAttributes { diff --git a/src/Hydration/Hydrator.php b/src/Hydration/Hydrator.php index 35e33d1..37264a9 100644 --- a/src/Hydration/Hydrator.php +++ b/src/Hydration/Hydrator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Timatic\SDK\Hydration; +namespace Timatic\Hydration; use Carbon\Carbon; use Illuminate\Support\Arr; @@ -11,9 +11,9 @@ use ReflectionException; use ReflectionNamedType; use ReflectionProperty; -use Timatic\SDK\Hydration\Attributes\DateTime; -use Timatic\SDK\Hydration\Attributes\Property; -use Timatic\SDK\Hydration\Attributes\Relationship; +use Timatic\Hydration\Attributes\DateTime; +use Timatic\Hydration\Attributes\Property; +use Timatic\Hydration\Attributes\Relationship; use Webmozart\Assert\Assert; use function is_null; diff --git a/src/Hydration/Model.php b/src/Hydration/Model.php index 0403d79..33acac1 100644 --- a/src/Hydration/Model.php +++ b/src/Hydration/Model.php @@ -2,12 +2,11 @@ declare(strict_types=1); -namespace Timatic\SDK\Hydration; +namespace Timatic\Hydration; use Illuminate\Support\Str; use ReflectionClass; -use Timatic\SDK\Concerns\HasAttributes; -use Timatic\SDK\Hydration\Attributes\Property; +use Timatic\Hydration\Attributes\Property; abstract class Model implements ModelInterface { @@ -18,6 +17,21 @@ abstract class Model implements ModelInterface protected ?string $type = null; + /** + * Get a new factory instance for the model. + */ + public static function factory(): mixed + { + $modelClass = static::class; + $factoryClass = str_replace('\\Dto\\', '\\Factories\\', $modelClass).'Factory'; + + if (class_exists($factoryClass)) { + return $factoryClass::new(); + } + + throw new \RuntimeException("Factory [{$factoryClass}] not found for model [{$modelClass}]."); + } + /** * @param array $attributes */ @@ -67,17 +81,23 @@ public function type(): string } /** - * Convert Model to JSON:API format + * Convert Model to JSON:API data object. + * Returns the data object without the 'data' wrapper. * * @return array */ public function toJsonApi(): array { - return [ - 'data' => [ - 'type' => $this->type(), - 'attributes' => $this->attributes(), - ], + $data = [ + 'type' => $this->type(), ]; + + if (isset($this->id)) { + $data['id'] = $this->id; + } + + $data['attributes'] = $this->attributes(); + + return $data; } } diff --git a/src/Hydration/ModelInterface.php b/src/Hydration/ModelInterface.php index a57f2aa..31d72db 100644 --- a/src/Hydration/ModelInterface.php +++ b/src/Hydration/ModelInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Timatic\SDK\Hydration; +namespace Timatic\Hydration; interface ModelInterface { diff --git a/src/Hydration/RelationType.php b/src/Hydration/RelationType.php index 7f2c55e..7321588 100644 --- a/src/Hydration/RelationType.php +++ b/src/Hydration/RelationType.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Timatic\SDK\Hydration; +namespace Timatic\Hydration; enum RelationType { diff --git a/src/Pagination/JsonApiPaginator.php b/src/Pagination/JsonApiPaginator.php index f266876..6abd8ab 100644 --- a/src/Pagination/JsonApiPaginator.php +++ b/src/Pagination/JsonApiPaginator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Timatic\SDK\Pagination; +namespace Timatic\Pagination; use Saloon\Http\Request; use Saloon\Http\Response; diff --git a/src/Providers/TimaticServiceProvider.php b/src/Providers/TimaticServiceProvider.php index 5394521..e7df7d3 100644 --- a/src/Providers/TimaticServiceProvider.php +++ b/src/Providers/TimaticServiceProvider.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Timatic\SDK\Providers; +namespace Timatic\Providers; use Illuminate\Support\ServiceProvider; -use Timatic\SDK\TimaticConnector; +use Timatic\TimaticConnector; class TimaticServiceProvider extends ServiceProvider { diff --git a/src/Requests/Approve/PostOvertimeApproveRequest.php b/src/Requests/Approve/PostOvertimeApproveRequest.php index 5d6457f..92a7831 100644 --- a/src/Requests/Approve/PostOvertimeApproveRequest.php +++ b/src/Requests/Approve/PostOvertimeApproveRequest.php @@ -1,15 +1,17 @@ data ? $this->data->toJsonApi() : []; + return $this->data ? ['data' => $this->data->toJsonApi()] : []; } } diff --git a/src/Requests/Budget/DeleteBudgetRequest.php b/src/Requests/Budget/DeleteBudgetRequest.php index b107701..76b84a6 100644 --- a/src/Requests/Budget/DeleteBudgetRequest.php +++ b/src/Requests/Budget/DeleteBudgetRequest.php @@ -1,6 +1,8 @@ data ? $this->data->toJsonApi() : []; + return $this->data ? ['data' => $this->data->toJsonApi()] : []; } } diff --git a/src/Requests/Budget/PostBudgetsRequest.php b/src/Requests/Budget/PostBudgetsRequest.php index c030729..c9e3777 100644 --- a/src/Requests/Budget/PostBudgetsRequest.php +++ b/src/Requests/Budget/PostBudgetsRequest.php @@ -1,15 +1,17 @@ data ? $this->data->toJsonApi() : []; + return $this->data ? ['data' => $this->data->toJsonApi()] : []; } } diff --git a/src/Requests/BudgetTimeSpentTotal/GetBudgetTimeSpentTotalsRequest.php b/src/Requests/BudgetTimeSpentTotal/GetBudgetTimeSpentTotalsRequest.php index 9c61cff..b4d56ce 100644 --- a/src/Requests/BudgetTimeSpentTotal/GetBudgetTimeSpentTotalsRequest.php +++ b/src/Requests/BudgetTimeSpentTotal/GetBudgetTimeSpentTotalsRequest.php @@ -1,14 +1,16 @@ data ? $this->data->toJsonApi() : []; + return $this->data ? ['data' => $this->data->toJsonApi()] : []; } } diff --git a/src/Requests/Correction/PostCorrectionsRequest.php b/src/Requests/Correction/PostCorrectionsRequest.php index a328c9e..33c6843 100644 --- a/src/Requests/Correction/PostCorrectionsRequest.php +++ b/src/Requests/Correction/PostCorrectionsRequest.php @@ -1,15 +1,17 @@ data ? $this->data->toJsonApi() : []; + return $this->data ? ['data' => $this->data->toJsonApi()] : []; } } diff --git a/src/Requests/Customer/DeleteCustomerRequest.php b/src/Requests/Customer/DeleteCustomerRequest.php index 4d3677a..279d596 100644 --- a/src/Requests/Customer/DeleteCustomerRequest.php +++ b/src/Requests/Customer/DeleteCustomerRequest.php @@ -1,6 +1,8 @@ data ? $this->data->toJsonApi() : []; + return $this->data ? ['data' => $this->data->toJsonApi()] : []; } } diff --git a/src/Requests/Customer/PostCustomersRequest.php b/src/Requests/Customer/PostCustomersRequest.php index 3a5fa72..3ea3b83 100644 --- a/src/Requests/Customer/PostCustomersRequest.php +++ b/src/Requests/Customer/PostCustomersRequest.php @@ -1,15 +1,17 @@ data ? $this->data->toJsonApi() : []; + return $this->data ? ['data' => $this->data->toJsonApi()] : []; } } diff --git a/src/Requests/DailyProgress/GetDailyProgressesRequest.php b/src/Requests/DailyProgress/GetDailyProgressesRequest.php index 224aa2f..3503173 100644 --- a/src/Requests/DailyProgress/GetDailyProgressesRequest.php +++ b/src/Requests/DailyProgress/GetDailyProgressesRequest.php @@ -1,13 +1,15 @@ data ? $this->data->toJsonApi() : []; + return $this->data ? ['data' => $this->data->toJsonApi()] : []; } } diff --git a/src/Requests/Entry/PostEntriesRequest.php b/src/Requests/Entry/PostEntriesRequest.php index b5ff24f..a5d8e0c 100644 --- a/src/Requests/Entry/PostEntriesRequest.php +++ b/src/Requests/Entry/PostEntriesRequest.php @@ -1,15 +1,17 @@ data ? $this->data->toJsonApi() : []; + return $this->data ? ['data' => $this->data->toJsonApi()] : []; } } diff --git a/src/Requests/EntrySuggestion/DeleteEntrySuggestionRequest.php b/src/Requests/EntrySuggestion/DeleteEntrySuggestionRequest.php index 96e86e5..1440bd3 100644 --- a/src/Requests/EntrySuggestion/DeleteEntrySuggestionRequest.php +++ b/src/Requests/EntrySuggestion/DeleteEntrySuggestionRequest.php @@ -1,6 +1,8 @@ data ? $this->data->toJsonApi() : []; + return $this->data ? ['data' => $this->data->toJsonApi()] : []; } } diff --git a/src/Requests/ExportMail/GetBudgetsExportMailsRequest.php b/src/Requests/ExportMail/GetBudgetsExportMailsRequest.php index 6aa927b..746f472 100644 --- a/src/Requests/ExportMail/GetBudgetsExportMailsRequest.php +++ b/src/Requests/ExportMail/GetBudgetsExportMailsRequest.php @@ -1,13 +1,15 @@ data ? $this->data->toJsonApi() : []; + return $this->data ? ['data' => $this->data->toJsonApi()] : []; } } diff --git a/src/Requests/MarkAsInvoiced/PostEntryMarkAsInvoicedRequest.php b/src/Requests/MarkAsInvoiced/PostEntryMarkAsInvoicedRequest.php index fafe2ff..d0d2dfb 100644 --- a/src/Requests/MarkAsInvoiced/PostEntryMarkAsInvoicedRequest.php +++ b/src/Requests/MarkAsInvoiced/PostEntryMarkAsInvoicedRequest.php @@ -1,15 +1,17 @@ data ? $this->data->toJsonApi() : []; + return $this->data ? ['data' => $this->data->toJsonApi()] : []; } } diff --git a/src/Requests/Me/GetMesRequest.php b/src/Requests/Me/GetMesRequest.php index 107fd45..a53715c 100644 --- a/src/Requests/Me/GetMesRequest.php +++ b/src/Requests/Me/GetMesRequest.php @@ -1,6 +1,8 @@ data ? $this->data->toJsonApi() : []; + return $this->data ? ['data' => $this->data->toJsonApi()] : []; } } diff --git a/src/Requests/Team/PostTeamsRequest.php b/src/Requests/Team/PostTeamsRequest.php index f031ae8..853b84d 100644 --- a/src/Requests/Team/PostTeamsRequest.php +++ b/src/Requests/Team/PostTeamsRequest.php @@ -1,15 +1,17 @@ data ? $this->data->toJsonApi() : []; + return $this->data ? ['data' => $this->data->toJsonApi()] : []; } } diff --git a/src/Requests/TimeSpentTotal/GetTimeSpentTotalsRequest.php b/src/Requests/TimeSpentTotal/GetTimeSpentTotalsRequest.php index 4e8624c..0a003c8 100644 --- a/src/Requests/TimeSpentTotal/GetTimeSpentTotalsRequest.php +++ b/src/Requests/TimeSpentTotal/GetTimeSpentTotalsRequest.php @@ -1,14 +1,16 @@ data ? $this->data->toJsonApi() : []; + return $this->data ? ['data' => $this->data->toJsonApi()] : []; } } diff --git a/src/Requests/User/PostUsersRequest.php b/src/Requests/User/PostUsersRequest.php index ae43f6b..53dd639 100644 --- a/src/Requests/User/PostUsersRequest.php +++ b/src/Requests/User/PostUsersRequest.php @@ -1,15 +1,17 @@ data ? $this->data->toJsonApi() : []; + return $this->data ? ['data' => $this->data->toJsonApi()] : []; } } diff --git a/src/Requests/UserCustomerHoursAggregate/GetUserCustomerHoursAggregatesRequest.php b/src/Requests/UserCustomerHoursAggregate/GetUserCustomerHoursAggregatesRequest.php index 61c1239..f75df89 100644 --- a/src/Requests/UserCustomerHoursAggregate/GetUserCustomerHoursAggregatesRequest.php +++ b/src/Requests/UserCustomerHoursAggregate/GetUserCustomerHoursAggregatesRequest.php @@ -1,14 +1,16 @@ state([ + 'title' => 'Project Budget', + ]) + ->make(); + + expect($budget) + ->toBeInstanceOf(Budget::class) + ->title->toBe('Project Budget') + ->totalPrice->toBeString() + ->customerId->toBeString(); +}); + +test('it can create multiple budgets using factory', function () { + $budgets = Budget::factory()->count(3)->make(); + + expect($budgets)->toHaveCount(3); + $budgets->each(fn ($budget) => expect($budget)->toBeInstanceOf(Budget::class)); +}); + +test('it can override attributes using state', function () { + $budget = Budget::factory()->state([ + 'title' => 'Custom Title', + 'totalPrice' => '1234.56', + ])->make(); + + expect($budget) + ->title->toBe('Custom Title') + ->totalPrice->toBe('1234.56'); +}); + +test('it can chain state calls', function () { + $budget = Budget::factory() + ->state(['title' => 'First']) + ->state(['totalPrice' => '100.00']) + ->make(); + + expect($budget) + ->title->toBe('First') + ->totalPrice->toBe('100.00'); +}); + +test('it converts to json api format correctly', function () { + $budget = Budget::factory()->state([ + 'id' => 'test-123', + 'title' => 'Test Budget', + 'totalPrice' => '999.99', + ])->make(); + + $jsonApi = $budget->toJsonApi(); + + expect($jsonApi) + ->toHaveKeys(['type', 'id', 'attributes']) + ->type->toBe('budgets') + ->id->toBe('test-123') + ->attributes->toHaveKey('title', 'Test Budget') + ->toHaveKey('totalPrice', '999.99'); +}); + +test('it can generate multiple budgets with unique UUID IDs', function () { + $budgets = Budget::factory()->withId()->count(3)->make(); + + expect($budgets)->toHaveCount(3); + + $ids = $budgets->pluck('id'); + + // All budgets should have IDs + $budgets->each(fn ($budget) => expect($budget->id)->toBeString()->not->toBeEmpty()); + + // All IDs should be unique + expect($ids->unique())->toHaveCount(3); +}); diff --git a/tests/Factories/MockingWithFactoriesTest.php b/tests/Factories/MockingWithFactoriesTest.php new file mode 100644 index 0000000..953545c --- /dev/null +++ b/tests/Factories/MockingWithFactoriesTest.php @@ -0,0 +1,126 @@ +state([ + 'id' => 'mock-123', + 'title' => 'Mocked Budget', + ])->make(); + + $mockClient = new MockClient([ + GetBudgetsRequest::class => MockResponse::make([ + 'data' => [$budget->toJsonApi()], + ], 200), + ]); + + $connector = new TimaticConnector; + $connector->withMockClient($mockClient); + + $response = $connector->send(new GetBudgetsRequest); + $dtos = $response->dto(); + + expect($dtos)->toBeInstanceOf(\Illuminate\Support\Collection::class); + + expect($dtos->first()) + ->toBeInstanceOf(Budget::class) + ->id->toBe('mock-123') + ->title->toBe('Mocked Budget'); +}); + +test('it can mock a collection response using factories', function () { + $budgets = Budget::factory()->withId()->count(3)->make(); + + $mockClient = new MockClient([ + GetBudgetsRequest::class => MockResponse::make([ + 'data' => $budgets->map(fn ($budget) => $budget->toJsonApi())->toArray(), + ], 200), + ]); + + $connector = new TimaticConnector; + $connector->withMockClient($mockClient); + + $response = $connector->send(new GetBudgetsRequest); + $dtos = $response->dto(); + + expect($dtos)->toHaveCount(3); + $dtos->each(fn ($dto) => expect($dto)->toBeInstanceOf(Budget::class)); +}); + +test('it can mock a POST request to create a budget using factory', function () { + // Create a budget to send + $budgetToCreate = Budget::factory()->state([ + 'title' => 'New Budget', + 'totalPrice' => '5000.00', + 'customerId' => 'customer-123', + ])->make(); + + // Mock the response with an ID + $createdBudget = Budget::factory()->state([ + 'id' => 'created-456', + 'title' => 'New Budget', + 'totalPrice' => '5000.00', + 'customerId' => 'customer-123', + ])->make(); + + $mockClient = new MockClient([ + PostBudgetsRequest::class => MockResponse::make([ + 'data' => $createdBudget->toJsonApi(), + ], 201), + ]); + + $connector = new TimaticConnector; + $connector->withMockClient($mockClient); + + // Send POST request + $response = $connector->send(new PostBudgetsRequest($budgetToCreate)); + + // Assert the request body was sent correctly + $mockClient->assertSent(function (\Saloon\Http\Request $request) { + $body = $request->body()->all(); + + return $body['data']['type'] === 'budgets' + && $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') + ->customerId->toBe('customer-123'); +}); + +test('it preserves all factory-generated attributes in json api format', function () { + $budget = Budget::factory()->state([ + 'id' => '123', + 'title' => 'Full Budget', + 'totalPrice' => '5000.00', + 'customerId' => 'customer-456', + 'budgetTypeId' => 'type-789', + 'showToCustomer' => true, + 'isArchived' => false, + ])->make(); + + $jsonApi = $budget->toJsonApi(); + + expect($jsonApi) + ->attributes->toHaveKey('title', 'Full Budget') + ->toHaveKey('totalPrice', '5000.00') + ->toHaveKey('customerId', 'customer-456') + ->toHaveKey('budgetTypeId', 'type-789') + ->toHaveKey('showToCustomer', true) + ->toHaveKey('isArchived', false); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 79a7abd..fe7c356 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,5 +1,5 @@ in(__DIR__); diff --git a/tests/Requests/ApproveTest.php b/tests/Requests/ApproveTest.php index 276517a..7fa7d21 100644 --- a/tests/Requests/ApproveTest.php +++ b/tests/Requests/ApproveTest.php @@ -1,12 +1,14 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the postOvertimeApprove method in the Approve resource', function () { @@ -15,11 +17,12 @@ ]); // Create DTO with sample data - $dto = new \Timatic\SDK\Dto\Approve; - $dto->entryId = 'entry_id-123'; - $dto->overtimeTypeId = 'overtime_type_id-123'; - $dto->startedAt = \Carbon\Carbon::parse('2025-01-15T10:30:00Z'); - $dto->endedAt = \Carbon\Carbon::parse('2025-01-15T10:30:00Z'); + $dto = \Timatic\Dto\Approve::factory()->state([ + 'entryId' => 'entry_id-123', + 'overtimeTypeId' => 'overtime_type_id-123', + 'startedAt' => \Carbon\Carbon::parse('2025-01-15T10:30:00Z'), + 'endedAt' => \Carbon\Carbon::parse('2025-01-15T10:30:00Z'), + ])->make(); $request = new PostOvertimeApproveRequest(overtimeId: 'test string', data: $dto); $this->timaticConnector->send($request); diff --git a/tests/Requests/BudgetTest.php b/tests/Requests/BudgetTest.php index 898feca..4bb2b22 100644 --- a/tests/Requests/BudgetTest.php +++ b/tests/Requests/BudgetTest.php @@ -1,17 +1,19 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the getBudgets method in the Budget resource', function () { @@ -109,11 +111,12 @@ ]); // Create DTO with sample data - $dto = new \Timatic\SDK\Dto\Budget; - $dto->budgetTypeId = 'budget_type_id-123'; - $dto->customerId = 'customer_id-123'; - $dto->showToCustomer = true; - $dto->changeId = 'change_id-123'; + $dto = \Timatic\Dto\Budget::factory()->state([ + 'budgetTypeId' => 'budget_type_id-123', + 'customerId' => 'customer_id-123', + 'showToCustomer' => true, + 'changeId' => 'change_id-123', + ])->make(); $request = new PostBudgetsRequest($dto); $this->timaticConnector->send($request); @@ -210,11 +213,12 @@ ]); // Create DTO with sample data - $dto = new \Timatic\SDK\Dto\Budget; - $dto->budgetTypeId = 'budget_type_id-123'; - $dto->customerId = 'customer_id-123'; - $dto->showToCustomer = true; - $dto->changeId = 'change_id-123'; + $dto = \Timatic\Dto\Budget::factory()->state([ + 'budgetTypeId' => 'budget_type_id-123', + 'customerId' => 'customer_id-123', + 'showToCustomer' => true, + 'changeId' => 'change_id-123', + ])->make(); $request = new PatchBudgetRequest(budgetId: 'test string', data: $dto); $this->timaticConnector->send($request); diff --git a/tests/Requests/BudgetTimeSpentTotalTest.php b/tests/Requests/BudgetTimeSpentTotalTest.php index 86ec251..e94b8d4 100644 --- a/tests/Requests/BudgetTimeSpentTotalTest.php +++ b/tests/Requests/BudgetTimeSpentTotalTest.php @@ -1,13 +1,15 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the getBudgetTimeSpentTotals method in the BudgetTimeSpentTotal resource', function () { diff --git a/tests/Requests/BudgetTypeTest.php b/tests/Requests/BudgetTypeTest.php index f8a836a..244249b 100644 --- a/tests/Requests/BudgetTypeTest.php +++ b/tests/Requests/BudgetTypeTest.php @@ -1,11 +1,13 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the getBudgetTypes method in the BudgetType resource', function () { diff --git a/tests/Requests/CorrectionTest.php b/tests/Requests/CorrectionTest.php index 5c76d63..88af97d 100644 --- a/tests/Requests/CorrectionTest.php +++ b/tests/Requests/CorrectionTest.php @@ -1,13 +1,15 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the postCorrections method in the Correction resource', function () { @@ -16,8 +18,9 @@ ]); // Create DTO with sample data - $dto = new \Timatic\SDK\Dto\Correction; - $dto->name = 'test value'; + $dto = \Timatic\Dto\Correction::factory()->state([ + 'name' => 'test value', + ])->make(); $request = new PostCorrectionsRequest($dto); $this->timaticConnector->send($request); @@ -39,8 +42,9 @@ ]); // Create DTO with sample data - $dto = new \Timatic\SDK\Dto\Correction; - $dto->name = 'test value'; + $dto = \Timatic\Dto\Correction::factory()->state([ + 'name' => 'test value', + ])->make(); $request = new PatchCorrectionRequest(correctionId: 'test string', data: $dto); $this->timaticConnector->send($request); diff --git a/tests/Requests/CustomerTest.php b/tests/Requests/CustomerTest.php index 84803d3..b23aba8 100644 --- a/tests/Requests/CustomerTest.php +++ b/tests/Requests/CustomerTest.php @@ -1,16 +1,18 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the getCustomers method in the Customer resource', function () { @@ -74,11 +76,12 @@ ]); // Create DTO with sample data - $dto = new \Timatic\SDK\Dto\Customer; - $dto->externalId = 'external_id-123'; - $dto->name = 'test name'; - $dto->hourlyRate = 'test value'; - $dto->accountManagerUserId = 'account_manager_user_id-123'; + $dto = \Timatic\Dto\Customer::factory()->state([ + 'externalId' => 'external_id-123', + 'name' => 'test name', + 'hourlyRate' => 'test value', + 'accountManagerUserId' => 'account_manager_user_id-123', + ])->make(); $request = new PostCustomersRequest($dto); $this->timaticConnector->send($request); @@ -155,11 +158,12 @@ ]); // Create DTO with sample data - $dto = new \Timatic\SDK\Dto\Customer; - $dto->externalId = 'external_id-123'; - $dto->name = 'test name'; - $dto->hourlyRate = 'test value'; - $dto->accountManagerUserId = 'account_manager_user_id-123'; + $dto = \Timatic\Dto\Customer::factory()->state([ + 'externalId' => 'external_id-123', + 'name' => 'test name', + 'hourlyRate' => 'test value', + 'accountManagerUserId' => 'account_manager_user_id-123', + ])->make(); $request = new PatchCustomerRequest(customerId: 'test string', data: $dto); $this->timaticConnector->send($request); diff --git a/tests/Requests/DailyProgressTest.php b/tests/Requests/DailyProgressTest.php index 8c66689..3d515bd 100644 --- a/tests/Requests/DailyProgressTest.php +++ b/tests/Requests/DailyProgressTest.php @@ -1,12 +1,14 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the getDailyProgresses method in the DailyProgress resource', function () { diff --git a/tests/Requests/EntrySuggestionTest.php b/tests/Requests/EntrySuggestionTest.php index 6d2d2b7..4741297 100644 --- a/tests/Requests/EntrySuggestionTest.php +++ b/tests/Requests/EntrySuggestionTest.php @@ -1,14 +1,16 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the getEntrySuggestions method in the EntrySuggestion resource', function () { diff --git a/tests/Requests/EntryTest.php b/tests/Requests/EntryTest.php index 4b9889b..2a29ada 100644 --- a/tests/Requests/EntryTest.php +++ b/tests/Requests/EntryTest.php @@ -1,17 +1,19 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the getEntries method in the Entry resource', function () { @@ -142,11 +144,12 @@ ]); // Create DTO with sample data - $dto = new \Timatic\SDK\Dto\Entry; - $dto->ticketId = 'ticket_id-123'; - $dto->ticketNumber = 'test value'; - $dto->ticketTitle = 'test value'; - $dto->ticketType = 'test value'; + $dto = \Timatic\Dto\Entry::factory()->state([ + 'ticketId' => 'ticket_id-123', + 'ticketNumber' => 'test value', + 'ticketTitle' => 'test value', + 'ticketType' => 'test value', + ])->make(); $request = new PostEntriesRequest($dto); $this->timaticConnector->send($request); @@ -265,11 +268,12 @@ ]); // Create DTO with sample data - $dto = new \Timatic\SDK\Dto\Entry; - $dto->ticketId = 'ticket_id-123'; - $dto->ticketNumber = 'test value'; - $dto->ticketTitle = 'test value'; - $dto->ticketType = 'test value'; + $dto = \Timatic\Dto\Entry::factory()->state([ + 'ticketId' => 'ticket_id-123', + 'ticketNumber' => 'test value', + 'ticketTitle' => 'test value', + 'ticketType' => 'test value', + ])->make(); $request = new PatchEntryRequest(entryId: 'test string', data: $dto); $this->timaticConnector->send($request); diff --git a/tests/Requests/EventTest.php b/tests/Requests/EventTest.php index 6ca98b6..10f378e 100644 --- a/tests/Requests/EventTest.php +++ b/tests/Requests/EventTest.php @@ -1,12 +1,14 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the postEvents method in the Event resource', function () { @@ -15,11 +17,12 @@ ]); // Create DTO with sample data - $dto = new \Timatic\SDK\Dto\Event; - $dto->userId = 'user_id-123'; - $dto->budgetId = 'budget_id-123'; - $dto->ticketId = 'ticket_id-123'; - $dto->sourceId = 'source_id-123'; + $dto = \Timatic\Dto\Event::factory()->state([ + 'userId' => 'user_id-123', + 'budgetId' => 'budget_id-123', + 'ticketId' => 'ticket_id-123', + 'sourceId' => 'source_id-123', + ])->make(); $request = new PostEventsRequest($dto); $this->timaticConnector->send($request); diff --git a/tests/Requests/MarkAsExportedTest.php b/tests/Requests/MarkAsExportedTest.php index 94e238e..4bef4b1 100644 --- a/tests/Requests/MarkAsExportedTest.php +++ b/tests/Requests/MarkAsExportedTest.php @@ -1,12 +1,14 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the postOvertimeMarkAsExported method in the MarkAsExported resource', function () { @@ -15,11 +17,12 @@ ]); // Create DTO with sample data - $dto = new \Timatic\SDK\Dto\MarkAsExported; - $dto->entryId = 'entry_id-123'; - $dto->overtimeTypeId = 'overtime_type_id-123'; - $dto->startedAt = \Carbon\Carbon::parse('2025-01-15T10:30:00Z'); - $dto->endedAt = \Carbon\Carbon::parse('2025-01-15T10:30:00Z'); + $dto = \Timatic\Dto\MarkAsExported::factory()->state([ + 'entryId' => 'entry_id-123', + 'overtimeTypeId' => 'overtime_type_id-123', + 'startedAt' => \Carbon\Carbon::parse('2025-01-15T10:30:00Z'), + 'endedAt' => \Carbon\Carbon::parse('2025-01-15T10:30:00Z'), + ])->make(); $request = new PostOvertimeMarkAsExportedRequest(overtimeId: 'test string', data: $dto); $this->timaticConnector->send($request); diff --git a/tests/Requests/MarkAsInvoicedTest.php b/tests/Requests/MarkAsInvoicedTest.php index 0982dd2..0e0e896 100644 --- a/tests/Requests/MarkAsInvoicedTest.php +++ b/tests/Requests/MarkAsInvoicedTest.php @@ -1,12 +1,14 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the postEntryMarkAsInvoiced method in the MarkAsInvoiced resource', function () { @@ -15,11 +17,12 @@ ]); // Create DTO with sample data - $dto = new \Timatic\SDK\Dto\MarkAsInvoiced; - $dto->ticketId = 'ticket_id-123'; - $dto->ticketNumber = 'test value'; - $dto->ticketTitle = 'test value'; - $dto->ticketType = 'test value'; + $dto = \Timatic\Dto\MarkAsInvoiced::factory()->state([ + 'ticketId' => 'ticket_id-123', + 'ticketNumber' => 'test value', + 'ticketTitle' => 'test value', + 'ticketType' => 'test value', + ])->make(); $request = new PostEntryMarkAsInvoicedRequest(entryId: 'test string', data: $dto); $this->timaticConnector->send($request); diff --git a/tests/Requests/OvertimeTest.php b/tests/Requests/OvertimeTest.php index 368e45c..30a6482 100644 --- a/tests/Requests/OvertimeTest.php +++ b/tests/Requests/OvertimeTest.php @@ -1,13 +1,15 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the getOvertimes method in the Overtime resource', function () { diff --git a/tests/Requests/TeamTest.php b/tests/Requests/TeamTest.php index 0f3b254..708ca34 100644 --- a/tests/Requests/TeamTest.php +++ b/tests/Requests/TeamTest.php @@ -1,16 +1,18 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the getTeams method in the Team resource', function () { @@ -58,9 +60,10 @@ ]); // Create DTO with sample data - $dto = new \Timatic\SDK\Dto\Team; - $dto->externalId = 'external_id-123'; - $dto->name = 'test name'; + $dto = \Timatic\Dto\Team::factory()->state([ + 'externalId' => 'external_id-123', + 'name' => 'test name', + ])->make(); $request = new PostTeamsRequest($dto); $this->timaticConnector->send($request); @@ -131,9 +134,10 @@ ]); // Create DTO with sample data - $dto = new \Timatic\SDK\Dto\Team; - $dto->externalId = 'external_id-123'; - $dto->name = 'test name'; + $dto = \Timatic\Dto\Team::factory()->state([ + 'externalId' => 'external_id-123', + 'name' => 'test name', + ])->make(); $request = new PatchTeamRequest(teamId: 'test string', data: $dto); $this->timaticConnector->send($request); diff --git a/tests/Requests/TimeSpentTotalTest.php b/tests/Requests/TimeSpentTotalTest.php index 5bffc14..305c152 100644 --- a/tests/Requests/TimeSpentTotalTest.php +++ b/tests/Requests/TimeSpentTotalTest.php @@ -1,13 +1,15 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the getTimeSpentTotals method in the TimeSpentTotal resource', function () { diff --git a/tests/Requests/UserCustomerHoursAggregateTest.php b/tests/Requests/UserCustomerHoursAggregateTest.php index d425bae..368bf61 100644 --- a/tests/Requests/UserCustomerHoursAggregateTest.php +++ b/tests/Requests/UserCustomerHoursAggregateTest.php @@ -1,12 +1,14 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the getUserCustomerHoursAggregates method in the UserCustomerHoursAggregate resource', function () { diff --git a/tests/Requests/UserTest.php b/tests/Requests/UserTest.php index 30ece34..6fdea90 100644 --- a/tests/Requests/UserTest.php +++ b/tests/Requests/UserTest.php @@ -1,16 +1,18 @@ timaticConnector = new Timatic\SDK\TimaticConnector; + $this->timaticConnector = new Timatic\TimaticConnector; }); it('calls the getUsers method in the User resource', function () { @@ -68,9 +70,10 @@ ]); // Create DTO with sample data - $dto = new \Timatic\SDK\Dto\User; - $dto->externalId = 'external_id-123'; - $dto->email = 'test@example.com'; + $dto = \Timatic\Dto\User::factory()->state([ + 'externalId' => 'external_id-123', + 'email' => 'test@example.com', + ])->make(); $request = new PostUsersRequest($dto); $this->timaticConnector->send($request); @@ -141,9 +144,10 @@ ]); // Create DTO with sample data - $dto = new \Timatic\SDK\Dto\User; - $dto->externalId = 'external_id-123'; - $dto->email = 'test@example.com'; + $dto = \Timatic\Dto\User::factory()->state([ + 'externalId' => 'external_id-123', + 'email' => 'test@example.com', + ])->make(); $request = new PatchUserRequest(userId: 'test string', data: $dto); $this->timaticConnector->send($request); diff --git a/tests/TestCase.php b/tests/TestCase.php index dd8abe2..fcb0fb7 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,11 +1,11 @@