PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.1
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
fluent-cart / app / Http / Controllers / FrontendControllers / CustomerProfileController.php

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

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