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 / CustomerController.php

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

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