Skip to content

Commit

Permalink
Add onboarding completion functionality and related user options.
Browse files Browse the repository at this point in the history
  • Loading branch information
deeravenger committed Dec 22, 2024
1 parent 54c4401 commit f91a522
Show file tree
Hide file tree
Showing 16 changed files with 362 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace App\EconumoBundle\Application\User\Assembler;

use App\EconumoBundle\Application\User\Dto\CompleteOnboardingV1ResultDto;
use App\EconumoBundle\Domain\Entity\ValueObject\Id;

readonly class CompleteOnboardingV1ResultAssembler
{
public function __construct(
private CurrentUserIdToDtoResultAssembler $currentUserIdToDtoResultAssembler
) {
}

public function assemble(Id $userId): CompleteOnboardingV1ResultDto
{
$result = new CompleteOnboardingV1ResultDto();
$result->user = $this->currentUserIdToDtoResultAssembler->assemble($userId);

return $result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace App\EconumoBundle\Application\User\Dto;

use OpenApi\Annotations as OA;

/**
* @OA\Schema(
* required={"id"}
* )
*/
class CompleteOnboardingV1RequestDto
{
/**
* @OA\Property(example="123")
*/
public string $id;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace App\EconumoBundle\Application\User\Dto;

use OpenApi\Annotations as OA;

/**
* @OA\Schema(
* required={"user"}
* )
*/
class CompleteOnboardingV1ResultDto
{
public CurrentUserResultDto $user;
}
28 changes: 28 additions & 0 deletions src/EconumoBundle/Application/User/OnboardingService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace App\EconumoBundle\Application\User;

use App\EconumoBundle\Application\User\Dto\CompleteOnboardingV1RequestDto;
use App\EconumoBundle\Application\User\Dto\CompleteOnboardingV1ResultDto;
use App\EconumoBundle\Application\User\Assembler\CompleteOnboardingV1ResultAssembler;
use App\EconumoBundle\Domain\Entity\ValueObject\Id;
use App\EconumoBundle\Domain\Service\UserServiceInterface;

readonly class OnboardingService
{
public function __construct(
private CompleteOnboardingV1ResultAssembler $completeOnboardingV1ResultAssembler,
private UserServiceInterface $userService
) {
}

public function completeOnboarding(
CompleteOnboardingV1RequestDto $dto,
Id $userId
): CompleteOnboardingV1ResultDto {
$this->userService->completeOnboarding($userId);
return $this->completeOnboardingV1ResultAssembler->assemble($userId);
}
}
12 changes: 9 additions & 3 deletions src/EconumoBundle/Domain/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@

namespace App\EconumoBundle\Domain\Entity;

use App\EconumoBundle\Domain\Entity\UserOption;
use App\EconumoBundle\Domain\Entity\ValueObject\CurrencyCode;
use App\EconumoBundle\Domain\Entity\ValueObject\Email;
use App\EconumoBundle\Domain\Entity\ValueObject\Id;
use App\EconumoBundle\Domain\Entity\ValueObject\Identifier;
use App\EconumoBundle\Domain\Entity\ValueObject\ReportPeriod;
use App\EconumoBundle\Domain\Events\UserRegisteredEvent;
use App\EconumoBundle\Domain\Traits\EntityTrait;
Expand Down Expand Up @@ -299,6 +296,15 @@ public function updateDefaultBudget(?Id $budgetId): void
}
}

public function completeOnboarding(): void
{
foreach ($this->options as $option) {
if ($option->getName() === UserOption::ONBOARDING) {
$option->updateValue(UserOption::ONBOARDING_VALUE_COMPLETED);
}
}
}

public function isActive(): bool
{
return $this->isActive;
Expand Down
16 changes: 16 additions & 0 deletions src/EconumoBundle/Domain/Entity/UserOption.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ class UserOption
*/
final public const BUDGET = 'budget';

/**
* @var string
*/
final public const ONBOARDING = 'onboarding';

/**
* @var string
*/
final public const ONBOARDING_VALUE_STARTED = 'started';

/**
* @var string
*/
final public const ONBOARDING_VALUE_COMPLETED = 'completed';


/**
* @var string[]
Expand All @@ -50,6 +65,7 @@ class UserOption
self::CURRENCY,
self::REPORT_PERIOD,
self::BUDGET,
self::ONBOARDING,
];

private readonly DateTimeImmutable $createdAt;
Expand Down
1 change: 1 addition & 0 deletions src/EconumoBundle/Domain/Service/Budget/BudgetService.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public function createBudget(
[$position, $categoriesOptions] = $this->budgetElementService->createCategoriesElements($userId, $budgetId);
$this->budgetElementService->createTagsElements($userId, $budgetId, $position);
$this->userService->updateBudget($userId, $budgetId);
$this->userService->completeOnboarding($userId);

$this->antiCorruptionService->commit(__METHOD__);
} catch (Throwable $throwable) {
Expand Down
17 changes: 16 additions & 1 deletion src/EconumoBundle/Domain/Service/UserService.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ public function register(Email $email, string $password, string $name): User
[
$this->userOptionFactory->create($user, UserOption::CURRENCY, UserOption::DEFAULT_CURRENCY),
$this->userOptionFactory->create($user, UserOption::REPORT_PERIOD, UserOption::DEFAULT_REPORT_PERIOD),
$this->userOptionFactory->create($user, UserOption::BUDGET, null)
$this->userOptionFactory->create($user, UserOption::BUDGET, null),
$this->userOptionFactory->create($user, UserOption::ONBOARDING, UserOption::ONBOARDING_VALUE_STARTED)
]
);

Expand Down Expand Up @@ -131,4 +132,18 @@ public function updateBudget(Id $userId, ?Id $budgetId): void
throw $throwable;
}
}

public function completeOnboarding(Id $userId): void
{
$this->antiCorruptionService->beginTransaction(__METHOD__);
try {
$user = $this->userRepository->get($userId);
$user->completeOnboarding();
$this->userRepository->save([$user]);
$this->antiCorruptionService->commit(__METHOD__);
} catch (Throwable $throwable) {
$this->antiCorruptionService->rollback(__METHOD__);
throw $throwable;
}
}
}
5 changes: 2 additions & 3 deletions src/EconumoBundle/Domain/Service/UserServiceInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@
interface UserServiceInterface
{
/**
* @param Email $email
* @param string $password
* @param string $name
* @return User
*/
public function register(Email $email, string $password, string $name): User;
Expand All @@ -27,4 +24,6 @@ public function updateCurrency(Id $userId, CurrencyCode $currencyCode): void;
public function updateReportPeriod(Id $userId, ReportPeriod $reportPeriod): void;

public function updateBudget(Id $userId, ?Id $budgetId): void;

public function completeOnboarding(Id $userId): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace App\EconumoBundle\Infrastructure\Doctrine\Migration;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
use Ramsey\Uuid\Uuid;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20241222201140 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}

public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->skipIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', "Migration can only be executed safely on 'sqlite'.");

$users = $this->connection->fetchAllAssociative('SELECT id, created_at, updated_at FROM users;');
foreach ($users as $i => $user) {
$id = (string) Uuid::uuid4();
$userId = $user['id'];
$createdAt = $user['created_at'];
$updatedAt = $user['updated_at'];
$this->addSql(sprintf('INSERT INTO users_options (id, user_id, name, value, created_at, updated_at) VALUES (\'%s\', \'%s\', \'onboarding\', \'started\', \'%s\', \'%s\')', $id, $userId, $createdAt, $updatedAt));
}
}

