PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
← All changes | app/Models/Customer.php +41 -46 1.3.21 → 1.6.5 View file →
@@ -7,9 +7,9 @@
7 7 use FluentCart\App\Models\Concerns\CanSearch;
8 8 use FluentCart\App\Models\Concerns\CanUpdateBatch;
9 9 use FluentCart\App\Services\Localization\LocalizationManager;
10 10 use FluentCart\Framework\Database\Orm\Relations\BelongsTo;
11 -use FluentCart\Framework\Database\Orm\Relations\hasOne;
11 +use FluentCart\Framework\Database\Orm\Relations\HasOne;
12 12 use FluentCart\Framework\Database\Orm\Relations\MorphMany;
13 13 use FluentCart\Framework\Support\Arr;
14 14 use FluentCart\App\Helpers\Helper;
15 15
@@ -93,8 +93,18 @@
93 93 return $query->where('status', 'archived');
94 94 }
95 95
96 96 /**
97 + * Customers not linked to any WordPress account. Legacy rows carry 0 as well as NULL.
98 + */
99 + public function scopeUnclaimed($query)
100 + {
101 + return $query->where(function ($query) {
102 + $query->whereNull('user_id')->orWhere('user_id', 0);
103 + });
104 + }
105 +
106 + /**
97 107 * todo - contact_id ? - do we need it anymore?
98 108 */
99 109
100 110 public function orders()
@@ -125,14 +135,14 @@
125 135 {
126 136 return $this->hasMany(CustomerAddresses::class, 'customer_id', 'id')->where('type', 'billing');
127 137 }
128 138
129 - public function primary_shipping_address(): hasOne
139 + public function primary_shipping_address(): HasOne
130 140 {
131 141 return $this->hasOne(CustomerAddresses::class, 'customer_id', 'id')->where('type', 'shipping')->where('is_primary', 1);
132 142 }
133 143
134 - public function primary_billing_address(): hasOne
144 + public function primary_billing_address(): HasOne
135 145 {
136 146 return $this->hasOne(CustomerAddresses::class, 'customer_id', 'id')->where('type', 'billing')->where('is_primary', 1);
137 147 }
138 148
@@ -200,26 +210,20 @@
200 210
201 211 public function recountStat()
202 212 {
203 213
204 - $orders = \FluentCart\App\Models\Order::query()->where('customer_id', $this->id)
214 + $stats = Order::query()->where('customer_id', $this->id)
205 215 ->whereIn('payment_status', Status::getOrderPaymentSuccessStatuses())
206 - ->get();
216 + ->selectRaw('COUNT(*) AS purchase_count, MIN(created_at) AS first_purchase_date, MAX(created_at) AS last_purchase_date')
217 + // Check before subtracting: payment columns can be unsigned in MySQL.
218 + ->selectRaw('COALESCE(SUM(CASE WHEN COALESCE(total_paid, 0) > COALESCE(total_refund, 0) THEN COALESCE(total_paid, 0) - COALESCE(total_refund, 0) ELSE 0 END), 0) AS ltv')
219 + ->toBase()->first();
207 220
208 - $totalPayments = [];
209 - $ltv = 0;
210 - foreach ($orders as $order) {
211 - $netPaid = $order->total_paid - $order->total_refund;
212 - if ($netPaid > 0) {
213 - $ltv += $netPaid;
214 - }
215 - }
216 -
217 - $this->purchase_count = $orders->count();
218 - $this->first_purchase_date = $orders->min('created_at') ?? null;
219 - $this->last_purchase_date = $orders->max('created_at') ?? null;
220 - $this->ltv = $ltv;
221 - $this->aov = $this->purchase_count ? $ltv / $this->purchase_count : 0;
221 + $this->purchase_count = (int) $stats->purchase_count;
222 + $this->first_purchase_date = $stats->first_purchase_date;
223 + $this->last_purchase_date = $stats->last_purchase_date;
224 + $this->ltv = (int) $stats->ltv;
225 + $this->aov = $this->purchase_count ? $this->ltv / $this->purchase_count : 0;
222 226 $this->save();
223 227
224 228
225 229 return $this;
@@ -392,21 +396,20 @@
392 396 {
393 397 return $this->belongsTo(User::class, 'user_id');
394 398 }
395 399
400 + /**
401 + * The WordPress user this customer is linked to, or empty when unlinked.
402 + *
403 + * A read never rewrites identity. The old $recheck path looked the user up
404 + * by email and saved that ID onto the row — a rebinding path every caller
405 + * inherited, trusting an address its holder can change with no
406 + * confirmation. The link is written where identity is established
407 + * (explicit creation, verified claims, admin). $recheck is kept so existing
408 + * callers and integrations need no change.
409 + */
396 410 public function getWpUserId($recheck = false)
397 411 {
398 - if ($recheck) {
399 - $user = get_user_by('email', $this->email);
400 - if ($user) {
401 - if ($user->ID != $this->user_id) {
402 - $this->user_id = $user->ID;
403 - unset($this->preventsLazyLoading);
404 - $this->save();
405 - }
406 - }
407 - }
408 -
409 412 return $this->user_id;
410 413 }
411 414
412 415 public function getFormattedAddressAttribute(): array
@@ -466,28 +469,20 @@
466 469 return $exist;
467 470 }
468 471
469 472
473 + /**
474 + * @return \WP_User|false The linked WordPress user; false when unlinked or
475 + * the linked account no longer exists. See getWpUserId()
476 + * for why there is no email fallback.
477 + */
470 478 public function getWpUser()
471 479 {
472 - if ($this->user_id) {
473 - $user = get_user_by('ID', $this->user_id);
474 - if ($user) {
475 - return $user;
476 - }
480 + if (!$this->user_id) {
481 + return false;
477 482 }
478 -
479 - $user = get_user_by('email', $this->email);
480 483
481 - if ($user) {
482 - if ($user->ID != $this->user_id) {
483 - $this->user_id = $user->ID;
484 - unset($this->preventsLazyLoading);
485 - $this->save();
486 - }
487 - }
488 -
489 - return $user;
484 + return get_user_by('ID', $this->user_id);
490 485 }
491 486
492 487 public function scopeSearchByFullName ($query, $data) {
493 488