HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/ai.naniguide.com/app/Model/LocalCustomer.php
<?php

/**
 * Customer class.
 *
 * Model class for customer
 *
 * LICENSE: This product includes software developed at
 * the Acelle Co., Ltd. (http://acellemail.com/).
 *
 * @category   MVC Model
 *
 * @author     N. Pham <[email protected]>
 * @author     L. Pham <[email protected]>
 * @copyright  Acelle Co., Ltd
 * @license    Acelle Co., Ltd
 *
 * @version    1.0
 *
 * @link       http://acellemail.com
 */

namespace App\Model;

use Illuminate\Database\Eloquent\Model;
use App\Model\Customer as MasterCustomer;
use App\Library\Traits\HasModelCacheIdentity;
use App\Library\Cache\AppCache;
use App\Library\Cache\Cacheable;

class LocalCustomer extends Customer implements Cacheable
{
    use HasModelCacheIdentity;

    // protected $connection = 'set-it-to-a-dummy-value-to-avoid-falling-back-to-the-connection-of-the-caller-class';

    protected $table = 'customers';

    public function mailLists()
    {
        return $this->hasMany('App\Model\MailList', 'customer_id');
    }

    public function lists()
    {
        return $this->hasMany('App\Model\MailList', 'customer_id');
    }

    public function campaigns()
    {
        return $this->hasMany('App\Model\Campaign', 'customer_id');
    }

    public function adPlatforms()
    {
        return $this->hasMany('App\Model\AdPlatform', 'customer_id');
    }

    public function adCampaigns()
    {
        return $this->hasMany('App\Model\AdCampaign', 'customer_id');
    }

    public function adAudiences()
    {
        return $this->hasMany('App\Model\AdAudience', 'customer_id');
    }

    public function sentCampaigns()
    {
        return $this->hasMany('App\Model\Campaign', 'customer_id')->where('status', '=', 'done')->orderBy('created_at', 'desc');
    }

    public function subscribers()
    {
        return $this->hasManyThrough('App\Model\Subscriber', 'App\Model\MailList', 'customer_id');
    }

    public function trackingLogs()
    {
        return $this->hasMany('App\Model\TrackingLog', 'customer_id')->orderBy('created_at', 'asc');
    }

    public function automation2s()
    {
        return $this->hasMany('App\Model\Automation2', 'customer_id');
    }

    public function activeAutomation2s()
    {
        return $this->hasMany('App\Model\Automation2', 'customer_id')->where('status', Automation2::STATUS_ACTIVE);
    }

    /**
     * Customer-owned identities (LOCAL_DKIM server-less + identities they
     * registered against vendor-mode servers).
     */
    public function identities()
    {
        return $this->hasMany('App\Model\SendingIdentity', 'customer_id');
    }

    /**
     * Customer-owned LOCAL_DKIM domains (server-less, Acelle DKIM-signed).
     * Replaces the legacy `sendingDomains()` relation which mixed
     * server-less + server-attached domains.
     */
    public function localDkimIdentities()
    {
        return $this->identities()
            ->where('management_mode', \App\SendingServers\Identities\ManagementMode::LOCAL_DKIM->value);
    }

    // Only direct senders children
    public function senders()
    {
        return $this->hasMany('App\Model\Sender', 'customer_id');
    }

    // tracking domain
    public function trackingDomains()
    {
        return $this->hasMany('App\Model\TrackingDomain', 'customer_id');
    }

    public function products()
    {
        return $this->hasMany('App\Model\Product', 'customer_id');
    }

    public function forms()
    {
        return $this->hasMany('App\Model\Form', 'customer_id');
    }

    public function websites()
    {
        return $this->hasMany('App\Model\Website', 'customer_id');
    }

    public function sources()
    {
        return $this->hasmany('App\Model\Source', 'customer_id');
    }

    public function webhooks()
    {
        return $this->hasMany(Webhook::class, 'customer_id');
    }

    public function getVerifiedTrackingDomainOptions()
    {
        return $this->trackingDomains()->verified()->get()->map(function ($domain) {
            return ['value' => $domain->uid, 'text' => $domain->name];
        });
    }

    public function subscribersCount($cache = false)
    {
        if ($cache) {
            return AppCache::for($this)->read('SubscriberCount');
        }

        // return distinctCount($this->subscribers(), 'subscribers.email', 'distinct');
        return $this->subscribers()->count();
    }

    public function subscribersUsage($cache = false)
    {
        $max = $this->maxSubscribers();
        if ($max === '∞') {
            return 0;
        }

        // PHP 8.x: arithmetic on a string operand throws TypeError. maxSubscribers()
        // is typed string ('∞' | numeric string); cast to int once the infinity case
        // is handled.
        $maxInt = (int) $max;
        if ($maxInt === 0) {
            return 0;
        }

        $count = $this->subscribersCount($cache);
        if ($count > $maxInt) {
            return 100;
        }

        return round((($count / $maxInt) * 100), 2);
    }

