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

namespace App\Model;

use Illuminate\Database\Eloquent\Model;
use App\Model\PaymentIntent;
use App\Library\Traits\HasUid;
use Dompdf\Dompdf;
use App\Library\Facades\SubscriptionFacade;
use Exception;
use Closure;
use League\Pipeline\PipelineBuilder;
use App\Library\Contracts\TagResolverInterface;
use App\Library\HtmlHandler\DecodeHtmlSpecialChars;
use App\Library\HtmlHandler\TransformTag;
use App\Library\HtmlHandler\TransformUrl;
use Illuminate\Support\Facades\DB;

/**
 * Logically abstract, must be mapped to a specific type in order to execute.
 *
 * @property int|null $processing_intent_id  In-flight PaymentIntent lock (see PaymentIntentFactory)
 * @property-read Customer|null $customer
 * @property-read Order|null $order
 */
class Invoice extends Model implements TagResolverInterface
{
    use \Illuminate\Database\Eloquent\Factories\HasFactory;
    use HasUid;

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

    // statuses
    public const STATUS_NEW = 'new';               // unpaid
    public const STATUS_PAID = 'paid';

    protected $fillable = [
        'billing_first_name',
        'billing_last_name',
        'billing_address',
        'billing_email',
        'billing_phone',
        'billing_country_id',
        'processing_intent_id',
    ];

    public function scopeNew($query)
    {
        $query->whereIn('status', [
            self::STATUS_NEW,
        ]);
    }

    public function scopeUnpaid($query)
    {
        $query->whereIn('status', [
            self::STATUS_NEW,
        ]);
    }

    public function scopePaid($query)
    {
        $query->whereIn('status', [
            self::STATUS_PAID,
        ]);
    }

    public function scopePending($query)
    {
        $query->whereHas('paymentIntents', function ($q) {
            $q->where('status', PaymentIntent::STATUS_PENDING);
        });
    }

    public function scopeNotPending($query)
    {
        $query->orWhereDoesntHave('paymentIntents', function ($q) {
            $q->where('status', PaymentIntent::STATUS_PENDING);
        });
    }

    /**
     * Invoice currency.
     */
    public function currency()
    {
        return $this->belongsTo('App\Model\Currency');
    }

    /**
     * Invoice customer.
     */
    public function customer()
    {
        return $this->belongsTo('App\Model\Customer');
    }

    public function billingCountry()
    {
        return $this->belongsTo('App\Model\Country', 'billing_country_id');
    }

    public function order()
    {
        return $this->belongsTo(Order::class, 'order_id');
    }

    public function getPayerUid(): string
    {
        return $this->customer->uid;
    }

    public function getPayerName(): string
    {
        return $this->customer->getName();
    }

    public function getPlan()
    {
        try {
            $item = $this->order->orderItems->first();
            return $item && $item->subscription ? $item->subscription->plan : null;
        } catch (\Throwable $e) {
            return null;
        }
    }

    public function getPlanName()
    {
        $plan = $this->getPlan();
        return $plan ? $plan->name : '';
    }

    /** All PaymentIntents on this invoice (history of attempts). */
    public function paymentIntents()
    {
        return $this->hasMany(PaymentIntent::class, 'invoice_id');
    }

    /** The active in-flight PaymentIntent (if any) — points to processing_intent_id. */
    public function activePaymentIntent(): ?PaymentIntent
    {
        return $this->processing_intent_id
            ? PaymentIntent::find($this->processing_intent_id)
            : null;
    }

    /** Active intent if non-terminal (pending / requires_action / awaiting_admin_approval), else null. */
    public function getPendingPaymentIntent(): ?PaymentIntent
    {
        $intent = $this->activePaymentIntent();
        return ($intent && !$intent->isTerminal()) ? $intent : null;
    }

    /** Most recent attempt failed — was: lastTransactionIsFailed(). */
    public function lastPaymentIntentIsFailed(): bool
    {
        $intent = $this->paymentIntents()->orderByDesc('created_at')->orderByDesc('id')->first();
        return $intent && $intent->isFailed();
    }

