PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.0
1.7.0 1.6.6 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 All 50 releases
← All changes | app/Models/Customer.php +47 -64 1.3.27 → 1.7.0 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
@@ -150,28 +160,16 @@
150 160 return trim("{$fname} {$lname}");
151 161 }
152 162
153 163 /**
154 - * Accessor method to get the user's avatar URL using their email,
155 - * with a size of 100x100 pixels.
156 - *
157 - * @return string
164 + * The customer's avatar URL: their uploaded photo when one is set,
165 + * otherwise the WordPress avatar by user id or email. Empty when the
166 + * customer has no avatar of their own — every display site falls back
167 + * to a placeholder icon rather than Gravatar's stock image.
158 168 */
159 - public function getPhotoAttribute()
169 + public function getPhotoAttribute(): string
160 170 {
161 - // Get the custom photo URL from user meta using the user_id of this instance
162 - $customPhotoUrl = get_user_meta($this->user_id, 'fc_customer_photo_url', true);
163 -
164 - // Sanitize the customer photo URL
165 - $customPhotoUrl = esc_url($customPhotoUrl ?? '');
166 -
167 - // Return the custom photo URL if it exists, otherwise fallback to Gravatar
168 - if (!empty($customPhotoUrl)) {
169 - return $customPhotoUrl;
170 - }
171 -
172 - // Fallback to Gravatar if no customer avatar is set and sanitize the Gravatar URL
173 - return esc_url(get_avatar_url($this->email, ['size' => 100]));
171 + return Helper::getUserAvatarUrl($this->user_id, $this->email);
174 172 }
175 173
176 174 /**
177 175 * Accessor method to get the country's name with country code,
@@ -200,26 +198,20 @@
200 198
201 199 public function recountStat()
202 200 {
203 201
204 - $orders = \FluentCart\App\Models\Order::query()->where('customer_id', $this->id)
202 + $stats = Order::query()->where('customer_id', $this->id)
205 203 ->whereIn('payment_status', Status::getOrderPaymentSuccessStatuses())
206 - ->get();
204 + ->selectRaw('COUNT(*) AS purchase_count, MIN(created_at) AS first_purchase_date, MAX(created_at) AS last_purchase_date')
205 + // Check before subtracting: payment columns can be unsigned in MySQL.
206 + ->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')
207 + ->toBase()->first();
207 208
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;
209 + $this->purchase_count = (int) $stats->purchase_count;
210 + $this->first_purchase_date = $stats->first_purchase_date;
211 + $this->last_purchase_date = $stats->last_purchase_date;
212 + $this->ltv = (int) $stats->ltv;
213 + $this->aov = $this->purchase_count ? $this->ltv / $this->purchase_count : 0;
222 214 $this->save();
223 215
224 216
225 217 return $this;
@@ -392,21 +384,20 @@
392 384 {
393 385 return $this->belongsTo(User::class, 'user_id');
394 386 }
395 387
388 + /**
389 + * The WordPress user this customer is linked to, or empty when unlinked.
390 + *
391 + * A read never rewrites identity. The old $recheck path looked the user up
392 + * by email and saved that ID onto the row — a rebinding path every caller
393 + * inherited, trusting an address its holder can change with no
394 + * confirmation. The link is written where identity is established
395 + * (explicit creation, verified claims, admin). $recheck is kept so existing
396 + * callers and integrations need no change.
397 + */
396 398 public function getWpUserId($recheck = false)
397 399 {
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 400 return $this->user_id;
410 401 }
411 402
412 403 public function getFormattedAddressAttribute(): array
@@ -466,28 +457,20 @@
466 457 return $exist;
467 458 }
468 459
469 460
461 + /**
462 + * @return \WP_User|false The linked WordPress user; false when unlinked or
463 + * the linked account no longer exists. See getWpUserId()
464 + * for why there is no email fallback.
465 + */
470 466 public function getWpUser()
471 467 {
472 - if ($this->user_id) {
473 - $user = get_user_by('ID', $this->user_id);
474 - if ($user) {
475 - return $user;
476 - }
468 + if (!$this->user_id) {
469 + return false;
477 470 }
478 -
479 - $user = get_user_by('email', $this->email);
480 471
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;
472 + return get_user_by('ID', $this->user_id);
490 473 }
491 474
492 475 public function scopeSearchByFullName ($query, $data) {
493 476