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
fluent-cart / app / Http / Controllers / FrontendControllers / CustomerController.php

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

528 lines 19.0 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\CustomerAddressResource;
6 use FluentCart\Api\Resource\FrontendResource\CustomerResource;
7 use FluentCart\App\Helpers\AddressHelper;
8 use FluentCart\App\Helpers\CustomerHelper;
9 use FluentCart\App\Hooks\Cart\WebCheckoutHandler;
10 use FluentCart\App\Http\Controllers\Controller;
11 use FluentCart\App\Http\Requests\CustomerRequest;
12 use FluentCart\App\Http\Requests\FrontendRequests\CustomerAddressRequest;
13 use FluentCart\App\Models\CustomerAddresses;
14 use FluentCart\App\Services\CustomerIdentity\EmailVerificationService;
15 use FluentCart\App\Services\Localization\LocalizationManager;
16 use FluentCart\App\Services\Renderer\AddressSelectRenderer;
17 use FluentCart\App\Services\Renderer\CheckoutFieldsSchema;
18 use FluentCart\Framework\Http\Request\Request;
19 use FluentCart\Framework\Support\Arr;
20 use FluentCart\App\Helpers\CartHelper;
21 use FluentCart\Framework\Support\Str;
22
23 class CustomerController extends Controller
24 {
25 public function index(Request $request)
26 {
27 return ['customers' => CustomerResource::get($request->all())];
28 }
29
30 public function store(CustomerRequest $request)
31 {
32 $data = $request->getSafe($request->sanitize());
33 $isCreated = CustomerResource::create($data);
34
35 if (is_wp_error($isCreated)) {
36 return $isCreated;
37 }
38 return $this->response->sendSuccess($isCreated);
39 }
40
41 public function updateDetails(CustomerRequest $request, $customerId)
42 {
43 $customer = \FluentCart\Api\Resource\CustomerResource::getCurrentCustomer();
44 if (empty($customer) || $customer->id != $customerId) {
45 return $this->sendError([
46 'message' => __('You are not authorized to update this customer', 'fluent-cart')
47 ]);
48 }
49 $data = $request->getSafe($request->sanitize());
50 $isUpdated = CustomerResource::update($data, $customerId);
51 if (is_wp_error($isUpdated)) {
52 return $isUpdated;
53 }
54 return $this->response->sendSuccess($isUpdated);
55 }
56
57 public function getDetails(Request $request, $customerId)
58 {
59 $customer = \FluentCart\Api\Resource\CustomerResource::getCurrentCustomer();
60 if (empty($customer) || $customer->id != $customerId) {
61 return $this->sendError([
62 'message' => __('You are not authorized to view this customer', 'fluent-cart')
63 ]);
64 }
65 // The customer never chooses its own eager loads. Forwarding the request's
66 // `with` here let a logged-in customer walk relations off their own record
67 // — `wpUser` for the WordPress user row, or `orders`/`subscriptions` for
68 // gateway identifiers the account pages never show. The ownership check
69 // above limits it to their own data, which is not the same as safe.
70 //
71 // These four are the customer's own addresses, which is what a profile
72 // detail view is for. Anything wider belongs to the admin endpoint.
73 return CustomerResource::find($customerId, [
74 'with' => [
75 'billing_address',
76 'shipping_address',
77 'primary_billing_address',
78 'primary_shipping_address',
79 ],
80 ]);
81 }
82
83 public function getAddress(Request $request, $customerId)
84 {
85 return CustomerAddressResource::get([
86 'customer_id' => $customerId,
87 'type' => $request->type
88 ]);
89 }
90
91 public function updateAddressSelect(Request $request, $customerAddressId)
92 {
93 if (is_user_logged_in() && EmailVerificationService::isRequired(get_current_user_id())) {
94 return $this->sendError([
95 'message' => __('Please verify your email before using saved addresses.', 'fluent-cart')
96 ], 403);
97 }
98
99 // The imported CustomerResource is the FrontendResource variant, which has
100 // no getCurrentCustomer — calling it there hits BaseResourceApi::__callStatic
101 // and 500s for every caller. The current-customer resolver lives on the
102 // core resource, same as getDetails/createAddress above.
103 $customer = \FluentCart\Api\Resource\CustomerResource::getCurrentCustomer();
104 if (!$customer) {
105 return $this->sendError([
106 'message' => __('Address not found', 'fluent-cart')
107 ]);
108 }
109
110 $addressModel = CustomerAddresses::query()
111 ->where('id', $customerAddressId)
112 ->where('customer_id', $customer->id)
113 ->first();
114
115 if (!$addressModel) {
116 return $this->sendError([
117 'message' => __('Address not found', 'fluent-cart')
118 ]);
119 }
120
121 $address = ['address' => $addressModel];
122
123 //update address into cart
124 $addressId = Arr::get($address, 'address.id');
125 $country = Arr::get($address, 'address.country');
126 $state = Arr::get($address, 'address.state');
127 $type = Arr::get($address, 'address.type', 'billing');
128 $cart = CartHelper::getCart($request->getSafe('fct_cart_hash', 'sanitize_text_field'));
129
130 $checkoutData = Arr::wrap($cart->checkout_data);
131 Arr::set($checkoutData, 'form_data.' . $type . '_address_id', $addressId);
132 Arr::set($checkoutData, 'form_data.' . $type . '_country', $country);
133 Arr::set($checkoutData, 'form_data.' . $type . '_state', $state);
134
135 if ($type === 'billing' && Arr::get($checkoutData, 'form_data.ship_to_different', 'no') === 'no') {
136 Arr::set($checkoutData, 'form_data.shipping_address_id', $addressId);
137 Arr::set($checkoutData, 'form_data.shipping_country', $country);
138 Arr::set($checkoutData, 'form_data.shipping_state', $state);
139 }
140
141 $cart->checkout_data = $checkoutData;
142 $cart->save();
143
144 $formattedAddress = Arr::get($address, 'address.formatted_address');
145
146 // Use output buffering to generate HTML
147 ob_start();
148
149 // Extract the address parts
150 $addressParts = [
151 trim(Arr::get($formattedAddress, 'address_1') ?? ''),
152 trim(Arr::get($formattedAddress, 'address_2') ?? ''),
153 trim(Arr::get($formattedAddress, 'city') ?? ''),
154 trim(Arr::get($formattedAddress, 'state') ?? ''),
155 trim(Arr::get($formattedAddress, 'country') ?? ''),
156 ];
157
158 // Filter out empty or null parts
159 $addressParts = array_filter($addressParts, function ($part) {
160 return $part !== '';
161 });
162
163 // Join parts with comma and space
164 $addressString = implode(', ', $addressParts);
165
166 do_action('fluent_cart/views/checkout_page_form_address_info_wrapper', [
167 'name' => Arr::get($address, 'address.name'),
168 'phone' => Arr::get($address, 'address.phone'),
169 'label' => Arr::get($address, 'address.label'),
170 'address' => $addressString,
171 ]);
172 $htmlOutput = ob_get_clean();
173
174 $result = (new WebCheckoutHandler())->handleGetCheckoutSummaryViewAjax();
175
176 return $this->response->sendSuccess([
177 'message' => __('Address Attached', 'fluent-cart'),
178 'data' => $htmlOutput,
179 'fragments' => $result['fragments']
180 ]);
181 }
182
183
184 public function createAddress(Request $request) //CustomerAddressRequest
185 {
186 if (is_user_logged_in() && EmailVerificationService::isRequired(get_current_user_id())) {
187 return $this->sendError([
188 'message' => __('Please verify your email before using saved addresses.', 'fluent-cart')
189 ], 403);
190 }
191
192 $customer = \FluentCart\Api\Resource\CustomerResource::getCurrentCustomer();
193
194 if (empty($customer)) {
195 return $this->sendError([
196 'message' => __('You don\'t have any associated account', 'fluent-cart')
197 ]);
198 }
199
200 $customerId = $customer->id;
201 $data = $request->all();//$request->getSafe($request->sanitize());
202
203 $validatedData = $this->validateAddressData($data);
204 if (is_wp_error($validatedData)) {
205 wp_send_json([
206 'status' => 'failed',
207 'errors' => $validatedData->get_error_data(),
208 ], 422);
209 }
210
211
212 // Sanitize all fields with special handling for emails
213 $sanitized = [];
214
215 foreach ($validatedData as $key => $value) {
216 if (is_array($value)) {
217 // Recursively sanitize nested arrays
218 $sanitized[$key] = array_map('sanitize_text_field', $value);
219 continue;
220 }
221
222 // If key contains "email", sanitize as email
223 if (stripos($key, 'email') !== false && !empty($value)) {
224 $sanitized[$key] = sanitize_email($value);
225 continue;
226 }
227
228 // Default text sanitization
229 $sanitized[$key] = sanitize_text_field($value);
230 }
231
232 $data = $sanitized;
233
234
235 $data = self::formattedAddress($data);
236
237 // Validate label length
238 if (!empty($data['label']) && strlen($data['label']) > 15) {
239 return $this->sendError([
240 'message' => __('Label must not exceed 15 characters.', 'fluent-cart')
241 ]);
242 }
243
244 $data['status'] = 'active';
245 $isCreated = CustomerAddressResource::create($data, ['id' => $customerId]);
246
247 if (is_wp_error($isCreated)) {
248 return $isCreated;
249 }
250
251
252 $isCreated = $isCreated['data'];
253
254 $cart = CartHelper::getCart();
255 $requiredShipping = $cart->requireShipping();
256 $type = Arr::get($data, 'type', 'billing');
257 $config = [
258 'type' => $type,
259 'product_type' => $requiredShipping ? 'physical' : 'digital',
260 'with_shipping' => $requiredShipping
261 ];
262
263 $customer = \FluentCart\Api\Resource\CustomerResource::getCurrentCustomer();
264 $addresses = AddressHelper::getCustomerValidatedAddresses($config, $customer);
265 $address = AddressHelper::getPrimaryAddress($addresses, $config, $customer, $type);
266 $requirementsFields = CheckoutFieldsSchema::getCheckoutFieldsRequirements(
267 $type,
268 Arr::get($config, 'product_type'),
269 Arr::get($config, 'with_shipping')
270 );
271
272 ob_start();
273 (new AddressSelectRenderer(
274 $addresses,
275 $address,
276 $requirementsFields,
277 $type
278 ))->renderAddressSelector();
279 $selectors = ob_get_clean();
280
281
282 return $this->response->sendSuccess([
283 'message' => __('Customer address created successfully!', 'fluent-cart'),
284 'fragment' => [
285 [
286 'selector' => '[data-fluent-cart-checkout-page-form-address-modal-address-selector-button-wrapper]',
287 'content' => $selectors,
288 'type' => 'replace'
289 ]
290 ]
291 ]);
292 }
293
294 private function validateAddressData($data)
295 {
296 $type = sanitize_text_field(Arr::get($data, 'type'));
297 $fulfillmentType = sanitize_text_field(Arr::get($data, 'product_type'));
298
299 // Address creation validates against its own type's rules only — no shipping merge
300 $validations = array_filter(CheckoutFieldsSchema::getCheckoutFieldsRequirements($type, $fulfillmentType, false));
301
302 // Name fields are validated via basic_info, not address sections
303 unset($validations['full_name'], $validations['first_name'], $validations['last_name']);
304
305 $address = [];
306 foreach ($validations as $key => $validation) {
307 $address[$key] = Arr::get($data, $type . '_' . $key, '');
308 }
309
310 $country = $this->resolveCountryForValidation($address, $type);
311 $errors = [];
312
313 foreach ($validations as $key => $rule) {
314 $value = Arr::get($address, $key, '');
315 $prefixedKey = $type . '_' . $key;
316 $titledKey = Str::headline($key);
317
318 $fieldErrors = $this->validateAddressField($key, $value, $rule, $titledKey, $country);
319 if (!empty($fieldErrors)) {
320 $errors[$prefixedKey] = $fieldErrors;
321 }
322 }
323
324 if (!empty($errors)) {
325 return new \Wp_Error('validation_error', __('Validation error', 'fluent-cart'), $errors);
326 }
327
328 return $data;
329 }
330
331 private function resolveCountryForValidation(array $address, string $addressType): string
332 {
333 $country = Arr::get($address, 'country', '');
334 if (!empty($country)) {
335 return $country;
336 }
337
338 // Fall back to store country only when the country field is disabled by admin
339 $fieldSettings = CheckoutFieldsSchema::getFieldsSettings();
340 $countryEnabled = Arr::get($fieldSettings, $addressType . '_address.country.enabled', 'no') === 'yes';
341
342 if (!$countryEnabled) {
343 return (new \FluentCart\Api\StoreSettings())->get('store_country') ?: '';
344 }
345
346 return '';
347 }
348
349 private function validateAddressField(string $field, $value, string $rule, string $label, string $country): array
350 {
351 $localization = LocalizationManager::getInstance();
352
353 switch ($field) {
354 case 'country':
355 return $this->validateCountryField($value, $rule, $label, $localization);
356 case 'state':
357 return $this->validateStateField($value, $rule, $label, $country, $localization);
358 case 'postcode':
359 if ($rule === 'required' && empty($value)) {
360 return ['required' => sprintf(__('%s is required.', 'fluent-cart'), $label)];
361 }
362 if (!empty($value) && !empty($country) && $localization->postcode->isValid($value, $country) === false) {
363 return ['invalid' => sprintf(__('%s is invalid.', 'fluent-cart'), $label)];
364 }
365 return [];
366 default:
367 if ($rule === 'required' && empty($value)) {
368 return ['required' => sprintf(__('%s is required.', 'fluent-cart'), $label)];
369 }
370 return [];
371 }
372 }
373
374 private function validateCountryField($value, string $rule, string $label, LocalizationManager $localization): array
375 {
376 if ($rule === 'required' && empty($value)) {
377 return ['required' => sprintf(__('%s is required.', 'fluent-cart'), $label)];
378 }
379
380 if (!empty($value) && !Arr::has($localization->countries(), $value)) {
381 return ['invalid' => sprintf(__('%s is invalid.', 'fluent-cart'), $label)];
382 }
383
384 return [];
385 }
386
387 private function validateStateField($value, string $rule, string $label, string $country, LocalizationManager $localization): array
388 {
389 if (empty($country)) {
390 return [];
391 }
392
393 $states = $localization->statesOptions($country);
394 if (empty($states)) {
395 return [];
396 }
397
398 $stateValues = array_column($states, 'value');
399
400 if ($rule === 'required' && empty($value)) {
401 return ['required' => sprintf(__('%s is required.', 'fluent-cart'), $label)];
402 }
403
404 if (!empty($value) && !in_array($value, $stateValues)) {
405 return ['invalid' => sprintf(__('%s is invalid.', 'fluent-cart'), $label)];
406 }
407
408 return [];
409 }
410
411 public function updateAddress(CustomerAddressRequest $request)
412 {
413
414 $data = $request->getSafe($request->sanitize());
415 $data = self::formattedAddress($data);
416 $id = Arr::get($request->get('address'), 'id');
417
418 $address = CustomerAddresses::query()->findOrFail($id);
419
420 $customer = \FluentCart\Api\Resource\CustomerResource::getCurrentCustomer();
421 if (empty($customer) || $customer->id != $address->customer_id) {
422 return $this->sendError([
423 'message' => __('You are not authorized to update this address', 'fluent-cart')
424 ]);
425 }
426
427 if ($address->update($data)) {
428 return $this->response->sendSuccess([
429 'message' => __('Address updated successfully', 'fluent-cart')
430 ]);
431 }
432
433 return $this->sendError([
434 'message' => __('Failed to update address', 'fluent-cart')
435 ]);
436 }
437
438 public function removeAddress(Request $request)
439 {
440
441 $id = Arr::get($request->address, 'id', false);
442
443 $address = CustomerAddresses::query()->findOrFail($id);
444
445 $customer = \FluentCart\Api\Resource\CustomerResource::getCurrentCustomer();
446
447 if (empty($customer) || $customer->id != $address->customer_id) {
448 return $this->sendError([
449 'message' => __('You are not authorized to delete this address', 'fluent-cart')
450 ]);
451 }
452
453 if ($address->delete()) {
454 return $this->response->sendSuccess([
455 'message' => __('Address deleted successfully', 'fluent-cart')
456 ]);
457 }
458
459 return $this->sendError([
460 'message' => __('Failed to delete address', 'fluent-cart')
461 ]);
462 }
463
464 public function setAddressPrimary(Request $request, $customerId)
465 {
466 $customer = \FluentCart\Api\Resource\CustomerResource::getCurrentCustomer();
467 if (empty($customer) || $customer->id != $customerId) {
468 return $this->sendError([
469 'message' => __('You are not authorized to update this address', 'fluent-cart')
470 ]);
471 }
472
473 $isUpdated = CustomerAddressResource::makePrimary($customerId, $request->all());
474
475 if (is_wp_error($isUpdated)) {
476 return $isUpdated;
477 }
478 return $this->response->sendSuccess($isUpdated);
479 }
480
481 public function getCustomerOrders(Request $request, $customerId): array
482 {
483
484 $customer = \FluentCart\Api\Resource\CustomerResource::getCurrentCustomer();
485 if (empty($customer) || $customer->id != $customerId) {
486 return [
487 'orders' => []
488 ];
489 }
490 return [
491 'orders' => CustomerResource::getOrders($request->all(), $customerId)
492 ];
493 }
494
495 private static function formattedAddress(array $data = []): array
496 {
497 $address = [];
498
499 foreach ($data as $key => $value) {
500 if (strpos($key, "billing_") === 0) {
501 $newKey = str_replace("billing_", "", $key);
502 $address[$newKey] = $value;
503 }
504 if (strpos($key, "shipping_") === 0) {
505 $newKey = str_replace("shipping_", "", $key);
506 $address[$newKey] = $value;
507 }
508 }
509
510 $address['type'] = Arr::get($data, 'type');
511
512 if (empty($address['name'])) {
513 if (!empty($address['full_name'])) {
514 $address['name'] = $address['full_name'];
515 } else {
516 $firstName = trim(Arr::get($address, 'first_name', ''));
517 $lastName = trim(Arr::get($address, 'last_name', ''));
518 if ($firstName || $lastName) {
519 $address['name'] = trim($firstName . ' ' . $lastName);
520 }
521 }
522 }
523
524 return $address;
525 }
526
527 }
528