From d8955662b40ba85494ee1da3e1b1303e37617bb3 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 10 Feb 2025 12:26:59 +0100 Subject: [PATCH 01/13] Add models for AccountingSerie api resource and input dto --- .../Accounting/AccountingSerieApiResource.php | 44 +++++++++++++++++++ src/Dto/AccountingSerieDto.php | 36 +++++++++++++++ .../AccountingSerieStateProcessor.php | 18 ++++++++ 3 files changed, 98 insertions(+) create mode 100644 src/ApiResource/Accounting/AccountingSerieApiResource.php create mode 100644 src/Dto/AccountingSerieDto.php create mode 100644 src/State/Accounting/AccountingSerieStateProcessor.php diff --git a/src/ApiResource/Accounting/AccountingSerieApiResource.php b/src/ApiResource/Accounting/AccountingSerieApiResource.php new file mode 100644 index 0000000..c728351 --- /dev/null +++ b/src/ApiResource/Accounting/AccountingSerieApiResource.php @@ -0,0 +1,44 @@ + + */ + public array $data; +} diff --git a/src/Dto/AccountingSerieDto.php b/src/Dto/AccountingSerieDto.php new file mode 100644 index 0000000..fe02be3 --- /dev/null +++ b/src/Dto/AccountingSerieDto.php @@ -0,0 +1,36 @@ + Date: Mon, 10 Feb 2025 14:00:31 +0100 Subject: [PATCH 02/13] Update AccountingSerieDto default values --- src/Dto/AccountingSerieDto.php | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Dto/AccountingSerieDto.php b/src/Dto/AccountingSerieDto.php index fe02be3..4703469 100644 --- a/src/Dto/AccountingSerieDto.php +++ b/src/Dto/AccountingSerieDto.php @@ -2,13 +2,14 @@ namespace App\Dto; +use ApiPlatform\Metadata\ApiProperty; use App\ApiResource\Accounting\AccountingApiResource; use Symfony\Component\Validator\Constraints as Assert; class AccountingSerieDto { /** - * The source Accounting for the data serie. + * The source Accounting for the data serie. */ #[Assert\NotBlank()] public AccountingApiResource $accounting; @@ -22,15 +23,20 @@ class AccountingSerieDto /** * The end date for the data serie. Inclusive. */ - #[Assert\NotBlank()] - #[Assert\LessThanOrEqual('today')] - public \DateTimeInterface $dateEnd; + #[Assert\LessThanOrEqual('now')] + #[ApiProperty(default: 'now')] + public ?\DateTimeInterface $dateEnd = null; - /** - * The number of data points in the serie. + /** + * The max number of data points in the serie.\ + * Returned data set might have less data points if there is not enough data. */ - #[Assert\NotBlank()] #[Assert\Positive()] - #[Assert\Range(min: 10, max: 100)] - public int $length = 10; + #[Assert\Range(min: 1, max: 100)] + public int $maxLength = 10; + + public function __construct() + { + $this->dateEnd = $this->dateEnd ?: new \DateTimeImmutable('now'); + } } From 930750ad6f97e7fa4489d8c99cc13e130470f11b Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 10 Feb 2025 14:04:29 +0100 Subject: [PATCH 03/13] Make transactions be timestamped --- src/Entity/Accounting/Transaction.php | 3 ++ .../Accounting/TransactionRepository.php | 41 ++++++++++++++----- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/Entity/Accounting/Transaction.php b/src/Entity/Accounting/Transaction.php index afa9a1b..a0a1cab 100644 --- a/src/Entity/Accounting/Transaction.php +++ b/src/Entity/Accounting/Transaction.php @@ -3,6 +3,7 @@ namespace App\Entity\Accounting; use App\Entity\Money; +use App\Entity\Trait\TimestampedCreationEntity; use App\Repository\Accounting\TransactionRepository; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -18,6 +19,8 @@ use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: TransactionRepository::class)] class Transaction { + use TimestampedCreationEntity; + #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] diff --git a/src/Repository/Accounting/TransactionRepository.php b/src/Repository/Accounting/TransactionRepository.php index 625b233..a160df0 100644 --- a/src/Repository/Accounting/TransactionRepository.php +++ b/src/Repository/Accounting/TransactionRepository.php @@ -23,18 +23,39 @@ class TransactionRepository extends ServiceEntityRepository } /** + * @param \DateTimeInterface|null $dateStart a date to act as lower bound of the selection + * @param \DateTimeInterface|null $dateEnd A date to act as upper bound of the selection. Current date will be used if null. + * * @return Transaction[] Transactions originated from or targeting the Accounting */ - public function findByAccounting(Accounting $accounting): array - { - return $this->createQueryBuilder('t') - ->andWhere('t.origin = :val') - ->orWhere('t.target = :val') - ->setParameter('val', $accounting) - ->orderBy('t.id', 'ASC') - ->getQuery() - ->getResult() - ; + public function findByAccounting( + Accounting $accounting, + ?\DateTimeInterface $dateStart = null, + ?\DateTimeInterface $dateEnd = null, + ): array { + $queryBuilder = $this->createQueryBuilder('t'); + $accountingExpr = $queryBuilder->expr(); + + if ($dateEnd === null) { + $dateEnd = new \DateTime(); + } + + $query = $queryBuilder + ->andWhere($accountingExpr->orX( + $accountingExpr->eq('t.origin', $accounting->getId()), + $accountingExpr->eq('t.target', $accounting->getId()), + )) + ->andWhere('t.dateCreated <= :dateEnd') + ->setParameter('dateEnd', $dateEnd); + + if ($dateStart !== null) { + $query->andWhere('t.dateCreated >= :dateStart') + ->setParameter('dateStart', $dateStart); + } + + $query->orderBy('t.id', 'ASC'); + + return $query->getQuery()->getResult(); } /** From 1b31241cf919feacf506cf61e819b9db2c5fb5e9 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 10 Feb 2025 14:37:44 +0100 Subject: [PATCH 04/13] Add test for accounting service --- tests/Service/AccountingServiceTest.php | 77 +++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/Service/AccountingServiceTest.php diff --git a/tests/Service/AccountingServiceTest.php b/tests/Service/AccountingServiceTest.php new file mode 100644 index 0000000..74a196a --- /dev/null +++ b/tests/Service/AccountingServiceTest.php @@ -0,0 +1,77 @@ +accountingService = static::getContainer()->get(AccountingService::class); + $this->entityManager = static::getContainer()->get(EntityManagerInterface::class); + } + + public function getBalancedAccountings(): array + { + $tipjars = []; + + for ($i = 0; $i < 10; ++$i) { + $tipjar = new Tipjar(); + $tipjar->setName(\sprintf('TEST_TIPJAR_%d', $i)); + + $this->entityManager->persist($tipjar); + + $tipjars[] = $tipjar; + } + + $this->entityManager->flush(); + + for ($i = 0; $i < \count($tipjars) * 5; ++$i) { + $origin = $i % 2 ? $tipjars[\random_int(0, 5)] : $tipjars[\random_int(5, 9)]; + $target = $i % 2 ? $tipjars[\random_int(5, 9)] : $tipjars[\random_int(0, 5)]; + + $trx = new Transaction(); + $trx->setMoney(new Money(random_int(100, 999), 'EUR')); + $trx->setOrigin($origin->getAccounting()); + $trx->setTarget($target->getAccounting()); + + $this->entityManager->persist($trx); + } + + $this->entityManager->flush(); + + return \array_map(function (Tipjar $tipjar) { + return $tipjar->getAccounting(); + }, $tipjars); + } + + public function testBalanceSerieNotOverLength() + { + $accountings = $this->getBalancedAccountings(); + + foreach ($accountings as $accounting) { + $serie = $this->accountingService->calcBalanceSerie($accounting, maxLength: 3); + + $this->assertLessThanOrEqual(3, \count($serie)); + + foreach ($serie as $dataPoint) { + $this->assertInstanceOf(Money::class, $dataPoint); + $this->assertGreaterThan(0, $dataPoint->amount); + } + } + } +} From 78208155708321404d320d80bb96d0a4bcbadf75 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 10 Feb 2025 14:38:14 +0100 Subject: [PATCH 05/13] Add method to calculate a data series for an accounting balance --- src/Service/AccountingService.php | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/Service/AccountingService.php b/src/Service/AccountingService.php index cc5d179..00f059a 100644 --- a/src/Service/AccountingService.php +++ b/src/Service/AccountingService.php @@ -40,4 +40,42 @@ class AccountingService return $balance; } + + /** + * Calculates a balance data series for a given Accounting over a period of time. + * + * @param Accounting The Accounting to calc the data series for + * @param \DateTimeInterface|null The date to start calculating from + * @param \DateTimeInterface|null The date to calculate up to. Inclusive + * @param int $maxLength The max number of data points to include in the returned series + * + * @return array a series of aggregated balances, up to $maxLength in size + */ + public function calcBalanceSerie( + Accounting $accounting, + ?\DateTimeInterface $dateStart = null, + ?\DateTimeInterface $dateEnd = null, + int $maxLength = 10, + ): array { + $transactions = $this->transactionRepository->findByAccounting($accounting, $dateStart, $dateEnd, true); + + $dataPointLength = \ceil(\count($transactions) / $maxLength); + $dataPointLength = $dataPointLength < 1 ? 1 : $dataPointLength; + + $dataPointItems = \array_chunk($transactions, $dataPointLength); + + /** @var Money[] */ + $dataPoints = []; + foreach ($dataPointItems as $dataPointItem) { + $dataPointBalance = $dataPointItem[0]->getMoney(); + + foreach (\array_slice($dataPointItem, 1) as $trx) { + $dataPointBalance = $this->money->add($trx->getMoney(), $dataPointBalance); + } + + $dataPoints[] = $dataPointBalance; + } + + return $dataPoints; + } } From a1655bdc74c80e5096cd294a94a2d54293d5a76c Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 10 Feb 2025 14:38:39 +0100 Subject: [PATCH 06/13] Update accounting serie api resource --- .../Accounting/AccountingSerieApiResource.php | 3 +- .../AccountingSerieStateProcessor.php | 31 +++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/ApiResource/Accounting/AccountingSerieApiResource.php b/src/ApiResource/Accounting/AccountingSerieApiResource.php index c728351..27adcea 100644 --- a/src/ApiResource/Accounting/AccountingSerieApiResource.php +++ b/src/ApiResource/Accounting/AccountingSerieApiResource.php @@ -3,7 +3,6 @@ namespace App\ApiResource\Accounting; use ApiPlatform\Metadata as API; -use App\ApiResource\Accounting\AccountingApiResource; use App\Dto\AccountingSerieDto; use App\State\Accounting\AccountingSerieStateProcessor; @@ -18,7 +17,7 @@ use App\State\Accounting\AccountingSerieStateProcessor; class AccountingSerieApiResource { /** - * The source Accounting for the data serie. + * The source Accounting for the data serie. */ public AccountingApiResource $accounting; diff --git a/src/State/Accounting/AccountingSerieStateProcessor.php b/src/State/Accounting/AccountingSerieStateProcessor.php index 691ee91..140a107 100644 --- a/src/State/Accounting/AccountingSerieStateProcessor.php +++ b/src/State/Accounting/AccountingSerieStateProcessor.php @@ -4,15 +4,42 @@ namespace App\State\Accounting; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; +use App\ApiResource\Accounting\AccountingSerieApiResource; use App\Dto\AccountingSerieDto; +use App\Entity\Accounting\Accounting; +use App\Mapping\AutoMapper; +use App\Service\AccountingService; class AccountingSerieStateProcessor implements ProcessorInterface { + public function __construct( + private AutoMapper $autoMapper, + private AccountingService $accountingService, + ) {} + /** * @param AccountingSerieDto $data + * + * @return AccountingSerieApiResource */ - public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) { - dd($data); + /** @var Accounting */ + $accounting = $this->autoMapper->map($data->accounting, Accounting::class); + + $balanceData = $this->accountingService->calcBalanceSerie( + $accounting, + $data->dateStart, + $data->dateEnd, + $data->maxLength + ); + + /** @var AccountingSerieApiResource */ + $serie = $this->autoMapper->map($data, AccountingSerieApiResource::class); + + $serie->data = $balanceData; + $serie->length = \count($balanceData); + + return $serie; } } From 6152bca2bad38ea6f68e726d6f3aaaff35684f72 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 10 Feb 2025 14:53:05 +0100 Subject: [PATCH 07/13] Update description of accounting serie api resource --- .../Accounting/AccountingSerieApiResource.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ApiResource/Accounting/AccountingSerieApiResource.php b/src/ApiResource/Accounting/AccountingSerieApiResource.php index 27adcea..d6f067a 100644 --- a/src/ApiResource/Accounting/AccountingSerieApiResource.php +++ b/src/ApiResource/Accounting/AccountingSerieApiResource.php @@ -7,7 +7,14 @@ use App\Dto\AccountingSerieDto; use App\State\Accounting\AccountingSerieStateProcessor; /** - * An AccountingSerie displays plottable aggregated balance data over a time range for a given Accounting. + * An AccountingSerie displays plottable, aggregated balance data over a time range for a given Accounting.\ + * \ + * AccountingSeries are calculated in real-time for each request from the available data for the Accountings, + * each data point is a Money object representing the aggregated balance of the Accounting at that point in the serie.\ + * \ + * Data points are relative to the series and do not relate to any specific unit of time.\ + * You might get data points for specific days when you make the `maxLength` match the number of days in your date range, + * but the behaviour is not guaranteed and data points might be less than your desired output if there is not enough data. */ #[API\ApiResource(shortName: 'AccountingSerie')] #[API\Post( From 665ba09a88b3200625f7e7ebe35e77c742a04813 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 10 Feb 2025 15:07:17 +0100 Subject: [PATCH 08/13] Make serie data be aggregated --- src/Service/AccountingService.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Service/AccountingService.php b/src/Service/AccountingService.php index 00f059a..6b830a3 100644 --- a/src/Service/AccountingService.php +++ b/src/Service/AccountingService.php @@ -69,6 +69,10 @@ class AccountingService foreach ($dataPointItems as $dataPointItem) { $dataPointBalance = $dataPointItem[0]->getMoney(); + if (!empty($dataPoints)) { + $dataPointBalance = $this->money->add(\end($dataPoints), $dataPointBalance); + } + foreach (\array_slice($dataPointItem, 1) as $trx) { $dataPointBalance = $this->money->add($trx->getMoney(), $dataPointBalance); } From 7b46265b13cffbdb12d4d87d0d9faa7d58a94a2a Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 10 Feb 2025 17:13:17 +0100 Subject: [PATCH 09/13] Update AccountingService balance series calc to work by date periods --- src/Service/AccountingService.php | 77 +++++++++++++++---------- tests/Service/AccountingServiceTest.php | 19 ++++-- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/src/Service/AccountingService.php b/src/Service/AccountingService.php index 6b830a3..d02e8c1 100644 --- a/src/Service/AccountingService.php +++ b/src/Service/AccountingService.php @@ -3,6 +3,7 @@ namespace App\Service; use App\Entity\Accounting\Accounting; +use App\Entity\Accounting\Transaction; use App\Entity\Money; use App\Entity\User\User; use App\Gateway\Wallet\WalletService; @@ -26,9 +27,9 @@ class AccountingService } $balance = new Money(0, $accounting->getCurrency()); - $transactions = $this->transactionRepository->findByAccounting($accounting); + $trxs = $this->transactionRepository->findByAccounting($accounting); - foreach ($transactions as $transaction) { + foreach ($trxs as $transaction) { if ($transaction->getTarget() === $accounting) { $balance = $this->money->add($transaction->getMoney(), $balance); } @@ -44,42 +45,58 @@ class AccountingService /** * Calculates a balance data series for a given Accounting over a period of time. * - * @param Accounting The Accounting to calc the data series for - * @param \DateTimeInterface|null The date to start calculating from - * @param \DateTimeInterface|null The date to calculate up to. Inclusive - * @param int $maxLength The max number of data points to include in the returned series + * @param Accounting $accounting The Accounting to calc the data series for + * @param \DatePeriod $period A period of time for the desired transactions and time unit of the serie * - * @return array a series of aggregated balances, up to $maxLength in size + * @return array A series of aggregated balances */ - public function calcBalanceSerie( + public function calcBalanceSeries( Accounting $accounting, - ?\DateTimeInterface $dateStart = null, - ?\DateTimeInterface $dateEnd = null, - int $maxLength = 10, + \DatePeriod $period, ): array { - $transactions = $this->transactionRepository->findByAccounting($accounting, $dateStart, $dateEnd, true); + $trxs = $this->transactionRepository->findByAccounting( + $accounting, + $period->getStartDate(), + $period->getEndDate(), + true + ); - $dataPointLength = \ceil(\count($transactions) / $maxLength); - $dataPointLength = $dataPointLength < 1 ? 1 : $dataPointLength; + /** @var Transaction[][] */ + $intervals = []; - $dataPointItems = \array_chunk($transactions, $dataPointLength); + foreach ($period as $lowerBound) { + $upperBound = \DateTime::createFromInterface($lowerBound); + $upperBound->add($period->getDateInterval()); - /** @var Money[] */ - $dataPoints = []; - foreach ($dataPointItems as $dataPointItem) { - $dataPointBalance = $dataPointItem[0]->getMoney(); - - if (!empty($dataPoints)) { - $dataPointBalance = $this->money->add(\end($dataPoints), $dataPointBalance); - } - - foreach (\array_slice($dataPointItem, 1) as $trx) { - $dataPointBalance = $this->money->add($trx->getMoney(), $dataPointBalance); - } - - $dataPoints[] = $dataPointBalance; + $intervals[] = [...\array_filter($trxs, function (Transaction $trx) use ($lowerBound, $upperBound) { + return $trx->getDateCreated() >= $lowerBound + && $trx->getDateCreated() < $upperBound; + })]; } - return $dataPoints; + /** @var Money[] */ + $points = []; + + foreach ($intervals as $interval) { + $point = new Money(0, $accounting->getCurrency()); + + foreach ($interval as $trx) { + if ($trx->getTarget() === $accounting) { + $point = $this->money->add($trx->getMoney(), $point); + } + + if ($trx->getOrigin() === $accounting) { + $point = $this->money->substract($trx->getMoney(), $point); + } + } + + if (!empty($points)) { + $point = $this->money->add(\end($points), $point); + } + + $points[] = $point; + } + + return $points; } } diff --git a/tests/Service/AccountingServiceTest.php b/tests/Service/AccountingServiceTest.php index 74a196a..0886ace 100644 --- a/tests/Service/AccountingServiceTest.php +++ b/tests/Service/AccountingServiceTest.php @@ -59,19 +59,26 @@ class AccountingServiceTest extends KernelTestCase }, $tipjars); } - public function testBalanceSerieNotOverLength() + public function testBalanceSerieAggregatesData() { $accountings = $this->getBalancedAccountings(); + $period = new \DatePeriod( + (new \DateTime('now'))->sub(new \DateInterval('PT1H')), + new \DateInterval('PT10M'), + new \DateTime('now') + ); + foreach ($accountings as $accounting) { - $serie = $this->accountingService->calcBalanceSerie($accounting, maxLength: 3); + $series = $this->accountingService->calcBalanceSeries($accounting, $period); - $this->assertLessThanOrEqual(3, \count($serie)); + $this->assertCount(6, $series); - foreach ($serie as $dataPoint) { - $this->assertInstanceOf(Money::class, $dataPoint); - $this->assertGreaterThan(0, $dataPoint->amount); + foreach ($series as $point) { + $this->assertInstanceOf(Money::class, $point); } + + $this->assertNotEquals(0, end($series)->amount); } } } From 6530c3ab1fba224e15da0df9553f3398251dfeed Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Mon, 10 Feb 2025 17:14:06 +0100 Subject: [PATCH 10/13] Update AccountingSeries resource to new service calculation functionality --- .../Accounting/AccountingSerieApiResource.php | 50 ----------------- .../AccountingSeriesApiResource.php | 54 +++++++++++++++++++ src/Dto/AccountingSerieDto.php | 42 --------------- src/Dto/AccountingSeriesDto.php | 41 ++++++++++++++ ...php => AccountingSeriesStateProcessor.php} | 27 +++++----- 5 files changed, 110 insertions(+), 104 deletions(-) delete mode 100644 src/ApiResource/Accounting/AccountingSerieApiResource.php create mode 100644 src/ApiResource/Accounting/AccountingSeriesApiResource.php delete mode 100644 src/Dto/AccountingSerieDto.php create mode 100644 src/Dto/AccountingSeriesDto.php rename src/State/Accounting/{AccountingSerieStateProcessor.php => AccountingSeriesStateProcessor.php} (51%) diff --git a/src/ApiResource/Accounting/AccountingSerieApiResource.php b/src/ApiResource/Accounting/AccountingSerieApiResource.php deleted file mode 100644 index d6f067a..0000000 --- a/src/ApiResource/Accounting/AccountingSerieApiResource.php +++ /dev/null @@ -1,50 +0,0 @@ - - */ - public array $data; -} diff --git a/src/ApiResource/Accounting/AccountingSeriesApiResource.php b/src/ApiResource/Accounting/AccountingSeriesApiResource.php new file mode 100644 index 0000000..c56693d --- /dev/null +++ b/src/ApiResource/Accounting/AccountingSeriesApiResource.php @@ -0,0 +1,54 @@ + + */ + public array $data; +} diff --git a/src/Dto/AccountingSerieDto.php b/src/Dto/AccountingSerieDto.php deleted file mode 100644 index 4703469..0000000 --- a/src/Dto/AccountingSerieDto.php +++ /dev/null @@ -1,42 +0,0 @@ -dateEnd = $this->dateEnd ?: new \DateTimeImmutable('now'); - } -} diff --git a/src/Dto/AccountingSeriesDto.php b/src/Dto/AccountingSeriesDto.php new file mode 100644 index 0000000..94e8087 --- /dev/null +++ b/src/Dto/AccountingSeriesDto.php @@ -0,0 +1,41 @@ +end ??= new \DateTimeImmutable('now'); + } +} diff --git a/src/State/Accounting/AccountingSerieStateProcessor.php b/src/State/Accounting/AccountingSeriesStateProcessor.php similarity index 51% rename from src/State/Accounting/AccountingSerieStateProcessor.php rename to src/State/Accounting/AccountingSeriesStateProcessor.php index 140a107..cbd4ad1 100644 --- a/src/State/Accounting/AccountingSerieStateProcessor.php +++ b/src/State/Accounting/AccountingSeriesStateProcessor.php @@ -4,13 +4,13 @@ namespace App\State\Accounting; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; -use App\ApiResource\Accounting\AccountingSerieApiResource; +use App\ApiResource\Accounting\AccountingSeriesApiResource; use App\Dto\AccountingSerieDto; use App\Entity\Accounting\Accounting; use App\Mapping\AutoMapper; use App\Service\AccountingService; -class AccountingSerieStateProcessor implements ProcessorInterface +class AccountingSeriesStateProcessor implements ProcessorInterface { public function __construct( private AutoMapper $autoMapper, @@ -20,26 +20,29 @@ class AccountingSerieStateProcessor implements ProcessorInterface /** * @param AccountingSerieDto $data * - * @return AccountingSerieApiResource + * @return AccountingSeriesApiResource */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) { /** @var Accounting */ $accounting = $this->autoMapper->map($data->accounting, Accounting::class); - $balanceData = $this->accountingService->calcBalanceSerie( + $points = $this->accountingService->calcBalanceSeries( $accounting, - $data->dateStart, - $data->dateEnd, - $data->maxLength + new \DatePeriod( + $data->start, + new \DateInterval(\sprintf('PT%s', $data->interval)), + $data->end, + \DatePeriod::INCLUDE_END_DATE + ) ); - /** @var AccountingSerieApiResource */ - $serie = $this->autoMapper->map($data, AccountingSerieApiResource::class); + /** @var AccountingSeriesApiResource */ + $series = $this->autoMapper->map($data, AccountingSeriesApiResource::class); - $serie->data = $balanceData; - $serie->length = \count($balanceData); + $series->data = $points; + $series->length = \count($points); - return $serie; + return $series; } } From 674c31b28819c3e2be82ab50ede01d4f071948d0 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 11 Feb 2025 12:15:55 +0100 Subject: [PATCH 11/13] Experimental dated balance point resource --- .../Accounting/AccountingBalancePoint.php | 59 +++++++++++++++++++ .../AccountingBalancePointStateProvider.php | 55 +++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/ApiResource/Accounting/AccountingBalancePoint.php create mode 100644 src/State/Accounting/AccountingBalancePointStateProvider.php diff --git a/src/ApiResource/Accounting/AccountingBalancePoint.php b/src/ApiResource/Accounting/AccountingBalancePoint.php new file mode 100644 index 0000000..bb73529 --- /dev/null +++ b/src/ApiResource/Accounting/AccountingBalancePoint.php @@ -0,0 +1,59 @@ + new QueryParameter( + required: true, + schema: ['type' => 'string', 'format' => 'iri-reference'], + ), + 'start' => new QueryParameter( + required: true, + schema: ['type' => 'string', 'format' => 'date-time'], + ), + 'interval' => new QueryParameter( + schema: ['type' => 'string', 'default' => '24h'], + constraints: [new Assert\Choice(['24h'])], + ), + 'end' => new QueryParameter( + schema: ['type' => 'string', 'format' => 'date-time', 'default' => 'now'], + ), + ], +)] +class AccountingBalancePoint +{ + /** + * The start date for items aggregated in this point. + */ + public \DateTimeInterface $start; + + /** + * The end date for items aggregated in this point. Not inclusive. + */ + public \DateTimeInterface $end; + + /** + * Resulting balance for items in this point. + */ + public Money $balance; + + /** + * The number of items aggregated in this point. + */ + public int $length; +} diff --git a/src/State/Accounting/AccountingBalancePointStateProvider.php b/src/State/Accounting/AccountingBalancePointStateProvider.php new file mode 100644 index 0000000..b719635 --- /dev/null +++ b/src/State/Accounting/AccountingBalancePointStateProvider.php @@ -0,0 +1,55 @@ +getParameters(); + + $accounting = $this->getAccounting($parameters); + $period = $this->buildPeriod($parameters); + + $points = $this->accountingService->calcBalancePoints($accounting, $period); + + dd($points); + } + + private function getAccounting(Parameters $parameters): Accounting + { + $parameter = $parameters->get('accounting'); + $resource = $this->iriConverter->getResourceFromIri($parameter->getValue()); + + /** @var Accounting */ + $accounting = $this->autoMapper->map($resource, Accounting::class); + + return $accounting; + } + + private function buildPeriod(Parameters $parameters): \DatePeriod + { + $start = new \DateTimeImmutable($parameters->get('start')->getValue()); + + $interval = $parameters->get('interval')->getValue(); + $interval = new \DateInterval(\sprintf('PT%s', \strtoupper($interval))); + + $end = new \DateTimeImmutable($parameters->get('end')->getValue()); + + return new \DatePeriod($start, $interval, $end); + } +} From 954bf8e7c16bcb24cf431b46d37f51983cf6d8bd Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 11 Feb 2025 12:17:29 +0100 Subject: [PATCH 12/13] Update accounting service to work with AccountingBalancePoint --- src/Service/AccountingService.php | 36 +++++++++++-------------- tests/Service/AccountingServiceTest.php | 35 ++++++++++++++++-------- 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/src/Service/AccountingService.php b/src/Service/AccountingService.php index d02e8c1..954214a 100644 --- a/src/Service/AccountingService.php +++ b/src/Service/AccountingService.php @@ -2,6 +2,7 @@ namespace App\Service; +use App\ApiResource\Accounting\AccountingBalancePoint; use App\Entity\Accounting\Accounting; use App\Entity\Accounting\Transaction; use App\Entity\Money; @@ -43,14 +44,14 @@ class AccountingService } /** - * Calculates a balance data series for a given Accounting over a period of time. + * Calculates an AccountingBalancePoint series for a given Accounting over a period of time. * * @param Accounting $accounting The Accounting to calc the data series for * @param \DatePeriod $period A period of time for the desired transactions and time unit of the serie * - * @return array A series of aggregated balances + * @return array One AccountingBalancePoint for each interval inside the date period range */ - public function calcBalanceSeries( + public function calcBalancePoints( Accounting $accounting, \DatePeriod $period, ): array { @@ -58,41 +59,36 @@ class AccountingService $accounting, $period->getStartDate(), $period->getEndDate(), - true ); - - /** @var Transaction[][] */ - $intervals = []; + $points = []; foreach ($period as $lowerBound) { $upperBound = \DateTime::createFromInterface($lowerBound); $upperBound->add($period->getDateInterval()); - $intervals[] = [...\array_filter($trxs, function (Transaction $trx) use ($lowerBound, $upperBound) { + /** @var Transactions[] */ + $periodTrxs = [...\array_filter($trxs, function (Transaction $trx) use ($lowerBound, $upperBound) { return $trx->getDateCreated() >= $lowerBound && $trx->getDateCreated() < $upperBound; })]; - } - /** @var Money[] */ - $points = []; + $balance = new Money(0, $accounting->getCurrency()); - foreach ($intervals as $interval) { - $point = new Money(0, $accounting->getCurrency()); - - foreach ($interval as $trx) { + foreach ($periodTrxs as $trx) { if ($trx->getTarget() === $accounting) { - $point = $this->money->add($trx->getMoney(), $point); + $balance = $this->money->add($trx->getMoney(), $balance); } if ($trx->getOrigin() === $accounting) { - $point = $this->money->substract($trx->getMoney(), $point); + $balance = $this->money->substract($trx->getMoney(), $balance); } } - if (!empty($points)) { - $point = $this->money->add(\end($points), $point); - } + $point = new AccountingBalancePoint(); + $point->start = $lowerBound; + $point->end = $upperBound; + $point->balance = $balance; + $point->length = \count($periodTrxs); $points[] = $point; } diff --git a/tests/Service/AccountingServiceTest.php b/tests/Service/AccountingServiceTest.php index 0886ace..097952b 100644 --- a/tests/Service/AccountingServiceTest.php +++ b/tests/Service/AccountingServiceTest.php @@ -2,6 +2,7 @@ namespace App\Tests\Service; +use App\ApiResource\Accounting\AccountingBalancePoint; use App\Entity\Accounting\Transaction; use App\Entity\Money; use App\Entity\Tipjar; @@ -59,26 +60,38 @@ class AccountingServiceTest extends KernelTestCase }, $tipjars); } - public function testBalanceSerieAggregatesData() + public function provideDateIntervals(): array + { + return [ + [new \DateInterval('PT1H'), new \DateInterval('PT10M'), 6], + [new \DateInterval('PT1H'), new \DateInterval('PT23M'), 3], + [new \DateInterval('PT2M'), new \DateInterval('PT37S'), 4], + ]; + } + + /** + * @dataProvider provideDateIntervals + */ + public function testBalancePointsLengthIsOfPeriod(\DateInterval $started, \DateInterval $interval, int $count) { $accountings = $this->getBalancedAccountings(); - $period = new \DatePeriod( - (new \DateTime('now'))->sub(new \DateInterval('PT1H')), - new \DateInterval('PT10M'), - new \DateTime('now') - ); + $now = new \DateTimeImmutable('now'); + $period = new \DatePeriod($now->sub($started), $interval, $now); foreach ($accountings as $accounting) { - $series = $this->accountingService->calcBalanceSeries($accounting, $period); + $points = $this->accountingService->calcBalancePoints($accounting, $period); - $this->assertCount(6, $series); + $this->assertCount($count, $points); - foreach ($series as $point) { - $this->assertInstanceOf(Money::class, $point); + foreach ($points as $point) { + $this->assertInstanceOf(AccountingBalancePoint::class, $point); } - $this->assertNotEquals(0, end($series)->amount); + $end = $points[\array_key_last($points)]; + + $this->assertNotEquals(0, $end->length); + $this->assertNotEquals(0, $end->balance->amount); } } } From fbbebbc7589b7b0c122b641645075df5690a25b5 Mon Sep 17 00:00:00 2001 From: Daniel Subiabre Date: Tue, 11 Feb 2025 12:22:29 +0100 Subject: [PATCH 13/13] Deprecate AccountingSeries for AccountingBalancePoint --- .../AccountingSeriesApiResource.php | 54 ------------------- src/Dto/AccountingSeriesDto.php | 41 -------------- .../AccountingBalancePointStateProvider.php | 4 +- .../AccountingSeriesStateProcessor.php | 48 ----------------- 4 files changed, 1 insertion(+), 146 deletions(-) delete mode 100644 src/ApiResource/Accounting/AccountingSeriesApiResource.php delete mode 100644 src/Dto/AccountingSeriesDto.php delete mode 100644 src/State/Accounting/AccountingSeriesStateProcessor.php diff --git a/src/ApiResource/Accounting/AccountingSeriesApiResource.php b/src/ApiResource/Accounting/AccountingSeriesApiResource.php deleted file mode 100644 index c56693d..0000000 --- a/src/ApiResource/Accounting/AccountingSeriesApiResource.php +++ /dev/null @@ -1,54 +0,0 @@ - - */ - public array $data; -} diff --git a/src/Dto/AccountingSeriesDto.php b/src/Dto/AccountingSeriesDto.php deleted file mode 100644 index 94e8087..0000000 --- a/src/Dto/AccountingSeriesDto.php +++ /dev/null @@ -1,41 +0,0 @@ -end ??= new \DateTimeImmutable('now'); - } -} diff --git a/src/State/Accounting/AccountingBalancePointStateProvider.php b/src/State/Accounting/AccountingBalancePointStateProvider.php index b719635..853eecb 100644 --- a/src/State/Accounting/AccountingBalancePointStateProvider.php +++ b/src/State/Accounting/AccountingBalancePointStateProvider.php @@ -25,9 +25,7 @@ class AccountingBalancePointStateProvider implements ProviderInterface $accounting = $this->getAccounting($parameters); $period = $this->buildPeriod($parameters); - $points = $this->accountingService->calcBalancePoints($accounting, $period); - - dd($points); + return $this->accountingService->calcBalancePoints($accounting, $period); } private function getAccounting(Parameters $parameters): Accounting diff --git a/src/State/Accounting/AccountingSeriesStateProcessor.php b/src/State/Accounting/AccountingSeriesStateProcessor.php deleted file mode 100644 index cbd4ad1..0000000 --- a/src/State/Accounting/AccountingSeriesStateProcessor.php +++ /dev/null @@ -1,48 +0,0 @@ -autoMapper->map($data->accounting, Accounting::class); - - $points = $this->accountingService->calcBalanceSeries( - $accounting, - new \DatePeriod( - $data->start, - new \DateInterval(\sprintf('PT%s', $data->interval)), - $data->end, - \DatePeriod::INCLUDE_END_DATE - ) - ); - - /** @var AccountingSeriesApiResource */ - $series = $this->autoMapper->map($data, AccountingSeriesApiResource::class); - - $series->data = $points; - $series->length = \count($points); - - return $series; - } -}