...
 
Commits (11)
<?php
namespace Minds\Controllers\Cli;
use Minds\Core;
use Minds\Core\Monetization\Partners\Manager;
use Minds\Cli;
use Minds\Interfaces;
use Minds\Exceptions;
use Minds\Entities;
class PartnerEarnings extends Cli\Controller implements Interfaces\CliControllerInterface
{
public function __construct()
{
}
public function help($command = null)
{
$this->out('TBD');
}
public function exec()
{
$this->out('Missing subcommand');
}
public function sync()
{
error_reporting(E_ALL);
ini_set('display_errors', 1);
$daysAgo = $this->getOpt('daysAgo') ?: 0;
$from = $this->getOpt('from') ?: strtotime("midnight $daysAgo days ago");
$manager = new Manager();
$i = 0;
foreach ($manager->issueDeposits([ 'from' => $from ]) as $record) {
$this->out(++$i);
}
}
}
......@@ -39,6 +39,11 @@ class config implements Interfaces\Api, Interfaces\ApiIgnorePam
"plus" => Minds\Core\Config::_()->get('plus'),
"report_reasons" => Minds\Core\Config::_()->get('report_reasons'),
"last_tos_update" => (Minds\Core\Config::_()->get('last_tos_update') ?: time()),
'handlers' => [
'plus' => Minds\Core\Di\Di::_()->get('Config')->get('plus')['handler'] ?? null,
'pro' => Minds\Core\Di\Di::_()->get('Config')->get('pro')['handler'] ?? null,
],
'upgrades' => Minds\Core\Di\Di::_()->get('Config')->get('upgrades'),
];
return Factory::response($minds);
......
<?php
namespace Minds\Controllers\api\v2\analytics;
use Minds\Api\Factory;
use Minds\Core;
use Minds\Core\Session;
use Minds\Core\Di\Di;
use Minds\Common\Cookie;
use Minds\Entities;
use Minds\Helpers\Counters;
use Minds\Interfaces;
class dashboards implements Interfaces\Api
{
public function get($pages)
{
$dashboardsManager = Di::_()->get('Analytics\Dashboards\Manager');
$id = $pages[0] ?? 'unknown';
$dashboard = $dashboardsManager->getDashboardById($id);
$dashboard->setUser(Session::getLoggedInUser());
if (isset($_GET['timespan'])) {
$dashboard->setTimespanId($_GET['timespan']);
}
if (isset($_GET['filter'])) {
$filterIds = explode(',', $_GET['filter']);
$dashboard->setFilterIds($filterIds);
}
if (isset($_GET['metric'])) {
$dashboard->setMetricId($_GET['metric']);
}
return Factory::response([
'dashboard' => $dashboard->export(),
]);
}
public function post($pages)
{
return Factory::response([]);
}
public function put($pages)
{
return Factory::response([]);
}
public function delete($pages)
{
return Factory::response([]);
}
}
......@@ -16,5 +16,9 @@ class AnalyticsProvider extends Provider
$this->di->bind('Analytics\Graphs\Repository', function ($di) {
return new Graphs\Repository();
}, ['useFactory' => true]);
$this->di->bind('Analytics\Dashboards\Manager', function ($di) {
return new Dashboards\Manager();
}, ['useFactory' => true]);
}
}
<?php
namespace Minds\Core\Analytics\Dashboards;
interface DashboardCollectionInterface
{
/**
* Export everything in the collection
* @param array $extras
* @return array
*/
public function export(array $extras = []): array;
}
<?php
namespace Minds\Core\Analytics\Dashboards;
interface DashboardInterface
{
/**
* Build the dashboard
* NOTE: return type not specified due to php
* having terrible typing support
* @return self
*/
public function build();
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array;
}
<?php
/**
* Earnings Dashboard
*/
namespace Minds\Core\Analytics\Dashboards;
use Minds\Entities\User;
use Minds\Traits\MagicAttributes;
/**
* @method EarningsDashboard setTimespanId(string $timespanId)
* @method EarningsDashboard setFilterIds(array $filtersIds)
* @method EarningsDashboard setUser(User $user)
*/
class EarningsDashboard implements DashboardInterface
{
use MagicAttributes;
/** @var string */
private $timespanId = '30d';
/** @var string[] */
private $filterIds = [ 'platform::browser' ];
/** @var string */
private $metricId = 'active_users';
/** @var Timespans\TimespansCollection */
private $timespansCollection;
/** @var Metrics\MetricsCollection */
private $metricsCollection;
/** @var Filters\FiltersCollection */
private $filtersCollection;
/** @var User */
private $user;
public function __construct(
$timespansCollection = null,
$metricsCollection = null,
$filtersCollection = null
) {
$this->timespansCollection = $timespansCollection ?? new Timespans\TimespansCollection();
$this->metricsCollection = $metricsCollection ?? new Metrics\MetricsCollection();
$this->filtersCollection = $filtersCollection ?? new Filters\FiltersCollection();
}
/**
* Build the dashboard
* @return self
*/
public function build(): self
{
$this->timespansCollection
->setSelectedId($this->timespanId)
->addTimespans(
new Timespans\TodayTimespan(),
new Timespans\_30dTimespan(),
new Timespans\_1yTimespan(),
new Timespans\MtdTimespan(),
new Timespans\YtdTimespan()
);
$this->filtersCollection
->setSelectedIds($this->filterIds)
->setUser($this->user)
->addFilters(
new Filters\ChannelFilter()
);
$this->metricsCollection
->setTimespansCollection($this->timespansCollection)
->setFiltersCollection($this->filtersCollection)
->setSelectedId($this->metricId)
->setUser($this->user)
->addMetrics(
new Metrics\Earnings\TotalEarningsMetric(),
new Metrics\Earnings\ViewsEarningsMetric(),
new Metrics\Earnings\ReferralsEarningsMetric(),
new Metrics\Earnings\SalesEarningsMetric()
)
->build();
return $this;
}
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
$this->build();
return [
'category' => 'earnings',
'label' => 'Pro Earnings',
'description' => 'Earnings for PRO members will be paid out within 30 days upon reaching a minumum balance of $100.00.',
'timespan' => $this->timespansCollection->getSelected()->getId(),
'timespans' => $this->timespansCollection->export(),
'metric' => $this->metricsCollection->getSelected()->getId(),
'metrics' => $this->metricsCollection->export(),
'filter' => $this->filtersCollection->getSelectedIds(),
'filters' => $this->filtersCollection->export(),
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Filters;
use Minds\Traits\MagicAttributes;
abstract class AbstractFilter
{
use MagicAttributes;
/** @var string */
protected $id;
/** @var string */
protected $label;
/** @var string */
protected $description;
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var FilterOptions */
protected $options;
/** @var string */
protected $selectedOption;
/**
* Set the selected option and toggle if selected
* @param string $selectedOptionId
* @return self
*/
public function setSelectedOption(string $selectedOptionId): self
{
$this->selectedOption = $selectedOptionId;
foreach ($this->options->getOptions() as $k => $option) {
if ($option->getId() === $selectedOptionId) {
$option->setSelected(true);
}
}
return $this;
}
/**
* Export
* @param array $extras
* @return array
*/
public function export($extras = []): array
{
return [
'id' => (string) $this->id,
'label' => (string) $this->label,
'description' => (string) $this->description,
'options' => (array) $this->options->export(),
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Filters;
class ChannelFilter extends AbstractFilter
{
/** @var string */
protected $id = "channel";
/** @var string */
protected $label = "Channel";
/** @var array */
protected $permissions = [ 'admin' ];
/** @var string */
protected $description = "Filter by channels or by the full site";
/** @var string */
protected $selectedOption = "all";
public function __construct()
{
$this->options = (new FilterOptions())
->setOptions(
(new FilterOptionsOption())
->setId("all")
->setLabel("All")
->setDescription("Global, site-wide metrics"),
(new FilterOptionsOption())
->setId("self")
->setLabel("Me")
->setDescription("Your currently logged in user"),
(new FilterOptionsOption())
->setId("custom")
->setLabel("Custom (Search)")
->setDescription("Search for a channel to view their metrics")
);
}
}
<?php
/**
*
*/
namespace Minds\Core\Analytics\Dashboards\Filters;
use Minds\Traits\MagicAttributes;
class FilterOptions
{
use MagicAttributes;
/** @var FilterOptionsOption[] */
private $options = [];
/**
* Set options
* @param FilterOptionsOption $options
* @return self
*/
public function setOptions(FilterOptionsOption ...$options): self
{
$this->options = $options;
return $this;
}
/**
* Export
* @param array $export
* @return array
*/
public function export(array $export = []): array
{
$options = [];
foreach ($this->options as $option) {
$options[] = $option->export();
}
return $options;
}
}
<?php
/**
*
*/
namespace Minds\Core\Analytics\Dashboards\Filters;
use Minds\Traits\MagicAttributes;
class FilterOptionsOption
{
use MagicAttributes;
/** @var string */
private $id;
/** @var string */
private $label;
/** @var string */
private $description;
/** @var bool */
private $available = true;
/** @var bool */
private $selected = false;
/**
* Export
* @param array $export
* @return array
*/
public function export(array $export = []): array
{
return [
'id' => $this->id,
'label' => $this->label,
'description' => $this->description,
'available' => (bool) $this->available,
'selected' => (bool) $this->selected,
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Filters;
use Minds\Entities\User;
use Minds\Core\Analytics\Dashboards\DashboardCollectionInterface;
class FiltersCollection implements DashboardCollectionInterface
{
/** @var AbstractFilter[] */
private $filters = [];
/** @var string[] */
private $selectedIds;
/** @var User */
private $user;
/**
* Set the selected metric id
* @param string[]
* @return self
*/
public function setSelectedIds(array $selectedIds): self
{
$this->selectedIds = $selectedIds;
return $this;
}
/**
* Selected ids
* @return string[]
*/
public function getSelectedIds(): array
{
return $this->selectedIds;
}
/**
* @param User $user
* @return self
*/
public function setUser(User $user): self
{
$this->user = $user;
return $this;
}
public function getSelected(): array
{
// Filters have scoped key pairs like
// key::value
// platform::browser
$selected = [];
foreach ($this->selectedIds as $selectedId) {
list($key, $value) = explode('::', $selectedId);
if (!isset($this->filters[$key])) {
continue;
}
$selected[$key] = $this->filters[$key];
$selected[$key]->setSelectedOption($value);
}
return $selected;
}
/**
* Set the filters
* @param AbstractFilter[] $filters
* @return self
*/
public function addFilters(AbstractFilter ...$filters): self
{
foreach ($filters as $filter) {
if (
in_array('admin', $filter->getPermissions(), true)
&& !$this->user->isAdmin()
&& !in_array('user', $filter->getPermissions(), true)
) {
continue;
}
$this->filters[$filter->getId()] = $filter;
}
return $this;
}
/**
* Return the set metrics
* @return AbstractFilter[]
*/
public function getFilters(): array
{
return $this->filters;
}
public function clear(): self
{
$this->filters = [];
$this->selectedIds = [];
return $this;
}
// public function build(): self
// {
// foreach ($this->filters as $filter) {
// $filter->build();
// }
// return $this;
// }
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
$export = [];
foreach ($this->filters as $filter) {
$export[] = $filter->export();
}
return $export;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Filters;
class PlatformFilter extends AbstractFilter
{
/** @var string */
protected $id = "platform";
/** @var string */
protected $label = "Platform";
/** @var string */
protected $description = "Filter by device types";
public function __construct()
{
$this->options = (new FilterOptions())
->setOptions(
(new FilterOptionsOption())
->setId("all")
->setLabel("All")
->setDescription("Browsers, Mobile and APIs"),
(new FilterOptionsOption())
->setId("browser")
->setLabel("Browser")
->setDescription("Browsers"),
(new FilterOptionsOption())
->setId("mobile")
->setLabel("Mobile")
->setDescription("Native mobile applications")
);
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Filters;
class ViewTypeFilter extends AbstractFilter
{
/** @var string */
protected $id = "view_type";
/** @var string */
protected $label = "View types";
/** @var string */
protected $description = "Filter by the breakdown of views";
public function __construct()
{
$this->options = (new FilterOptions())
->setOptions(
(new FilterOptionsOption())
->setId("total")
->setLabel("Total")
->setDescription("All views recorded on assets"),
(new FilterOptionsOption())
->setId("organic")
->setLabel("Organic")
->setDescription("Views on assets that excludes boosted impressions"),
(new FilterOptionsOption())
->setId("boosted")
->setLabel("Boosted")
->setDescription("Views recorded on assets that were boosted"),
(new FilterOptionsOption())
->setId("single")
->setLabel("Pageview")
->setDecription("Views recorded on single pages, not in feeds")
);
}
}
<?php
namespace Minds\Core\Analytics\Dashboards;
class Manager
{
const DASHBOARDS = [
'traffic' => TrafficDashboard::class,
'trending' => TrendingDashboard::class,
'earnings' => EarningsDashboard::class,
];
/**
* @param string $id
* @return DashboardInterface
*/
public function getDashboardById(string $id): DashboardInterface
{
$class = self::DASHBOARDS[$id];
return new $class;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Analytics\Dashboards\Timespans\TimespansCollection;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Entities\User;
use Minds\Traits\MagicAttributes;
/**
* @method AbstractMetric setTimespansCollection(TimespansCollection $timespansCollection)
* @method AbstractMetric setFiltersCollection(FiltersCollection $filtersCollection)
* @method string getId()
* @method string getLabel()
* @method MetricSummary getSummary()
* @method array getPermissions()
* @method AbstractMetric setUser(User $user)
*/
abstract class AbstractMetric
{
use MagicAttributes;
/** @var string */
protected $id;
/** @var string */
protected $label;
/** @var string */
protected $description;
/** @var string */
protected $unit = 'number';
/** @var string[] */
protected $permissions;
/** @var MetricSummary */
protected $summary;
/** @var VisualisationInterface */
protected $visualisation;
/** @var TimespansCollection */
protected $timespansCollection;
/** @var FiltersCollection */
protected $filtersCollection;
/** @var User */
protected $user;
/**
* Return the usd guid for metrics
* @return string
*/
protected function getUserGuid(): ?string
{
$filters = $this->filtersCollection->getSelected();
$channelFilter = $filters['channel'] ?? null;
if (!$channelFilter) {
if (!$this->user) {
throw new \Exception("You must be loggedin");
}
if ($this->user->isAdmin()) {
return "";
}
return $this->user->getGuid();
}
if ($channelFilter->getSelectedOption() === 'all') {
if ($this->user->isAdmin()) {
return "";
}
$channelFilter->setSelectedOption('self');
}
if ($channelFilter->getSelectedOption() === 'self') {
return $this->user->getGuid();
}
// TODO: check permissions first
return $channelFilter->getSelectedOption();
}
/**
* Export
* @param array $extras
* @return array
*/
public function export($extras = []): array
{
return [
'id' => (string) $this->id,
'label' => (string) $this->label,
'description' => (string) $this->description,
'unit' => (string) $this->unit,
'permissions' => (array) $this->permissions,
'summary' => $this->summary ? (array) $this->summary->export() : null,
'visualisation' => $this->visualisation ? (array) $this->visualisation->export() : null,
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Di\Di;
use Minds\Core\Data\ElasticSearch;
class ActiveUsersMetric extends AbstractMetric
{
/** @var ElasticSearch\Client */
private $es;
/** @var string */
protected $id = 'active_users';
/** @var string */
protected $label = 'Active Users';
/** @var string */
protected $description = 'Users who make at least one single request to Minds';
/** @var array */
protected $permissions = [ 'admin' ];
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
}
/**
* Build the metric summary
* @return self
*/
public function buildSummary(): self
{
if ($this->getUserGuid()) {
return $this;
}
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
$comparisonTsMs = strtotime("-{$timespan->getComparisonInterval()} days", $timespan->getFromTsMs() / 1000) * 1000;
$currentTsMs = $timespan->getFromTsMs();
// Field name to use for the aggregation
$aggField = "active::total";
// The aggregation type, this differs by resolution
$aggType = "sum";
// The resolution to use
$resolution = 'day';
switch ($timespan->getId()) {
case 'today':
$resolution = 'day';
$aggType = "sum";
break;
case '30d':
case 'mtd':
$resolution = 'month';
$aggType = "max";
break;
case '1y':
case 'ytd':
$resolution = 'month';
$aggType = "avg";
break;
}
$values = [];
foreach ([ 'value' => $currentTsMs, 'comparison' => $comparisonTsMs ] as $key => $tsMs) {
$must = [];
// Specify the resolution to avoid duplicates
$must[] = [
'term' => [
'resolution' => $resolution,
],
];
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
'lte' => strtotime("midnight +{$timespan->getComparisonInterval()} days", $tsMs / 1000) * 1000,
],
];
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
$aggType => [
'field' => $aggField,
],
],
],
],
];
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$values[$key] = $response['aggregations']['1']['value'];
}
$this->summary = new MetricSummary();
$this->summary->setValue($values['value'])
->setComparisonValue($values['comparison'])
->setComparisonInterval($timespan->getComparisonInterval());
return $this;
}
/**
* Build a visualisation for the metric
* @return self
*/
public function buildVisualisation(): self
{
if ($this->getUserGuid()) {
$this->visualisation = (new Visualisations\ChartVisualisation());
return $this;
}
$timespan = $this->timespansCollection->getSelected();
$xValues = [];
$yValues = [];
$must = [];
// Range must be from previous period
$must[]['range'] = [
'@timestamp' => [
'gte' => $timespan->getFromTsMs(),
],
];
// Use our global metrics
$must[]['term'] = [
'entity_urn' => 'urn:metric:global'
];
// Specify the resolution to avoid duplicates
$must[] = [
'term' => [
'resolution' => $timespan->getInterval(),
],
];
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'date_histogram' => [
'field' => '@timestamp',
'interval' => $timespan->getInterval(),
'min_doc_count' => 1,
],
'aggs' => [
'2' => [
'sum' => [
'field' => 'active::total',
],
],
],
],
],
],
];
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$buckets = [];
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
$date = date(Visualisations\ChartVisualisation::DATE_FORMAT, $bucket['key'] / 1000);
$xValues[] = $date;
$yValues[] = $bucket['2']['value'];
$buckets[] = [
'key' => $bucket['key'],
'date' => date('c', $bucket['key'] / 1000),
'value' => $bucket['2']['value']
];
}
$this->visualisation = (new Visualisations\ChartVisualisation())
->setXValues($xValues)
->setYValues($yValues)
->setXLabel('Date')
->setYLabel('Count')
->setBuckets($buckets);
return $this;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Earnings;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
abstract class AbstractEarningsMetric extends AbstractMetric
{
/** @var Elasticsearch\Client */
private $es;
/** @var string */
protected $id = '';
/** @var string */
protected $label = '';
/** @var string */
protected $description = '';
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $unit = 'usd';
/** @var string */
protected $aggField = '';
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
}
/**
* Build the metrics
* @return self
*/
public function buildSummary(): self
{
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
$comparisonTsMs = strtotime("midnight -{$timespan->getComparisonInterval()} days", $timespan->getFromTsMs() / 1000) * 1000;
$currentTsMs = $timespan->getFromTsMs();
$values = [];
foreach ([ 'value' => $currentTsMs, 'comparison' => $comparisonTsMs ] as $key => $tsMs) {
$must = [];
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
'lte' => strtotime("midnight +{$timespan->getComparisonInterval()} days", $tsMs / 1000) * 1000,
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'sum' => [
'field' => $this->aggField,
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$values[$key] = $response['aggregations']['1']['value'];
}
$this->summary = new MetricSummary();
$this->summary
->setValue($values['value'])
->setComparisonValue($values['comparison'])
->setComparisonInterval($timespan->getComparisonInterval())
->setComparisonPositivity(true);
return $this;
}
/**
* Build a visualisation for the metric
* @return self
*/
public function buildVisualisation(): self
{
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
$must = [];
// Range must be from previous period
$must[]['range'] = [
'@timestamp' => [
'gte' => $timespan->getFromTsMs(),
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'date_histogram' => [
'field' => '@timestamp',
'interval' => $timespan->getInterval(),
'min_doc_count' => 0,
'extended_bounds' => [
'min' => $timespan->getFromTsMs(),
'max' => time() * 1000,
],
],
'aggs' => [
'2' => [
'sum' => [
'field' => $this->aggField,
],
],
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$buckets = [];
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
$date = date(Visualisations\ChartVisualisation::DATE_FORMAT, $bucket['key'] / 1000);
$buckets[] = [
'key' => $bucket['key'],
'date' => date('c', $bucket['key'] / 1000),
'value' => $bucket['2']['value']
];
}
$this->visualisation = (new Visualisations\ChartVisualisation())
->setXLabel('Date')
->setYLabel('Count')
->setBuckets($buckets);
return $this;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Earnings;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
class ReferralsEarningsMetric extends AbstractEarningsMetric
{
/** @var string */
protected $id = 'earnings_referrals';
/** @var string */
protected $label = 'Referrals USD';
/** @var string */
protected $description = "Total earnings for your active referrals. You earn $0.10 for every active referral. A referral must log in at least 3 of 7 days after registration to be credited.";
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'usd_earnings::referrals';
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Earnings;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
class SalesEarningsMetric extends AbstractEarningsMetric
{
/** @var string */
protected $id = 'earnings_sales';
/** @var string */
protected $label = 'Sales USD';
/** @var string */
protected $description = "Total earnings for the sales you have referred. You earn a 25% commission when your referrals purchase Plus, Pro or Minds Tokens.";
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'usd_earnings::sales';
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Earnings;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
class TotalEarningsMetric extends AbstractEarningsMetric
{
/** @var string */
protected $id = 'earnings_total';
/** @var string */
protected $label = 'Total Earnings';
/** @var string */
protected $description = 'Total earnings for the selected timespan.';
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'usd_earnings::total';
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Earnings;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
class ViewsEarningsMetric extends AbstractEarningsMetric
{
/** @var string */
protected $id = 'earnings_views';
/** @var string */
protected $label = 'Pageviews USD';
/** @var string */
protected $description = "Total earnings for the pageviews on your channel's assets. You earn $1 for every 1,000 pageviews.";
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'usd_earnings::views';
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Traits\MagicAttributes;
/**
* @method MetricSummary setValue(int $value)
* @method MetricSummary setComparisonValue(int $value)
* @method MetricSummary setComparisonInterval(int $interval)
*/
class MetricSummary
{
use MagicAttributes;
/** @var int */
private $value = 0;
/** @var int */
private $comparisonValue = 0;
/** @var int */
private $comparisonInterval = 1;
/** @var bool */
private $comparisonPositivity = true;
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
return [
'current_value' => (int) $this->value,
'comparison_value' => (int) $this->comparisonValue,
'comparison_interval' => (int) $this->comparisonInterval,
'comparison_positive_inclination' => (bool) $this->comparisonPositivity,
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Traits\MagicAttributes;
class MetricTimeseries
{
/** @var array */
private $dateHistogram = [];
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
return [
'date_histogram' => (array) $this->dateHistogram,
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Entities\User;
use Minds\Core\Di\Di;
use Minds\Core\Analytics\Dashboards\Timespans\TimespansCollection;
use Minds\Core\Analytics\Dashboards\Filters\FiltersCollection;
use Minds\Core\Analytics\Dashboards\DashboardCollectionInterface;
class MetricsCollection implements DashboardCollectionInterface
{
/** @var AbstractMetric[] */
private $metrics = [];
/** @var string */
private $selectedId;
/** @var TimespansCollection */
private $timespansCollection;
/** @var FiltersCollection */
private $filtersCollection;
/** @var User */
private $user;
/**
* @param TimespansCollection $timespansCollection
* @return self
*/
public function setTimespansCollection(TimespansCollection $timespansCollection): self
{
$this->timespansCollection = $timespansCollection;
return $this;
}
/**
* @param FiltersCollection $filtersCollection
* @return self
*/
public function setFiltersCollection(?FiltersCollection $filtersCollection): self
{
$this->filtersCollection = $filtersCollection;
return $this;
}
/**
* @param User $user
* @return self
*/
public function setUser(User $user): self
{
$this->user = $user;
return $this;
}
/**
* Set the selected metric id
* @param string
* @return self
*/
public function setSelectedId(string $selectedId): self
{
$this->selectedId = $selectedId;
return $this;
}
/**
* Set the metrics
* @param AbstractMetric[] $metric
* @return self
*/
public function addMetrics(AbstractMetric ...$metrics): self
{
foreach ($metrics as $metric) {
if (
in_array('admin', $metric->getPermissions(), true)
&& !$this->user->isAdmin()
&& !in_array('user', $metric->getPermissions(), true)
) {
continue;
}
$metric->setUser($this->user);
$this->metrics[$metric->getId()] = $metric;
}
return $this;
}
/**
* Return the selected metric
* @return AbstractMetric
*/
public function getSelected(): AbstractMetric
{
if (!isset($this->metrics[$this->selectedId])) {
$this->selectedId = key($this->metrics);
}
return $this->metrics[$this->selectedId];
}
/**
* Return the set metrics
* @return AbstractMetric[]
*/
public function getMetrics(): array
{
return $this->metrics;
}
/**
* Build the metrics
* @return self
*/
public function build(): self
{
// Build all summaries
$this->buildSummaries();
// Build current visualisation
$this->getSelected()->buildVisualisation();
return $this;
}
public function buildSummaries(): self
{
foreach ($this->metrics as $metric) {
$metric
->setTimespansCollection($this->timespansCollection)
->setFiltersCollection($this->filtersCollection)
->buildSummary();
}
return $this;
}
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
$export = [];
foreach ($this->metrics as $metric) {
$export[] = $metric->export();
}
return $export;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
class PageviewsMetric extends AbstractMetric
{
/** @var Elasticsearch\Client */
private $es;
/** @var string */
protected $id = 'pageviews';
/** @var string */
protected $label = 'Pageviews';
/** @var string */
protected $description = "Total pageviews on all of your channel's assets. A pageview is registered when a unique page is viewed and does not include feeds.";
/** @var array */
protected $permissions = [ 'admin', 'user' ];
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
}
/**
* Build the metrics
* @return self
*/
public function buildSummary(): self
{
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
$comparisonTsMs = strtotime("midnight -{$timespan->getComparisonInterval()} days", $timespan->getFromTsMs() / 1000) * 1000;
$currentTsMs = $timespan->getFromTsMs();
// TODO: Allow this to be changed based on supplied filters
$aggField = "views::single";
$values = [];
foreach ([ 'value' => $currentTsMs, 'comparison' => $comparisonTsMs ] as $key => $tsMs) {
$must = [];
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
'lte' => strtotime("midnight +{$timespan->getComparisonInterval()} days", $tsMs / 1000) * 1000,
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'sum' => [
'field' => $aggField,
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$values[$key] = $response['aggregations']['1']['value'];
}
$this->summary = new MetricSummary();
$this->summary
->setValue($values['value'])
->setComparisonValue($values['comparison'])
->setComparisonInterval($timespan->getComparisonInterval())
->setComparisonPositivity(true);
return $this;
}
/**
* Build a visualisation for the metric
* @return self
*/
public function buildVisualisation(): self
{
$timespan = $this->timespansCollection->getSelected();
$this->filtersCollection->clear();
// TODO: make this respect the filters
$field = "views::single";
$must = [];
// Range must be from previous period
$must[]['range'] = [
'@timestamp' => [
'gte' => $timespan->getFromTsMs(),
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'date_histogram' => [
'field' => '@timestamp',
'interval' => $timespan->getInterval(),
'min_doc_count' => 0,
'extended_bounds' => [
'min' => $timespan->getFromTsMs(),
'max' => time() * 1000,
],
],
'aggs' => [
'2' => [
'sum' => [
'field' => $field,
],
],
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$buckets = [];
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
$date = date(Visualisations\ChartVisualisation::DATE_FORMAT, $bucket['key'] / 1000);
$buckets[] = [
'key' => $bucket['key'],
'date' => date('c', $bucket['key'] / 1000),
'value' => $bucket['2']['value']
];
}
$this->visualisation = (new Visualisations\ChartVisualisation())
->setXLabel('Date')
->setYLabel('Count')
->setBuckets($buckets);
return $this;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Di\Di;
use Minds\Core\Data\Elasticsearch;
class SignupsMetric extends AbstractMetric
{
/** @var Elasticsearch\Client */
private $es;
/** @var string */
protected $id = 'signups';
/** @var string */
protected $label = 'Signups';
/** @var string */
protected $description = 'New accounts registered';
/** @var array */
protected $permissions = [ 'admin' ];
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
}
/**
* Build the metrics
* @return self
*/
public function buildSummary(): self
{
if ($this->getUserGuid()) {
return $this;
}
$timespan = $this->timespansCollection->getSelected();
$comparisonTsMs = strtotime("-{$timespan->getComparisonInterval()} days", $timespan->getFromTsMs() / 1000) * 1000;
$currentTsMs = $timespan->getFromTsMs();
$aggField = "signups::total";
$values = [];
foreach ([ 'value' => $currentTsMs, 'comparison' => $comparisonTsMs ] as $key => $tsMs) {
$must = [];
// Return our global metrics
$must[]['term'] = [
'entity_urn' => 'urn:metric:global',
];
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
'lte' => strtotime("midnight +{$timespan->getComparisonInterval()} days", $tsMs / 1000) * 1000,
],
];
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'sum' => [
'field' => $aggField,
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$values[$key] = $response['aggregations']['1']['value'];
}
$this->summary = new MetricSummary();
$this->summary
->setValue($values['value'])
->setComparisonValue($values['comparison'])
->setComparisonInterval($timespan->getComparisonInterval());
return $this;
}
/**
* Build a visualisation for the metric
* @return self
*/
public function buildVisualisation(): self
{
if ($this->getUserGuid()) {
$this->visualisation = (new Visualisations\ChartVisualisation());
return $this;
}
$timespan = $this->timespansCollection->getSelected();
$xValues = [];
$yValues = [];
// TODO: make this respect the filters
$field = "signups::total";
$must = [];
// Range must be from previous period
$must[]['range'] = [
'@timestamp' => [
'gte' => $timespan->getFromTsMs(),
],
];
// Use our global metrics
$must[]['term'] = [
'entity_urn' => 'urn:metric:global'
];
// Specify the resolution to avoid duplicates
/*$must[] = [
'term' => [
'resolution' => $timespan->getInterval(),
],
];*/
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'date_histogram' => [
'field' => '@timestamp',
'interval' => $timespan->getInterval(),
'min_doc_count' => 1,
],
'aggs' => [
'2' => [
'sum' => [
'field' => $field,
],
],
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$buckets = [];
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
$date = date(Visualisations\ChartVisualisation::DATE_FORMAT, $bucket['key'] / 1000);
$xValues[] = $date;
$yValues[] = $bucket['2']['value'];
$buckets[] = [
'key' => $bucket['key'],
'date' => date('c', $bucket['key'] / 1000),
'value' => $bucket['2']['value']
];
}
$this->visualisation = (new Visualisations\ChartVisualisation())
->setXValues($xValues)
->setYValues($yValues)
->setXLabel('Date')
->setYLabel('Count')
->setBuckets($buckets);
return $this;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
class ViewsMetric extends AbstractMetric
{
/** @var Elasticsearch\Client */
private $es;
/** @var string */
protected $id = 'views';
/** @var string */
protected $label = 'Impressions';
/** @var string */
protected $description = "Impressions on all of your channel's assets. An impression is registered when your content is displayed and includes feeds.";
/** @var array */
protected $permissions = [ 'admin', 'user' ];
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
}
/**
* Build the metrics
* @return self
*/
public function buildSummary(): self
{
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
$comparisonTsMs = strtotime("midnight -{$timespan->getComparisonInterval()} days", $timespan->getFromTsMs() / 1000) * 1000;
$currentTsMs = $timespan->getFromTsMs();
// TODO: Allow this to be changed based on supplied filters
$aggField = "views::total";
if ($filters['view_type'] ?? false) {
$aggField = "views::" . $filters['view_type']->getSelectedOption();
}
$values = [];
foreach ([ 'value' => $currentTsMs, 'comparison' => $comparisonTsMs ] as $key => $tsMs) {
$must = [];
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
'lte' => strtotime("midnight +{$timespan->getComparisonInterval()} days", $tsMs / 1000) * 1000,
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'sum' => [
'field' => $aggField,
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$values[$key] = $response['aggregations']['1']['value'];
}
$this->summary = new MetricSummary();
$this->summary
->setValue($values['value'])
->setComparisonValue($values['comparison'])
->setComparisonInterval($timespan->getComparisonInterval())
->setComparisonPositivity(true);
return $this;
}
/**
* Build a visualisation for the metric
* @return self
*/
public function buildVisualisation(): self
{
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
$xValues = [];
$yValues = [];
// TODO: make this respect the filters
$field = "views::total";
if ($filters['view_type'] ?? false) {
$field = "views::" . $filters['view_type']->getSelectedOption();
}
$must = [];
// Range must be from previous period
$must[]['range'] = [
'@timestamp' => [
'gte' => $timespan->getFromTsMs(),
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'date_histogram' => [
'field' => '@timestamp',
'interval' => $timespan->getInterval(),
'min_doc_count' => 0,
'extended_bounds' => [
'min' => $timespan->getFromTsMs(),
'max' => time() * 1000,
],
],
'aggs' => [
'2' => [
'sum' => [
'field' => $field,
//'min_doc_count' => 0,
],
],
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$buckets = [];
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
$date = date(Visualisations\ChartVisualisation::DATE_FORMAT, $bucket['key'] / 1000);
$buckets[] = [
'key' => $bucket['key'],
'date' => date('c', $bucket['key'] / 1000),
'value' => $bucket['2']['value']
];
}
$this->visualisation = (new Visualisations\ChartVisualisation())
->setXLabel('Date')
->setYLabel('Count')
->setBuckets($buckets);
return $this;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Di\Di;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Entities\Resolver;
use Minds\Common\Urn;
class ViewsTableMetric extends AbstractMetric
{
/** @var Elasticsearch\Client */
private $es;
/** @var Resolver */
private $entitiesResolver;
/** @var string */
protected $id = 'views_table';
/** @var string */
protected $label = 'Views breakdown';
/** @var string */
protected $description = 'Views by post';
/** @var array */
protected $permissions = [ 'user', 'admin' ];
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
$this->entitiesResolver = $entitiesResolver ?? new Resolver();
}
/**
* Build the metrics
* @return self
*/
public function buildSummary(): self
{
$this->summary = new MetricSummary();
$this->summary
->setValue(0)
->setComparisonValue(0)
->setComparisonInterval(null);
return $this;
}
/**
* Build a visualisation for the metric
* @return self
*/
public function buildVisualisation(): self
{
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
// TODO: make this respect the filters
$field = "views::total";
if ($filters['view_type']) {
$field = "views::" . $filters['view_type']->getSelectedOption();
}
$must = [];
// Range must be from previous period
$must[]['range'] = [
'@timestamp' => [
'gte' => $timespan->getFromTsMs(),
],
];
// Specify the resolution to avoid duplicates
// $must[] = [
// 'term' => [
// 'resolution' => $timespan->getInterval(),
// ],
// ];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'terms' => [
'field' => 'entity_urn',
'min_doc_count' => 1,
'order' => [
$field => 'desc',
],
],
'aggs' => [
'views::total' => [
'sum' => [
'field' => 'views::total',
],
],
'views::organic' => [
'sum' => [
'field' => 'views::organic',
],
],
'views::single' => [
'sum' => [
'field' => 'views::single',
],
],
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$buckets = [];
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
$entity = $this->entitiesResolver->single(new Urn($bucket['key']));
$buckets[] = [
'key' => $bucket['key'],
'values' => [
'entity' => $entity ? $entity->export() : null,
'views::total' => $bucket['views::total']['value'],
'views::organic' => $bucket['views::organic']['value'],
'views::single' => $bucket['views::single']['value'],
],
];
}
$this->visualisation = (new Visualisations\TableVisualisation())
->setBuckets($buckets)
->setColumns([
[
'id' => 'entity',
'label' => '',
'order' => 0,
],
[
'id' => 'views::total',
'label' => 'Total Views',
'order' => 1,
],
[
'id' => 'views::organic',
'label' => 'Organic',
'order' => 2,
],
[
'id' => 'views::single',
'label' => 'Pageviews',
'order' => 3,
]
]);
return $this;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
use Minds\Traits\MagicAttributes;
abstract class AbstractVisualisation implements VisualisationInterface
{
use MagicAttributes;
/** @var string */
private $type;
public function export(array $extras = []): array
{
return [
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
use Minds\Traits\MagicAttributes;
class ChartVisualisation extends AbstractVisualisation
{
use MagicAttributes;
const DATE_FORMAT = "d-m-Y";
/** @var string */
private $type = 'chart';
/** @var string */
private $xLabel = 'Date';
/** @var array */
private $xValues;
/** @var string */
private $yLabel = 'count';
/** @var array */
private $yValues;
/** @var array */
private $buckets = [];
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
return [
'type' => $this->type,
'segments' => [
[
'buckets' => (array) $this->buckets,
],
]
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
use Minds\Traits\MagicAttributes;
class TableVisualisation extends AbstractVisualisation
{
use MagicAttributes;
const DATE_FORMAT = "d-m-Y";
/** @var string */
private $type = 'table';
/** @var array */
private $buckets;
/** @var array */
private $columns;
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
return [
'type' => $this->type,
'buckets' => (array) $this->buckets,
'columns' => (array) $this->columns,
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
interface VisualisationInterface
{
}
<?php
namespace Minds\Core\Analytics\Dashboards\Timespans;
use Minds\Traits\MagicAttributes;
/**
* @method string getId()
* @method string getLabel()
* @method string getInterval()
* @method int getComparisonInterval()
* @method int getFromTsMs()
*/
abstract class AbstractTimespan
{
use MagicAttributes;
/** @var string */
protected $id;
/** @var string */
protected $label;
/** @var string */
protected $interval;
/** @var bool */
protected $selected = false;
/** @var int */
protected $fromTsMs;
/** @var string */
protected $comparisonInterval = 'day';
/**
* Export
* @param array $extras
* @return array
*/
public function export($extras = []): array
{
return [
'id' => (string) $this->id,
'label' => (string) $this->label,
'interval' => (string) $this->interval,
'selected' => (bool) $this->selected,
'comparison_interval' => (int) $this->comparisonInterval,
'from_ts_ms' => (int) $this->fromTsMs,
'from_ts_iso' => date('c', $this->fromTsMs / 1000),
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Timespans;
class MtdTimespan extends AbstractTimespan
{
/** @var string */
protected $id = 'mtd';
/** @var string */
protected $label = 'Month to date';
/** @var string */
protected $interval = 'day';
/** @var int */
protected $fromTsMs;
/** @var int */
protected $comparisonInterval = 28;
public function __construct()
{
$this->fromTsMs = strtotime('midnight first day of this month') * 1000;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Timespans;
use Minds\Core\Analytics\Dashboards\DashboardCollectionInterface;
class TimespansCollection implements DashboardCollectionInterface
{
/** @var AbstractTimespan[] */
private $timespans = [];
/** @var string */
private $selectedId;
/**
* Set the current timespan we are using
* @param string $selectedId
* @return self
*/
public function setSelectedId(string $selectedId): self
{
$this->selectedId = $selectedId;
return $this;
}
/**
* Return the selected timespan
* @return AbstractTimespan
*/
public function getSelected(): AbstractTimespan
{
return $this->timespans[$this->selectedId];
}
/**
* Set the timespans
* @param AbstractTimespan[] $timespans
* @return self
*/
public function addTimespans(AbstractTimespan ...$timespans): self
{
foreach ($timespans as $timespan) {
$this->timespans[$timespan->getId()] = $timespan;
}
return $this;
}
/**
* Return the set timestamps
* @return TimestampAbstract[]
*/
public function getTimespans(): array
{
return $this->timespans;
}
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
$export = [];
foreach ($this->timespans as $timespan) {
if ($timespan->getId() === $this->selectedId) {
$timespan->setSelected(true);
}
$export[] = $timespan->export();
}
return $export;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Timespans;
/**
* @method string getId()
* @method string getLabel()
* @method string getInterval()
*/
class TodayTimespan extends AbstractTimespan
{
/** @var string */
protected $id = 'today';
/** @var string */
protected $label = 'Today';
/** @var string */
protected $interval = 'day';
/** @var int */
protected $fromTsMs;
/** @var int */
protected $comparisonInterval = 1;
public function __construct()
{
$this->fromTsMs = strtotime('midnight') * 1000;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Timespans;
class YtdTimespan extends AbstractTimespan
{
/** @var string */
protected $id = 'ytd';
/** @var string */
protected $label = 'Year to date';
/** @var string */
protected $interval = 'month';
/** @var int */
protected $fromTsMs;
/** @var string */
protected $comparisonInterval = 365;
public function __construct()
{
$this->fromTsMs = strtotime('midnight first day of January') * 1000;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Timespans;
class _1yTimespan extends AbstractTimespan
{
/** @var string */
protected $id = '1y';
/** @var string */
protected $label = 'Last 12 months';
/** @var string */
protected $interval = 'month';
/** @var int */
protected $fromTsMs;
/** @var int */
protected $comparisonInterval = 365;
public function __construct()
{
$this->fromTsMs = strtotime('midnight 365 days ago') * 1000;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Timespans;
class _30dTimespan extends AbstractTimespan
{
/** @var string */
protected $id = '30d';
/** @var string */
protected $label = 'Last 30 days';
/** @var string */
protected $interval = 'day';
/** @var int */
protected $fromTsMs;
/** @var int */
protected $comparisonInterval = 28;
public function __construct()
{
$this->fromTsMs = strtotime('midnight 30 days ago') * 1000;
}
}
<?php
/**
* Traffic Dashboard
*/
namespace Minds\Core\Analytics\Dashboards;
use Minds\Entities\User;
use Minds\Traits\MagicAttributes;
/**
* @method TrafficDashboard setTimespanId(string $timespanId)
* @method TrafficDashboard setFilterIds(array $filtersIds)
* @method TrafficDashboard setUser(User $user)
*/
class TrafficDashboard implements DashboardInterface
{
use MagicAttributes;
/** @var string */
private $timespanId = '30d';
/** @var string[] */
private $filterIds = [ 'platform::browser' ];
/** @var string */
private $metricId = 'active_users';
/** @var Timespans\TimespansCollection */
private $timespansCollection;
/** @var Metrics\MetricsCollection */
private $metricsCollection;
/** @var Filters\FiltersCollection */
private $filtersCollection;
/** @var User */
private $user;
public function __construct(
$timespansCollection = null,
$metricsCollection = null,
$filtersCollection = null
) {
$this->timespansCollection = $timespansCollection ?? new Timespans\TimespansCollection();
$this->metricsCollection = $metricsCollection ?? new Metrics\MetricsCollection();
$this->filtersCollection = $filtersCollection ?? new Filters\FiltersCollection();
}
/**
* Build the dashboard
* @return self
*/
public function build(): self
{
$this->timespansCollection
->setSelectedId($this->timespanId)
->addTimespans(
new Timespans\TodayTimespan(),
new Timespans\_30dTimespan(),
new Timespans\_1yTimespan(),
new Timespans\MtdTimespan(),
new Timespans\YtdTimespan()
);
$this->filtersCollection
->setSelectedIds($this->filterIds)
->setUser($this->user)
->addFilters(
// new Filters\PlatformFilter(),
new Filters\ViewTypeFilter(),
new Filters\ChannelFilter()
);
$this->metricsCollection
->setTimespansCollection($this->timespansCollection)
->setFiltersCollection($this->filtersCollection)
->setSelectedId($this->metricId)
->setUser($this->user)
->addMetrics(
new Metrics\ActiveUsersMetric(),
new Metrics\SignupsMetric(),
new Metrics\PageviewsMetric(),
new Metrics\ViewsMetric()
)
->build();
return $this;
}
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
$this->build();
return [
'category' => 'traffic',
'label' => 'Traffic',
'description' => null,
'timespan' => $this->timespansCollection->getSelected()->getId(),
'timespans' => $this->timespansCollection->export(),
'metric' => $this->metricsCollection->getSelected()->getId(),
'metrics' => $this->metricsCollection->export(),
'filter' => $this->filtersCollection->getSelectedIds(),
'filters' => $this->filtersCollection->export(),
];
}
}
<?php
/**
* Trending Dashboard
*/
namespace Minds\Core\Analytics\Dashboards;
use Minds\Entities\User;
use Minds\Traits\MagicAttributes;
/**
* @method TrendingDashboard setTimespanId(string $timespanId)
* @method TrendingDashboard setFilterIds(array $filtersIds)
* @method TrendingDashboard setUser(User $user)
*/
class TrendingDashboard implements DashboardInterface
{
use MagicAttributes;
/** @var string */
private $timespanId = '30d';
/** @var string[] */
private $filterIds = [ 'platform::browser' ];
/** @var string */
private $metricId = 'views_table';
/** @var Timespans\TimespansCollection */
private $timespansCollection;
/** @var Metrics\MetricsCollection */
private $metricsCollection;
/** @var Filters\FiltersCollection */
private $filtersCollection;
/** @var User */
private $user;
public function __construct(
$timespansCollection = null,
$metricsCollection = null,
$filtersCollection = null
) {
$this->timespansCollection = $timespansCollection ?? new Timespans\TimespansCollection();
$this->metricsCollection = $metricsCollection ?? new Metrics\MetricsCollection();
$this->filtersCollection = $filtersCollection ?? new Filters\FiltersCollection();
}
/**
* Build the dashboard
* @return self
*/
public function build(): self
{
$this->timespansCollection
->setSelectedId($this->timespanId)
->addTimespans(
new Timespans\TodayTimespan(),
new Timespans\_30dTimespan(),
new Timespans\_1yTimespan(),
new Timespans\MtdTimespan(),
new Timespans\YtdTimespan()
);
$this->filtersCollection
->setSelectedIds($this->filterIds)
->setUser($this->user)
->addFilters(
// new Filters\PlatformFilter(),
new Filters\ViewTypeFilter(),
new Filters\ChannelFilter()
);
$this->metricsCollection
->setTimespansCollection($this->timespansCollection)
->setFiltersCollection($this->filtersCollection)
->setSelectedId($this->metricId)
->setUser($this->user)
->addMetrics(
new Metrics\ViewsTableMetric()
)
->build();
return $this;
}
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
$this->build();
return [
'category' => 'trending',
'label' => 'Trending',
'description' => null,
'timespan' => $this->timespansCollection->getSelected()->getId(),
'timespans' => $this->timespansCollection->export(),
'metric' => $this->metricsCollection->getSelected()->getId(),
'metrics' => $this->metricsCollection->export(),
'filter' => $this->filtersCollection->getSelectedIds(),
'filters' => $this->filtersCollection->export(),
];
}
}
......@@ -60,4 +60,12 @@ class EntityCentricRecord
$this->sums[$metric] = $this->sums[$metric] + $value;
return $this;
}
/**
* @return string
*/
public function getUrn(): string
{
return (string) implode('-', [ $this->getEntityUrn(), $this->getResolution(), $this->getTimestamp() ]);
}
}
......@@ -13,6 +13,7 @@ class Manager
{
/** @var array */
const SYNCHRONISERS = [
PartnerEarningsSynchroniser::class,
SignupsSynchroniser::class,
ActiveUsersSynchroniser::class,
ViewsSynchroniser::class,
......@@ -21,6 +22,9 @@ class Manager
/** @var Repository */
protected $repository;
/** @var Sums */
protected $sums;
/** @var int */
private $from;
......@@ -28,9 +32,11 @@ class Manager
private $to;
public function __construct(
$repository = null
$repository = null,
$sums = null
) {
$this->repository = $repository ?: new Repository();
$this->repository = $repository ?? new Repository();
$this->sums = $sums ?? new Sums();
}
/**
......@@ -81,4 +87,13 @@ class Manager
public function getAggregateByQuery(array $query): array
{
}
/**
* @param array $opts
* @retun iterable
*/
public function getListAggregatedByOwner(array $opts = []): iterable
{
return $this->sums->getByOwner($opts);
}
}
<?php
namespace Minds\Core\Analytics\EntityCentric;
use Minds\Core\Monetization\Partners\Manager as PartnersManager;
use DateTime;
use Exception;
class PartnerEarningsSynchroniser
{
/** @var PartnersManager */
private $partnersManager;
public function __construct($partnersManager = null)
{
$this->partnersManager = $partnersManager ?? new PartnersManager;
}
/**
* @param int $from
* @return self
*/
public function setFrom($from): self
{
$this->from = $from;
return $this;
}
/**
* Convert to records
* @return iterable
*/
public function toRecords(): iterable
{
$opts = [];
$opts['from'] = $this->from;
$records = [];
$i = 0;
while (true) {
$result = $this->partnersManager->getList($opts);
$opts['offset'] = $result->getPagingToken();
foreach ($result as $deposit) {
$urn = "urn:user:{$deposit->getUserGuid()}";
$record = new EntityCentricRecord();
$record->setEntityUrn($urn)
->setOwnerGuid($deposit->getUserGuid())
->setTimestamp($deposit->getTimestamp()) // TODO: confirm if this should be rounded to midnight
->setResolution('day');
// In order to increment sums, replace with what has already been seen
if (isset($records[$record->getUrn()])) {
$record = $records[$record->getUrn()];
}
$record->incrementSum('usd_earnings::total', $deposit->getAmountCents());
$record->incrementSum("usd_earnings::{$deposit->getItem()}", $deposit->getAmountCents());
$records[$record->getUrn()] = $record;
}
if ($result->isLastPage()) {
break;
}
}
foreach ($records as $record) {
var_dump($record);
yield $record;
}
}
}
<?php
/**
* EntityCentric Sums
* @author Mark
*/
namespace Minds\Core\Analytics\EntityCentric;
use DateTime;
use DateTimeZone;
use Exception;
use Minds\Common\Repository\Response;
use Minds\Core\Data\ElasticSearch\Client as ElasticClient;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Di\Di;
class Sums
{
/** @var ElasticClient */
protected $es;
/**
* Repository constructor.
* @param ElasticClient $es
*/
public function __construct(
$es = null
) {
$this->es = $es ?: Di::_()->get('Database\ElasticSearch');
}
public function getByOwner(array $opts = []): iterable
{
$opts = array_merge([
'fields' => [],
'from' => time(),
], $opts);
$must = [];
$must[] = [
'range' => [
'@timestamp' => [
'gte' => $opts['from'] * 1000,
'lt' => strtotime('+1 day', $opts['from']) * 1000,
],
],
];
$termsAgg = [];
foreach ($opts['fields'] as $field) {
$termsAgg[$field] = [
'sum' => [
'field' => $field,
],
];
$must[] = [
'exists' => [
'field' => $field,
],
];
}
$partition = 0;
$partitions = 100;
$partitionSize = 5000; // Allows for 500,000 users
while (++$partition < $partitions) {
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'terms' => [
'field' => 'owner_guid',
'min_doc_count' => 1,
'size' => $partitionSize,
'include' => [
'partition' => $partition,
'num_partitions' => $partitions,
],
],
'aggs' => $termsAgg,
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
yield $bucket;
}
}
}
}
......@@ -49,7 +49,7 @@ class Retention implements AnalyticsMetric
$startTs = $timestamps[$x+1];
$signups = [];
$offset = "";
echo "\n Gathering signups \n";
// echo "\n Gathering signups \n";
while (true) {
$data = $this->db->getRow("analytics:signup:day:$startTs", ['limit'=>200, 'offset' => $offset]);
if (count($data) <= 1) {
......@@ -61,13 +61,13 @@ class Retention implements AnalyticsMetric
$offset = $k;
}
}
echo " (done)";
// echo " (done)";
//now get active users from each interval after this date
$endTs = $timestamps[$x-$x+1];
//echo "[$x]:: actives: " . date('d-m-Y', $endTs) . " signups: " . date('d-m-Y', $startTs) . "\n";
$offset = "";
echo "\n Gathering actives \n";
// echo "\n Gathering actives \n";
foreach ($signups as $signup => $ts) {
if ($this->wasActive($signup, $now)) {
$this->db->insert("{$this->namespace}:$x:$now", [$signup=>time()]);
......@@ -76,7 +76,7 @@ class Retention implements AnalyticsMetric
echo "\r $x: $signup (not active) $offset";
}
}
echo "(done)";
// echo "(done)";
}
return true;
......
<?php
/**
* EarningsDeposit
*/
namespace Minds\Core\Monetization\Partners;
use Minds\Traits\MagicAttributes;
class EarningsDeposit
{
use MagicAttributes;
/** @var string */
private $userGuid;
/** @var int */
private $timestamp;
/** @var string */
private $item;
/** @var int */
private $amountCents;
/** @var int */
private $amountTokens;
/**
* Export
* @param array $extras
* @return array
*/
public function export($extras = []): array
{
return [
'user_guid' => $this->userGuid,
'timestamp' => $this->timestamp,
'item' => $this->item,
'amount_cents' => (int) $this->amountCents,
'amount_usd' => (int) $this->amountCents / 100,
'amount_tokens' => (string) $this->amountTokens,
];
}
}
<?php
/**
* Manager
*/
namespace Minds\Core\Monetization\Partners;
use Minds\Core\Analytics\EntityCentric\Manager as EntityCentricManager;
use Minds\Core\EntitiesBuilder;
use Minds\Common\Repository\Response;
use Minds\Core\Di\Di;
use DateTime;
class Manager
{
const VIEWS_RPM_CENTS = 100; // $1 USD
/** @var Repository */
private $repository;
/** @var EntityCentricManager */
private $entityCentricManager;
/** @var EntitiesBuilder */
private $entitiesBuilder;
public function __construct($repository = null, $entityCentricManager = null, $entitiesBuilder = null)
{
$this->repository = $repository ?? new Repository();
$this->entityCentricManager = $entityCentricManager ?? new EntityCentricManager();
$this->entitiesBuilder = $entitiesBuilder ?? Di::_()->get('EntitiesBuilder');
}
/**
* Return a list of earning deposits
* @param array $opts
* @return Response
*/
public function getList(array $opts = []): Response
{
return $this->repository->getList($opts);
}
/**
* Add an earnings deposit
* @param EarningsDeposit $deposit
* @return bool
*/
public function add(EarningsDeposit $deposit): bool
{
$this->repository->add($deposit);
return true;
}
/**
* @param array $opts
* @return iterable
*/
public function issueDeposits(array $opts = []): iterable
{
$opts = array_merge([
'from' => strtotime('midnight'),
], $opts);
$users = [];
foreach ($this->entityCentricManager->getListAggregatedByOwner([
'fields' => [ 'views::single' ],
'from' => $opts['from'],
]) as $ownerSum) {
$views = $ownerSum['views::single']['value'];
$amountCents = ($views / 1000) * static::VIEWS_RPM_CENTS;
if ($amountCents < 1) { // Has to be at least 1 cent / $0.01
continue;
}
// Is this user in the pro program?
$owner = $this->entitiesBuilder->single($ownerSum['key']);
if (!$owner || !$owner->isPro()) {
continue;
}
$deposit = new EarningsDeposit();
$deposit->setTimestamp($opts['from'])
->setUserGuid($ownerSum['key'])
->setAmountCents($amountCents)
->setItem("views");
$this->repository->add($deposit);
yield $deposit;
}
}
}
<?php
namespace Minds\Core\Monetization\Partners;
use Minds\Core\Data\Cassandra\Client;
use Minds\Core\Data\Cassandra\Prepared\Custom as Prepared;
use Minds\Common\Repository\Response;
use Minds\Core\Di\Di;
use Cassandra\Bigint;
use Cassandra\Timestamp;
class Repository
{
/** @var Client */
private $db;
public function __construct($db = null)
{
$this->db = $db ?? Di::_()->get('Database\Cassandra\Cql');
}
/**
* @param array $opts
* @return Response
*/
public function getList(array $opts = []): Response
{
$opts = array_merge([
'from' => null,
'to' => null,
'user_guid' => null,
'allow_filtering' => false,
], $opts);
$statement = "SELECT * FROM partner_earnings_ledger";
$values = [];
$where = [];
if ($opts['user_guid']) {
$where[] = "user_guid = ?";
$values[] = new Bigint($opts['user_guid']);
}
if ($opts['from']) {
$where[] = "timestamp >= ?";
$values[] = new Timestamp($opts['from']);
if (!$opts['user_guid']) { // This is a temporary work around (MH)
$opts['allow_filtering'] = true;
}
}
if ($opts['to']) {
$where[] = "timestamp < ?";
$values[] = new Timestamp($opts['to']);
}
$statement .= " WHERE " . implode(' ', $where);
if ($opts['allow_filtering']) {
$statement .= " ALLOW FILTERING";
}
$prepared = new Prepared();
$prepared->query($statement, $values);
$result = $this->db->request($prepared);
if (!$result || !$result[0]) {
return (new Response())->setLastPage(true);
}
$response = new Response();
foreach ($result as $row) {
$deposit = new EarningsDeposit();
$deposit->setTimestamp($row['timestamp']->time())
->setItem($row['item'])
->setUserGuid((string) $row['user_guid'])
->setAmountCents($row['amount_cents'])
->setAmountTokens($row['amount_tokens'] ? $row['amount_tokens']->value() : 0);
$response[] = $deposit;
}
$response->setLastPage($result->isLastPage());
return $response;
}
/**
* @param string $urn
* @return EarningsDeposit
*/
public function get($urn): EarningsDeposit
{
// TODO
}
/**
* @param EarningsDeposit $deposit
* @return bool
*/
public function add(EarningsDeposit $deposit): bool
{
$prepared = new Prepared();
$prepared->query("INSERT INTO partner_earnings_ledger (user_guid, timestamp, item, amount_cents, amount_tokens) VALUES (?,?,?,?,?)",
[
new Bigint($deposit->getUserGuid()),
new Timestamp($deposit->getTimestamp()),
$deposit->getItem(),
$deposit->getAmountCents() ? (int) $deposit->getAmountCents() : null,
$deposit->getAmountTokens() ? new Bigint($deposit->getAmountTokens()) : null,
]);
return (bool) $this->db->request($prepared);
}
/**
* @param EarningsDeposit $deposit
* @param array $fields
* @return bool
*/
public function update(EarningsDeposit $deposit, $fields = []): bool
{
}
/**
* @param EarningsDeposit $deposit
* @return bool
*/
public function delete(EarningsDeposit $deposit): bool
{
}
}
......@@ -96,6 +96,7 @@ class HydrateSettingsDelegate
error_log($e);
}
$settings->setPublished($user->isProPublished());
return $settings;
}
}
......@@ -322,6 +322,13 @@ class Manager
->setCustomHead($values['custom_head']);
}
if (isset($values['published'])) {
$this->user->setProPublished($values['published']);
$this->saveAction
->setEntity($this->user)
->save();
}
$this->setupRoutingDelegate
->onUpdate($settings);
......
......@@ -46,6 +46,8 @@ use Minds\Traits\MagicAttributes;
* @method Settings setFeaturedContent(array $featuredContent)
* @method string getCustomHead()
* @method Settings setCustomHead(string $customHead)
* @method bool isPublished()
* @method Settings setPublished(bool $published)
*/
class Settings implements JsonSerializable
{
......@@ -120,6 +122,9 @@ class Settings implements JsonSerializable
/** @var string */
protected $customHead = '';
/** @var bool */
protected $published;
/**
* @return string
*/
......@@ -158,6 +163,7 @@ class Settings implements JsonSerializable
'custom_head' => $this->customHead,
'one_line_headline' => $this->getOneLineHeadline(),
'styles' => $this->buildStyles(),
'published' => $this->published,
];
}
......
......@@ -355,11 +355,6 @@ class Defaults
});
$marketing = [
'plus' => [
'title' => 'Minds Plus',
'description' => 'Upgrade your channel for premium features',
'image' => 'assets/photos/fractal.jpg'
],
'wallet' => [
'title' => 'Wallet',
'description' => 'Manage all of your transactions and earnings on Minds',
......@@ -424,6 +419,21 @@ class Defaults
'title' => 'Minds Mobile App',
'description' => 'Download the Minds mobile app for Android & iOS.',
'image' => 'assets/photos/mobile-app.jpg',
],
'upgrades' => [
'title' => 'Upgrade your Minds experience',
'description' => 'Minds offers a unique range of powerful upgrades that will supercharge your experience.',
'image' => 'assets/marketing/upgrades-1.jpg',
],
'plus' => [
'title' => 'Minds Plus',
'description' => 'Upgrade your channel and unlock premium features.',
'image' => 'assets/photos/browsing-mobileapp-discovery.jpg',
],
'pro' => [
'title' => 'Minds Pro',
'description' => 'The ultimate platform for creators and brands.',
'image' => 'assets/photos/podcast-people.jpg',
]
];
......
<?php
/**
* Upgrades Delegate
*/
namespace Minds\Core\Wire\Delegates;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Wire\Wire;
use Minds\Core\Pro\Manager as ProManager;
class UpgradesDelegate
{
/** @var Config */
private $config;
/** @var EntitiesBuilder */
private $entitiesBuilder;
/** @var ProManager */
private $proManager;
public function __construct($config = null, $entitiesBuilder = null, $proManager = null)
{
$this->config = $config ?: Di::_()->get('Config');
$this->entitiesBuilder = $entitiesBuilder ?: Di::_()->get('EntitiesBuilder');
$this->proManager = $proManager ?? Di::_()->get('Pro\Manager');
}
/**
* On Wire
* @param Wire $wire
* @param string $receiver_address
* @return Wire $wire
*/
public function onWire($wire, $receiver_address): Wire
{
switch ($wire->getReceiver()->guid) {
case $this->config->get('blockchain')['contracts']['wire']['plus_guid']:
return $this->onPlusUpgrade($wire, $receiver_address);
break;
case $this->config->get('pro')['handler']:
return $this->onProUpgrade($wire, $receiver_address);
break;
}
return $wire; // Not expected
}
private function onPlusUpgrade($wire, $receiver_address): Wire
{
/*if (
!(
$receiver_address == 'offchain'
|| $receiver_address == $this->config->get('blockchain')['contracts']['wire']['plus_address']
)
) {
return $wire; //not offchain or potential onchain fraud
}
// 20 tokens
if ($wire->getAmount() != "20000000000000000000") {
return $wire; //incorrect wire amount sent
}*/
//set the plus period for this user
$user = $wire->getSender();
// rebuild the user as we can't trust upstream
$user = $this->entitiesBuilder->single($user->getGuid(), [
'cache' => false,
]);
if (!$user) {
return $wire;
}
$days = 30;
$monthly = $this->config->get('upgrades')['plus']['monthly'];
$yearly = $this->config->get('upgrades')['plus']['yearly'];
switch ($wire->getMethod()) {
case 'tokens':
if ($monthly['tokens'] == $wire->getAmount() / (10 ** 18)) {
$days = 30;
} elseif ($yearly['tokens'] == $wire->getAmount() / (10 ** 18)) {
$days = 365;
} else {
return $wire;
}
break;
case 'usd':
if ($monthly['usd'] == $wire->getAmount() / 100) {
$days = 30;
} elseif ($yearly['usd'] == $wire->getAmount() / 100) {
$days = 365;
} else {
return $wire;
}
break;
default:
return $wire;
}
$expires = strtotime("+{$days} days", $wire->getTimestamp());
$user->setPlusExpires($expires);
$user->save();
//$wire->setSender($user);
return $wire;
}
private function onProUpgrade($wire, $receiver_address): Wire
{
//set the plus period for this user
$user = $wire->getSender();
// rebuild the user as we can't trust upstream
$user = $this->entitiesBuilder->single($user->getGuid(), [
'cache' => false,
]);
if (!$user) {
return $wire;
}
$days = 30;
$monthly = $this->config->get('upgrades')['pro']['monthly'];
$yearly = $this->config->get('upgrades')['pro']['yearly'];
error_log($wire->getMethod());
switch ($wire->getMethod()) {
case 'tokens':
error_log($wire->getAmount());
if ($monthly['tokens'] == $wire->getAmount() / (10 ** 18)) {
$days = 30;
} elseif ($yearly['tokens'] == $wire->getAmount() / (10 ** 18)) {
$days = 365;
} else {
return $wire;
}
break;
case 'usd':
if ($monthly['usd'] == $wire->getAmount() / 100) {
$days = 30;
} elseif ($yearly['usd'] == $wire->getAmount() / 100) {
$days = 365;
} else {
return $wire;
}
break;
default:
return $wire;
}
$expires = strtotime("+{$days} days", $wire->getTimestamp());
$this->proManager->setUser($user)
->enable($expires);
return $wire;
}
}
......@@ -61,8 +61,8 @@ class Manager
/** @var Core\Blockchain\Wallets\OffChain\Cap $cap */
protected $cap;
/** @var Delegates\Plus $plusDelegate */
protected $plusDelegate;
/** @var Delegates\UpgradesDelegate */
protected $upgradesDelegate;
/** @var Delegates\RecurringDelegate $recurringDelegate */
protected $recurringDelegate;
......@@ -87,7 +87,7 @@ class Manager
$client = null,
$token = null,
$cap = null,
$plusDelegate = null,
$upgradesDelegate = null,
$recurringDelegate = null,
$notificationDelegate = null,
$cacheDelegate = null,
......@@ -101,7 +101,8 @@ class Manager
$this->client = $client ?: Di::_()->get('Blockchain\Services\Ethereum');
$this->token = $token ?: Di::_()->get('Blockchain\Token');
$this->cap = $cap ?: Di::_()->get('Blockchain\Wallets\OffChain\Cap');
$this->plusDelegate = $plusDelegate ?: new Delegates\Plus();
$this->upgradesDelegate = $upgradesDelegate ?? new Delegates\UpgradesDelegate();
;
$this->recurringDelegate = $recurringDelegate ?: new Delegates\RecurringDelegate();
$this->notificationDelegate = $notificationDelegate ?: new Delegates\NotificationDelegate();
$this->cacheDelegate = $cacheDelegate ?: new Delegates\CacheDelegate();
......@@ -248,8 +249,8 @@ class Manager
$wire->setAddress('offchain');
// Notify plus
$this->plusDelegate
// Notify plus/pro
$this->upgradesDelegate
->onWire($wire, 'offchain');
// Send notification
......@@ -287,6 +288,10 @@ class Manager
// Save the wire to the Repository
$this->repository->add($wire);
// Notify plus/pro
$this->upgradesDelegate
->onWire($wire, 'usd');
// Send notification
$this->notificationDelegate->onAdd($wire);
......@@ -330,7 +335,7 @@ class Manager
->setCompleted(true);
$this->txRepo->add($transaction);
$this->plusDelegate
$this->upgradesDelegate
->onWire($wire, $data['receiver_address']);
$this->notificationDelegate->onAdd($wire);
......
......@@ -32,6 +32,7 @@ class User extends \ElggUser
$this->attributes['plus'] = 0; //TODO: REMOVE
$this->attributes['plus_expires'] = 0;
$this->attributes['pro_expires'] = 0;
$this->attributes['pro_published'] = 0;
$this->attributes['verified'] = 0;
$this->attributes['founder'] = 0;
$this->attributes['disabled_boost'] = 0;
......@@ -824,6 +825,7 @@ class User extends \ElggUser
$export['programs'] = $this->getPrograms();
$export['plus'] = (bool) $this->isPlus();
$export['pro'] = (bool) $this->isPro();
$export['pro_published'] = (bool) $this->isProPublished();
$export['verified'] = (bool) $this->verified;
$export['founder'] = (bool) $this->founder;
$export['disabled_boost'] = (bool) $this->disabled_boost;
......@@ -945,6 +947,35 @@ class User extends \ElggUser
return $this->getProExpires() >= time();
}
/**
* Set if pro is published
* @param bool $published
* @return self
*/
public function setProPublished(bool $published): self
{
$this->pro_published = $published ? 1 : 0;
return $this;
}
/**
* Return if is published
* @return bool
*/
public function getProPublished(): bool
{
return (bool) $this->pro_published;
}
/**
* Return if is published
* @return bool
*/
public function isProPublished(): bool
{
return (bool) $this->pro_published;
}
/**
* Gets the categories to which the user is subscribed.
*
......
<?php
namespace Spec\Minds\Core\Analytics\Dashboards\Filters;
use Minds\Entities\User;
use Minds\Core\Analytics\Dashboards\Filters\FiltersCollection;
use Minds\Core\Analytics\Dashboards\Filters\ViewTypeFilter;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class FiltersCollectionSpec extends ObjectBehavior
{
public function it_is_initializable()
{
$this->shouldHaveType(FiltersCollection::class);
}
public function it_should_add_filters_to_collection()
{
$this->setUser(new User());
$this->addFilters(new ViewTypeFilter);
$filters = $this->getFilters();
$filters['view_type']->getId()
->shouldBe('view_type');
}
public function it_should_export_filters()
{
$this->setUser(new User());
$this->addFilters(new ViewTypeFilter);
$export = $this->export();
$export[0]['id']
->shouldBe('view_type');
$export[0]['options']
->shouldHaveCount(4);
}
}
<?php
namespace Spec\Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Analytics\Dashboards\Metrics\ActiveUsersMetric;
use Minds\Core\Analytics\Dashboards\Timespans\TimespansCollection;
use Minds\Core\Analytics\Dashboards\Timespans\AbstractTimespan;
use Minds\Core\Analytics\Dashboards\Filters\FiltersCollection;
use Minds\Core\Analytics\Dashboards\Filters\AbstractFilter;
use Minds\Core\Data\Elasticsearch\Client;
use Minds\Entities\User;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class ActiveUsersMetricSpec extends ObjectBehavior
{
private $es;
private $timespansCollection;
private $filtersCollection;
public function let(Client $es, TimespansCollection $timespansCollection, FiltersCollection $filtersCollection)
{
$this->beConstructedWith($es);
$this->es = $es;
$this->setTimespansCollection($timespansCollection);
$this->setFiltersCollection($filtersCollection);
$this->timespansCollection = $timespansCollection;
$this->filtersCollection = $filtersCollection;
}
public function it_is_initializable()
{
$this->shouldHaveType(ActiveUsersMetric::class);
}
public function it_should_build_summary(AbstractTimespan $mockTimespan, AbstractFilter $mockFilter)
{
$this->setUser(new User());
$this->timespansCollection->getSelected()
->willReturn($mockTimespan);
$this->filtersCollection->getSelected()
->willReturn([$mockFilter]);
$this->es->request(Argument::any())
->willReturn(
[
'aggregations' => [
'1' => [
'value' => 128,
],
]
],
[
'aggregations' => [
'1' => [
'value' => 256,
],
]
]
);
$this->buildSummary();
$this->getSummary()->getValue()
->shouldBe(128);
$this->getSummary()->getComparisonValue()
->shouldBe(256);
}
public function it_should_build_visualisation(AbstractTimespan $mockTimespan, AbstractFilter $mockFilter)
{
$this->setUser(new User());
$this->timespansCollection->getSelected()
->willReturn($mockTimespan);
$this->filtersCollection->getSelected()
->willReturn([$mockFilter]);
$this->es->request(Argument::any())
->willReturn([
'aggregations' => [
'1' => [
'buckets' => [
[
'key' => strtotime('Midnight 1st December 2018') * 1000,
'2' => [
'value' => 256,
],
],
[
'key' => strtotime('Midnight 1st January 2019') * 1000,
'2' => [
'value' => 128,
],
],
[
'key' => strtotime('Midnight 1st February 2019') * 1000,
'2' => [
'value' => 4,
],
],
[
'key' => strtotime('Midnight 1st March 2019') * 1000,
'2' => [
'value' => 685,
],
],
],
],
]
]);
$this->buildVisualisation();
$xValues = $this->getVisualisation()->getXValues();
$xValues[0]->shouldBe('01-12-2018');
$xValues[1]->shouldBe('01-01-2019');
$xValues[2]->shouldBe('01-02-2019');
$xValues[3]->shouldBe('01-03-2019');
$yValues = $this->getVisualisation()->getYValues();
$yValues[0]->shouldBe(256);
$yValues[1]->shouldBe(128);
$yValues[2]->shouldBe(4);
$yValues[3]->shouldBe(685);
}
}
<?php
namespace Spec\Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Analytics\Dashboards\Metrics\MetricsCollection;
use Minds\Core\Analytics\Dashboards\Metrics\ActiveUsersMetric;
use Minds\Core\Analytics\Dashboards\Metrics\SignupsMetric;
use Minds\Core\Analytics\Dashboards\Metrics\ViewsMetric;
use Minds\Core\Analytics\Dashboards\Timespans\TimespansCollection;
use Minds\Core\Analytics\Dashboards\Filters\FiltersCollection;
use Minds\Core\Analytics\Dashboards\Timespans\TodayTimespan;
use Minds\Entities\User;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class MetricsCollectionSpec extends ObjectBehavior
{
private $timespansCollection;
private $todayTimespan;
public function let(TimespansCollection $timespansCollection, TodayTimespan $todayTimespan)
{
$this->timespansCollection = $timespansCollection;
$this->todayTimespan = $todayTimespan;
$this->setTimespansCollection($timespansCollection);
$this->setFiltersCollection(new FiltersCollection());
$this->timespansCollection->setSelectedId('today');
}
public function it_is_initializable()
{
$this->shouldHaveType(MetricsCollection::class);
}
public function it_should_add_metrics_to_collection()
{
$this->setUser(new User());
$this->addMetrics(
// new ActiveUsersMetric,
// new SignupsMetric,
new ViewsMetric
);
$metrics = $this->getMetrics();
$metrics['views']->getId()
->shouldBe('views');
}
public function it_should_export_metrics()
{
$this->setUser(new User());
$this->addMetrics(
new ActiveUsersMetric, // Not admin so won't export
new SignupsMetric, // Not admin so wont export
new ViewsMetric
);
$export = $this->export();
$export[0]['id']
->shouldBe('views');
}
public function it_should_build_metrics(ActiveUsersMetric $activeUsersMetric)
{
$this->setUser(new User());
$activeUsersMetric->getId()
->shouldBeCalled()
->willReturn('active_users');
$activeUsersMetric->setTimespansCollection($this->timespansCollection)
->shouldBeCalled()
->willReturn($activeUsersMetric);
$activeUsersMetric->setFiltersCollection(Argument::type(FiltersCollection::class))
->shouldBeCalled()
->willReturn($activeUsersMetric);
$activeUsersMetric->buildSummary()
->shouldBeCalled();
$activeUsersMetric->getPermissions()
->willReturn([ 'user ']);
$activeUsersMetric->setUser(Argument::any())
->wilLReturn($activeUsersMetric);
$this->addMetrics($activeUsersMetric);
$this->buildSummaries();
}
}
<?php
namespace Spec\Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Analytics\Dashboards\Metrics\SignupsMetric;
use Minds\Core\Analytics\Dashboards\Metrics\ActiveUsersMetric;
use Minds\Core\Analytics\Dashboards\Timespans\TimespansCollection;
use Minds\Core\Analytics\Dashboards\Timespans\AbstractTimespan;
use Minds\Core\Analytics\Dashboards\Filters\FiltersCollection;
use Minds\Core\Analytics\Dashboards\Filters\AbstractFilter;
use Minds\Core\Data\Elasticsearch\Client;
use Minds\Entities\User;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class SignupsMetricSpec extends ObjectBehavior
{
private $es;
private $timespansCollection;
private $filtersCollection;
public function let(Client $es, TimespansCollection $timespansCollection, FiltersCollection $filtersCollection)
{
$this->beConstructedWith($es);
$this->es = $es;
$this->setTimespansCollection($timespansCollection);
$this->setFiltersCollection($filtersCollection);
$this->timespansCollection = $timespansCollection;
$this->filtersCollection = $filtersCollection;
}
public function it_is_initializable()
{
$this->shouldHaveType(SignupsMetric::class);
}
public function it_should_build_summary(AbstractTimespan $mockTimespan, AbstractFilter $mockFilter)
{
$this->setUser(new User());
$this->timespansCollection->getSelected()
->willReturn($mockTimespan);
$this->filtersCollection->getSelected()
->willReturn([$mockFilter]);
$this->es->request(Argument::any())
->willReturn(
[
'aggregations' => [
'1' => [
'value' => 128,
],
],
],
[
'aggregations' => [
'1' => [
'value' => 256,
],
],
]);
$this->buildSummary();
$this->getSummary()->getValue()
->shouldBe(128);
$this->getSummary()->getComparisonValue()
->shouldBe(256);
}
public function it_should_build_visualisation(AbstractTimespan $mockTimespan, AbstractFilter $mockFilter)
{
$this->setUser(new User());
$this->timespansCollection->getSelected()
->willReturn($mockTimespan);
$this->filtersCollection->getSelected()
->willReturn([$mockFilter]);
$this->es->request(Argument::any())
->willReturn([
'aggregations' => [
'1' => [
'buckets' => [
[
'key' => strtotime('Midnight 1st December 2018') * 1000,
'2' => [
'value' => 256,
],
],
[
'key' => strtotime('Midnight 1st January 2019') * 1000,
'2' => [
'value' => 128,
],
],
[
'key' => strtotime('Midnight 1st February 2019') * 1000,
'2' => [
'value' => 4,
],
],
[
'key' => strtotime('Midnight 1st March 2019') * 1000,
'2' => [
'value' => 685,
],
],
],
],
]
]);
$this->buildVisualisation();
$xValues = $this->getVisualisation()->getXValues();
$xValues[0]->shouldBe('01-12-2018');
$xValues[1]->shouldBe('01-01-2019');
$xValues[2]->shouldBe('01-02-2019');
$xValues[3]->shouldBe('01-03-2019');
$yValues = $this->getVisualisation()->getYValues();
$yValues[0]->shouldBe(256);
$yValues[1]->shouldBe(128);
$yValues[2]->shouldBe(4);
$yValues[3]->shouldBe(685);
}
}
<?php
namespace Spec\Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Analytics\Dashboards\Metrics\ViewsMetric;
use Minds\Core\Analytics\Dashboards\Metrics\ActiveUsersMetric;
use Minds\Core\Analytics\Dashboards\Timespans\TimespansCollection;
use Minds\Core\Analytics\Dashboards\Timespans\AbstractTimespan;
use Minds\Core\Analytics\Dashboards\Filters\FiltersCollection;
use Minds\Core\Analytics\Dashboards\Filters\AbstractFilter;
use Minds\Core\Data\Elasticsearch\Client;
use Minds\Entities\User;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class ViewsMetricSpec extends ObjectBehavior
{
private $es;
private $timespansCollection;
private $filtersCollection;
public function let(Client $es, TimespansCollection $timespansCollection, FiltersCollection $filtersCollection)
{
$this->beConstructedWith($es);
$this->es = $es;
$this->setTimespansCollection($timespansCollection);
$this->setFiltersCollection($filtersCollection);
$this->timespansCollection = $timespansCollection;
$this->filtersCollection = $filtersCollection;
}
public function it_is_initializable()
{
$this->shouldHaveType(ViewsMetric::class);
}
public function it_should_build_summary(AbstractTimespan $mockTimespan, AbstractFilter $mockFilter)
{
$this->setUser(new User());
$this->timespansCollection->getSelected()
->willReturn($mockTimespan);
$this->filtersCollection->getSelected()
->willReturn([$mockFilter]);
$this->es->request(Argument::any())
->willReturn(
[
'aggregations' => [
'1' => [
'value' => 128,
]
]
],
[
'aggregations' => [
'1' => [
'value' => 256,
]
]
]);
$this->buildSummary();
$this->getSummary()->getValue()
->shouldBe(128);
$this->getSummary()->getComparisonValue()
->shouldBe(256);
}
public function it_should_build_visualisation(AbstractTimespan $mockTimespan, AbstractFilter $mockFilter)
{
$this->setUser(new User());
$this->timespansCollection->getSelected()
->willReturn($mockTimespan);
$this->filtersCollection->getSelected()
->willReturn([$mockFilter]);
$this->es->request(Argument::any())
->willReturn([
'aggregations' => [
'1' => [
'buckets' => [
[
'key' => strtotime('Midnight 1st December 2018') * 1000,
'2' => [
'value' => 256,
],
],
[
'key' => strtotime('Midnight 1st January 2019') * 1000,
'2' => [
'value' => 128,
],
],
[
'key' => strtotime('Midnight 1st February 2019') * 1000,
'2' => [
'value' => 4,
],
],
[
'key' => strtotime('Midnight 1st March 2019') * 1000,
'2' => [
'value' => 685,
],
],
],
],
]
]);
$this->buildVisualisation();
$buckets = $this->getVisualisation()->getBuckets();
// $xValues[0]->shouldBe('01-12-2018');
// $xValues[1]->shouldBe('01-01-2019');
// $xValues[2]->shouldBe('01-02-2019');
// $xValues[3]->shouldBe('01-03-2019');
}
}
<?php
namespace Spec\Minds\Core\Analytics\Dashboards\Timespans;
use Minds\Core\Analytics\Dashboards\Timespans\TimespansCollection;
use Minds\Core\Analytics\Dashboards\Timespans\TodayTimespan;
use Minds\Core\Analytics\Dashboards\Timespans\MtdTimespan;
use Minds\Core\Analytics\Dashboards\Timespans\YtdTimespan;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class TimespansCollectionSpec extends ObjectBehavior
{
public function it_is_initializable()
{
$this->shouldHaveType(TimespansCollection::class);
}
public function it_should_add_timespans_to_collection()
{
$this->addTimespans(
new TodayTimespan(),
new MtdTimespan(),
new YtdTimespan()
);
$this->getTimespans()['today']->getId()
->shouldBe('today');
$this->getTimespans()['mtd']->getId()
->shouldBe('mtd');
$this->getTimespans()['ytd']->getId()
->shouldBe('ytd');
}
public function it_should_export_timestamps()
{
$this->addTimespans(
new TodayTimespan(),
new MtdTimespan(),
new YtdTimespan()
);
$exported = $this->export();
$exported[0]['id']->shouldBe('today');
$exported[0]['from_ts_ms']->shouldBe(strtotime('midnight') * 1000);
$exported[1]['id']->shouldBe('mtd');
$exported[1]['from_ts_ms']->shouldBe(strtotime('midnight first day of this month') * 1000);
$exported[2]['id']->shouldBe('ytd');
$exported[2]['from_ts_ms']->shouldBe(strtotime('midnight first day of January') * 1000);
$exported[2]['interval']->shouldBe('month');
}
}
<?php
namespace Spec\Minds\Core\Analytics\Dashboards;
use Minds\Core\Analytics\Dashboards\TrafficDashboard;
use Minds\Core\Analytics\Dashboards\Timespans\TimespansCollection;
use Minds\Core\Analytics\Dashboards\Metrics\MetricsCollection;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Filters\FiltersCollection;
use Minds\Entities\User;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class TrafficDashboardSpec extends ObjectBehavior
{
private $timespansCollection;
private $metricsCollection;
private $filtersCollection;
public function let(TimespansCollection $timespansCollection, MetricsCollection $metricsCollection, FiltersCollection $filtersCollection)
{
$this->beConstructedWith(null, $metricsCollection, null);
$this->timespansCollection = $timespansCollection;
$this->metricsCollection = $metricsCollection;
$this->filtersCollection = $filtersCollection;
}
public function it_is_initializable()
{
$this->shouldHaveType(TrafficDashboard::class);
}
public function it_should_build_dashboard(AbstractMetric $mockMetric)
{
$user = new User();
$this->setUser($user);
$this->setTimespanId('today');
$this->setFilterIds([
'platform::browser'
]);
// Metrics
$this->metricsCollection->setTimespansCollection(Argument::type(TimespansCollection::class))
->willReturn($this->metricsCollection);
$this->metricsCollection->setFiltersCollection(Argument::type(FiltersCollection::class))
->willReturn($this->metricsCollection);
$this->metricsCollection->setSelectedId('active_users')
->willReturn($this->metricsCollection);
$this->metricsCollection->setUser($user)
->willReturn($this->metricsCollection);
$this->metricsCollection->addMetrics(Argument::any(), Argument::any(), Argument::any(), Argument::any())
->shouldBeCalled()
->willReturn($this->metricsCollection);
$this->metricsCollection->build()
->shouldBeCalled()
->willReturn($this->metricsCollection);
$this->metricsCollection->getSelected()
->willReturn($mockMetric);
$this->build();
}
}
......@@ -110,6 +110,12 @@ class HydrateSettingsDelegateSpec extends ObjectBehavior
->shouldBeCalled()
->willReturn($settings);
$user->isProPublished()
->willReturn(false);
$settings->setPublished(false)
->shouldBeCalled();
$this
->shouldNotThrow(Exception::class)
->duringOnGet($user, $settings);
......
......@@ -380,6 +380,7 @@ $CONFIG->set('blockchain_override', [
]);
$CONFIG->set('plus', [
'handler' => '',
'tokens' => [
'month' => 5,
'year' => 50
......@@ -575,7 +576,32 @@ $CONFIG->set('gitlab', [
]);
$CONFIG->set('pro', [
'handler' => '',
'root_domains' => ['minds.com', 'www.minds.com', 'localhost'],
'subdomain_suffix' => 'minds.com',
'dynamodb_table_name' => 'traefik',
]);
$CONFIG->set('upgrades', [
'pro' => [
'monthly' => [
'tokens' => 240,
'usd' => 60,
],
'yearly' => [
'tokens' => 2400,
'usd' => 600,
]
],
'plus' => [
'monthly' => [
'tokens' => 28,
'usd' => 7,
],
'yearly' => [
'tokens' => 240,
'usd' => 60,
]
],
]);