Changeset View
Changeset View
Standalone View
Standalone View
src/app/Http/Controllers/API/V4/Admin/StatsController.php
<?php | <?php | ||||
namespace App\Http\Controllers\API\V4\Admin; | namespace App\Http\Controllers\API\V4\Admin; | ||||
use App\Providers\PaymentProvider; | use App\Providers\PaymentProvider; | ||||
use App\User; | use App\User; | ||||
use Carbon\Carbon; | use Carbon\Carbon; | ||||
use Illuminate\Http\Request; | use Illuminate\Http\Request; | ||||
use Illuminate\Support\Facades\Auth; | |||||
use Illuminate\Support\Facades\DB; | use Illuminate\Support\Facades\DB; | ||||
class StatsController extends \App\Http\Controllers\Controller | class StatsController extends \App\Http\Controllers\Controller | ||||
{ | { | ||||
public const COLOR_GREEN = '#48d368'; // '#28a745' | public const COLOR_GREEN = '#48d368'; // '#28a745' | ||||
public const COLOR_GREEN_DARK = '#19692c'; | public const COLOR_GREEN_DARK = '#19692c'; | ||||
public const COLOR_RED = '#e77681'; // '#dc3545' | public const COLOR_RED = '#e77681'; // '#dc3545' | ||||
public const COLOR_RED_DARK = '#a71d2a'; | public const COLOR_RED_DARK = '#a71d2a'; | ||||
▲ Show 20 Lines • Show All 105 Lines • ▼ Show 20 Lines | protected function chartIncome(): array | ||||
if ($weeks) { | if ($weeks) { | ||||
$start->subWeeks(1); | $start->subWeeks(1); | ||||
} | } | ||||
} | } | ||||
$labels = array_reverse($labels); | $labels = array_reverse($labels); | ||||
$start->startOfWeek(Carbon::MONDAY); | $start->startOfWeek(Carbon::MONDAY); | ||||
$payments = DB::table('payments') | // FIXME: We're using wallets.currency instead of payments.currency and payments.currency_amount | ||||
->selectRaw("date_format(updated_at, '%Y-%v') as period, sum(amount) as amount") | // as I believe this way we have more precise amounts for this use-case (and default currency) | ||||
$query = DB::table('payments') | |||||
->selectRaw("date_format(updated_at, '%Y-%v') as period, sum(amount) as amount, wallets.currency") | |||||
->join('wallets', 'wallets.id', '=', 'wallet_id') | |||||
->where('updated_at', '>=', $start->toDateString()) | ->where('updated_at', '>=', $start->toDateString()) | ||||
->where('status', PaymentProvider::STATUS_PAID) | ->where('status', PaymentProvider::STATUS_PAID) | ||||
->whereIn('type', [PaymentProvider::TYPE_ONEOFF, PaymentProvider::TYPE_RECURRING]) | ->whereIn('type', [PaymentProvider::TYPE_ONEOFF, PaymentProvider::TYPE_RECURRING]) | ||||
->groupByRaw('1'); | ->groupByRaw('period, wallets.currency'); | ||||
$addTenantScope = function ($builder, $tenantId) { | $addTenantScope = function ($builder, $tenantId) { | ||||
$where = '`wallet_id` IN (' | $where = sprintf( | ||||
. 'select `id` from `wallets` ' | '`wallets`.`user_id` IN (select `id` from `users` where `tenant_id` = %d)', | ||||
. 'join `users` on (`wallets`.`user_id` = `users`.`id`) ' | $tenantId | ||||
. 'where `payments`.`wallet_id` = `wallets`.`id` ' | ); | ||||
. 'and `users`.`tenant_id` = ' . intval($tenantId) | |||||
. ')'; | |||||
return $builder->whereRaw($where); | return $builder->whereRaw($where); | ||||
}; | }; | ||||
$payments = $this->applyTenantScope($payments, $addTenantScope) | $currency = $this->currency(); | ||||
->pluck('amount', 'period') | $payments = []; | ||||
->map(function ($amount) { | |||||
return $amount / 100; | $this->applyTenantScope($query, $addTenantScope) | ||||
->get() | |||||
->each(function ($record) use (&$payments, $currency) { | |||||
$amount = $record->amount; | |||||
if ($record->currency != $currency) { | |||||
$amount = intval(round($amount * \App\Utils::exchangeRate($record->currency, $currency))); | |||||
} | |||||
if (isset($payments[$record->period])) { | |||||
$payments[$record->period] += $amount / 100; | |||||
} else { | |||||
$payments[$record->period] = $amount / 100; | |||||
} | |||||
}); | }); | ||||
// TODO: exclude refunds/chargebacks | // TODO: exclude refunds/chargebacks | ||||
$empty = array_fill_keys($labels, 0); | $empty = array_fill_keys($labels, 0); | ||||
$payments = array_values(array_merge($empty, $payments->all())); | $payments = array_values(array_merge($empty, $payments)); | ||||
// $payments = [1000, 1200.25, 3000, 1897.50, 2000, 1900, 2134, 3330]; | // $payments = [1000, 1200.25, 3000, 1897.50, 2000, 1900, 2134, 3330]; | ||||
$avg = collect($payments)->slice(0, count($labels) - 1)->avg(); | $avg = collect($payments)->slice(0, count($labels) - 1)->avg(); | ||||
// See https://frappe.io/charts/docs for format/options description | // See https://frappe.io/charts/docs for format/options description | ||||
return [ | return [ | ||||
'title' => 'Income in CHF - last 8 weeks', | 'title' => "Income in {$currency} - last 8 weeks", | ||||
'type' => 'bar', | 'type' => 'bar', | ||||
'colors' => [self::COLOR_BLUE], | 'colors' => [self::COLOR_BLUE], | ||||
'axisOptions' => [ | 'axisOptions' => [ | ||||
'xIsSeries' => true, | 'xIsSeries' => true, | ||||
], | ], | ||||
'data' => [ | 'data' => [ | ||||
'labels' => $labels, | 'labels' => $labels, | ||||
'datasets' => [ | 'datasets' => [ | ||||
▲ Show 20 Lines • Show All 171 Lines • ▼ Show 20 Lines | class StatsController extends \App\Http\Controllers\Controller | ||||
* | * | ||||
* @param \Illuminate\Database\Query\Builder $query The query | * @param \Illuminate\Database\Query\Builder $query The query | ||||
* @param callable $addQuery Additional tenant-scope query-modifier | * @param callable $addQuery Additional tenant-scope query-modifier | ||||
* | * | ||||
* @return \Illuminate\Database\Query\Builder | * @return \Illuminate\Database\Query\Builder | ||||
*/ | */ | ||||
protected function applyTenantScope($query, $addQuery = null) | protected function applyTenantScope($query, $addQuery = null) | ||||
{ | { | ||||
$user = Auth::guard()->user(); | // TODO: Per-tenant stats for admins | ||||
mollekopf: This needs an implementation otherwise $addTenantScope above is never used, no? | |||||
Done Inline ActionsapplyTenantScope() is implemented in API/V4/Reseller/StatsController. Here it can be empty (until we implement tenant selector for admin stats). For now admins work without the tenant scope, resellers work in their tenant scope. machniak: applyTenantScope() is implemented in API/V4/Reseller/StatsController. Here it can be empty… | |||||
if ($user->role == 'reseller') { | return $query; | ||||
if ($addQuery) { | |||||
$query = $addQuery($query, \config('app.tenant_id')); | |||||
} else { | |||||
$query = $query->withEnvTenantContext(); | |||||
} | } | ||||
/** | |||||
* Get the currency for stats | |||||
* | |||||
* @return string Currency code | |||||
*/ | |||||
protected function currency() | |||||
{ | |||||
$user = $this->guard()->user(); | |||||
// For resellers return their wallet currency | |||||
if ($user->role == 'reseller') { | |||||
$currency = $user->wallet()->currency; | |||||
} | } | ||||
return $query; | // System currency for others | ||||
return \config('app.currency'); | |||||
Not Done Inline ActionsAre the resellers users not in the resellers currency? mollekopf: Are the resellers users not in the resellers currency? | |||||
Done Inline ActionsI'm not sure I get the question. Admin work in system currency. Resellers work in their tenant currency, which is their wallet currency. Note that right now it does not really matter, as we do not have Income chart implementation for resellers (yet). machniak: I'm not sure I get the question. Admin work in system currency. Resellers work in their tenant… | |||||
} | } | ||||
} | } |
This needs an implementation otherwise $addTenantScope above is never used, no?