/** * Plugin Name: Vardo Order Create API * Description: Single order-creation core for the Vardo mobile app. Serves both * POST /wp-json/v1/create-order (customer booking flow) * POST /wp-json/v1/support/create-order (admin/manager manual booking) * Replaces "Vardo Mobile App Order API" (1.0.1) and * "Vardo Support Order Endpoint" (2.4) — deactivate both when enabling this. * Request/response formats of both routes are unchanged, so older app * builds keep working. Optional `Authorization: Bearer ` * header (same token as custom/v1/validate-auth-token) is verified when sent. * Version: 3.0.0 */ if (!defined('ABSPATH')) { exit; } // Legacy shared secrets (still baked into older app builds). Override via wp-config.php. if (!defined('VARDO_ORDER_KEY')) { define('VARDO_ORDER_KEY', 'PgTaJ9DK0Kv6LS6SQEC'); } if (!defined('VARDO_SUPPORT_ORDER_KEY')) { define('VARDO_SUPPORT_ORDER_KEY', '768DmIZ0oclR1BAfo'); } function vardo_oc_staff_roles() { return ['app_admin', 'app_manager', 'administrator']; } // --- Routes ----------------------------------------------------------------- add_action('rest_api_init', function () { register_rest_route('v1', '/create-order', array( 'methods' => 'POST', 'callback' => 'vardo_oc_customer_callback', 'permission_callback' => '__return_true', )); register_rest_route('v1', '/support/create-order', array( 'methods' => 'POST', 'callback' => 'vardo_oc_support_callback', 'permission_callback' => '__return_true', )); }); /** * Send WooCommerce transactional emails after the response instead of inside * the request. WooCommerce reads this filter on `init` (before the REST * callback runs), so it has to be decided from the request URI. */ add_filter('woocommerce_defer_transactional_emails', function ($defer) { return vardo_oc_is_order_create_request() ? true : $defer; }); function vardo_oc_is_order_create_request() { $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : ''; $route = isset($_GET['rest_route']) ? (string) $_GET['rest_route'] : ''; foreach (['/v1/create-order', '/v1/support/create-order'] as $path) { if (strpos($uri, '/wp-json' . $path) !== false || rtrim($route, '/') === $path) { return true; } } return false; } // --- Auth ------------------------------------------------------------------- /** * Resolve the WP user from `Authorization: Bearer `. * Same lookup as custom_validate_auth_token() in firebase-custom-auth.php. * * @return int|null|false user ID; null when no token was sent; false when a token was sent but is invalid */ function vardo_oc_user_from_token(WP_REST_Request $request) { $header = (string) $request->get_header('authorization'); if ($header === '' && function_exists('getallheaders')) { $headers = getallheaders(); $header = (string) ($headers['Authorization'] ?? $headers['authorization'] ?? ''); } $token = trim(str_replace('Bearer ', '', $header)); if ($token === '') { return null; } $users = (new WP_User_Query([ 'meta_key' => 'auth_token', 'meta_value' => $token, 'number' => 1, 'fields' => 'ID', ]))->get_results(); if (empty($users)) { return false; } $user_id = (int) $users[0]; $expiration = (int) get_user_meta($user_id, 'auth_token_expiration', true); if ($expiration && $expiration < time()) { return false; } return $user_id; } function vardo_oc_secret_ok($params, $expected) { return !empty($params['secret_key']) && is_string($params['secret_key']) && hash_equals($expected, $params['secret_key']); } function vardo_oc_error($message, $status) { return new WP_REST_Response(['success' => false, 'message' => $message], $status); } // --- Callbacks -------------------------------------------------------------- function vardo_oc_customer_callback(WP_REST_Request $request) { $started = microtime(true); $params = $request->get_json_params(); if (!is_array($params)) { $params = []; } if (!vardo_oc_secret_ok($params, VARDO_ORDER_KEY)) { return vardo_oc_error('Invalid secret key', 401); } $input = vardo_oc_normalize_customer($params); // A valid token pins the order to its owner; without one the legacy rule applies. $token_user = vardo_oc_user_from_token($request); if (is_int($token_user)) { $input['customer_id'] = $token_user; } try { $order = vardo_oc_build_order($input, vardo_oc_mode('customer')); return new WP_REST_Response([ 'success' => true, 'order_id' => $order->get_id(), 'order_number' => $order->get_order_number(), 'order_total' => $order->get_total(), 'order_status' => $order->get_status(), 'order_data' => $order->get_data(), 'timing_ms' => (int) round((microtime(true) - $started) * 1000), ], 200); } catch (Exception $e) { return vardo_oc_error($e->getMessage(), 500); } } function vardo_oc_support_callback(WP_REST_Request $request) { $started = microtime(true); $params = $request->get_json_params(); if (!is_array($params)) { $params = []; } // New builds send the staff member's token; it must belong to admin/manager. // Older builds send only the secret key (transitional). $token_user = vardo_oc_user_from_token($request); if ($token_user === false) { return vardo_oc_error('Invalid auth token.', 403); } if (is_int($token_user)) { $user = get_userdata($token_user); if (!$user || !array_intersect(vardo_oc_staff_roles(), (array) $user->roles)) { return vardo_oc_error('Not allowed to create support orders.', 403); } } elseif (!vardo_oc_secret_ok($params, VARDO_SUPPORT_ORDER_KEY)) { return vardo_oc_error('Invalid secret key.', 401); } $input = vardo_oc_normalize_support($params); try { $order = vardo_oc_build_order($input, vardo_oc_mode('support')); return new WP_REST_Response([ 'success' => true, 'order_id' => $order->get_id(), 'order_number' => $order->get_order_number(), 'order_total' => $order->get_total(), 'customer_id' => $order->get_customer_id(), 'payment_method' => $order->get_payment_method(), 'timing_ms' => (int) round((microtime(true) - $started) * 1000), ], 200); } catch (Exception $e) { return vardo_oc_error($e->getMessage(), 500); } } // --- Modes ------------------------------------------------------------------ function vardo_oc_mode($name) { $modes = [ 'customer' => [ 'status' => 'on-hold', 'calculate_totals' => false, 'created_via' => 'vardo-app', 'meta' => [ '_wc_order_attribution_source' => 'vardo_app', '_wc_order_attribution_utm_source' => 'Vardo App', '_wc_order_attribution_utm_medium' => 'internal', '_wc_order_attribution_utm_campaign' => 'order_creation', '_wc_order_attribution_source_type' => 'organic', 'order_source' => 'აპლიკაცია', ], ], 'support' => [ 'status' => 'processing', 'calculate_totals' => true, 'created_via' => 'vardo-app-support', 'meta' => [ '_wc_order_attribution_source' => 'vardo_app_manual', '_wc_order_attribution_utm_source' => 'Vardo App Manual', '_wc_order_attribution_utm_medium' => 'internal', '_wc_order_attribution_utm_campaign' => 'manual_order_creation', '_wc_order_attribution_source_type' => 'organic', '_order_source' => 'აპლიკაცია - მანუალურად', 'order_source' => 'აპლიკაცია - მანუალურად', '_created_by' => 'app-manual', ], ], ]; return $modes[$name]; } // --- Normalizers: legacy payload -> internal input ---------------------------- // // Internal input keys: // customer_id (int|null), customer_must_exist (bool), billing (array|null), // shipping (array|null), payment_method, payment_method_title, line_items, // customer_note, prices [discount, shipping, total], meta (key => value), // salary_data (array|null) function vardo_oc_normalize_customer(array $p) { $meta = []; $fields = [ 'summary' => 'calc_summary', 'appointment_order_date' => 'appointment_date', 'appointment_order_time' => 'appointment_time', 'expense' => 'expense', 'expense_comment' => 'expense_comment', 'hidden_field_source_identifier' => '_source_identifier', 'custom_service_name' => '_custom_service_name', 'terms-approvement' => 'terms-approvement', 'order_manager-comment' => 'order_manager-comment', ]; foreach ($fields as $param => $meta_key) { if (!empty($p[$param])) { $meta[$meta_key] = sanitize_text_field($p[$param]); } } if (!empty($p['selectedFlag'])) { $meta['wpml_language'] = ($p['selectedFlag'] === 'GE') ? 'ka-ge' : 'en'; } $line_items = []; foreach ((array) ($p['line_items'] ?? []) as $item) { // Legacy rule: product lines need product_id + quantity and an existing product. if (!is_array($item) || empty($item['product_id']) || empty($item['quantity'])) { continue; } $line_items[] = [ 'product_id' => absint($item['product_id']), 'variation_id' => !empty($item['variation_id']) ? absint($item['variation_id']) : 0, 'quantity' => absint($item['quantity']), 'price' => !empty($item['price']) ? floatval($item['price']) : 0, 'require_product' => true, ]; } $prices = is_array($p['prices'] ?? null) ? $p['prices'] : []; return [ 'customer_id' => !empty($p['customer_id']) ? absint($p['customer_id']) : null, 'customer_must_exist' => false, 'billing' => (!empty($p['billing']) && is_array($p['billing'])) ? vardo_oc_sanitize_address($p['billing']) : null, 'shipping' => (!empty($p['shipping']) && is_array($p['shipping'])) ? vardo_oc_sanitize_address($p['shipping']) : null, 'payment_method' => !empty($p['payment_method']) ? sanitize_text_field($p['payment_method']) : null, 'payment_method_title' => !empty($p['payment_method_title']) ? sanitize_text_field($p['payment_method_title']) : null, 'line_items' => $line_items, 'customer_note' => !empty($p['customer_note']) ? sanitize_text_field($p['customer_note']) : null, 'prices' => [ 'discount' => !empty($prices['discount']) ? floatval($prices['discount']) : 0, 'shipping' => !empty($prices['shipping']) ? floatval($prices['shipping']) : 0, 'total' => isset($prices['total']) ? floatval($prices['total']) : null, ], 'meta' => $meta, 'salary_data' => null, ]; } function vardo_oc_normalize_support(array $p) { $meta = []; // isset (not empty) on purpose: the legacy support endpoint stored empty strings too. $meta_map = [ 'calc_summary' => 'calculatorSummary', 'appointment_date' => 'date', 'appointment_time' => 'time', 'expense' => 'expense', 'expense_comment' => 'expenseComment', '_source_identifier' => '_source_identifier', 'order_author' => 'orderAuthor', ]; foreach ($meta_map as $meta_key => $param_key) { if (isset($p[$param_key])) { $meta[$meta_key] = sanitize_text_field($p[$param_key]); } } if (isset($p['selectedFlag'])) { $meta['wpml_language'] = ($p['selectedFlag'] === 'GE') ? 'ka-ge' : 'en'; } if (isset($p['send_sms_email_notifications'])) { $meta['send_sms_email_notifications'] = filter_var($p['send_sms_email_notifications'], FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false'; } if (!empty($p['orderComment'])) { $meta['order_manager-comment'] = sanitize_textarea_field($p['orderComment']); } // New (optional): staff-entered second contact number. Stored only when filled. if (isset($p['alternative_phone'])) { $alt = sanitize_text_field($p['alternative_phone']); if ($alt !== '') { $meta['alternative_phone'] = $alt; } } $line_items = []; foreach ((array) ($p['line_items'] ?? []) as $item) { if (!is_array($item)) { continue; } $line_items[] = [ 'product_id' => !empty($item['product_id']) ? absint($item['product_id']) : 0, 'variation_id' => 0, 'name' => !empty($item['other_service_name']) ? sanitize_text_field($item['other_service_name']) : null, 'description' => !empty($item['other_service_description']) ? sanitize_textarea_field($item['other_service_description']) : null, 'quantity' => !empty($item['quantity']) ? absint($item['quantity']) : 1, 'price' => !empty($item['price']) ? floatval($item['price']) : 0, 'require_product' => false, ]; } $salary_data = null; foreach ((array) ($p['meta_data'] ?? []) as $m) { if (isset($m['key'], $m['value']) && $m['key'] === 'cleaners_salary_info' && is_array($m['value'])) { $data = []; foreach ($m['value'] as $entry) { if (!isset($entry['id'])) { continue; } $data[intval($entry['id'])] = isset($entry['salary']) ? floatval($entry['salary']) : ''; } if (!empty($data)) { $salary_data = $data; } } } $payment_methods = [ 'cod' => __('Cash on Delivery', 'woocommerce'), 'bacs' => __('Bank Transfer', 'woocommerce'), ]; $payment_method = !empty($p['payment_method']) ? sanitize_text_field($p['payment_method']) : 'cod'; if (!array_key_exists($payment_method, $payment_methods)) { $payment_method = 'cod'; } return [ 'customer_id' => !empty($p['customer_id']) ? absint($p['customer_id']) : 0, 'customer_must_exist' => true, 'billing' => [ 'first_name' => sanitize_text_field($p['name'] ?? ''), 'last_name' => sanitize_text_field($p['lastName'] ?? ''), 'company' => sanitize_text_field($p['companyName'] ?? ''), 'address_1' => sanitize_text_field($p['street'] ?? ''), 'address_2' => sanitize_text_field($p['streetNumber'] ?? ''), 'city' => sanitize_text_field($p['city'] ?? ''), 'country' => sanitize_text_field($p['country'] ?? 'GE'), 'email' => sanitize_email($p['email'] ?? ''), 'phone' => sanitize_text_field($p['phone'] ?? ''), ], 'shipping' => null, 'payment_method' => $payment_method, 'payment_method_title' => $payment_methods[$payment_method], 'line_items' => $line_items, 'customer_note' => null, 'prices' => [ 'discount' => !empty($p['discount']) ? floatval($p['discount']) : 0, 'shipping' => 0, 'total' => isset($p['finalPrice']) ? floatval($p['finalPrice']) : null, ], 'meta' => $meta, 'salary_data' => $salary_data, ]; } function vardo_oc_sanitize_address(array $address) { return [ 'first_name' => sanitize_text_field($address['first_name'] ?? ''), 'last_name' => sanitize_text_field($address['last_name'] ?? ''), 'company' => sanitize_text_field($address['company'] ?? ''), 'address_1' => sanitize_text_field($address['address_1'] ?? ''), 'address_2' => sanitize_text_field($address['address_2'] ?? ''), 'city' => sanitize_text_field($address['city'] ?? ''), 'state' => sanitize_text_field($address['state'] ?? ''), 'postcode' => sanitize_text_field($address['postcode'] ?? ''), 'country' => sanitize_text_field($address['country'] ?? ''), 'email' => sanitize_email($address['email'] ?? ''), 'phone' => sanitize_text_field($address['phone'] ?? ''), ]; } // --- Core ------------------------------------------------------------------- /** * Build and save the order. Items are added in memory and persisted by the * single final save (plus calculate_totals' own save in support mode), so the * status transition — and with it emails/SMS hooks — fires exactly once. */ function vardo_oc_build_order(array $in, array $mode) { $order = wc_create_order(); if (is_wp_error($order)) { throw new Exception('Failed to create order.'); } // Customer if ($in['customer_id']) { if ($in['customer_must_exist'] && !get_user_by('id', $in['customer_id'])) { $order->set_customer_id(0); // unknown user -> guest (legacy support behaviour) } else { $order->set_customer_id($in['customer_id']); } } elseif ($in['customer_must_exist']) { $order->set_customer_id(0); } // Addresses if ($in['billing'] !== null) { $order->set_address($in['billing'], 'billing'); } if ($in['shipping'] !== null) { $order->set_address($in['shipping'], 'shipping'); } // Payment if ($in['payment_method'] !== null) { $order->set_payment_method($in['payment_method']); } if ($in['payment_method_title'] !== null) { $order->set_payment_method_title($in['payment_method_title']); } // Line items foreach ($in['line_items'] as $item) { $order_item = new WC_Order_Item_Product(); $product = $item['product_id'] ? wc_get_product($item['product_id']) : null; if ($item['require_product'] && !$product) { continue; } if (!empty($item['name'])) { $order_item->set_name($item['name']); if (!empty($item['description'])) { $order_item->add_meta_data('Description', $item['description']); } } elseif ($product) { $order_item->set_product_id($product->get_id()); $order_item->set_name($product->get_name()); } if ($item['variation_id'] > 0) { $order_item->set_variation_id($item['variation_id']); } $order_item->set_quantity($item['quantity']); $order_item->set_subtotal($item['price'] * $item['quantity']); $order_item->set_total($item['price'] * $item['quantity']); $order->add_item($order_item); } if ($in['customer_note'] !== null) { $order->set_customer_note($in['customer_note']); } // Prices if ($in['prices']['discount']) { $fee = new WC_Order_Item_Fee(); $fee->set_name('Discount'); $fee->set_amount($in['prices']['discount'] * -1); $fee->set_total($in['prices']['discount'] * -1); $fee->set_tax_status('none'); $order->add_item($fee); } if ($in['prices']['shipping']) { $shipping_item = new WC_Order_Item_Shipping(); $shipping_item->set_method_title('Custom Shipping'); $shipping_item->set_method_id('custom_shipping'); $shipping_item->set_total($in['prices']['shipping']); $shipping_item->set_tax_status('none'); $order->add_item($shipping_item); } if ($in['prices']['total'] !== null) { $order->set_total($in['prices']['total']); } // Meta foreach ($in['meta'] as $key => $value) { $order->update_meta_data($key, $value); } if (!empty($in['salary_data'])) { $order->update_meta_data('_cleaners_salary_data', $in['salary_data']); } // Source tracking $order->set_created_via($mode['created_via']); foreach ($mode['meta'] as $key => $value) { $order->update_meta_data($key, $value); } if ($mode['calculate_totals']) { $order->calculate_totals(); // also saves (legacy support behaviour: recomputed total wins) } $order->set_status($mode['status']); $order->save(); return $order; } https://www.vardocleaner.ge/post-sitemap.xml 2026-07-28T18:06:37+00:00 https://www.vardocleaner.ge/page-sitemap.xml 2026-08-19T02:46:56+00:00 https://www.vardocleaner.ge/easy_invoice-sitemap.xml 2025-10-23T15:18:06+00:00 https://www.vardocleaner.ge/product-sitemap.xml 2026-08-25T03:17:33+00:00 https://www.vardocleaner.ge/product_cat-sitemap.xml 2026-08-25T03:17:33+00:00 https://www.vardocleaner.ge/author-sitemap.xml 2026-09-13T03:36:43+00:00