164 lines
5.3 KiB
PHP
164 lines
5.3 KiB
PHP
<?php
|
|||
|
|
|
||
|
|
namespace App\Services;
|
||
|
|
|
||
|
|
use App\Models\Customer;
|
||
|
|
use Illuminate\Support\Collection;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* مخاطب اعلان داخل اپ — همسبک سگمنتهای پیامک + غیرفعالهای اخیر.
|
||
|
|
*/
|
||
|
|
class AppNotificationAudienceService
|
||
|
|
{
|
||
|
|
public const TYPE_ALL = 'all';
|
||
|
|
|
||
|
|
public const TYPE_SEGMENT = 'segment';
|
||
|
|
|
||
|
|
public const TYPE_CUSTOMERS = 'customers';
|
||
|
|
|
||
|
|
/** @return array<int, array{key: string, name: string, description: string}> */
|
||
|
|
public function segmentOptions(): array
|
||
|
|
{
|
||
|
|
return [
|
||
|
|
[
|
||
|
|
'key' => 'all_household',
|
||
|
|
'name' => 'همه کاربران خانگی',
|
||
|
|
'description' => 'همه مشتریان نوع خانگی (آرشیو نشده)',
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'key' => 'no_login',
|
||
|
|
'name' => 'هرگز وارد اپ نشدهاند',
|
||
|
|
'description' => 'آخرین بازدید خالی است',
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'key' => 'inactive_7d',
|
||
|
|
'name' => '۷ روز اخیر نیامدهاند',
|
||
|
|
'description' => 'آخرین بازدید بیش از ۷ روز پیش یا هرگز',
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'key' => 'inactive_30d',
|
||
|
|
'name' => '۳۰ روز اخیر نیامدهاند',
|
||
|
|
'description' => 'آخرین بازدید بیش از ۳۰ روز پیش یا هرگز',
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'key' => 'no_valid',
|
||
|
|
'name' => 'احراز هویت نشده',
|
||
|
|
'description' => 'is_legal = 0',
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'key' => 'active_wallet',
|
||
|
|
'name' => 'کاربران فعال (دارای کیف)',
|
||
|
|
'description' => 'حداقل یک ردیف در wallets دارند',
|
||
|
|
],
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
public function segmentLabel(string $key): string
|
||
|
|
{
|
||
|
|
foreach ($this->segmentOptions() as $opt) {
|
||
|
|
if ($opt['key'] === $key) {
|
||
|
|
return $opt['name'];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $key;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param array<int, string> $segmentKeys
|
||
|
|
* @return Collection<int, array{id: int, name: ?string, mobile: ?string}>
|
||
|
|
*/
|
||
|
|
public function resolveSegmentCustomers(array $segmentKeys): Collection
|
||
|
|
{
|
||
|
|
$byId = [];
|
||
|
|
foreach ($segmentKeys as $key) {
|
||
|
|
foreach ($this->customersForSegment((string) $key) as $row) {
|
||
|
|
$id = (int) ($row['id'] ?? 0);
|
||
|
|
if ($id > 0) {
|
||
|
|
$byId[$id] = $row;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return collect(array_values($byId));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param array<int, int> $customerIds
|
||
|
|
* @return Collection<int, array{id: int, name: ?string, mobile: ?string}>
|
||
|
|
*/
|
||
|
|
public function resolveCustomersByIds(array $customerIds): Collection
|
||
|
|
{
|
||
|
|
$ids = array_values(array_unique(array_filter(array_map('intval', $customerIds))));
|
||
|
|
if ($ids === []) {
|
||
|
|
return collect();
|
||
|
|
}
|
||
|
|
|
||
|
|
return Customer::query()
|
||
|
|
->whereIn('id', $ids)
|
||
|
|
->where('is_archive', false)
|
||
|
|
->get(['id', 'name', 'mobile'])
|
||
|
|
->map(fn (Customer $c) => [
|
||
|
|
'id' => (int) $c->id,
|
||
|
|
'name' => $c->name,
|
||
|
|
'mobile' => $c->mobile,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param array<int, string> $segmentKeys
|
||
|
|
*/
|
||
|
|
public function buildAudienceLabel(string $audienceType, array $segmentKeys = [], int $recipientsCount = 0): string
|
||
|
|
{
|
||
|
|
return match ($audienceType) {
|
||
|
|
self::TYPE_ALL => 'همه کاربران',
|
||
|
|
self::TYPE_SEGMENT => collect($segmentKeys)
|
||
|
|
->map(fn ($k) => $this->segmentLabel((string) $k))
|
||
|
|
->filter()
|
||
|
|
->implode(' + ') ?: 'گروهی',
|
||
|
|
self::TYPE_CUSTOMERS => $recipientsCount <= 1
|
||
|
|
? 'تکی'
|
||
|
|
: ('تکی / چندنفره (' . $recipientsCount . ' نفر)'),
|
||
|
|
default => $audienceType,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return array<int, array{id: int, name: ?string, mobile: ?string}>
|
||
|
|
*/
|
||
|
|
private function customersForSegment(string $key): array
|
||
|
|
{
|
||
|
|
$base = Customer::query()
|
||
|
|
->where('type', 2)
|
||
|
|
->where('is_archive', false);
|
||
|
|
|
||
|
|
$query = match ($key) {
|
||
|
|
'all_household' => $base,
|
||
|
|
'no_login' => (clone $base)->whereNull('last_seen_at'),
|
||
|
|
'inactive_7d' => (clone $base)->where(function ($q) {
|
||
|
|
$q->whereNull('last_seen_at')
|
||
|
|
->orWhere('last_seen_at', '<', now()->subDays(7));
|
||
|
|
}),
|
||
|
|
'inactive_30d' => (clone $base)->where(function ($q) {
|
||
|
|
$q->whereNull('last_seen_at')
|
||
|
|
->orWhere('last_seen_at', '<', now()->subDays(30));
|
||
|
|
}),
|
||
|
|
'no_valid' => (clone $base)->where('is_legal', 0),
|
||
|
|
'active_wallet' => (clone $base)->whereIn('id', function ($q) {
|
||
|
|
$q->select('owner')->from('wallets');
|
||
|
|
}),
|
||
|
|
default => null,
|
||
|
|
};
|
||
|
|
|
||
|
|
if ($query === null) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
return $query->get(['id', 'name', 'mobile'])->map(fn (Customer $c) => [
|
||
|
|
'id' => (int) $c->id,
|
||
|
|
'name' => $c->name,
|
||
|
|
'mobile' => $c->mobile,
|
||
|
|
])->all();
|
||
|
|
}
|
||
|
|
}
|