Skip to content

Commit

Permalink
Add limit copy API endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
vokomarov committed Sep 7, 2024
1 parent 2979d24 commit 3034db7
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 1 deletion.
35 changes: 35 additions & 0 deletions app/src/Controller/Wallets/Limits/LimitsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,39 @@ public function delete(string $walletId, string $limitId): ResponseInterface

return $this->response->create(200);
}

#[Route(route: '/wallets/<walletId>/limits/copy/<sourceWalletId>', name: 'wallet.limit.copy', methods: 'POST', group: 'auth')]
public function copy(string $walletId, string $sourceWalletId): ResponseInterface
{
$wallet = $this->walletRepository->findByPKByUserPK((int) $walletId, (int) $this->user->id);

if (! $wallet instanceof Wallet) {
return $this->response->create(404);
}

$sourceWallet = $this->walletRepository->findByPKByUserPK((int) $sourceWalletId, (int) $this->user->id);

if (! $sourceWallet instanceof Wallet) {
return $this->response->create(404);
}

try {
$limits = $this->limitService->copy($wallet, $sourceWallet);
} catch (Throwable $exception) {
$this->logger->error('Unable to copy limits', [
'action' => 'wallet.limit.copy',
'walletId' => $wallet->id,
'sourceWalletId' => $sourceWallet->id,
'userId' => $this->user->id,
'msg' => $exception->getMessage(),
]);

return $this->response->json([
'message' => $this->say('limit_create_exception'),
'error' => $exception->getMessage(),
], 500);
}

return $this->walletLimitsView->json($limits);
}
}
9 changes: 9 additions & 0 deletions app/src/Controller/Wallets/ListController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use App\View\WalletsView;
use Psr\Http\Message\ResponseInterface;
use Spiral\Auth\AuthScope;
use Spiral\Http\Request\InputManager;
use Spiral\Http\ResponseWrapper;
use Spiral\Router\Annotation\Route;

Expand Down Expand Up @@ -64,4 +65,12 @@ public function listArchived(): ResponseInterface
$this->walletRepository->findAllByUserPKByArchived((int) $this->user->id, true)
);
}

#[Route(route: '/wallets/has-limits', name: 'wallet.list.has-limits', methods: 'GET', group: 'auth', priority: -1)]
public function listHasLimits(InputManager $input): ResponseInterface
{
return $this->walletsView->json(
$this->walletRepository->findAllHasLimitsByUserPK((int) $this->user->id, $input->query->has('archived'))
);
}
}
22 changes: 22 additions & 0 deletions app/src/Database/Wallet.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ class Wallet implements Sortable
#[ORM\Relation\ManyToMany(target: User::class, through: UserWallet::class, collection: 'doctrine')]
public PivotedCollection $users;

#[ORM\Relation\HasMany(target: Limit::class, outerKey: 'wallet_id', load: 'lazy')]
private PivotedCollection $limits;

/**
* @var \Doctrine\Common\Collections\ArrayCollection<int, \App\Database\Charge>|null
*/
Expand All @@ -69,6 +72,7 @@ public function __construct()
{
$this->defaultCurrency = new Currency();
$this->users = new PivotedCollection();
$this->limits = new PivotedCollection();
$this->createdAt = new \DateTimeImmutable();
$this->updatedAt = new \DateTimeImmutable();
}
Expand Down Expand Up @@ -101,6 +105,24 @@ public function getUsers(): array
return $users;
}

/**
* @return array<int, \App\Database\Limit>
*/
public function getLimits(): array
{
$limits = [];

foreach ($this->limits->getValues() as $limit) {
if (! $limit instanceof Limit) {
continue;
}

$limits[] = $limit;
}

return $limits;
}

/**
* @return array<array-key, int>
*/
Expand Down
20 changes: 20 additions & 0 deletions app/src/Repository/WalletRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,24 @@ public function findByPKByUserPKWithUsers(int $id, int $userID)
{
return $this->select()->wherePK($id)->where('users.id', $userID)->with('users')->fetchOne();
}

/**
* @param int $userID
* @param bool $isArchived
* @return \App\Database\Wallet[]
*/
public function findAllHasLimitsByUserPK(int $userID, bool $isArchived = false): array
{
/**
* @var \App\Database\Wallet[] $wallets
* @psalm-suppress InternalClass
*/
$wallets = $this->select()
->with('limits', ['method' => Select\JoinableLoader::JOIN])
->where('users.id', $userID)
->where('is_archived', $isArchived)
->fetchAll();

return $wallets;
}
}
38 changes: 37 additions & 1 deletion app/src/Service/Limit/LimitService.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@

