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/hi.naniguide.com/app/Models/Customer.php
<?php

namespace App\Models;

use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Auth\MustVerifyEmail as MustVerifyEmailTrait;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Storage;
use Laravel\Sanctum\HasApiTokens;

class Customer extends Authenticatable implements MustVerifyEmail, CanResetPasswordContract
{
    use HasApiTokens, HasFactory, Notifiable, SoftDeletes, MustVerifyEmailTrait, CanResetPassword;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'first_name',
        'last_name',
        'email',
        'google_id',
        'password',
        'timezone',
        'language',
        'status',
        'company_name',
        'phone',
        'address',
        'city',
        'state',
        'zip_code',
        'country',
        'currency',
        'quota',
        'quota_usage',
        'max_lists',
        'max_subscribers',
        'max_campaigns',
        'monthly_sending_limit',
        'daily_sending_limit',
        'max_campaigns_per_day',
        'welcome_campaign',
        'auto_tagging_rules',
        'plan_id',
        'renewal_type',
        'expires_at',
        'stripe_customer_id',
        'avatar_path',
        'bio',
        'website_url',
        'twitter_url',
        'facebook_url',
        'linkedin_url',
        'billing_address',
        'tax_id',
        'referred_by_affiliate_id',
        'referred_at',
        'openai_api_key',
        'gemini_api_key',
        'ai_token_usage',
        'ai_image_credits_used',
        'ai_own_daily_limit',
        'ai_own_monthly_limit',
        'ai_own_daily_usage',
        'ai_own_daily_usage_date',
        'ai_own_monthly_usage',
        'ai_own_monthly_usage_month',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
        'openai_api_key',
        'gemini_api_key',
    ];

    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'quota' => 'decimal:2',
            'quota_usage' => 'decimal:2',
            'expires_at' => 'date',
            'monthly_sending_limit' => 'integer',
            'daily_sending_limit' => 'integer',
            'max_campaigns_per_day' => 'integer',
            'welcome_campaign' => 'boolean',
            'auto_tagging_rules' => 'array',
            'last_login_at' => 'datetime',
            'billing_address' => 'array',
            'referred_at' => 'datetime',
            'openai_api_key' => 'encrypted',
            'gemini_api_key' => 'encrypted',
            'ai_token_usage' => 'integer',
            'ai_image_credits_used' => 'integer',
            'ai_own_daily_limit' => 'integer',
            'ai_own_monthly_limit' => 'integer',
            'ai_own_daily_usage' => 'integer',
            'ai_own_daily_usage_date' => 'date',
            'ai_own_monthly_usage' => 'integer',
        ];
    }

    /**
     * Get the customer's full name.
     */
    public function getFullNameAttribute(): string
    {
        return "{$this->first_name} {$this->last_name}";
    }

    /**
     * Get the customer's avatar URL.
     */
    public function getAvatarUrlAttribute(): ?string
    {
        $path = trim((string) ($this->avatar_path ?? ''));

        if ($path === '') {
            return null;
        }

        if (filter_var($path, FILTER_VALIDATE_URL)) {
            return $path;
        }

        $path = ltrim($path, '/');

        try {
            if (!Storage::disk('public')->exists($path)) {
                return null;
            }
        } catch (\Throwable $e) {
            return null;
        }

        $root = null;

        try {
            $root = request()->root();
        } catch (\Throwable $e) {
            $root = config('app.url');
        }

        $root = rtrim((string) $root, '/');

        if ($root === '') {
            return null;
        }

        $prefix = '';

        try {
            $prefix = \App\Models\Setting::get('storage_url_prefix', '');
        } catch (\Throwable $e) {
            $prefix = '';
        }

        $prefix = trim((string) $prefix, '/');

        return $root . ($prefix !== '' ? '/' . $prefix : '') . '/storage/' . $path;
    }

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

    /**
     * Check if customer account has expired.
     */
    public function isExpired(): bool
    {
        return $this->expires_at && $this->expires_at->isPast();
    }

    /**
     * Get actual quota usage (calculated from sent emails this month).
     */
    public function getActualQuotaUsageAttribute(): float
    {
        $periodStart = now()->copy()->startOfMonth()->startOfDay();
        $periodEnd = now()->copy()->endOfMonth()->endOfDay();

        $sentCount = \App\Models\CampaignRecipient::query()
            ->whereNotNull('sent_at')
            ->whereBetween('sent_at', [$periodStart, $periodEnd])
            ->whereIn('status', ['sent', 'opened', 'clicked'])
            ->whereHas('campaign', function ($q) {
                $q->where('customer_id', $this->id);
            })
            ->count();

        return (float) $sentCount;
    }

    /**
     * Get remaining quota.
     */
    public function getRemainingQuotaAttribute(): float
    {
        return max(0, $this->quota - $this->actual_quota_usage);
    }

    /**
     * Check if customer has available quota.
     */
    public function hasQuota(float $amount = 0): bool
    {
        return $this->remaining_quota >= $amount;
    }

    /**
     * Get the customer groups that belong to the customer.
     */
    public function customerGroups(): BelongsToMany
    {
        return $this->belongsToMany(CustomerGroup::class, 'customer_customer_group')
            ->withTimestamps();
    }

    public function allocatedDeliveryServers(): BelongsToMany
    {
        return $this->belongsToMany(DeliveryServer::class, 'customer_delivery_server')
            ->withTimestamps();
    }

    /**
     * Get the affiliate account associated with the customer.
     */
    public function affiliate(): HasOne
    {
        return $this->hasOne(Affiliate::class);
    }

    /**
     * Check if customer belongs to a specific group.
     */
    public function hasCustomerGroup(string $groupName): bool
    {
        return $this->customerGroups()->where('name', $groupName)->exists();
    }

    /**
     * Check if customer has a specific permission.
     */
    public function hasPermission(string $permission): bool
    {
        foreach ($this->permissionAliases($permission) as $candidate) {
            if ($this->customerGroups()->whereJsonContains('permissions', $candidate)->exists()) {
                return true;
            }
        }

        return (bool) $this->groupSetting($permission, false);
    }

    /**
     * Get a merged view of the customer's group settings.
     *
     * Merge strategy:
     * - booleans: OR (any group enables)
     * - integers: max (highest allowed)
     * - strings: first non-empty
     * - arrays: merge recursively
     */
    public function groupSettings(): array
    {
        $this->syncPlanCustomerGroup();

        $groups = $this->relationLoaded('customerGroups')
            ? $this->customerGroups
            : $this->customerGroups()->get();

        $merged = [];

        foreach ($groups as $group) {
            $groupSettings = (array) ($group->settings ?? []);

            if (empty($groupSettings)) {
                $legacySettings = [
                    'sending_quota' => [
                        'monthly_quota' => (int) ($group->quota ?? 0),
                    ],
                    'lists' => [
                        'limits' => [
                            'max_lists' => (int) ($group->max_lists ?? 0),
                            'max_subscribers' => (int) ($group->max_subscribers ?? 0),
                        ],
                    ],
                    'campaigns' => [
                        'limits' => [
                            'max_campaigns' => (int) ($group->max_campaigns ?? 0),
                        ],
                    ],
                ];

                $perm = (array) ($group->permissions ?? []);

                if (in_array('servers.permissions.can_add_delivery_servers', $perm, true)) {
                    $legacySettings['servers']['permissions']['can_add_delivery_servers'] = true;
                }
                if (in_array('servers.permissions.can_use_system_servers', $perm, true)) {
                    $legacySettings['servers']['permissions']['can_use_system_servers'] = true;
                }
                if (in_array('servers.permissions.can_select_delivery_servers_for_campaigns', $perm, true)) {
                    $legacySettings['servers']['permissions']['can_select_delivery_servers_for_campaigns'] = true;
                }
                if (in_array('servers.permissions.can_add_bounce_servers', $perm, true)) {
                    $legacySettings['servers']['permissions']['can_add_bounce_servers'] = true;
                }
                if (in_array('domains.tracking_domains.can_manage', $perm, true)) {
                    $legacySettings['domains']['tracking_domains']['can_manage'] = true;
                }
                if (in_array('domains.sending_domains.can_manage', $perm, true)) {
                    $legacySettings['domains']['sending_domains']['can_manage'] = true;
                }

                $groupSettings = $legacySettings;
            }

            $merged = $this->mergeSettingsRecursive($merged, $groupSettings);
        }

        return $merged;
    }

    private function syncPlanCustomerGroup(): void
    {
        $subscription = $this->subscriptions()->latest()->first();

        if (
            !$subscription
            || !in_array($subscription->status, ['active', 'trialing', 'past_due'], true)
        ) {
            return;
        }

        $plan = $subscription->plan;

        $stripePriceId = $subscription->stripe_price_id;
        $stripeProductId = null;

        if (is_string($stripePriceId) && \Illuminate\Support\Str::startsWith($stripePriceId, 'prod_')) {
            $stripeProductId = $stripePriceId;
            $stripePriceId = null;
        }

        if (is_string($subscription->plan_id)) {
            if (!$stripePriceId && \Illuminate\Support\Str::startsWith($subscription->plan_id, 'price_')) {
                $stripePriceId = $subscription->plan_id;
            }
            if (!$stripeProductId && \Illuminate\Support\Str::startsWith($subscription->plan_id, 'prod_')) {
                $stripeProductId = $subscription->plan_id;
            }
        }

        if (!$plan && $stripePriceId) {
            $plan = Plan::query()->where('stripe_price_id', $stripePriceId)->first();
        }

        if (!$plan && $stripeProductId) {
            $plan = Plan::query()->where('stripe_product_id', $stripeProductId)->first();
        }

        if (!$plan || !$plan->customer_group_id) {
            return;
        }

        $planGroupId = (int) $plan->customer_group_id;

        $planDerivedGroupIds = Plan::query()
            ->whereNotNull('customer_group_id')
            ->pluck('customer_group_id')
            ->map(fn ($id) => (int) $id)
            ->unique()
            ->values()
            ->all();

        $currentGroupIds = $this->customerGroups()->pluck('customer_groups.id')
            ->map(fn ($id) => (int) $id)
            ->all();

        $toDetach = array_values(array_diff(array_intersect($currentGroupIds, $planDerivedGroupIds), [$planGroupId]));
        if (!empty($toDetach)) {
            $this->customerGroups()->detach($toDetach);
        }

        $this->customerGroups()->syncWithoutDetaching([$planGroupId]);

        if ($this->relationLoaded('customerGroups')) {
            $this->unsetRelation('customerGroups');
        }
    }

    /**
     * Get a specific group setting value by dot path.
     */
    public function groupSetting(string $path, mixed $default = null): mixed
    {
        $settings = $this->groupSettings();

        foreach ($this->permissionAliases($path) as $candidate) {
            $value = data_get($settings, $candidate, '__mailpurse_missing__');
            if ($value !== '__mailpurse_missing__') {
                return $value;
            }
        }

        return $default;
    }

    /**
     * Check if any group allows/enables a feature.
     */
    public function groupAllows(string $path): bool
    {
        $subscription = $this->subscriptions()->latest()->first();
        $subscriptionAllowsFeature = $subscription
            && in_array($subscription->status, ['active', 'trialing', 'past_due'], true)
            && in_array($path, (array) ($subscription->features ?? []), true);

        return (bool) $this->groupSetting($path, false)
            || $this->hasPermission($path)
            || $subscriptionAllowsFeature;
    }

    /**
     * Get a numeric limit from group settings. If it's 0, treat as "unlimited" (returns null).
     */
    public function groupLimit(string $path): ?int
    {
        $value = (int) $this->groupSetting($path, 0);
        if ($value > 0) {
            return $value;
        }

        $groups = $this->relationLoaded('customerGroups')
            ? $this->customerGroups
            : $this->customerGroups()->get();

        $legacy = 0;
        switch ($path) {
            case 'sending_quota.monthly_quota':
                $legacy = (int) $groups->max(fn ($g) => (int) ($g->quota ?? 0));
                break;
            case 'workspaces.limits.max_workspaces':
                $legacy = (int) $groups->max(fn ($g) => (int) data_get($g->settings ?? [], 'workspaces.limits.max_workspaces', 0));
                break;
            case 'workspaces.limits.max_team_members':
                $legacy = (int) $groups->max(fn ($g) => (int) data_get($g->settings ?? [], 'workspaces.limits.max_team_members', 0));
                break;
            case 'workspaces.limits.max_email_lists':
                $legacy = (int) $groups->max(fn ($g) => (int) data_get($g->settings ?? [], 'workspaces.limits.max_email_lists', 0));
                break;
            case 'workspaces.limits.max_templates':
                $legacy = (int) $groups->max(fn ($g) => (int) data_get($g->settings ?? [], 'workspaces.limits.max_templates', 0));
                break;
            case 'workspaces.limits.max_campaigns':
                $legacy = (int) $groups->max(fn ($g) => (int) data_get($g->settings ?? [], 'workspaces.limits.max_campaigns', 0));
                break;
            case 'workspaces.limits.max_outreach_campaigns':
                $legacy = (int) $groups->max(fn ($g) => (int) data_get($g->settings ?? [], 'workspaces.limits.max_outreach_campaigns', 0));
                break;
            case 'workspaces.limits.max_inbox_accounts':
                $legacy = (int) $groups->max(fn ($g) => (int) data_get($g->settings ?? [], 'workspaces.limits.max_inbox_accounts', 0));
                break;
            case 'workspaces.limits.max_delivery_servers':
                $legacy = (int) $groups->max(fn ($g) => (int) data_get($g->settings ?? [], 'workspaces.limits.max_delivery_servers', 0));
                break;
            case 'workspaces.limits.max_bounce_servers':
                $legacy = (int) $groups->max(fn ($g) => (int) data_get($g->settings ?? [], 'workspaces.limits.max_bounce_servers', 0));
                break;
            case 'workspaces.limits.max_sending_domains':
                $legacy = (int) $groups->max(fn ($g) => (int) data_get($g->settings ?? [], 'workspaces.limits.max_sending_domains', 0));
                break;
            case 'workspaces.limits.max_tracking_domains':
                $legacy = (int) $groups->max(fn ($g) => (int) data_get($g->settings ?? [], 'workspaces.limits.max_tracking_domains', 0));
                break;
            case 'lists.limits.max_lists':
                $legacy = (int) $groups->max(fn ($g) => (int) ($g->max_lists ?? 0));
                break;
            case 'lists.limits.max_subscribers':
                $legacy = (int) $groups->max(fn ($g) => (int) ($g->max_subscribers ?? 0));
                break;
            case 'campaigns.limits.max_campaigns':
                $legacy = (int) $groups->max(fn ($g) => (int) ($g->max_campaigns ?? 0));
                break;
        }

        return $legacy > 0 ? $legacy : null;
    }

    /**
     * Abort (429) if the given current value would exceed a group limit.
     */
    public function enforceGroupLimit(string $limitPath, int $currentCount, string $message): void
    {
        $limit = $this->groupLimit($limitPath);

        if ($limit !== null && $currentCount >= $limit) {
            $customMessage = $this->groupSetting("messages.limits.{$limitPath}");
            if (is_string($customMessage) && trim($customMessage) !== '') {
                $message = $customMessage;
            }

            abort(429, $message);
        }
    }

    private function mergeSettingsRecursive(array $base, array $add): array
    {
        foreach ($add as $key => $value) {
            if (!array_key_exists($key, $base)) {
                $base[$key] = $value;
                continue;
            }

            $existing = $base[$key];

            if (is_array($existing) && is_array($value)) {
                $base[$key] = $this->mergeSettingsRecursive($existing, $value);
                continue;
            }

            if (is_bool($existing) || is_bool($value)) {
                $base[$key] = (bool) $existing || (bool) $value;
                continue;
            }

            if (is_numeric($existing) && is_numeric($value)) {
                $base[$key] = max((int) $existing, (int) $value);
                continue;
            }

            // Keep first non-empty
            $base[$key] = ($existing !== null && $existing !== '') ? $existing : $value;
        }

        return $base;
    }

    private function permissionAliases(string $path): array
    {
        return match ($path) {
            'servers.permissions.can_create_delivery_servers' => [
                'servers.permissions.can_create_delivery_servers',
                'servers.permissions.can_add_delivery_servers',
            ],
            'servers.permissions.can_add_delivery_servers' => [
                'servers.permissions.can_add_delivery_servers',
                'servers.permissions.can_create_delivery_servers',
            ],
            'servers.permissions.can_access_delivery_servers' => [
                'servers.permissions.can_access_delivery_servers',
                'servers.permissions.can_add_delivery_servers',
            ],
            'servers.permissions.can_edit_delivery_servers' => [
                'servers.permissions.can_edit_delivery_servers',
                'servers.permissions.can_add_delivery_servers',
            ],
            'servers.permissions.can_delete_delivery_servers' => [
                'servers.permissions.can_delete_delivery_servers',
                'servers.permissions.can_add_delivery_servers',
            ],
            default => [$path],
        };
    }

    /**
     * Get the email lists that belong to the customer.
     */
    public function emailLists(): HasMany
    {
        return $this->hasMany(EmailList::class);
    }

    public function googleIntegrations(): HasMany
    {
        return $this->hasMany(GoogleIntegration::class);
    }

    public function googleIntegrationFor(string $service): ?GoogleIntegration
    {
        $service = strtolower(trim($service));
        if ($service === '') {
            return null;
        }

        if ($this->relationLoaded('googleIntegrations')) {
            return $this->googleIntegrations->firstWhere('service', $service);
        }

        return $this->googleIntegrations()->where('service', $service)->first();
    }

    /**
     * Get the campaigns that belong to the customer.
     */
    public function campaigns(): HasMany
    {
        return $this->hasMany(Campaign::class);
    }

    /**
     * Get the auto responders that belong to the customer.
     */
    public function autoResponders(): HasMany
    {
        return $this->hasMany(AutoResponder::class);
    }

    public function automations(): HasMany
    {
        return $this->hasMany(Automation::class);
    }

    /**
     * Get the tracking domains that belong to the customer.
     */
    public function trackingDomains(): HasMany
    {
        return $this->hasMany(TrackingDomain::class);
    }

    public function bounceServers(): HasMany
    {
        return $this->hasMany(BounceServer::class);
    }

    public function replyServers(): HasMany
    {
        return $this->hasMany(ReplyServer::class);
    }

    /**
     * Get the sending domains that belong to the customer.
     */
    public function sendingDomains(): HasMany
    {
        return $this->hasMany(SendingDomain::class);
    }
    public function deliveryServers(): HasMany
    {
        return $this->hasMany(DeliveryServer::class);
    }

    /**
     * Get workspaces owned by this customer.
     */
    public function ownedWorkspaces(): HasMany
    {
        return $this->hasMany(Workspace::class);
    }

    /**
     * Get workspace memberships for this customer.
     */
    public function workspaceMemberships(): HasMany
    {
        return $this->hasMany(WorkspaceMember::class);
    }

    /**
     * Get the transactional emails that belong to the customer.
     */
    public function transactionalEmails(): HasMany
    {
        return $this->hasMany(TransactionalEmail::class);
    }

    /**
     * Get the subscriptions that belong to the customer.
     */
    public function subscriptions(): HasMany
    {
        return $this->hasMany(Subscription::class);
    }

    /**
     * Get the active subscription for the customer.
     */
    public function activeSubscription(): HasMany
    {
        return $this->hasMany(Subscription::class)
            ->where('status', 'active')
            ->where(function ($query) {
                $query->whereNull('ends_at')
                    ->orWhere('ends_at', '>', now());
            });
    }
}