    /**
     * Calculate subscibers usage.
     *
     * @return number
     */
    public function displaySubscribersUsage()
    {
        if ($this->maxSubscribers() == '∞') {
            return trans('messages.unlimited');
        }

        // @todo: avoid using cached value in a function
        //        cache value must be called directly from view only
        return AppCache::for($this)->read('SubscriberUsage', 0).'%';
    }

    public function listsCount()
    {
        return $this->lists()->count();
    }

    public function campaignsCount()
    {
        return $this->campaigns()->count();
    }

    public function newProductSource($type)
    {
        $class = '\\App\\Model\\' . $type;
        $source = new $class();
        $source->customer_id = $this->id;
        $source->type = $type;

        return $source;
    }

    public function findProductSource($type)
    {
        $source = $this->sources()
            ->where('type', '=', $type)
            ->first();

        if (!$source) {
            $source = new Source();
            $source->customer_id = $this->id;
            $source->type = $type;
        }

        return $source;
    }

    public function getConnectedWebsiteSelectOptions($long = true)
    {
        $query = $this->websites()->connected();

        $result = $query->orderBy('title')->get()->map(function ($item) use ($long) {
            if ($long) {
                return ['value' => $item->uid, 'text' => '<span class="fw-600">' . $item->title . '</span><br><span class="text-muted">' . $item->url . '</span>'];
            } else {
                return ['value' => $item->uid, 'text' => $item->title];
            }
        });

        return $result;
    }

    public function getSubscriberCountByStatus($status)
    {
        // @note: in this particular case, a simple count(distinct) query is much more efficient
        $query = $this->subscribers()->where('subscribers.status', $status)->distinct('subscribers.email');

        return $query->count();
    }

    public function getBounceFeedbackRate()
    {
        $delivery = $this->trackingLogs()->count();

        if ($delivery == 0) {
            return 0;
        }

        $bounce = \DB::table('bounce_logs')->leftJoin('tracking_logs', 'tracking_logs.message_id', '=', 'bounce_logs.message_id')->count();
        $feedback = \DB::table('feedback_logs')->leftJoin('tracking_logs', 'tracking_logs.message_id', '=', 'feedback_logs.message_id')->count();

        $percentage = ($feedback + $bounce) / $delivery;
    }

    public function sendingDomainsCount()
    {
        return $this->identities()
            ->where('kind', \App\SendingServers\Identities\IdentityKind::DOMAIN->value)
            ->count();
    }

    public function automationsCount()
    {
        return $this->automation2sCount();
    }

    public function automation2sCount()
    {
        return $this->automation2s()->count();
    }

    public function newProduct()
    {
        $product = new \App\Model\Product();
        $product->customer_id = $this->id;

        return $product;
    }

    public function newDefaultCampaign()
    {
        $campaign = Campaign::newDefault();
        $campaign->customer_id = $this->id;

        // default signature
        $defaultSignature = $this->signatures()->isDefault()->first();
        if ($defaultSignature) {
            $campaign->signature_id = $defaultSignature->id;
        }

        return $campaign;
    }

    public function createAbandonedEmailAutomation($store)
    {
        $auto = $this->automation2s()->create([
            'name' => 'Abandoned Cart Notification - Auto',
            'mail_list_id' => $store->getList()->id,
            'status' => 'inactive',
        ]);

        $email = new \App\Model\AutomationEmail([
            'action_id' => '1000000001',
        ]);
        $email->customer_id = $this->id;
        $email->automation2_id = $auto->id;
        $email->save();

        $auto->data = json_encode([
            [
                "title" => "Abandoned Cart Reminder",
                "id" => "trigger",
                "type" => "ElementTrigger",
                "child" => "1000000001",
                "options" => [
                    "key" => "woo-abandoned-cart",
                    "type" => "woo-abandoned-cart",
                    "source_uid" => $store->uid,
                    "wait" => "24_hour",
                    "init"  => "true"
                ]
            ], [
                "title" => "Hey, you have an item left in cart",
                "id" => "1000000001",
                "type" => "ElementAction",
                "child" => null,
                "options" => [
                    "init" => "true",
                    "email_uid" => $email->uid
                ]
            ]
        ]);

        $auto->save();

        return $auto;
    }

    public function getAbandonedEmailAutomation($store)
    {
        $auto = $this->automation2s()->where('mail_list_id', '=', $store->mail_list_id)->first();
        if (!$auto) {
            $auto = $this->createAbandonedEmailAutomation($store);
        }
        return $auto;
    }

