282 lines
10 KiB
PHP
282 lines
10 KiB
PHP
<?php
|
|
namespace Sodino\Services;
|
|
|
|
use Sodino\Repositories\EventRepository;
|
|
use Sodino\Repositories\RuleRepository;
|
|
|
|
class AnalyticsService {
|
|
private $eventRepository;
|
|
private $ruleRepository;
|
|
|
|
public function __construct(EventRepository $eventRepository, RuleRepository $ruleRepository) {
|
|
$this->eventRepository = $eventRepository;
|
|
$this->ruleRepository = $ruleRepository;
|
|
}
|
|
|
|
public function primeCache() {
|
|
$cache_keys = [
|
|
'sodino_dashboard_summary',
|
|
'sodino_dashboard_sales_chart',
|
|
'sodino_dashboard_rule_performance',
|
|
'sodino_dashboard_user_behavior',
|
|
];
|
|
|
|
foreach ($cache_keys as $key) {
|
|
delete_transient($key);
|
|
}
|
|
}
|
|
|
|
public function getDashboardData(array $filters = []) {
|
|
$cache_key = 'sodino_dashboard_' . md5(wp_json_encode($filters));
|
|
$cached = get_transient($cache_key);
|
|
if ($cached !== false) {
|
|
return $cached;
|
|
}
|
|
|
|
$range = $this->getDateRange($filters['range'] ?? '7d', $filters['start_date'] ?? '', $filters['end_date'] ?? '');
|
|
$filters['from'] = $range['start'];
|
|
$filters['to'] = $range['end'];
|
|
|
|
if (!empty($filters['category_id'])) {
|
|
$filters['product_ids'] = $this->getProductIdsByCategory($filters['category_id']);
|
|
}
|
|
|
|
$summary = $this->getSummary($filters);
|
|
$salesChart = $this->getSalesChart($filters);
|
|
$rulePerformance = $this->getRulePerformance($filters);
|
|
$userBehavior = $this->getUserBehavior($filters);
|
|
$insights = $this->getInsights($summary, $filters);
|
|
|
|
$result = [
|
|
'summary' => $summary,
|
|
'sales_chart' => $salesChart,
|
|
'rule_performance' => $rulePerformance,
|
|
'user_behavior' => $userBehavior,
|
|
'insights' => $insights,
|
|
];
|
|
|
|
set_transient($cache_key, $result, 10 * MINUTE_IN_SECONDS);
|
|
return $result;
|
|
}
|
|
|
|
public function getSummary(array $filters = []) {
|
|
$purchaseFilters = array_merge($filters, ['event_type' => 'purchase']);
|
|
$discountFilters = array_merge($filters, ['event_type' => 'discount_applied']);
|
|
|
|
$purchaseCount = $this->eventRepository->getCount($purchaseFilters);
|
|
$totalRevenue = $this->eventRepository->getSum('value', $purchaseFilters);
|
|
$totalDiscount = $this->eventRepository->getSum('discount_value', $discountFilters);
|
|
|
|
$productViewCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'product_view']));
|
|
$addToCartCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'add_to_cart']));
|
|
$checkoutStartCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'checkout_start']));
|
|
|
|
$conversionRate = 0;
|
|
if ($checkoutStartCount > 0) {
|
|
$conversionRate = round(($purchaseCount / $checkoutStartCount) * 100, 2);
|
|
} elseif ($addToCartCount > 0) {
|
|
$conversionRate = round(($purchaseCount / $addToCartCount) * 100, 2);
|
|
}
|
|
|
|
$addToCartRate = 0;
|
|
if ($productViewCount > 0) {
|
|
$addToCartRate = round(($addToCartCount / $productViewCount) * 100, 2);
|
|
}
|
|
|
|
$bestRule = $this->getBestRule($filters);
|
|
|
|
return [
|
|
'total_revenue' => $totalRevenue,
|
|
'total_discount' => $totalDiscount,
|
|
'purchase_count' => $purchaseCount,
|
|
'conversion_rate' => $conversionRate,
|
|
'add_to_cart_rate' => $addToCartRate,
|
|
'best_rule' => $bestRule,
|
|
];
|
|
}
|
|
|
|
public function getSalesChart(array $filters = []) {
|
|
$filters = array_merge($filters, $this->getDateRange($filters['range'] ?? '7d', $filters['start_date'] ?? '', $filters['end_date'] ?? ''));
|
|
|
|
if (!empty($filters['category_id'])) {
|
|
$filters['product_ids'] = $this->getProductIdsByCategory($filters['category_id']);
|
|
}
|
|
|
|
$start = new \DateTime($filters['start']);
|
|
$end = new \DateTime($filters['end']);
|
|
$days = [];
|
|
$series = [ 'before' => [], 'after' => [], 'labels' => [] ];
|
|
|
|
while ($start <= $end) {
|
|
$date = $start->format('Y-m-d');
|
|
$series['labels'][] = $date;
|
|
$dayFilters = array_merge($filters, ['from' => $date . ' 00:00:00', 'to' => $date . ' 23:59:59']);
|
|
$purchases = $this->eventRepository->getEvents(array_merge($dayFilters, ['event_type' => 'purchase']));
|
|
$dayRevenue = 0;
|
|
$dayDiscount = 0;
|
|
foreach ($purchases as $purchase) {
|
|
$dayRevenue += floatval($purchase['value']);
|
|
}
|
|
$discountEvents = $this->eventRepository->getEvents(array_merge($dayFilters, ['event_type' => 'discount_applied']));
|
|
foreach ($discountEvents as $discount) {
|
|
$dayDiscount += floatval($discount['discount_value']);
|
|
}
|
|
$series['after'][] = round($dayRevenue, 2);
|
|
$series['before'][] = round($dayRevenue + $dayDiscount, 2);
|
|
$start->modify('+1 day');
|
|
}
|
|
|
|
return $series;
|
|
}
|
|
|
|
public function getRulePerformance(array $filters = []) {
|
|
$rules = $this->ruleRepository->getAll();
|
|
$result = [];
|
|
|
|
foreach ($rules as $rule) {
|
|
$ruleFilters = array_merge($filters, ['event_type' => 'discount_applied', 'rule_id' => $rule->id]);
|
|
$count = $this->eventRepository->getCount($ruleFilters);
|
|
$revenue = $this->eventRepository->getSum('value', $ruleFilters);
|
|
$totalDiscount = $this->eventRepository->getSum('discount_value', $ruleFilters);
|
|
|
|
if ($count > 0) {
|
|
$result[] = [
|
|
'name' => $rule->name,
|
|
'count' => $count,
|
|
'revenue' => round($revenue, 2),
|
|
'discount' => round($totalDiscount, 2),
|
|
];
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function getUserBehavior(array $filters = []) {
|
|
$productViewCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'product_view']));
|
|
$addToCartCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'add_to_cart']));
|
|
$checkoutStartCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'checkout_start']));
|
|
$purchaseCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'purchase']));
|
|
|
|
return [
|
|
'product_views' => $productViewCount,
|
|
'add_to_cart' => $addToCartCount,
|
|
'checkout_start' => $checkoutStartCount,
|
|
'purchases' => $purchaseCount,
|
|
];
|
|
}
|
|
|
|
private function getBestRule(array $filters = []) {
|
|
$performance = $this->getRulePerformance($filters);
|
|
if (empty($performance)) {
|
|
return null;
|
|
}
|
|
|
|
usort($performance, function ($a, $b) {
|
|
return $b['revenue'] <=> $a['revenue'];
|
|
});
|
|
|
|
return $performance[0]['name'] ?? null;
|
|
}
|
|
|
|
private function getInsights(array $summary, array $filters = []) {
|
|
$insights = [];
|
|
|
|
if (!empty($summary['best_rule'])) {
|
|
$insights[] = sprintf('%s %s', __('قانون برتر:', 'sodino'), esc_html($summary['best_rule']));
|
|
}
|
|
|
|
if ($summary['total_discount'] > 0 && $summary['total_revenue'] > 0) {
|
|
$discountShare = round(($summary['total_discount'] / ($summary['total_revenue'] + $summary['total_discount'])) * 100, 2);
|
|
$insights[] = sprintf(
|
|
'%s %s%% %s',
|
|
__('تخفیفها باعث ایجاد', 'sodino'),
|
|
esc_html($discountShare),
|
|
__('درصد از درآمد شدهاند.', 'sodino')
|
|
);
|
|
}
|
|
|
|
if ($summary['conversion_rate'] > 0) {
|
|
$insights[] = sprintf('%s %s%% %s', __('نرخ تبدیل تقریبی', 'sodino'), esc_html($summary['conversion_rate']), __('است.', 'sodino'));
|
|
}
|
|
|
|
if (empty($insights)) {
|
|
$insights[] = __('هیچ دادهٔ قابل تحلیلی هنوز ثبت نشده است.', 'sodino');
|
|
}
|
|
|
|
return $insights;
|
|
}
|
|
|
|
private function getDateRange($range, $start, $end) {
|
|
$result = [
|
|
'start' => date('Y-m-d', strtotime('-6 days')),
|
|
'end' => date('Y-m-d'),
|
|
];
|
|
|
|
if ($range === '30d') {
|
|
$result['start'] = date('Y-m-d', strtotime('-29 days'));
|
|
$result['end'] = date('Y-m-d');
|
|
}
|
|
|
|
if ($range === 'custom' && !empty($start) && !empty($end)) {
|
|
$result['start'] = date('Y-m-d', strtotime($start));
|
|
$result['end'] = date('Y-m-d', strtotime($end));
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function getProductIdsByCategory($category_id) {
|
|
$products = get_posts([
|
|
'post_type' => 'product',
|
|
'numberposts' => -1,
|
|
'fields' => 'ids',
|
|
'tax_query' => [
|
|
[
|
|
'taxonomy' => 'product_cat',
|
|
'field' => 'term_id',
|
|
'terms' => intval($category_id),
|
|
],
|
|
],
|
|
]);
|
|
|
|
return $products ?: [];
|
|
}
|
|
|
|
public function getProductOptions() {
|
|
$products = get_posts([
|
|
'post_type' => 'product',
|
|
'numberposts' => -1,
|
|
'fields' => 'ids',
|
|
'orderby' => 'title',
|
|
'order' => 'ASC',
|
|
]);
|
|
|
|
$options = [];
|
|
foreach ($products as $product_id) {
|
|
$options[] = [
|
|
'id' => $product_id,
|
|
'name' => get_the_title($product_id),
|
|
];
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
|
|
public function getCategoryOptions() {
|
|
$categories = get_terms(['taxonomy' => 'product_cat', 'hide_empty' => false]);
|
|
$options = [];
|
|
|
|
if (!is_wp_error($categories)) {
|
|
foreach ($categories as $category) {
|
|
$options[] = [
|
|
'id' => $category->term_id,
|
|
'name' => $category->name,
|
|
];
|
|
}
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
}
|