diff --git a/src/ApiResource/Project/ProjectApiResource.php b/src/ApiResource/Project/ProjectApiResource.php index e61d749..2ce8ef5 100644 --- a/src/ApiResource/Project/ProjectApiResource.php +++ b/src/ApiResource/Project/ProjectApiResource.php @@ -8,16 +8,18 @@ use ApiPlatform\Metadata as API; use App\ApiResource\Accounting\AccountingApiResource; use App\ApiResource\LocalizedApiResourceTrait; use App\ApiResource\User\UserApiResource; +use App\Dto\ProjectCreateDto; +use App\Dto\ProjectUpdateDto; use App\Entity\Project\Category; use App\Entity\Project\Project; +use App\Entity\Project\ProjectCalendar; +use App\Entity\Project\ProjectDeadline; use App\Entity\Project\ProjectStatus; use App\Entity\Project\ProjectVideo; use App\Mapping\Transformer\BudgetMapTransformer; -use App\Mapping\Transformer\ProjectVideoMapTransformer; use App\State\ApiResourceStateProvider; use App\State\Project\ProjectStateProcessor; use AutoMapper\Attribute\MapFrom; -use AutoMapper\Attribute\MapTo; use Symfony\Component\Validator\Constraints as Assert; /** @@ -27,12 +29,19 @@ use Symfony\Component\Validator\Constraints as Assert; shortName: 'Project', stateOptions: new Options(entityClass: Project::class), provider: ApiResourceStateProvider::class, - processor: ProjectStateProcessor::class )] #[API\GetCollection()] -#[API\Post(security: 'is_granted("ROLE_USER")')] +#[API\Post( + input: ProjectCreateDto::class, + processor: ProjectStateProcessor::class, + security: 'is_granted("ROLE_USER")', +)] #[API\Get()] -#[API\Patch(security: 'is_granted("PROJECT_EDIT", previous_object)')] +#[API\Patch( + input: ProjectUpdateDto::class, + processor: ProjectStateProcessor::class, + security: 'is_granted("PROJECT_EDIT", previous_object)', +)] #[API\Delete(security: 'is_granted("PROJECT_EDIT", previous_object)')] class ProjectApiResource { @@ -67,6 +76,20 @@ class ProjectApiResource #[Assert\NotBlank()] public string $subtitle; + /** + * On `minimum`, Project will campaign until the minimum deadline.\ + * On `optimum`, Project will campaing until the minimum deadline, + * and then until the optimum deadline if it did raise the minimum. + */ + #[API\ApiProperty(writable: false)] + public ProjectDeadline $deadline; + + /** + * Deadlines and important Project dates. + */ + #[API\ApiProperty(writable: false)] + public ProjectCalendar $calendar; + /** * One of the available categories. */ @@ -88,20 +111,12 @@ class ProjectApiResource #[Assert\NotBlank()] public string $description; - /** - * A URL to a video showcasing the Project. - */ - #[Assert\Url()] - #[API\ApiProperty(readable: false)] - #[MapTo(target: Project::class, transformer: ProjectVideoMapTransformer::class)] - public string $video; - /** * Extracted embedding data from the Project's video. */ #[API\ApiProperty(writable: false)] #[MapFrom(source: Project::class, property: 'video')] - public ProjectVideo $videoEmbed; + public ProjectVideo $video; /** * The status of a Project represents how far it is in it's life-cycle. diff --git a/src/Benzina/ProjectsPump.php b/src/Benzina/ProjectsPump.php index 1eb27e9..53fc825 100644 --- a/src/Benzina/ProjectsPump.php +++ b/src/Benzina/ProjectsPump.php @@ -4,6 +4,7 @@ namespace App\Benzina; use App\Entity\Project\Category; use App\Entity\Project\Project; +use App\Entity\Project\ProjectDeadline; use App\Entity\Project\ProjectStatus; use App\Entity\Project\ProjectTerritory; use App\Entity\Project\ProjectVideo; @@ -11,6 +12,7 @@ use App\Entity\Project\Update; use App\Entity\User\User; use App\Repository\User\UserRepository; use App\Service\Embed\EmbedService; +use App\Service\Project\CalendarService; use App\Service\Project\TerritoryService; use Goteo\Benzina\Pump\ArrayPumpTrait; use Goteo\Benzina\Pump\DoctrinePumpTrait; @@ -27,6 +29,7 @@ class ProjectsPump implements PumpInterface private UserRepository $userRepository, private TerritoryService $territoryService, private EmbedService $embedService, + private CalendarService $calendarService, ) {} public function supports(mixed $sample): bool @@ -74,6 +77,16 @@ class ProjectsPump implements PumpInterface $project->addUpdate($update); } + $conf = $this->getProjectConf($project, $context); + + $project->setDeadline($this->getProjectDeadline($conf)); + $project->setCalendar($this->calendarService->makeCalendar( + $project->getDeadline(), + new \DateTimeImmutable($record['published']), + $conf['days_round1'], + $conf['days_round2'], + )); + $this->persist($project, $context); } @@ -235,4 +248,34 @@ class ProjectsPump implements PumpInterface return $query->fetchAll(); } + + private function getProjectConf(Project $project, array $context): array + { + $query = $this->getDbConnection($context)->prepare( + 'SELECT * FROM `project_conf` WHERE `project` = :project' + ); + + $query->execute(['project' => $project->getMigratedId()]); + + $conf = $query->fetch(); + + if (!\is_array($conf)) { + return [ + 'one_round' => 0, + 'days_round1' => 40, + 'days_round2' => 40, + ]; + } + + return $conf; + } + + private function getProjectDeadline(array $conf): ProjectDeadline + { + if ($conf['one_round'] < 1) { + return ProjectDeadline::Optimum; + } + + return ProjectDeadline::Minimum; + } } diff --git a/src/Dto/ProjectCreateDto.php b/src/Dto/ProjectCreateDto.php new file mode 100644 index 0000000..e2140a0 --- /dev/null +++ b/src/Dto/ProjectCreateDto.php @@ -0,0 +1,59 @@ + @@ -154,6 +160,30 @@ class Project implements UserOwnedInterface, AccountingOwnerInterface, Localized return $this; } + public function getDeadline(): ?ProjectDeadline + { + return $this->deadline; + } + + public function setDeadline(ProjectDeadline $deadline): static + { + $this->deadline = $deadline; + + return $this; + } + + public function getCalendar(): ?ProjectCalendar + { + return $this->calendar; + } + + public function setCalendar(ProjectCalendar $calendar): static + { + $this->calendar = $calendar; + + return $this; + } + public function getCategory(): ?Category { return $this->category; diff --git a/src/Entity/Project/ProjectCalendar.php b/src/Entity/Project/ProjectCalendar.php new file mode 100644 index 0000000..0efd88d --- /dev/null +++ b/src/Entity/Project/ProjectCalendar.php @@ -0,0 +1,34 @@ +hasChangedField('status')) { + return; + } + + if ($project->getStatus() === ProjectStatus::InCampaign) { + $calendar = $this->calendarService->makeCalendar($project->getDeadline()); + + $project->setCalendar($calendar); + } + } +} diff --git a/src/Service/Project/CalendarService.php b/src/Service/Project/CalendarService.php new file mode 100644 index 0000000..b99063f --- /dev/null +++ b/src/Service/Project/CalendarService.php @@ -0,0 +1,34 @@ +release = $release; + + switch ($deadline) { + case ProjectDeadline::Minimum: + $calendar->minimum = $calendar->release->add($minimumTtl); + break; + case ProjectDeadline::Optimum: + $calendar->minimum = $calendar->release->add($minimumTtl); + $calendar->optimum = $calendar->minimum->add($optimumTtl); + break; + } + + return $calendar; + } +} diff --git a/src/State/Project/ProjectStateProcessor.php b/src/State/Project/ProjectStateProcessor.php index 8055415..54a4305 100644 --- a/src/State/Project/ProjectStateProcessor.php +++ b/src/State/Project/ProjectStateProcessor.php @@ -5,6 +5,8 @@ namespace App\State\Project; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use App\ApiResource\Project\ProjectApiResource; +use App\Dto\ProjectCreateDto; +use App\Dto\ProjectUpdateDto; use App\Entity\Project\Project; use App\Mapping\AutoMapper; use App\Service\Auth\AuthService; @@ -20,16 +22,17 @@ class ProjectStateProcessor implements ProcessorInterface ) {} /** - * @param ProjectApiResource $data + * @param ProjectCreateDto|ProjectUpdateDto $data + * @param array{id: int} $uriVariables * * @return ProjectApiResource|null */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) { - /** @var Project */ - $project = $this->autoMapper->map($data, Project::class); + if ($data instanceof ProjectCreateDto) { + /** @var Project */ + $project = $this->autoMapper->map($data, Project::class); - if (!$project->getId()) { $owner = $this->authService->getUser(); if (!$owner) { @@ -39,12 +42,19 @@ class ProjectStateProcessor implements ProcessorInterface $project->setOwner($owner); } + if ($data instanceof ProjectUpdateDto) { + $data = $this->autoMapper->map($uriVariables, $data); + + /** @var Project */ + $project = $this->autoMapper->map($data, Project::class); + } + $project = $this->entityStateProcessor->process($project, $operation, $uriVariables, $context); if ($project === null) { return null; } - return $this->autoMapper->map($project, $data); + return $this->autoMapper->map($project, ProjectApiResource::class); } } diff --git a/tests/Entity/ProjectApiTest.php b/tests/Entity/ProjectApiTest.php index e92b303..486200a 100644 --- a/tests/Entity/ProjectApiTest.php +++ b/tests/Entity/ProjectApiTest.php @@ -5,6 +5,7 @@ namespace App\Tests\Entity; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; use App\Entity\Project\Category; use App\Entity\Project\Project; +use App\Entity\Project\ProjectDeadline; use App\Entity\Project\ProjectStatus; use App\Entity\Project\ProjectTerritory; use App\Entity\User\User; @@ -43,6 +44,7 @@ class ProjectApiTest extends ApiTestCase $project = new Project(); $project->setTitle('Test Project'); $project->setSubtitle('Test Project Subtitle'); + $project->setDeadline(ProjectDeadline::Minimum); $project->setCategory(Category::LibreSoftware); $project->setDescription('Test Project Description'); $project->setTerritory(new ProjectTerritory('ES'));