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 / Http / Controllers / FrontendControllers / CustomerProfileController.php

CustomerProfileController.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Http/Controllers/FrontendControllers/CustomerProfileController.php

631 lines 22.4 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\Http\Controllers\FrontendControllers;
4
5 //use FluentCart\Api\Resource\FrontendResource\CustomerResource;
6 use FluentCart\Api\Resource\CustomerAddressResource;
7 use FluentCart\Api\Resource\CustomerResource;
8 use FluentCart\Api\Resource\FrontendResource\OrderResource;
9 use FluentCart\Api\Resource\OrderDownloadPermissionResource;
10 use FluentCart\App\App;
11 use FluentCart\App\Helpers\Helper;
12 use FluentCart\App\Helpers\Status;
13 use FluentCart\App\Http\Controllers\Controller;
14 use FluentCart\App\Http\Requests\FrontendRequests\CustomerRequests\CustomerProfileAccountDetailsRequest;
15 use FluentCart\App\Http\Requests\FrontendRequests\CustomerRequests\CustomerProfileRequest;
16 use FluentCart\App\Models\Customer;
17 use FluentCart\App\Models\CustomerAddresses;
18 use FluentCart\App\Models\Meta;
19 use FluentCart\App\Models\Order;
20 use FluentCart\App\Models\OrderItem;
21 use FluentCart\App\Models\ProductDownload;
22 use FluentCart\App\Models\ProductVariation;
23 use FluentCart\App\Models\Subscription;
24 use FluentCart\App\Services\FileSystem\FileManager;
25 use FluentCart\App\Services\FrontendView;
26 use FluentCart\App\Services\Payments\PaymentHelper;
27 use FluentCart\App\Services\PlanUpgradeService;
28 use FluentCart\App\Services\URL;
29 use FluentCart\Framework\Database\Orm\Builder;
30 use FluentCart\Framework\Database\Orm\Relations\HasMany;
31 use FluentCart\Framework\Http\Request\Request;
32 use FluentCart\Framework\Support\Arr;
33 use FluentCart\Framework\Support\Collection;
34 use FluentCartPro\App\Hooks\Handlers\UpgradeHandler;
35 use FluentCartPro\App\Modules\Licensing\Models\License;
36 use FluentCartPro\App\Modules\Licensing\Models\LicenseActivation;
37 use FluentCartPro\App\Modules\Licensing\Services\LicenseHelper;
38
39
40 class CustomerProfileController extends BaseFrontendController
41 {
42 /**
43 * Portal surfaces an add-on may attach a section to. Each value maps to the
44 * hook `fluent_cart/customer_portal/{value}`.
45 */
46 const PORTAL_SECTION_FILTERS = [
47 'profile_sections'
48 ];
49
50 /**
51 * Handle the request to retrieve the customer's orders.
52 *
53 * This method checks if the user is logged in, retrieves the current customer,
54 * and searches for their orders based on the provided search parameters.
55 *
56 * @param Request $request The incoming HTTP request.
57 * @return array
58 */
59 public function index(Request $request)
60 {
61 $customer = CustomerResource::getCurrentCustomer();
62
63
64 if (!$customer) {
65 return apply_filters('fluent_cart/customer_dashboard_data', [
66 'message' => __('Success', 'fluent-cart'),
67 'dashboard_data' => [
68 'orders' => []
69 ],
70 'sections_parts' => [
71 'before_orders_table' => '',
72 'after_orders_table' => ''
73 ]
74 ], [
75 'customer' => null
76 ]);
77 }
78
79
80 $orders = Order::query()
81 ->with(['order_items' => function ($query) {
82 $query->select('id', 'order_id', 'object_id', 'post_title', 'title', 'quantity', 'payment_type', 'line_meta', 'other_info');
83 }])
84 ->where('customer_id', $customer->id)
85 ->where(function ($query) {
86 $query
87 ->where(function ($query) {
88 $query->where('parent_id', '')->orWhereNull('parent_id');
89 })
90 ->orWhere('type', '!=', 'renewal');
91 })
92 ->withCount('renewals')
93 ->orderBy('created_at', 'DESC')
94 ->limit(5)
95 ->get();
96
97 $orders = $orders->map(function ($order) {
98 return [
99 'created_at' => $order->created_at->format('Y-m-d H:i:s'),
100 'invoice_no' => $order->invoice_no,
101 'total_amount' => $order->total_amount,
102 'currency' => $order->currency,
103 'uuid' => $order->uuid,
104 'type' => $order->type,
105 'status' => $order->status,
106 'renewals_count' => $order->renewals_count,
107 'order_items' => $order->order_items->map(function ($item) {
108 return [
109 'id' => $item->id,
110 'post_title' => $item->post_title,
111 'title' => $item->title,
112 'variation_display_title' => $item->variation_display_title,
113 'quantity' => $item->quantity,
114 'payment_type' => $item->payment_type,
115 'line_meta' => [
116 'bundle_parent_item_id' => Arr::get($item, 'line_meta.bundle_parent_item_id', null),
117 ]
118 ];
119 }),
120 ];
121 });
122
123 return apply_filters('fluent_cart/customer_dashboard_data', [
124 'message' => __('Success', 'fluent-cart'),
125 'dashboard_data' => [
126 'orders' => $orders
127 ],
128 'sections_parts' => [
129 'before_orders_table' => '',
130 'after_orders_table' => ''
131 ]
132 ], [
133 'customer' => $customer
134 ]);
135 }
136
137 public function getCustomerProfileDetails()
138 {
139 // Get the current logged-in customer
140 $customer = CustomerResource::getCurrentCustomer();
141
142 if (!$customer) {
143 // get current user by id
144 $userId = get_current_user_id();
145 $currentUser = get_user_by('ID', $userId);
146
147 // get user first_name and last_name from usermeta
148 $currentUser->data->first_name = get_user_meta($userId, 'first_name', true);
149 $currentUser->data->last_name = get_user_meta($userId, 'last_name', true);
150
151
152 return $this->sendSuccess([
153 'message' => __('Success', 'fluent-cart'),
154 'data' => [
155 'first_name' => $currentUser->data->first_name,
156 'last_name' => $currentUser->data->last_name,
157 'user_login' => $currentUser->data->user_login,
158 'user_email' => $currentUser->data->user_email,
159 'email' => $currentUser->data->user_email,
160 'user_nicename' => $currentUser->data->user_nicename,
161 'display_name' => $currentUser->data->display_name,
162 'billing_address' => [],
163 'shipping_address' => [],
164 'not_a_customer' => true
165 ]
166 ]);
167 }
168
169 // Fetch the customer along with the related WordPress user
170 $customerData = Customer::query()
171 ->where('id', $customer->id)
172 ->with(['billing_address', 'shipping_address'])
173 ->first()
174 ->toArray();
175
176 $userData = Arr::only($customerData, [
177 'first_name',
178 'last_name',
179 'email',
180 'billing_address',
181 'shipping_address'
182 ]);
183 // Combine the customer and WordPress user data in the response
184 return $this->sendSuccess([
185 'message' => __('Success', 'fluent-cart'),
186 'data' => $userData
187 ]);
188 }
189
190 /**
191 * Return the add-on sections registered for one customer-portal surface.
192 *
193 * Each entry carries an UNCOMPILED Vue component string plus its payload;
194 * the portal SPA compiles it in the browser (see the shared
195 * DynamicTemplateParser). This is how an add-on renders inside the SPA at
196 * all — the router's route table is static and cannot be extended.
197 *
198 * @param Request $request
199 * @return \WP_REST_Response
200 */
201 public function getSections(Request $request): \WP_REST_Response
202 {
203 // `?filter[]=x` makes this an array, and casting one to string emits a
204 // warning a caller could raise at will.
205 $requestedFilter = $request->get('filter', '');
206 $filter = is_string($requestedFilter) ? sanitize_text_field($requestedFilter) : '';
207
208 // Allowlisted, never interpolated from the raw request value: building
209 // a hook name out of caller input would let anyone fire arbitrary
210 // filters through this endpoint.
211 if (!in_array($filter, self::PORTAL_SECTION_FILTERS, true)) {
212 return $this->sendError([
213 'message' => __('Unknown portal section group.', 'fluent-cart')
214 ], 422);
215 }
216
217 $customer = CustomerResource::getCurrentCustomer();
218
219 // A logged-in WP user who has never bought anything is not a customer.
220 // Short-circuit rather than firing the filter with a null customer,
221 // so no add-on has to remember to handle that case correctly.
222 if (!$customer) {
223 return $this->sendSuccess([
224 'message' => __('Success', 'fluent-cart'),
225 'sections' => []
226 ]);
227 }
228
229 $sections = apply_filters('fluent_cart/customer_portal/' . $filter, [], [
230 'customer' => $customer
231 ]);
232
233 return $this->sendSuccess([
234 'message' => __('Success', 'fluent-cart'),
235 'sections' => $this->formatPortalSections($sections)
236 ]);
237 }
238
239 /**
240 * Normalise whatever add-ons returned into the shape the SPA renders, and
241 * drop entries with nothing to compile.
242 *
243 * @param mixed $sections
244 * @return array
245 */
246 private function formatPortalSections($sections): array
247 {
248 if (!is_array($sections)) {
249 return [];
250 }
251
252 $formatted = [];
253
254 foreach ($sections as $sectionKey => $section) {
255 if (!is_array($section) || empty($section['component'])) {
256 continue;
257 }
258
259 $key = Arr::get($section, 'key', $sectionKey);
260 $type = Arr::get($section, 'type', 'vue-template');
261
262 // sanitize_key()/sanitize_text_field() are scalar-only and throw on
263 // an array in PHP 8. A malformed add-on entry should drop out here,
264 // not 500 the whole endpoint.
265 if (!is_scalar($section['component']) || !is_scalar($key) || !is_scalar($type)) {
266 continue;
267 }
268
269 $formatted[] = [
270 'key' => sanitize_key($key),
271 'type' => sanitize_text_field($type),
272 'component' => (string)$section['component'],
273 'payload' => Arr::get($section, 'payload', [])
274 ];
275 }
276
277 return $formatted;
278 }
279
280 public function updateCustomerProfileDetails(CustomerProfileAccountDetailsRequest $request): \WP_REST_Response
281 {
282 $errorResponse = $this->checkUserLoggedIn();
283
284 // Check if there is an error response and return it if exists
285 if ($errorResponse !== null) {
286 return $errorResponse;
287 }
288 // Get the current logged-in customer
289 $customer = CustomerResource::getCurrentCustomer();
290
291 if (!$customer) {
292 return $this->sendError([
293 'message' => __('Customer not found', 'fluent-cart')
294 ]);
295 }
296
297 // Validate the request data for customer and addresses
298 $validatedData = $request->getSafe($request->sanitize());
299 $firstName = Arr::get($validatedData, 'first_name');
300 $lastName = Arr::get($validatedData, 'last_name');
301
302 // Update the customer with the validated data
303 $customer->first_name = $firstName;
304 $customer->last_name = $lastName;
305 $customerUpdate = $customer->save();
306
307
308 if (is_wp_error($customerUpdate)) {
309 return $this->sendError([
310 'message' => $customerUpdate->get_error_message()
311 ]);
312 }
313
314 // Also update the WordPress user profile
315 $name = trim($firstName . ' ' . $lastName);
316 $wpResult = wp_update_user([
317 'ID' => $customer->user_id,
318 'first_name' => $firstName,
319 'last_name' => $lastName,
320 'display_name' => $name,
321 ]);
322
323 if (is_wp_error($wpResult)) {
324 return $this->sendError([
325 'message' => $wpResult->get_error_message()
326 ]);
327 }
328
329 return $this->sendSuccess([
330 'message' => __('Profile updated successfully', 'fluent-cart'),
331 ]);
332 }
333
334 /**
335 * Helper method to validate email uniqueness
336 */
337 private function validateEmailUniqueness($email, $currentCustomerId): ?\WP_REST_Response
338 {
339 $customerId = $currentCustomerId;
340 $userId = Customer::query()->find($customerId)->user_id;
341
342 // Fetch the current customer's email to compare
343 $currentEmail = Customer::query()->find($customerId)->email;
344
345 // Check if the email is different from the current user's email and exists in WordPress users table
346 if ($email !== $currentEmail) {
347 // Check if the email exists in the WordPress users table
348 $emailExistsInWp = get_user_by('email', $email);
349
350 if ($emailExistsInWp && $emailExistsInWp->ID !== $userId) {
351 return $this->sendError([
352 'message' => __('The email address is already in use in WordPress users.', 'fluent-cart')
353 ]);
354 }
355 }
356
357 // Check if the email exists in the Customer table, excluding the current customer
358 $emailExistsInCustomer = Customer::query()->where('email', $email)->where('id', '!=', $customerId)->exists();
359
360 if ($emailExistsInCustomer) {
361 return $this->sendError([
362 'message' => __('The email address is already in use in the customer records.', 'fluent-cart')
363 ]);
364 }
365
366 return null; // Return null mean email is unique
367 }
368
369 public function createCustomerProfileAddress(CustomerProfileRequest $request): \WP_REST_Response
370 {
371 // Get the current logged-in customer
372 $customer = CustomerResource::getCurrentCustomer(true);
373
374 // Sanitize and retrieve the request data
375 $data = CustomerAddressResource::normalizeBusinessFields($request->getSafe($request->sanitize()));
376
377 // Attempt to create a new address for the logged-in customer
378 $isCreated = CustomerAddressResource::create(
379 $data,
380 ['id' => $customer->id]
381 );
382
383 // Check if there was an error during the creation process, return the error if one occurred
384 if (is_wp_error($isCreated)) {
385 return $this->sendError([
386 'message' => $isCreated->get_error_message()
387 ]);
388 }
389
390 // Return the success response
391 return $this->response->sendSuccess($isCreated);
392
393 }
394
395 public function updateCustomerProfileAddress(CustomerProfileRequest $request): \WP_REST_Response
396 {
397 // Call the method and store the response
398 $errorResponse = $this->checkUserLoggedIn();
399
400 // Check if there is an error response and return it if exists
401 if ($errorResponse !== null) {
402 return $errorResponse;
403 }
404
405 // Get the current logged-in customer
406 $customer = CustomerResource::getCurrentCustomer();
407
408 // Sanitize and retrieve the request data
409 $data = CustomerAddressResource::normalizeBusinessFields($request->getSafe($request->sanitize()));
410
411 // Retrieve the address ID from the request
412 $id = $request->getSafe('id', 'intval');
413
414
415 $address = CustomerAddresses::query()->findOrFail($id);
416
417 if ($address->customer_id != $customer->id) {
418 return $this->sendError([
419 'message' => __('You are not authorized to update this address', 'fluent-cart')
420 ]);
421 }
422
423 // Proceed with the update since IDs match
424 $isUpdated = CustomerAddressResource::update($data, $id);
425
426 // Check for errors during the update process
427 if (is_wp_error($isUpdated)) {
428 return $this->sendError([
429 'message' => $isUpdated->get_error_message()
430 ]);
431 }
432
433 // Return the success response
434 return $this->response->sendSuccess($isUpdated);
435
436 }
437
438 public function makePrimaryCustomerProfileAddress(Request $request): \WP_REST_Response
439 {
440 // Call the method and store the response
441 $errorResponse = $this->checkUserLoggedIn();
442 // Check if there is an error response and return it if exists
443 if ($errorResponse !== null) {
444 // Return error if user is not logged in
445 return $errorResponse;
446 }
447
448 $customer = CustomerResource::getCurrentCustomer();
449
450 $id = $request->getSafe('addressId', 'intval');
451
452
453 $address = CustomerAddresses::query()->findOrFail($id);
454
455 if ($address->customer_id != $customer->id) {
456 return $this->sendError([
457 'message' => __('You are not authorized to update this address', 'fluent-cart')
458 ]);
459 }
460
461 $isUpdated = CustomerAddressResource::makePrimary(
462 $customer->id,
463 $request->getSafe('addressId', 'intval'),
464 $request->getSafe('type', 'sanitize_text_field')
465 );
466
467 // Check for errors during the update process
468 if (is_wp_error($isUpdated)) {
469 return $this->sendError([
470 'message' => $isUpdated->get_error_message()
471 ]);
472 }
473
474 // Return the success response
475 return $this->response->sendSuccess($isUpdated);
476 }
477
478 public function deleteCustomerProfileAddress(Request $request)
479 {
480 // Call the method and store the response
481 $errorResponse = $this->checkUserLoggedIn();
482 if ($errorResponse !== null) {
483 return $errorResponse;
484 }
485
486 $id = $request->getSafe('addressId', 'intval');
487 if (!$id) {
488 return $this->sendError([
489 'message' => __('Address ID is required', 'fluent-cart')
490 ]);
491 }
492
493 $customer = CustomerResource::getCurrentCustomer();
494
495 $address = CustomerAddresses::query()->findOrFail($id);
496
497 if ($address->customer_id != $customer->id) {
498 return $this->sendError([
499 'message' => __('You are not authorized to update this address', 'fluent-cart')
500 ]);
501 }
502
503 $isDeleted = CustomerAddressResource::delete($id);
504
505 if (is_wp_error($isDeleted)) {
506 return $this->sendError([
507 'message' => $isDeleted->get_error_message()
508 ]);
509 }
510 return $this->response->sendSuccess($isDeleted);
511 }
512
513 public function getDownloads(Request $request): \WP_REST_Response
514 {
515 $page = $request->get('page', 1);
516 $perPage = $request->get('per_page', 10);
517
518 $errorResponse = $this->checkUserLoggedIn();
519
520 if ($errorResponse !== null) {
521 return $this->sendSuccess([
522 'message' => __('Success', 'fluent-cart'),
523 'data' => [],
524 'total' => 0,
525 'per_page' => $perPage,
526 'current_page' => $page,
527 'last_page' => 1
528 ]);
529 }
530
531 $customer = CustomerResource::getCurrentCustomer();
532
533 $orderItems = OrderItem::query()
534 ->with('variants')
535 ->withWhereHas('order', function ($query) use ($customer) {
536 $query->where('customer_id', $customer->id)
537 ->where(function (Builder $query) {
538 $query->whereIn('payment_status', Status::getOrderPaymentSuccessStatuses());
539 });
540 })
541 ->whereHas('product_downloads')
542 ->get();
543
544 $productIds = $orderItems->pluck('post_id')->unique()->values();
545 $orders = $orderItems->pluck('order');
546
547 // Extract all unique variation IDs from the customer's orders
548 $variationIds = $orders->pluck('order_items')
549 ->flatten()
550 ->pluck('variants.id')
551 ->filter()
552 ->unique()
553 ->values();
554
555 $downloads = ProductDownload::query()->whereIn('post_id', $productIds)->get()->filter(function ($download) use ($variationIds) {
556 $ids = $download->product_variation_id;
557 return empty($ids) || array_intersect($variationIds->toArray(), $ids);
558 });
559
560 $orderIdMapByPostID = [];
561 foreach ($orderItems as $orderItem) {
562 if (!isset($orderIdMapByPostID[$orderItem->post_id])) {
563 $orderIdMapByPostID[$orderItem->post_id] = [];
564 }
565 $orderIdMapByPostID[$orderItem->post_id][] = $orderItem->order_id;
566 }
567
568 $total = $downloads->count();
569 $paginated = $downloads->forPage($page, $perPage)->values();
570
571
572 $data = $paginated->map(function ($download) use ($orderIdMapByPostID) {
573 return [
574 'file_size' => $download->file_size,
575 'title' => $download->title,
576 'download_url' => Helper::generateDownloadFileLink(
577 $download,
578 Arr::get($orderIdMapByPostID, $download->post_id)
579 ),
580 ];
581 })->values();
582
583 return $this->sendSuccess([
584 'message' => __('Success', 'fluent-cart'),
585 'downloads' => [
586 'data' => $data,
587 'total' => $total,
588 'per_page' => $perPage,
589 'current_page' => $page,
590 'last_page' => (int)ceil($total / $perPage),
591 ]
592 ]);
593 }
594
595 /*
596 * Get upgradable paths for a given variation
597 */
598 public function getUpgradePaths(Request $request, $orderHash)
599 {
600
601 $currentCustomer = CustomerResource::getCurrentCustomer();
602 if (!$currentCustomer) {
603 return $this->sendError([
604 'message' => __('You must be logged in to view upgrade paths.', 'fluent-cart')
605 ]);
606 }
607
608 $order = Order::query()->where('uuid', $orderHash)
609 ->where('customer_id', $currentCustomer->id)
610 ->first();
611
612 if (!$order) {
613 return $this->sendError([
614 'message' => __('Order not found or you do not have permission to view it.', 'fluent-cart')
615 ]);
616 }
617
618 $variationId = $request->get('variation_id');
619 if (!$variationId) {
620 return [];
621 }
622
623 $upgradePaths = PlanUpgradeService::getUpgardePathsFromVariation($variationId, $orderHash);
624
625 return $this->sendSuccess([
626 'upgradePaths' => $upgradePaths
627 ]);
628 }
629
630 }
631