Skip to content

Commit

Permalink
Feature / Move Charges (#126)
Browse files Browse the repository at this point in the history
Added API endpoint to move multiple charges to a different wallet.
  • Loading branch information
vokomarov authored Aug 27, 2023
2 parents 48949bc + 832e424 commit cc2edcf
Show file tree
Hide file tree
Showing 12 changed files with 442 additions and 49 deletions.
2 changes: 1 addition & 1 deletion .rr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ http:
- http_metrics
- gzip
pool:
num_workers: 1
num_workers: ${RR_HTTP_NUM_WORKERS}
supervisor:
max_worker_memory: 100

Expand Down
41 changes: 41 additions & 0 deletions app/src/Controller/Wallets/Charges/ChargesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use App\Repository\TagRepository;
use App\Repository\WalletRepository;
use App\Request\Charge\CreateRequest;
use App\Request\Charge\MoveRequest;
use App\Service\ChargeWalletService;
use App\Service\Pagination\PaginationFactory;
use App\Service\Statistics\ChargeAmountGraph;
Expand Down Expand Up @@ -214,4 +215,44 @@ public function delete(string $walletId, string $chargeId): ResponseInterface

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

#[Route(route: '/wallets/<walletId>/charges/move/<targetWalletId>', name: 'wallet.charges.move', methods: 'POST', group: 'auth')]
public function move(string $walletId, string $targetWalletId, MoveRequest $request): ResponseInterface
{
$this->verifyIsProfileConfirmed();

$wallet = $this->walletRepository->findByPKByUserPK((int) $walletId, (int) $this->user->id);

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

$targetWallet = $this->walletRepository->findByPKByUserPK((int) $targetWalletId, (int) $this->user->id);

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

$charges = $this->chargeRepository->findByPKsByWalletPK($request->chargeIds, (int) $wallet->id);

try {
$this->chargeWalletService->move($wallet, $targetWallet, $charges);
} catch (\Throwable $exception) {
$this->logger->error('Unable to move charges', [
'action' => 'wallet.charges.move',
'id' => $wallet->id,
'targetId' => $targetWallet->id,
'chargeIds' => $request->chargeIds,
'userId' => $this->user->id,
'msg' => $exception->getMessage(),
]);

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

return $this->response->create(200);
}
}
4 changes: 2 additions & 2 deletions app/src/Database/GoogleAccount.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function getData(): array
}

try {
return (array) json_decode($this->data, true, JSON_THROW_ON_ERROR);
return (array) json_decode($this->data, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException $_) {
return [];
}
Expand All @@ -58,7 +58,7 @@ public function setData(array $data): void
{
try {
$this->data = json_encode($data, JSON_THROW_ON_ERROR);
} catch (\JsonException $exception) {
} catch (\JsonException $_) {
$this->data = '';
}
}
Expand Down
13 changes: 13 additions & 0 deletions app/src/Repository/ChargeRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ public function findByPKByWalletPK(string $chargeId, int $walletId)
->fetchOne();
}

/**
* @param array $chargeIds
* @param int $walletId
* @return array<array-key, \App\Database\Charge>
*/
public function findByPKsByWalletPK(array $chargeIds, int $walletId)
{
return $this->select()
->wherePK(...$chargeIds)
->where('wallet_id', $walletId)
->fetchAll();
}

/**
* @param int $walletId
* @param int $limit
Expand Down
30 changes: 30 additions & 0 deletions app/src/Request/Charge/MoveRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace App\Request\Charge;

use App\Database\Charge;
use Spiral\Filters\Attribute\Input\Data;
use Spiral\Filters\Model\Filter;
use Spiral\Filters\Model\FilterDefinitionInterface;
use Spiral\Filters\Model\HasFilterDefinition;
use Spiral\Validator\FilterDefinition;

class MoveRequest extends Filter implements HasFilterDefinition
{
/**
* @var array<array-key, string>
*/
#[Data]
public array $chargeIds = [];

public function filterDefinition(): FilterDefinitionInterface
{
return new FilterDefinition(validationRules: [
'chargeIds' => [
['array::of', ['entity:exists', Charge::class]],
],
]);
}
}
77 changes: 31 additions & 46 deletions app/src/Service/ChargeWalletService.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,12 @@

