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/Customer.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
 *
 * @copyright  Acelle Co., Ltd
 * @license    Acelle Co., Ltd
 *
 * @version    1.0
 *
 * @link       http://acellemail.com
 */

namespace App\Model;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Carbon\Carbon;
use App\Model\Subscription;
use App\Model\Source;
use Exception;
use DB;
use App\Library\Facades\Billing;
use App\Library\OrderFulfillment\OrderItemTypes;
use App\Library\Traits\TrackJobs;
use App\Model\PaymentIntent;
use App\Jobs\ImportBlacklistJob;
use App\Library\Traits\HasUid;
use App\Model\AppNotification as AppNotificationModel;
use App\Notifications\AppNotification as AppNotificationEvent;
use App\Library\Facades\Hook;
use Illuminate\Notifications\Notifiable;
use App\Services\Plans\Entitlements\EntitlementKey;
use App\Services\Plans\Gates\EntitlementGate;
use App\Services\Plans\Quotas\QuotaKey;
use App\Services\Plans\Quotas\QuotasService;
use App\Services\Plans\RateLimits\RateLimitKey;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Customer extends Model
{
    use HasFactory;
    use TrackJobs;
    use HasUid;
    use Notifiable;

    protected static function newFactory()
    {
        return \Database\Factories\CustomerFactory::new();
    }

    protected $connection = 'mysql';

    protected $quotaTracker;

    // Plan status
    public const STATUS_INACTIVE = 'inactive';
    public const STATUS_ACTIVE = 'active';

    public const BASE_DIR = 'app/customers'; // storage/customers/000000
    public const PRODUCT_DIR = 'home/products';
    public const LOGS_DIR = 'home/logs/';

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'timezone', 'language_id', 'color_scheme', 'text_direction', 'menu_layout', 'theme_mode', 'app_tour_completed'
    ];

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

    public function getNewSubscription()
    {
        // ONLY one new subscription allowed
        $subscriptions = $this->subscriptions()->new();

        if ($subscriptions->count() > 1) {
            throw new Exception('There are 2 subscriptions of [new] status. Please clean up the DB');
        }

        return $subscriptions->first();
    }

    public function getLastCancelledOrEndedSubscription()
    {
        // Lấy subscription ended gần đây nhất. Null nếu chưa đăng ký bao giờ
        return $this->subscriptions()->cancelledOrEdned()->orderBy('created_at', 'desc')->first();
    }

    /**
     * @return BelongsTo<Contact, $this>
     */
    public function contact(): BelongsTo
    {
        return $this->belongsTo(Contact::class);
    }

    /**
     * @return HasMany<MailList, $this>
     */
    public function mailLists()
    {
        return $this->hasMany(MailList::class, 'customer_id');
    }

    /**
     * @return HasMany<User, $this>
     */
    public function users(): HasMany
    {
        return $this->hasMany(User::class);
    }

    public function attachments()
    {
        return $this->hasMany(Attachment::class)->where('type', Attachment::TYPE_ATTACHMENT);
    }

    // remove it after update cashier
    /**
     * @return HasOne<User, $this>
     */
    public function user(): HasOne
    {
        return $this->hasOne(User::class);
    }

    public function getFirstUserLegacy()
    {
        return $this->users()->first();
    }

    public function admin()
    {
        return $this->belongsTo('App\Model\Admin');
    }

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

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

    /**
     * @return BelongsTo<Language, $this>
     */
    public function language(): BelongsTo
    {
        return $this->belongsTo(Language::class);
    }

    public function activityLogs()
    {
        return $this->hasMany(ActivityLog::class, 'customer_id')->orderBy('created_at', 'desc');
    }

    /**
     * Direct notifications addressed to this customer (audience='direct').
     * Customer events are always direct — no shared-customers broadcast in
     * the current product (announcements / banners are a separate concern).
     */
    public function notifications()
    {
        return $this->morphMany(AppNotificationModel::class, 'notifiable')
            ->whereNull('dismissed_at')
            ->orderBy('created_at', 'desc');
    }

    public function unreadNotifications()
    {
        return $this->notifications()->whereNull('read_at');
    }

    /**
     * Inbox shape symmetric with Admin::inboxNotifications — direct rows for
     * this customer plus any audience='customers' / 'everyone' broadcasts.
     * Returns an Eloquent builder.
     */
    public function inboxNotifications()
    {
        return AppNotificationModel::query()
            ->where(function ($q) {
                $q->where(function ($d) {
                    $d->where('audience', AppNotificationEvent::AUDIENCE_DIRECT)
                      ->where('notifiable_type', $this->getMorphClass())
                      ->where('notifiable_id', $this->getKey());
                })->orWhere('audience', AppNotificationEvent::AUDIENCE_CUSTOMERS)
                  ->orWhere('audience', AppNotificationEvent::AUDIENCE_EVERYONE);
            })
            ->orderByDesc('created_at');
    }

    public function unreadInboxCount(): int
    {
        return $this->inboxNotifications()
            ->whereNull('dismissed_at')
            ->whereNull('read_at')
            ->count();
    }

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

    public function activeEmailVerificationServers()
    {
        return $this->emailVerificationServers()->where('status', '=', EmailVerificationServer::STATUS_ACTIVE);
    }

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

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

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

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

    // billing addresses
    public function billingAddresses()
    {
        return $this->hasMany('App\Model\BillingAddress', 'customer_id');
    }

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

    public function newBillingAddress()
    {
        $address = new \App\Model\BillingAddress();
        $address->customer_id = $this->id;
        return $address;
    }

    public function getDefaultBillingAddress()
    {
        return $this->billingAddresses()->first();
    }

    public function updateBillingInformationFromInvoice($invoice)
    {
        $billingAddress = $this->getDefaultBillingAddress();

        // has no address yet
        if (!$billingAddress) {
            $billingAddress = $this->newBillingAddress();
        }

        $billingAddress->fill([
            'first_name' => $invoice->billing_first_name,
            'last_name' => $invoice->billing_last_name,
            'address' => $invoice->billing_address,
            'email' => $invoice->billing_email,
            'phone' => $invoice->billing_phone,
            'country_id' => $invoice->billing_country_id,
        ]);

        $billingAddress->save();
    }

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

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

    public function getBasePath($path = null)
    {
        $base = storage_path(join_paths(self::BASE_DIR, $this->uid)); // storage/app/customers/000000/

        if (!\Illuminate\Support\Facades\File::exists($base)) {
            \Illuminate\Support\Facades\File::makeDirectory($base, 0777, true, true);
        }

        return join_paths($base, $path);
    }

    /**
     * Items per page.
     *
     * @var array
     */
    public static $itemsPerPage = 25;

    /**
     * Filter items.
     *
     * @return collect
     */
    public static function scopeFilter($query, $request)
    {
        // filters
        $filters = $request->all();
        if (!empty($filters)) {
        }

        // Admin filter
        if (!empty($request->admin_id)) {
            $query = $query->where('customers.admin_id', '=', $request->admin_id);
        }
    }

    /**
     * Search items.
     *
     * @return collect
     */
    public static function scopeSearch($query, $keyword)
    {
        $query = $query->select('customers.*')
            ->leftJoin('users', 'users.customer_id', '=', 'customers.id')
            ->distinct();

        // Keyword
        if (!empty(trim($keyword))) {
            foreach (explode(' ', trim($keyword)) as $keyword) {
                $query = $query->where(function ($q) use ($keyword) {
                    $q->orwhere('users.first_name', 'like', '%'.$keyword.'%')
                        ->orWhere('users.last_name', 'like', '%'.$keyword.'%')
                        ->orWhere('users.email', 'like', '%'.$keyword.'%');
                });
            }
        }
    }

    public function getCurrentActiveSubscription()
    {
        // only ONE
        $subscriptions = $this->subscriptions()->active();

        if ($subscriptions->count() > 1) {
            throw new Exception('There are 2 subscriptions of [active] status. Please clean up the DB');
        }

        return $subscriptions->first();
    }

    public function maxSubscribers(): string
    {
        return app(QuotasService::class)->display($this, QuotaKey::MAX_SUBSCRIBERS);
    }

    /**
     * Display-formatted sending rate limit (from plan_rate_limits SEND_EMAIL_RATE).
     * Returns '∞' if unlimited, or the limit int.
     */
    public function maxQuota()
    {
        $sub = $this->getCurrentActiveSubscription();
        if (!$sub) {
            return '∞';
        }
        $row = app(\App\Services\Plans\RateLimits\RateLimitsService::class)
            ->configForPlan($sub->plan, RateLimitKey::SEND_EMAIL_RATE);
        if ($row === null) {
            return '∞';
        }
        return (int) $row->limit_value;
    }

    /**
     * Get customer's color scheme.
     *
     * @return string
     */
    public function getColorScheme()
    {
        // Store mode support only sms theme
        if (config('app.store')) {
            return 'store';
        }

        if (!empty($this->color_scheme)) {
            return $this->color_scheme;
        } else {
            return \App\Model\Setting::get('frontend_scheme');
        }
    }

    /**
     * Color array.
     *
     * @return array
     */
    public static function colors($default)
    {
        return [
            ['value' => 'default', 'text' => trans('messages.system_default')],
            ['value' => 'blue', 'text' => trans('messages.blue')],
            ['value' => 'green', 'text' => trans('messages.green')],
            ['value' => 'brown', 'text' => trans('messages.brown')],
            ['value' => 'pink', 'text' => trans('messages.pink')],
            ['value' => 'grey', 'text' => trans('messages.grey')],
            ['value' => 'white', 'text' => trans('messages.white')],
        ];
    }

    /**
     * Disable customer.
     *
     * @return bool
     */
    public function disable()
    {
        $this->status = 'inactive';

        return $this->save();
    }

    /**
     * Enable customer.
     *
     * @return bool
     */
    public function enable()
    {
        $this->status = 'active';

        return $this->save();
    }

    /**
     * Get customer timezone.
     *
     * @return string
     */
    public function getTimezone()
    {
        return $this->timezone;
    }

    /**
     * Get customer language code.
     *
     * @return string
     */
    public function getLanguageCode()
    {
        return $this->language ? $this->language->code : null;
    }

    /**
     * Get customer language code.
     *
     * @return string
     */
    public function getLanguageCodeFull()
    {
        $region_code = $this->language->region_code ? strtoupper($this->language->region_code) : strtoupper($this->language->code);
        return $this->language ? ($this->language->code.'-'.$region_code) : null;
    }

    /**
     * Get customer select2 select options. Searches the customers.name column
     * directly — no JOIN to the users table.
     *
     * Filters out customers that already hold a NEW or ACTIVE subscription
     * (`Subscription::newOrActive()` scope) — the admin "New Subscription"
     * popup is the only consumer of this endpoint, and assigning a plan to a
     * customer with an active sub is rejected downstream by
     * SubscriptionManagementService::assignPlan() anyway. Filtering at the
     * source avoids surfacing customers the admin can't act on.
     *
     * Past bug: the previous query did `leftJoin('users', ...)` and filtered
     * by user fields (first_name, last_name, email) without `distinct`/`groupBy`,
     * which surfaced N duplicate rows per customer (one per associated user).
     * Since `customers.name` is the only label rendered in the dropdown anyway,
     * drop the join.
     *
     * @return string  JSON for select2 ({items: [{id, text}], more: bool})
     */
    public static function select2($request)
    {
        $data = ['items' => [], 'more' => true];
        $query = self::query();

        if (filled($request->q ?? null)) {
            $query->where('name', 'like', '%' . $request->q . '%');
        }

        // Read-all gate: admins without `readAll` only see their own customers.
        if (!$request->user()->admin->can('readAll', new \App\Model\Customer())) {
            $query->where('admin_id', '=', $request->user()->admin->id);
        }

        // Exclude customers that already have a NEW or ACTIVE subscription —
        // they can't accept a new one without first cancelling the existing.
        $query->whereDoesntHave('subscriptions', function ($q) {
            $q->newOrActive();
        });

        foreach ($query->orderBy('id', 'desc')->limit(20)->get() as $customer) {
            $data['items'][] = ['id' => $customer->uid, 'text' => $customer->name];
        }

        return json_encode($data);
    }

    /**
     * @return HasMany<SendingServer, $this>
     */
    public function sendingServers(): HasMany
    {
        return $this->hasMany(SendingServer::class, 'customer_id');
    }

    /**
     * Customers count by time.
     *
     * @return number
     */
    public static function customersCountByTime($begin, $end, $admin = null)
    {
        $query = \App\Model\Customer::select('customers.*');

        if (isset($admin) && !$admin->can('readAll', new \App\Model\Customer())) {
            $query = $query->where('customers.admin_id', '=', $admin->id);
        }

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

        return $query->count();
    }

    /**
     * Sending servers count.
     *
     * @var int
     */
    public function sendingServersCount()
    {
        return $this->sendingServers()->count();
    }

    /**
     * Get max sending server count.
     *
     * @var int
     */
    public function maxSendingServers(): string
    {
        return app(QuotasService::class)->display($this, QuotaKey::MAX_SENDING_SERVERS);
    }

    /**
     * Get max email verification server count.
     *
     * @var int
     */
    public function maxEmailVerificationServers(): string
    {
        return app(QuotasService::class)->display($this, QuotaKey::MAX_EMAIL_VERIFICATION_SERVERS);
    }

    /**
     * Calculate email verification server usage.
     *
     * @return number
     */
    public function emailVerificationServersUsage()
    {
        $max = $this->maxEmailVerificationServers();
        $count = $this->emailVerificationServersCount();

        if ($max == '∞') {
            return 0;
        }
        if ($max == 0) {
            return 0;
        }
        if ($count > $max) {
            return 100;
        }

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

    /**
     * Calculate email verigfication servers usage.
     *
     * @return number
     */
    public function displayEmailVerificationServersUsage()
    {
        if ($this->maxEmailVerificationServers() == '∞') {
            return trans('messages.unlimited');
        }

        return $this->emailVerificationServersUsage().'%';
    }

    /**
     * Calculate sending servers usage.
     *
     * @return number
     */
    public function sendingServersUsage()
    {
        $max = $this->maxSendingServers();
        $count = $this->sendingServersCount();

        if ($max == '∞') {
            return 0;
        }
        if ($max == 0) {
            return 0;
        }
        if ($count > $max) {
            return 100;
        }

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

    /**
     * Calculate sending servers usage.
     *
     * @return number
     */
    public function displaySendingServersUsage()
    {
        if ($this->maxSendingServers() == '∞') {
            return trans('messages.unlimited');
        }

        return $this->sendingServersUsage().'%';
    }

    /**
     * Get max sending server count.
     *
     * @var int
     */
    public function maxSendingDomains(): string
    {
        return app(QuotasService::class)->display($this, QuotaKey::MAX_SENDING_DOMAINS);
    }

    /**
     * Get all customer active sending servers.
     *
     * @return HasMany<SendingServer, $this>
     */
    public function activeSendingServers()
    {
        return $this->sendingServers()->where('status', '=', \App\Model\SendingServer::STATUS_ACTIVE);
    }

    /**
     * Server pool the customer can send through, as a `[server_id => fitness]`
     * map suitable for `RouletteWheel::take()`. Replaces the legacy
     * MailList-wrapper lookup — server resolution now goes through Customer
     * (own servers) or Plan (admin-managed pool) only.
     *
     * Case A (`HAS_OWN_SENDING_SERVER` entitlement granted) →
     *   customer's own active SendingServers, fitness 100 each.
     * Case B (entitlement not granted) →
     *   plan's `plans_sending_servers` rows with their stored fitness.
     * No subscription / no plan → empty.
     *
     * @return array<int,int>
     */
    public function getSendingServerPool(): array
    {
        if (\App\Services\Plans\Gates\EntitlementGate::allows(
            $this,
            \App\Services\Plans\Entitlements\EntitlementKey::HAS_OWN_SENDING_SERVER
        )) {
            return $this->activeSendingServers()->get()
                ->mapWithKeys(fn ($s) => [(int) $s->id => 100])
                ->all();
        }

        $subscription = $this->getCurrentActiveSubscription();
        if (!$subscription || !$subscription->plan) {
            return [];
        }

        return $subscription->plan->activeSendingServers()->get()
            ->mapWithKeys(fn ($r) => [(int) $r->sending_server_id => (int) $r->fitness])
            ->all();
    }

    /**
     * Pick one SendingServer from the customer's pool via RouletteWheel.
     * Returns null when the pool is empty — callers (e.g. MailList::send)
     * must throw a domain-specific error so the failure surfaces clearly.
     */
    public function pickSendingServer(): ?\App\Model\SendingServer
    {
        $pool = $this->getSendingServerPool();
        if (empty($pool)) {
            return null;
        }
        $id = \App\Library\RouletteWheel::take($pool);
        return $id ? \App\Model\SendingServer::find($id) : null;
    }

    /**
     * Check if customer is disabled.
     *
     * @return bool
     */
    public function isActive()
    {
        return $this->status == self::STATUS_ACTIVE;
    }

    /**
     * Get total file size usage.
     *
     * @return number
     */
    public function totalUploadSize()
    {
        return \App\Library\Tool::getDirectorySize(base_path('public/source/'.$this->uid)) / 1048576;
    }

    /**
     * Get max upload size quota — display string ('∞' / 'Not included' / int).
     */
    public function maxTotalUploadSize(): string
    {
        return app(QuotasService::class)->display($this, QuotaKey::MAX_UPLOAD_SIZE_TOTAL);
    }

    /**
     * Storage usage percentage (0..100). Encapsulates 4-state limit semantic:
     *   - not granted (false) or exhausted (0) → 100
     *   - unlimited (null) → 0
     *   - capped (N>0) → used / N * 100
     */
    public function totalUploadSizeUsage(): float
    {
        $rawLimit = app(QuotasService::class)->limit($this, QuotaKey::MAX_UPLOAD_SIZE_TOTAL);
        return round(QuotasService::ratioFor((float) $this->totalUploadSize(), $rawLimit) * 100, 2);
    }

    /**
     * Custom can for customer.
     *
     * @return bool
     */
    public function can($action, $item)
    {
        return \Auth::user()->can($action, [$item, 'customer']);
    }

    /**
     * Email verification servers count.
     *
     * @var int
     */
    public function emailVerificationServersCount()
    {
        return $this->emailVerificationServers()->count();
    }

    /**
     * Get list of available email verification servers.
     *
     * @var bool
     */
    public function getEmailVerificationServers()
    {
        $subscription = $this->getCurrentActiveSubscription();
        if (!$subscription) {
            return collect();
        }

        if (EntitlementGate::allows($this, EntitlementKey::HAS_OWN_EMAIL_VERIFICATION_SERVER)) {
            return $this->activeEmailVerificationServers()->get()->map(function ($server) {
                return $server;
            });
            // If customer dont have permission creating sending servers
        } else {
            // Get server from the plan
            return  $subscription->plan->getEmailVerificationServers();
        }
    }

    /**
     * Get email verification servers select options.
     *
     * @return array
     */
    public function emailVerificationServerSelectOptions()
    {
        $servers = $this->getEmailVerificationServers();
        $options = [];
        foreach ($servers as $server) {
            $options[] = ['text' => $server->name, 'value' => $server->uid];
        }

        return $options;
    }

    /**
     * Get customer's sending servers type.
     *
     * @return array
     */
    public function getSendingServertypes()
    {
        // DriverRegistry::all() returns keys like 'amazon-api' / 'amazon-smtp'.
        // Entitlement granularity is at the vendor *family* level (one
        // ALLOW_SENDING_AMAZON covers both API and SMTP variants — see
        // EntitlementKey enum comments). Map every concrete type constant to
        // its family entitlement so no type silently falls through to null.
        $typeToEntitlement = [
            \App\Model\SendingServer::TYPE_AMAZON_API        => EntitlementKey::ALLOW_SENDING_AMAZON,
            \App\Model\SendingServer::TYPE_AMAZON_SMTP       => EntitlementKey::ALLOW_SENDING_AMAZON,
            \App\Model\SendingServer::TYPE_SENDGRID_API      => EntitlementKey::ALLOW_SENDING_SENDGRID,
            \App\Model\SendingServer::TYPE_SENDGRID_SMTP     => EntitlementKey::ALLOW_SENDING_SENDGRID,
            \App\Model\SendingServer::TYPE_MAILGUN_API       => EntitlementKey::ALLOW_SENDING_MAILGUN,
            \App\Model\SendingServer::TYPE_MAILGUN_SMTP      => EntitlementKey::ALLOW_SENDING_MAILGUN,
            \App\Model\SendingServer::TYPE_ELASTICEMAIL_API  => EntitlementKey::ALLOW_SENDING_ELASTICEMAIL,
            \App\Model\SendingServer::TYPE_ELASTICEMAIL_SMTP => EntitlementKey::ALLOW_SENDING_ELASTICEMAIL,
            \App\Model\SendingServer::TYPE_SPARKPOST_API     => EntitlementKey::ALLOW_SENDING_SPARKPOST,
            \App\Model\SendingServer::TYPE_SPARKPOST_SMTP    => EntitlementKey::ALLOW_SENDING_SPARKPOST,
            \App\Model\SendingServer::TYPE_BLASTENGINE_API   => EntitlementKey::ALLOW_SENDING_BLASTENGINE,
            \App\Model\SendingServer::TYPE_BLASTENGINE_SMTP  => EntitlementKey::ALLOW_SENDING_BLASTENGINE,
            \App\Model\SendingServer::TYPE_SMTP              => EntitlementKey::ALLOW_SENDING_SMTP,
            \App\Model\SendingServer::TYPE_SENDMAIL          => EntitlementKey::ALLOW_SENDING_SENDMAIL,
            \App\Model\SendingServer::TYPE_GMAIL_OAUTH       => EntitlementKey::ALLOW_SENDING_GMAIL_OAUTH,
        ];
        $allTypes = \App\SendingServers\DriverRegistry::all();
        $types = [];

        foreach ($allTypes as $type => $driverClass) {
            if (!isset($typeToEntitlement[$type])) {
                // Plugin-registered drivers may live outside the core
                // entitlement map. Allowed by default — admins gate plugins
                // through the plugin registry, not the customer entitlement
                // map. (Future: introduce a generic ALLOW_SENDING_PLUGIN
                // entitlement if per-plugin gating becomes a requirement.)
                $types[$type] = $driverClass;
                continue;
            }
            if (EntitlementGate::allows($this, $typeToEntitlement[$type])) {
                $types[$type] = $driverClass;
            }
        }

        if (!Setting::isYes('delivery.sendmail')) {
            unset($types['sendmail']);
        }

        return $types;
    }

    /**
     * Check customer can create sending servers type.
     *
     * @return bool
     */

    /**
     * Add email to customer's blacklist.
     */
    public function addEmaillToBlacklist($email)
    {
        $email = trim(strtolower($email));

        if (\App\Library\Tool::isValidEmail($email)) {
            $exist = $this->blacklists()->where('email', '=', $email)->count();
            if (!$exist) {
                $blacklist = new \App\Model\Blacklist();
                $blacklist->customer_id = $this->id;
                $blacklist->email = $email;
                $blacklist->save();
            }
        }
    }

    /**
     * Check if customer has api access.
     *
     * @return bool
     */

    // verifiedIdentitiesDroplist() removed — call
    // app(\App\SendingServers\Identities\IdentityCatalog::class)->droplistForCustomer(...)

    public function getLockPath($path)
    {
        return $this->getFirstUserLegacy()->getLockPath($path);
    }

    public function allowUnverifiedFromEmailAddress()
    {
        if (config('custom.sign_with_default_domain')) {
            // cho phép FROM EMAIL nào cũng được
            return true;
        }


        $server = null;
        $subscription = $this->getNewOrActiveSubscription();
        if (!$subscription) {
            return true;
        }
        $plan = $subscription->plan;

        if ($plan->useSystemSendingServer()) {
            $server = $plan->primarySendingServer();
        }

        return (is_null($server)) ? true : $server->allowUnverifiedFromEmailAddress();
    }

    // Get the current time in Customer timezone
    public function getCurrentTime()
    {
        return Carbon::now($this->timezone);
    }

    public function parseDateTime($datetime, $fallback = false)
    {
        // IMPORTANT: datetime string must NOT contain timezone information
        // IMPORTANT: passing [$this->timezone] is critically needed, to make sure this function works in console
        try {
            $dt = Carbon::parse($datetime, $this->timezone);
            $dt = $dt->timezone($this->timezone);
        } catch (\Exception $ex) {
            // parse special chars, for example ('sadfh&#($783943') ==> exception
            if ($fallback) {
                $dt = $this->parseDateTime('1900-01-01');
            } else {
                throw $ex;
            }
        }
        return $dt;
    }

    public function formatCurrentDateTime($name)
    {
        return $this->formatDateTime($this->getCurrentTime(), $name);
    }

    public function formatDateTime(Carbon $datetime, string $name)
    {
        // $name is a format name like: date_full | date_short
        // See config('localization')['*'] for the full list of available format names
        return format_datetime($datetime->timezone($this->getTimezone()), $name, $this->getLanguageCode());
    }

    // Do not call delete() directly on Customer
    // This method helps delete Customer account but KEEP
    // the associated User account if it is also associated with an Admin
    public function deleteAccount()
    {
        DB::transaction(function () {
            // Delete all customer users if user is not admin
            foreach ($this->users as $user) {
                if (!$user->admin) {
                    $user->deleteAndCleanup();
                }
            }

            $this->delete();
        });
    }

    public function copyTemplateAs(Template $template, $name)
    {
        $copy = $template->copy([
            'name' => $name,
            'customer_id' => $this->id
        ]);

        return $copy;
    }

    /**
     * Get builder templates.
     *
     * @return mixed
     */
    public function getBuilderTemplates()
    {
        $result = [];

        // Gallery — eager-load categories so callers (e.g. theme picker
        // popup) can partition by Base/Extended without triggering N+1.
        $templates = $this->customerEmailTemplates()->with('template.categories')->get()->map(function ($ct) {
            return $ct->template;
        });

        // merge collection
        $templates = $templates->merge(
            SystemEmailTemplate::with('template.categories')->get()->map(function ($ct) {
                return $ct->template;
            })
        );

        return $templates;
    }

    public function getFirstPaymentMethodThatCanAutoCharge()
    {
        return $this->paymentMethods()->where('can_auto_charge', true)->latest()->first();
    }

    public function importBlacklistJobs()
    {
        return $this->jobMonitors()->orderBy('job_monitors.id', 'DESC')->where('job_type', ImportBlacklistJob::class);
    }

    public function getMenuLayout()
    {
        return ($this->menu_layout == 'left' ? 'left' : 'top');
    }

    public function allowVerifyingOwnDomains()
    {
        $subscription = $this->getCurrentActiveSubscription();
        if (!$subscription) {
            return false;
        }
        $plan = $subscription->plan;
        if ($plan->useSystemSendingServer()) {
            $server = $plan->primarySendingServer();
        } else {
            return true;
        }

        if (!$server) {
            return false;
        }
        $driver = $server->driver();
        return $driver instanceof \App\SendingServers\Capabilities\AllowsLocalDomainVerify
            || $driver instanceof \App\SendingServers\Capabilities\SupportsRemoteDomainVerify;
    }

    public static function newCustomer()
    {
        $customer = new self();
        $customer->menu_layout = \App\Model\Setting::get('layout.menu_bar');
        $customer->status = self::STATUS_ACTIVE;

        return $customer;
    }

    public static function scopeByPlan($query, $plan)
    {
        $query = $query->whereHas('subscriptions', function ($q) use ($plan) {
            $q->newOrActive()->where('plan_id', '=', $plan->id);
        });
    }

    public function displayName()
    {
        return $this->name;
    }

    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;
    }

    /*
     *
     * FUNCTIONS THAT ARE DEPENDENT ON PLAN/SUBSCRIPTIONS
     *
     */

    public function getNewOrActiveSubscription()
    {
        $subscriptions = $this->subscriptions()->newOrActive();
        if ($subscriptions->count() > 1) {
            throw new Exception('There are 2 subscriptions of [active, new] status. Please clean up the DB');
        }

        return $subscriptions->first();
    }

    // getVerifiedIdentities() removed — call
    // app(\App\SendingServers\Identities\IdentityCatalog::class)->valuesForCustomer($this)

    /**
     * Check customer if has notice.
     */
    public function hasSubscriptionNotice()
    {
        $subscription = $this->getNewOrActiveSubscription();

        if (is_null($subscription)) {
            return false;
        }


        if ($subscription->getUnpaidOrder()) {
            return true;
        }

        return false;
    }

    public function emailVerificationCreditsOrders()
    {
        return $this->orders()->whereHas('orderItems', function ($q) {
            $q->where('order_items.type', OrderItemTypes::CREDIT_VERIFICATION_TOPUP);
        });
    }

    public function emailVerificationCreditsInvoices()
    {
        return $this->invoices()->whereHas('order', function ($q) {
            $q->whereHas('orderItems', function ($q) {
                $q->leftJoin('subscriptions', 'order_items.subscription_id', '=', 'subscriptions.id')
                    ->leftJoin('plans', 'subscriptions.plan_id', '=', 'plans.id')
                    ->where('order_items.type', OrderItemTypes::CREDIT_VERIFICATION_TOPUP);
            });
        });
    }

    public function emailVerificationCreditsPaymentIntents()
    {
        return PaymentIntent::whereHas('invoice', function ($q) {
            $q->where('customer_id', $this->id)->whereHas('order', function ($q) {
                $q->whereHas('orderItems', function ($q) {
                    $q->where('order_items.type', OrderItemTypes::CREDIT_VERIFICATION_TOPUP);
                });
            });
        });
    }

    public function getLastPaidEmailVerificationCreditsOrder()
    {
        return $this->emailVerificationCreditsOrders()
            ->paid()
            ->orderBy('created_at', 'DESC')
            ->first();
    }

    public function sendingCreditsOrders()
    {
        return $this->orders()->whereHas('orderItems', function ($q) {
            $q->where('type', OrderItemTypes::CREDIT_SENDING_TOPUP);
        });
    }

    public function sendingCreditsInvoices()
    {
        return $this->invoices()->whereHas('order', function ($q) {
            $q->whereHas('orderItems', function ($q) {
                $q->where('type', OrderItemTypes::CREDIT_SENDING_TOPUP);
            });
        });
    }

    public function sendingCreditsPaymentIntents()
    {
        return PaymentIntent::whereHas('invoice', function ($q) {
            $q->where('customer_id', $this->id)->whereHas('order', function ($q) {
                $q->whereHas('orderItems', function ($q) {
                    $q->where('type', OrderItemTypes::CREDIT_SENDING_TOPUP);
                });
            });
        });
    }

    public function getSendingCredits()
    {
        return $this->sendingCreditsOrders()->paid()->sum('sending_credits');
    }

    public function getLastPaidSendingCreditsOrderItem()
    {
        $lastOrder = $this->sendingCreditsOrders()
            ->paid()
            ->orderBy('created_at', 'DESC')
            ->first();

        return $lastOrder ? $lastOrder->orderItems->first() : null;
    }

    public static function findOrCreateCustomerByEmail($email)
    {
        $user = User::where('email', $email)->first();

        // Create new customer
        if (!$user) {
            // default infos
            $locale = app()->getLocale();
            $language = \App\Model\Language::where('code', '=', $locale)->first();
            $password = uniqid();

            // validation
            list($validator, $customer, $user) = app(\App\Services\AccountManagement\AccountProvisioningService::class)->createCustomer(
                // customer information
                $admin = null,
                $name = $email,
                $timezone = config('app.timezone'),
                $language_id = $language->id,
                // user information
                $email = $email,
                $password = $password,
                $passwordConfirmation = $password,
                $first_name = $email,
                $last_name = trans('messages.account.default_user'),
                $image = null,
                $role_uid = \App\Model\Role::getDefaultAdminRole()->uid
            );

            //  errors
            if (!$validator->errors()->isEmpty()) {
                return $validator->errors()->toArray();
            }
        } else {
            $customer = $user->customer;
        }

        return [$customer, $user];
    }

    public function newUser()
    {
        $user = User::newDefault();
        $user->customer_id = $this->id;
        return $user;
    }

    public function saveChatgptApiKey($key)
    {
        $this->openai_api_key = $key;
        $this->save();
    }

    public function getChatgptApiKey()
    {
        return $this->openai_api_key;
    }

    public function validateFromRequest($request)
    {
        // validation
        $validator = \Validator::make($request->all(), [
            'name' => 'required',
            'timezone' => 'required',
            'language_id' => 'required',
        ]);

        // errors
        if ($validator->fails()) {
            return $validator;
        }

        return $validator;
    }

    public function getName()
    {
        return $this->name ?: '[no organization name]';
    }

    public function getGoogleAccessToken()
    {
        if (!$this->google_auth) {
            return null;
        }
        return json_decode($this->google_auth, true)['access_token'];
    }

    /**
     * Will adding $addNumber subscribers to $mailList stay under both caps?
     * Respects 4-state limit union: false=blocked (not granted), null=unlimited,
     * int=cap. Returns true only if customer owns the list AND both dimensions
     * have room.
     */
    public function checkAddingSubscribersToMailListReachLimit($mailList, $addNumber): bool
    {
        if ($this->id != $mailList->customer_id) {
            return false;
        }

        $quotas     = app(QuotasService::class);
        $max        = $quotas->limit($this, QuotaKey::MAX_SUBSCRIBERS);
        $maxPerList = $quotas->limit($this, QuotaKey::MAX_SUBSCRIBERS_PER_LIST);

        $okFor = fn (int|null|false $limit, int $currentCount): bool => match (true) {
            $limit === false => false,                          // not granted
            $limit === null  => true,                            // unlimited
            default          => $currentCount + $addNumber <= $limit,
        };

        return $okFor($max, $this->subscribersCount())
            && $okFor($maxPerList, $mailList->subscribersCount());
    }

    public static function createCustomer(
        $name,
        $timezone,
        $language_id,
        $admin = null
    ) {
        // init
        $customer = static::newCustomer();

        // fill customer data
        $customer->name = $name;
        $customer->timezone = $timezone;
        $customer->language_id = $language_id;

        // rules
        $rules = [
            'name' => 'required',
            'timezone' => 'required',
            'language_id' => 'required',
        ];

        // validation
        $validator = \Validator::make([
            'name' => $name,
            'timezone' => $timezone,
            'language_id' => $language_id,
        ], $rules);

        // errors
        if ($validator->fails()) {
            return [$validator, $customer];
        }

        // SAVE CUSTOMER
        if ($admin) {
            $customer->admin_id = $admin->id;
        }
        $customer->save();

        // Execute registered hooks
        Hook::fire('customer_added', [$customer]);

        // success
        return [$validator, $customer];
    }

    public function updateCustomer(
        $name,
        $timezone,
        $language_id
    ) {
        // fill customer data
        $this->name = $name;
        $this->timezone = $timezone;
        $this->language_id = $language_id;

        // rules
        $rules = [
            'name' => 'required',
            'timezone' => 'required',
            'language_id' => 'required',
        ];

        // validation
        $validator = \Validator::make([
            'name' => $name,
            'timezone' => $timezone,
            'language_id' => $language_id,
        ], $rules);

        // errors
        if ($validator->fails()) {
            return [$validator, $this];
        }

        // SAVE CUSTOMER
        $this->save();

        // success
        return [$validator, $this];
    }


    public function addUser($user)
    {
        $user->customer_id = $this->id;
        $user->save();
    }

    public function hasLocalDb()
    {
        return !is_null($this->db_connection);
    }

    public function local()
    {
        if (config('sharding.enabled') && $this->hasLocalDb()) {
            $local = LocalCustomer::on($this->db_connection)->where('uid', $this->uid)->first();

            if (is_null($local)) {
                throw new Exception("Cannot find a local customer with UID '{$this->uid}' in '{$this->db_connection}' connection. Execute the 'createLocalInstance()' function to create one");
            }

            return $local;
        } else {

            // In case of NO SHARDING, simply return the same customer in the same connection, typecast it to a LocalCustomer;
            $local = LocalCustomer::on('mysql')->where('uid', $this->uid)->first();
            return $local;
        }
    }

    public function createLocalInstance()
    {
        return LocalCustomer::sync($this);
    }

    public function newMailList()
    {
        $list = new MailList();
        $list->customer_id = $this->id;

        return $list;
    }

    /**
     * Max user quota as a display string ('Not included' / '∞' / int).
     */
    public function getMaxUserQuota(): string
    {
        if (!$this->getCurrentActiveSubscription()) {
            return trans('messages.not_included');
        }
        return app(\App\Services\Plans\Quotas\QuotasService::class)
            ->display($this, \App\Services\Plans\Quotas\QuotaKey::MAX_USERS);
    }

    public function setUserDbConnection()
    {
        if (config('sharding.enabled')) {
            if ($this->hasLocalDb()) {
                \App\Helpers\set_userdb_connection($this->db_connection);
            }
        }
    }

    public function ownOrGlobalRoles()
    {
        return $this->roles()
            ->orWhere(function ($q) {
                $q->global();
            });
    }

    public function getSelectableRoles()
    {
        return $this->ownOrGlobalRoles()->get();
    }

    public static function mapKeyToCustomer($namespace, $key, $customerId)
    {
        $fullkey = static::getFullKey($namespace, $key);
        cache([$fullkey => $customerId], now()->addMonths(2));
    }

    // @tricky: if cannot find a mapping in cache, then try to extract from the key itself
    public static function getCustomerFromKey($namespace, $key)
    {
        $fullkey = static::getFullKey($namespace, $key);
        $customerId = cache($fullkey);
        $customer = static::find($customerId);

        // @trick here!
        if (is_null($customer)) {
            $customerUid = \App\Library\StringHelper::extractCustomerUidFromMessageId($key);

            if (!is_null($customerUid)) {
                $customer = \App\Model\Customer::findByUid($customerUid);
            }
        }

        return $customer;
    }

    private static function getFullKey($namespace, $key)
    {
        $connector = '_';
        $fullkey = "{$namespace}{$connector}{$key}";
        return $fullkey;
    }

    public function getDbConnection()
    {
        return $this->db_connection ?? 'mysql';
    }

    public function verificationSubscriptions()
    {
        return $this->subscriptions()->verification();
    }

    // Customer has only ONE Verification subscription which is NEW or ACTIVE
    public function getNewOrActiveVerificationSubscription()
    {
        $subscriptions = $this->verificationSubscriptions()->newOrActive();

        if ($subscriptions->count() > 1) {
            throw new Exception('There are 2 email verification subscriptions of [active, new] status. Please clean up the DB');
        }

        return $subscriptions->first();
    }

    public function assignVerificationPlan($plan)
    {
        // Put in transaction. Make sure senderID always with subscription
        $subscription = \DB::transaction(function () use ($plan) {
            if ($this->getNewOrActiveVerificationSubscription()) {
                throw new \Exception('Customer has more than ONE Verification subscription which is NEW or ACTIVE');
            }

            // allways create init subscription with init invoice (by design)
            $subscription = Subscription::createNewSubscription($this, $plan);

            if ($plan->hasTrial()) {
                $invoice = $subscription->createNewSubscriptionInvoiceWithTrial();
            } else {
                $invoice = $subscription->createNewSubscriptionInvoiceWithoutTrial();
            }

            // log
            SubscriptionFacade::log($subscription, SubscriptionLog::TYPE_SELECT_PLAN, $invoice->uid, [
                'plan' => $subscription->getPlanName(),
                'customer' => $subscription->getCustomerName(),
                'amount' => $invoice->total(),
            ]);

            return $subscription;
        });

        return $subscription;
    }

    public function getCurrentActiveVerificationSubscription()
    {
        if (!config('app.saas')) {
            throw new Exception('Operation not allowed in NON-SAAS mode');
        }

        // only ONE
        $subscriptions = $this->verificationSubscriptions()->active();

        if ($subscriptions->count() > 1) {
            throw new Exception('There are 2 subscriptions of [active] status. Please clean up the DB');
        }

        return $subscriptions->first();
    }

    public function getMsgInQueueKey()
    {
        return "message-in-queue-{$this->uid}";
    }

    public function incrementMsgInQueue()
    {
        $key = $this->getMsgInQueueKey();
        Cache::increment($key);
    }

    public function decrementMsgInQueue()
    {
        $key = $this->getMsgInQueueKey();
        $count = Cache::decrement($key);

        if ($count < 0) {
            Cache::set($key, 0);
        }
    }

    public function getMsgInQueue()
    {
        $key = $this->getMsgInQueueKey();
        return Cache::get($key);
    }

    public function clearMsgInQueue()
    {
        $key = $this->getMsgInQueueKey();
        Cache::forget($key);
    }

    public function calculateSendEmailDelayTime()
    {
        $subscription = $this->getCurrentActiveSubscription();

        $trackers = $subscription->getSendEmailRateTracker();


    }
}