    public function getSelectOptions($type = null)
    {
        $query = $this->sources();

        if ($type) {
            $query = $query->where('type', '=', $type);
        }

        return $query->get()->map(function ($source) {
            return ['text' => $source->getData()['data']['name'], 'value' => $source->uid];
        });
    }

    public static function subscribersCountByTime($begin, $end, $customer_id = null, $list_id = null, $status = null)
    {
        $query = \App\Model\Subscriber::leftJoin('mail_lists', 'mail_lists.id', '=', 'subscribers.mail_list_id')
                                ->leftJoin('customers', 'customers.id', '=', 'mail_lists.customer_id');

        if (isset($list_id)) {
            $query = $query->where('subscribers.mail_list_id', '=', $list_id);
        }
        if (isset($customer_id)) {
            $query = $query->where('customers.id', '=', $customer_id);
        }
        if (isset($status)) {
            $query = $query->where('subscribers.status', '=', $status);
        }

        $query = $query->where('subscribers.created_at', '>=', $begin)
                        ->where('subscribers.created_at', '<=', $end);

        return $query->count();
    }

    public function getMailListSelectOptions($options = [], $cache = false)
    {
        $query = $this->mailLists();

        # Other list
        if (isset($options['other_list_of'])) {
            $query->where('id', '!=', $options['other_list_of']);
        }

        $result = $query->orderBy('name')->get()->map(function ($item) use ($cache) {
            return ['id' => $item->id, 'value' => $item->uid, 'text' => $item->name.' ('.$item->subscribersCount($cache).' '.strtolower(trans('messages.subscribers')).')'];
        });

        return $result;
    }

    /**
     * Update Campaign cached data.
     */
    public function cacheManifest(): array
    {
        return [
            // @note: SubscriberCount must come first as its value shall be used by the others
            'SubscriberCount' => function () {
                return $this->subscribersCount(false);
            },
            'SubscriberUsage' => function () {
                return $this->subscribersUsage(true);
            },
            'SubscribedCount' => function () {
                return $this->getSubscriberCountByStatus(Subscriber::STATUS_SUBSCRIBED);
            },
            'UnsubscribedCount' => function () {
                return $this->getSubscriberCountByStatus(Subscriber::STATUS_UNSUBSCRIBED);
            },
            'UnconfirmedCount' => function () {
                return $this->getSubscriberCountByStatus(Subscriber::STATUS_UNCONFIRMED);
            },
            'BlacklistedCount' => function () {
                return $this->getSubscriberCountByStatus(Subscriber::STATUS_BLACKLISTED);
            },
            'SpamReportedCount' => function () {
                return $this->getSubscriberCountByStatus(Subscriber::STATUS_SPAM_REPORTED);
            },
            'MailListSelectOptions' => function () {
                return $this->getMailListSelectOptions([], true);
            },
            'Bounce/FeedbackRate' => function () {
                return $this->getBounceFeedbackRate();
            },
            'SentByDay_7' => [
                'compute' => fn () => $this->computeSentByDay(7),
                'ttl' => 604800,
            ],
            'SentByDay_30' => [
                'compute' => fn () => $this->computeSentByDay(30),
                'ttl' => 604800,
            ],
            'SentByDay_90' => [
                'compute' => fn () => $this->computeSentByDay(90),
                'ttl' => 604800,
            ],
            'EmailMetrics_7' => [
                'compute' => fn () => $this->computeEmailMetrics(7),
                'ttl' => 3600,
            ],
            'TopCountries_5' => [
                'compute' => fn () => $this->computeTopCountries(5),
                'ttl' => 3600,
            ],
        ];
    }

    /**
     * Aggregate sent/open/click/bounce metrics for the last $days, scoped to this customer.
     *
     * @return array{total_sent: int, open_rate: float, click_rate: float, bounce_rate: float}
     */
    private function computeEmailMetrics(int $days): array
    {
        $since = \Illuminate\Support\Carbon::now()->subDays($days);
        $customerId = $this->id;

        $totalSent = TrackingLog::where('customer_id', $customerId)
            ->where('status', TrackingLog::STATUS_SENT)
            ->where('created_at', '>=', $since)
            ->count();

        $openRate = 0.0;
        $clickRate = 0.0;
        $bounceRate = 0.0;

        if ($totalSent > 0) {
            $sentMessages = TrackingLog::where('customer_id', $customerId)
                ->where('status', TrackingLog::STATUS_SENT)
                ->where('created_at', '>=', $since)
                ->select('message_id');

            $uniqueOpens = \DB::table('open_logs')
                ->whereIn('message_id', $sentMessages)
                ->distinct()
                ->count('message_id');

            $uniqueClicks = \DB::table('click_logs')
                ->whereIn('message_id', $sentMessages)
                ->distinct()
                ->count('message_id');

            $totalBounces = \DB::table('bounce_logs')
                ->whereIn('message_id', $sentMessages)
                ->count();

            $openRate = round(($uniqueOpens / $totalSent) * 100, 1);
            $clickRate = round(($uniqueClicks / $totalSent) * 100, 1);
            $bounceRate = round(($totalBounces / $totalSent) * 100, 1);
        }

        return [
            'total_sent' => $totalSent,
            'open_rate' => $openRate,
            'click_rate' => $clickRate,
            'bounce_rate' => $bounceRate,
        ];
    }