class ChargeWalletService
{
/**
* @var \Cycle\ORM\EntityManagerInterface
*/
private $tr;

/**
* ChargeWalletService constructor.
*
* @param \Cycle\ORM\EntityManagerInterface $tr
*/
public function __construct(EntityManagerInterface $tr)
const PRECISION = 2;

public function __construct(private readonly EntityManagerInterface $tr)
{
$this->tr = $tr;
}

/**
* @param \App\Database\Wallet $wallet
* @param \App\Database\Charge $charge
* @return \App\Database\Charge
* @throws \Throwable
*/
public function create(Wallet $wallet, Charge $charge): Charge
{
$wallet = $this->apply($wallet, $charge);
Expand All @@ -42,13 +27,6 @@ public function create(Wallet $wallet, Charge $charge): Charge
return $charge;
}

/**
* @param \App\Database\Wallet $wallet
* @param \App\Database\Charge $oldCharge
* @param \App\Database\Charge $newCharge
* @return \App\Database\Charge
* @throws \Throwable
*/
public function update(Wallet $wallet, Charge $oldCharge, Charge $newCharge): Charge
{
$wallet = $this->rollback($wallet, $oldCharge);
Expand All @@ -61,11 +39,6 @@ public function update(Wallet $wallet, Charge $oldCharge, Charge $newCharge): Ch
return $newCharge;
}

/**
* @param \App\Database\Wallet $wallet
* @param \App\Database\Charge $charge
* @throws \Throwable
*/
public function delete(Wallet $wallet, Charge $charge): void
{
$wallet = $this->rollback($wallet, $charge);
Expand All @@ -75,21 +48,29 @@ public function delete(Wallet $wallet, Charge $charge): void
$this->tr->run();
}

/**
* @param float $income
* @param float $expense
* @return float
*/
public function move(Wallet $wallet, Wallet $targetWallet, array $charges): void
{
foreach ($charges as $charge) {
if (! $charge instanceof Charge) {
continue;
}

$this->rollback($wallet, $charge);
$this->apply($targetWallet, $charge);
$charge->setWallet($targetWallet);
$this->tr->persist($charge);
}

$this->tr->persist($wallet);
$this->tr->persist($targetWallet);
$this->tr->run();
}

public function totalByIncomeAndExpense(float $income, float $expense): float
{
return $income - $expense;
return $this->safeFloatNumber($income - $expense);
}

/**
* @param \App\Database\Wallet $wallet
* @param \App\Database\Charge $charge
* @return \App\Database\Wallet
*/
protected function apply(Wallet $wallet, Charge $charge): Wallet
{
switch ($charge->type) {
Expand All @@ -101,14 +82,11 @@ protected function apply(Wallet $wallet, Charge $charge): Wallet
break;
}

$wallet->totalAmount = $this->safeFloatNumber($wallet->totalAmount);

return $wallet;
}

/**
* @param \App\Database\Wallet $wallet
* @param \App\Database\Charge $charge
* @return \App\Database\Wallet
*/
protected function rollback(Wallet $wallet, Charge $charge): Wallet
{
switch ($charge->type) {
Expand All @@ -120,6 +98,13 @@ protected function rollback(Wallet $wallet, Charge $charge): Wallet
break;
}

$wallet->totalAmount = $this->safeFloatNumber($wallet->totalAmount);

return $wallet;
}

protected function safeFloatNumber(float $number): float
{
return round($number, self::PRECISION);
}
}
37 changes: 37 additions & 0 deletions tests/Feature/Bootloader/GoogleApiBootloaderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Tests\Feature\Bootloader;

use App\Bootloader\GoogleApiBootloader;
use App\Config\GoogleApiConfig;
use Google\Client;
use Tests\Fixtures;
use Tests\TestCase;

class GoogleApiBootloaderTest extends TestCase
{
public function testResolve(): void
{
$config = $this->getContainer()->get(GoogleApiConfig::class);

$class = new \ReflectionClass($config);
$class->getProperty('config')->setValue($config, [
'clientId' => Fixtures::string(),
'clientSecret' => Fixtures::string(),
'projectId' => Fixtures::string(),
'authUri' => Fixtures::url(),
'tokenUri' => Fixtures::url(),
'authProviderX509CertUrl' => Fixtures::url(),
'redirectUris' => [Fixtures::url()],
]);

$bootloader = new GoogleApiBootloader($config);
$bootloader->boot($this->getContainer());

$client = $this->getContainer()->get(Client::class);

$this->assertInstanceOf(Client::class, $client);
}
}
37 changes: 37 additions & 0 deletions tests/Feature/Config/GoogleApiConfigTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Tests\Feature\Config;

use App\Config\GoogleApiConfig;
use Tests\Fixtures;
use Tests\TestCase;

class GoogleApiConfigTest extends TestCase
{
public function testConfigs(): void
{
/** @var \App\Config\GoogleApiConfig $config */
$config = $this->getContainer()->get(GoogleApiConfig::class);

$class = new \ReflectionClass($config);
$class->getProperty('config')->setValue($config, [
'clientId' => Fixtures::string(),
'clientSecret' => Fixtures::string(),
'projectId' => Fixtures::string(),
'authUri' => Fixtures::url(),
'tokenUri' => Fixtures::url(),
'authProviderX509CertUrl' => Fixtures::url(),
'redirectUris' => [Fixtures::url()],
]);

$this->assertNotEmpty($config->getClientId());
$this->assertNotEmpty($config->getClientSecret());
$this->assertNotEmpty($config->getProjectId());
$this->assertNotEmpty($config->getAuthUri());
$this->assertNotEmpty($config->getTokenUri());
$this->assertNotEmpty($config->getAuthProviderX509CertUrl());
$this->assertNotEmpty($config->getRedirectUris());
}
}
Loading

0 comments on commit cc2edcf

Please sign in to comment.