feature/RVRNEXT-43-split-transactions #59

Merged
bence merged 9 commits from feature/RVRNEXT-43-split-transactions into master 2023-06-17 14:59:09 +02:00
11 changed files with 216 additions and 20 deletions

View File

@ -10,7 +10,7 @@
} }
], ],
"require": { "require": {
"esoko/soko-web": "0.12.2", "esoko/soko-web": "0.13",
"firebase/php-jwt": "^6.4" "firebase/php-jwt": "^6.4"
}, },
"require-dev": { "require-dev": {

8
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "de44c3f047848705e8e3d6b460f73ff1", "content-hash": "707bad5bd796500db300c5384f2ce378",
"packages": [ "packages": [
{ {
"name": "cocur/slugify", "name": "cocur/slugify",
@ -82,11 +82,11 @@
}, },
{ {
"name": "esoko/soko-web", "name": "esoko/soko-web",
"version": "v0.12.2", "version": "v0.13",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://git.esoko.eu/esoko/soko-web.git", "url": "https://git.esoko.eu/esoko/soko-web.git",
"reference": "8d490e48aaeb7ad2843e402fa42ec266db52e809" "reference": "4283bc9bb15d17914393b4ba3463d83717487c53"
}, },
"require": { "require": {
"cocur/slugify": "^4.3", "cocur/slugify": "^4.3",
@ -108,7 +108,7 @@
"GNU GPL 3.0" "GNU GPL 3.0"
], ],
"description": "Lightweight web framework", "description": "Lightweight web framework",
"time": "2023-05-28T19:13:20+00:00" "time": "2023-06-17T12:32:56+00:00"
}, },
{ {
"name": "firebase/php-jwt", "name": "firebase/php-jwt",

View File

@ -0,0 +1,11 @@
CREATE TABLE `transaction_payees` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`transaction_id` int(10) unsigned NOT NULL,
`user_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_user_for_transaction` (`transaction_id`, `user_id`),
KEY `transaction_id` (`transaction_id`),
KEY `user_id` (`user_id`),
CONSTRAINT `transaction_payees_transaction_id` FOREIGN KEY (`transaction_id`) REFERENCES `transactions` (`id`),
CONSTRAINT `transaction_payees_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

View File

@ -6,11 +6,13 @@ use RVR\Finance\ExchangeRateCalculator;
use RVR\PersistentData\Model\Community; use RVR\PersistentData\Model\Community;
use RVR\PersistentData\Model\CommunityMember; use RVR\PersistentData\Model\CommunityMember;
use RVR\PersistentData\Model\Transaction; use RVR\PersistentData\Model\Transaction;
use RVR\PersistentData\Model\TransactionPayee;
use RVR\PersistentData\Model\User; use RVR\PersistentData\Model\User;
use RVR\Repository\CommunityMemberRepository; use RVR\Repository\CommunityMemberRepository;
use RVR\Repository\CommunityRepository; use RVR\Repository\CommunityRepository;
use RVR\Repository\CurrencyRepository; use RVR\Repository\CurrencyRepository;
use RVR\Repository\TransactionRepository; use RVR\Repository\TransactionRepository;
use RVR\Repository\TransactionPayeeRepository;
use RVR\Repository\EventRepository; use RVR\Repository\EventRepository;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired; use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Authorization\ISecured; use SokoWeb\Interfaces\Authorization\ISecured;
@ -28,6 +30,8 @@ class TransactionController implements IAuthenticationRequired, ISecured
private TransactionRepository $transactionRepository; private TransactionRepository $transactionRepository;
private TransactionPayeeRepository $transactionPayeeRepository;
private EventRepository $eventRepository; private EventRepository $eventRepository;
private ?Community $community; private ?Community $community;
@ -40,6 +44,7 @@ class TransactionController implements IAuthenticationRequired, ISecured
$this->communityMemberRepository = new CommunityMemberRepository(); $this->communityMemberRepository = new CommunityMemberRepository();
$this->currencyRepository = new CurrencyRepository(); $this->currencyRepository = new CurrencyRepository();
$this->transactionRepository = new TransactionRepository(); $this->transactionRepository = new TransactionRepository();
$this->transactionPayeeRepository = new TransactionPayeeRepository();
$this->eventRepository = new EventRepository(); $this->eventRepository = new EventRepository();
} }
@ -91,16 +96,19 @@ class TransactionController implements IAuthenticationRequired, ISecured
$currentPage, $currentPage,
$itemsPerPage, $itemsPerPage,
true, true,
['currency', 'payer_user', 'payee_user'] ['currency', 'payer_user']
) : ) :
$this->transactionRepository->getPagedByCommunity( $this->transactionRepository->getPagedByCommunity(
$this->community, $this->community,
$currentPage, $currentPage,
$itemsPerPage, $itemsPerPage,
true, true,
['event', 'currency', 'payer_user', 'payee_user'] ['event', 'currency', 'payer_user']
); );
$transactions = iterator_to_array($transactions);
Container::$persistentDataManager->loadMultiRelationsFromDb($transactions, 'payees', true, ['user']);
return new HtmlContent('communities/transactions', [ return new HtmlContent('communities/transactions', [
'community' => $this->community, 'community' => $this->community,
'event' => $event, 'event' => $event,
@ -108,7 +116,8 @@ class TransactionController implements IAuthenticationRequired, ISecured
'pages' => ceil($numberOfTransactions / $itemsPerPage), 'pages' => ceil($numberOfTransactions / $itemsPerPage),
'currentPage' => $currentPage, 'currentPage' => $currentPage,
'numberOfTransactions' => $numberOfTransactions, 'numberOfTransactions' => $numberOfTransactions,
'transactions' => $transactions 'transactions' => $transactions,
'members' => $this->getMembers($this->community)
]); ]);
} }
@ -122,6 +131,10 @@ class TransactionController implements IAuthenticationRequired, ISecured
} }
Container::$persistentDataManager->loadRelationsFromDb($transaction, false, ['event']); Container::$persistentDataManager->loadRelationsFromDb($transaction, false, ['event']);
$event = $transaction->getEvent(); $event = $transaction->getEvent();
$payeeUserIds = [];
foreach ($this->transactionPayeeRepository->getAllByTransaction($transaction) as $payee) {
$payeeUserIds[] = $payee->getUserId();
}
} else { } else {
$transaction = null; $transaction = null;
$eventSlug = Container::$request->query('event'); $eventSlug = Container::$request->query('event');
@ -130,11 +143,13 @@ class TransactionController implements IAuthenticationRequired, ISecured
} else { } else {
$event = null; $event = null;
} }
$payeeUserIds = [];
} }
return new HtmlContent('communities/transaction_edit', [ return new HtmlContent('communities/transaction_edit', [
'community' => $this->community, 'community' => $this->community,
'transaction' => $transaction, 'transaction' => $transaction,
'payeeUserIds' => $payeeUserIds,
'event' => $event, 'event' => $event,
'members' => $this->getMembers($this->community), 'members' => $this->getMembers($this->community),
'currencies' => $this->getCurrencies($this->community) 'currencies' => $this->getCurrencies($this->community)
@ -154,18 +169,48 @@ class TransactionController implements IAuthenticationRequired, ISecured
$transaction->setEventId(Container::$request->post('event_id') ?: null); $transaction->setEventId(Container::$request->post('event_id') ?: null);
$transaction->setCurrencyId(Container::$request->post('currency_id')); $transaction->setCurrencyId(Container::$request->post('currency_id'));
$transaction->setPayerUserId(Container::$request->post('payer_user_id')); $transaction->setPayerUserId(Container::$request->post('payer_user_id'));
$transaction->setPayeeUserId(Container::$request->post('payee_user_id') ?: null);
$transaction->setDescription(Container::$request->post('description')); $transaction->setDescription(Container::$request->post('description'));
$transaction->setSum(Container::$request->post('sum')); $transaction->setSum(Container::$request->post('sum'));
$transaction->setTimeDate(new DateTime(Container::$request->post('time'))); $transaction->setTimeDate(new DateTime(Container::$request->post('time')));
Container::$persistentDataManager->saveToDb($transaction); Container::$persistentDataManager->saveToDb($transaction);
$payeeUserIds = array_unique(Container::$request->post('payee_user_ids'));
if (count($payeeUserIds) === $this->communityMemberRepository->countAllByCommunity($this->community)) {
$payeeUserIds = [];
}
$currentPayees = [];
foreach ($payeeUserIds as $payeeUserId) {
$payee = new TransactionPayee();
$payee->setTransaction($transaction);
$payee->setUserId((int)$payeeUserId);
$currentPayees[(int)$payeeUserId] = $payee;
}
$existingPayees = [];
if ($transactionId) {
foreach ($this->transactionPayeeRepository->getAllByTransaction($transaction) as $payee) {
$existingPayees[$payee->getUserId()] = $payee;
}
}
foreach (array_diff_key($currentPayees, $existingPayees) as $newPayee) {
Container::$persistentDataManager->saveToDb($newPayee);
}
foreach (array_diff_key($existingPayees, $currentPayees) as $deletedPayee) {
Container::$persistentDataManager->deleteFromDb($deletedPayee);
}
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);
} }
public function deleteTransaction(): IContent public function deleteTransaction(): IContent
{ {
$transaction = $this->transactionRepository->getById(Container::$request->query('transactionId')); $transaction = $this->transactionRepository->getById(Container::$request->query('transactionId'));
foreach ($this->transactionPayeeRepository->getAllByTransaction($transaction) as $payee) {
Container::$persistentDataManager->deleteFromDb($payee);
}
Container::$persistentDataManager->deleteFromDb($transaction); Container::$persistentDataManager->deleteFromDb($transaction);
return new JsonContent(['success' => true]); return new JsonContent(['success' => true]);

View File

@ -1,5 +1,6 @@
<?php namespace RVR\Finance; <?php namespace RVR\Finance;
use Container;
use RVR\PersistentData\Model\Community; use RVR\PersistentData\Model\Community;
use RVR\Repository\CommunityMemberRepository; use RVR\Repository\CommunityMemberRepository;
use RVR\Repository\TransactionRepository; use RVR\Repository\TransactionRepository;
@ -56,13 +57,18 @@ class BalanceCalculator
private function sumTransactions(): void private function sumTransactions(): void
{ {
$membersCount = count($this->members); $membersCount = count($this->members);
$transactions = $this->transactionRepository->getAllByCommunity($this->community, true, ['currency']); $transactions = iterator_to_array($this->transactionRepository->getAllByCommunity($this->community, true, ['currency']));
Container::$persistentDataManager->loadMultiRelationsFromDb($transactions, 'payees');
foreach ($transactions as $transaction) { foreach ($transactions as $transaction) {
$sum = $this->exchangeRateCalculator->calculate($transaction->getSum(), $transaction->getCurrency(), $transaction->getTimeDate()); $sum = $this->exchangeRateCalculator->calculate($transaction->getSum(), $transaction->getCurrency(), $transaction->getTimeDate());
$payees = $transaction->getPayees();
$payeeCount = count($payees);
if ($transaction->getPayeeUserId()) { if ($payeeCount > 0) {
$this->payments[$transaction->getPayerUserId()][$transaction->getPayeeUserId()] += $sum; foreach ($payees as $payee) {
$this->payments[$transaction->getPayerUserId()][$payee->getUserId()] += $sum / $payeeCount;
}
} else { } else {
foreach ($this->members as $payeeUserId => $member) { foreach ($this->members as $payeeUserId => $member) {
$this->payments[$transaction->getPayerUserId()][$payeeUserId] += $sum / $membersCount; $this->payments[$transaction->getPayerUserId()][$payeeUserId] += $sum / $membersCount;

View File

@ -17,6 +17,10 @@ class Transaction extends Model
'payee_user' => User::class 'payee_user' => User::class
]; ];
protected static array $multiRelations = [
'payees' => [TransactionPayee::class, 'transaction']
];
private ?Community $community = null; private ?Community $community = null;
private int $communityId; private int $communityId;
@ -37,6 +41,8 @@ class Transaction extends Model
private ?int $payeeUserId = null; private ?int $payeeUserId = null;
private ?array $payees = null;
private string $description = ''; private string $description = '';
private float $sum = 0.0; private float $sum = 0.0;
@ -93,6 +99,11 @@ class Transaction extends Model
$this->payeeUserId = $payeeUserId; $this->payeeUserId = $payeeUserId;
} }
public function setPayees(array $payees): void
{
$this->payees = $payees;
}
public function setDescription(string $description): void public function setDescription(string $description): void
{ {
$this->description = $description; $this->description = $description;
@ -163,6 +174,11 @@ class Transaction extends Model
return $this->payeeUserId; return $this->payeeUserId;
} }
public function getPayees(): ?array
{
return $this->payees;
}
public function getDescription(): string public function getDescription(): string
{ {
return $this->description; return $this->description;

View File

@ -0,0 +1,60 @@
<?php namespace RVR\PersistentData\Model;
use SokoWeb\PersistentData\Model\Model;
class TransactionPayee extends Model
{
protected static string $table = 'transaction_payees';
protected static array $fields = ['transaction_id', 'user_id'];
protected static array $relations = ['transaction' => Transaction::class, 'user' => User::class];
private ?Transaction $transaction = null;
private ?int $transactionId = null;
private ?User $user = null;
private ?int $userId = null;
public function setTransaction(Transaction $transaction): void
{
$this->transaction = $transaction;
}
public function setTransactionId(int $transactionId): void
{
$this->transactionId = $transactionId;
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function getTransaction(): ?Transaction
{
return $this->transaction;
}
public function getTransactionId(): ?int
{
return $this->transactionId;
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
}

View File

@ -15,12 +15,16 @@ class CommunityMemberRepository
public function getAllByCommunity(Community $community, bool $useRelations = false, array $withRelations = []): Generator public function getAllByCommunity(Community $community, bool $useRelations = false, array $withRelations = []): Generator
{ {
$select = new Select(\Container::$dbConnection); $select = $this->selectAllByCommunity($community);
$select->where('community_id', '=', $community->getId());
yield from \Container::$persistentDataManager->selectMultipleFromDb($select, CommunityMember::class, $useRelations, $withRelations); yield from \Container::$persistentDataManager->selectMultipleFromDb($select, CommunityMember::class, $useRelations, $withRelations);
} }
public function countAllByCommunity(Community $community): int
{
return $this->selectAllByCommunity($community)->count();
}
public function getAllByUser(User $user, bool $useRelations = false, array $withRelations = []): Generator public function getAllByUser(User $user, bool $useRelations = false, array $withRelations = []): Generator
{ {
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
@ -37,4 +41,11 @@ class CommunityMemberRepository
return \Container::$persistentDataManager->selectFromDb($select, CommunityMember::class); return \Container::$persistentDataManager->selectFromDb($select, CommunityMember::class);
} }
private function selectAllByCommunity(Community $community): Select
{
$select = new Select(\Container::$dbConnection, CommunityMember::getTable());
$select->where('community_id', '=', $community->getId());
return $select;
}
} }

View File

@ -0,0 +1,23 @@
<?php namespace RVR\Repository;
use Container;
use Generator;
use RVR\PersistentData\Model\Transaction;
use RVR\PersistentData\Model\TransactionPayee;
use SokoWeb\Database\Query\Select;
class TransactionPayeeRepository
{
public function getById(int $id): ?TransactionPayee
{
return Container::$persistentDataManager->selectFromDbById($id, TransactionPayee::class);
}
public function getAllByTransaction(Transaction $transaction, bool $useRelations = false, array $withRelations = []): Generator
{
$select = new Select(Container::$dbConnection);
$select->where('transaction_id', '=', $transaction->getId());
yield from Container::$persistentDataManager->selectMultipleFromDb($select, TransactionPayee::class, $useRelations, $withRelations);
}
}

View File

@ -39,13 +39,15 @@
<option value="<?= $member->getUser()->getId() ?>" <?= isset($transaction) && $transaction->getPayerUserId() === $member->getUser()->getId() ? 'selected' : '' ?>><?= $member->getUser()->getDisplayName() ?></option> <option value="<?= $member->getUser()->getId() ?>" <?= isset($transaction) && $transaction->getPayerUserId() === $member->getUser()->getId() ? 'selected' : '' ?>><?= $member->getUser()->getDisplayName() ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<p class="formLabel marginTop">Payee</p> <p class="formLabel marginTop">Payee(s)</p>
<select class="big fullWidth" name="payee_user_id"> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); grid-gap: 10px;">
<option value="">[common]</option>
<?php foreach ($members as $member): ?> <?php foreach ($members as $member): ?>
<option value="<?= $member->getUser()->getId() ?>" <?= isset($transaction) && $transaction->getPayeeUserId() === $member->getUser()->getId() ? 'selected' : '' ?>><?= $member->getUser()->getDisplayName() ?></option> <div style="text-align: center;">
<input id="payee_<?= $member->getUserId() ?>" type="checkbox" name="payee_user_ids[]" value="<?= $member->getUserId() ?>" <?= !isset($transaction) || count($payeeUserIds) === 0 || in_array($member->getUserId(), $payeeUserIds) ? 'checked' : '' ?>><!--
--><label for="payee_<?= $member->getUserId() ?>"><?= $member->getUser()->getDisplayName() ?></label>
</div>
<?php endforeach; ?> <?php endforeach; ?>
</select> </div>
<p class="formLabel marginTop">Description</p> <p class="formLabel marginTop">Description</p>
<input type="text" class="text big fullWidth" name="description" value="<?= isset($transaction) ? $transaction->getDescription() : '' ?>" required> <input type="text" class="text big fullWidth" name="description" value="<?= isset($transaction) ? $transaction->getDescription() : '' ?>" required>
<p class="formLabel marginTop">Sum</p> <p class="formLabel marginTop">Sum</p>

View File

@ -30,7 +30,29 @@
<p><span class="label"><?= $transaction->getEvent()->getTitle() ?></span></p> <p><span class="label"><?= $transaction->getEvent()->getTitle() ?></span></p>
<?php endif; ?> <?php endif; ?>
<p style="font-weight: bold;"><?= $transaction->getDescription() ?></p> <p style="font-weight: bold;"><?= $transaction->getDescription() ?></p>
<p class="small"><?= $transaction->getPayerUser()->getDisplayName() ?> <i class="fa-solid fa-caret-right"></i> <?= $transaction->getPayeeUser() ? $transaction->getPayeeUser()->getDisplayName() : '[common]' ?></p> <p class="small">
<?= $transaction->getPayerUser()->getDisplayName() ?>
<i class="fa-solid fa-caret-right"></i>
<?php foreach ($members as $member): ?>
<?php
if (count($transaction->getPayees()) > 0) {
$found = false;
foreach ($transaction->getPayees() as $payee) {
if ($member->getUserId() === $payee->getUserId()) {
$found = true;
}
}
} else {
$found = true;
}
?>
<?php if ($found): ?>
<?= $member->getUser()->getDisplayName() ?>
<?php else: ?>
<span class="gray" style="text-decoration: line-through;"><?= $member->getUser()->getDisplayName() ?></span>
<?php endif; ?>
<?php endforeach; ?>
</p>
<p class="small"><?= $transaction->getTimeDate()->format('Y-m-d H:i') ?></p> <p class="small"><?= $transaction->getTimeDate()->format('Y-m-d H:i') ?></p>
</div> </div>
<div style="text-align: right;"> <div style="text-align: right;">