'POST', 'callback' => array( $this, 'submit_quote' ), 'permission_callback' => array( $this, 'check_logged_in' ), ) ); register_rest_route( $ns, '/quotes', array( 'methods' => 'GET', 'callback' => array( $this, 'get_user_quotes' ), 'permission_callback' => array( $this, 'check_logged_in' ), ) ); register_rest_route( $ns, '/quotes/(?P\d+)', array( 'methods' => 'GET', 'callback' => array( $this, 'get_quote' ), 'permission_callback' => array( $this, 'check_logged_in' ), ) ); register_rest_route( $ns, '/quotes/(?P\d+)/add-to-cart', array( 'methods' => 'POST', 'callback' => array( $this, 'add_quote_to_cart' ), 'permission_callback' => array( $this, 'check_logged_in' ), ) ); // Admin. register_rest_route( $ns, '/admin/quotes', array( 'methods' => 'GET', 'callback' => array( $this, 'admin_get_quotes' ), 'permission_callback' => array( $this, 'check_admin' ), ) ); register_rest_route( $ns, '/admin/quotes/(?P\d+)', array( 'methods' => 'GET', 'callback' => array( $this, 'admin_get_quote' ), 'permission_callback' => array( $this, 'check_admin' ), ) ); register_rest_route( $ns, '/admin/quotes/(?P\d+)/status', array( 'methods' => 'POST', 'callback' => array( $this, 'admin_update_status' ), 'permission_callback' => array( $this, 'check_admin' ), ) ); register_rest_route( $ns, '/admin/quotes/(?P\d+)/delete', array( 'methods' => 'DELETE', 'callback' => array( $this, 'admin_delete_quote' ), 'permission_callback' => array( $this, 'check_admin' ), ) ); register_rest_route( $ns, '/admin/quotes/(?P\d+)/update-items', array( 'methods' => 'POST', 'callback' => array( $this, 'admin_update_items' ), 'permission_callback' => array( $this, 'check_admin' ), ) ); register_rest_route( $ns, '/admin/stats', array( 'methods' => 'GET', 'callback' => array( $this, 'admin_get_stats' ), 'permission_callback' => array( $this, 'check_admin' ), ) ); register_rest_route( $ns, '/admin/ui-language', array( 'methods' => 'POST', 'callback' => array( $this, 'admin_set_ui_language' ), 'permission_callback' => array( $this, 'check_admin' ), ) ); register_rest_route( $ns, '/admin/test-email', array( 'methods' => 'POST', 'callback' => array( $this, 'admin_send_test_email' ), 'permission_callback' => array( $this, 'check_admin' ), ) ); register_rest_route( $ns, '/admin/settings', array( array( 'methods' => 'GET', 'callback' => array( $this, 'admin_get_settings' ), 'permission_callback' => array( $this, 'check_admin' ), ), array( 'methods' => 'POST', 'callback' => array( $this, 'admin_save_settings' ), 'permission_callback' => array( $this, 'check_admin' ), ), ) ); } /** * Permission: logged-in user. * * @return bool */ public function check_logged_in() { return is_user_logged_in(); } /** * Permission: shop manager / admin. * * @return bool */ public function check_admin() { return current_user_can( 'manage_woocommerce' ); } /** * Make sure the WC cart is available in a REST context. */ private function ensure_cart() { if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { if ( is_null( WC()->cart ) ) { wc_load_cart(); } if ( WC()->cart && WC()->session && ! WC()->session->has_session() ) { WC()->session->set_customer_session_cookie( true ); } if ( WC()->cart ) { WC()->cart->get_cart(); } } } /** * Submit the current cart as a quote request. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function submit_quote( $request ) { $user_id = get_current_user_id(); $settings = DevisQR_Settings::resolved(); // Rate limit: one request per minute per user. $transient_key = 'devisqr_rate_' . $user_id; if ( get_transient( $transient_key ) ) { return new WP_Error( 'rate_limit', __( 'Please wait a moment before submitting another quote request.', 'nuxwp-devis' ), array( 'status' => 429 ) ); } if ( ! DevisQR_Helpers::user_can_request( $user_id ) ) { return new WP_Error( 'not_allowed', __( 'Your account is not allowed to request quotes.', 'nuxwp-devis' ), array( 'status' => 403 ) ); } $this->ensure_cart(); $cart = WC()->cart; if ( ! $cart || $cart->is_empty() ) { return new WP_Error( 'empty_cart', __( 'Your cart is empty.', 'nuxwp-devis' ), array( 'status' => 400 ) ); } // Products at catalog price incl. tax (shipping, fees and coupons are not part of a quote). $cart_total_ttc = (float) $cart->get_subtotal() + (float) $cart->get_subtotal_tax(); $min_total = DevisQR_Helpers::min_cart_total(); if ( $min_total > 0 && $cart_total_ttc < $min_total ) { return new WP_Error( 'min_amount', /* translators: %s: formatted minimum amount */ sprintf( __( 'The minimum cart total for a quote request is %s.', 'nuxwp-devis' ), DevisQR_Helpers::price_text( $min_total ) ), array( 'status' => 400 ) ); } // Customer message. $customer_note = ''; if ( ! empty( $settings['customer_message_enabled'] ) ) { $customer_note = sanitize_textarea_field( (string) $request->get_param( 'customer_note' ) ); $customer_note = mb_substr( $customer_note, 0, 2000 ); } $attachment_name = null; $validity_days = max( 1, (int) $settings['validity_days'] ); $expires_at = gmdate( 'Y-m-d H:i:s', strtotime( current_time( 'mysql' ) ) + $validity_days * DAY_IN_SECONDS ); // Concurrent submissions may draw the same sequential number: retry on the UNIQUE key. $quote_id = false; for ( $try = 0; $try < 3 && ! $quote_id; $try++ ) { $quote_number = DevisQR_Database::generate_quote_number(); $quote_id = $this->insert_quote_row( $quote_number, $user_id, $cart, $cart_total_ttc, $customer_note, $attachment_name, $expires_at ); } if ( ! $quote_id ) { return new WP_Error( 'db_error', __( 'The quote could not be created.', 'nuxwp-devis' ), array( 'status' => 500 ) ); } $this->insert_cart_items( $quote_id, $cart ); // Header totals must be the sum of the lines (the WC total includes shipping/fees/coupons). DevisQR_Database::recalculate_quote_totals( $quote_id ); /** * Fires once the quote and all its lines are stored (prices frozen). * * @param int $quote_id Quote ID. */ do_action( 'devisqr_quote_created', $quote_id ); $pdf_path = DevisQR_PDF::generate( $quote_id ); if ( $pdf_path ) { DevisQR_Database::update_quote( $quote_id, array( 'pdf_path' => $pdf_path ) ); } $this->schedule_or_run( 'devisqr_send_new_quote_emails', $quote_id, 'devisqr_handle_new_quote_emails' ); WC()->cart->empty_cart(); set_transient( $transient_key, 1, MINUTE_IN_SECONDS ); return rest_ensure_response( array( 'success' => true, 'message' => __( 'Your quote request has been sent!', 'nuxwp-devis' ), 'quote_id' => $quote_id, 'quote_number' => $quote_number, 'redirect_url' => wc_get_account_endpoint_url( DevisQR_Settings::account_endpoint() ), ) ); } /** * Insert the quote header row. * * @return int|false */ private function insert_quote_row( $quote_number, $user_id, $cart, $cart_total_ttc, $customer_note, $attachment_name, $expires_at ) { return DevisQR_Database::insert_quote( array( 'quote_number' => $quote_number, 'user_id' => $user_id, 'status' => 'pending', 'total_ht' => (float) $cart->get_subtotal(), 'total_tax' => (float) $cart->get_subtotal_tax(), 'total_ttc' => $cart_total_ttc, 'customer_note' => $customer_note, 'attachment_path' => $attachment_name, 'expires_at' => $expires_at, 'created_at' => current_time( 'mysql' ), ) ); } /** * Copy the cart lines into the quote (catalog prices, real tax rate per customer). * * @param int $quote_id Quote ID. * @param WC_Cart $cart Cart. */ private function insert_cart_items( $quote_id, $cart ) { foreach ( $cart->get_cart() as $cart_item ) { $product = $cart_item['data']; $quantity = (float) $cart_item['quantity']; $variation_id = isset( $cart_item['variation_id'] ) ? (int) $cart_item['variation_id'] : 0; $tax_rate = 0.0; foreach ( (array) WC_Tax::get_rates( $product->get_tax_class(), WC()->customer ) as $r ) { $tax_rate += (float) $r['rate']; } $unit_price_ht = (float) wc_get_price_excluding_tax( $product, array( 'qty' => 1 ) ); $unit_price_ttc = $unit_price_ht * ( 1 + $tax_rate / 100 ); $meta = array(); if ( ! empty( $cart_item['variation'] ) ) { $meta['variation'] = $cart_item['variation']; } DevisQR_Database::insert_quote_item( array( 'quote_id' => $quote_id, 'product_id' => (int) $cart_item['product_id'], 'variation_id' => $variation_id, 'product_name' => $product->get_name(), 'product_sku' => $product->get_sku(), 'quantity' => $quantity, 'unit_price_ht' => $unit_price_ht, 'unit_price_ttc' => $unit_price_ttc, 'tax_rate' => $tax_rate, 'line_total_ht' => round( $unit_price_ht * $quantity, 4 ), 'line_total_ttc' => round( $unit_price_ttc * $quantity, 4 ), 'meta_data' => ! empty( $meta ) ? wp_json_encode( $meta ) : null, ) ); } } /** * Schedule a deferred e-mail (or run now if cron is unavailable). * * @param string $hook Cron hook. * @param int $quote_id Quote ID. * @param string $fallback Function to run directly. */ private function schedule_or_run( $hook, $quote_id, $fallback ) { if ( function_exists( 'wp_schedule_single_event' ) && ! ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) ) { if ( false !== wp_schedule_single_event( time(), $hook, array( (int) $quote_id ) ) ) { return; } } if ( function_exists( $fallback ) ) { call_user_func( $fallback, (int) $quote_id ); } } /** * Customer: list own quotes. * * @param WP_REST_Request $request Request. * @return WP_REST_Response */ public function get_user_quotes( $request ) { $user_id = get_current_user_id(); $page = max( 1, absint( $request->get_param( 'page' ) ) ); $limit = 10; $offset = ( $page - 1 ) * $limit; $quotes = DevisQR_Database::get_user_quotes( $user_id, $limit, $offset ); $total = DevisQR_Database::count_user_quotes( $user_id ); $data = array(); foreach ( $quotes as $quote ) { $data[] = $this->format_quote( $quote ); } return rest_ensure_response( array( 'quotes' => $data, 'total' => $total, 'total_pages' => (int) ceil( $total / $limit ), 'page' => $page, ) ); } /** * Customer: one quote (owner or manager). * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function get_quote( $request ) { $quote_id = absint( $request['id'] ); $quote = DevisQR_Database::get_quote( $quote_id ); if ( ! $quote || ( ! current_user_can( 'manage_woocommerce' ) && (int) $quote->user_id !== get_current_user_id() ) ) { return new WP_Error( 'not_found', __( 'Quote not found.', 'nuxwp-devis' ), array( 'status' => 404 ) ); } $data = $this->format_quote( $quote ); $data['items'] = $this->get_formatted_items( $quote_id ); return rest_ensure_response( $data ); } /** * Customer: load an accepted quote into the cart to pay it. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function add_quote_to_cart( $request ) { $quote_id = absint( $request['id'] ); $quote = DevisQR_Database::get_quote( $quote_id ); if ( ! $quote || (int) $quote->user_id !== get_current_user_id() ) { return new WP_Error( 'not_found', __( 'Quote not found.', 'nuxwp-devis' ), array( 'status' => 404 ) ); } if ( 'converted' === $quote->status ) { return new WP_Error( 'already_converted', __( 'This quote has already been converted into an order.', 'nuxwp-devis' ), array( 'status' => 400 ) ); } if ( 'accepted' !== $quote->status ) { return new WP_Error( 'not_accepted', __( 'This quote has not been accepted yet.', 'nuxwp-devis' ), array( 'status' => 400 ) ); } if ( DevisQR_Helpers::is_expired( $quote ) ) { DevisQR_Database::update_quote( $quote->id, array( 'status' => 'expired' ) ); return new WP_Error( 'expired', __( 'This quote has expired. Please submit a new quote request.', 'nuxwp-devis' ), array( 'status' => 400 ) ); } $this->ensure_cart(); $added = DevisQR_Cart::load_quote_into_cart( $quote ); if ( is_wp_error( $added ) ) { return $added; } return rest_ensure_response( array( 'success' => true, /* translators: %d: number of products */ 'message' => sprintf( _n( '%d product added to the cart.', '%d products added to the cart.', $added, 'nuxwp-devis' ), $added ), 'redirect_url' => wc_get_checkout_url(), ) ); } /** * Admin: list. * * @param WP_REST_Request $request Request. * @return WP_REST_Response */ public function admin_get_quotes( $request ) { $per_page = $request->get_param( 'per_page' ) ? min( 100, absint( $request->get_param( 'per_page' ) ) ) : 20; $page = max( 1, absint( $request->get_param( 'page' ) ) ); $args = array( 'status' => sanitize_text_field( (string) $request->get_param( 'status' ) ), 'search' => sanitize_text_field( (string) $request->get_param( 'search' ) ), 'orderby' => sanitize_text_field( (string) $request->get_param( 'orderby' ) ), 'order' => sanitize_text_field( (string) $request->get_param( 'order' ) ), 'limit' => $per_page, 'offset' => ( $page - 1 ) * $per_page, ); $quotes = DevisQR_Database::get_all_quotes( $args ); $total = DevisQR_Database::count_all_quotes( $args['status'], $args['search'] ); $data = array(); foreach ( $quotes as $quote ) { $formatted = $this->format_quote( $quote ); $user = get_userdata( $quote->user_id ); $formatted['customer_name'] = $user ? $user->display_name : __( 'Deleted user', 'nuxwp-devis' ); $formatted['customer_email'] = $user ? $user->user_email : ''; $formatted['customer_company'] = $user ? get_user_meta( $user->ID, 'billing_company', true ) : ''; $data[] = $formatted; } return rest_ensure_response( array( 'quotes' => $data, 'total' => $total, 'total_pages' => (int) ceil( $total / $per_page ), ) ); } /** * Admin: one quote with customer block. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function admin_get_quote( $request ) { $quote_id = absint( $request['id'] ); $quote = DevisQR_Database::get_quote( $quote_id ); if ( ! $quote ) { return new WP_Error( 'not_found', __( 'Quote not found.', 'nuxwp-devis' ), array( 'status' => 404 ) ); } $data = $this->format_quote( $quote ); $data['items'] = $this->get_formatted_items( $quote_id ); $user = get_userdata( $quote->user_id ); $data['customer'] = array( 'name' => $user ? $user->display_name : '', 'email' => $user ? $user->user_email : '', 'phone' => $user ? get_user_meta( $user->ID, 'billing_phone', true ) : '', 'company' => $user ? get_user_meta( $user->ID, 'billing_company', true ) : '', 'address' => $user ? $this->get_user_address( $user->ID ) : '', ); $data['wc_order_id'] = (int) $quote->wc_order_id; $data['wc_order_url'] = $quote->wc_order_id ? admin_url( 'post.php?post=' . (int) $quote->wc_order_id . '&action=edit' ) : ''; if ( $quote->wc_order_id && function_exists( 'wc_get_order' ) ) { $order = wc_get_order( (int) $quote->wc_order_id ); if ( $order ) { $data['wc_order_url'] = $order->get_edit_order_url(); } } return rest_ensure_response( $data ); } /** * Admin: status + note. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function admin_update_status( $request ) { $quote_id = absint( $request['id'] ); $quote = DevisQR_Database::get_quote( $quote_id ); if ( ! $quote ) { return new WP_Error( 'not_found', __( 'Quote not found.', 'nuxwp-devis' ), array( 'status' => 404 ) ); } $params = (array) $request->get_json_params(); $status = isset( $params['status'] ) ? sanitize_key( $params['status'] ) : ''; $note = isset( $params['admin_note'] ) ? sanitize_textarea_field( $params['admin_note'] ) : null; if ( ! array_key_exists( $status, DevisQR_Helpers::status_labels() ) ) { return new WP_Error( 'invalid_status', __( 'Invalid status.', 'nuxwp-devis' ), array( 'status' => 400 ) ); } $update = array( 'status' => $status ); if ( null !== $note ) { $update['admin_note'] = $note; } DevisQR_Database::update_quote( $quote_id, $update ); $pdf_path = DevisQR_PDF::generate( $quote_id ); if ( $pdf_path ) { DevisQR_Database::update_quote( $quote_id, array( 'pdf_path' => $pdf_path ) ); } /** * Fires after an admin changed the status of a quote. * * @param int $quote_id Quote ID. * @param string $status New status. * @param string $old_status Previous status. */ do_action( 'devisqr_quote_status_changed', $quote_id, $status, $quote->status ); $this->schedule_or_run( 'devisqr_send_status_email', $quote_id, 'devisqr_handle_status_email' ); return rest_ensure_response( array( 'success' => true, 'message' => __( 'Status updated.', 'nuxwp-devis' ), ) ); } /** * Admin: delete. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function admin_delete_quote( $request ) { $quote_id = absint( $request['id'] ); if ( ! DevisQR_Database::get_quote( $quote_id ) ) { return new WP_Error( 'not_found', __( 'Quote not found.', 'nuxwp-devis' ), array( 'status' => 404 ) ); } DevisQR_Database::delete_quote( $quote_id ); return rest_ensure_response( array( 'success' => true, 'message' => __( 'Quote deleted.', 'nuxwp-devis' ), ) ); } /** * Admin: line discounts, global discount, expiry. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function admin_update_items( $request ) { $quote_id = absint( $request['id'] ); $quote = DevisQR_Database::get_quote( $quote_id ); if ( ! $quote ) { return new WP_Error( 'not_found', __( 'Quote not found.', 'nuxwp-devis' ), array( 'status' => 404 ) ); } $params = (array) $request->get_json_params(); if ( isset( $params['expires_at'] ) ) { $expires = sanitize_text_field( $params['expires_at'] ); $dt = DateTime::createFromFormat( 'Y-m-d', $expires ); if ( $dt && $dt->format( 'Y-m-d' ) === $expires ) { DevisQR_Database::update_quote( $quote_id, array( 'expires_at' => $expires . ' 23:59:59' ) ); } } if ( ! empty( $params['items'] ) && is_array( $params['items'] ) ) { $items_by_id = array(); foreach ( DevisQR_Database::get_quote_items( $quote_id ) as $item ) { $items_by_id[ (int) $item->id ] = $item; } foreach ( $params['items'] as $item_data ) { $item_id = isset( $item_data['id'] ) ? absint( $item_data['id'] ) : 0; if ( ! $item_id || ! isset( $items_by_id[ $item_id ] ) ) { continue; } $discount = isset( $item_data['discount_percent'] ) ? (float) $item_data['discount_percent'] : 0; DevisQR_Database::update_quote_item( $item_id, array( 'discount_percent' => max( 0, min( 100, $discount ) ) ) ); } } $global = null; if ( isset( $params['global_discount'] ) ) { $global = max( 0, min( 100, (float) $params['global_discount'] ) ); } DevisQR_Database::recompute_items( $quote_id, $global ); $totals = DevisQR_Database::recalculate_quote_totals( $quote_id ); $quote = DevisQR_Database::get_quote( $quote_id ); if ( 'pending' === $quote->status ) { DevisQR_Database::update_quote( $quote_id, array( 'status' => 'quoted' ) ); } $pdf_path = DevisQR_PDF::generate( $quote_id ); if ( $pdf_path ) { DevisQR_Database::update_quote( $quote_id, array( 'pdf_path' => $pdf_path ) ); } return rest_ensure_response( array( 'success' => true, 'message' => __( 'Quote updated.', 'nuxwp-devis' ), 'totals' => $totals, ) ); } /** * Admin: counters. * * @return WP_REST_Response */ public function admin_get_stats() { return rest_ensure_response( DevisQR_Database::get_status_counts() ); } /** * Admin: settings (raw stored + resolved hints + role choices). * * @return WP_REST_Response */ public function admin_get_settings() { return rest_ensure_response( array( 'settings' => DevisQR_Settings::all(), 'logo_warning' => DevisQR_Helpers::logo_warning_text(), 'resolved' => DevisQR_Settings::resolved(), 'roles' => DevisQR_Helpers::role_choices(), 'texts' => DevisQR_Texts::rows_for_admin(), 'ui_language' => DevisQR_Texts::user_choice(), ) ); } /** * Admin: save settings. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function admin_save_settings( $request ) { $params = (array) $request->get_json_params(); $saved = DevisQR_Settings::save( $params ); if ( is_wp_error( $saved ) ) { return $saved; } return rest_ensure_response( array( 'success' => true, 'message' => __( 'Settings saved.', 'nuxwp-devis' ), 'settings' => $saved, 'resolved' => DevisQR_Settings::resolved(), 'texts' => DevisQR_Texts::rows_for_admin(), 'logo_warning' => DevisQR_Helpers::logo_warning_text( true ), ) ); } /** * Admin: per-user admin language (auto|en|fr). * * @param WP_REST_Request $request Request. * @return WP_REST_Response */ public function admin_set_ui_language( $request ) { $params = (array) $request->get_json_params(); $choice = DevisQR_Texts::set_user_choice( isset( $params['language'] ) ? sanitize_key( $params['language'] ) : 'auto' ); return rest_ensure_response( array( 'success' => true, 'ui_language' => $choice ) ); } /** * POST /admin/test-email — send the sample "request received" e-mail to the current user. * Uses the SAVED settings (texts, logo, colours): save first to preview changes. * * @return WP_REST_Response|WP_Error */ public function admin_send_test_email() { $user = wp_get_current_user(); if ( ! $user || ! is_email( $user->user_email ) ) { return new WP_Error( 'devisqr_no_email', __( 'Your account has no valid e-mail address.', 'nuxwp-devis' ), array( 'status' => 400 ) ); } // One test per 30 seconds per user. $key = 'devisqr_test_mail_' . $user->ID; if ( get_transient( $key ) ) { return new WP_Error( 'devisqr_too_fast', __( 'Please wait a few seconds before sending another test.', 'nuxwp-devis' ), array( 'status' => 429 ) ); } set_transient( $key, 1, 30 ); if ( ! DevisQR_Email::send_test( $user->user_email ) ) { return new WP_Error( 'devisqr_mail_failed', __( 'WordPress could not send the e-mail. Check your SMTP configuration.', 'nuxwp-devis' ), array( 'status' => 500 ) ); } return rest_ensure_response( array( 'success' => true, /* translators: %s: e-mail address */ 'message' => sprintf( __( 'Test e-mail sent to %s.', 'nuxwp-devis' ), $user->user_email ), ) ); } /** * Quote → API array. * * @param object $quote Row. * @return array */ private function format_quote( $quote ) { $labels = DevisQR_Helpers::status_labels(); return array( 'id' => (int) $quote->id, 'quote_number' => $quote->quote_number, 'status' => $quote->status, 'status_label' => isset( $labels[ $quote->status ] ) ? $labels[ $quote->status ] : $quote->status, 'total_ht' => (float) $quote->total_ht, 'total_tax' => (float) $quote->total_tax, 'total_ttc' => (float) $quote->total_ttc, 'total_ht_html' => DevisQR_Helpers::price_html( $quote->total_ht ), 'total_tax_html' => DevisQR_Helpers::price_html( $quote->total_tax ), 'total_ttc_html' => DevisQR_Helpers::price_html( $quote->total_ttc ), 'discount_percent' => (float) ( isset( $quote->discount_percent ) ? $quote->discount_percent : 0 ), 'has_pdf' => true, 'pdf_url' => DevisQR_Helpers::download_url( $quote->id, 'pdf' ), 'attachment_url' => '', 'customer_note' => (string) $quote->customer_note, 'admin_note' => (string) $quote->admin_note, 'expires_at' => $quote->expires_at, 'expires_at_formatted' => DevisQR_Helpers::date_text( $quote->expires_at ), 'created_at' => $quote->created_at, 'created_at_formatted' => DevisQR_Helpers::date_text( $quote->created_at ), 'created_at_datetime' => DevisQR_Helpers::date_text( $quote->created_at, true ), 'updated_at' => $quote->updated_at, ); } /** * Items → API array. * * @param int $quote_id Quote ID. * @return array */ private function get_formatted_items( $quote_id ) { $items = DevisQR_Database::get_quote_items( $quote_id ); $data = array(); if ( ! empty( $items ) ) { $ids = array_map( function ( $item ) { return $item->variation_id ? (int) $item->variation_id : (int) $item->product_id; }, $items ); _prime_post_caches( array_unique( $ids ), true, true ); } foreach ( $items as $item ) { $product = wc_get_product( $item->variation_id ? $item->variation_id : $item->product_id ); $thumbnail = $product ? wp_get_attachment_image_url( $product->get_image_id(), 'thumbnail' ) : ''; $data[] = array( 'id' => (int) $item->id, 'product_id' => (int) $item->product_id, 'variation_id' => (int) $item->variation_id, 'product_name' => $item->product_name, 'product_sku' => $item->product_sku, 'quantity' => (float) $item->quantity, 'unit_price_ht' => (float) $item->unit_price_ht, 'unit_price_ttc' => (float) $item->unit_price_ttc, 'tax_rate' => (float) $item->tax_rate, 'line_total_ht' => (float) $item->line_total_ht, 'line_total_ttc' => (float) $item->line_total_ttc, 'discount_percent' => (float) ( isset( $item->discount_percent ) ? $item->discount_percent : 0 ), 'thumbnail' => $thumbnail ? $thumbnail : '', 'meta_data' => $item->meta_data ? json_decode( $item->meta_data, true ) : null, ); } return $data; } /** * Billing address as one line. * * @param int $user_id User. * @return string */ private function get_user_address( $user_id ) { $parts = array( get_user_meta( $user_id, 'billing_address_1', true ), get_user_meta( $user_id, 'billing_address_2', true ), trim( get_user_meta( $user_id, 'billing_postcode', true ) . ' ' . get_user_meta( $user_id, 'billing_city', true ) ), get_user_meta( $user_id, 'billing_country', true ), ); return implode( ', ', array_filter( $parts ) ); } }