public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->skipIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', "Migration can only be executed safely on 'sqlite'.");

$this->addSql("DELETE FROM users_options WHERE name = 'onboarding';");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace App\EconumoBundle\UI\Controller\Api\User\Onboarding;

use App\EconumoBundle\Application\User\OnboardingService;
use App\EconumoBundle\Application\User\Dto\CompleteOnboardingV1RequestDto;
use App\EconumoBundle\UI\Controller\Api\User\Onboarding\Validation\CompleteOnboardingV1Form;
use App\EconumoBundle\Application\Exception\ValidationException;
use App\EconumoBundle\Domain\Entity\User;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use App\EconumoBundle\UI\Service\Validator\ValidatorInterface;
use App\EconumoBundle\UI\Service\Response\ResponseFactory;
use Symfony\Component\Routing\Annotation\Route;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Annotations as OA;

class CompleteOnboardingV1Controller extends AbstractController
{
public function __construct(
private readonly OnboardingService $onboardingService,
private readonly ValidatorInterface $validator
) {
}

/**
* Complete users onboarding
*
* @OA\Tag(name="User"),
* @OA\RequestBody(@OA\JsonContent(ref=@Model(type=\App\EconumoBundle\Application\User\Dto\CompleteOnboardingV1RequestDto::class))),
* @OA\Response(
* response=200,
* description="OK",
* @OA\JsonContent(
* type="object",
* allOf={
* @OA\Schema(ref="#/components/schemas/JsonResponseOk"),
* @OA\Schema(
* @OA\Property(
* property="data",
* ref=@Model(type=\App\EconumoBundle\Application\User\Dto\CompleteOnboardingV1ResultDto::class)
* )
* )
* }
* )
* ),
* @OA\Response(response=400, description="Bad Request", @OA\JsonContent(ref="#/components/schemas/JsonResponseError")),
* @OA\Response(response=401, description="Unauthorized", @OA\JsonContent(ref="#/components/schemas/JsonResponseUnauthorized")),
* @OA\Response(response=500, description="Internal Server Error", @OA\JsonContent(ref="#/components/schemas/JsonResponseException")),
*
*
* @return Response
* @throws ValidationException
*/
#[Route(path: '/api/v1/user/complete-onboarding', name: 'api_user_complete_onboarding', methods: ['POST'])]
public function __invoke(Request $request): Response
{
$dto = new CompleteOnboardingV1RequestDto();
$this->validator->validate(CompleteOnboardingV1Form::class, $request->request->all(), $dto);
/** @var User $user */
$user = $this->getUser();
$result = $this->onboardingService->completeOnboarding($dto, $user->getId());

return ResponseFactory::createOkResponse($request, $result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace App\EconumoBundle\UI\Controller\Api\User\Onboarding\Validation;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class CompleteOnboardingV1Form extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(['csrf_protection' => false]);
}

public function buildForm(FormBuilderInterface $builder, array $options): void
{
}
}
6 changes: 3 additions & 3 deletions tests/_data/fixtures/users.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
'name' => 'John',
'identifier' => 'ce2ff29bb72be7ee509d088bce780c32',
'email' => '[email protected]',
'avatar_url' => '',
'avatar_url' => 'https://i.pravatar.cc/100?img=60',
'password' => 'OWSXlB4C5oBuH6U/NTNZd0T2jQiTJXZvOQzaDgPc6i7IMyeeY+URRaNAlZdQ4DF8UVyRSkW1+n0IC9xog1HPrg==',
'salt' => '9dfc7b4f71b29d37e8b3855cac8314082ff8afc6',
'created_at' => date('Y-m-01 10:00:00'),
Expand All @@ -17,7 +17,7 @@
'name' => 'Dany',
'identifier' => 'e1e3c80be7c8183143a09b7078bfc8e0',
'email' => '[email protected]',
'avatar_url' => '',
'avatar_url' => 'https://i.pravatar.cc/100?img=47',
'password' => 'Ac6pLj+WdJYTTK58AKKSp1uHVtdtFVSjZVOLxt6oFAzqKIZ7nKBD7cztXt5vJn7q2U2iMTCRapP/VWf+nvtSoA==',
'salt' => '6ab0b56832e1f148e1051499d341ca772fb3b322',
'created_at' => date('Y-m-01 10:00:00'),
Expand All @@ -28,7 +28,7 @@
'name' => 'Sansa',
'identifier' => 'efb5dee2edea2a03a803c9c77b7dae94',
'email' => '[email protected]',
'avatar_url' => '',
'avatar_url' => 'https://i.pravatar.cc/100?img=20',
'password' => 'Ac6pLj+WdJYTTK58AKKSp1uHVtdtFVSjZVOLxt6oFAzqKIZ7nKBD7cztXt5vJn7q2U2iMTCRapP/VWf+nvtSoA==',
'salt' => '6ab0b56832e1f148e1051499d341ca772fb3b322',
'created_at' => date('Y-m-01 10:00:00'),
Expand Down
24 changes: 24 additions & 0 deletions tests/_data/fixtures/users_options.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,30 @@
"value": "RUB",
"created_at": "2021-10-07 12:22:18",
"updated_at": "2023-10-20 04:48:13"
},
{
"id": "b1a6b623-d727-4579-8484-50355eebaecd",
"user_id": "aff21334-96f0-4fb1-84d8-0223d0280954",
"name": "onboarding",
"value": "started",
"created_at": "2024-12-01 10:00:00",
"updated_at": "2024-12-22 15:23:37"
},
{
"id": "4f35d1b6-a9f0-4696-bd21-b4a5e0e135e5",
"user_id": "77be9577-147b-4f05-9aa7-91d9b159de5b",
"name": "onboarding",
"value": "started",
"created_at": "2024-12-01 10:00:00",
"updated_at": "2024-12-01 11:00:00"
},
{
"id": "c9930459-f10f-419b-af1b-415fb95b8f5b",
"user_id": "48044d88-5081-11ec-bf63-0242ac130002",
"name": "onboarding",
"value": "started",
"created_at": "2024-12-01 10:00:00",
"updated_at": "2024-12-01 11:00:00"
}
]
JSON;
Expand Down
Loading

0 comments on commit f91a522

Please sign in to comment.