    public function getTax()
    {
        return $this->order->getTax();
    }

    public function subTotal()
    {
        return $this->order->subTotal();
    }

    public function total()
    {
        return $this->order->total(); // tạm thời là vầy, sau này invoice phải amount riêng, trả nhiều đợt chẳng hạn
    }

    public function isNew()
    {
        return $this->status == self::STATUS_NEW;
    }

    public function isPaid()
    {
        return $this->status == self::STATUS_PAID;
    }

    public function isUnpaid()
    {
        return in_array($this->status, [
            self::STATUS_NEW,
        ]);
    }

    public function isFree()
    {
        return $this->order->total() == 0;
    }

    public function updateBillingInformation($params)
    {
        $this->fill($params);

        $validator = \Validator::make($params, [
            'billing_first_name' => 'required',
            'billing_last_name' => 'required',
            'billing_address' => 'required',
            'billing_country_id' => 'required',
            'billing_email' => 'required|email',
            'billing_phone' => 'required',
        ]);

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

        $this->save();

        return $validator;
    }

    public function fillBillingAddressNotReplace($billingAddress)
    {
        if (!$this->billing_first_name) {
            $this->billing_first_name = $billingAddress->first_name;
        }
        if (!$this->billing_last_name) {
            $this->billing_last_name = $billingAddress->last_name;
        }
        if (!$this->billing_address) {
            $this->billing_address = $billingAddress->address;
        }
        if (!$this->billing_email) {
            $this->billing_email = $billingAddress->email;
        }
        if (!$this->billing_phone) {
            $this->billing_phone = $billingAddress->phone;
        }
        if (!$this->billing_country_id) {
            $this->billing_country_id = $billingAddress->country_id;
        }
    }

    public function getBillingName()
    {
        $lastNameFirst = get_localization_config('show_last_name_first', $this->customer->getLanguageCode());

        if ($lastNameFirst) {
            return htmlspecialchars(trim($this->billing_last_name . ' ' . $this->billing_first_name));
        } else {
            return htmlspecialchars(trim($this->billing_first_name . ' ' . $this->billing_last_name));
        }
    }

    public static function newDefault()
    {
        $invoice = new self();
        $invoice->status = self::STATUS_NEW;

        return $invoice;
    }

    public function hasBillingInformation()
    {
        if (empty($this->billing_first_name) ||
            empty($this->billing_last_name) ||
            empty($this->billing_phone) ||
            empty($this->billing_address) ||
            empty($this->billing_country_id) ||
            empty($this->billing_email)
        ) {
            return false;
        }

        return true;
    }

    public static function getTemplateContent($languageCode = 'en')
    {
        if (Setting::get('invoice.custom_template')) {
            return Setting::get('invoice.custom_template');
        }

        $sysTemplate = \App\Model\SystemTemplate::getInvoiceTemplate();
        if ($sysTemplate && $sysTemplate->template) {
            return $sysTemplate->template->content;
        }

        return view('invoices.template', [
            'languageCode' => $languageCode,
        ]);
    }

    public function getResolvedTags(): array
    {
        return [
            'FIRST_NAME'       => $this->billing_first_name,
            'LAST_NAME'        => $this->billing_last_name,
            'ADDRESS'          => $this->billing_address,
            'COUNTRY'          => $this->billingCountry ? $this->billingCountry->name : '',
            'EMAIL'            => $this->billing_email,
            'PHONE'            => $this->billing_phone,
            'INVOICE_NUMBER'   => $this->number,
            'CURRENT_DATETIME' => $this->customer->formatCurrentDateTime('datetime_full'),
            'INVOICE_DUE_DATE' => $this->customer->formatDateTime($this->created_at, 'datetime_full'),
            'ITEMS'            => view('invoices._template_items', ['invoice' => $this])->render(),
        ];
    }

    public function getInvoiceHtml()
    {
        $content = (string) self::getTemplateContent($this->customer->getLanguageCode());

        return (new PipelineBuilder())
            ->add(new TransformTag($this))
            ->add(new TransformUrl($msgId = null, $domain = null, $trackLink = false))
            ->add(new DecodeHtmlSpecialChars())
            ->build()
            ->process($content);
    }

