diff --git a/src/app/Console/Commands/WalletTransactions.php b/src/app/Console/Commands/WalletTransactions.php --- a/src/app/Console/Commands/WalletTransactions.php +++ b/src/app/Console/Commands/WalletTransactions.php @@ -43,7 +43,7 @@ return 1; } - foreach ($wallet->transactions() as $transaction) { + foreach ($wallet->transactions()->orderBy('created_at')->get() as $transaction) { $this->info( sprintf( "%s: %s %s", diff --git a/src/app/Http/Controllers/API/V4/WalletsController.php b/src/app/Http/Controllers/API/V4/WalletsController.php --- a/src/app/Http/Controllers/API/V4/WalletsController.php +++ b/src/app/Http/Controllers/API/V4/WalletsController.php @@ -2,8 +2,11 @@ namespace App\Http\Controllers\API\V4; +use App\Transaction; +use App\Wallet; use App\Http\Controllers\Controller; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; /** * API\WalletsController @@ -90,4 +93,80 @@ { return $this->errorResponse(404); } + + /** + * Fetch wallet transactions. + * + * @param string $id Wallet identifier + * + * @return \Illuminate\Http\JsonResponse + */ + public function transactions($id) + { + $wallet = Wallet::find($id); + + // Only owner (or admin) has access to the wallet + if (!Auth::guard()->user()->canRead($wallet)) { + return $this->errorResponse(403); + } + + $pageSize = 10; + $page = intval(request()->input('page')) ?: 1; + $hasMore = false; + + if ($transaction = request()->input('transaction')) { + // Get sub-transactions for the specified transaction ID, first + // check access rights to the transaction's wallet + + $transaction = $wallet->transactions()->where('id', $transaction)->first(); + + if (!$transaction) { + return $this->errorResponse(404); + } + + $result = Transaction::where('transaction_id', $transaction->id)->get(); + } else { + // Get main transactions (paged) + $result = $wallet->transactions() + // FIXME: Do we know which (type of) transaction has sub-transactions + // without the sub-query? + ->selectRaw("*, (SELECT count(*) FROM transactions sub " + . "WHERE sub.transaction_id = transactions.id) AS cnt") + ->whereNull('transaction_id') + ->latest() + ->limit($pageSize + 1) + ->offset($pageSize * ($page - 1)) + ->get(); + + if (count($result) > $pageSize) { + $result->pop(); + $hasMore = true; + } + } + + $result = $result->map(function ($item) { + $amount = $item->amount; + + if (in_array($item->type, [Transaction::WALLET_PENALTY, Transaction::WALLET_DEBIT])) { + $amount *= -1; + } + + return [ + 'id' => $item->id, + 'createdAt' => $item->created_at->format('Y-m-d H:i'), + 'type' => $item->type, + 'description' => $item->shortDescription(), + 'amount' => $amount, + 'hasDetails' => !empty($item->cnt), + ]; + }); + + return response()->json([ + 'status' => 'success', + 'list' => $result, + 'count' => count($result), + 'hasMore' => $hasMore, + 'page' => $page, + ]); + } } diff --git a/src/app/Transaction.php b/src/app/Transaction.php --- a/src/app/Transaction.php +++ b/src/app/Transaction.php @@ -4,6 +4,18 @@ use Illuminate\Database\Eloquent\Model; +/** + * The eloquent definition of a Transaction. + * + * @property int $amount + * @property string $description + * @property string $id + * @property string $object_id + * @property string $object_type + * @property string $type + * @property string $transaction_id + * @property string $user_email + */ class Transaction extends Model { protected $fillable = [ @@ -100,6 +112,13 @@ return \trans("transactions.{$label}", $this->toArray()); } + public function shortDescription() + { + $label = $this->objectTypeToLabelString() . '-' . $this->{'type'} . '-short'; + + return \trans("transactions.{$label}", $this->toArray()); + } + public function wallet() { if ($this->object_type !== \App\Wallet::class) { diff --git a/src/app/User.php b/src/app/User.php --- a/src/app/User.php +++ b/src/app/User.php @@ -220,16 +220,12 @@ /** * Check if current user can read data of another object. * - * @param \App\User|\App\Domain $object A user|domain object + * @param \App\User|\App\Domain|\App\Wallet $object A user|domain|wallet object * * @return bool True if he can, False otherwise */ public function canRead($object): bool { - if (!method_exists($object, 'wallet')) { - return false; - } - if ($this->role == "admin") { return true; } @@ -238,6 +234,14 @@ return true; } + if ($object instanceof Wallet) { + return $object->user_id == $this->id || $object->controllers->contains($this); + } + + if (!method_exists($object, 'wallet')) { + return false; + } + $wallet = $object->wallet(); return $this->wallets->contains($wallet) || $this->accounts->contains($wallet); diff --git a/src/app/Wallet.php b/src/app/Wallet.php --- a/src/app/Wallet.php +++ b/src/app/Wallet.php @@ -131,8 +131,10 @@ // Prefer intl extension's number formatter if (class_exists('NumberFormatter')) { - $nf = new \NumberFormatter($locale, \NumberFormatter::DECIMAL); - return $nf->formatCurrency($amount, $this->currency); + $nf = new \NumberFormatter($locale, \NumberFormatter::CURRENCY); + $result = $nf->formatCurrency($amount, $this->currency); + // Replace non-breaking space + return str_replace("\xC2\xA0", " ", $result); } return sprintf('%.2f %s', $amount, $this->currency); @@ -313,7 +315,7 @@ /** * Retrieve the transactions against this wallet. * - * @return iterable \App\Transaction + * @return \Illuminate\Database\Eloquent\Builder Query builder */ public function transactions() { @@ -322,6 +324,6 @@ 'object_id' => $this->id, 'object_type' => \App\Wallet::class ] - )->orderBy('created_at')->get(); + ); } } diff --git a/src/phpstan.neon b/src/phpstan.neon --- a/src/phpstan.neon +++ b/src/phpstan.neon @@ -7,6 +7,9 @@ ignoreErrors: - '#Access to an undefined property Illuminate\\Contracts\\Auth\\Authenticatable#' - '#Access to an undefined property App\\Package::\$pivot#' + - '#Access to an undefined property Illuminate\\Database\\Eloquent\\Model::\$id#' + - '#Access to an undefined property Illuminate\\Database\\Eloquent\\Model::\$created_at#' + - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::toString()#' level: 4 paths: - app/ diff --git a/src/resources/lang/en/transactions.php b/src/resources/lang/en/transactions.php --- a/src/resources/lang/en/transactions.php +++ b/src/resources/lang/en/transactions.php @@ -8,5 +8,14 @@ 'wallet-award' => 'Bonus of :amount awarded to :wallet_description; :description', 'wallet-credit' => ':amount was added to the balance of :wallet_description', 'wallet-debit' => ':amount was deducted from the balance of :wallet_description', - 'wallet-penalty' => 'The balance of wallet :wallet_description was reduced by :amount; :description' + 'wallet-penalty' => 'The balance of wallet :wallet_description was reduced by :amount; :description', + + 'entitlement-created-short' => 'Added :sku_title for :object_email', + 'entitlement-billed-short' => 'Billed :sku_title for :object_email', + 'entitlement-deleted-short' => 'Deleted :sku_title for :object_email', + + 'wallet-award-short' => 'Bonus: :description', + 'wallet-credit-short' => 'Payment', + 'wallet-debit-short' => 'Deduction', + 'wallet-penalty-short' => 'Charge: :description', ]; diff --git a/src/resources/sass/app.scss b/src/resources/sass/app.scss --- a/src/resources/sass/app.scss +++ b/src/resources/sass/app.scss @@ -107,10 +107,10 @@ background-color: #f8f8f8; color: grey; text-align: center; - height: 8em; td { vertical-align: middle; + height: 8em; } tbody:not(:empty) + & { @@ -121,12 +121,17 @@ table { td.buttons, td.price, + td.datetime, td.selection { width: 1%; + white-space: nowrap; } + th.price, td.price { + width: 1%; text-align: right; + white-space: nowrap; } &.form-list { @@ -142,6 +147,20 @@ } } } + + .list-details { + min-height: 1em; + + ul { + margin: 0; + padding-left: 1.2em; + } + } + + .btn-action { + line-height: 1; + padding: 0; + } } #status-box { @@ -246,3 +265,7 @@ .btn-link { border: 0; } + +.table thead th { + border: 0; +} diff --git a/src/resources/vue/Wallet.vue b/src/resources/vue/Wallet.vue --- a/src/resources/vue/Wallet.vue +++ b/src/resources/vue/Wallet.vue @@ -12,6 +12,55 @@ + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
DateDescriptionAmount
{{ transaction.createdAt }} + + {{ transactionDescription(transaction) }}{{ transactionAmount(transaction) }}
There are no transactions for this account.
+
+ +
+
+
+
+
+