use App\Database\Limit;
use App\Database\Tag;
use App\Database\Wallet;
use App\Repository\ChargeRepository;
use App\Repository\LimitRepository;
use Cycle\ORM\EntityManagerInterface;

class LimitService
{
public function __construct(
private readonly EntityManagerInterface $tr,
private readonly ChargeRepository $chargeRepository
private readonly ChargeRepository $chargeRepository,
private readonly LimitRepository $limitRepository,
) {
}

Expand All @@ -40,6 +43,39 @@ public function calculate(array $limits): array
return $list;
}

/**
* @param \App\Database\Wallet $target
* @param \App\Database\Wallet $source
* @return \App\Service\Limit\WalletLimit[]
*/
public function copy(Wallet $target, Wallet $source): array
{
$sourceLimits = $this->limitRepository->findAllByWalletPK((int) $source->id);

$list = [];

foreach ($sourceLimits as $sourceLimit) {
/** @var Limit $sourceLimit */

$limit = new Limit();
$limit->type = $sourceLimit->type;
$limit->amount = $sourceLimit->amount;
$limit->setWallet($target);

foreach ($sourceLimit->tags as $tag) {
$limit->tags->add($tag);
}

$this->tr->persist($limit);

$list[] = new WalletLimit($limit, 0);
}

$this->tr->run();

return $list;
}

public function store(Limit $limit): Limit
{
$this->tr->persist($limit);
Expand Down
90 changes: 90 additions & 0 deletions tests/Feature/Controller/Wallets/Limits/LimitsControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -667,4 +667,94 @@ public function testDeleteThrownException(): void
$this->assertArrayHasKey('error', $body);
$this->assertArrayHasKey('message', $body);
}

public function testCopyRequireAuth(): void
{
$user = $this->userFactory->create();
$wallet = $this->walletFactory->forUser($user)->create();
$sourceWallet = $this->walletFactory->forUser($user)->create();
$this->limitFactory->forWallet($sourceWallet)->create();

$response = $this->post("/wallets/{$wallet->id}/limits/copy/{$sourceWallet->id}");

$response->assertUnauthorized();
}

public function testCopyMissingWalletsStillRequireAuth(): void
{
$user = $this->userFactory->create();
$wallet = $this->walletFactory->forUser($user)->create();
$sourceWallet = $this->walletFactory->forUser($user)->create();
$this->limitFactory->forWallet($sourceWallet)->create();

$walletId = Fixtures::integer();
$sourceWalletId = Fixtures::integer();

$response = $this->post("/wallets/{$walletId}/limits/copy/{$sourceWallet->id}");
$response->assertUnauthorized();

$response = $this->post("/wallets/{$wallet->id}/limits/copy/{$sourceWalletId}");
$response->assertUnauthorized();

$response = $this->post("/wallets/{$walletId}/limits/copy/{$sourceWalletId}");
$response->assertUnauthorized();
}

public function testCopyNonMemberWalletReturnNotFound(): void
{
$wallet = $this->walletFactory->forUser($this->userFactory->create())->create();

$auth = $this->makeAuth($user = $this->userFactory->create());
$sourceWallet = $this->walletFactory->forUser($user)->create();
$this->limitFactory->forWallet($sourceWallet)->create();

$response = $this->withAuth($auth)->post("/wallets/{$wallet->id}/limits/copy/{$sourceWallet->id}");
$response->assertNotFound();
}

public function testCopyNonMemberSourceWalletReturnNotFound(): void
{
$auth = $this->makeAuth($user = $this->userFactory->create());
$wallet = $this->walletFactory->forUser($user)->create();

$sourceWallet = $this->walletFactory->forUser($this->userFactory->create())->create();
$this->limitFactory->forWallet($sourceWallet)->create();

$response = $this->withAuth($auth)->post("/wallets/{$wallet->id}/limits/copy/{$sourceWallet->id}");
$response->assertNotFound();
}