    public function exportToPdf()
    {
        // instantiate and use the dompdf class
        $dompdf = new Dompdf(array('enable_remote' => true));
        $content = mb_convert_encoding($this->getInvoiceHtml(), 'HTML-ENTITIES', 'UTF-8');
        $dompdf->loadHtml($content);

        // (Optional) Setup the paper size and orientation
        $dompdf->setPaper('A4');

        // Render the HTML as PDF
        $dompdf->render();

        return $dompdf->output();
    }

    public static function getTags()
    {
        $tags = [
            ['name' => '{{ FIRST_NAME }}', 'required' => false],
            ['name' => '{{ LAST_NAME }}', 'required' => false],
            ['name' => '{{ ADDRESS }}', 'required' => false],
            ['name' => '{{ COUNTRY }}', 'required' => false],
            ['name' => '{{ EMAIL }}', 'required' => false],
            ['name' => '{{ PHONE }}', 'required' => false],
            ['name' => '{{ INVOICE_NUMBER }}', 'required' => false],
            ['name' => '{{ CURRENT_DATETIME }}', 'required' => false],
            ['name' => '{{ INVOICE_DUE_DATE }}', 'required' => false],
            ['name' => '{{ ITEMS }}', 'required' => false],
            ['name' => '{{ CUSTOMER_ADDRESS }}', 'required' => false],
        ];

        return $tags;
    }

    public function createInvoiceNumber()
    {
        $format  = Setting::get('invoice.format') ?: '%08d';
        $current = (int) (Setting::get('invoice.current') ?: 1);

        $candidate = sprintf($format, $current);
        if (!preg_match('/^\d+$/', $candidate)) {
            throw new \RuntimeException(sprintf(
                'Invalid invoice number format "%s" (rendered: "%s"). Must produce a numeric value (digits only). Please update setting "invoice.format".',
                $format,
                $candidate
            ));
        }

        // Audit + snap counter to MAX(number)+1 if it drifted underneath: DB
        // restore, manual SQL, plugin-inserted invoices, or a prior save failure
        // leaving the counter stuck below MAX. Notify admin so the reset is
        // visible — never silent.
        $max = $this->getMaxExistingInvoiceNumber();
        if ($current <= $max) {
            $newCurrent = $max + 1;
            Setting::set('invoice.current', $newCurrent);
            app(\App\Services\Notifications\Notifier::class)->dispatch(
                new \App\Notifications\System\InvoiceCounterReset($current, $newCurrent, $max)
            );
            $current   = $newCurrent;
            $candidate = sprintf($format, $current);
        }

        $this->number = $candidate;
        $this->save();

        Setting::set('invoice.current', $current + 1);
    }

    /**
     * Highest numeric value already used in `invoices.number`, interpreted as
     * an unsigned integer (non-numeric historical values cast to 0 and are
     * effectively ignored). Extracted from createInvoiceNumber() so tests can
     * stub it without seeding rows.
     */
    protected function getMaxExistingInvoiceNumber(): int
    {
        return (int) static::query()
            ->selectRaw('MAX(CAST(`number` AS UNSIGNED)) AS m')
            ->value('m');
    }

    public function getCurrencyCode()
    {
        return $this->order->currency->code;
    }

    public function getBillingCountryCode()
    {
        return ($this->billingCountry ? $this->billingCountry->code : '');
    }

    public function getPaymentIntents()
    {
        return $this->paymentIntents()
            ->orderBy('created_at', 'desc')
            ->get();
    }

    public function formattedTotal()
    {
        return format_price($this->order->total(), $this->order->currency->format);
    }

    public function getAmountInCurrencyCode($currencyCode)
    {
        if ($currencyCode == $this->order->currency->code) {
            return $this->order->total();
        }

        // convert amount
        return \App\Helpers\convertPrice($this->order->total(), $this->order->currency->code, $currencyCode);
    }

}