    /**
     * Top $limit countries by unique opens on this customer's most recent sent campaign.
     *
     * @return array<int, array{name: string, code: string, flag: string, count: int}>
     */
    private function computeTopCountries(int $limit): array
    {
        $lastCampaign = $this->sentCampaigns()->first();

        if (!$lastCampaign || intval(AppCache::for($lastCampaign)->read('UniqOpenCount', 0)) <= 0) {
            return [];
        }

        try {
            return $lastCampaign->topOpenCountries($limit)->get()->map(function ($item) {
                $code = strtoupper($item->country_code ?? '');
                $flag = '';
                if (strlen($code) === 2) {
                    $flag = mb_chr(0x1F1E6 + ord($code[0]) - ord('A')) . mb_chr(0x1F1E6 + ord($code[1]) - ord('A'));
                }
                return [
                    'name' => $item->country_name ?? 'Unknown',
                    'code' => $code,
                    'flag' => $flag,
                    'count' => (int) ($item->aggregate ?? 0),
                ];
            })->all();
        } catch (\Exception $e) {
            return [];
        }
    }

    /**
     * Aggregate TrackingLog sent counts per day for the last $days,
     * scoped to this customer. Single GROUP BY query.
     *
     * @return array<int, array{date: string, sent: int}>
     */
    private function computeSentByDay(int $days): array
    {
        $start = \Illuminate\Support\Carbon::now()->subDays($days - 1)->startOfDay();

        $rows = TrackingLog::where('customer_id', $this->id)
            ->where('status', TrackingLog::STATUS_SENT)
            ->where('created_at', '>=', $start)
            ->selectRaw('DATE(created_at) as day, COUNT(*) as c')
            ->groupBy('day')
            ->pluck('c', 'day');

        $interval = $days <= 30 ? 1 : 3;
        $data = [];
        for ($i = $days - 1; $i >= 0; $i -= $interval) {
            $date = \Illuminate\Support\Carbon::now()->subDays($i);
            $data[] = [
                'date' => $date->format('M d'),
                'sent' => (int) ($rows[$date->toDateString()] ?? 0),
            ];
        }

        return $data;
    }

    public function master()
    {
        return MasterCustomer::on('mysql')->where('uid', $this->uid)->first();
    }

    public static function sync(MasterCustomer $masterCustomer)
    {

        if (!$masterCustomer->hasLocalDb()) {
            throw new \Exception("Master customer #{$masterCustomer->id} '{$masterCustomer->name}' does not have a local db configured in 'db_connection'");
        }

        $localCustomer = static::on($masterCustomer->db_connection)->find($masterCustomer->id);

        if (!is_null($localCustomer)) {
            throw new \Exception("Master customer #{$masterCustomer->id} '{$masterCustomer->name}' already has a local instance at '{$masterCustomer->db_connection}'");
        }

        $localCustomer = new static();
        $localCustomer->id = $masterCustomer->id;
        $localCustomer->uid = $masterCustomer->uid;

        return $localCustomer->setConnection($masterCustomer->db_connection)->save();
    }

    public function newSignature()
    {
        $signature = Signature::newDefault();
        $signature->customer_id = $this->id;

        return $signature;
    }

    public function newDefaultAutomation2()
    {
        $automation = $this->automation2s()->make()->newDefault();
        $automation->customer_id = $this->id;

        return $automation;
    }

    public function sites()
    {
        return $this->hasMany(Site::class, 'customer_id');
    }

    public function newSite()
    {
        $site = $this->sites()->make()->newDefault();
        $site->customer_id = $this->id;

        return $site;
    }

    public function newWebhook()
    {
        $webhook = Webhook::newDefault();
        $webhook->customer_id = $this->id;

        return $webhook;
    }

    public function newHttpConfig()
    {
        $httpConfig = HttpConfig::newDefault();
        $httpConfig->customer_id = $this->id;

        return $httpConfig;
    }

    public function newCustomerEmailTemplate()
    {
        $customerEmailTemplate = CustomerEmailTemplate::newDefault();
        $customerEmailTemplate->customer_id = $this->id;

        return $customerEmailTemplate;
    }
}