← All changes
|
app/Http/Controllers/FrontendControllers/CustomerProfileController.php
+108
-9
1.3.21
→
1.6.5
View file →
| @@ -38,8 +38,15 @@ | ||
| 38 | 38 | |
| 39 | 39 | |
| 40 | 40 | class CustomerProfileController extends BaseFrontendController |
| 41 | 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 | + ]; | |
| 42 | 49 | |
| 43 | 50 | /** |
| 44 | 51 | * Handle the request to retrieve the customer's orders. |
| 45 | 52 | * |
| @@ -71,9 +78,9 @@ | ||
| 71 | 78 | |
| 72 | 79 | |
| 73 | 80 | $orders = Order::query() |
| 74 | 81 | ->with(['order_items' => function ($query) { |
| 75 | - $query->select('id', 'order_id', 'post_title', 'title', 'quantity', 'payment_type', 'line_meta'); | |
| 82 | + $query->select('id', 'order_id', 'object_id', 'post_title', 'title', 'quantity', 'payment_type', 'line_meta', 'other_info'); | |
| 76 | 83 | }]) |
| 77 | 84 | ->where('customer_id', $customer->id) |
| 78 | 85 | ->where(function ($query) { |
| 79 | 86 | $query |
| @@ -91,8 +98,9 @@ | ||
| 91 | 98 | return [ |
| 92 | 99 | 'created_at' => $order->created_at->format('Y-m-d H:i:s'), |
| 93 | 100 | 'invoice_no' => $order->invoice_no, |
| 94 | 101 | 'total_amount' => $order->total_amount, |
| 102 | + 'currency' => $order->currency, | |
| 95 | 103 | 'uuid' => $order->uuid, |
| 96 | 104 | 'type' => $order->type, |
| 97 | 105 | 'status' => $order->status, |
| 98 | 106 | 'renewals_count' => $order->renewals_count, |
| @@ -97,14 +105,15 @@ | ||
| 97 | 105 | 'status' => $order->status, |
| 98 | 106 | 'renewals_count' => $order->renewals_count, |
| 99 | 107 | 'order_items' => $order->order_items->map(function ($item) { |
| 100 | 108 | return [ |
| 101 | - 'id' => $item->id, | |
| 102 | - 'post_title' => $item->post_title, | |
| 103 | - 'title' => $item->title, | |
| 104 | - 'quantity' => $item->quantity, | |
| 105 | - 'payment_type' => $item->payment_type, | |
| 106 | - 'line_meta' => [ | |
| 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' => [ | |
| 107 | 116 | 'bundle_parent_item_id' => Arr::get($item, 'line_meta.bundle_parent_item_id', null), |
| 108 | 117 | ] |
| 109 | 118 | ]; |
| 110 | 119 | }), |
| @@ -177,8 +186,98 @@ | ||
| 177 | 186 | 'data' => $userData |
| 178 | 187 | ]); |
| 179 | 188 | } |
| 180 | 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 | + | |
| 181 | 280 | public function updateCustomerProfileDetails(CustomerProfileAccountDetailsRequest $request): \WP_REST_Response |
| 182 | 281 | { |
| 183 | 282 | $errorResponse = $this->checkUserLoggedIn(); |
| 184 | 283 | |
| @@ -272,9 +371,9 @@ | ||
| 272 | 371 | // Get the current logged-in customer |
| 273 | 372 | $customer = CustomerResource::getCurrentCustomer(true); |
| 274 | 373 | |
| 275 | 374 | // Sanitize and retrieve the request data |
| 276 | - $data = $request->getSafe($request->sanitize()); | |
| 375 | + $data = CustomerAddressResource::normalizeBusinessFields($request->getSafe($request->sanitize())); | |
| 277 | 376 | |
| 278 | 377 | // Attempt to create a new address for the logged-in customer |
| 279 | 378 | $isCreated = CustomerAddressResource::create( |
| 280 | 379 | $data, |
| @@ -306,9 +405,9 @@ | ||
| 306 | 405 | // Get the current logged-in customer |
| 307 | 406 | $customer = CustomerResource::getCurrentCustomer(); |
| 308 | 407 | |
| 309 | 408 | // Sanitize and retrieve the request data |
| 310 | - $data = $request->getSafe($request->sanitize()); | |
| 409 | + $data = CustomerAddressResource::normalizeBusinessFields($request->getSafe($request->sanitize())); | |
| 311 | 410 | |
| 312 | 411 | // Retrieve the address ID from the request |
| 313 | 412 | $id = $request->getSafe('id', 'intval'); |
| 314 | 413 | |