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/Plan.php
<?php

/**
 * Plan — flat model (no polymorphism, no plans.options JSON, no plans.type).
 *
 * All plan capabilities live in the 4-concept tables:
 *   - plan_credits        (consumable cycle caps)
 *   - plan_quotas         (static resource limits)
 *   - plan_entitlements   (boolean capabilities, explicit enabled flag)
 *   - plan_rate_limits    (time-bounded throttle)
 *
 * See docs/payment-order-plan-subscription-saas/SUBSCRIPTION-COMPREHENSIVE-DESIGN.md.
 */

namespace App\Model;

use App\Library\Traits\HasUid;
use App\Services\Plans\Credits\CreditKey;
use App\Services\Plans\Entitlements\EntitlementKey;
use App\Services\Plans\Quotas\QuotaKey;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Plan extends Model
{
    use HasFactory;
    use HasUid;

    protected $connection = 'mysql';
    protected $table      = 'plans';
    protected $guarded    = [];

    public const STATUS_INACTIVE = 'inactive';
    public const STATUS_ACTIVE   = 'active';
    public const UNLIMITED_TIME  = 'unlimited';

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

    public static function findByUid($uid)
    {
        return self::where('uid', $uid)->first();
    }

    // =========================================================================
    // RELATIONS
    // =========================================================================

    public function admin()                   { return $this->belongsTo(Admin::class); }
    public function currency()                { return $this->belongsTo(Currency::class); }
    public function subscriptions()           { return $this->hasMany(Subscription::class, 'plan_id'); }
    public function activeSubscriptions()     { return $this->subscriptions()->active(); }
    public function remoteMappings()          { return $this->hasMany(PlanRemoteMapping::class, 'plan_id'); }
    public function plansSendingServers()     { return $this->hasMany(PlansSendingServer::class, 'plan_id'); }
    public function plansEmailVerificationServers() { return $this->hasMany(PlansEmailVerificationServer::class, 'plan_id'); }
    public function emailVerificationServers()
    {
        return $this->belongsToMany(
            EmailVerificationServer::class,
            'plans_email_verification_servers',
            'plan_id',
            'server_id'
        );
    }
    public function sendingServers()
    {
        return SendingServer::whereIn('id', $this->plansSendingServers()->pluck('sending_server_id')->toArray());
    }
    public function activeSendingServers()
    {
        return $this->plansSendingServers()
            ->join('sending_servers', 'sending_servers.id', 'plans_sending_servers.sending_server_id');
    }

    public function getRemoteMappingForGateway(PaymentGateway $gateway): ?PlanRemoteMapping
    {
        return $this->remoteMappings()->where('payment_gateway_id', $gateway->id)->first();
    }

    public function hasRemoteMappings(): bool
    {
        return $this->remoteMappings()->exists();
    }

    // =========================================================================
    // SCOPES
    // =========================================================================

    public function scopeActive($query)    { return $query->where('plans.status', self::STATUS_ACTIVE); }
    public function scopeAvailable($query) { return $query->where('plans.visible', true); }

    public static function scopeFilter($query, $request)
    {
        $query = $query->select('plans.*');
        if (!empty($request->admin_id)) {
            $query = $query->where('plans.admin_id', $request->admin_id);
        }
        return $query;
    }

    public static function scopeSearch($query, $keyword)
    {
        if (!empty(trim($keyword))) {
            foreach (explode(' ', trim($keyword)) as $k) {
                $query = $query->where(function ($q) use ($k) {
                    $q->orWhere('plans.name', 'like', "%{$k}%");
                });
            }
        }
        return $query;
    }

    // =========================================================================
    // STATE / STATUS
    // =========================================================================

    public function isActive(): bool  { return $this->status === self::STATUS_ACTIVE; }
    public function isFree(): bool    { return $this->price == 0; }
    public function isUnlimited(): bool { return $this->frequency_unit === self::UNLIMITED_TIME; }

    public function canRenew(): bool { return $this->price > 0 || $this->isFree(); }

    public function periodEndsAt($startDate)
    {
        return getPeriodEndsAt($startDate, $this->frequency_amount, $this->frequency_unit);
    }

    public function trialPeriodEndsAt($startDate)
    {
        return getPeriodEndsAt($startDate, $this->trial_amount, $this->trial_unit);
    }

    public function visibleOff() { $this['visible'] = 0; $this->save(); }
    public function visibleOn()  { $this['visible'] = true; $this->save(); }

    public function disable()
    {
        $this->status    = self::STATUS_INACTIVE;
        $this['visible'] = false;
        return $this->save();
    }

    public function enable()
    {
        $this->status = self::STATUS_ACTIVE;
        return $this->save();
    }

    public function isValid(): bool
    {
        if ($this->useSystemSendingServer() && !$this->hasPrimarySendingServer()) {
            return false;
        }
        return true;
    }

    public function checkStatus()
    {
        if (!$this->isValid()) {
            $this->disable();
            $this->visibleOff();
        } else {
            $this->enable();
        }
    }

    public function copy($name)
    {
        $copy = $this->replicate(['cache', 'last_error', 'run_at']);
        $copy->uid = uniqid();
        $copy->name = $name;
        $copy->created_at = \Carbon\Carbon::now();
        $copy->updated_at = \Carbon\Carbon::now();
        $copy->status = self::STATUS_ACTIVE;
        $copy->save();
        $copy->checkStatus();
    }

    // =========================================================================
    // GETTERS
    // =========================================================================

    public function getPrice()            { return $this->price; }
    public function getFormattedPrice()   { return \App\Library\Tool::format_price($this->price, $this->currency->format); }
    public function getFrequencyAmount()  { return $this->frequency_amount; }
    public function getFrequencyUnit()    { return $this->frequency_unit; }
    public function getTrialAmount()      { return $this->trial_amount; }
    public function getTrialUnit()        { return $this->trial_unit; }
    public function hasTrial(): bool      { return $this->trial_amount > 0; }
    public function subscriptionsCount()  { return $this->subscriptions()->count(); }
    public function customersCount()      { return $this->activeSubscriptions()->count(); }

    public function getTrialPeriodTimePhrase()
    {
        return trans_choice('messages.' . $this->getTrialUnit() . '.choice', $this->getTrialAmount(), [
            'amount' => $this->trial_amount,
        ]);
    }

    public function displayFrequencyTime()
    {
        return trans('messages.plan.period', [
            'amount' => number_with_delimiter($this->getFrequencyAmount()),
            'unit'   => trans('messages.' . \App\Library\Tool::getPluralPrase($this->getFrequencyUnit(), $this->getFrequencyAmount())),
        ]);
    }

    /**
     * Human-readable cap for a credit (4-state, per SUBSCRIPTION-COMPREHENSIVE-DESIGN §5.5):
     *   - no plan_credits row   → "Not included" (canUse returns false at runtime)
     *   - row + NULL amount     → "Unlimited"
     *   - row + int 0           → "0" (supported but exhausted — upsell prompt)
     *   - row + int > 0         → "10,000"
     */
    public function displayCredit(CreditKey $key): string
    {
        $amount = app(\App\Services\Plans\Credits\CreditsService::class)->creditForPlan($this, $key);
        if ($amount === false) return trans('messages.not_included');
        if ($amount === null)  return trans('messages.unlimited');
        return number_with_delimiter($amount); // includes 0 (supported but exhausted)
    }

    /**
     * Human-readable limit for a quota (4-state, per SUBSCRIPTION-COMPREHENSIVE-DESIGN §5.5):
     *   - no plan_quotas row → "Not included" (canCreate/canAccept returns false at runtime)
     *   - row + NULL limit   → "Unlimited"
     *   - row + int 0        → "0" (granted but zero allocation)
     *   - row + int > 0      → "5,000"
     */
    public function displayQuota(QuotaKey $key): string
    {
        $limit = app(\App\Services\Plans\Quotas\QuotasService::class)->limitForPlan($this, $key);
        if ($limit === false) return trans('messages.not_included');
        if ($limit === null)  return trans('messages.unlimited');
        return number_with_delimiter($limit); // includes 0
    }

    // =========================================================================
    // ENTITLEMENT-BACKED SENDING SERVER HELPERS
    // =========================================================================

    /**
     * Whether the plan allows customers to bring their own sending servers.
     * Backed by HAS_OWN_SENDING_SERVER entitlement (plan_entitlements table).
     */
    public function useOwnSendingServer(): bool
    {
        return $this->hasEntitlement(EntitlementKey::HAS_OWN_SENDING_SERVER);
    }

    /**
     * Whether the plan forces customers onto the system's primary sending server.
     * Inverse of useOwnSendingServer by convention.
     */
    public function useSystemSendingServer(): bool
    {
        return !$this->useOwnSendingServer();
    }

    private function hasEntitlement(EntitlementKey $key): bool
    {
        return app(\App\Services\Plans\Entitlements\EntitlementsService::class)->hasForPlan($this, $key);
    }

    public function hasPrimarySendingServer(): bool
    {
        return !is_null($this->primarySendingServer());
    }

    public function primarySendingServer()
    {
        if (!$this->useSystemSendingServer()) {
            throw new \Exception('ACELLE ERROR: 120000700392');
        }
        $pss = $this->plansSendingServers()->where('is_primary', true)->first();
        return $pss ? $pss->sendingServer : null;
    }

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

    public function addSendingServerByUid($uid)
    {
        $server = SendingServer::findByUid($uid);
        $row = new PlansSendingServer();
        $row->plan_id = $this->id;
        $row->sending_server_id = $server->id;
        $row->fitness = '50';
        if (!$this->plansSendingServers()->where('is_primary', true)->count()) {
            $row->is_primary = true;
        }
        $row->save();
    }

    public function removeSendingServerByUid($uid)
    {
        $server = SendingServer::findByUid($uid);
        $this->plansSendingServers()->where('sending_server_id', $server->id)->delete();
        $query = $this->plansSendingServers()->where('is_primary', true);
        if (!$query->count()) {
            $first = $this->plansSendingServers()->first();
            if ($first) {
                $first->is_primary = true;
                $first->save();
            }
        }
    }

    public function setPrimarySendingServer($uid)
    {
        $this->plansSendingServers()->update(['is_primary' => false]);
        $server = SendingServer::findByUid($uid);
        $this->plansSendingServers()->where('sending_server_id', $server->id)->update(['is_primary' => true]);
    }

    public function updateFitnesses($hash)
    {
        foreach ($hash as $uid => $value) {
            $server = SendingServer::findByUid($uid);
            PlansSendingServer::where('sending_server_id', $server->id)->update(['fitness' => $value]);
        }
    }

    public function updateSendingServers($servers)
    {
        $this->plansSendingServers()->delete();
        foreach ($servers as $key => $param) {
            if ($param['check']) {
                $server = SendingServer::findByUid($key);
                $row = new PlansSendingServer();
                $row->plan_id = $this->id;
                $row->sending_server_id = $server->id;
                $row->fitness = $param['fitness'];
                $row->save();
            }
        }
    }

    public function allowSenderVerification()
    {
        if (!$this->useSystemSendingServer()) {
            return true;
        }
        $server = $this->primarySendingServer();
        if (!$server) {
            return false;
        }
        $driver = $server->driver();
        return $driver instanceof \App\SendingServers\Capabilities\SupportsIdentitySync
            || $driver instanceof \App\SendingServers\Capabilities\AllowsLocalEmailVerify;
    }

    // =========================================================================
    // EMAIL VERIFICATION SERVER PLAN HELPERS
    // =========================================================================

    public function activePlansEmailVerificationServers()
    {
        return $this->plansEmailVerificationServers();
    }

    public function getEmailVerificationServers()
    {
        if ($this->hasEntitlement(EntitlementKey::HAS_ALL_EMAIL_VERIFICATION_SERVERS)) {
            return EmailVerificationServer::getAllAdminActive()->get()->map(fn ($s) => $s);
        }
        return $this->activePlansEmailVerificationServers()->get()->map(fn ($r) => $r->emailVerificationServer);
    }

    public function fillPlansEmailVerificationServers($params)
    {
        $this->plansEmailVerificationServers = collect([]);
        foreach ($params as $key => $param) {
            if ($param['check']) {
                $server = EmailVerificationServer::findByUid($key);
                $row = new PlansEmailVerificationServer();
                $row->plan_id   = $this->id;
                $row->server_id = $server->id;
                $this->plansEmailVerificationServers->push($row);
            }
        }
    }

    public function updateEmailVerificationServers($servers)
    {
        $this->plansEmailVerificationServers()->delete();
        foreach ($servers as $key => $param) {
            if ($param['check']) {
                $server = EmailVerificationServer::findByUid($key);
                $row = new PlansEmailVerificationServer();
                $row->plan_id   = $this->id;
                $row->server_id = $server->id;
                $row->save();
            }
        }
    }

    public function getItsOnlyEmailVerificationServer()
    {
        if ($this->emailVerificationServers()->count() > 1) {
            throw new \Exception("There are more than one email verification server of the plan {$this->name}");
        }
        return $this->emailVerificationServers()->first();
    }

    public function setItsOnlyEmailVerificationServer($server)
    {
        $this->emailVerificationServers()->detach();
        $this->emailVerificationServers()->attach($server->id);
    }

    // =========================================================================
    // STATIC FACTORIES / QUERIES
    // =========================================================================

    public static function newDefaultPlan()
    {
        $plan = new self([
            'price'            => 0,
            'frequency_amount' => 1,
            'frequency_unit'   => 'month',
            'trial_amount'     => 0,
            'trial_unit'       => 'day',
        ]);
        $plan->status = self::STATUS_INACTIVE;
        return $plan;
    }

    public static function getAvailablePlans()
    {
        return self::active()->available()->get();
    }

    public static function billingCycleValues(): array
    {
        return [
            'daily'   => ['frequency_amount' => 1, 'frequency_unit' => 'day'],
            'monthly' => ['frequency_amount' => 1, 'frequency_unit' => 'month'],
            'yearly'  => ['frequency_amount' => 1, 'frequency_unit' => 'year'],
        ];
    }

    public static function timeUnitOptions(): array
    {
        return [
            ['value' => 'day',   'text' => trans('messages.day')],
            ['value' => 'week',  'text' => trans('messages.week')],
            ['value' => 'month', 'text' => trans('messages.month')],
            ['value' => 'year',  'text' => trans('messages.year')],
        ];
    }

    public static function select2($request)
    {
        $data = ['items' => [], 'more' => true];

        $query = self::active();
        if (isset($request->q)) {
            $keyword = $request->q;
            $query = $query->where(function ($q) use ($keyword) {
                $q->orwhere('plans.name', 'like', '%' . $keyword . '%');
            });
        }

        if ($request->user()->admin && !$request->user()->admin->can('readAll', self::newDefaultPlan())) {
            $query = $query->where('plans.admin_id', $request->user()->admin->id);
        }

        if ($request->change_from_uid) {
            $plan = self::findByUid($request->change_from_uid);
            $query = $query->where('id', '<>', $request->change_from_uid);
            $query = $query->where('uid', '<>', $request->change_from_uid);
            $query = $query->where('frequency_amount', $plan->getFrequencyAmount());
            $query = $query->where('frequency_unit', $plan->getFrequencyUnit());
        }

        foreach ($query->limit(20)->get() as $plan) {
            $data['items'][] = [
                'id'   => $plan->uid,
                'text' => htmlspecialchars($plan->name) . '|||' . htmlspecialchars(\App\Library\Tool::format_price($plan->getPrice(), $plan->currency->format)),
            ];
        }

        return json_encode($data);
    }

    // =========================================================================
    // TERMS OF SERVICE (app-level setting, not plan field — kept on Plan for legacy caller)
    // =========================================================================

    public function updateTos($params)
    {
        $rules = [];
        if ($params['terms_of_service']['enabled'] === 'no') {
            $rules['terms_of_service.content'] = 'required';
        }
        $validator = \Validator::make($params, $rules);
        if ($validator->fails()) {
            return $validator;
        }
        Setting::set('terms_of_service.enabled', $params['terms_of_service']['enabled']);
        Setting::set('terms_of_service.content', $params['terms_of_service']['content']);
        return $validator;
    }
}