public function testCopyCreatesLimitsFromSourceWallet(): void
{
$auth = $this->makeAuth($user = $this->userFactory->create());

$wallet = $this->walletFactory->forUser($user)->create();

$sourceWallet = $this->walletFactory->forUser($user)->create();
$tags = $this->tagFactory->forUser($user)->createMany(2);
$limits = $this->limitFactory->forWallet($sourceWallet)->withTags($tags->toArray())->createMany(2);

$response = $this->withAuth($auth)->post("/wallets/{$wallet->id}/limits/copy/{$sourceWallet->id}");

$response->assertOk();

$body = $this->getJsonResponseBody($response);

$this->assertIsArray($body);
$this->assertArrayHasKey('data', $body);
$this->assertCount(count($limits), $body['data']);

foreach ($limits as $i => $limit) {
/** @var \App\Database\Limit $limit */
$this->assertArrayContains($limit->type, $body, 'data.*.limit.operation');
$this->assertArrayContains($limit->amount, $body, 'data.*.limit.amount');
$this->assertArrayContains($wallet->id, $body, 'data.*.limit.walletId');

$this->assertArrayHasKey('tags', $body['data'][$i]['limit']);
$this->assertCount(count($limits), $body['data'][$i]['limit']['tags']);
foreach ($tags as $tag) {
$this->assertArrayContains($tag->id, $body['data'][$i], 'limit.tags.*.id');
}
}
}
}
44 changes: 44 additions & 0 deletions tests/Feature/Controller/Wallets/ListControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
use App\Service\Sort\SortService;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\DatabaseTransaction;
use Tests\Factories\LimitFactory;
use Tests\Factories\TagFactory;
use Tests\Factories\UserFactory;
use Tests\Factories\WalletFactory;
use Tests\TestCase;
Expand All @@ -19,12 +21,18 @@ class ListControllerTest extends TestCase implements DatabaseTransaction

protected WalletFactory $walletFactory;

protected LimitFactory $limitFactory;

protected TagFactory $tagFactory;

protected function setUp(): void
{
parent::setUp();

$this->userFactory = $this->getContainer()->get(UserFactory::class);
$this->walletFactory = $this->getContainer()->get(WalletFactory::class);
$this->limitFactory = $this->getContainer()->get(LimitFactory::class);
$this->tagFactory = $this->getContainer()->get(TagFactory::class);
}

public function testListRequireAuth(): void
Expand Down Expand Up @@ -219,4 +227,40 @@ public function testSortUnArchivedThrownException(): void

$response->assertOk();
}

public function testListHasLimitsRequireAuth(): void
{
$response = $this->get('/wallets/has-limits');

$response->assertUnauthorized();
}

public function testListHasLimitsReturnsWalletsWithLimits(): void
{
$auth = $this->makeAuth($user = $this->userFactory->create());

/** @var \Doctrine\Common\Collections\ArrayCollection<int, Wallet> $wallets */
$wallets = $this->walletFactory->forUser($user)->createMany(3);
foreach ($wallets as $wallet) {
$tag = $this->tagFactory->forUser($user)->create();
$this->limitFactory->forWallet($wallet)->withTags([$tag])->create();
}
$walletWithoutLimit = $this->walletFactory->forUser($user)->create();

$response = $this->withAuth($auth)->get('/wallets/has-limits');

$response->assertOk();

$body = $this->getJsonResponseBody($response);

foreach ($wallets as $wallet) {
$this->assertArrayContains($wallet->id, $body, 'data.*.id');
$this->assertArrayContains($wallet->name, $body, 'data.*.name');
$this->assertArrayContains($wallet->slug, $body, 'data.*.slug');
}

$this->assertArrayNotContains($walletWithoutLimit->id, $body, 'data.*.id');
$this->assertArrayNotContains($walletWithoutLimit->name, $body, 'data.*.name');
$this->assertArrayNotContains($walletWithoutLimit->slug, $body, 'data.*.slug');
}
}
7 changes: 7 additions & 0 deletions tests/Traits/AssertHelpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ protected function assertArrayContains(mixed $needle, array $haystack, string $k
$value = data_get($haystack, $key, []);

if (is_array($value)) {
foreach ($value as &$item) {
if (is_float($item) || is_int($item)) {
$item = (string) $item;
$needle = (string) $needle;
}
}

$this->assertContains($needle, $value, $debug);
return;
}
Expand Down

0 comments on commit 3034db7

Please sign in to comment.