PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
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 1.2.0 All 47 releases
fluent-cart / app / Models / Customer.php

Customer.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Models/Customer.php

518 lines 14.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Models;
4
5 use FluentCart\App\Helpers\AddressHelper;
6 use FluentCart\App\Helpers\Status;
7 use FluentCart\App\Models\Concerns\CanSearch;
8 use FluentCart\App\Models\Concerns\CanUpdateBatch;
9 use FluentCart\App\Services\Localization\LocalizationManager;
10 use FluentCart\Framework\Database\Orm\Relations\BelongsTo;
11 use FluentCart\Framework\Database\Orm\Relations\HasOne;
12 use FluentCart\Framework\Database\Orm\Relations\MorphMany;
13 use FluentCart\Framework\Support\Arr;
14 use FluentCart\App\Helpers\Helper;
15
16 /**
17 * Customer Model - DB Model for Customers
18 *
19 * Database Model
20 *
21 * @package FluentCart\App\Models
22 *
23 * @version 1.0.0
24 */
25 class Customer extends Model
26 {
27 use CanSearch, CanUpdateBatch;
28
29 protected $table = 'fct_customers';
30 protected $appends = ['full_name', 'photo', 'country_name', 'formatted_address', 'user_link'];
31
32 /**
33 * The attributes that are mass assignable.
34 *
35 * @var array
36 */
37 protected $fillable = [
38 'user_id',
39 'contact_id',
40 'email',
41 'first_name',
42 'last_name',
43 'status',
44 'purchase_value',
45 'purchase_count',
46 'ltv',
47 'first_purchase_date',
48 'last_purchase_date',
49 'aov',
50 'notes',
51 'uuid',
52 'country',
53 'city',
54 'state',
55 'postcode',
56 ];
57
58 protected $searchable = [
59 'first_name',
60 'last_name',
61 'email',
62 ];
63
64 public function setPurchaseValueAttribute($value)
65 {
66 if (is_array($value) || is_object($value)) {
67 $this->attributes['purchase_value'] = json_encode($value);
68 } else {
69 $this->attributes['purchase_value'] = $value;
70 }
71 }
72
73 public function getPurchaseValueAttribute($value)
74 {
75 return !empty($value) ? json_decode($value,true) : null;
76 }
77
78 public static function boot()
79 {
80 parent::boot();
81 static::creating(function ($model) {
82 $model->uuid = md5($model->email . '_' . wp_generate_uuid4());
83 });
84 }
85
86 public function scopeOfActive($query)
87 {
88 return $query->where('status', 'active');
89 }
90
91 public function scopeOfArchived($query)
92 {
93 return $query->where('status', 'archived');
94 }
95
96 /**
97 * todo - contact_id ? - do we need it anymore?
98 */
99
100 public function orders()
101 {
102 return $this->hasMany(Order::class, 'customer_id', 'id');
103 }
104
105 public function success_order_items()
106 {
107 return $this->hasManyThrough(OrderItem::class, Order::class, 'customer_id', 'order_id', 'id', 'id')
108 ->whereHas('order', function ($q) {
109 $q->whereIn('payment_status', Status::getOrderPaymentSuccessStatuses());
110 });
111 }
112
113
114 public function subscriptions()
115 {
116 return $this->hasMany(Subscription::class, 'customer_id', 'id');
117 }
118
119 public function shipping_address()
120 {
121 return $this->hasMany(CustomerAddresses::class, 'customer_id', 'id')->where('type', 'shipping');
122 }
123
124 public function billing_address()
125 {
126 return $this->hasMany(CustomerAddresses::class, 'customer_id', 'id')->where('type', 'billing');
127 }
128
129 public function primary_shipping_address(): HasOne
130 {
131 return $this->hasOne(CustomerAddresses::class, 'customer_id', 'id')->where('type', 'shipping')->where('is_primary', 1);
132 }
133
134 public function primary_billing_address(): HasOne
135 {
136 return $this->hasOne(CustomerAddresses::class, 'customer_id', 'id')->where('type', 'billing')->where('is_primary', 1);
137 }
138
139
140 /**
141 * Accessor to get dynamic full_name attribute
142 *
143 * @return string
144 */
145 public function getFullNameAttribute()
146 {
147 $fname = isset($this->attributes['first_name']) ? $this->attributes['first_name'] : '';
148 $lname = isset($this->attributes['last_name']) ? $this->attributes['last_name'] : '';
149
150 return trim("{$fname} {$lname}");
151 }
152
153 /**
154 * Accessor method to get the user's avatar URL using their email,
155 * with a size of 100x100 pixels.
156 *
157 * @return string
158 */
159 public function getPhotoAttribute()
160 {
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]));
174 }
175
176 /**
177 * Accessor method to get the country's name with country code,
178 *
179 * @return string
180 */
181 public function getCountryNameAttribute(): string
182 {
183 return Helper::getCountryName($this->country);
184 }
185
186 public function recountStats()
187 {
188 $this->total_order_count = Order::query()->where('customer_id', $this->id)
189 // ->whereIn('order_status', Status::getOrderSuccessStatuses())
190 ->count();
191
192 $this->total_order_value = Order::query()->where('customer_id', $this->id)
193 // ->whereIn('order_status', Status::getOrderSuccessStatuses())
194 ->sum('total_amount');
195
196 $this->save();
197
198 return $this;
199 }
200
201 public function recountStat()
202 {
203
204 $orders = \FluentCart\App\Models\Order::query()->where('customer_id', $this->id)
205 ->whereIn('payment_status', Status::getOrderPaymentSuccessStatuses())
206 ->get();
207
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;
222 $this->save();
223
224
225 return $this;
226 }
227
228 /**
229 * Local scope to filter subscribers by search/query string
230 *
231 * @param \FluentCart\Framework\Database\Query\Builder $query
232 * @param string $search
233 *
234 * @return \FluentCart\Framework\Database\Query\Builder $query
235 */
236 public function scopeSearchBy($query, $search)
237 {
238 if ($search) {
239
240 $fields = $this->searchable;
241
242 // maybe operator based search
243 $operators = ['=', '!=', '>', '<'];
244
245 // check if search has an operator with regexp
246 $operatorPattern = '/\s*(' . implode('|', $operators) . ')\s*/';
247
248 $search = trim($search);
249 if (preg_match($operatorPattern, $search, $matches)) {
250 $operator = $matches[1];
251 $searchParts = explode($operator, $search);
252 if (count($searchParts) >= 2) {
253 $column = trim($searchParts[0]);
254 $value = trim($searchParts[1]);
255
256 // Check if the column is valid
257 $validColumns = $this->fillable;
258 $validColumns[] = 'id';
259
260 if (in_array($column, $validColumns)) {
261 return $query->where($column, $operator, $value);
262 }
263 }
264 }
265
266 $maybeColumnSearch = explode(':', $search);
267
268 if (count($maybeColumnSearch) >= 2) {
269 $column = $maybeColumnSearch[0];
270 $validColumns = $this->fillable;
271 $validColumns[] = 'id';
272 if (in_array($column, $validColumns)) {
273 return $query->where($column, 'LIKE', '%%' . trim($maybeColumnSearch[1]) . '%%');
274 }
275 }
276
277 $maybeExactSearch = explode('=', $search);
278 if (count($maybeExactSearch) >= 2) {
279 $column = $maybeExactSearch[0];
280 $validColumns = $this->fillable;
281 $validColumns[] = 'id';
282 if (in_array($column, $validColumns)) {
283 return $query->where($column, trim($maybeExactSearch[1]));
284 }
285 }
286
287 $query->where(function ($query) use ($fields, $search) {
288 $query->where(array_shift($fields), 'LIKE', "%$search%");
289
290 $nameArray = explode(' ', $search);
291 if (count($nameArray) >= 2) {
292 $query->orWhere(function ($q) use ($nameArray) {
293 $fname = array_shift($nameArray);
294 $lastName = implode(' ', $nameArray);
295 $q->where('first_name', 'LIKE', "$fname%");
296 $q->where('last_name', 'LIKE', "$lastName%");
297 });
298 }
299
300 foreach ($fields as $field) {
301 $query->orWhere($field, 'LIKE', "%$search%");
302 }
303 });
304 }
305
306 return $query;
307 }
308
309 public function scopeApplyCustomFilters($query, $filters)
310 {
311 if (!$filters) {
312 return $query;
313 }
314
315 $acceptedKeys = $this->fillable;
316
317 foreach ($filters as $filterKey => $filter) {
318
319 if (!in_array($filterKey, $acceptedKeys)) {
320 continue;
321 }
322
323 $value = Arr::get($filter, 'value', '');
324 $operator = Arr::get($filter, 'operator', '');
325 if (!$value || !$operator || is_array($value)) {
326 continue;
327 }
328
329 switch (strtolower($operator)) {
330 case 'includes':
331 $operator = "like_all";
332 break;
333 case 'not_includes':
334 $operator = "not_like";
335 break;
336 case 'gt':
337 $operator = ">";
338 break;
339 case 'lt':
340 $operator = "<";
341 break;
342
343 default:
344
345 }
346 $param = [$filterKey => ["column" => $filterKey, "operator" => $operator, "value" => trim($value)]];
347 $query->when($param, function ($query) use ($param) {
348 return $query->search($param);
349 });
350 }
351
352 return $query;
353 }
354
355 public function updateCustomerStatus($newStatus)
356 {
357 $oldStatus = $this->status;
358
359 if ($newStatus == $oldStatus) {
360 return $this;
361 }
362
363 $this->status = $newStatus;
364 $this->save();
365
366 do_action('fluent_cart/customer_status_to_' . $newStatus, [
367 'customer' => $this,
368 'old_status' => $oldStatus,
369 'new_status' => $newStatus
370 ]);
371 do_action('fluent_cart/customer_status_updated', [
372 'customer' => $this,
373 'old_status' => $oldStatus,
374 'new_status' => $newStatus
375 ]);
376
377 return $this;
378 }
379
380 /**
381 * Get the customer's label.
382 */
383 public function labels(): MorphMany
384 {
385 return $this->morphMany(LabelRelationship::class, 'labelable');
386 }
387
388 /**
389 * Define the relationship with the User model.
390 */
391 public function wpUser(): BelongsTo
392 {
393 return $this->belongsTo(User::class, 'user_id');
394 }
395
396 public function getWpUserId($recheck = false)
397 {
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 return $this->user_id;
410 }
411
412 public function getFormattedAddressAttribute(): array
413 {
414
415 return [
416 'country' => $this->country ? AddressHelper::getCountryNameByCode($this->country): '',
417 'state' => AddressHelper::getStateNameByCode($this->state, $this->country),
418 'city' => $this->city,
419 'postcode' => $this->postcode,
420 'first_name' => $this->first_name,
421 'last_name' => $this->last_name,
422 'full_name' => $this->full_name
423 ];
424 }
425
426 public function getUserLinkAttribute()
427 {
428 if ($this->user_id) {
429 return admin_url('user-edit.php?user_id=' . $this->user_id);
430 }
431 return '';
432 }
433
434 public function getMeta($metaKey, $default = null)
435 {
436 $exist = CustomerMeta::query()->where('customer_id', $this->id)
437 ->where('meta_key', $metaKey)
438 ->first();
439
440 if ($exist) {
441 return $exist->meta_value;
442 }
443
444 return $default;
445 }
446
447 public function updateMeta($metaKey, $metaValue)
448 {
449 $exist = CustomerMeta::query()->where('customer_id', $this->id)
450 ->where('meta_key', $metaKey)
451 ->first();
452
453 if ($exist) {
454 $exist->meta_value = $metaValue;
455 $exist->save();
456 } else {
457 $exist = CustomerMeta::query()->create([
458 'customer_id' => $this->id,
459 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
460 'meta_key' => $metaKey,
461 //phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
462 'meta_value' => $metaValue
463 ]);
464 }
465
466 return $exist;
467 }
468
469
470 public function getWpUser()
471 {
472 if ($this->user_id) {
473 $user = get_user_by('ID', $this->user_id);
474 if ($user) {
475 return $user;
476 }
477 }
478
479 $user = get_user_by('email', $this->email);
480
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;
490 }
491
492 public function scopeSearchByFullName ($query, $data) {
493
494 $operator = Arr::get($data, 'operator', 'like_all');
495
496 $search = Arr::get($data, 'value');
497 $search = sanitize_text_field(trim($search));
498
499 $fullName = \FluentCart\App\App::db()->raw("CONCAT(first_name, ' ', last_name)");
500
501 switch ($operator) {
502 case 'starts_with':
503 $pattern = "{$search}%";
504 break;
505 case 'ends_with':
506 $pattern = "%{$search}";
507 break;
508 case 'not_like':
509 return $query->where($fullName, 'NOT LIKE', "%{$search}%");
510 default: // contains
511 $pattern = "%{$search}%";
512 }
513
514 return $query->where($fullName, 'LIKE', $pattern);
515
516 }
517 }
518