Files
WooNooW/includes/Frontend/AccountController.php
Dwindi Ramadhana 0e561d9e8c fix: checkout issues - hidden fields, coupons, shipping in order totals
1. Hidden fields now properly hidden in SPA
   - Added billing_postcode and shipping_postcode to isFieldHidden checks
   - Fields with type='hidden' from PHP now conditionally rendered

2. Coupons now applied to order total
   - Added coupons array to order submission payload
   - CartController now calls calculate_totals() before reading discounts
   - Returns per-coupon discount amounts {code, discount, type}

3. Shipping now applied to order total
   - Already handled in submit() via find_shipping_rate_for_order
   - Frontend now sends shipping_method in payload

4. Order details now include shipping/tracking info
   - checkout/order/{id} API includes shipping_lines, tracking_number, tracking_url
   - account/orders/{id} API includes same shipping/tracking fields
   - Tracking info read from multiple plugin meta keys

5. Thank you/OrderDetails page shows shipping method and AWB
   - Shipping Method section with courier name and cost
   - AWB tracking for processing/completed orders with Track Shipment button
2026-01-08 23:04:31 +07:00

560 lines
21 KiB
PHP

<?php
namespace WooNooW\Frontend;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
/**
* Account Controller - Customer account API
* Handles customer account operations for customer-spa
*/
class AccountController {
/**
* Register REST API routes
*/
public static function register_routes() {
$namespace = 'woonoow/v1';
// Get customer orders
register_rest_route($namespace, '/account/orders', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_orders'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
'args' => [
'page' => [
'default' => 1,
'sanitize_callback' => 'absint',
],
'per_page' => [
'default' => 10,
'sanitize_callback' => 'absint',
],
],
]);
// Get single order
register_rest_route($namespace, '/account/orders/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_order'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
'args' => [
'id' => [
'validate_callback' => function($param) {
return is_numeric($param);
},
],
],
]);
// Get customer profile
register_rest_route($namespace, '/account/profile', [
[
'methods' => 'GET',
'callback' => [__CLASS__, 'get_profile'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
],
[
'methods' => 'POST',
'callback' => [__CLASS__, 'update_profile'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
],
]);
// Update password
register_rest_route($namespace, '/account/password', [
'methods' => 'POST',
'callback' => [__CLASS__, 'update_password'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
'args' => [
'current_password' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
'new_password' => [
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
]);
// Address routes moved to AddressController
// Get downloads (for digital products)
register_rest_route($namespace, '/account/downloads', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_downloads'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
]);
// Avatar upload
register_rest_route($namespace, '/account/avatar', [
[
'methods' => 'POST',
'callback' => [__CLASS__, 'upload_avatar'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
],
[
'methods' => 'DELETE',
'callback' => [__CLASS__, 'delete_avatar'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
],
]);
// Get avatar settings (check if custom avatars are enabled)
register_rest_route($namespace, '/account/avatar-settings', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_avatar_settings'],
'permission_callback' => [__CLASS__, 'check_customer_permission'],
]);
}
/**
* Check if user is logged in
*/
public static function check_customer_permission() {
return is_user_logged_in();
}
/**
* Get customer orders
*/
public static function get_orders(WP_REST_Request $request) {
$customer_id = get_current_user_id();
$page = $request->get_param('page');
$per_page = $request->get_param('per_page');
$args = [
'customer_id' => $customer_id,
'limit' => $per_page,
'page' => $page,
'orderby' => 'date',
'order' => 'DESC',
];
$orders = wc_get_orders($args);
$formatted_orders = array_map(function($order) {
return self::format_order($order);
}, $orders);
// Get total count
$total_args = [
'customer_id' => $customer_id,
'return' => 'ids',
];
$total = count(wc_get_orders($total_args));
return new WP_REST_Response([
'orders' => $formatted_orders,
'total' => $total,
'total_pages' => ceil($total / $per_page),
'page' => $page,
'per_page' => $per_page,
], 200);
}
/**
* Get single order
*/
public static function get_order(WP_REST_Request $request) {
$order_id = $request->get_param('id');
$customer_id = get_current_user_id();
$order = wc_get_order($order_id);
if (!$order) {
return new WP_Error('order_not_found', 'Order not found', ['status' => 404]);
}
// Check if order belongs to customer
if ($order->get_customer_id() !== $customer_id) {
return new WP_Error('forbidden', 'You do not have permission to view this order', ['status' => 403]);
}
return new WP_REST_Response(self::format_order($order, true), 200);
}
/**
* Get customer profile
*/
public static function get_profile(WP_REST_Request $request) {
$user_id = get_current_user_id();
$user = get_userdata($user_id);
if (!$user) {
return new WP_Error('user_not_found', 'User not found', ['status' => 404]);
}
return new WP_REST_Response([
'id' => $user->ID,
'email' => $user->user_email,
'first_name' => get_user_meta($user_id, 'first_name', true),
'last_name' => get_user_meta($user_id, 'last_name', true),
'username' => $user->user_login,
], 200);
}
/**
* Update customer profile
*/
public static function update_profile(WP_REST_Request $request) {
$user_id = get_current_user_id();
$first_name = $request->get_param('first_name');
$last_name = $request->get_param('last_name');
$email = $request->get_param('email');
// Update user meta
if ($first_name !== null) {
update_user_meta($user_id, 'first_name', sanitize_text_field($first_name));
}
if ($last_name !== null) {
update_user_meta($user_id, 'last_name', sanitize_text_field($last_name));
}
// Update email if changed
if ($email !== null && is_email($email)) {
$user = get_userdata($user_id);
if ($user->user_email !== $email) {
wp_update_user([
'ID' => $user_id,
'user_email' => $email,
]);
}
}
return new WP_REST_Response([
'message' => 'Profile updated successfully',
], 200);
}
/**
* Upload customer avatar
*/
public static function upload_avatar(WP_REST_Request $request) {
// Check if custom avatars are enabled (stored as 'yes' or 'no')
$allow_custom_avatar = get_option('woonoow_allow_custom_avatar', 'no') === 'yes';
if (!$allow_custom_avatar) {
return new WP_Error('avatar_disabled', 'Custom avatars are not enabled', ['status' => 403]);
}
$user_id = get_current_user_id();
// Check for file data (base64 or URL)
$avatar_data = $request->get_param('avatar');
$avatar_url = $request->get_param('avatar_url');
if ($avatar_url) {
// Avatar URL provided (from media library)
update_user_meta($user_id, 'woonoow_custom_avatar', esc_url_raw($avatar_url));
return new WP_REST_Response([
'success' => true,
'message' => 'Avatar updated successfully',
'avatar_url' => $avatar_url,
], 200);
}
if (!$avatar_data) {
return new WP_Error('no_avatar', 'No avatar data provided', ['status' => 400]);
}
// Handle base64 image upload
if (strpos($avatar_data, 'data:image') === 0) {
// Extract base64 data
$parts = explode(',', $avatar_data);
if (count($parts) !== 2) {
return new WP_Error('invalid_data', 'Invalid image data format', ['status' => 400]);
}
$image_data = base64_decode($parts[1]);
// Determine file extension from mime type
preg_match('/data:image\/(\w+);/', $parts[0], $matches);
$extension = $matches[1] ?? 'png';
// Validate extension
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!in_array(strtolower($extension), $allowed)) {
return new WP_Error('invalid_type', 'Invalid image type. Allowed: jpg, png, gif, webp', ['status' => 400]);
}
// Create upload directory
$upload_dir = wp_upload_dir();
$avatar_dir = $upload_dir['basedir'] . '/woonoow-avatars';
if (!file_exists($avatar_dir)) {
wp_mkdir_p($avatar_dir);
}
// Generate unique filename
$filename = 'avatar-' . $user_id . '-' . time() . '.' . $extension;
$filepath = $avatar_dir . '/' . $filename;
// Delete old avatar if exists
$old_avatar = get_user_meta($user_id, 'woonoow_custom_avatar', true);
if ($old_avatar) {
$old_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $old_avatar);
if (file_exists($old_path)) {
unlink($old_path);
}
}
// Save new avatar
if (file_put_contents($filepath, $image_data) === false) {
return new WP_Error('upload_failed', 'Failed to save avatar', ['status' => 500]);
}
// Get URL
$avatar_url = $upload_dir['baseurl'] . '/woonoow-avatars/' . $filename;
// Save to user meta
update_user_meta($user_id, 'woonoow_custom_avatar', $avatar_url);
return new WP_REST_Response([
'success' => true,
'message' => 'Avatar uploaded successfully',
'avatar_url' => $avatar_url,
], 200);
}
return new WP_Error('invalid_data', 'Invalid avatar data', ['status' => 400]);
}
/**
* Delete customer avatar
*/
public static function delete_avatar(WP_REST_Request $request) {
$user_id = get_current_user_id();
// Get current avatar
$avatar_url = get_user_meta($user_id, 'woonoow_custom_avatar', true);
if ($avatar_url) {
// Try to delete the file
$upload_dir = wp_upload_dir();
$filepath = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $avatar_url);
if (file_exists($filepath)) {
unlink($filepath);
}
// Remove from user meta
delete_user_meta($user_id, 'woonoow_custom_avatar');
}
return new WP_REST_Response([
'success' => true,
'message' => 'Avatar removed successfully',
], 200);
}
/**
* Get avatar settings
*/
public static function get_avatar_settings(WP_REST_Request $request) {
$user_id = get_current_user_id();
// Use correct option key (stored as 'yes' or 'no')
$allow_custom_avatar = get_option('woonoow_allow_custom_avatar', 'no') === 'yes';
return new WP_REST_Response([
'allow_custom_avatar' => $allow_custom_avatar,
'current_avatar' => get_user_meta($user_id, 'woonoow_custom_avatar', true) ?: null,
'gravatar_url' => get_avatar_url($user_id),
], 200);
}
/**
* Update password
*/
public static function update_password(WP_REST_Request $request) {
$user_id = get_current_user_id();
$current_password = $request->get_param('current_password');
$new_password = $request->get_param('new_password');
$user = get_userdata($user_id);
// Verify current password
if (!wp_check_password($current_password, $user->user_pass, $user_id)) {
return new WP_Error('invalid_password', 'Current password is incorrect', ['status' => 400]);
}
// Update password
wp_set_password($new_password, $user_id);
return new WP_REST_Response([
'message' => 'Password updated successfully',
], 200);
}
/**
* Get customer addresses
*/
public static function get_addresses(WP_REST_Request $request) {
$customer_id = get_current_user_id();
$customer = new \WC_Customer($customer_id);
return new WP_REST_Response([
'billing' => [
'first_name' => $customer->get_billing_first_name(),
'last_name' => $customer->get_billing_last_name(),
'company' => $customer->get_billing_company(),
'address_1' => $customer->get_billing_address_1(),
'address_2' => $customer->get_billing_address_2(),
'city' => $customer->get_billing_city(),
'state' => $customer->get_billing_state(),
'postcode' => $customer->get_billing_postcode(),
'country' => $customer->get_billing_country(),
'email' => $customer->get_billing_email(),
'phone' => $customer->get_billing_phone(),
],
'shipping' => [
'first_name' => $customer->get_shipping_first_name(),
'last_name' => $customer->get_shipping_last_name(),
'company' => $customer->get_shipping_company(),
'address_1' => $customer->get_shipping_address_1(),
'address_2' => $customer->get_shipping_address_2(),
'city' => $customer->get_shipping_city(),
'state' => $customer->get_shipping_state(),
'postcode' => $customer->get_shipping_postcode(),
'country' => $customer->get_shipping_country(),
],
], 200);
}
/**
* Update customer addresses
*/
public static function update_addresses(WP_REST_Request $request) {
$customer_id = get_current_user_id();
$customer = new \WC_Customer($customer_id);
$billing = $request->get_param('billing');
$shipping = $request->get_param('shipping');
// Update billing address
if ($billing) {
foreach ($billing as $key => $value) {
$method = 'set_billing_' . $key;
if (method_exists($customer, $method)) {
$customer->$method(sanitize_text_field($value));
}
}
}
// Update shipping address
if ($shipping) {
foreach ($shipping as $key => $value) {
$method = 'set_shipping_' . $key;
if (method_exists($customer, $method)) {
$customer->$method(sanitize_text_field($value));
}
}
}
$customer->save();
return new WP_REST_Response([
'message' => 'Addresses updated successfully',
], 200);
}
/**
* Get customer downloads
*/
public static function get_downloads(WP_REST_Request $request) {
$customer_id = get_current_user_id();
$downloads = wc_get_customer_available_downloads($customer_id);
return new WP_REST_Response($downloads, 200);
}
/**
* Format order data for API response
*/
private static function format_order($order, $detailed = false) {
$payment_title = $order->get_payment_method_title();
if (empty($payment_title)) {
$payment_title = $order->get_payment_method() ?: 'Not specified';
}
$data = [
'id' => $order->get_id(),
'order_number' => $order->get_order_number(),
'status' => $order->get_status(),
'date' => $order->get_date_created()->date('Y-m-d H:i:s'),
'total' => html_entity_decode(strip_tags(wc_price($order->get_total()))),
'currency' => $order->get_currency(),
'payment_method_title' => $payment_title,
];
if ($detailed) {
$items = $order->get_items();
$data['items'] = is_array($items) ? array_values(array_map(function($item) {
$product = $item->get_product();
return [
'id' => $item->get_id(),
'name' => $item->get_name(),
'quantity' => $item->get_quantity(),
'total' => html_entity_decode(strip_tags(wc_price($item->get_total()))),
'image' => $product ? wp_get_attachment_url($product->get_image_id()) : '',
];
}, $items)) : [];
// Check if order needs shipping (not virtual-only)
$needs_shipping = false;
foreach ($order->get_items() as $item) {
$product = $item->get_product();
if ($product && !$product->is_virtual()) {
$needs_shipping = true;
break;
}
}
$data['billing'] = $order->get_address('billing');
$data['shipping'] = $order->get_address('shipping');
$data['needs_shipping'] = $needs_shipping;
$data['subtotal'] = html_entity_decode(strip_tags(wc_price($order->get_subtotal())));
$data['shipping_total'] = html_entity_decode(strip_tags(wc_price($order->get_shipping_total())));
$data['tax_total'] = html_entity_decode(strip_tags(wc_price($order->get_total_tax())));
$data['discount_total'] = html_entity_decode(strip_tags(wc_price($order->get_discount_total())));
// Shipping lines with method details
$shipping_lines = [];
foreach ($order->get_shipping_methods() as $shipping_item) {
$shipping_lines[] = [
'id' => $shipping_item->get_id(),
'method_title' => $shipping_item->get_method_title(),
'method_id' => $shipping_item->get_method_id(),
'total' => html_entity_decode(strip_tags(wc_price($shipping_item->get_total()))),
];
}
$data['shipping_lines'] = $shipping_lines;
// Tracking info (from various shipping tracking plugins)
$tracking_number = $order->get_meta('_tracking_number')
?: $order->get_meta('_wc_shipment_tracking_items')
?: $order->get_meta('_rajaongkir_awb_number')
?: '';
$tracking_url = $order->get_meta('_tracking_url')
?: $order->get_meta('_rajaongkir_tracking_url')
?: '';
// Handle WooCommerce Shipment Tracking plugin format (array)
if (is_array($tracking_number) && !empty($tracking_number)) {
$first_tracking = reset($tracking_number);
$tracking_number = $first_tracking['tracking_number'] ?? '';
$tracking_url = $first_tracking['tracking_url'] ?? $tracking_url;
}
$data['tracking_number'] = $tracking_number;
$data['tracking_url'] = $tracking_url;
}
return $data;
}
}