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