diff --git a/.gitignore b/.gitignore index 723f82e..aa55e3f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,6 @@ composer.lock .phpunit.result.cache .DS_Store -openapi.json +openapi.json.backup .claude/settings.local.json +openapi.json diff --git a/factories/ApproveFactory.php b/factories/ActivityFactory.php similarity index 52% rename from factories/ApproveFactory.php rename to factories/ActivityFactory.php index 05e2aa2..b9da2d3 100644 --- a/factories/ApproveFactory.php +++ b/factories/ActivityFactory.php @@ -5,21 +5,23 @@ namespace Timatic\Factories; use Carbon\Carbon; -use Timatic\Dto\Approve; +use Timatic\Dto\Activity; -class ApproveFactory extends Factory +class ActivityFactory extends Factory { protected function definition(): array { return [ - 'entryId' => $this->faker->uuid(), - 'overtimeTypeId' => $this->faker->uuid(), + 'sourceId' => $this->faker->uuid(), + 'eventTypeId' => $this->faker->uuid(), + 'customerId' => $this->faker->uuid(), + 'ticketId' => $this->faker->uuid(), + 'entrySuggestionId' => $this->faker->numberBetween(1, 1000), '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)), + 'title' => $this->faker->sentence(), + 'description' => $this->faker->sentence(), + 'isInternal' => $this->faker->boolean(), 'createdAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), 'updatedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), ]; @@ -27,6 +29,6 @@ protected function definition(): array protected function modelClass(): string { - return Approve::class; + return Activity::class; } } diff --git a/factories/BudgetFactory.php b/factories/BudgetFactory.php index 0fdbad5..1276717 100644 --- a/factories/BudgetFactory.php +++ b/factories/BudgetFactory.php @@ -13,7 +13,7 @@ protected function definition(): array { return [ 'budgetTypeId' => $this->faker->uuid(), - 'customerId' => $this->faker->uuid(), + 'customerId' => $this->faker->numberBetween(1, 1000), 'showToCustomer' => $this->faker->boolean(), 'changeId' => $this->faker->uuid(), 'contractId' => $this->faker->uuid(), @@ -27,7 +27,7 @@ protected function definition(): array '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(), + 'supervisorUserId' => $this->faker->numberBetween(1, 1000), ]; } diff --git a/factories/BudgetTimeSpentTotalFactory.php b/factories/BudgetTimeSpentTotalFactory.php deleted file mode 100644 index 31e918c..0000000 --- a/factories/BudgetTimeSpentTotalFactory.php +++ /dev/null @@ -1,27 +0,0 @@ - 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 index 7d279a5..cd6e6a4 100644 --- a/factories/BudgetTypeFactory.php +++ b/factories/BudgetTypeFactory.php @@ -14,9 +14,9 @@ protected function definition(): array 'title' => $this->faker->sentence(), 'isArchived' => $this->faker->boolean(), 'hasChangeTicket' => $this->faker->boolean(), - 'renewalFrequencies' => $this->faker->word(), + 'renewalFrequencies' => [], 'hasSupervisor' => $this->faker->boolean(), - 'hasContractId' => $this->faker->uuid(), + 'hasContractId' => $this->faker->boolean(), 'hasTotalPrice' => $this->faker->boolean(), 'ticketIsRequired' => $this->faker->boolean(), 'defaultTitle' => $this->faker->sentence(), diff --git a/factories/CustomerFactory.php b/factories/CustomerFactory.php index 25b5fe8..57b325f 100644 --- a/factories/CustomerFactory.php +++ b/factories/CustomerFactory.php @@ -15,7 +15,7 @@ protected function definition(): array 'externalId' => $this->faker->uuid(), 'name' => $this->faker->name(), 'hourlyRate' => number_format($this->faker->randomFloat(2, 50, 150), 2, '.', ''), - 'accountManagerUserId' => $this->faker->uuid(), + 'accountManagerUserId' => $this->faker->numberBetween(1, 1000), 'createdAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), 'updatedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), ]; diff --git a/factories/DailyProgressFactory.php b/factories/DailyProgressFactory.php deleted file mode 100644 index a2dfa19..0000000 --- a/factories/DailyProgressFactory.php +++ /dev/null @@ -1,25 +0,0 @@ - $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 index 1338443..641f248 100644 --- a/factories/EntryFactory.php +++ b/factories/EntryFactory.php @@ -16,17 +16,17 @@ protected function definition(): array 'ticketNumber' => $this->faker->word(), 'ticketTitle' => $this->faker->sentence(), 'ticketType' => $this->faker->word(), - 'customerId' => $this->faker->uuid(), + 'customerId' => $this->faker->numberBetween(1, 1000), 'customerName' => $this->faker->company(), - 'hourlyRate' => number_format($this->faker->randomFloat(2, 50, 150), 2, '.', ''), + 'hourlyRate' => $this->faker->randomFloat(2, 0, 1000), 'hadEmergencyShift' => $this->faker->boolean(), - 'budgetId' => $this->faker->uuid(), - 'isPaidPerHour' => $this->faker->boolean(), + 'budgetId' => $this->faker->numberBetween(1, 1000), + 'isPaidPerHour' => $this->faker->word(), 'minutesSpent' => $this->faker->numberBetween(15, 480), - 'userId' => $this->faker->uuid(), + 'userId' => $this->faker->numberBetween(1, 1000), 'userEmail' => $this->faker->safeEmail(), 'userFullName' => $this->faker->name(), - 'createdByUserId' => $this->faker->uuid(), + 'createdByUserId' => $this->faker->numberBetween(1, 1000), 'createdByUserEmail' => $this->faker->safeEmail(), 'createdByUserFullName' => $this->faker->company(), 'entryType' => $this->faker->word(), diff --git a/factories/EntrySuggestionFactory.php b/factories/EntrySuggestionFactory.php index 26fe97a..a95b472 100644 --- a/factories/EntrySuggestionFactory.php +++ b/factories/EntrySuggestionFactory.php @@ -15,13 +15,13 @@ protected function definition(): array 'ticketId' => $this->faker->uuid(), 'ticketNumber' => $this->faker->word(), 'customerId' => $this->faker->uuid(), - 'userId' => $this->faker->uuid(), + 'userId' => $this->faker->numberBetween(1, 1000), '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(), + 'budgetId' => $this->faker->numberBetween(1, 1000), ]; } diff --git a/factories/EventFactory.php b/factories/EventFactory.php index d9818a6..3c62312 100644 --- a/factories/EventFactory.php +++ b/factories/EventFactory.php @@ -12,8 +12,8 @@ class EventFactory extends Factory protected function definition(): array { return [ - 'userId' => $this->faker->uuid(), - 'budgetId' => $this->faker->uuid(), + 'userId' => $this->faker->numberBetween(1, 1000), + 'budgetId' => $this->faker->numberBetween(1, 1000), 'ticketId' => $this->faker->uuid(), 'sourceId' => $this->faker->uuid(), 'ticketNumber' => $this->faker->word(), @@ -26,7 +26,7 @@ protected function definition(): array '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(), + 'isInternal' => $this->faker->boolean(), ]; } diff --git a/factories/MarkAsExportedFactory.php b/factories/MarkAsExportedFactory.php deleted file mode 100644 index a5a5e59..0000000 --- a/factories/MarkAsExportedFactory.php +++ /dev/null @@ -1,32 +0,0 @@ - $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 deleted file mode 100644 index bf9e2f2..0000000 --- a/factories/MarkAsInvoicedFactory.php +++ /dev/null @@ -1,49 +0,0 @@ - $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 index 48b9376..a06ff2e 100644 --- a/factories/OvertimeFactory.php +++ b/factories/OvertimeFactory.php @@ -12,13 +12,13 @@ class OvertimeFactory extends Factory protected function definition(): array { return [ - 'entryId' => $this->faker->uuid(), + 'entryId' => $this->faker->numberBetween(1, 1000), '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(), + 'approvedByUserId' => $this->faker->numberBetween(1, 1000), '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)), diff --git a/factories/OvertimeTypeFactory.php b/factories/OvertimeTypeFactory.php new file mode 100644 index 0000000..027e82b --- /dev/null +++ b/factories/OvertimeTypeFactory.php @@ -0,0 +1,22 @@ + $this->faker->sentence(), + ]; + } + + protected function modelClass(): string + { + return OvertimeType::class; + } +} diff --git a/factories/ExportMailFactory.php b/factories/PeriodFactory.php similarity index 66% rename from factories/ExportMailFactory.php rename to factories/PeriodFactory.php index 2a7a6a3..a676346 100644 --- a/factories/ExportMailFactory.php +++ b/factories/PeriodFactory.php @@ -4,9 +4,9 @@ namespace Timatic\Factories; -use Timatic\Dto\ExportMail; +use Timatic\Dto\Period; -class ExportMailFactory extends Factory +class PeriodFactory extends Factory { protected function definition(): array { @@ -16,6 +16,6 @@ protected function definition(): array protected function modelClass(): string { - return ExportMail::class; + return Period::class; } } diff --git a/factories/PermissionFactory.php b/factories/PermissionFactory.php new file mode 100644 index 0000000..cda874a --- /dev/null +++ b/factories/PermissionFactory.php @@ -0,0 +1,22 @@ + [], + ]; + } + + protected function modelClass(): string + { + return Permission::class; + } +} diff --git a/factories/SourceFactory.php b/factories/SourceFactory.php new file mode 100644 index 0000000..22930fb --- /dev/null +++ b/factories/SourceFactory.php @@ -0,0 +1,23 @@ + $this->faker->word(), + 'title' => $this->faker->sentence(), + ]; + } + + protected function modelClass(): string + { + return Source::class; + } +} diff --git a/factories/TimeSpentTotalFactory.php b/factories/TimeSpentTotalFactory.php deleted file mode 100644 index 1c7fbd9..0000000 --- a/factories/TimeSpentTotalFactory.php +++ /dev/null @@ -1,28 +0,0 @@ - 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 deleted file mode 100644 index de87f41..0000000 --- a/factories/UserCustomerHoursAggregateFactory.php +++ /dev/null @@ -1,26 +0,0 @@ - $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 index 3649d73..1169e42 100644 --- a/factories/UserFactory.php +++ b/factories/UserFactory.php @@ -4,6 +4,7 @@ namespace Timatic\Factories; +use Carbon\Carbon; use Timatic\Dto\User; class UserFactory extends Factory @@ -15,7 +16,10 @@ protected function definition(): array 'email' => $this->faker->safeEmail(), 'givenName' => $this->faker->company(), 'familyName' => $this->faker->company(), - 'teamId' => $this->faker->uuid(), + 'isImpersonated' => $this->faker->boolean(), + 'impersonatedById' => $this->faker->numberBetween(1, 1000), + 'createdAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), + 'updatedAt' => Carbon::now()->subDays($this->faker->numberBetween(0, 365)), ]; } diff --git a/generator/JsonApiConnectorGenerator.php b/generator/JsonApiConnectorGenerator.php index 2d17c2c..2b98773 100644 --- a/generator/JsonApiConnectorGenerator.php +++ b/generator/JsonApiConnectorGenerator.php @@ -35,8 +35,16 @@ protected function generateConnectorClass(ApiSpecification $specification): ?Php $namespace->addUse(JsonApiPaginator::class); $namespace->addUse(TimaticResponse::class); - // Keep the empty constructor for test compatibility - // (PestTestGenerator needs it) + // Remove any constructor parameters added by parent generator + // (we handle auth via config in defaultHeaders) + if ($classType->hasMethod('__construct')) { + $constructor = $classType->getMethod('__construct'); + // Clear all parameters and body - we don't need constructor auth + foreach ($constructor->getParameters() as $param) { + $constructor->removeParameter($param->getName()); + } + $constructor->setBody(''); + } // Override resolveBaseUrl to use Laravel config $resolveBaseUrl = $classType->getMethod('resolveBaseUrl'); diff --git a/generator/JsonApiDtoGenerator.php b/generator/JsonApiDtoGenerator.php index ca72a04..7279e0e 100644 --- a/generator/JsonApiDtoGenerator.php +++ b/generator/JsonApiDtoGenerator.php @@ -10,21 +10,35 @@ use Crescat\SaloonSdkGenerator\Generator; use Crescat\SaloonSdkGenerator\Helpers\NameHelper; use Crescat\SaloonSdkGenerator\Helpers\Utils; +use Illuminate\Support\Collection; use Illuminate\Support\Str; use Nette\PhpGenerator\ClassType; +use Nette\PhpGenerator\Literal; use Nette\PhpGenerator\PhpFile; use Timatic\Hydration\Attributes\DateTime; use Timatic\Hydration\Attributes\Property; +use Timatic\Hydration\Attributes\Relationship; use Timatic\Hydration\Model; +use Timatic\Hydration\RelationType; class JsonApiDtoGenerator extends Generator { protected array $generated = []; + protected ApiSpecification $specification; + public function generate(ApiSpecification $specification): PhpFile|array { + $this->specification = $specification; + if ($specification->components) { foreach ($specification->components->schemas as $className => $schema) { + // Skip schemas that aren't useful + if (str_ends_with($className, 'Identifier') || + str_ends_with($className, 'Request')) { + continue; + } + $this->generateModelClass(NameHelper::safeClassName($className), $schema); } } @@ -52,9 +66,17 @@ protected function generateModelClass(string $className, Schema $schema): PhpFil // Add properties to the class foreach ($properties as $propertyName => $propertySpec) { + // Skip 'id' and 'type' as they're already defined in the base Model class + if (in_array($propertyName, ['id', 'type'])) { + continue; + } + $this->addPropertyToClass($classType, $namespace, $propertyName, $propertySpec); } + // Add relationship properties + $this->addRelationshipProperties($classType, $namespace, $schema); + // Add imports $namespace->addUse(Model::class); $namespace->addUse(Property::class); @@ -155,15 +177,41 @@ protected function convertOpenApiTypeToPhp(Schema|Reference $schema): string return Str::afterLast($schema->getReference(), '/'); } + // Handle anyOf, oneOf, allOf + if (isset($schema->anyOf) && is_array($schema->anyOf)) { + return $this->handleCompositeType($schema->anyOf); + } + + if (isset($schema->oneOf) && is_array($schema->oneOf)) { + return $this->handleCompositeType($schema->oneOf); + } + + if (isset($schema->allOf) && is_array($schema->allOf)) { + return $this->handleCompositeType($schema->allOf); + } + + // Handle array union types if (is_array($schema->type)) { - return collect($schema->type)->map(fn ($type) => $this->mapType($type))->implode('|'); + return collect($schema->type) + ->map(fn ($type) => $this->mapType($type)) + ->implode('|'); + } + + // Handle simple types (or null) + if ($schema->type !== null) { + return $this->mapType($schema->type, $schema->format); } - return $this->mapType($schema->type, $schema->format); + // Fallback for schemas without type information + return 'mixed'; } - protected function mapType(string $type, ?string $format = null): string + protected function mapType(?string $type, ?string $format = null): string { + if ($type === null) { + return 'mixed'; + } + return match ($type) { 'integer' => 'int', 'string' => 'string', @@ -172,9 +220,179 @@ protected function mapType(string $type, ?string $format = null): string 'number' => match ($format) { 'float' => 'float', 'int32', 'int64' => 'int', + default => 'float', // Default for number without format }, 'array' => 'array', 'null' => 'null', + default => 'mixed', // Fallback for unknown types }; } + + /** + * Handle anyOf, oneOf, allOf composite types + * Returns a PHP union type string + */ + protected function handleCompositeType(array $types): string + { + $phpTypes = []; + + foreach ($types as $typeSchema) { + if ($typeSchema instanceof Reference) { + $phpTypes[] = Str::afterLast($typeSchema->getReference(), '/'); + } elseif ($typeSchema instanceof Schema) { + if ($typeSchema->type !== null) { + if (is_array($typeSchema->type)) { + // Nested union + foreach ($typeSchema->type as $t) { + $phpTypes[] = $this->mapType($t, $typeSchema->format ?? null); + } + } else { + $phpTypes[] = $this->mapType($typeSchema->type, $typeSchema->format ?? null); + } + } + } + } + + // Remove duplicates and return union + return collect($phpTypes) + ->unique() + ->filter() + ->implode('|') ?: 'mixed'; + } + + /** + * Add relationship properties to the DTO class + */ + protected function addRelationshipProperties(ClassType $classType, $namespace, Schema $schema): void + { + // Check if schema has relationships + if (! isset($schema->properties['relationships'])) { + return; + } + + $relationships = $schema->properties['relationships']; + + if (! isset($relationships->properties) || ! is_array($relationships->properties)) { + return; + } + + // Import required classes + $namespace->addUse(Relationship::class); + $namespace->addUse(RelationType::class); + $namespace->addUse(Collection::class); + + foreach ($relationships->properties as $relationName => $relationSpec) { + $relationType = $this->detectRelationType($relationName, $relationSpec); + $relatedModel = $this->detectRelatedModel($relationName, $relationSpec); + + if (! $relatedModel) { + // Skip if we can't determine the related model + echo " ⚠️ Skipping relationship '{$relationName}' - model not found\n"; + + continue; + } + + // Check if related model schema exists + if (! isset($this->specification->components->schemas[$relatedModel])) { + echo " ⚠️ Skipping relationship '{$relationName}' - model '{$relatedModel}' not found in schemas\n"; + + continue; + } + + // Import related model + $namespace->addUse("Timatic\\Dto\\{$relatedModel}"); + + // Create property + $property = $classType->addProperty($relationName) + ->setPublic() + ->setNullable(true) + ->setValue(null); // Add default value + + // Set type based on relationship type + // Use FQN for Collection and related model to avoid backslash prefix + if ($relationType === 'Many') { + $property->setType('null|\\Illuminate\\Support\\Collection'); + $property->addComment("@var Collection|null"); + } else { + $property->setType("null|\\Timatic\\Dto\\{$relatedModel}"); + } + + // Add Relationship attribute (use full class name for attribute) + $property->addAttribute(Relationship::class, [ + new Literal("{$relatedModel}::class"), + new Literal("RelationType::{$relationType}"), + ]); + } + } + + /** + * Detect relationship type (One or Many) from relationship name + */ + protected function detectRelationType(string $relationName, $relationSpec): string + { + // Plural relationship names are typically "to-many" + if (Str::plural($relationName) === $relationName) { + return 'Many'; + } + + // Singular names are "to-one" + return 'One'; + } + + /** + * Detect related model class name from relationship schema + */ + protected function detectRelatedModel(string $relationName, Schema|Reference $relationSpec): ?string + { + // Look at relationship.properties.data.anyOf to find the schema reference + // Example: for budget.currentPeriod, we find PeriodIdentifier in anyOf + // Then we strip "Identifier" to get "Period" + + if ($relationSpec instanceof Reference) { + $relationSpec = $relationSpec->resolve(); + } + + // Navigate to properties.data.anyOf + if (! isset($relationSpec->properties['data'])) { + return null; + } + + $dataSpec = $relationSpec->properties['data']; + + if ($dataSpec instanceof Reference) { + $dataSpec = $dataSpec->resolve(); + } + + // Check for anyOf (union types) + if (isset($dataSpec->anyOf) && is_array($dataSpec->anyOf)) { + foreach ($dataSpec->anyOf as $typeSchema) { + if ($typeSchema instanceof Reference) { + // Extract schema name from reference + // e.g., #/components/schemas/PeriodIdentifier -> PeriodIdentifier + $schemaName = Str::afterLast($typeSchema->getReference(), '/'); + + // Skip null types + if ($schemaName === 'null') { + continue; + } + + // Strip "Identifier" suffix to get the DTO name + if (Str::endsWith($schemaName, 'Identifier')) { + $modelName = Str::beforeLast($schemaName, 'Identifier'); + + return $modelName; + } + + // If no "Identifier" suffix, use as-is + return $schemaName; + } + } + } + + // Fallback: convert relationship name to model name + $singular = Str::singular($relationName); + $modelName = NameHelper::dtoClassName($singular); + + return $modelName; + } } diff --git a/generator/JsonApiFactoryGenerator.php b/generator/JsonApiFactoryGenerator.php index 27bbf70..5e9d12b 100644 --- a/generator/JsonApiFactoryGenerator.php +++ b/generator/JsonApiFactoryGenerator.php @@ -21,6 +21,12 @@ public function generate(ApiSpecification $specification): PhpFile|array { if ($specification->components) { foreach ($specification->components->schemas as $className => $schema) { + // Skip schemas that aren't useful + if (str_ends_with($className, 'Identifier') || + str_ends_with($className, 'Request')) { + continue; + } + $dtoClassName = NameHelper::dtoClassName(NameHelper::safeClassName($className)); $this->generateFactoryClass($dtoClassName); } @@ -95,6 +101,20 @@ protected function getDtoProperties(string $dtoFullClass): array continue; } + // Skip relationship properties (they have Relationship attribute) + $hasRelationshipAttribute = false; + foreach ($property->getAttributes() as $attribute) { + $attrName = $attribute->getName(); + if ($attrName === 'Relationship' || str_ends_with($attrName, '\\Relationship')) { + $hasRelationshipAttribute = true; + break; + } + } + + if ($hasRelationshipAttribute) { + continue; + } + // Check if property has DateTime attribute $isDateTime = ! empty($property->getAttributes(DateTime::class)); @@ -152,28 +172,7 @@ protected function generateFakerCall(string $propertyName, ?string $propertyType 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 + // Handle by property type FIRST (type takes precedence over naming) if ($propertyType) { $baseType = ltrim($propertyType, '?\\'); @@ -182,6 +181,10 @@ protected function generateFakerCall(string $propertyName, ?string $propertyType } if ($baseType === 'int' || $baseType === 'integer') { + // For integer IDs, generate a number not a UUID + if (str_ends_with($propertyName, 'Id') || str_ends_with($lowerName, '_id')) { + return '$this->faker->numberBetween(1, 1000)'; + } // Special cases for specific property names if (str_contains($lowerName, 'minute')) { return '$this->faker->numberBetween(15, 480)'; @@ -191,10 +194,41 @@ protected function generateFakerCall(string $propertyName, ?string $propertyType } if ($baseType === 'float' || $baseType === 'double') { - return '$this->faker->randomFloat(2, 1, 1000)'; + return '$this->faker->randomFloat(2, 0, 1000)'; + } + + if ($baseType === 'object') { + return '(object) []'; + } + + if ($baseType === 'array') { + return '[]'; } } + // Handle specific property names (case-insensitive) - only for string types + if (str_contains($lowerName, 'email')) { + return '$this->faker->safeEmail()'; + } + + // ID fields - only generate UUID for string types + if ((str_ends_with($propertyName, 'Id') || str_ends_with($lowerName, '_id')) && (! $propertyType || str_contains($propertyType, 'string'))) { + return '$this->faker->uuid()'; + } + + // Rate fields - only for string types (object types handled above) + if (($lowerName === 'hourlyrate' || $lowerName === 'hourly_rate' || str_contains($lowerName, 'rate')) && (! $propertyType || str_contains($propertyType, 'string'))) { + 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 name patterns if (str_ends_with($propertyName, 'Name') && ! str_starts_with($lowerName, 'user')) { return '$this->faker->company()'; diff --git a/generator/JsonApiPestTestGenerator.php b/generator/JsonApiPestTestGenerator.php index 39e403d..b047b99 100644 --- a/generator/JsonApiPestTestGenerator.php +++ b/generator/JsonApiPestTestGenerator.php @@ -177,6 +177,7 @@ protected function getRequestClassName(Endpoint $endpoint): string if ($isCollection) { $className .= 'Collection'; + $className = str_replace('Index', '', $className); } if (! str_ends_with($className, 'Request')) { diff --git a/generator/JsonApiRequestGenerator.php b/generator/JsonApiRequestGenerator.php index 1f189bf..afb3515 100644 --- a/generator/JsonApiRequestGenerator.php +++ b/generator/JsonApiRequestGenerator.php @@ -9,6 +9,7 @@ use Crescat\SaloonSdkGenerator\Data\Generator\Parameter; use Crescat\SaloonSdkGenerator\Generators\RequestGenerator; use Crescat\SaloonSdkGenerator\Helpers\MethodGeneratorHelper; +use Illuminate\Support\Str; use Nette\PhpGenerator\ClassType; use Nette\PhpGenerator\PhpFile; use Saloon\Http\Response; @@ -16,7 +17,8 @@ use Timatic\Generator\TestGenerators\Traits\DtoHelperTrait; use Timatic\Hydration\Facades\Hydrator; use Timatic\Hydration\Model; -use Timatic\Requests\HasFilters; +use Timatic\Requests\Concerns\HasFilters; +use Timatic\Requests\Concerns\HasIncludes; class JsonApiRequestGenerator extends RequestGenerator { @@ -50,6 +52,7 @@ protected function getRequestClassName(Endpoint $endpoint): string // For collection requests, add "Collection" suffix if ($this->isCollectionRequest($endpoint)) { $className .= 'Collection'; + $className = str_replace('Index', '', $className); } if (! str_ends_with($className, 'Request')) { @@ -86,6 +89,15 @@ protected function customizeRequestClass(ClassType $classType, $namespace, Endpo $namespace->addUse(HasFilters::class); $classType->addTrait(HasFilters::class); } + + // Add HasIncludes trait if endpoint has include parameter + if ($this->hasIncludeParameter($endpoint)) { + $namespace->addUse(HasIncludes::class); + $classType->addTrait(HasIncludes::class); + + // Add relationship-specific include methods + $this->addIncludeMethods($classType, $namespace, $endpoint); + } } // Add hydration support to GET, POST, and PATCH requests @@ -121,11 +133,21 @@ protected function customizeConstructor($classConstructor, ClassType $classType, } /** - * Hook: Filter out filter* query parameters (handled by HasFilters trait) + * Hook: Filter out filter* and include query parameters (handled by traits) */ protected function shouldIncludeQueryParameter(string $paramName): bool { - return ! str_starts_with($paramName, 'filter'); + // Filter out filter* parameters (handled by HasFilters trait) + if (str_starts_with($paramName, 'filter')) { + return false; + } + + // Filter out include parameter (handled by HasIncludes trait) + if ($paramName === 'include') { + return false; + } + + return true; } /** @@ -133,12 +155,10 @@ protected function shouldIncludeQueryParameter(string $paramName): bool */ protected function generateDefaultQueryMethod(\Nette\PhpGenerator\ClassType $classType, $namespace, array $queryParams, Endpoint $endpoint): void { - // If we have any query parameters (likely just 'include'), use array_filter + + // For other cases with query parameters, use parent implementation if (! empty($queryParams)) { - $classType->addMethod('defaultQuery') - ->setProtected() - ->setReturnType('array') - ->addBody("return array_filter(['include' => \$this->include]);"); + parent::generateDefaultQueryMethod($classType, $namespace, $queryParams, $endpoint); } } @@ -168,6 +188,20 @@ protected function hasFilterParameters(Endpoint $endpoint): bool return false; } + /** + * Check if endpoint has include query parameter + */ + protected function hasIncludeParameter(Endpoint $endpoint): bool + { + foreach ($endpoint->queryParameters as $param) { + if ($param->name === 'include') { + return true; + } + } + + return false; + } + /** * Determine if request should have hydration support */ @@ -229,4 +263,40 @@ protected function addHydrationSupport(ClassType $classType, $namespace, Endpoin $method->addBody(');'); } } + + /** + * Add relationship-specific include methods to request class + */ + protected function addIncludeMethods(ClassType $classType, $namespace, Endpoint $endpoint): void + { + // Get the DTO class name for this endpoint + $dtoClassName = $this->getDtoClassName($endpoint); + + // Check if schema exists in specification + if (! isset($this->specification->components->schemas[$dtoClassName])) { + return; + } + + $schema = $this->specification->components->schemas[$dtoClassName]; + + // Check if schema has relationships + if (! isset($schema->properties['relationships'])) { + return; + } + + $relationships = $schema->properties['relationships']; + + // Generate include method for each relationship + if (isset($relationships->properties) && is_array($relationships->properties)) { + foreach ($relationships->properties as $relationName => $relationSpec) { + $methodName = 'include'.Str::studly($relationName); + + $classType->addMethod($methodName) + ->setPublic() + ->setReturnType('static') + ->addComment("Include the {$relationName} relationship in the response") + ->addBody("return \$this->addInclude('{$relationName}');"); + } + } + } } diff --git a/generator/TestGenerators/CollectionRequestTestGenerator.php b/generator/TestGenerators/CollectionRequestTestGenerator.php index 3b2f02a..14aa4bb 100644 --- a/generator/TestGenerators/CollectionRequestTestGenerator.php +++ b/generator/TestGenerators/CollectionRequestTestGenerator.php @@ -60,11 +60,35 @@ public function replaceStubVariables(string $functionStub, Endpoint $endpoint): // Only include filter assertions block if there are filters if (! empty($filterData['assertions'])) { $filterAssertionBlock = $this->generateFilterAssertionBlock($filterData['assertions']); - $functionStub = str_replace('{{ filterAssertionBlock }}', $filterAssertionBlock, $functionStub); + $functionStub = str_replace('{{ filterAssertionBlock }}', $filterAssertionBlock."\n\t\t", $functionStub); } else { $functionStub = str_replace('{{ filterAssertionBlock }}', '', $functionStub); } + // Add include chain and assertions + $includeData = $this->generateIncludeChainWithData($endpoint); + + // Add newline before include chain if there are filters + if (! empty($filterData['chain']) && ! empty($includeData['chain'])) { + $functionStub = str_replace('{{ includeChain }}', "\n\t\t".$includeData['chain'], $functionStub); + } else { + $functionStub = str_replace('{{ includeChain }}', $includeData['chain'], $functionStub); + } + + // Only include assertion if there are includes + if (! empty($includeData['assertion'])) { + $functionStub = str_replace('{{ includeAssertion }}', $includeData['assertion'], $functionStub); + } else { + $functionStub = str_replace('{{ includeAssertion }}', '', $functionStub); + } + + // Add relationship assertions if there are includes + if (! empty($includeData['relationshipAssertions'])) { + $functionStub = str_replace('{{ relationshipAssertions }}', "\n\t\t".$includeData['relationshipAssertions'], $functionStub); + } else { + $functionStub = str_replace('{{ relationshipAssertions }}', '', $functionStub); + } + // Add non-filter query parameters (like 'include') $nonFilterParams = $this->getNonFilterQueryParameters($endpoint); $functionStub = str_replace('{{ nonFilterParams }}', $nonFilterParams, $functionStub); @@ -110,8 +134,12 @@ public function generateMockData(Endpoint $endpoint): array $resourceType = $this->getResourceTypeFromEndpoint($endpoint); - // Generate 2-3 items for collections - return [ + // Get relationships for this endpoint + $relationships = $this->getRelationshipsFromSchema($dtoClassName); + $includeData = $this->generateIncludeChainWithData($endpoint); + $hasIncludes = ! empty($includeData['chain']); + + $data = [ 'data' => [ [ 'type' => $resourceType, @@ -125,6 +153,56 @@ public function generateMockData(Endpoint $endpoint): array ], ], ]; + + // Add relationships and included data if this endpoint has includes + if ($hasIncludes && ! empty($relationships)) { + $included = []; + $relationshipsData = []; + + foreach ($relationships as $index => $relationName) { + $relatedModel = $this->detectRelatedModel($relationName); + + // Skip if the related model doesn't exist in the schema + if (! isset($this->specification->components->schemas[$relatedModel])) { + continue; + } + + $relationType = strtolower(\Illuminate\Support\Str::plural($relatedModel)); + + // Check if this is a "Many" relationship + $isMany = \Illuminate\Support\Str::plural($relationName) === $relationName; + + if ($isMany) { + // For "Many" relationships, data should be an array of objects + $relationshipsData[$relationName] = [ + 'data' => [ + ['type' => $relationType, 'id' => "related-{$relationName}-1"], + ], + ]; + } else { + // For "One" relationships, data is a single object + $relationshipsData[$relationName] = [ + 'data' => ['type' => $relationType, 'id' => "related-{$relationName}-1"], + ]; + } + + // Add to included array with minimal attributes + $included[] = [ + 'type' => $relationType, + 'id' => "related-{$relationName}-1", + 'attributes' => [], + ]; + } + + // Add relationships to both data items + $data['data'][0]['relationships'] = $relationshipsData; + $data['data'][1]['relationships'] = $relationshipsData; + + // Add included array + $data['included'] = $included; + } + + return $data; } /** @@ -132,9 +210,8 @@ public function generateMockData(Endpoint $endpoint): array */ protected function generateFilterAssertionBlock(string $assertions): string { - $stub = file_get_contents(__DIR__.'/stubs/pest-filter-assertion-block.stub'); - - return str_replace('{{ filterAssertions }}', $assertions, $stub); + // Just return the assertions with proper indentation, no wrapper needed + return $assertions; } /** @@ -215,7 +292,8 @@ protected function getNonFilterQueryParameters(Endpoint $endpoint): string $params = []; foreach ($endpoint->queryParameters as $parameter) { - if (! str_starts_with($parameter->name, 'filter[')) { + // Skip filter parameters and include parameter (handled by HasIncludes trait) + if (! str_starts_with($parameter->name, 'filter[') && $parameter->name !== 'include') { $paramName = NameHelper::safeVariableName($parameter->name); $value = match ($parameter->type) { 'string' => "'test string'", @@ -230,4 +308,136 @@ protected function getNonFilterQueryParameters(Endpoint $endpoint): string return implode(', ', $params); } + + /** + * Check if endpoint has include query parameter + */ + protected function hasIncludeParameter(Endpoint $endpoint): bool + { + foreach ($endpoint->queryParameters as $param) { + if ($param->name === 'include') { + return true; + } + } + + return false; + } + + /** + * Get relationships from the DTO schema + */ + protected function getRelationshipsFromSchema(string $dtoClassName): array + { + if (! isset($this->specification->components->schemas[$dtoClassName])) { + return []; + } + + $schema = $this->specification->components->schemas[$dtoClassName]; + + if (! isset($schema->properties['relationships'])) { + return []; + } + + $relationships = $schema->properties['relationships']; + + if (! isset($relationships->properties) || ! is_array($relationships->properties)) { + return []; + } + + return array_keys($relationships->properties); + } + + /** + * Generate include method chain with data for testing + */ + protected function generateIncludeChainWithData(Endpoint $endpoint): array + { + if (! $this->hasIncludeParameter($endpoint)) { + return [ + 'chain' => '', + 'assertion' => '', + 'relationshipAssertions' => '', + ]; + } + + $dtoClassName = $this->getDtoClassName($endpoint); + $relationships = $this->getRelationshipsFromSchema($dtoClassName); + + if (empty($relationships)) { + return [ + 'chain' => '', + 'assertion' => '', + 'relationshipAssertions' => '', + ]; + } + + // Filter out relationships where the model doesn't exist + $testRelationships = array_filter($relationships, function ($relationName) { + $relatedModel = $this->detectRelatedModel($relationName); + + return isset($this->specification->components->schemas[$relatedModel]); + }); + + // Generate include method calls + $includeCalls = []; + foreach ($testRelationships as $relationName) { + $methodName = 'include'.\Illuminate\Support\Str::studly($relationName); + $includeCalls[] = "->{$methodName}()"; + } + + $includeChain = implode("\n\t\t", $includeCalls); + + // Generate assertion for include parameter + $expectedInclude = implode(',', $testRelationships); + $assertion = "expect(\$query)->toHaveKey('include', '{$expectedInclude}');"; + + // Generate relationship hydration assertions + $relationshipAssertions = $this->generateRelationshipAssertions($testRelationships); + + return [ + 'chain' => $includeChain, + 'assertion' => $assertion, + 'relationshipAssertions' => $relationshipAssertions, + ]; + } + + /** + * Detect the related model class name from relationship name + */ + protected function detectRelatedModel(string $relationName): string + { + // Convert to singular studly case (e.g., budgetType -> BudgetType, entries -> Entry) + return \Illuminate\Support\Str::studly(\Illuminate\Support\Str::singular($relationName)); + } + + /** + * Generate assertions to verify relationships are hydrated + */ + protected function generateRelationshipAssertions(array $relationships): string + { + $assertions = []; + + foreach ($relationships as $relationName) { + $relatedModel = $this->detectRelatedModel($relationName); + $propertyName = \Illuminate\Support\Str::camel($relationName); + + // Skip if the related model doesn't exist in the schema + if (! isset($this->specification->components->schemas[$relatedModel])) { + continue; + } + + // Check if this is likely a "Many" relationship (plural name) + $isMany = \Illuminate\Support\Str::plural($relationName) === $relationName; + + if ($isMany) { + // For collection relationships, verify it's not null + $assertions[] = "->{$propertyName}->not->toBeNull()"; + } else { + // For single relationships, verify it's an instance of the related model + $assertions[] = "->{$propertyName}->toBeInstanceOf(\\Timatic\\Dto\\{$relatedModel}::class)"; + } + } + + return implode("\n\t\t", $assertions); + } } diff --git a/generator/TestGenerators/MutationRequestTestGenerator.php b/generator/TestGenerators/MutationRequestTestGenerator.php index 8135d4a..f1ced41 100644 --- a/generator/TestGenerators/MutationRequestTestGenerator.php +++ b/generator/TestGenerators/MutationRequestTestGenerator.php @@ -80,7 +80,11 @@ protected function generateMethodArguments(Endpoint $endpoint): string if (! str_ends_with($paramName, 'Id')) { $paramName .= 'Id'; } - $args[] = "{$paramName}: 'test string'"; + + // Generate typed value based on parameter type + $value = $this->generateValue($paramName, $param->type); + $formattedValue = $this->formatAsCode($value); + $args[] = "{$paramName}: {$formattedValue}"; } // Add $dto parameter last - use named argument if there are path params diff --git a/generator/TestGenerators/Traits/DtoAssertions.php b/generator/TestGenerators/Traits/DtoAssertions.php index 3c05636..ac6b5b8 100644 --- a/generator/TestGenerators/Traits/DtoAssertions.php +++ b/generator/TestGenerators/Traits/DtoAssertions.php @@ -77,6 +77,21 @@ protected function getDtoPropertiesFromGeneratedCode(string $dtoClassName): arra continue; } + // Skip relationship properties (they have Relationship attribute) + $hasRelationshipAttribute = false; + foreach ($property->getAttributes() as $attribute) { + $attrName = $attribute->getName(); + // Check for both short name and full class name + if ($attrName === 'Relationship' || str_ends_with($attrName, '\\Relationship')) { + $hasRelationshipAttribute = true; + break; + } + } + + if ($hasRelationshipAttribute) { + continue; + } + $type = $property->getType(); $typeName = null; @@ -128,12 +143,27 @@ protected function generateMockValueForDtoProperty(string $propertyName, string // Normalize type name (remove nullable prefix) $typeName = ltrim($typeName, '?'); + // Handle union types (e.g., "string|null", "string|float", "int|null") + if (str_contains($typeName, '|')) { + $types = explode('|', $typeName); + // Filter out 'null' and 'mixed', get the first concrete type + $concreteTypes = array_filter($types, fn ($t) => ! in_array(trim($t), ['null', 'mixed'])); + + if (! empty($concreteTypes)) { + // Use the first concrete type + $typeName = trim(reset($concreteTypes)); + } else { + // If all types are null/mixed, default to string + $typeName = 'string'; + } + } + // DateTime fields if (str_contains($typeName, 'Carbon') || str_contains($typeName, 'DateTime')) { return '2025-11-22T10:40:04.065Z'; } - // Type-based generation (handle explicit types first) + // Type-based generation (type takes precedence over name-based heuristics) if ($typeName === 'bool') { return true; } @@ -150,6 +180,14 @@ protected function generateMockValueForDtoProperty(string $propertyName, string return []; } + if ($typeName === 'object') { + return (object) []; + } + + if ($typeName === 'mixed') { + return 'Mock value'; + } + // String type - apply name-based heuristics if ($typeName === 'string') { // ID fields @@ -165,8 +203,8 @@ protected function generateMockValueForDtoProperty(string $propertyName, string return 'Mock value'; } - // This should never be reached with the current OpenAPI spec - throw new \RuntimeException("Unexpected type '{$typeName}' for property '{$propertyName}'"); + // Fallback for unknown types + return 'Mock value'; } /** @@ -185,10 +223,22 @@ protected function generateAssertionForValue(string $key, mixed $value): string return " ->{$key}->toBe({$value})"; } + if (is_float($value)) { + return " ->{$key}->toBe({$value})"; + } + if (is_null($value)) { return " ->{$key}->toBeNull()"; } + if (is_object($value)) { + return " ->{$key}->toBeInstanceOf(stdClass::class)"; + } + + if (is_array($value)) { + return " ->{$key}->toBeArray()"; + } + // Check if it's a datetime string if (is_string($value) && $this->isDateTimeString($value)) { return " ->{$key}->toEqual(new Carbon(\"{$value}\"))"; diff --git a/generator/TestGenerators/Traits/MockJsonDataTrait.php b/generator/TestGenerators/Traits/MockJsonDataTrait.php index 55474dd..ffda65c 100644 --- a/generator/TestGenerators/Traits/MockJsonDataTrait.php +++ b/generator/TestGenerators/Traits/MockJsonDataTrait.php @@ -16,6 +16,8 @@ protected function formatArrayAsPhp(array $data): string if (is_array($value)) { $lines[] = "$keyStr => ".$this->formatArrayAsPhp($value).','; + } elseif (is_object($value)) { + $lines[] = "$keyStr => (object) [],"; } elseif (is_string($value)) { $escapedValue = addslashes($value); $lines[] = "$keyStr => '$escapedValue',"; diff --git a/generator/TestGenerators/Traits/TestDataGeneratorTrait.php b/generator/TestGenerators/Traits/TestDataGeneratorTrait.php index 054d555..4ade667 100644 --- a/generator/TestGenerators/Traits/TestDataGeneratorTrait.php +++ b/generator/TestGenerators/Traits/TestDataGeneratorTrait.php @@ -26,27 +26,12 @@ protected function generateValue(string $propertyName, Schema|array|string|null return $example; } - // DateTime fields (by format or name) - if ($format === 'date-time' || str_contains($propertyName, 'At') || str_contains($propertyName, 'Date')) { - return '2025-01-15T10:30:00Z'; - } - - // ID fields - if (str_ends_with($propertyName, 'Id')) { - return Str::snake($propertyName).'-123'; - } - - // Email fields - if (str_contains($propertyName, 'email') || str_contains($propertyName, 'Email')) { - return 'test@example.com'; - } - // Boolean fields (by type or name prefix) if ($type === 'boolean' || $type === 'bool' || str_starts_with($propertyName, 'is') || str_starts_with($propertyName, 'has')) { return true; } - // Numeric fields + // Numeric fields (type takes precedence over naming) if ($type === 'integer' || $type === 'int') { return 42; } @@ -55,11 +40,30 @@ protected function generateValue(string $propertyName, Schema|array|string|null return 3.14; } + // DateTime fields (by format or name) + if ($format === 'date-time' || str_contains($propertyName, 'At') || str_contains($propertyName, 'Date')) { + return '2025-01-15T10:30:00Z'; + } + + // Email fields + if (str_contains($propertyName, 'email') || str_contains($propertyName, 'Email')) { + return 'test@example.com'; + } + + // ID fields - only if type is string or null + if (str_ends_with($propertyName, 'Id') && ($type === 'string' || $type === null)) { + return Str::snake($propertyName).'-123'; + } + // Array/Object fields - if ($type === 'array' || $type === 'object') { + if ($type === 'array') { return []; } + if ($type === 'object') { + return (object) []; + } + // Common string property names if ($type === 'string' || $type === null) { if ($propertyName === 'title') { @@ -75,8 +79,13 @@ protected function generateValue(string $propertyName, Schema|array|string|null return 'test value'; } - // This should never be reached with the current OpenAPI spec - throw new \RuntimeException("Unexpected type '{$type}' for property '{$propertyName}'"); + // Handle 'mixed' type + if ($type === 'mixed') { + return 'test value'; + } + + // Fallback for any unknown types + return 'test value'; } /** @@ -105,6 +114,10 @@ protected function formatAsCode(mixed $value): string return '[]'; } + if (is_object($value)) { + return '(object) []'; + } + if (is_null($value)) { return 'null'; } @@ -159,6 +172,21 @@ private function extractTypeInfo(Schema|array|string|null $typeInfo): array // Normalize type name (remove nullable prefix) $normalizedType = ltrim($typeInfo, '?'); + // Handle union types (e.g., "string|null", "string|float", "int|null") + if (str_contains($normalizedType, '|')) { + $types = explode('|', $normalizedType); + // Filter out 'null' and 'mixed', get the first concrete type + $concreteTypes = array_filter($types, fn ($t) => ! in_array(trim($t), ['null', 'mixed'])); + + if (! empty($concreteTypes)) { + // Use the first concrete type + $normalizedType = trim(reset($concreteTypes)); + } else { + // If all types are null/mixed, default to string + $normalizedType = 'string'; + } + } + // Check for DateTime type hints if (str_contains($normalizedType, 'Carbon') || str_contains($normalizedType, 'DateTime')) { return ['string', 'date-time', null]; diff --git a/generator/TestGenerators/stubs/pest-collection-request-test-func.stub b/generator/TestGenerators/stubs/pest-collection-request-test-func.stub index 1d260a4..fbce88c 100644 --- a/generator/TestGenerators/stubs/pest-collection-request-test-func.stub +++ b/generator/TestGenerators/stubs/pest-collection-request-test-func.stub @@ -4,17 +4,20 @@ it('{{ testDescription }}', function () { ]); $request = (new {{ requestClass }}({{ nonFilterParams }})) - {{ filterChain }}; + {{ filterChain }}{{ includeChain }}; $response = $this->{{ clientName }}->send($request); - Saloon::assertSent({{ requestClass }}::class); - {{ filterAssertionBlock }} + Saloon::assertSent(function ({{ requestClass }} $request) { + $query = $request->query()->all(); + {{ filterAssertionBlock }}{{ includeAssertion }} + return true; + }); expect($response->status())->toBe(200); $dtoCollection = $response->dto(); expect($dtoCollection->first()) - {{ dtoAssertions }}; + {{ dtoAssertions }}{{ relationshipAssertions }}; }); diff --git a/generator/generate.php b/generator/generate.php index 56aab1f..dc7d8ba 100644 --- a/generator/generate.php +++ b/generator/generate.php @@ -18,21 +18,28 @@ use Timatic\Generator\JsonApiResourceGenerator; // Download OpenAPI spec -echo "📥 Downloading OpenAPI specification...\n"; -$openApiJson = file_get_contents('https://api.app.timatic.test/docs/json', false, stream_context_create([ - 'ssl' => [ - 'verify_peer' => false, - 'verify_peer_name' => false, - ], -])); - -if (! $openApiJson) { - echo "❌ Failed to download OpenAPI specification\n"; - exit(1); -} +$openApiPath = __DIR__.'/../openapi.json'; + +if (file_exists($openApiPath)) { + echo "📥 Using existing OpenAPI specification...\n"; + echo "✅ OpenAPI specification found\n\n"; +} else { + echo "📥 Downloading OpenAPI specification...\n"; + $openApiJson = file_get_contents('https://api.app.timatic.test/docs/api.json', false, stream_context_create([ + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ], + ])); + + if (! $openApiJson) { + echo "❌ Failed to download OpenAPI specification\n"; + exit(1); + } -file_put_contents(__DIR__.'/../openapi.json', $openApiJson); -echo "✅ OpenAPI specification downloaded\n\n"; + file_put_contents($openApiPath, $openApiJson); + echo "✅ OpenAPI specification downloaded\n\n"; +} // Clean up previously generated folders echo "🧹 Cleaning up previously generated files...\n"; diff --git a/src/Dto/Activity.php b/src/Dto/Activity.php new file mode 100644 index 0000000..5b71fd5 --- /dev/null +++ b/src/Dto/Activity.php @@ -0,0 +1,62 @@ +|null */ + #[Relationship(Event::class, RelationType::Many)] + public ?Collection $events = null; +} diff --git a/src/Dto/Approve.php b/src/Dto/Approve.php deleted file mode 100644 index aef906e..0000000 --- a/src/Dto/Approve.php +++ /dev/null @@ -1,48 +0,0 @@ -|null */ + #[Relationship(Entry::class, RelationType::Many)] + public ?Collection $entries = null; + + #[Relationship(BudgetType::class, RelationType::One)] + public ?BudgetType $budgetType = null; + + #[Relationship(Period::class, RelationType::One)] + public ?Period $currentPeriod = null; + + #[Relationship(Period::class, RelationType::One)] + public ?Period $lastPeriod = null; + + #[Relationship(Customer::class, RelationType::One)] + public ?Customer $customer = null; } diff --git a/src/Dto/BudgetTimeSpentTotal.php b/src/Dto/BudgetTimeSpentTotal.php deleted file mode 100644 index 7c0c572..0000000 --- a/src/Dto/BudgetTimeSpentTotal.php +++ /dev/null @@ -1,29 +0,0 @@ -|null */ + #[Relationship(Activity::class, RelationType::Many)] + public ?Collection $activities = null; } diff --git a/src/Dto/Event.php b/src/Dto/Event.php index 55dda90..38ce898 100644 --- a/src/Dto/Event.php +++ b/src/Dto/Event.php @@ -6,15 +6,20 @@ use Timatic\Hydration\Attributes\DateTime; use Timatic\Hydration\Attributes\Property; +use Timatic\Hydration\Attributes\Relationship; use Timatic\Hydration\Model; +use Timatic\Hydration\RelationType; +/** + * Event + */ class Event extends Model { #[Property] - public ?string $userId; + public ?int $userId; #[Property] - public ?string $budgetId; + public ?int $budgetId; #[Property] public ?string $ticketId; @@ -57,5 +62,8 @@ class Event extends Model public ?\Carbon\Carbon $updatedAt; #[Property] - public ?string $isInternal; + public ?bool $isInternal; + + #[Relationship(Source::class, RelationType::One)] + public ?Source $source = null; } diff --git a/src/Dto/MarkAsExported.php b/src/Dto/MarkAsExported.php deleted file mode 100644 index 992e09a..0000000 --- a/src/Dto/MarkAsExported.php +++ /dev/null @@ -1,48 +0,0 @@ -|null */ + #[Relationship(Permission::class, RelationType::Many)] + public ?Collection $permissions = null; + + #[Relationship(Team::class, RelationType::One)] + public ?Team $team = null; } diff --git a/src/Dto/UserCustomerHoursAggregate.php b/src/Dto/UserCustomerHoursAggregate.php deleted file mode 100644 index bd792cd..0000000 --- a/src/Dto/UserCustomerHoursAggregate.php +++ /dev/null @@ -1,26 +0,0 @@ -{$relationshipName} = $this->hydrate($relationModel, $includedItem, $included); + if (! is_null($includedItem)) { + $model->{$relationshipName} = $this->hydrate($relationModel, $includedItem, $included); + } } } } diff --git a/src/Requests/Budget/BudgetsCollectionRequest.php b/src/Requests/Budget/BudgetsCollectionRequest.php new file mode 100644 index 0000000..3f3eecb --- /dev/null +++ b/src/Requests/Budget/BudgetsCollectionRequest.php @@ -0,0 +1,103 @@ +addInclude('entries'); + } + + /** + * Include the budgetType relationship in the response + */ + public function includeBudgetType(): static + { + return $this->addInclude('budgetType'); + } + + /** + * Include the currentPeriod relationship in the response + */ + public function includeCurrentPeriod(): static + { + return $this->addInclude('currentPeriod'); + } + + /** + * Include the lastPeriod relationship in the response + */ + public function includeLastPeriod(): static + { + return $this->addInclude('lastPeriod'); + } + + /** + * Include the customer relationship in the response + */ + public function includeCustomer(): static + { + return $this->addInclude('customer'); + } + + /** + * Include the allowedUsers relationship in the response + */ + public function includeAllowedUsers(): static + { + return $this->addInclude('allowedUsers'); + } + + public function createDtoFromResponse(Response $response): mixed + { + return Hydrator::hydrateCollection( + $this->model, + $response->json('data'), + $response->json('included') + ); + } + + public function resolveEndpoint(): string + { + return '/budgets'; + } + + /** + * @param null|int $pagesize The number of results that will be returned per page. + * @param null|int $pagenumber The page number to start the pagination from. + */ + public function __construct( + protected ?int $pagesize = null, + protected ?int $pagenumber = null, + ) {} + + public function defaultQuery(): array + { + return array_filter(['page[size]' => $this->pagesize, 'page[number]' => $this->pagenumber]); + } +} diff --git a/src/Requests/Budget/DeleteBudgetRequest.php b/src/Requests/Budget/BudgetsDestroyRequest.php similarity index 67% rename from src/Requests/Budget/DeleteBudgetRequest.php rename to src/Requests/Budget/BudgetsDestroyRequest.php index 76b84a6..9a84840 100644 --- a/src/Requests/Budget/DeleteBudgetRequest.php +++ b/src/Requests/Budget/BudgetsDestroyRequest.php @@ -8,9 +8,9 @@ use Saloon\Http\Request; /** - * deleteBudget + * budgets.destroy */ -class DeleteBudgetRequest extends Request +class BudgetsDestroyRequest extends Request { protected Method $method = Method::DELETE; @@ -19,7 +19,10 @@ public function resolveEndpoint(): string return "/budgets/{$this->budgetId}"; } + /** + * @param int $budgetId The budget ID + */ public function __construct( - protected string $budgetId, + protected int $budgetId, ) {} } diff --git a/src/Requests/Budget/GetBudgetRequest.php b/src/Requests/Budget/BudgetsShowRequest.php similarity index 81% rename from src/Requests/Budget/GetBudgetRequest.php rename to src/Requests/Budget/BudgetsShowRequest.php index 75487cb..6b4da7c 100644 --- a/src/Requests/Budget/GetBudgetRequest.php +++ b/src/Requests/Budget/BudgetsShowRequest.php @@ -11,9 +11,9 @@ use Timatic\Hydration\Facades\Hydrator; /** - * getBudget + * budgets.show */ -class GetBudgetRequest extends Request +class BudgetsShowRequest extends Request { protected $model = Budget::class; @@ -33,7 +33,10 @@ public function resolveEndpoint(): string return "/budgets/{$this->budgetId}"; } + /** + * @param int $budgetId The budget ID + */ public function __construct( - protected string $budgetId, + protected int $budgetId, ) {} } diff --git a/src/Requests/Budget/PostBudgetsRequest.php b/src/Requests/Budget/BudgetsStoreRequest.php similarity index 93% rename from src/Requests/Budget/PostBudgetsRequest.php rename to src/Requests/Budget/BudgetsStoreRequest.php index c9e3777..c12e667 100644 --- a/src/Requests/Budget/PostBudgetsRequest.php +++ b/src/Requests/Budget/BudgetsStoreRequest.php @@ -14,9 +14,9 @@ use Timatic\Hydration\Model; /** - * postBudgets + * budgets.store */ -class PostBudgetsRequest extends Request implements HasBody +class BudgetsStoreRequest extends Request implements HasBody { use HasJsonBody; diff --git a/src/Requests/Budget/GetBudgetsCollectionRequest.php b/src/Requests/Budget/GetBudgetsCollectionRequest.php deleted file mode 100644 index 685dbd6..0000000 --- a/src/Requests/Budget/GetBudgetsCollectionRequest.php +++ /dev/null @@ -1,48 +0,0 @@ -model, - $response->json('data'), - $response->json('included') - ); - } - - public function resolveEndpoint(): string - { - return '/budgets'; - } - - public function __construct( - protected ?string $include = null, - ) {} - - protected function defaultQuery(): array - { - return array_filter(['include' => $this->include]); - } -} diff --git a/src/Requests/Budget/PatchBudgetRequest.php b/src/Requests/Budget/PatchBudgetRequest.php deleted file mode 100644 index abd4b0f..0000000 --- a/src/Requests/Budget/PatchBudgetRequest.php +++ /dev/null @@ -1,53 +0,0 @@ -model, - $response->json('data'), - $response->json('included') - ); - } - - public function resolveEndpoint(): string - { - return "/budgets/{$this->budgetId}"; - } - - /** - * @param null|\Timatic\Hydration\Model|array|null $data Request data - */ - public function __construct( - protected string $budgetId, - protected Model|array|null $data = null, - ) {} - - protected function defaultBody(): array - { - return $this->data ? ['data' => $this->data->toJsonApi()] : []; - } -} diff --git a/src/Requests/BudgetTimeSpentTotal/GetBudgetTimeSpentTotalsCollectionRequest.php b/src/Requests/BudgetTimeSpentTotal/GetBudgetTimeSpentTotalsCollectionRequest.php deleted file mode 100644 index a31c76e..0000000 --- a/src/Requests/BudgetTimeSpentTotal/GetBudgetTimeSpentTotalsCollectionRequest.php +++ /dev/null @@ -1,41 +0,0 @@ -model, - $response->json('data'), - $response->json('included') - ); - } - - public function resolveEndpoint(): string - { - return '/budget-time-spent-totals'; - } - - public function __construct() {} -} diff --git a/src/Requests/BudgetType/GetBudgetTypesCollectionRequest.php b/src/Requests/BudgetType/BudgetTypesCollectionRequest.php similarity index 88% rename from src/Requests/BudgetType/GetBudgetTypesCollectionRequest.php rename to src/Requests/BudgetType/BudgetTypesCollectionRequest.php index 3646018..a115d33 100644 --- a/src/Requests/BudgetType/GetBudgetTypesCollectionRequest.php +++ b/src/Requests/BudgetType/BudgetTypesCollectionRequest.php @@ -12,9 +12,9 @@ use Timatic\Hydration\Facades\Hydrator; /** - * getBudgetTypes + * budget-types.index */ -class GetBudgetTypesCollectionRequest extends Request implements Paginatable +class BudgetTypesCollectionRequest extends Request implements Paginatable { protected $model = BudgetType::class; diff --git a/src/Requests/Change/GetChangeRequest.php b/src/Requests/Change/GetChangeRequest.php deleted file mode 100644 index 9f7c6ff..0000000 --- a/src/Requests/Change/GetChangeRequest.php +++ /dev/null @@ -1,25 +0,0 @@ -changeId}"; - } - - public function __construct( - protected string $changeId, - ) {} -} diff --git a/src/Requests/Change/GetChangesCollectionRequest.php b/src/Requests/Change/GetChangesCollectionRequest.php deleted file mode 100644 index be5a0e6..0000000 --- a/src/Requests/Change/GetChangesCollectionRequest.php +++ /dev/null @@ -1,24 +0,0 @@ -query()->add('include', implode(',', $relationships)); + + return $this; + } + + /** + * Add a single include to the request (for fluent method chaining) + */ + protected function addInclude(string $relation): static + { + $includes = $this->getCurrentIncludes(); + $includes[] = $relation; + + return $this->include(...$includes); + } + + /** + * Get current includes from query string + */ + protected function getCurrentIncludes(): array + { + $currentIncludeString = $this->query()->get('include'); + + if ($currentIncludeString === null) { + return []; + } + + return explode(',', $currentIncludeString); + } +} diff --git a/src/Requests/Correction/PostCorrectionsRequest.php b/src/Requests/Correction/CorrectionsStoreRequest.php similarity index 92% rename from src/Requests/Correction/PostCorrectionsRequest.php rename to src/Requests/Correction/CorrectionsStoreRequest.php index 33c6843..52c0a3a 100644 --- a/src/Requests/Correction/PostCorrectionsRequest.php +++ b/src/Requests/Correction/CorrectionsStoreRequest.php @@ -14,9 +14,9 @@ use Timatic\Hydration\Model; /** - * postCorrections + * corrections.store */ -class PostCorrectionsRequest extends Request implements HasBody +class CorrectionsStoreRequest extends Request implements HasBody { use HasJsonBody; diff --git a/src/Requests/Correction/PatchCorrectionRequest.php b/src/Requests/Correction/PatchCorrectionRequest.php deleted file mode 100644 index aca93ad..0000000 --- a/src/Requests/Correction/PatchCorrectionRequest.php +++ /dev/null @@ -1,53 +0,0 @@ -model, - $response->json('data'), - $response->json('included') - ); - } - - public function resolveEndpoint(): string - { - return "/corrections/{$this->correctionId}"; - } - - /** - * @param null|\Timatic\Hydration\Model|array|null $data Request data - */ - public function __construct( - protected string $correctionId, - protected Model|array|null $data = null, - ) {} - - protected function defaultBody(): array - { - return $this->data ? ['data' => $this->data->toJsonApi()] : []; - } -} diff --git a/src/Requests/Customer/GetCustomersCollectionRequest.php b/src/Requests/Customer/CustomersCollectionRequest.php similarity index 53% rename from src/Requests/Customer/GetCustomersCollectionRequest.php rename to src/Requests/Customer/CustomersCollectionRequest.php index 621b517..5e42f2d 100644 --- a/src/Requests/Customer/GetCustomersCollectionRequest.php +++ b/src/Requests/Customer/CustomersCollectionRequest.php @@ -10,12 +10,12 @@ use Saloon\PaginationPlugin\Contracts\Paginatable; use Timatic\Dto\Customer; use Timatic\Hydration\Facades\Hydrator; -use Timatic\Requests\HasFilters; +use Timatic\Requests\Concerns\HasFilters; /** - * getCustomers + * customers.index */ -class GetCustomersCollectionRequest extends Request implements Paginatable +class CustomersCollectionRequest extends Request implements Paginatable { use HasFilters; @@ -37,5 +37,17 @@ public function resolveEndpoint(): string return '/customers'; } - public function __construct() {} + /** + * @param null|int $pagesize The number of results that will be returned per page. + * @param null|int $pagenumber The page number to start the pagination from. + */ + public function __construct( + protected ?int $pagesize = null, + protected ?int $pagenumber = null, + ) {} + + public function defaultQuery(): array + { + return array_filter(['page[size]' => $this->pagesize, 'page[number]' => $this->pagenumber]); + } } diff --git a/src/Requests/Customer/DeleteCustomerRequest.php b/src/Requests/Customer/CustomersDestroyRequest.php similarity index 66% rename from src/Requests/Customer/DeleteCustomerRequest.php rename to src/Requests/Customer/CustomersDestroyRequest.php index 279d596..585f0b0 100644 --- a/src/Requests/Customer/DeleteCustomerRequest.php +++ b/src/Requests/Customer/CustomersDestroyRequest.php @@ -8,9 +8,9 @@ use Saloon\Http\Request; /** - * deleteCustomer + * customers.destroy */ -class DeleteCustomerRequest extends Request +class CustomersDestroyRequest extends Request { protected Method $method = Method::DELETE; @@ -19,7 +19,10 @@ public function resolveEndpoint(): string return "/customers/{$this->customerId}"; } + /** + * @param int $customerId The customer ID + */ public function __construct( - protected string $customerId, + protected int $customerId, ) {} } diff --git a/src/Requests/Customer/GetCustomerRequest.php b/src/Requests/Customer/CustomersShowRequest.php similarity index 80% rename from src/Requests/Customer/GetCustomerRequest.php rename to src/Requests/Customer/CustomersShowRequest.php index 3df3902..6a891af 100644 --- a/src/Requests/Customer/GetCustomerRequest.php +++ b/src/Requests/Customer/CustomersShowRequest.php @@ -11,9 +11,9 @@ use Timatic\Hydration\Facades\Hydrator; /** - * getCustomer + * customers.show */ -class GetCustomerRequest extends Request +class CustomersShowRequest extends Request { protected $model = Customer::class; @@ -33,7 +33,10 @@ public function resolveEndpoint(): string return "/customers/{$this->customerId}"; } + /** + * @param int $customerId The customer ID + */ public function __construct( - protected string $customerId, + protected int $customerId, ) {} } diff --git a/src/Requests/Customer/PostCustomersRequest.php b/src/Requests/Customer/CustomersStoreRequest.php similarity index 92% rename from src/Requests/Customer/PostCustomersRequest.php rename to src/Requests/Customer/CustomersStoreRequest.php index 3ea3b83..6d14498 100644 --- a/src/Requests/Customer/PostCustomersRequest.php +++ b/src/Requests/Customer/CustomersStoreRequest.php @@ -14,9 +14,9 @@ use Timatic\Hydration\Model; /** - * postCustomers + * customers.store */ -class PostCustomersRequest extends Request implements HasBody +class CustomersStoreRequest extends Request implements HasBody { use HasJsonBody; diff --git a/src/Requests/Customer/PatchCustomerRequest.php b/src/Requests/Customer/PatchCustomerRequest.php deleted file mode 100644 index a577110..0000000 --- a/src/Requests/Customer/PatchCustomerRequest.php +++ /dev/null @@ -1,53 +0,0 @@ -model, - $response->json('data'), - $response->json('included') - ); - } - - public function resolveEndpoint(): string - { - return "/customers/{$this->customerId}"; - } - - /** - * @param null|\Timatic\Hydration\Model|array|null $data Request data - */ - public function __construct( - protected string $customerId, - protected Model|array|null $data = null, - ) {} - - protected function defaultBody(): array - { - return $this->data ? ['data' => $this->data->toJsonApi()] : []; - } -} diff --git a/src/Requests/DailyProgress/GetDailyProgressesCollectionRequest.php b/src/Requests/DailyProgress/GetDailyProgressesCollectionRequest.php deleted file mode 100644 index 209870b..0000000 --- a/src/Requests/DailyProgress/GetDailyProgressesCollectionRequest.php +++ /dev/null @@ -1,38 +0,0 @@ -model, - $response->json('data'), - $response->json('included') - ); - } - - public function resolveEndpoint(): string - { - return '/daily-progress'; - } - - public function __construct() {} -} diff --git a/src/Requests/EntriesExport/GetBudgetEntriesExportRequest.php b/src/Requests/EntriesExport/GetBudgetEntriesExportRequest.php deleted file mode 100644 index eba2a5d..0000000 --- a/src/Requests/EntriesExport/GetBudgetEntriesExportRequest.php +++ /dev/null @@ -1,31 +0,0 @@ -budgetId}/entries-export"; - } - - public function __construct( - protected string $budgetId, - protected ?string $include = null, - ) {} - - protected function defaultQuery(): array - { - return array_filter(['include' => $this->include]); - } -} diff --git a/src/Requests/Entry/EntriesCollectionRequest.php b/src/Requests/Entry/EntriesCollectionRequest.php new file mode 100644 index 0000000..fc916e2 --- /dev/null +++ b/src/Requests/Entry/EntriesCollectionRequest.php @@ -0,0 +1,113 @@ +addInclude('personalOvertime'); + } + + /** + * Include the customerOvertime relationship in the response + */ + public function includeCustomerOvertime(): static + { + return $this->addInclude('customerOvertime'); + } + + /** + * Include the correctionEntryCorrection relationship in the response + */ + public function includeCorrectionEntryCorrection(): static + { + return $this->addInclude('correctionEntryCorrection'); + } + + /** + * Include the correctedEntryCorrection relationship in the response + */ + public function includeCorrectedEntryCorrection(): static + { + return $this->addInclude('correctedEntryCorrection'); + } + + /** + * Include the newEntryCorrection relationship in the response + */ + public function includeNewEntryCorrection(): static + { + return $this->addInclude('newEntryCorrection'); + } + + /** + * Include the customer relationship in the response + */ + public function includeCustomer(): static + { + return $this->addInclude('customer'); + } + + /** + * Include the budget relationship in the response + */ + public function includeBudget(): static + { + return $this->addInclude('budget'); + } + + public function createDtoFromResponse(Response $response): mixed + { + return Hydrator::hydrateCollection( + $this->model, + $response->json('data'), + $response->json('included') + ); + } + + public function resolveEndpoint(): string + { + return '/entries'; + } + + /** + * @param null|string $sort Available sorts are `id`, `startedAt`, `createdAt`, `customerName`, `ticketNumber`, `minutesSpent`, `userFullName`. You can sort by multiple options by separating them with a comma. To sort in descending order, use `-` sign in front of the sort, for example: `-id`. + * @param null|int $pagesize The number of results that will be returned per page. + * @param null|int $pagenumber The page number to start the pagination from. + */ + public function __construct( + protected ?string $sort = null, + protected ?int $pagesize = null, + protected ?int $pagenumber = null, + ) {} + + public function defaultQuery(): array + { + return array_filter(['sort' => $this->sort, 'page[size]' => $this->pagesize, 'page[number]' => $this->pagenumber]); + } +} diff --git a/src/Requests/Entry/DeleteEntryRequest.php b/src/Requests/Entry/EntriesDestroyRequest.php similarity index 67% rename from src/Requests/Entry/DeleteEntryRequest.php rename to src/Requests/Entry/EntriesDestroyRequest.php index 313fa41..dd4cca3 100644 --- a/src/Requests/Entry/DeleteEntryRequest.php +++ b/src/Requests/Entry/EntriesDestroyRequest.php @@ -8,9 +8,9 @@ use Saloon\Http\Request; /** - * deleteEntry + * entries.destroy */ -class DeleteEntryRequest extends Request +class EntriesDestroyRequest extends Request { protected Method $method = Method::DELETE; @@ -19,7 +19,10 @@ public function resolveEndpoint(): string return "/entries/{$this->entryId}"; } + /** + * @param int $entryId The entry ID + */ public function __construct( - protected string $entryId, + protected int $entryId, ) {} } diff --git a/src/Requests/Entry/GetEntryRequest.php b/src/Requests/Entry/EntriesShowRequest.php similarity index 81% rename from src/Requests/Entry/GetEntryRequest.php rename to src/Requests/Entry/EntriesShowRequest.php index 80afd6e..fdfc518 100644 --- a/src/Requests/Entry/GetEntryRequest.php +++ b/src/Requests/Entry/EntriesShowRequest.php @@ -11,9 +11,9 @@ use Timatic\Hydration\Facades\Hydrator; /** - * getEntry + * entries.show */ -class GetEntryRequest extends Request +class EntriesShowRequest extends Request { protected $model = Entry::class; @@ -33,7 +33,10 @@ public function resolveEndpoint(): string return "/entries/{$this->entryId}"; } + /** + * @param int $entryId The entry ID + */ public function __construct( - protected string $entryId, + protected int $entryId, ) {} } diff --git a/src/Requests/Entry/PostEntriesRequest.php b/src/Requests/Entry/EntriesStoreRequest.php similarity index 93% rename from src/Requests/Entry/PostEntriesRequest.php rename to src/Requests/Entry/EntriesStoreRequest.php index a5d8e0c..cc67209 100644 --- a/src/Requests/Entry/PostEntriesRequest.php +++ b/src/Requests/Entry/EntriesStoreRequest.php @@ -14,9 +14,9 @@ use Timatic\Hydration\Model; /** - * postEntries + * entries.store */ -class PostEntriesRequest extends Request implements HasBody +class EntriesStoreRequest extends Request implements HasBody { use HasJsonBody; diff --git a/src/Requests/MarkAsInvoiced/PostEntryMarkAsInvoicedRequest.php b/src/Requests/Entry/EntryMarkAsInvoicedRequest.php similarity index 78% rename from src/Requests/MarkAsInvoiced/PostEntryMarkAsInvoicedRequest.php rename to src/Requests/Entry/EntryMarkAsInvoicedRequest.php index d0d2dfb..b293545 100644 --- a/src/Requests/MarkAsInvoiced/PostEntryMarkAsInvoicedRequest.php +++ b/src/Requests/Entry/EntryMarkAsInvoicedRequest.php @@ -2,25 +2,25 @@ // auto-generated -namespace Timatic\Requests\MarkAsInvoiced; +namespace Timatic\Requests\Entry; use Saloon\Contracts\Body\HasBody; use Saloon\Enums\Method; use Saloon\Http\Request; use Saloon\Http\Response; use Saloon\Traits\Body\HasJsonBody; -use Timatic\Dto\MarkAsInvoiced; +use Timatic\Dto\Entry; use Timatic\Hydration\Facades\Hydrator; use Timatic\Hydration\Model; /** - * postEntryMarkAsInvoiced + * entry.mark-as-invoiced */ -class PostEntryMarkAsInvoicedRequest extends Request implements HasBody +class EntryMarkAsInvoicedRequest extends Request implements HasBody { use HasJsonBody; - protected $model = MarkAsInvoiced::class; + protected $model = Entry::class; protected Method $method = Method::POST; @@ -39,10 +39,11 @@ public function resolveEndpoint(): string } /** + * @param int $entryId The entry ID * @param null|\Timatic\Hydration\Model|array|null $data Request data */ public function __construct( - protected string $entryId, + protected int $entryId, protected Model|array|null $data = null, ) {} diff --git a/src/Requests/Entry/GetEntriesCollectionRequest.php b/src/Requests/Entry/GetEntriesCollectionRequest.php deleted file mode 100644 index e4671b7..0000000 --- a/src/Requests/Entry/GetEntriesCollectionRequest.php +++ /dev/null @@ -1,48 +0,0 @@ -model, - $response->json('data'), - $response->json('included') - ); - } - - public function resolveEndpoint(): string - { - return '/entries'; - } - - public function __construct( - protected ?string $include = null, - ) {} - - protected function defaultQuery(): array - { - return array_filter(['include' => $this->include]); - } -} diff --git a/src/Requests/Entry/PatchEntryRequest.php b/src/Requests/Entry/PatchEntryRequest.php deleted file mode 100644 index fc2c712..0000000 --- a/src/Requests/Entry/PatchEntryRequest.php +++ /dev/null @@ -1,53 +0,0 @@ -model, - $response->json('data'), - $response->json('included') - ); - } - - public function resolveEndpoint(): string - { - return "/entries/{$this->entryId}"; - } - - /** - * @param null|\Timatic\Hydration\Model|array|null $data Request data - */ - public function __construct( - protected string $entryId, - protected Model|array|null $data = null, - ) {} - - protected function defaultBody(): array - { - return $this->data ? ['data' => $this->data->toJsonApi()] : []; - } -} diff --git a/src/Requests/EntrySuggestion/EntrySuggestionsCollectionRequest.php b/src/Requests/EntrySuggestion/EntrySuggestionsCollectionRequest.php new file mode 100644 index 0000000..b88bf27 --- /dev/null +++ b/src/Requests/EntrySuggestion/EntrySuggestionsCollectionRequest.php @@ -0,0 +1,63 @@ +addInclude('activities'); + } + + public function createDtoFromResponse(Response $response): mixed + { + return Hydrator::hydrateCollection( + $this->model, + $response->json('data'), + $response->json('included') + ); + } + + public function resolveEndpoint(): string + { + return '/entry-suggestions'; + } + + /** + * @param null|int $pagesize The number of results that will be returned per page. + * @param null|int $pagenumber The page number to start the pagination from. + */ + public function __construct( + protected ?int $pagesize = null, + protected ?int $pagenumber = null, + ) {} + + public function defaultQuery(): array + { + return array_filter(['page[size]' => $this->pagesize, 'page[number]' => $this->pagenumber]); + } +} diff --git a/src/Requests/EntrySuggestion/DeleteEntrySuggestionRequest.php b/src/Requests/EntrySuggestion/EntrySuggestionsDestroyRequest.php similarity index 62% rename from src/Requests/EntrySuggestion/DeleteEntrySuggestionRequest.php rename to src/Requests/EntrySuggestion/EntrySuggestionsDestroyRequest.php index 1440bd3..28c1636 100644 --- a/src/Requests/EntrySuggestion/DeleteEntrySuggestionRequest.php +++ b/src/Requests/EntrySuggestion/EntrySuggestionsDestroyRequest.php @@ -8,9 +8,9 @@ use Saloon\Http\Request; /** - * deleteEntrySuggestion + * entry-suggestions.destroy */ -class DeleteEntrySuggestionRequest extends Request +class EntrySuggestionsDestroyRequest extends Request { protected Method $method = Method::DELETE; @@ -19,7 +19,10 @@ public function resolveEndpoint(): string return "/entry-suggestions/{$this->entrySuggestionId}"; } + /** + * @param int $entrySuggestionId The entry suggestion ID + */ public function __construct( - protected string $entrySuggestionId, + protected int $entrySuggestionId, ) {} } diff --git a/src/Requests/EntrySuggestion/GetEntrySuggestionRequest.php b/src/Requests/EntrySuggestion/EntrySuggestionsShowRequest.php similarity index 78% rename from src/Requests/EntrySuggestion/GetEntrySuggestionRequest.php rename to src/Requests/EntrySuggestion/EntrySuggestionsShowRequest.php index e821d8f..1ad0877 100644 --- a/src/Requests/EntrySuggestion/GetEntrySuggestionRequest.php +++ b/src/Requests/EntrySuggestion/EntrySuggestionsShowRequest.php @@ -11,9 +11,9 @@ use Timatic\Hydration\Facades\Hydrator; /** - * getEntrySuggestion + * entry-suggestions.show */ -class GetEntrySuggestionRequest extends Request +class EntrySuggestionsShowRequest extends Request { protected $model = EntrySuggestion::class; @@ -33,7 +33,10 @@ public function resolveEndpoint(): string return "/entry-suggestions/{$this->entrySuggestionId}"; } + /** + * @param int $entrySuggestionId The entry suggestion ID + */ public function __construct( - protected string $entrySuggestionId, + protected int $entrySuggestionId, ) {} } diff --git a/src/Requests/EntrySuggestion/GetEntrySuggestionsCollectionRequest.php b/src/Requests/EntrySuggestion/GetEntrySuggestionsCollectionRequest.php deleted file mode 100644 index 5bf1a74..0000000 --- a/src/Requests/EntrySuggestion/GetEntrySuggestionsCollectionRequest.php +++ /dev/null @@ -1,41 +0,0 @@ -model, - $response->json('data'), - $response->json('included') - ); - } - - public function resolveEndpoint(): string - { - return '/entry-suggestions'; - } - - public function __construct() {} -} diff --git a/src/Requests/Event/PostEventsRequest.php b/src/Requests/Event/EventsStoreRequest.php similarity index 93% rename from src/Requests/Event/PostEventsRequest.php rename to src/Requests/Event/EventsStoreRequest.php index 990bca0..4004382 100644 --- a/src/Requests/Event/PostEventsRequest.php +++ b/src/Requests/Event/EventsStoreRequest.php @@ -14,9 +14,9 @@ use Timatic\Hydration\Model; /** - * postEvents + * events.store */ -class PostEventsRequest extends Request implements HasBody +class EventsStoreRequest extends Request implements HasBody { use HasJsonBody; diff --git a/src/Requests/ExportMail/GetBudgetsExportMailsCollectionRequest.php b/src/Requests/ExportMail/GetBudgetsExportMailsCollectionRequest.php deleted file mode 100644 index 3565bbc..0000000 --- a/src/Requests/ExportMail/GetBudgetsExportMailsCollectionRequest.php +++ /dev/null @@ -1,38 +0,0 @@ -model, - $response->json('data'), - $response->json('included') - ); - } - - public function resolveEndpoint(): string - { - return '/budgets/export-mail'; - } - - public function __construct() {} -} diff --git a/src/Requests/Incident/GetIncidentRequest.php b/src/Requests/Incident/GetIncidentRequest.php deleted file mode 100644 index 6115264..0000000 --- a/src/Requests/Incident/GetIncidentRequest.php +++ /dev/null @@ -1,25 +0,0 @@ -incidentId}"; - } - - public function __construct( - protected string $incidentId, - ) {} -} diff --git a/src/Requests/Incident/GetIncidentsCollectionRequest.php b/src/Requests/Incident/GetIncidentsCollectionRequest.php deleted file mode 100644 index 3327690..0000000 --- a/src/Requests/Incident/GetIncidentsCollectionRequest.php +++ /dev/null @@ -1,24 +0,0 @@ -incidentId}"; - } - - public function __construct( - protected string $incidentId, - ) {} -} diff --git a/src/Requests/Overtime/GetOvertimesCollectionRequest.php b/src/Requests/Overtime/GetOvertimesCollectionRequest.php deleted file mode 100644 index 4575e61..0000000 --- a/src/Requests/Overtime/GetOvertimesCollectionRequest.php +++ /dev/null @@ -1,41 +0,0 @@ -model, - $response->json('data'), - $response->json('included') - ); - } - - public function resolveEndpoint(): string - { - return '/overtimes'; - } - - public function __construct() {} -} diff --git a/src/Requests/Approve/PostOvertimeApproveRequest.php b/src/Requests/Overtime/OvertimeApproveRequest.php similarity index 78% rename from src/Requests/Approve/PostOvertimeApproveRequest.php rename to src/Requests/Overtime/OvertimeApproveRequest.php index 92a7831..c4efb52 100644 --- a/src/Requests/Approve/PostOvertimeApproveRequest.php +++ b/src/Requests/Overtime/OvertimeApproveRequest.php @@ -2,25 +2,25 @@ // auto-generated -namespace Timatic\Requests\Approve; +namespace Timatic\Requests\Overtime; use Saloon\Contracts\Body\HasBody; use Saloon\Enums\Method; use Saloon\Http\Request; use Saloon\Http\Response; use Saloon\Traits\Body\HasJsonBody; -use Timatic\Dto\Approve; +use Timatic\Dto\Overtime; use Timatic\Hydration\Facades\Hydrator; use Timatic\Hydration\Model; /** - * postOvertimeApprove + * overtime.approve */ -class PostOvertimeApproveRequest extends Request implements HasBody +class OvertimeApproveRequest extends Request implements HasBody { use HasJsonBody; - protected $model = Approve::class; + protected $model = Overtime::class; protected Method $method = Method::POST; @@ -39,10 +39,11 @@ public function resolveEndpoint(): string } /** + * @param int $overtimeId The overtime ID * @param null|\Timatic\Hydration\Model|array|null $data Request data */ public function __construct( - protected string $overtimeId, + protected int $overtimeId, protected Model|array|null $data = null, ) {} diff --git a/src/Requests/MarkAsExported/PostOvertimeMarkAsExportedRequest.php b/src/Requests/Overtime/OvertimeMarkAsExportedRequest.php similarity index 77% rename from src/Requests/MarkAsExported/PostOvertimeMarkAsExportedRequest.php rename to src/Requests/Overtime/OvertimeMarkAsExportedRequest.php index e0ab9cb..d6942a6 100644 --- a/src/Requests/MarkAsExported/PostOvertimeMarkAsExportedRequest.php +++ b/src/Requests/Overtime/OvertimeMarkAsExportedRequest.php @@ -2,25 +2,25 @@ // auto-generated -namespace Timatic\Requests\MarkAsExported; +namespace Timatic\Requests\Overtime; use Saloon\Contracts\Body\HasBody; use Saloon\Enums\Method; use Saloon\Http\Request; use Saloon\Http\Response; use Saloon\Traits\Body\HasJsonBody; -use Timatic\Dto\MarkAsExported; +use Timatic\Dto\Overtime; use Timatic\Hydration\Facades\Hydrator; use Timatic\Hydration\Model; /** - * postOvertimeMarkAsExported + * overtime.mark-as-exported */ -class PostOvertimeMarkAsExportedRequest extends Request implements HasBody +class OvertimeMarkAsExportedRequest extends Request implements HasBody { use HasJsonBody; - protected $model = MarkAsExported::class; + protected $model = Overtime::class; protected Method $method = Method::POST; @@ -39,10 +39,11 @@ public function resolveEndpoint(): string } /** + * @param int $overtimeId The overtime ID * @param null|\Timatic\Hydration\Model|array|null $data Request data */ public function __construct( - protected string $overtimeId, + protected int $overtimeId, protected Model|array|null $data = null, ) {} diff --git a/src/Requests/Overtime/OvertimesCollectionRequest.php b/src/Requests/Overtime/OvertimesCollectionRequest.php new file mode 100644 index 0000000..a2e7188 --- /dev/null +++ b/src/Requests/Overtime/OvertimesCollectionRequest.php @@ -0,0 +1,71 @@ +addInclude('overtimeType'); + } + + /** + * Include the entry relationship in the response + */ + public function includeEntry(): static + { + return $this->addInclude('entry'); + } + + public function createDtoFromResponse(Response $response): mixed + { + return Hydrator::hydrateCollection( + $this->model, + $response->json('data'), + $response->json('included') + ); + } + + public function resolveEndpoint(): string + { + return '/overtimes'; + } + + /** + * @param null|int $pagesize The number of results that will be returned per page. + * @param null|int $pagenumber The page number to start the pagination from. + */ + public function __construct( + protected ?int $pagesize = null, + protected ?int $pagenumber = null, + ) {} + + public function defaultQuery(): array + { + return array_filter(['page[size]' => $this->pagesize, 'page[number]' => $this->pagenumber]); + } +} diff --git a/src/Requests/Period/GetBudgetPeriodsRequest.php b/src/Requests/Period/GetBudgetPeriodsRequest.php deleted file mode 100644 index 6674caf..0000000 --- a/src/Requests/Period/GetBudgetPeriodsRequest.php +++ /dev/null @@ -1,25 +0,0 @@ -budgetId}/periods"; - } - - public function __construct( - protected string $budgetId, - ) {} -} diff --git a/src/Requests/Team/PatchTeamRequest.php b/src/Requests/Team/PatchTeamRequest.php deleted file mode 100644 index 69174ed..0000000 --- a/src/Requests/Team/PatchTeamRequest.php +++ /dev/null @@ -1,53 +0,0 @@ -model, - $response->json('data'), - $response->json('included') - ); - } - - public function resolveEndpoint(): string - { - return "/teams/{$this->teamId}"; - } - - /** - * @param null|\Timatic\Hydration\Model|array|null $data Request data - */ - public function __construct( - protected string $teamId, - protected Model|array|null $data = null, - ) {} - - protected function defaultBody(): array - { - return $this->data ? ['data' => $this->data->toJsonApi()] : []; - } -} diff --git a/src/Requests/Team/GetTeamsCollectionRequest.php b/src/Requests/Team/TeamsCollectionRequest.php similarity index 89% rename from src/Requests/Team/GetTeamsCollectionRequest.php rename to src/Requests/Team/TeamsCollectionRequest.php index fd9b901..4e15424 100644 --- a/src/Requests/Team/GetTeamsCollectionRequest.php +++ b/src/Requests/Team/TeamsCollectionRequest.php @@ -12,9 +12,9 @@ use Timatic\Hydration\Facades\Hydrator; /** - * getTeams + * teams.index */ -class GetTeamsCollectionRequest extends Request implements Paginatable +class TeamsCollectionRequest extends Request implements Paginatable { protected $model = Team::class; diff --git a/src/Requests/Team/DeleteTeamRequest.php b/src/Requests/Team/TeamsDestroyRequest.php similarity index 68% rename from src/Requests/Team/DeleteTeamRequest.php rename to src/Requests/Team/TeamsDestroyRequest.php index 2a2ed14..13d9e77 100644 --- a/src/Requests/Team/DeleteTeamRequest.php +++ b/src/Requests/Team/TeamsDestroyRequest.php @@ -8,9 +8,9 @@ use Saloon\Http\Request; /** - * deleteTeam + * teams.destroy */ -class DeleteTeamRequest extends Request +class TeamsDestroyRequest extends Request { protected Method $method = Method::DELETE; @@ -19,7 +19,10 @@ public function resolveEndpoint(): string return "/teams/{$this->teamId}"; } + /** + * @param int $teamId The team ID + */ public function __construct( - protected string $teamId, + protected int $teamId, ) {} } diff --git a/src/Requests/Team/GetTeamRequest.php b/src/Requests/Team/TeamsShowRequest.php similarity index 82% rename from src/Requests/Team/GetTeamRequest.php rename to src/Requests/Team/TeamsShowRequest.php index 2b98a84..9fb5459 100644 --- a/src/Requests/Team/GetTeamRequest.php +++ b/src/Requests/Team/TeamsShowRequest.php @@ -11,9 +11,9 @@ use Timatic\Hydration\Facades\Hydrator; /** - * getTeam + * teams.show */ -class GetTeamRequest extends Request +class TeamsShowRequest extends Request { protected $model = Team::class; @@ -33,7 +33,10 @@ public function resolveEndpoint(): string return "/teams/{$this->teamId}"; } + /** + * @param int $teamId The team ID + */ public function __construct( - protected string $teamId, + protected int $teamId, ) {} } diff --git a/src/Requests/Team/PostTeamsRequest.php b/src/Requests/Team/TeamsStoreRequest.php similarity index 93% rename from src/Requests/Team/PostTeamsRequest.php rename to src/Requests/Team/TeamsStoreRequest.php index 853b84d..95b7c43 100644 --- a/src/Requests/Team/PostTeamsRequest.php +++ b/src/Requests/Team/TeamsStoreRequest.php @@ -14,9 +14,9 @@ use Timatic\Hydration\Model; /** - * postTeams + * teams.store */ -class PostTeamsRequest extends Request implements HasBody +class TeamsStoreRequest extends Request implements HasBody { use HasJsonBody; diff --git a/src/Requests/TimeSpentTotal/GetTimeSpentTotalsCollectionRequest.php b/src/Requests/TimeSpentTotal/GetTimeSpentTotalsCollectionRequest.php deleted file mode 100644 index 03fb0fc..0000000 --- a/src/Requests/TimeSpentTotal/GetTimeSpentTotalsCollectionRequest.php +++ /dev/null @@ -1,41 +0,0 @@ -model, - $response->json('data'), - $response->json('included') - ); - } - - public function resolveEndpoint(): string - { - return '/time-spent-totals'; - } - - public function __construct() {} -} diff --git a/src/Requests/User/GetUsersCollectionRequest.php b/src/Requests/User/GetUsersCollectionRequest.php deleted file mode 100644 index f0d3291..0000000 --- a/src/Requests/User/GetUsersCollectionRequest.php +++ /dev/null @@ -1,41 +0,0 @@ -model, - $response->json('data'), - $response->json('included') - ); - } - - public function resolveEndpoint(): string - { - return '/users'; - } - - public function __construct() {} -} diff --git a/src/Requests/User/PatchUserRequest.php b/src/Requests/User/PatchUserRequest.php deleted file mode 100644 index 1f38429..0000000 --- a/src/Requests/User/PatchUserRequest.php +++ /dev/null @@ -1,53 +0,0 @@ -model, - $response->json('data'), - $response->json('included') - ); - } - - public function resolveEndpoint(): string - { - return "/users/{$this->userId}"; - } - - /** - * @param null|\Timatic\Hydration\Model|array|null $data Request data - */ - public function __construct( - protected string $userId, - protected Model|array|null $data = null, - ) {} - - protected function defaultBody(): array - { - return $this->data ? ['data' => $this->data->toJsonApi()] : []; - } -} diff --git a/src/Requests/User/UsersCollectionRequest.php b/src/Requests/User/UsersCollectionRequest.php new file mode 100644 index 0000000..6608097 --- /dev/null +++ b/src/Requests/User/UsersCollectionRequest.php @@ -0,0 +1,71 @@ +addInclude('permissions'); + } + + /** + * Include the team relationship in the response + */ + public function includeTeam(): static + { + return $this->addInclude('team'); + } + + public function createDtoFromResponse(Response $response): mixed + { + return Hydrator::hydrateCollection( + $this->model, + $response->json('data'), + $response->json('included') + ); + } + + public function resolveEndpoint(): string + { + return '/users'; + } + + /** + * @param null|int $pagesize The number of results that will be returned per page. + * @param null|int $pagenumber The page number to start the pagination from. + */ + public function __construct( + protected ?int $pagesize = null, + protected ?int $pagenumber = null, + ) {} + + public function defaultQuery(): array + { + return array_filter(['page[size]' => $this->pagesize, 'page[number]' => $this->pagenumber]); + } +} diff --git a/src/Requests/User/DeleteUserRequest.php b/src/Requests/User/UsersDestroyRequest.php similarity index 68% rename from src/Requests/User/DeleteUserRequest.php rename to src/Requests/User/UsersDestroyRequest.php index db186f7..4a32c60 100644 --- a/src/Requests/User/DeleteUserRequest.php +++ b/src/Requests/User/UsersDestroyRequest.php @@ -8,9 +8,9 @@ use Saloon\Http\Request; /** - * deleteUser + * users.destroy */ -class DeleteUserRequest extends Request +class UsersDestroyRequest extends Request { protected Method $method = Method::DELETE; @@ -19,7 +19,10 @@ public function resolveEndpoint(): string return "/users/{$this->userId}"; } + /** + * @param int $userId The user ID + */ public function __construct( - protected string $userId, + protected int $userId, ) {} } diff --git a/src/Requests/User/GetUserRequest.php b/src/Requests/User/UsersShowRequest.php similarity index 82% rename from src/Requests/User/GetUserRequest.php rename to src/Requests/User/UsersShowRequest.php index 181fc19..2be49e5 100644 --- a/src/Requests/User/GetUserRequest.php +++ b/src/Requests/User/UsersShowRequest.php @@ -11,9 +11,9 @@ use Timatic\Hydration\Facades\Hydrator; /** - * getUser + * users.show */ -class GetUserRequest extends Request +class UsersShowRequest extends Request { protected $model = User::class; @@ -33,7 +33,10 @@ public function resolveEndpoint(): string return "/users/{$this->userId}"; } + /** + * @param int $userId The user ID + */ public function __construct( - protected string $userId, + protected int $userId, ) {} } diff --git a/src/Requests/User/PostUsersRequest.php b/src/Requests/User/UsersStoreRequest.php similarity index 93% rename from src/Requests/User/PostUsersRequest.php rename to src/Requests/User/UsersStoreRequest.php index 53dd639..cdd006e 100644 --- a/src/Requests/User/PostUsersRequest.php +++ b/src/Requests/User/UsersStoreRequest.php @@ -14,9 +14,9 @@ use Timatic\Hydration\Model; /** - * postUsers + * users.store */ -class PostUsersRequest extends Request implements HasBody +class UsersStoreRequest extends Request implements HasBody { use HasJsonBody; diff --git a/src/Requests/UserCustomerHoursAggregate/GetUserCustomerHoursAggregatesCollectionRequest.php b/src/Requests/UserCustomerHoursAggregate/GetUserCustomerHoursAggregatesCollectionRequest.php deleted file mode 100644 index e5e89d8..0000000 --- a/src/Requests/UserCustomerHoursAggregate/GetUserCustomerHoursAggregatesCollectionRequest.php +++ /dev/null @@ -1,41 +0,0 @@ -model, - $response->json('data'), - $response->json('included') - ); - } - - public function resolveEndpoint(): string - { - return '/user-customer-hours-aggregates'; - } - - public function __construct() {} -} diff --git a/src/TimaticConnector.php b/src/TimaticConnector.php index 4c3d9c5..63def6f 100644 --- a/src/TimaticConnector.php +++ b/src/TimaticConnector.php @@ -5,6 +5,7 @@ use Saloon\Http\Connector; use Saloon\Http\Request; use Saloon\PaginationPlugin\Contracts\HasPagination; +use Saloon\Traits\Plugins\AlwaysThrowOnErrors; use Timatic\Pagination\JsonApiPaginator; use Timatic\Responses\TimaticResponse; @@ -13,6 +14,8 @@ */ class TimaticConnector extends Connector implements HasPagination { + use AlwaysThrowOnErrors; + protected function defaultHeaders(): array { $headers = [ diff --git a/tests/Factories/BudgetFactoryTest.php b/tests/Factories/BudgetFactoryTest.php index 0b3844f..1c43db8 100644 --- a/tests/Factories/BudgetFactoryTest.php +++ b/tests/Factories/BudgetFactoryTest.php @@ -13,7 +13,7 @@ ->toBeInstanceOf(Budget::class) ->title->toBe('Project Budget') ->totalPrice->toBeString() - ->customerId->toBeString(); + ->customerId->toBeInt(); }); test('it can create multiple budgets using factory', function () { diff --git a/tests/Factories/MockingWithFactoriesTest.php b/tests/Factories/MockingWithFactoriesTest.php index 6adac82..1ee154f 100644 --- a/tests/Factories/MockingWithFactoriesTest.php +++ b/tests/Factories/MockingWithFactoriesTest.php @@ -3,8 +3,8 @@ use Saloon\Http\Faking\MockClient; use Saloon\Http\Faking\MockResponse; use Timatic\Dto\Budget; -use Timatic\Requests\Budget\GetBudgetsCollectionRequest; -use Timatic\Requests\Budget\PostBudgetsRequest; +use Timatic\Requests\Budget\BudgetsCollectionRequest; +use Timatic\Requests\Budget\BudgetsStoreRequest; use Timatic\TimaticConnector; test('it can mock a single budget response using factory', function () { @@ -14,7 +14,7 @@ ])->make(); $mockClient = new MockClient([ - GetBudgetsCollectionRequest::class => MockResponse::make([ + BudgetsCollectionRequest::class => MockResponse::make([ 'data' => [$budget->toJsonApi()], ], 200), ]); @@ -22,7 +22,7 @@ $connector = new TimaticConnector; $connector->withMockClient($mockClient); - $response = $connector->send(new GetBudgetsCollectionRequest); + $response = $connector->send(new BudgetsCollectionRequest); $dtos = $response->dto(); expect($dtos)->toBeInstanceOf(\Illuminate\Support\Collection::class); @@ -37,7 +37,7 @@ $budgets = Budget::factory()->withId()->count(3)->make(); $mockClient = new MockClient([ - GetBudgetsCollectionRequest::class => MockResponse::make([ + BudgetsCollectionRequest::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 GetBudgetsCollectionRequest); + $response = $connector->send(new BudgetsCollectionRequest); $dtos = $response->dto(); expect($dtos)->toHaveCount(3); @@ -57,7 +57,7 @@ $budgetToCreate = Budget::factory()->state([ 'title' => 'New Budget', 'totalPrice' => '5000.00', - 'customerId' => 'customer-123', + 'customerId' => 123, ])->make(); // Mock the response with an ID @@ -65,11 +65,11 @@ 'id' => 'created-456', 'title' => 'New Budget', 'totalPrice' => '5000.00', - 'customerId' => 'customer-123', + 'customerId' => 123, ])->make(); $mockClient = new MockClient([ - PostBudgetsRequest::class => MockResponse::make([ + BudgetsStoreRequest::class => MockResponse::make([ 'data' => $createdBudget->toJsonApi(), ], 201), ]); @@ -78,7 +78,7 @@ $connector->withMockClient($mockClient); // Send POST request - $response = $connector->send(new PostBudgetsRequest($budgetToCreate)); + $response = $connector->send(new BudgetsStoreRequest($budgetToCreate)); // Assert the request body was sent correctly $mockClient->assertSent(function (\Saloon\Http\Request $request) { @@ -87,7 +87,7 @@ return $body['data']['type'] === 'budgets' && $body['data']['attributes']['title'] === 'New Budget' && $body['data']['attributes']['totalPrice'] === '5000.00' - && $body['data']['attributes']['customerId'] === 'customer-123'; + && $body['data']['attributes']['customerId'] === 123; }); // Assert response @@ -100,7 +100,7 @@ ->id->toBe('created-456') ->title->toBe('New Budget') ->totalPrice->toBe('5000.00') - ->customerId->toBe('customer-123'); + ->customerId->toBe(123); }); test('it preserves all factory-generated attributes in json api format', function () { @@ -108,7 +108,7 @@ 'id' => '123', 'title' => 'Full Budget', 'totalPrice' => '5000.00', - 'customerId' => 'customer-456', + 'customerId' => 456, 'budgetTypeId' => 'type-789', 'showToCustomer' => true, 'isArchived' => false, @@ -119,7 +119,7 @@ expect($jsonApi) ->attributes->toHaveKey('title', 'Full Budget') ->toHaveKey('totalPrice', '5000.00') - ->toHaveKey('customerId', 'customer-456') + ->toHaveKey('customerId', 456) ->toHaveKey('budgetTypeId', 'type-789') ->toHaveKey('showToCustomer', true) ->toHaveKey('isArchived', false); diff --git a/tests/Feature/IncludesTest.php b/tests/Feature/IncludesTest.php new file mode 100644 index 0000000..6979031 --- /dev/null +++ b/tests/Feature/IncludesTest.php @@ -0,0 +1,226 @@ +timaticConnector = new TimaticConnector; +}); + +it('can include relationships using fluent API', function () { + Saloon::fake([ + BudgetsCollectionRequest::class => MockResponse::make([ + 'data' => [ + [ + 'type' => 'budgets', + 'id' => 'budget-1', + 'attributes' => [ + 'title' => 'Test Budget', + 'budgetTypeId' => 'type-1', + 'customerId' => 1, + ], + 'relationships' => [ + 'budgetType' => [ + 'data' => ['type' => 'budgetTypes', 'id' => 'type-1'], + ], + 'customer' => [ + 'data' => ['type' => 'customers', 'id' => 'customer-1'], + ], + 'entries' => [ + 'data' => [ + ['type' => 'entries', 'id' => 'entry-1'], + ['type' => 'entries', 'id' => 'entry-2'], + ], + ], + ], + ], + ], + 'included' => [ + [ + 'type' => 'budgetTypes', + 'id' => 'type-1', + 'attributes' => ['title' => 'Fixed Price'], + ], + [ + 'type' => 'customers', + 'id' => 'customer-1', + 'attributes' => ['name' => 'Acme Corp'], + ], + [ + 'type' => 'entries', + 'id' => 'entry-1', + 'attributes' => ['description' => 'Entry 1'], + ], + [ + 'type' => 'entries', + 'id' => 'entry-2', + 'attributes' => ['description' => 'Entry 2'], + ], + ], + ], 200), + ]); + + $request = (new BudgetsCollectionRequest) + ->includeBudgetType() + ->includeCustomer() + ->includeEntries(); + + $response = $this->timaticConnector->send($request); + + // Verify include parameter was sent + Saloon::assertSent(function (BudgetsCollectionRequest $request) { + $query = $request->query()->all(); + expect($query)->toHaveKey('include', 'budgetType,customer,entries'); + + return true; + }); + + $budgets = $response->dto(); + $budget = $budgets->first(); + + // Verify relationships were hydrated + expect($budget) + ->toBeInstanceOf(Budget::class) + ->budgetType->toBeInstanceOf(BudgetType::class) + ->budgetType->title->toBe('Fixed Price') + ->customer->toBeInstanceOf(Customer::class) + ->customer->name->toBe('Acme Corp') + ->entries->toHaveCount(2) + ->entries->first()->toBeInstanceOf(Entry::class) + ->entries->first()->description->toBe('Entry 1'); +}); + +it('can use generic include method', function () { + Saloon::fake([ + BudgetsCollectionRequest::class => MockResponse::make([ + 'data' => [], + ], 200), + ]); + + $request = (new BudgetsCollectionRequest) + ->include('budgetType', 'customer', 'entries'); + + $this->timaticConnector->send($request); + + Saloon::assertSent(function (BudgetsCollectionRequest $request) { + $query = $request->query()->all(); + expect($query)->toHaveKey('include', 'budgetType,customer,entries'); + + return true; + }); +}); + +it('does not send include parameter when no includes are added', function () { + Saloon::fake([ + BudgetsCollectionRequest::class => MockResponse::make([ + 'data' => [], + ], 200), + ]); + + $request = new BudgetsCollectionRequest; + + $this->timaticConnector->send($request); + + Saloon::assertSent(function (BudgetsCollectionRequest $request) { + $query = $request->query()->all(); + expect($query)->not->toHaveKey('include'); + + return true; + }); +}); + +it('handles missing included data gracefully', function () { + Saloon::fake([ + BudgetsCollectionRequest::class => MockResponse::make([ + 'data' => [ + [ + 'type' => 'budgets', + 'id' => 'budget-1', + 'attributes' => [ + 'title' => 'Test Budget', + ], + 'relationships' => [ + 'budgetType' => [ + 'data' => ['type' => 'budgetTypes', 'id' => 'type-1'], + ], + ], + ], + ], + 'included' => [], // No included data + ], 200), + ]); + + $request = (new BudgetsCollectionRequest) + ->includeBudgetType(); + + $response = $this->timaticConnector->send($request); + $budgets = $response->dto(); + $budget = $budgets->first(); + + // Relationship should be null when not in included array + expect($budget->budgetType)->toBeNull(); +}); + +it('handles many relationships correctly', function () { + Saloon::fake([ + BudgetsCollectionRequest::class => MockResponse::make([ + 'data' => [ + [ + 'type' => 'budgets', + 'id' => 'budget-1', + 'attributes' => [ + 'title' => 'Test Budget', + ], + 'relationships' => [ + 'entries' => [ + 'data' => [ + ['type' => 'entries', 'id' => 'entry-1'], + ['type' => 'entries', 'id' => 'entry-2'], + ['type' => 'entries', 'id' => 'entry-3'], + ], + ], + ], + ], + ], + 'included' => [ + [ + 'type' => 'entries', + 'id' => 'entry-1', + 'attributes' => ['description' => 'First Entry'], + ], + [ + 'type' => 'entries', + 'id' => 'entry-2', + 'attributes' => ['description' => 'Second Entry'], + ], + [ + 'type' => 'entries', + 'id' => 'entry-3', + 'attributes' => ['description' => 'Third Entry'], + ], + ], + ], 200), + ]); + + $request = (new BudgetsCollectionRequest) + ->includeEntries(); + + $response = $this->timaticConnector->send($request); + $budget = $response->dto()->first(); + + expect($budget->entries) + ->toHaveCount(3) + ->each(fn ($entry) => $entry->toBeInstanceOf(Entry::class)); + + expect($budget->entries->pluck('description')->toArray())->toBe([ + 'First Entry', + 'Second Entry', + 'Third Entry', + ]); +}); diff --git a/tests/Pagination/JsonApiPaginatorTest.php b/tests/Pagination/JsonApiPaginatorTest.php index 00347b0..e721dc7 100644 --- a/tests/Pagination/JsonApiPaginatorTest.php +++ b/tests/Pagination/JsonApiPaginatorTest.php @@ -4,7 +4,7 @@ use Saloon\Http\Request; use Saloon\Laravel\Facades\Saloon; use Timatic\Dto\Entry; -use Timatic\Requests\Entry\GetEntriesCollectionRequest; +use Timatic\Requests\Entry\EntriesCollectionRequest; use Timatic\TimaticConnector; beforeEach(function () { @@ -14,7 +14,7 @@ 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([ + EntriesCollectionRequest::class => MockResponse::make([ 'data' => [ [ 'type' => 'entries', @@ -49,7 +49,7 @@ ], 200), ]); - $request = new GetEntriesCollectionRequest; + $request = new EntriesCollectionRequest; $paginator = $this->timaticConnector->paginate($request); // Get items from first page (convert Generator to array) @@ -123,7 +123,7 @@ ]); - $request = new GetEntriesCollectionRequest; + $request = new EntriesCollectionRequest; $paginator = $this->timaticConnector->paginate($request); // Collect all items across all pages using items() method @@ -140,20 +140,20 @@ it('applies pagination query parameters correctly', function () { Saloon::fake([ - GetEntriesCollectionRequest::class => MockResponse::make([ + EntriesCollectionRequest::class => MockResponse::make([ 'data' => [], 'links' => ['next' => null], ], 200), ]); $paginator = $this->timaticConnector - ->paginate(new GetEntriesCollectionRequest) + ->paginate(new EntriesCollectionRequest) ->setPerPageLimit(123); $paginator->dtoCollection(); // Verify the request had the correct query parameters - Saloon::assertSent(function (GetEntriesCollectionRequest $request) { + Saloon::assertSent(function (EntriesCollectionRequest $request) { $query = $request->query()->all(); expect($query)->toHaveKey('page[number]', 1); diff --git a/tests/Requests/ApproveTest.php b/tests/Requests/ApproveTest.php deleted file mode 100644 index 7fa7d21..0000000 --- a/tests/Requests/ApproveTest.php +++ /dev/null @@ -1,45 +0,0 @@ -timaticConnector = new Timatic\TimaticConnector; -}); - -it('calls the postOvertimeApprove method in the Approve resource', function () { - $mockClient = Saloon::fake([ - PostOvertimeApproveRequest::class => MockResponse::make([], 200), - ]); - - // Create DTO with sample data - $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); - - Saloon::assertSent(PostOvertimeApproveRequest::class); - - $mockClient->assertSent(function (Request $request) { - expect($request->body()->all()) - ->toHaveKey('data') - ->data->type->toBe('approves') - ->data->attributes->scoped(fn ($attributes) => $attributes - ->entryId->toBe('entry_id-123') - ->overtimeTypeId->toBe('overtime_type_id-123') - ->startedAt->toEqual(new \Carbon\Carbon('2025-01-15T10:30:00Z')) - ->endedAt->toEqual(new \Carbon\Carbon('2025-01-15T10:30:00Z')) - ); - - return true; - }); -}); diff --git a/tests/Requests/BudgetTest.php b/tests/Requests/BudgetTest.php index 49447d3..16a8b8b 100644 --- a/tests/Requests/BudgetTest.php +++ b/tests/Requests/BudgetTest.php @@ -6,26 +6,25 @@ use Saloon\Http\Faking\MockResponse; use Saloon\Http\Request; use Saloon\Laravel\Facades\Saloon; -use Timatic\Requests\Budget\DeleteBudgetRequest; -use Timatic\Requests\Budget\GetBudgetRequest; -use Timatic\Requests\Budget\GetBudgetsCollectionRequest; -use Timatic\Requests\Budget\PatchBudgetRequest; -use Timatic\Requests\Budget\PostBudgetsRequest; +use Timatic\Requests\Budget\BudgetsCollectionRequest; +use Timatic\Requests\Budget\BudgetsDestroyRequest; +use Timatic\Requests\Budget\BudgetsShowRequest; +use Timatic\Requests\Budget\BudgetsStoreRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getBudgetsCollection method in the Budget resource', function () { +it('calls the budgetsCollection method in the Budget resource', function () { Saloon::fake([ - GetBudgetsCollectionRequest::class => MockResponse::make([ + BudgetsCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'budgets', 'id' => 'mock-id-1', 'attributes' => [ 'budgetTypeId' => 'mock-id-123', - 'customerId' => 'mock-id-123', + 'customerId' => 42, 'showToCustomer' => true, 'changeId' => 'mock-id-123', 'contractId' => 'mock-id-123', @@ -37,7 +36,29 @@ 'initialMinutes' => 42, 'isArchived' => true, 'renewalFrequency' => 'Mock value', - 'supervisorUserId' => 'mock-id-123', + 'supervisorUserId' => 42, + ], + 'relationships' => [ + 'entries' => [ + 'data' => [ + 0 => [ + 'type' => 'entries', + 'id' => 'related-entries-1', + ], + ], + ], + 'budgetType' => [ + 'data' => [ + 'type' => 'budgettypes', + 'id' => 'related-budgetType-1', + ], + ], + 'customer' => [ + 'data' => [ + 'type' => 'customers', + 'id' => 'related-customer-1', + ], + ], ], ], 1 => [ @@ -45,7 +66,7 @@ 'id' => 'mock-id-2', 'attributes' => [ 'budgetTypeId' => 'mock-id-123', - 'customerId' => 'mock-id-123', + 'customerId' => 42, 'showToCustomer' => true, 'changeId' => 'mock-id-123', 'contractId' => 'mock-id-123', @@ -57,29 +78,68 @@ 'initialMinutes' => 42, 'isArchived' => true, 'renewalFrequency' => 'Mock value', - 'supervisorUserId' => 'mock-id-123', + 'supervisorUserId' => 42, + ], + 'relationships' => [ + 'entries' => [ + 'data' => [ + 0 => [ + 'type' => 'entries', + 'id' => 'related-entries-1', + ], + ], + ], + 'budgetType' => [ + 'data' => [ + 'type' => 'budgettypes', + 'id' => 'related-budgetType-1', + ], + ], + 'customer' => [ + 'data' => [ + 'type' => 'customers', + 'id' => 'related-customer-1', + ], + ], ], ], ], + 'included' => [ + 0 => [ + 'type' => 'entries', + 'id' => 'related-entries-1', + 'attributes' => [], + ], + 1 => [ + 'type' => 'budgettypes', + 'id' => 'related-budgetType-1', + 'attributes' => [], + ], + 2 => [ + 'type' => 'customers', + 'id' => 'related-customer-1', + 'attributes' => [], + ], + ], ], 200), ]); - $request = (new GetBudgetsCollectionRequest(include: 'test string')) + $request = (new BudgetsCollectionRequest(pagesize: 123, pagenumber: 123)) ->filter('customerId', 'customer_id-123') ->filter('budgetTypeId', 'budget_type_id-123') - ->filter('isArchived', true); + ->filter('isArchived', true) + ->includeEntries() + ->includeBudgetType() + ->includeCustomer(); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetBudgetsCollectionRequest::class); - - // Verify filter query parameters are present - Saloon::assertSent(function (Request $request) { + Saloon::assertSent(function (BudgetsCollectionRequest $request) { $query = $request->query()->all(); - expect($query)->toHaveKey('filter[customerId]', 'customer_id-123'); expect($query)->toHaveKey('filter[budgetTypeId]', 'budget_type_id-123'); expect($query)->toHaveKey('filter[isArchived]', true); + expect($query)->toHaveKey('include', 'entries,budgetType,customer'); return true; }); @@ -90,7 +150,7 @@ expect($dtoCollection->first()) ->budgetTypeId->toBe('mock-id-123') - ->customerId->toBe('mock-id-123') + ->customerId->toBe(42) ->showToCustomer->toBe(true) ->changeId->toBe('mock-id-123') ->contractId->toBe('mock-id-123') @@ -102,26 +162,29 @@ ->initialMinutes->toBe(42) ->isArchived->toBe(true) ->renewalFrequency->toBe('Mock value') - ->supervisorUserId->toBe('mock-id-123'); + ->supervisorUserId->toBe(42) + ->entries->not->toBeNull() + ->budgetType->toBeInstanceOf(\Timatic\Dto\BudgetType::class) + ->customer->toBeInstanceOf(\Timatic\Dto\Customer::class); }); -it('calls the postBudgets method in the Budget resource', function () { +it('calls the budgetsStore method in the Budget resource', function () { $mockClient = Saloon::fake([ - PostBudgetsRequest::class => MockResponse::make([], 200), + BudgetsStoreRequest::class => MockResponse::make([], 200), ]); // Create DTO with sample data $dto = \Timatic\Dto\Budget::factory()->state([ 'budgetTypeId' => 'budget_type_id-123', - 'customerId' => 'customer_id-123', + 'customerId' => 42, 'showToCustomer' => true, 'changeId' => 'change_id-123', ])->make(); - $request = new PostBudgetsRequest($dto); + $request = new BudgetsStoreRequest($dto); $this->timaticConnector->send($request); - Saloon::assertSent(PostBudgetsRequest::class); + Saloon::assertSent(BudgetsStoreRequest::class); $mockClient->assertSent(function (Request $request) { expect($request->body()->all()) @@ -129,7 +192,7 @@ ->data->type->toBe('budgets') ->data->attributes->scoped(fn ($attributes) => $attributes ->budgetTypeId->toBe('budget_type_id-123') - ->customerId->toBe('customer_id-123') + ->customerId->toBe(42) ->showToCustomer->toBe(true) ->changeId->toBe('change_id-123') ); @@ -138,15 +201,15 @@ }); }); -it('calls the getBudget method in the Budget resource', function () { +it('calls the budgetsShow method in the Budget resource', function () { Saloon::fake([ - GetBudgetRequest::class => MockResponse::make([ + BudgetsShowRequest::class => MockResponse::make([ 'data' => [ 'type' => 'budgets', 'id' => 'mock-id-123', 'attributes' => [ 'budgetTypeId' => 'mock-id-123', - 'customerId' => 'mock-id-123', + 'customerId' => 42, 'showToCustomer' => true, 'changeId' => 'mock-id-123', 'contractId' => 'mock-id-123', @@ -158,18 +221,18 @@ 'initialMinutes' => 42, 'isArchived' => true, 'renewalFrequency' => 'Mock value', - 'supervisorUserId' => 'mock-id-123', + 'supervisorUserId' => 42, ], ], ], 200), ]); - $request = new GetBudgetRequest( - budgetId: 'test string' + $request = new BudgetsShowRequest( + budgetId: 123 ); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetBudgetRequest::class); + Saloon::assertSent(BudgetsShowRequest::class); expect($response->status())->toBe(200); @@ -177,7 +240,7 @@ expect($dto) ->budgetTypeId->toBe('mock-id-123') - ->customerId->toBe('mock-id-123') + ->customerId->toBe(42) ->showToCustomer->toBe(true) ->changeId->toBe('mock-id-123') ->contractId->toBe('mock-id-123') @@ -189,53 +252,20 @@ ->initialMinutes->toBe(42) ->isArchived->toBe(true) ->renewalFrequency->toBe('Mock value') - ->supervisorUserId->toBe('mock-id-123'); + ->supervisorUserId->toBe(42); }); -it('calls the deleteBudget method in the Budget resource', function () { +it('calls the budgetsDestroy method in the Budget resource', function () { Saloon::fake([ - DeleteBudgetRequest::class => MockResponse::make([], 200), + BudgetsDestroyRequest::class => MockResponse::make([], 200), ]); - $request = new DeleteBudgetRequest( - budgetId: 'test string' + $request = new BudgetsDestroyRequest( + budgetId: 123 ); $response = $this->timaticConnector->send($request); - Saloon::assertSent(DeleteBudgetRequest::class); + Saloon::assertSent(BudgetsDestroyRequest::class); expect($response->status())->toBe(200); }); - -it('calls the patchBudget method in the Budget resource', function () { - $mockClient = Saloon::fake([ - PatchBudgetRequest::class => MockResponse::make([], 200), - ]); - - // Create DTO with sample data - $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); - - Saloon::assertSent(PatchBudgetRequest::class); - - $mockClient->assertSent(function (Request $request) { - expect($request->body()->all()) - ->toHaveKey('data') - ->data->type->toBe('budgets') - ->data->attributes->scoped(fn ($attributes) => $attributes - ->budgetTypeId->toBe('budget_type_id-123') - ->customerId->toBe('customer_id-123') - ->showToCustomer->toBe(true) - ->changeId->toBe('change_id-123') - ); - - return true; - }); -}); diff --git a/tests/Requests/BudgetTimeSpentTotalTest.php b/tests/Requests/BudgetTimeSpentTotalTest.php deleted file mode 100644 index f43ee9f..0000000 --- a/tests/Requests/BudgetTimeSpentTotalTest.php +++ /dev/null @@ -1,71 +0,0 @@ -timaticConnector = new Timatic\TimaticConnector; -}); - -it('calls the getBudgetTimeSpentTotalsCollection method in the BudgetTimeSpentTotal resource', function () { - Saloon::fake([ - GetBudgetTimeSpentTotalsCollectionRequest::class => MockResponse::make([ - 'data' => [ - 0 => [ - 'type' => 'budgetTimeSpentTotals', - 'id' => 'mock-id-1', - 'attributes' => [ - 'start' => '2025-11-22T10:40:04.065Z', - 'end' => '2025-11-22T10:40:04.065Z', - 'remainingMinutes' => 42, - 'periodUnit' => 'Mock value', - 'periodValue' => 42, - ], - ], - 1 => [ - 'type' => 'budgetTimeSpentTotals', - 'id' => 'mock-id-2', - 'attributes' => [ - 'start' => '2025-11-22T10:40:04.065Z', - 'end' => '2025-11-22T10:40:04.065Z', - 'remainingMinutes' => 42, - 'periodUnit' => 'Mock value', - 'periodValue' => 42, - ], - ], - ], - ], 200), - ]); - - $request = (new GetBudgetTimeSpentTotalsCollectionRequest) - ->filter('budgetId', 'budget_id-123'); - - $response = $this->timaticConnector->send($request); - - Saloon::assertSent(GetBudgetTimeSpentTotalsCollectionRequest::class); - - // Verify filter query parameters are present - Saloon::assertSent(function (Request $request) { - $query = $request->query()->all(); - - expect($query)->toHaveKey('filter[budgetId]', 'budget_id-123'); - - return true; - }); - - expect($response->status())->toBe(200); - - $dtoCollection = $response->dto(); - - expect($dtoCollection->first()) - ->start->toEqual(new Carbon('2025-11-22T10:40:04.065Z')) - ->end->toEqual(new Carbon('2025-11-22T10:40:04.065Z')) - ->remainingMinutes->toBe(42) - ->periodUnit->toBe('Mock value') - ->periodValue->toBe(42); -}); diff --git a/tests/Requests/BudgetTypeTest.php b/tests/Requests/BudgetTypeTest.php index 96292a6..fc89f5d 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\GetBudgetTypesCollectionRequest; +use Timatic\Requests\BudgetType\BudgetTypesCollectionRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getBudgetTypesCollection method in the BudgetType resource', function () { +it('calls the budgetTypesCollection method in the BudgetType resource', function () { Saloon::fake([ - GetBudgetTypesCollectionRequest::class => MockResponse::make([ + BudgetTypesCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'budgetTypes', @@ -21,7 +21,7 @@ 'title' => 'Mock value', 'isArchived' => true, 'hasChangeTicket' => true, - 'renewalFrequencies' => 'Mock value', + 'renewalFrequencies' => [], 'hasSupervisor' => true, 'hasContractId' => true, 'hasTotalPrice' => true, @@ -36,7 +36,7 @@ 'title' => 'Mock value', 'isArchived' => true, 'hasChangeTicket' => true, - 'renewalFrequencies' => 'Mock value', + 'renewalFrequencies' => [], 'hasSupervisor' => true, 'hasContractId' => true, 'hasTotalPrice' => true, @@ -48,11 +48,15 @@ ], 200), ]); - $request = (new GetBudgetTypesCollectionRequest); + $request = (new BudgetTypesCollectionRequest); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetBudgetTypesCollectionRequest::class); + Saloon::assertSent(function (BudgetTypesCollectionRequest $request) { + $query = $request->query()->all(); + + return true; + }); expect($response->status())->toBe(200); @@ -62,7 +66,6 @@ ->title->toBe('Mock value') ->isArchived->toBe(true) ->hasChangeTicket->toBe(true) - ->renewalFrequencies->toBe('Mock value') ->hasSupervisor->toBe(true) ->hasContractId->toBe(true) ->hasTotalPrice->toBe(true) diff --git a/tests/Requests/CorrectionTest.php b/tests/Requests/CorrectionTest.php index 88af97d..359377f 100644 --- a/tests/Requests/CorrectionTest.php +++ b/tests/Requests/CorrectionTest.php @@ -5,16 +5,15 @@ use Saloon\Http\Faking\MockResponse; use Saloon\Http\Request; use Saloon\Laravel\Facades\Saloon; -use Timatic\Requests\Correction\PatchCorrectionRequest; -use Timatic\Requests\Correction\PostCorrectionsRequest; +use Timatic\Requests\Correction\CorrectionsStoreRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the postCorrections method in the Correction resource', function () { +it('calls the correctionsStore method in the Correction resource', function () { $mockClient = Saloon::fake([ - PostCorrectionsRequest::class => MockResponse::make([], 200), + CorrectionsStoreRequest::class => MockResponse::make([], 200), ]); // Create DTO with sample data @@ -22,34 +21,10 @@ 'name' => 'test value', ])->make(); - $request = new PostCorrectionsRequest($dto); + $request = new CorrectionsStoreRequest($dto); $this->timaticConnector->send($request); - Saloon::assertSent(PostCorrectionsRequest::class); - - $mockClient->assertSent(function (Request $request) { - expect($request->body()->all()) - ->toHaveKey('data') - ->data->type->toBe('corrections'); - - return true; - }); -}); - -it('calls the patchCorrection method in the Correction resource', function () { - $mockClient = Saloon::fake([ - PatchCorrectionRequest::class => MockResponse::make([], 200), - ]); - - // Create DTO with sample data - $dto = \Timatic\Dto\Correction::factory()->state([ - 'name' => 'test value', - ])->make(); - - $request = new PatchCorrectionRequest(correctionId: 'test string', data: $dto); - $this->timaticConnector->send($request); - - Saloon::assertSent(PatchCorrectionRequest::class); + Saloon::assertSent(CorrectionsStoreRequest::class); $mockClient->assertSent(function (Request $request) { expect($request->body()->all()) diff --git a/tests/Requests/CustomerTest.php b/tests/Requests/CustomerTest.php index 0c851cb..93bfe95 100644 --- a/tests/Requests/CustomerTest.php +++ b/tests/Requests/CustomerTest.php @@ -5,19 +5,18 @@ use Saloon\Http\Faking\MockResponse; use Saloon\Http\Request; use Saloon\Laravel\Facades\Saloon; -use Timatic\Requests\Customer\DeleteCustomerRequest; -use Timatic\Requests\Customer\GetCustomerRequest; -use Timatic\Requests\Customer\GetCustomersCollectionRequest; -use Timatic\Requests\Customer\PatchCustomerRequest; -use Timatic\Requests\Customer\PostCustomersRequest; +use Timatic\Requests\Customer\CustomersCollectionRequest; +use Timatic\Requests\Customer\CustomersDestroyRequest; +use Timatic\Requests\Customer\CustomersShowRequest; +use Timatic\Requests\Customer\CustomersStoreRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getCustomersCollection method in the Customer resource', function () { +it('calls the customersCollection method in the Customer resource', function () { Saloon::fake([ - GetCustomersCollectionRequest::class => MockResponse::make([ + CustomersCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'customers', @@ -26,7 +25,7 @@ 'externalId' => 'mock-id-123', 'name' => 'Mock value', 'hourlyRate' => 'Mock value', - 'accountManagerUserId' => 'mock-id-123', + 'accountManagerUserId' => 42, ], ], 1 => [ @@ -36,24 +35,20 @@ 'externalId' => 'mock-id-123', 'name' => 'Mock value', 'hourlyRate' => 'Mock value', - 'accountManagerUserId' => 'mock-id-123', + 'accountManagerUserId' => 42, ], ], ], ], 200), ]); - $request = (new GetCustomersCollectionRequest) + $request = (new CustomersCollectionRequest(pagesize: 123, pagenumber: 123)) ->filter('externalId', 'external_id-123'); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetCustomersCollectionRequest::class); - - // Verify filter query parameters are present - Saloon::assertSent(function (Request $request) { + Saloon::assertSent(function (CustomersCollectionRequest $request) { $query = $request->query()->all(); - expect($query)->toHaveKey('filter[externalId]', 'external_id-123'); return true; @@ -67,12 +62,12 @@ ->externalId->toBe('mock-id-123') ->name->toBe('Mock value') ->hourlyRate->toBe('Mock value') - ->accountManagerUserId->toBe('mock-id-123'); + ->accountManagerUserId->toBe(42); }); -it('calls the postCustomers method in the Customer resource', function () { +it('calls the customersStore method in the Customer resource', function () { $mockClient = Saloon::fake([ - PostCustomersRequest::class => MockResponse::make([], 200), + CustomersStoreRequest::class => MockResponse::make([], 200), ]); // Create DTO with sample data @@ -80,13 +75,13 @@ 'externalId' => 'external_id-123', 'name' => 'test name', 'hourlyRate' => 'test value', - 'accountManagerUserId' => 'account_manager_user_id-123', + 'accountManagerUserId' => 42, ])->make(); - $request = new PostCustomersRequest($dto); + $request = new CustomersStoreRequest($dto); $this->timaticConnector->send($request); - Saloon::assertSent(PostCustomersRequest::class); + Saloon::assertSent(CustomersStoreRequest::class); $mockClient->assertSent(function (Request $request) { expect($request->body()->all()) @@ -96,16 +91,16 @@ ->externalId->toBe('external_id-123') ->name->toBe('test name') ->hourlyRate->toBe('test value') - ->accountManagerUserId->toBe('account_manager_user_id-123') + ->accountManagerUserId->toBe(42) ); return true; }); }); -it('calls the getCustomer method in the Customer resource', function () { +it('calls the customersShow method in the Customer resource', function () { Saloon::fake([ - GetCustomerRequest::class => MockResponse::make([ + CustomersShowRequest::class => MockResponse::make([ 'data' => [ 'type' => 'customers', 'id' => 'mock-id-123', @@ -113,18 +108,18 @@ 'externalId' => 'mock-id-123', 'name' => 'Mock value', 'hourlyRate' => 'Mock value', - 'accountManagerUserId' => 'mock-id-123', + 'accountManagerUserId' => 42, ], ], ], 200), ]); - $request = new GetCustomerRequest( - customerId: 'test string' + $request = new CustomersShowRequest( + customerId: 123 ); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetCustomerRequest::class); + Saloon::assertSent(CustomersShowRequest::class); expect($response->status())->toBe(200); @@ -134,53 +129,20 @@ ->externalId->toBe('mock-id-123') ->name->toBe('Mock value') ->hourlyRate->toBe('Mock value') - ->accountManagerUserId->toBe('mock-id-123'); + ->accountManagerUserId->toBe(42); }); -it('calls the deleteCustomer method in the Customer resource', function () { +it('calls the customersDestroy method in the Customer resource', function () { Saloon::fake([ - DeleteCustomerRequest::class => MockResponse::make([], 200), + CustomersDestroyRequest::class => MockResponse::make([], 200), ]); - $request = new DeleteCustomerRequest( - customerId: 'test string' + $request = new CustomersDestroyRequest( + customerId: 123 ); $response = $this->timaticConnector->send($request); - Saloon::assertSent(DeleteCustomerRequest::class); + Saloon::assertSent(CustomersDestroyRequest::class); expect($response->status())->toBe(200); }); - -it('calls the patchCustomer method in the Customer resource', function () { - $mockClient = Saloon::fake([ - PatchCustomerRequest::class => MockResponse::make([], 200), - ]); - - // Create DTO with sample data - $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); - - Saloon::assertSent(PatchCustomerRequest::class); - - $mockClient->assertSent(function (Request $request) { - expect($request->body()->all()) - ->toHaveKey('data') - ->data->type->toBe('customers') - ->data->attributes->scoped(fn ($attributes) => $attributes - ->externalId->toBe('external_id-123') - ->name->toBe('test name') - ->hourlyRate->toBe('test value') - ->accountManagerUserId->toBe('account_manager_user_id-123') - ); - - return true; - }); -}); diff --git a/tests/Requests/DailyProgressTest.php b/tests/Requests/DailyProgressTest.php deleted file mode 100644 index 6ea676e..0000000 --- a/tests/Requests/DailyProgressTest.php +++ /dev/null @@ -1,54 +0,0 @@ -timaticConnector = new Timatic\TimaticConnector; -}); - -it('calls the getDailyProgressesCollection method in the DailyProgress resource', function () { - Saloon::fake([ - GetDailyProgressesCollectionRequest::class => MockResponse::make([ - 'data' => [ - 0 => [ - 'type' => 'dailyProgresses', - 'id' => 'mock-id-1', - 'attributes' => [ - 'userId' => 'mock-id-123', - 'date' => '2025-11-22T10:40:04.065Z', - 'progress' => 'Mock value', - ], - ], - 1 => [ - 'type' => 'dailyProgresses', - 'id' => 'mock-id-2', - 'attributes' => [ - 'userId' => 'mock-id-123', - 'date' => '2025-11-22T10:40:04.065Z', - 'progress' => 'Mock value', - ], - ], - ], - ], 200), - ]); - - $request = (new GetDailyProgressesCollectionRequest); - - $response = $this->timaticConnector->send($request); - - Saloon::assertSent(GetDailyProgressesCollectionRequest::class); - - expect($response->status())->toBe(200); - - $dtoCollection = $response->dto(); - - expect($dtoCollection->first()) - ->userId->toBe('mock-id-123') - ->date->toEqual(new Carbon('2025-11-22T10:40:04.065Z')) - ->progress->toBe('Mock value'); -}); diff --git a/tests/Requests/EntrySuggestionTest.php b/tests/Requests/EntrySuggestionTest.php index a11e1c9..dfe1a50 100644 --- a/tests/Requests/EntrySuggestionTest.php +++ b/tests/Requests/EntrySuggestionTest.php @@ -3,19 +3,18 @@ // auto-generated use Saloon\Http\Faking\MockResponse; -use Saloon\Http\Request; use Saloon\Laravel\Facades\Saloon; -use Timatic\Requests\EntrySuggestion\DeleteEntrySuggestionRequest; -use Timatic\Requests\EntrySuggestion\GetEntrySuggestionRequest; -use Timatic\Requests\EntrySuggestion\GetEntrySuggestionsCollectionRequest; +use Timatic\Requests\EntrySuggestion\EntrySuggestionsCollectionRequest; +use Timatic\Requests\EntrySuggestion\EntrySuggestionsDestroyRequest; +use Timatic\Requests\EntrySuggestion\EntrySuggestionsShowRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getEntrySuggestionsCollection method in the EntrySuggestion resource', function () { +it('calls the entrySuggestionsCollection method in the EntrySuggestion resource', function () { Saloon::fake([ - GetEntrySuggestionsCollectionRequest::class => MockResponse::make([ + EntrySuggestionsCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'entrySuggestions', @@ -24,11 +23,21 @@ 'ticketId' => 'mock-id-123', 'ticketNumber' => 'Mock value', 'customerId' => 'mock-id-123', - 'userId' => 'mock-id-123', + 'userId' => 42, 'date' => 'Mock value', 'ticketTitle' => 'Mock value', 'ticketType' => 'Mock value', - 'budgetId' => 'mock-id-123', + 'budgetId' => 42, + ], + 'relationships' => [ + 'activities' => [ + 'data' => [ + 0 => [ + 'type' => 'activities', + 'id' => 'related-activities-1', + ], + ], + ], ], ], 1 => [ @@ -38,29 +47,44 @@ 'ticketId' => 'mock-id-123', 'ticketNumber' => 'Mock value', 'customerId' => 'mock-id-123', - 'userId' => 'mock-id-123', + 'userId' => 42, 'date' => 'Mock value', 'ticketTitle' => 'Mock value', 'ticketType' => 'Mock value', - 'budgetId' => 'mock-id-123', + 'budgetId' => 42, + ], + 'relationships' => [ + 'activities' => [ + 'data' => [ + 0 => [ + 'type' => 'activities', + 'id' => 'related-activities-1', + ], + ], + ], ], ], ], + 'included' => [ + 0 => [ + 'type' => 'activities', + 'id' => 'related-activities-1', + 'attributes' => [], + ], + ], ], 200), ]); - $request = (new GetEntrySuggestionsCollectionRequest) - ->filter('date', 'test value'); + $request = (new EntrySuggestionsCollectionRequest(pagesize: 123, pagenumber: 123)) + ->filter('date', 'test value') + ->includeActivities(); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetEntrySuggestionsCollectionRequest::class); - - // Verify filter query parameters are present - Saloon::assertSent(function (Request $request) { + Saloon::assertSent(function (EntrySuggestionsCollectionRequest $request) { $query = $request->query()->all(); - expect($query)->toHaveKey('filter[date]', 'test value'); + expect($query)->toHaveKey('include', 'activities'); return true; }); @@ -73,16 +97,17 @@ ->ticketId->toBe('mock-id-123') ->ticketNumber->toBe('Mock value') ->customerId->toBe('mock-id-123') - ->userId->toBe('mock-id-123') + ->userId->toBe(42) ->date->toBe('Mock value') ->ticketTitle->toBe('Mock value') ->ticketType->toBe('Mock value') - ->budgetId->toBe('mock-id-123'); + ->budgetId->toBe(42) + ->activities->not->toBeNull(); }); -it('calls the getEntrySuggestion method in the EntrySuggestion resource', function () { +it('calls the entrySuggestionsShow method in the EntrySuggestion resource', function () { Saloon::fake([ - GetEntrySuggestionRequest::class => MockResponse::make([ + EntrySuggestionsShowRequest::class => MockResponse::make([ 'data' => [ 'type' => 'entrySuggestions', 'id' => 'mock-id-123', @@ -90,22 +115,22 @@ 'ticketId' => 'mock-id-123', 'ticketNumber' => 'Mock value', 'customerId' => 'mock-id-123', - 'userId' => 'mock-id-123', + 'userId' => 42, 'date' => 'Mock value', 'ticketTitle' => 'Mock value', 'ticketType' => 'Mock value', - 'budgetId' => 'mock-id-123', + 'budgetId' => 42, ], ], ], 200), ]); - $request = new GetEntrySuggestionRequest( - entrySuggestionId: 'test string' + $request = new EntrySuggestionsShowRequest( + entrySuggestionId: 123 ); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetEntrySuggestionRequest::class); + Saloon::assertSent(EntrySuggestionsShowRequest::class); expect($response->status())->toBe(200); @@ -115,24 +140,24 @@ ->ticketId->toBe('mock-id-123') ->ticketNumber->toBe('Mock value') ->customerId->toBe('mock-id-123') - ->userId->toBe('mock-id-123') + ->userId->toBe(42) ->date->toBe('Mock value') ->ticketTitle->toBe('Mock value') ->ticketType->toBe('Mock value') - ->budgetId->toBe('mock-id-123'); + ->budgetId->toBe(42); }); -it('calls the deleteEntrySuggestion method in the EntrySuggestion resource', function () { +it('calls the entrySuggestionsDestroy method in the EntrySuggestion resource', function () { Saloon::fake([ - DeleteEntrySuggestionRequest::class => MockResponse::make([], 200), + EntrySuggestionsDestroyRequest::class => MockResponse::make([], 200), ]); - $request = new DeleteEntrySuggestionRequest( - entrySuggestionId: 'test string' + $request = new EntrySuggestionsDestroyRequest( + entrySuggestionId: 123 ); $response = $this->timaticConnector->send($request); - Saloon::assertSent(DeleteEntrySuggestionRequest::class); + Saloon::assertSent(EntrySuggestionsDestroyRequest::class); expect($response->status())->toBe(200); }); diff --git a/tests/Requests/EntryTest.php b/tests/Requests/EntryTest.php index e4f0b58..b6ada8d 100644 --- a/tests/Requests/EntryTest.php +++ b/tests/Requests/EntryTest.php @@ -6,19 +6,19 @@ use Saloon\Http\Faking\MockResponse; use Saloon\Http\Request; use Saloon\Laravel\Facades\Saloon; -use Timatic\Requests\Entry\DeleteEntryRequest; -use Timatic\Requests\Entry\GetEntriesCollectionRequest; -use Timatic\Requests\Entry\GetEntryRequest; -use Timatic\Requests\Entry\PatchEntryRequest; -use Timatic\Requests\Entry\PostEntriesRequest; +use Timatic\Requests\Entry\EntriesCollectionRequest; +use Timatic\Requests\Entry\EntriesDestroyRequest; +use Timatic\Requests\Entry\EntriesShowRequest; +use Timatic\Requests\Entry\EntriesStoreRequest; +use Timatic\Requests\Entry\EntryMarkAsInvoicedRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getEntriesCollection method in the Entry resource', function () { +it('calls the entriesCollection method in the Entry resource', function () { Saloon::fake([ - GetEntriesCollectionRequest::class => MockResponse::make([ + EntriesCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'entries', @@ -28,17 +28,17 @@ 'ticketNumber' => 'Mock value', 'ticketTitle' => 'Mock value', 'ticketType' => 'Mock value', - 'customerId' => 'mock-id-123', + 'customerId' => 42, 'customerName' => 'Mock value', - 'hourlyRate' => 'Mock value', + 'hourlyRate' => 3.14, 'hadEmergencyShift' => true, - 'budgetId' => 'mock-id-123', - 'isPaidPerHour' => true, + 'budgetId' => 42, + 'isPaidPerHour' => 'Mock value', 'minutesSpent' => 42, - 'userId' => 'mock-id-123', + 'userId' => 42, 'userEmail' => 'test@example.com', 'userFullName' => 'Mock value', - 'createdByUserId' => 'mock-id-123', + 'createdByUserId' => 42, 'createdByUserEmail' => 'test@example.com', 'createdByUserFullName' => 'Mock value', 'entryType' => 'Mock value', @@ -50,6 +50,20 @@ 'isInvoiced' => 'Mock value', 'isBasedOnSuggestion' => true, ], + 'relationships' => [ + 'customer' => [ + 'data' => [ + 'type' => 'customers', + 'id' => 'related-customer-1', + ], + ], + 'budget' => [ + 'data' => [ + 'type' => 'budgets', + 'id' => 'related-budget-1', + ], + ], + ], ], 1 => [ 'type' => 'entries', @@ -59,17 +73,17 @@ 'ticketNumber' => 'Mock value', 'ticketTitle' => 'Mock value', 'ticketType' => 'Mock value', - 'customerId' => 'mock-id-123', + 'customerId' => 42, 'customerName' => 'Mock value', - 'hourlyRate' => 'Mock value', + 'hourlyRate' => 3.14, 'hadEmergencyShift' => true, - 'budgetId' => 'mock-id-123', - 'isPaidPerHour' => true, + 'budgetId' => 42, + 'isPaidPerHour' => 'Mock value', 'minutesSpent' => 42, - 'userId' => 'mock-id-123', + 'userId' => 42, 'userEmail' => 'test@example.com', 'userFullName' => 'Mock value', - 'createdByUserId' => 'mock-id-123', + 'createdByUserId' => 42, 'createdByUserEmail' => 'test@example.com', 'createdByUserFullName' => 'Mock value', 'entryType' => 'Mock value', @@ -81,27 +95,52 @@ 'isInvoiced' => 'Mock value', 'isBasedOnSuggestion' => true, ], + 'relationships' => [ + 'customer' => [ + 'data' => [ + 'type' => 'customers', + 'id' => 'related-customer-1', + ], + ], + 'budget' => [ + 'data' => [ + 'type' => 'budgets', + 'id' => 'related-budget-1', + ], + ], + ], + ], + ], + 'included' => [ + 0 => [ + 'type' => 'customers', + 'id' => 'related-customer-1', + 'attributes' => [], + ], + 1 => [ + 'type' => 'budgets', + 'id' => 'related-budget-1', + 'attributes' => [], ], ], ], 200), ]); - $request = (new GetEntriesCollectionRequest(include: 'test string')) + $request = (new EntriesCollectionRequest(sort: 'test string', pagesize: 123, pagenumber: 123)) ->filter('userId', 'user_id-123') ->filter('budgetId', 'budget_id-123') - ->filter('startedAt', '2025-01-15T10:30:00Z'); + ->filter('startedAt', '2025-01-15T10:30:00Z') + ->includeCustomer() + ->includeBudget(); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetEntriesCollectionRequest::class); - - // Verify filter query parameters are present - Saloon::assertSent(function (Request $request) { + Saloon::assertSent(function (EntriesCollectionRequest $request) { $query = $request->query()->all(); - expect($query)->toHaveKey('filter[userId]', 'user_id-123'); expect($query)->toHaveKey('filter[budgetId]', 'budget_id-123'); expect($query)->toHaveKey('filter[startedAt]', '2025-01-15T10:30:00Z'); + expect($query)->toHaveKey('include', 'customer,budget'); return true; }); @@ -115,17 +154,17 @@ ->ticketNumber->toBe('Mock value') ->ticketTitle->toBe('Mock value') ->ticketType->toBe('Mock value') - ->customerId->toBe('mock-id-123') + ->customerId->toBe(42) ->customerName->toBe('Mock value') - ->hourlyRate->toBe('Mock value') + ->hourlyRate->toBe(3.14) ->hadEmergencyShift->toBe(true) - ->budgetId->toBe('mock-id-123') - ->isPaidPerHour->toBe(true) + ->budgetId->toBe(42) + ->isPaidPerHour->toBe('Mock value') ->minutesSpent->toBe(42) - ->userId->toBe('mock-id-123') + ->userId->toBe(42) ->userEmail->toBe('test@example.com') ->userFullName->toBe('Mock value') - ->createdByUserId->toBe('mock-id-123') + ->createdByUserId->toBe(42) ->createdByUserEmail->toBe('test@example.com') ->createdByUserFullName->toBe('Mock value') ->entryType->toBe('Mock value') @@ -135,12 +174,14 @@ ->endedAt->toEqual(new Carbon('2025-11-22T10:40:04.065Z')) ->invoicedAt->toEqual(new Carbon('2025-11-22T10:40:04.065Z')) ->isInvoiced->toBe('Mock value') - ->isBasedOnSuggestion->toBe(true); + ->isBasedOnSuggestion->toBe(true) + ->customer->toBeInstanceOf(\Timatic\Dto\Customer::class) + ->budget->toBeInstanceOf(\Timatic\Dto\Budget::class); }); -it('calls the postEntries method in the Entry resource', function () { +it('calls the entriesStore method in the Entry resource', function () { $mockClient = Saloon::fake([ - PostEntriesRequest::class => MockResponse::make([], 200), + EntriesStoreRequest::class => MockResponse::make([], 200), ]); // Create DTO with sample data @@ -151,10 +192,10 @@ 'ticketType' => 'test value', ])->make(); - $request = new PostEntriesRequest($dto); + $request = new EntriesStoreRequest($dto); $this->timaticConnector->send($request); - Saloon::assertSent(PostEntriesRequest::class); + Saloon::assertSent(EntriesStoreRequest::class); $mockClient->assertSent(function (Request $request) { expect($request->body()->all()) @@ -171,9 +212,9 @@ }); }); -it('calls the getEntry method in the Entry resource', function () { +it('calls the entriesShow method in the Entry resource', function () { Saloon::fake([ - GetEntryRequest::class => MockResponse::make([ + EntriesShowRequest::class => MockResponse::make([ 'data' => [ 'type' => 'entries', 'id' => 'mock-id-123', @@ -182,17 +223,17 @@ 'ticketNumber' => 'Mock value', 'ticketTitle' => 'Mock value', 'ticketType' => 'Mock value', - 'customerId' => 'mock-id-123', + 'customerId' => 42, 'customerName' => 'Mock value', - 'hourlyRate' => 'Mock value', + 'hourlyRate' => 3.14, 'hadEmergencyShift' => true, - 'budgetId' => 'mock-id-123', - 'isPaidPerHour' => true, + 'budgetId' => 42, + 'isPaidPerHour' => 'Mock value', 'minutesSpent' => 42, - 'userId' => 'mock-id-123', + 'userId' => 42, 'userEmail' => 'test@example.com', 'userFullName' => 'Mock value', - 'createdByUserId' => 'mock-id-123', + 'createdByUserId' => 42, 'createdByUserEmail' => 'test@example.com', 'createdByUserFullName' => 'Mock value', 'entryType' => 'Mock value', @@ -208,12 +249,12 @@ ], 200), ]); - $request = new GetEntryRequest( - entryId: 'test string' + $request = new EntriesShowRequest( + entryId: 123 ); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetEntryRequest::class); + Saloon::assertSent(EntriesShowRequest::class); expect($response->status())->toBe(200); @@ -224,17 +265,17 @@ ->ticketNumber->toBe('Mock value') ->ticketTitle->toBe('Mock value') ->ticketType->toBe('Mock value') - ->customerId->toBe('mock-id-123') + ->customerId->toBe(42) ->customerName->toBe('Mock value') - ->hourlyRate->toBe('Mock value') + ->hourlyRate->toBe(3.14) ->hadEmergencyShift->toBe(true) - ->budgetId->toBe('mock-id-123') - ->isPaidPerHour->toBe(true) + ->budgetId->toBe(42) + ->isPaidPerHour->toBe('Mock value') ->minutesSpent->toBe(42) - ->userId->toBe('mock-id-123') + ->userId->toBe(42) ->userEmail->toBe('test@example.com') ->userFullName->toBe('Mock value') - ->createdByUserId->toBe('mock-id-123') + ->createdByUserId->toBe(42) ->createdByUserEmail->toBe('test@example.com') ->createdByUserFullName->toBe('Mock value') ->entryType->toBe('Mock value') @@ -247,24 +288,24 @@ ->isBasedOnSuggestion->toBe(true); }); -it('calls the deleteEntry method in the Entry resource', function () { +it('calls the entriesDestroy method in the Entry resource', function () { Saloon::fake([ - DeleteEntryRequest::class => MockResponse::make([], 200), + EntriesDestroyRequest::class => MockResponse::make([], 200), ]); - $request = new DeleteEntryRequest( - entryId: 'test string' + $request = new EntriesDestroyRequest( + entryId: 123 ); $response = $this->timaticConnector->send($request); - Saloon::assertSent(DeleteEntryRequest::class); + Saloon::assertSent(EntriesDestroyRequest::class); expect($response->status())->toBe(200); }); -it('calls the patchEntry method in the Entry resource', function () { +it('calls the entryMarkAsInvoiced method in the Entry resource', function () { $mockClient = Saloon::fake([ - PatchEntryRequest::class => MockResponse::make([], 200), + EntryMarkAsInvoicedRequest::class => MockResponse::make([], 200), ]); // Create DTO with sample data @@ -275,10 +316,10 @@ 'ticketType' => 'test value', ])->make(); - $request = new PatchEntryRequest(entryId: 'test string', data: $dto); + $request = new EntryMarkAsInvoicedRequest(entryId: 42, data: $dto); $this->timaticConnector->send($request); - Saloon::assertSent(PatchEntryRequest::class); + Saloon::assertSent(EntryMarkAsInvoicedRequest::class); $mockClient->assertSent(function (Request $request) { expect($request->body()->all()) diff --git a/tests/Requests/EventTest.php b/tests/Requests/EventTest.php index 10f378e..b0bdcd6 100644 --- a/tests/Requests/EventTest.php +++ b/tests/Requests/EventTest.php @@ -5,37 +5,37 @@ use Saloon\Http\Faking\MockResponse; use Saloon\Http\Request; use Saloon\Laravel\Facades\Saloon; -use Timatic\Requests\Event\PostEventsRequest; +use Timatic\Requests\Event\EventsStoreRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the postEvents method in the Event resource', function () { +it('calls the eventsStore method in the Event resource', function () { $mockClient = Saloon::fake([ - PostEventsRequest::class => MockResponse::make([], 200), + EventsStoreRequest::class => MockResponse::make([], 200), ]); // Create DTO with sample data $dto = \Timatic\Dto\Event::factory()->state([ - 'userId' => 'user_id-123', - 'budgetId' => 'budget_id-123', + 'userId' => 42, + 'budgetId' => 42, 'ticketId' => 'ticket_id-123', 'sourceId' => 'source_id-123', ])->make(); - $request = new PostEventsRequest($dto); + $request = new EventsStoreRequest($dto); $this->timaticConnector->send($request); - Saloon::assertSent(PostEventsRequest::class); + Saloon::assertSent(EventsStoreRequest::class); $mockClient->assertSent(function (Request $request) { expect($request->body()->all()) ->toHaveKey('data') ->data->type->toBe('events') ->data->attributes->scoped(fn ($attributes) => $attributes - ->userId->toBe('user_id-123') - ->budgetId->toBe('budget_id-123') + ->userId->toBe(42) + ->budgetId->toBe(42) ->ticketId->toBe('ticket_id-123') ->sourceId->toBe('source_id-123') ); diff --git a/tests/Requests/MarkAsExportedTest.php b/tests/Requests/MarkAsExportedTest.php deleted file mode 100644 index 4bef4b1..0000000 --- a/tests/Requests/MarkAsExportedTest.php +++ /dev/null @@ -1,45 +0,0 @@ -timaticConnector = new Timatic\TimaticConnector; -}); - -it('calls the postOvertimeMarkAsExported method in the MarkAsExported resource', function () { - $mockClient = Saloon::fake([ - PostOvertimeMarkAsExportedRequest::class => MockResponse::make([], 200), - ]); - - // Create DTO with sample data - $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); - - Saloon::assertSent(PostOvertimeMarkAsExportedRequest::class); - - $mockClient->assertSent(function (Request $request) { - expect($request->body()->all()) - ->toHaveKey('data') - ->data->type->toBe('markAsExporteds') - ->data->attributes->scoped(fn ($attributes) => $attributes - ->entryId->toBe('entry_id-123') - ->overtimeTypeId->toBe('overtime_type_id-123') - ->startedAt->toEqual(new \Carbon\Carbon('2025-01-15T10:30:00Z')) - ->endedAt->toEqual(new \Carbon\Carbon('2025-01-15T10:30:00Z')) - ); - - return true; - }); -}); diff --git a/tests/Requests/MarkAsInvoicedTest.php b/tests/Requests/MarkAsInvoicedTest.php deleted file mode 100644 index 0e0e896..0000000 --- a/tests/Requests/MarkAsInvoicedTest.php +++ /dev/null @@ -1,45 +0,0 @@ -timaticConnector = new Timatic\TimaticConnector; -}); - -it('calls the postEntryMarkAsInvoiced method in the MarkAsInvoiced resource', function () { - $mockClient = Saloon::fake([ - PostEntryMarkAsInvoicedRequest::class => MockResponse::make([], 200), - ]); - - // Create DTO with sample data - $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); - - Saloon::assertSent(PostEntryMarkAsInvoicedRequest::class); - - $mockClient->assertSent(function (Request $request) { - expect($request->body()->all()) - ->toHaveKey('data') - ->data->type->toBe('markAsInvoiceds') - ->data->attributes->scoped(fn ($attributes) => $attributes - ->ticketId->toBe('ticket_id-123') - ->ticketNumber->toBe('test value') - ->ticketTitle->toBe('test value') - ->ticketType->toBe('test value') - ); - - return true; - }); -}); diff --git a/tests/Requests/OvertimeTest.php b/tests/Requests/OvertimeTest.php index ed7f7a2..f75d26b 100644 --- a/tests/Requests/OvertimeTest.php +++ b/tests/Requests/OvertimeTest.php @@ -6,64 +6,105 @@ use Saloon\Http\Faking\MockResponse; use Saloon\Http\Request; use Saloon\Laravel\Facades\Saloon; -use Timatic\Requests\Overtime\GetOvertimesCollectionRequest; +use Timatic\Requests\Overtime\OvertimeApproveRequest; +use Timatic\Requests\Overtime\OvertimeMarkAsExportedRequest; +use Timatic\Requests\Overtime\OvertimesCollectionRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getOvertimesCollection method in the Overtime resource', function () { +it('calls the overtimesCollection method in the Overtime resource', function () { Saloon::fake([ - GetOvertimesCollectionRequest::class => MockResponse::make([ + OvertimesCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'overtimes', 'id' => 'mock-id-1', 'attributes' => [ - 'entryId' => 'mock-id-123', + 'entryId' => 42, 'overtimeTypeId' => 'mock-id-123', 'startedAt' => '2025-11-22T10:40:04.065Z', 'endedAt' => '2025-11-22T10:40:04.065Z', 'percentages' => 'Mock value', 'approvedAt' => '2025-11-22T10:40:04.065Z', - 'approvedByUserId' => 'mock-id-123', + 'approvedByUserId' => 42, 'exportedAt' => '2025-11-22T10:40:04.065Z', ], + 'relationships' => [ + 'overtimeType' => [ + 'data' => [ + 'type' => 'overtimetypes', + 'id' => 'related-overtimeType-1', + ], + ], + 'entry' => [ + 'data' => [ + 'type' => 'entries', + 'id' => 'related-entry-1', + ], + ], + ], ], 1 => [ 'type' => 'overtimes', 'id' => 'mock-id-2', 'attributes' => [ - 'entryId' => 'mock-id-123', + 'entryId' => 42, 'overtimeTypeId' => 'mock-id-123', 'startedAt' => '2025-11-22T10:40:04.065Z', 'endedAt' => '2025-11-22T10:40:04.065Z', 'percentages' => 'Mock value', 'approvedAt' => '2025-11-22T10:40:04.065Z', - 'approvedByUserId' => 'mock-id-123', + 'approvedByUserId' => 42, 'exportedAt' => '2025-11-22T10:40:04.065Z', ], + 'relationships' => [ + 'overtimeType' => [ + 'data' => [ + 'type' => 'overtimetypes', + 'id' => 'related-overtimeType-1', + ], + ], + 'entry' => [ + 'data' => [ + 'type' => 'entries', + 'id' => 'related-entry-1', + ], + ], + ], + ], + ], + 'included' => [ + 0 => [ + 'type' => 'overtimetypes', + 'id' => 'related-overtimeType-1', + 'attributes' => [], + ], + 1 => [ + 'type' => 'entries', + 'id' => 'related-entry-1', + 'attributes' => [], ], ], ], 200), ]); - $request = (new GetOvertimesCollectionRequest) + $request = (new OvertimesCollectionRequest(pagesize: 123, pagenumber: 123)) ->filter('startedAt', '2025-01-15T10:30:00Z') ->filter('endedAt', '2025-01-15T10:30:00Z') - ->filter('isApproved', true); + ->filter('isApproved', true) + ->includeOvertimeType() + ->includeEntry(); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetOvertimesCollectionRequest::class); - - // Verify filter query parameters are present - Saloon::assertSent(function (Request $request) { + Saloon::assertSent(function (OvertimesCollectionRequest $request) { $query = $request->query()->all(); - expect($query)->toHaveKey('filter[startedAt]', '2025-01-15T10:30:00Z'); expect($query)->toHaveKey('filter[endedAt]', '2025-01-15T10:30:00Z'); expect($query)->toHaveKey('filter[isApproved]', true); + expect($query)->toHaveKey('include', 'overtimeType,entry'); return true; }); @@ -73,12 +114,80 @@ $dtoCollection = $response->dto(); expect($dtoCollection->first()) - ->entryId->toBe('mock-id-123') + ->entryId->toBe(42) ->overtimeTypeId->toBe('mock-id-123') ->startedAt->toEqual(new Carbon('2025-11-22T10:40:04.065Z')) ->endedAt->toEqual(new Carbon('2025-11-22T10:40:04.065Z')) ->percentages->toBe('Mock value') ->approvedAt->toEqual(new Carbon('2025-11-22T10:40:04.065Z')) - ->approvedByUserId->toBe('mock-id-123') - ->exportedAt->toEqual(new Carbon('2025-11-22T10:40:04.065Z')); + ->approvedByUserId->toBe(42) + ->exportedAt->toEqual(new Carbon('2025-11-22T10:40:04.065Z')) + ->overtimeType->toBeInstanceOf(\Timatic\Dto\OvertimeType::class) + ->entry->toBeInstanceOf(\Timatic\Dto\Entry::class); +}); + +it('calls the overtimeApprove method in the Overtime resource', function () { + $mockClient = Saloon::fake([ + OvertimeApproveRequest::class => MockResponse::make([], 200), + ]); + + // Create DTO with sample data + $dto = \Timatic\Dto\Overtime::factory()->state([ + 'entryId' => 42, + '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 OvertimeApproveRequest(overtimeId: 42, data: $dto); + $this->timaticConnector->send($request); + + Saloon::assertSent(OvertimeApproveRequest::class); + + $mockClient->assertSent(function (Request $request) { + expect($request->body()->all()) + ->toHaveKey('data') + ->data->type->toBe('overtimes') + ->data->attributes->scoped(fn ($attributes) => $attributes + ->entryId->toBe(42) + ->overtimeTypeId->toBe('overtime_type_id-123') + ->startedAt->toEqual(new \Carbon\Carbon('2025-01-15T10:30:00Z')) + ->endedAt->toEqual(new \Carbon\Carbon('2025-01-15T10:30:00Z')) + ); + + return true; + }); +}); + +it('calls the overtimeMarkAsExported method in the Overtime resource', function () { + $mockClient = Saloon::fake([ + OvertimeMarkAsExportedRequest::class => MockResponse::make([], 200), + ]); + + // Create DTO with sample data + $dto = \Timatic\Dto\Overtime::factory()->state([ + 'entryId' => 42, + '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 OvertimeMarkAsExportedRequest(overtimeId: 42, data: $dto); + $this->timaticConnector->send($request); + + Saloon::assertSent(OvertimeMarkAsExportedRequest::class); + + $mockClient->assertSent(function (Request $request) { + expect($request->body()->all()) + ->toHaveKey('data') + ->data->type->toBe('overtimes') + ->data->attributes->scoped(fn ($attributes) => $attributes + ->entryId->toBe(42) + ->overtimeTypeId->toBe('overtime_type_id-123') + ->startedAt->toEqual(new \Carbon\Carbon('2025-01-15T10:30:00Z')) + ->endedAt->toEqual(new \Carbon\Carbon('2025-01-15T10:30:00Z')) + ); + + return true; + }); }); diff --git a/tests/Requests/TeamTest.php b/tests/Requests/TeamTest.php index 2fe02aa..e5cdfed 100644 --- a/tests/Requests/TeamTest.php +++ b/tests/Requests/TeamTest.php @@ -5,19 +5,18 @@ use Saloon\Http\Faking\MockResponse; use Saloon\Http\Request; use Saloon\Laravel\Facades\Saloon; -use Timatic\Requests\Team\DeleteTeamRequest; -use Timatic\Requests\Team\GetTeamRequest; -use Timatic\Requests\Team\GetTeamsCollectionRequest; -use Timatic\Requests\Team\PatchTeamRequest; -use Timatic\Requests\Team\PostTeamsRequest; +use Timatic\Requests\Team\TeamsCollectionRequest; +use Timatic\Requests\Team\TeamsDestroyRequest; +use Timatic\Requests\Team\TeamsShowRequest; +use Timatic\Requests\Team\TeamsStoreRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getTeamsCollection method in the Team resource', function () { +it('calls the teamsCollection method in the Team resource', function () { Saloon::fake([ - GetTeamsCollectionRequest::class => MockResponse::make([ + TeamsCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'teams', @@ -39,11 +38,15 @@ ], 200), ]); - $request = (new GetTeamsCollectionRequest); + $request = (new TeamsCollectionRequest); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetTeamsCollectionRequest::class); + Saloon::assertSent(function (TeamsCollectionRequest $request) { + $query = $request->query()->all(); + + return true; + }); expect($response->status())->toBe(200); @@ -54,9 +57,9 @@ ->name->toBe('Mock value'); }); -it('calls the postTeams method in the Team resource', function () { +it('calls the teamsStore method in the Team resource', function () { $mockClient = Saloon::fake([ - PostTeamsRequest::class => MockResponse::make([], 200), + TeamsStoreRequest::class => MockResponse::make([], 200), ]); // Create DTO with sample data @@ -65,10 +68,10 @@ 'name' => 'test name', ])->make(); - $request = new PostTeamsRequest($dto); + $request = new TeamsStoreRequest($dto); $this->timaticConnector->send($request); - Saloon::assertSent(PostTeamsRequest::class); + Saloon::assertSent(TeamsStoreRequest::class); $mockClient->assertSent(function (Request $request) { expect($request->body()->all()) @@ -83,9 +86,9 @@ }); }); -it('calls the getTeam method in the Team resource', function () { +it('calls the teamsShow method in the Team resource', function () { Saloon::fake([ - GetTeamRequest::class => MockResponse::make([ + TeamsShowRequest::class => MockResponse::make([ 'data' => [ 'type' => 'teams', 'id' => 'mock-id-123', @@ -97,12 +100,12 @@ ], 200), ]); - $request = new GetTeamRequest( - teamId: 'test string' + $request = new TeamsShowRequest( + teamId: 123 ); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetTeamRequest::class); + Saloon::assertSent(TeamsShowRequest::class); expect($response->status())->toBe(200); @@ -113,46 +116,17 @@ ->name->toBe('Mock value'); }); -it('calls the deleteTeam method in the Team resource', function () { +it('calls the teamsDestroy method in the Team resource', function () { Saloon::fake([ - DeleteTeamRequest::class => MockResponse::make([], 200), + TeamsDestroyRequest::class => MockResponse::make([], 200), ]); - $request = new DeleteTeamRequest( - teamId: 'test string' + $request = new TeamsDestroyRequest( + teamId: 123 ); $response = $this->timaticConnector->send($request); - Saloon::assertSent(DeleteTeamRequest::class); + Saloon::assertSent(TeamsDestroyRequest::class); expect($response->status())->toBe(200); }); - -it('calls the patchTeam method in the Team resource', function () { - $mockClient = Saloon::fake([ - PatchTeamRequest::class => MockResponse::make([], 200), - ]); - - // Create DTO with sample data - $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); - - Saloon::assertSent(PatchTeamRequest::class); - - $mockClient->assertSent(function (Request $request) { - expect($request->body()->all()) - ->toHaveKey('data') - ->data->type->toBe('teams') - ->data->attributes->scoped(fn ($attributes) => $attributes - ->externalId->toBe('external_id-123') - ->name->toBe('test name') - ); - - return true; - }); -}); diff --git a/tests/Requests/TimeSpentTotalTest.php b/tests/Requests/TimeSpentTotalTest.php deleted file mode 100644 index 497ddbc..0000000 --- a/tests/Requests/TimeSpentTotalTest.php +++ /dev/null @@ -1,76 +0,0 @@ -timaticConnector = new Timatic\TimaticConnector; -}); - -it('calls the getTimeSpentTotalsCollection method in the TimeSpentTotal resource', function () { - Saloon::fake([ - GetTimeSpentTotalsCollectionRequest::class => MockResponse::make([ - 'data' => [ - 0 => [ - 'type' => 'timeSpentTotals', - 'id' => 'mock-id-1', - 'attributes' => [ - 'start' => '2025-11-22T10:40:04.065Z', - 'end' => '2025-11-22T10:40:04.065Z', - 'internalMinutes' => 42, - 'billableMinutes' => 42, - 'periodUnit' => 'Mock value', - 'periodValue' => 42, - ], - ], - 1 => [ - 'type' => 'timeSpentTotals', - 'id' => 'mock-id-2', - 'attributes' => [ - 'start' => '2025-11-22T10:40:04.065Z', - 'end' => '2025-11-22T10:40:04.065Z', - 'internalMinutes' => 42, - 'billableMinutes' => 42, - 'periodUnit' => 'Mock value', - 'periodValue' => 42, - ], - ], - ], - ], 200), - ]); - - $request = (new GetTimeSpentTotalsCollectionRequest) - ->filter('teamId', 'team_id-123') - ->filter('userId', 'user_id-123'); - - $response = $this->timaticConnector->send($request); - - Saloon::assertSent(GetTimeSpentTotalsCollectionRequest::class); - - // Verify filter query parameters are present - Saloon::assertSent(function (Request $request) { - $query = $request->query()->all(); - - expect($query)->toHaveKey('filter[teamId]', 'team_id-123'); - expect($query)->toHaveKey('filter[userId]', 'user_id-123'); - - return true; - }); - - expect($response->status())->toBe(200); - - $dtoCollection = $response->dto(); - - expect($dtoCollection->first()) - ->start->toEqual(new Carbon('2025-11-22T10:40:04.065Z')) - ->end->toEqual(new Carbon('2025-11-22T10:40:04.065Z')) - ->internalMinutes->toBe(42) - ->billableMinutes->toBe(42) - ->periodUnit->toBe('Mock value') - ->periodValue->toBe(42); -}); diff --git a/tests/Requests/UserCustomerHoursAggregateTest.php b/tests/Requests/UserCustomerHoursAggregateTest.php deleted file mode 100644 index 4aa38da..0000000 --- a/tests/Requests/UserCustomerHoursAggregateTest.php +++ /dev/null @@ -1,74 +0,0 @@ -timaticConnector = new Timatic\TimaticConnector; -}); - -it('calls the getUserCustomerHoursAggregatesCollection method in the UserCustomerHoursAggregate resource', function () { - Saloon::fake([ - GetUserCustomerHoursAggregatesCollectionRequest::class => MockResponse::make([ - 'data' => [ - 0 => [ - 'type' => 'userCustomerHoursAggregates', - 'id' => 'mock-id-1', - 'attributes' => [ - 'customerId' => 'mock-id-123', - 'userId' => 'mock-id-123', - 'internalMinutes' => 42, - 'budgetMinutes' => 42, - 'paidPerHourMinutes' => 42, - ], - ], - 1 => [ - 'type' => 'userCustomerHoursAggregates', - 'id' => 'mock-id-2', - 'attributes' => [ - 'customerId' => 'mock-id-123', - 'userId' => 'mock-id-123', - 'internalMinutes' => 42, - 'budgetMinutes' => 42, - 'paidPerHourMinutes' => 42, - ], - ], - ], - ], 200), - ]); - - $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(GetUserCustomerHoursAggregatesCollectionRequest::class); - - // Verify filter query parameters are present - Saloon::assertSent(function (Request $request) { - $query = $request->query()->all(); - - expect($query)->toHaveKey('filter[startedAt]', '2025-01-15T10:30:00Z'); - expect($query)->toHaveKey('filter[endedAt]', '2025-01-15T10:30:00Z'); - expect($query)->toHaveKey('filter[teamId]', 'team_id-123'); - - return true; - }); - - expect($response->status())->toBe(200); - - $dtoCollection = $response->dto(); - - expect($dtoCollection->first()) - ->customerId->toBe('mock-id-123') - ->userId->toBe('mock-id-123') - ->internalMinutes->toBe(42) - ->budgetMinutes->toBe(42) - ->paidPerHourMinutes->toBe(42); -}); diff --git a/tests/Requests/UserTest.php b/tests/Requests/UserTest.php index ca1c4fc..f703997 100644 --- a/tests/Requests/UserTest.php +++ b/tests/Requests/UserTest.php @@ -5,19 +5,18 @@ use Saloon\Http\Faking\MockResponse; use Saloon\Http\Request; use Saloon\Laravel\Facades\Saloon; -use Timatic\Requests\User\DeleteUserRequest; -use Timatic\Requests\User\GetUserRequest; -use Timatic\Requests\User\GetUsersCollectionRequest; -use Timatic\Requests\User\PatchUserRequest; -use Timatic\Requests\User\PostUsersRequest; +use Timatic\Requests\User\UsersCollectionRequest; +use Timatic\Requests\User\UsersDestroyRequest; +use Timatic\Requests\User\UsersShowRequest; +use Timatic\Requests\User\UsersStoreRequest; beforeEach(function () { $this->timaticConnector = new Timatic\TimaticConnector; }); -it('calls the getUsersCollection method in the User resource', function () { +it('calls the usersCollection method in the User resource', function () { Saloon::fake([ - GetUsersCollectionRequest::class => MockResponse::make([ + UsersCollectionRequest::class => MockResponse::make([ 'data' => [ 0 => [ 'type' => 'users', @@ -27,7 +26,24 @@ 'email' => 'test@example.com', 'givenName' => 'Mock value', 'familyName' => 'Mock value', - 'teamId' => 'mock-id-123', + 'isImpersonated' => true, + 'impersonatedById' => 42, + ], + 'relationships' => [ + 'permissions' => [ + 'data' => [ + 0 => [ + 'type' => 'permissions', + 'id' => 'related-permissions-1', + ], + ], + ], + 'team' => [ + 'data' => [ + 'type' => 'teams', + 'id' => 'related-team-1', + ], + ], ], ], 1 => [ @@ -38,25 +54,53 @@ 'email' => 'test@example.com', 'givenName' => 'Mock value', 'familyName' => 'Mock value', - 'teamId' => 'mock-id-123', + 'isImpersonated' => true, + 'impersonatedById' => 42, + ], + 'relationships' => [ + 'permissions' => [ + 'data' => [ + 0 => [ + 'type' => 'permissions', + 'id' => 'related-permissions-1', + ], + ], + ], + 'team' => [ + 'data' => [ + 'type' => 'teams', + 'id' => 'related-team-1', + ], + ], ], ], ], + 'included' => [ + 0 => [ + 'type' => 'permissions', + 'id' => 'related-permissions-1', + 'attributes' => [], + ], + 1 => [ + 'type' => 'teams', + 'id' => 'related-team-1', + 'attributes' => [], + ], + ], ], 200), ]); - $request = (new GetUsersCollectionRequest) - ->filter('externalId', 'external_id-123'); + $request = (new UsersCollectionRequest(pagesize: 123, pagenumber: 123)) + ->filter('externalId', 'external_id-123') + ->includePermissions() + ->includeTeam(); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetUsersCollectionRequest::class); - - // Verify filter query parameters are present - Saloon::assertSent(function (Request $request) { + Saloon::assertSent(function (UsersCollectionRequest $request) { $query = $request->query()->all(); - expect($query)->toHaveKey('filter[externalId]', 'external_id-123'); + expect($query)->toHaveKey('include', 'permissions,team'); return true; }); @@ -70,12 +114,15 @@ ->email->toBe('test@example.com') ->givenName->toBe('Mock value') ->familyName->toBe('Mock value') - ->teamId->toBe('mock-id-123'); + ->isImpersonated->toBe(true) + ->impersonatedById->toBe(42) + ->permissions->not->toBeNull() + ->team->toBeInstanceOf(\Timatic\Dto\Team::class); }); -it('calls the postUsers method in the User resource', function () { +it('calls the usersStore method in the User resource', function () { $mockClient = Saloon::fake([ - PostUsersRequest::class => MockResponse::make([], 200), + UsersStoreRequest::class => MockResponse::make([], 200), ]); // Create DTO with sample data @@ -86,10 +133,10 @@ 'familyName' => 'test value', ])->make(); - $request = new PostUsersRequest($dto); + $request = new UsersStoreRequest($dto); $this->timaticConnector->send($request); - Saloon::assertSent(PostUsersRequest::class); + Saloon::assertSent(UsersStoreRequest::class); $mockClient->assertSent(function (Request $request) { expect($request->body()->all()) @@ -106,9 +153,9 @@ }); }); -it('calls the getUser method in the User resource', function () { +it('calls the usersShow method in the User resource', function () { Saloon::fake([ - GetUserRequest::class => MockResponse::make([ + UsersShowRequest::class => MockResponse::make([ 'data' => [ 'type' => 'users', 'id' => 'mock-id-123', @@ -117,18 +164,19 @@ 'email' => 'test@example.com', 'givenName' => 'Mock value', 'familyName' => 'Mock value', - 'teamId' => 'mock-id-123', + 'isImpersonated' => true, + 'impersonatedById' => 42, ], ], ], 200), ]); - $request = new GetUserRequest( - userId: 'test string' + $request = new UsersShowRequest( + userId: 123 ); $response = $this->timaticConnector->send($request); - Saloon::assertSent(GetUserRequest::class); + Saloon::assertSent(UsersShowRequest::class); expect($response->status())->toBe(200); @@ -139,53 +187,21 @@ ->email->toBe('test@example.com') ->givenName->toBe('Mock value') ->familyName->toBe('Mock value') - ->teamId->toBe('mock-id-123'); + ->isImpersonated->toBe(true) + ->impersonatedById->toBe(42); }); -it('calls the deleteUser method in the User resource', function () { +it('calls the usersDestroy method in the User resource', function () { Saloon::fake([ - DeleteUserRequest::class => MockResponse::make([], 200), + UsersDestroyRequest::class => MockResponse::make([], 200), ]); - $request = new DeleteUserRequest( - userId: 'test string' + $request = new UsersDestroyRequest( + userId: 123 ); $response = $this->timaticConnector->send($request); - Saloon::assertSent(DeleteUserRequest::class); + Saloon::assertSent(UsersDestroyRequest::class); expect($response->status())->toBe(200); }); - -it('calls the patchUser method in the User resource', function () { - $mockClient = Saloon::fake([ - PatchUserRequest::class => MockResponse::make([], 200), - ]); - - // Create DTO with sample data - $dto = \Timatic\Dto\User::factory()->state([ - 'externalId' => 'external_id-123', - 'email' => 'test@example.com', - 'givenName' => 'test value', - 'familyName' => 'test value', - ])->make(); - - $request = new PatchUserRequest(userId: 'test string', data: $dto); - $this->timaticConnector->send($request); - - Saloon::assertSent(PatchUserRequest::class); - - $mockClient->assertSent(function (Request $request) { - expect($request->body()->all()) - ->toHaveKey('data') - ->data->type->toBe('users') - ->data->attributes->scoped(fn ($attributes) => $attributes - ->externalId->toBe('external_id-123') - ->email->toBe('test@example.com') - ->givenName->toBe('test value') - ->familyName->toBe('test value') - ); - - return true; - }); -});