diff --git a/includes/REST/DataCountryController.php b/includes/REST/DataCountryController.php new file mode 100644 index 0000000000..ee296ccf4e --- /dev/null +++ b/includes/REST/DataCountryController.php @@ -0,0 +1,48 @@ +check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check vendor permission. + * + * @return bool + */ + protected function check_vendor_permission(): bool { + return dokan_is_user_seller( dokan_get_current_user_id() ); + } +} diff --git a/includes/REST/Manager.php b/includes/REST/Manager.php index ce0d1f936b..e03cce8078 100644 --- a/includes/REST/Manager.php +++ b/includes/REST/Manager.php @@ -234,6 +234,16 @@ private function get_rest_api_class_map() { DOKAN_DIR . '/includes/REST/StoreSettingControllerV2.php' => '\WeDevs\Dokan\REST\StoreSettingControllerV2', DOKAN_DIR . '/includes/REST/VendorDashboardController.php' => '\WeDevs\Dokan\REST\VendorDashboardController', DOKAN_DIR . '/includes/REST/ProductBlockController.php' => '\WeDevs\Dokan\REST\ProductBlockController', + DOKAN_DIR . '/includes/REST/DataCountryController.php' => '\WeDevs\Dokan\REST\DataCountryController', + DOKAN_DIR . '/includes/REST/ShippingMethodController.php' => '\WeDevs\Dokan\REST\ShippingMethodController', + DOKAN_DIR . '/includes/REST/ShippingStatusController.php' => '\WeDevs\Dokan\REST\ShippingStatusController', + DOKAN_DIR . '/includes/REST/TaxClassController.php' => '\WeDevs\Dokan\REST\TaxClassController', + DOKAN_DIR . '/includes/REST/TaxController.php' => '\WeDevs\Dokan\REST\TaxController', + DOKAN_DIR . '/includes/REST/PaymentGatewayController.php' => '\WeDevs\Dokan\REST\PaymentGatewayController', + DOKAN_DIR . '/includes/REST/OrderControllerV3.php' => '\WeDevs\Dokan\REST\OrderControllerV3', + DOKAN_DIR . '/includes/REST/OrderNoteControllerV3.php' => '\WeDevs\Dokan\REST\OrderNoteControllerV3', + DOKAN_DIR . '/includes/REST/OrderRefundControllerV3.php' => '\WeDevs\Dokan\REST\OrderRefundControllerV3', + DOKAN_DIR . '/includes/REST/OrderActionControllerV3.php' => '\WeDevs\Dokan\REST\OrderActionControllerV3', ) ); } diff --git a/includes/REST/OrderActionControllerV3.php b/includes/REST/OrderActionControllerV3.php new file mode 100644 index 0000000000..a57afa7702 --- /dev/null +++ b/includes/REST/OrderActionControllerV3.php @@ -0,0 +1,323 @@ +/actions endpoint + * + * @since DOKAN_SINCE + */ +class OrderActionControllerV3 extends DokanRESTController { + + /** + * Endpoint namespace + * + * @var string + */ + protected $namespace = 'dokan/v3'; + + /** + * Route base + * + * @var string + */ + protected $rest_base = 'orders/(?P[\d]+)/actions'; + + /** + * Register routes + * + * @return void + */ + public function register_routes() { + register_rest_route( + $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + ) + ); + } + + /** + * Check if the current user has authorization for a specific order. + * + * @param int $order_id The order ID to check authorization for. + * @return bool|WP_Error True if authorized, WP_Error if not. + */ + protected function check_order_authorization( int $order_id ) { + $order = wc_get_order( $order_id ); + + if ( ! $order ) { + return new WP_Error( 'dokan_rest_invalid_order_id', __( 'Invalid order ID.', 'dokan-lite' ), array( 'status' => 404 ) ); + } + + if ( $order->get_meta( 'has_sub_order' ) ) { + return new WP_Error( 'dokan_rest_invalid_order', __( 'Sorry, this is a parent order', 'dokan-lite' ), array( 'status' => 404 ) ); + } + + $vendor_id = dokan_get_seller_id_by_order( $order_id ); + if ( $vendor_id !== dokan_get_current_user_id() ) { + return new WP_Error( 'dokan_rest_unauthorized_order', __( 'You do not have permission to access this order', 'dokan-lite' ), array( 'status' => 403 ) ); + } + + return true; + } + + /** + * Check if a given request has access to get items + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + public function get_items_permissions_check( $request ) { + if ( ! $this->check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return $this->check_order_authorization( $request['id'] ); + } + + /** + * Check if a given request has access to create items + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + public function create_item_permissions_check( $request ) { + if ( ! $this->check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_create', esc_html__( 'Sorry, you are not allowed to create resources.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return $this->check_order_authorization( $request['id'] ); + } + + /** + * Check permission for the request + * + * @return bool + */ + public function check_vendor_permission(): bool { + return dokan_is_user_seller( dokan_get_current_user_id() ); + } + + /** + * Get available order actions + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $order_id = $request['id']; + $auth_check = $this->check_order_authorization( $order_id ); + if ( is_wp_error( $auth_check ) ) { + return $auth_check; + } + + $order = wc_get_order( $order_id ); + $actions = $this->get_available_order_actions_for_order( $order ); + + return rest_ensure_response( $actions ); + } + + /** + * Create an order action + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + * @throws Exception If the action is invalid + */ + public function create_item( $request ) { + $order_id = $request['id']; + $auth_check = $this->check_order_authorization( $order_id ); + if ( is_wp_error( $auth_check ) ) { + return $auth_check; + } + + $action = $request['action']; + $order = wc_get_order( $order_id ); + + $result = $this->process_order_action( $order, $action ); + + if ( is_wp_error( $result ) ) { + return $result; + } + + return rest_ensure_response( + array( + 'message' => esc_html__( 'Order action applied successfully.', 'dokan-lite' ), + 'action' => $action, + ) + ); + } + + /** + * Get available order actions for a given order + * + * @param WC_Order $order + * @return array + */ + private function get_available_order_actions_for_order( WC_Order $order ): array { + $actions = array( + 'send_order_details' => __( 'Send order details to customer', 'dokan-lite' ), + 'send_order_details_admin' => __( 'Resend new order notification', 'dokan-lite' ), + 'regenerate_download_permissions' => __( 'Regenerate download permissions', 'dokan-lite' ), + ); + + /** + * Filters the list of available order actions. + * + * This filter allows you to add or remove order actions for the Dokan vendor dashboard. + * + * @since DOKAN_SINCE + * + * @param array $actions The list of available order actions. + * @param WC_Order $order The order object. + */ + return apply_filters( 'woocommerce_order_actions', $actions, $order ); + } + + /** + * Process the order action + * + * @param WC_Order $order + * @param string $action + * @return bool|WP_Error + * @throws Exception If the action is invalid + */ + private function process_order_action( WC_Order $order, string $action ) { + switch ( $action ) { + case 'send_order_details': + /** + * Fires before resending order emails. + * + * This action is triggered before resending various types of order emails. + * + * @since DOKAN_SINCE + * + * @param WC_Order $order The order object for which emails are being resent. + * @param string $email_type The type of email being resent (e.g., 'customer_invoice', 'new_order'). + */ + do_action( 'woocommerce_before_resend_order_emails', $order, 'customer_invoice' ); + + WC()->payment_gateways(); + WC()->shipping(); + WC()->mailer()->customer_invoice( $order ); + $order->add_order_note( __( 'Order details manually sent to customer.', 'dokan-lite' ), false, true ); + + /** + * Fires after resending an order email. + * + * This action is triggered after an order email has been resent. + * + * @since DOKAN_SINCE + * + * @param WC_Order $order The order object for which an email was resent. + * @param string $email_type The type of email that was resent (e.g., 'customer_invoice', 'new_order'). + */ + do_action( 'woocommerce_after_resend_order_email', $order, 'customer_invoice' ); + break; + + case 'send_order_details_admin': + /** + * Fires before resending order emails. + * + * This action is triggered before resending various types of order emails. + * + * @since DOKAN_SINCE + * + * @param WC_Order $order The order object for which emails are being resent. + * @param string $email_type The type of email being resent (e.g., 'customer_invoice', 'new_order'). + */ + do_action( 'woocommerce_before_resend_order_emails', $order, 'new_order' ); + + WC()->payment_gateways(); + WC()->shipping(); + + add_filter( 'woocommerce_new_order_email_allows_resend', '__return_true' ); + WC()->mailer()->emails['WC_Email_New_Order']->trigger( $order->get_id(), $order, true ); + remove_filter( 'woocommerce_new_order_email_allows_resend', '__return_true' ); + + /** + * Fires after resending an order email. + * + * This action is triggered after an order email has been resent. + * + * @since DOKAN_SINCE + * + * @param WC_Order $order The order object for which an email was resent. + * @param string $email_type The type of email that was resent (e.g., 'customer_invoice', 'new_order'). + */ + do_action( 'woocommerce_after_resend_order_email', $order, 'new_order' ); + break; + + case 'regenerate_download_permissions': + $data_store = WC_Data_Store::load( 'customer-download' ); + $data_store->delete_by_order_id( $order->get_id() ); + wc_downloadable_product_permissions( $order->get_id(), true ); + break; + + default: + if ( did_action( 'woocommerce_order_action_' . sanitize_title( $action ) ) ) { + /** + * Fires when a custom order action is being processed. + * + * This hook allows third-party plugins to add custom order actions + * and define their behavior when triggered through the API. + * + * @since DOKAN_SINCE + * + * @param WC_Order $order The order object for which the action is being performed. + * + * @hook woocommerce_order_action_{$action} + */ + do_action( 'woocommerce_order_action_' . sanitize_title( $action ), $order ); + } else { + return new WP_Error( 'dokan_rest_invalid_order_action', esc_html__( 'Invalid order action.', 'dokan-lite' ), array( 'status' => 400 ) ); + } + } + + return true; + } + + /** + * Get the Order Actions schema, conforming to JSON Schema + * + * @return array + */ + public function get_item_schema(): array { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'order_action', + 'type' => 'object', + 'properties' => array( + 'action' => array( + 'description' => esc_html__( 'Order action to perform.', 'dokan-lite' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'required' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/REST/OrderControllerV3.php b/includes/REST/OrderControllerV3.php new file mode 100644 index 0000000000..1979c7e528 --- /dev/null +++ b/includes/REST/OrderControllerV3.php @@ -0,0 +1,379 @@ +check_vendor_permission() ) { + $messages = [ + 'view' => __( 'Sorry, you cannot list resources.', 'dokan-lite' ), + 'create' => __( 'Sorry, you are not allowed to create resources.', 'dokan-lite' ), + 'edit' => __( 'Sorry, you are not allowed to edit this resource.', 'dokan-lite' ), + 'delete' => __( 'Sorry, you are not allowed to delete this resource.', 'dokan-lite' ), + ]; + return new WP_Error( "dokan_rest_cannot_$action", $messages[ $action ], [ 'status' => rest_authorization_required_code() ] ); + } + return true; + } + + /** + * Check if the current user has authorization for a specific order. + * + * @param int $order_id The order ID to check authorization for. + * + * @return bool|WP_Error True if authorized, WP_Error if not. + */ + protected function check_order_authorization( int $order_id ) { + $vendor_id = dokan_get_seller_id_by_order( $order_id ); + if ( $vendor_id !== dokan_get_current_user_id() ) { + return new WP_Error( 'dokan_rest_unauthorized_order', __( 'You do not have permission to this order', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check if the current user has vendor permissions. + * + * @return bool + */ + public function check_vendor_permission(): bool { + return dokan_is_user_seller( dokan_get_current_user_id() ); + } + + /** + * Prepare a single order for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * + * @return WP_Error|WC_Data + * @throws WC_REST_Exception When an invalid parameter is found. + * @throws WC_Data_Exception When an invalid parameter is found. + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $order = parent::prepare_object_for_database( $request, $creating ); + + if ( is_wp_error( $order ) ) { + return $order; + } + + if ( ! $order instanceof WC_Order ) { + return new WP_Error( 'dokan_rest_invalid_order', __( 'Invalid order.', 'dokan-lite' ), [ 'status' => 400 ] ); + } + + if ( $creating && ! $order->meta_exists( '_dokan_vendor_id' ) ) { + $order->update_meta_data( '_dokan_vendor_id', dokan_get_current_user_id() ); + $order->update_meta_data( '_wc_order_attribution_source_type', 'vendor' ); + $order->update_meta_data( '_wc_order_attribution_utm_source', 'typein' ); + } + + return apply_filters( "dokan_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating ); + } + + /** + * Save an object data. + * + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If is creating a new object. + * + * @return WC_Data|WP_Error + * @throws WC_REST_Exception When an invalid parameter is found. + */ + protected function save_object( $request, $creating = false ) { + $order = parent::save_object( $request, $creating ); + + if ( is_wp_error( $order ) ) { + return $order; + } + + $vendor_id = $order->get_meta( '_dokan_vendor_id' ); + if ( ! $vendor_id || dokan_get_current_user_id() !== (int) $vendor_id ) { + return new WP_Error( 'dokan_rest_cannot_edit_order', __( 'Sorry, you are not allowed to edit this resource.', 'dokan-lite' ), [ 'status' => rest_authorization_required_code() ] ); + } + + if ( $creating ) { + $order->set_created_via( 'dokan-rest-api' ); + $order->save(); + } + + $this->sync_order_and_balance( $order ); + + do_action( 'woocommerce_process_shop_order_meta', $order->get_id(), $order ); + + return $order; + } + + /** + * Sync order and balance + * + * @param WC_Data|WP_Error $order + * + * @return void + */ + private function sync_order_and_balance( $order ) { + if ( is_wp_error( $order ) ) { + return; + } + + $vendor_id = $order->get_meta( '_dokan_vendor_id' ); + $vendor = dokan()->vendor->get( $vendor_id ); + + if ( ! $vendor ) { + return; + } + + dokan_sync_insert_order( $order->get_id() ); + } + + /** + * Prepare objects query. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return array + */ + protected function prepare_objects_query( $request ): array { + $args = parent::prepare_objects_query( $request ); + $user = dokan_get_current_user_id(); + + if ( ! dokan_is_user_seller( $user ) ) { + return $args; + } + + $args['meta_query'] = $args['meta_query'] ?? []; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + $args['meta_query'][] = [ + 'key' => '_dokan_vendor_id', + 'value' => $user, + 'compare' => '=', + ]; + + return apply_filters( 'dokan_rest_orders_prepare_object_query', $args, $request ); + } + + /** + * Get all orders + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_REST_Response + */ + public function get_items( $request ): WP_REST_Response { + return $this->perform_vendor_action( + function () use ( $request ) { + return parent::get_items( $request ); + } + ); + } + + /** + * Get a single item. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $order_id = (int) $request['id']; + $result = $this->check_order_authorization( $order_id ); + if ( is_wp_error( $result ) ) { + return $result; + } + + return $this->perform_vendor_action( + function () use ( $request ) { + return parent::get_item( $request ); + } + ); + } + + /** + * Create a single order. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + return $this->perform_vendor_action( + function () use ( $request ) { + return parent::create_item( $request ); + } + ); + } + + /** + * Update a single post. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $order_id = (int) $request['id']; + $result = $this->check_order_authorization( $order_id ); + if ( is_wp_error( $result ) ) { + return $result; + } + + return $this->perform_vendor_action( + function () use ( $request ) { + return parent::update_item( $request ); + } + ); + } + + /** + * Delete a single item. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $order_id = (int) $request['id']; + $result = $this->check_order_authorization( $order_id ); + if ( is_wp_error( $result ) ) { + return $result; + } + + return $this->perform_vendor_action( + function () use ( $request ) { + return parent::delete_item( $request ); + } + ); + } + + /** + * Prepare a single order output for response. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ): WP_REST_Response { + $response = parent::prepare_object_for_response( $object, $request ); + + if ( ! $object instanceof WC_Order ) { + return $response; + } + + $response->data['subtotal'] = (string) $object->get_subtotal(); + $response->data['fee_total'] = (string) $object->get_total_fees(); + + return $response; + } + + /** + * Perform an action with vendor permission check. + * + * @param callable $action The action to perform. + * + * @return mixed The result of the action. + */ + private function perform_vendor_action( callable $action ) { + add_filter( 'woocommerce_rest_check_permissions', [ $this, 'check_vendor_permission' ] ); + $result = $action(); + remove_filter( 'woocommerce_rest_check_permissions', [ $this, 'check_vendor_permission' ] ); + return $result; + } + + /** + * Check if a given request has access to perform an action. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + return $this->check_permission( $request, 'view' ); + } + + /** + * Check if a given request has access to perform an action. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + return $this->check_permission( $request, 'create' ); + } + + /** + * Check if a given request has access to perform an action. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $result = $this->check_order_authorization( (int) $request['id'] ); + if ( is_wp_error( $result ) ) { + return $result; + } + return $this->check_permission( $request, 'view' ); + } + + /** + * Check if a given request has access to perform an action. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $result = $this->check_order_authorization( (int) $request['id'] ); + if ( is_wp_error( $result ) ) { + return $result; + } + return $this->check_permission( $request, 'edit' ); + } + + /** + * Check if a given request has access to perform an action. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + $result = $this->check_order_authorization( (int) $request['id'] ); + if ( is_wp_error( $result ) ) { + return $result; + } + return $this->check_permission( $request, 'delete' ); + } +} diff --git a/includes/REST/OrderNoteControllerV3.php b/includes/REST/OrderNoteControllerV3.php new file mode 100644 index 0000000000..f06e2f3075 --- /dev/null +++ b/includes/REST/OrderNoteControllerV3.php @@ -0,0 +1,160 @@ +/notes endpoint. + * + * @since DOKAN_SINCE + * + * @package dokan + */ +class OrderNoteControllerV3 extends WC_REST_Order_Notes_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'dokan/v3'; + + /** + * Check if the current user has authorization for a specific order. + * + * @param int $order_id The order ID to check authorization for. + * @return bool|WP_Error True if authorized, WP_Error if not. + */ + protected function check_order_authorization( int $order_id ) { + $vendor_id = dokan_get_seller_id_by_order( $order_id ); + if ( $vendor_id !== dokan_get_current_user_id() ) { + return new WP_Error( 'dokan_rest_unauthorized_order', __( 'You do not have permission to access this order', 'dokan-lite' ), array( 'status' => 403 ) ); + } + return true; + } + + /** + * Check if a given request has access to read items. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! $this->check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return $this->check_order_authorization( $request['order_id'] ); + } + + /** + * Check if a given request has access to create an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! $this->check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_create', esc_html__( 'Sorry, you are not allowed to create resources.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return $this->check_order_authorization( $request['order_id'] ); + } + + /** + * Check if a given request has access to read an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! $this->check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_view', esc_html__( 'Sorry, you cannot view this resource.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return $this->check_order_authorization( $request['order_id'] ); + } + + /** + * Check if a given request has access delete a order note. + * + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + if ( ! $this->check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return $this->check_order_authorization( $request['order_id'] ); + } + + /** + * Check permission for the request + * + * @return bool + */ + public function check_vendor_permission(): bool { + return dokan_is_user_seller( dokan_get_current_user_id() ); + } + + /** + * Create a single order note. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + $response = parent::create_item( $request ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + if ( $response->get_status() === 201 ) { + $note_id = $response->get_data()['id']; + $this->update_note_author( get_comment( $note_id ), $request, true ); + + $note = get_comment( $note_id ); + $response = $this->prepare_item_for_response( $note, $request ); + $response->set_status( 201 ); + } + + return $response; + } + + /** + * Update a single order note. + * + * @param WP_Comment $note Order note object. + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If the request is creating a new note. + * + * @return void + */ + public function update_note_author( WP_Comment $note, WP_REST_Request $request, bool $creating ) { + $user_id = dokan_get_current_user_id(); + $user = get_user_by( 'id', $user_id ); + + if ( $creating && $user instanceof \WP_User && dokan_is_user_seller( $user_id ) ) { + $data = array( + 'comment_ID' => $note->comment_ID, + 'user_id' => $user_id, + 'comment_author' => $user->display_name, + 'comment_author_email' => $user->user_email, + ); + + wp_update_comment( $data ); + + // Update comment meta to indicate it's a vendor note + update_comment_meta( (int) $note->comment_ID, 'dokan_vendor_note', 1 ); + } + } +} diff --git a/includes/REST/OrderRefundControllerV3.php b/includes/REST/OrderRefundControllerV3.php new file mode 100644 index 0000000000..44fc306bc0 --- /dev/null +++ b/includes/REST/OrderRefundControllerV3.php @@ -0,0 +1,115 @@ +/refunds endpoint. + * + * @since DOKAN_SINCE + * + * @package dokan + */ +class OrderRefundControllerV3 extends WC_REST_Order_Refunds_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'dokan/v3'; + + /** + * Check if the current user has authorization for a specific order. + * + * @param int $order_id The order ID to check authorization for. + * @return bool|WP_Error True if authorized, WP_Error if not. + */ + protected function check_order_authorization( int $order_id ) { + $vendor_id = dokan_get_seller_id_by_order( $order_id ); + if ( $vendor_id !== dokan_get_current_user_id() ) { + return new WP_Error( 'dokan_rest_unauthorized_order', __( 'You do not have permission to access this order', 'dokan-lite' ), array( 'status' => 403 ) ); + } + return true; + } + + /** + * Check if a given request has access to read items. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! $this->check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return $this->check_order_authorization( $request['order_id'] ); + } + + /** + * Check if a given request has access to create an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! $this->check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_create', esc_html__( 'Sorry, you are not allowed to create resources.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return $this->check_order_authorization( $request['order_id'] ); + } + + /** + * Check if a given request has access to read an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! $this->check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_view', esc_html__( 'Sorry, you cannot view this resource.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + + $refund = $this->get_object( (int) $request['id'] ); + if ( ! $refund || 0 === $refund->get_id() ) { + return new WP_Error( 'dokan_rest_invalid_id', __( 'Invalid ID.', 'dokan-lite' ), array( 'status' => 404 ) ); + } + + return $this->check_order_authorization( $refund->get_parent_id() ); + } + + /** + * Check if a given request has access to delete an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + if ( ! $this->check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_delete', esc_html__( 'Sorry, you are not allowed to delete this resource.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + + $refund = $this->get_object( (int) $request['id'] ); + if ( ! $refund || 0 === $refund->get_id() ) { + return new WP_Error( 'dokan_rest_invalid_id', __( 'Invalid ID.', 'dokan-lite' ), array( 'status' => 404 ) ); + } + + return $this->check_order_authorization( $refund->get_parent_id() ); + } + + /** + * Check permission for the request + * + * @return bool + */ + public function check_vendor_permission(): bool { + return dokan_is_user_seller( dokan_get_current_user_id() ); + } +} diff --git a/includes/REST/PaymentGatewayController.php b/includes/REST/PaymentGatewayController.php new file mode 100644 index 0000000000..fac4a87eea --- /dev/null +++ b/includes/REST/PaymentGatewayController.php @@ -0,0 +1,61 @@ +check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check whether a given request has permission to view a specific payment gateway. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! $this->check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check vendor permission. + * + * @return bool + */ + protected function check_vendor_permission(): bool { + return current_user_can( 'dokan_view_store_payment_menu' ); + } +} diff --git a/includes/REST/ShippingMethodController.php b/includes/REST/ShippingMethodController.php new file mode 100644 index 0000000000..7042125475 --- /dev/null +++ b/includes/REST/ShippingMethodController.php @@ -0,0 +1,61 @@ +check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check if a given request has access to read a shipping method. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! $this->check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check vendor permission. + * + * @return bool + */ + protected function check_vendor_permission(): bool { + return dokan_is_user_seller( dokan_get_current_user_id() ); + } +} diff --git a/includes/REST/TaxClassController.php b/includes/REST/TaxClassController.php new file mode 100644 index 0000000000..0e0da078b6 --- /dev/null +++ b/includes/REST/TaxClassController.php @@ -0,0 +1,61 @@ +check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check if a given request has access to delete a tax class. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + if ( ! $this->check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check vendor permission. + * + * @return bool + */ + protected function check_vendor_permission(): bool { + return dokan_is_user_seller( dokan_get_current_user_id() ); + } +} diff --git a/includes/REST/TaxController.php b/includes/REST/TaxController.php new file mode 100644 index 0000000000..1e50750aae --- /dev/null +++ b/includes/REST/TaxController.php @@ -0,0 +1,61 @@ +check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check if a given request has access to read a tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! $this->check_vendor_permission() ) { + return new WP_Error( 'dokan_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check vendor permission. + * + * @return bool + */ + protected function check_vendor_permission(): bool { + return dokan_is_user_seller( dokan_get_current_user_id() ); + } +} diff --git a/tests/php/src/REST/DataCountriesControllerTest.php b/tests/php/src/REST/DataCountriesControllerTest.php new file mode 100644 index 0000000000..f95c6524eb --- /dev/null +++ b/tests/php/src/REST/DataCountriesControllerTest.php @@ -0,0 +1,55 @@ +server->get_routes( $this->namespace ); + $full_route = $this->get_route( 'data/countries' ); + + $this->assertArrayHasKey( $full_route, $routes ); + $this->assertNestedContains( [ 'methods' => [ 'GET' => true ] ], $routes[ $full_route ] ); + } + + /** + * Test getting countries. + */ + public function test_get_countries() { + wp_set_current_user( $this->seller_id1 ); + + $response = $this->get_request( 'data/countries' ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertNotEmpty( $data ); + // Add more assertions based on expected country data + } + + /** + * Test getting states for a specific country. + */ + public function test_get_country_states() { + wp_set_current_user( $this->seller_id1 ); + + $response = $this->get_request( 'data/countries/US' ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertArrayHasKey( 'states', $data ); + $this->assertNotEmpty( $data['states'] ); + } +} diff --git a/tests/php/src/REST/OrderActionsControllerV3Test.php b/tests/php/src/REST/OrderActionsControllerV3Test.php new file mode 100644 index 0000000000..742b306dfe --- /dev/null +++ b/tests/php/src/REST/OrderActionsControllerV3Test.php @@ -0,0 +1,72 @@ +seller_id1 ); + + $order_id = $this->factory()->order->set_seller_id( $this->seller_id1 )->create(); + + $response = $this->get_request( "orders/{$order_id}/actions" ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + + $this->assertIsArray( $data ); + $this->assertArrayHasKey( 'send_order_details', $data ); + $this->assertArrayHasKey( 'send_order_details_admin', $data ); + $this->assertArrayHasKey( 'regenerate_download_permissions', $data ); + } + + /** + * Test applying an order action. + */ + public function test_apply_order_action() { + wp_set_current_user( $this->seller_id1 ); + + $order_id = $this->factory()->order->set_seller_id( $this->seller_id1 )->create(); + + $response = $this->post_request( + "orders/{$order_id}/actions", [ + 'action' => 'send_order_details', + ] + ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'message', $data ); + $this->assertEquals( 'Order action applied successfully.', $data['message'] ); + $this->assertEquals( 'send_order_details', $data['action'] ); + } + + /** + * Test applying an invalid order action. + */ + public function test_apply_invalid_order_action() { + wp_set_current_user( $this->seller_id1 ); + + $order_id = $this->factory()->order->set_seller_id( $this->seller_id1 )->create(); + + $response = $this->post_request( + "orders/{$order_id}/actions", [ + 'action' => 'invalid_action', + ] + ); + + $this->assertEquals( 400, $response->get_status() ); + } +} diff --git a/tests/php/src/REST/OrderNotesControllerTestV3.php b/tests/php/src/REST/OrderNotesControllerTestV3.php new file mode 100644 index 0000000000..ecef1c9312 --- /dev/null +++ b/tests/php/src/REST/OrderNotesControllerTestV3.php @@ -0,0 +1,81 @@ +seller_id1 ); + + $order_id = $this->factory()->order->set_seller_id( $this->seller_id1 )->create(); + + $response = $this->post_request( + "orders/{$order_id}/notes", [ + 'note' => 'Test note', + 'customer_note' => false, + ] + ); + + $this->assertEquals( 201, $response->get_status() ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'id', $data ); + $this->assertEquals( 'Test note', $data['note'] ); + $this->assertFalse( $data['customer_note'] ); + } + + /** + * Test getting order notes. + */ + public function test_get_order_notes() { + wp_set_current_user( $this->seller_id1 ); + + $order_id = $this->factory()->order->set_seller_id( $this->seller_id1 )->create(); + + // Add some notes + wc_create_order_note( $order_id, 'Note 1' ); + wc_create_order_note( $order_id, 'Note 2', 1, true ); + + $response = $this->get_request( "orders/{$order_id}/notes" ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + + $this->assertCount( 2, $data ); + $this->assertEquals( 'Note 1', $data[0]['note'] ); + $this->assertFalse( $data[0]['customer_note'] ); + $this->assertEquals( 'Note 2', $data[1]['note'] ); + $this->assertTrue( $data[1]['customer_note'] ); + } + + /** + * Test deleting an order note. + */ + public function test_delete_order_note() { + wp_set_current_user( $this->seller_id1 ); + + $order_id = $this->factory()->order->set_seller_id( $this->seller_id1 )->create(); + $note_id = wc_create_order_note( $order_id, 'Test note' ); + + $response = $this->delete_request( + "orders/{$order_id}/notes/{$note_id}", + array( 'force' => true ) + ); + + $this->assertEquals( 200, $response->get_status() ); + + // Verify the note is deleted + $this->assertEmpty( wc_get_order_notes( [ 'id' => $note_id ] ) ); + } +} diff --git a/tests/php/src/REST/OrderRefundsControllerV3Test.php b/tests/php/src/REST/OrderRefundsControllerV3Test.php new file mode 100644 index 0000000000..239a859da3 --- /dev/null +++ b/tests/php/src/REST/OrderRefundsControllerV3Test.php @@ -0,0 +1,68 @@ +seller_id1 ); + + $order_id = $this->factory()->order->create_order_with_fees_and_shipping(); + + $order = wc_get_order( $order_id ); + + $product_item = current( $order->get_items( 'line_item' ) ); + $fee_item = current( $order->get_items( 'fee' ) ); + $shipping_item = current( $order->get_items( 'shipping' ) ); + + $refund = wc_create_refund( + array( + 'order_id' => $order->get_id(), + 'reason' => 'testing', + 'line_items' => array( + $product_item->get_id() => + array( + 'qty' => 1, + 'refund_total' => 1, + ), + $fee_item->get_id() => + array( + 'refund_total' => 10, + ), + $shipping_item->get_id() => + array( + 'refund_total' => 20, + ), + ), + ) + ); + + $this->assertNotWPError( $refund ); + + $response = $this->get_request( 'orders/' . $order->get_id() . '/refunds/' . $refund->get_id() ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + + $this->assertContains( 'line_items', array_keys( $data ) ); + $this->assertEquals( -1, $data['line_items'][0]['total'] ); + + $this->assertContains( 'fee_lines', array_keys( $data ) ); + $this->assertEquals( -10, $data['fee_lines'][0]['total'] ); + + $this->assertContains( 'shipping_lines', array_keys( $data ) ); + $this->assertEquals( -20, $data['shipping_lines'][0]['total'] ); + } +} diff --git a/tests/php/src/REST/OrdersControllerV3Test.php b/tests/php/src/REST/OrdersControllerV3Test.php new file mode 100644 index 0000000000..e7324360ca --- /dev/null +++ b/tests/php/src/REST/OrdersControllerV3Test.php @@ -0,0 +1,666 @@ +server->get_routes( $this->namespace ); + $full_route = $this->get_route( 'orders' ); + + $this->assertArrayHasKey( $full_route, $routes ); + $this->assertNestedContains( [ 'methods' => [ 'GET' => true ] ], $routes[ $full_route ] ); + } + + /** + * Get all expected fields. + */ + public function get_expected_response_fields(): array { + return array( + 'id', + 'parent_id', + 'number', + 'order_key', + 'created_via', + 'version', + 'status', + 'currency', + 'date_created', + 'date_created_gmt', + 'date_modified', + 'date_modified_gmt', + 'discount_total', + 'discount_tax', + 'shipping_total', + 'shipping_tax', + 'cart_tax', + 'total', + 'total_tax', + 'prices_include_tax', + 'customer_id', + 'customer_ip_address', + 'customer_user_agent', + 'customer_note', + 'billing', + 'shipping', + 'payment_method', + 'payment_method_title', + 'transaction_id', + 'date_paid', + 'date_paid_gmt', + 'date_completed', + 'date_completed_gmt', + 'cart_hash', + 'meta_data', + 'line_items', + 'tax_lines', + 'shipping_lines', + 'fee_lines', + 'coupon_lines', + 'currency_symbol', + 'refunds', + 'payment_url', + 'is_editable', + 'needs_payment', + 'needs_processing', + 'stores', + 'store', + 'subtotal', + 'fee_total', + ); + } + + /** + * Test that all expected response fields are present. + * Note: This has fields hardcoded intentionally instead of fetching from schema to test for any bugs in schema result. Add new fields manually when added to schema. + */ + public function test_orders_api_get_all_fields() { + wp_set_current_user( $this->seller_id1 ); + + $order_id = $this->factory()->order->set_seller_id( $this->seller_id1 )->create(); + $response = $this->get_request( 'orders/' . $order_id ); + + $data = $response->get_data(); + $response_fields = array_keys( $data ); + + $expected_response_fields = $this->get_expected_response_fields(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEmpty( array_diff( $expected_response_fields, $response_fields ), 'These fields were expected but not present in API response: ' . print_r( array_diff( $expected_response_fields, $response_fields ), true ) ); + $this->assertEmpty( array_diff( $response_fields, $expected_response_fields ), 'These fields were not expected in the API response: ' . print_r( array_diff( $response_fields, $expected_response_fields ), true ) ); + } + + /** + * Tests getting all orders with the REST API. + * + * @todo: Meta query is not working in test cases + * + * @return void + */ + public function test_orders_get_all(): void { + $order_ids = $this->factory()->order->set_seller_id( $this->seller_id1 )->create_many( 5 ); + + // Create some orders for another seller. + $this->factory()->order->set_seller_id( $this->seller_id2 )->create_many( 5 ); + + wp_set_current_user( $this->seller_id1 ); + + $response = $this->get_request( 'orders', array( 'per_page' => 100 ) ); + $status = $response->get_status(); + $data = $response->get_data(); + + $this->assertEquals( 200, $status, 'Expected 200 response code, got ' . $status ); + $this->assertCount( 5, $data, 'Expected 5 orders, got ' . count( $data ) . '. Order IDs created: ' . implode( ', ', $order_ids ) ); + } + + /** + * Tests filtering with the 'before' and 'after' params. + * + * @return void + */ + public function test_orders_date_filtering(): void { + wp_set_current_user( $this->seller_id1 ); + + $time_before_orders = time(); + + $this->factory()->order->set_seller_id( $this->seller_id1 )->create_many( 5 ); + + $time_after_orders = time() + HOUR_IN_SECONDS; + + // No date params should return all orders. + $response = $this->get_request( 'orders', array( 'dates_are_gmt' => 1 ) ); + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 5, $response->get_data() ); + + // There are no orders before `$time_before_orders`. + $response = $this->get_request( 'orders', array( 'before' => gmdate( DateTime::ATOM, $time_before_orders ) ) ); + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 0, $response->get_data() ); + + // All orders are before `$time_after_orders`. + $response = $this->get_request( 'orders', array( 'before' => gmdate( DateTime::ATOM, $time_after_orders ) ) ); + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 5, $response->get_data() ); + } + + /** + * Tests filtering with the 'before' and 'after' params. + * + * @return void + */ + public function test_orders_create(): void { + wp_set_current_user( $this->seller_id1 ); + + $product_id = $this->factory()->product->create(); + $product = wc_get_product( $product_id ); + + $order_params = array( + 'payment_method' => 'bacs', + 'payment_method_title' => 'Direct Bank Transfer', + 'set_paid' => true, + 'billing' => array( + 'first_name' => 'John', + 'last_name' => 'Doe', + 'address_1' => '969 Market', + 'address_2' => '', + 'city' => 'San Francisco', + 'state' => 'CA', + 'postcode' => '94103', + 'country' => 'US', + 'email' => 'john.doe@example.com', + 'phone' => '(555) 555-5555', + ), + 'line_items' => array( + array( + 'product_id' => $product->get_id(), + 'quantity' => 3, + ), + ), + ); + $order_params['shipping'] = $order_params['billing']; + + $response = $this->post_request( 'orders', $order_params ); + + $this->assertEquals( 201, $response->get_status() ); + + $data = $response->get_data(); + $this->assertArrayHasKey( 'id', $data ); + $this->assertEquals( 'processing', $data['status'] ); + + wp_cache_flush(); + + // Fetch the order and compare some data. + $order = wc_get_order( $data['id'] ); + $this->assertNotEmpty( $order ); + + $this->assertEquals( (float) ( $product->get_price() * 3 ), (float) $order->get_total() ); + $this->assertEquals( $order_params['payment_method'], $order->get_payment_method( 'edit' ) ); + + foreach ( array_keys( $order_params['billing'] ) as $address_key ) { + $this->assertEquals( $order_params['billing'][ $address_key ], $order->{"get_billing_{$address_key}"}( 'edit' ) ); + } + } + + /** + * Tests deleting an order. + */ + public function test_orders_delete(): void { + wp_set_current_user( $this->seller_id1 ); + + $order_id = $this->factory()->order->set_seller_id( $this->seller_id1 )->create( + array( + 'status' => 'completed', + ) + ); + + $response = $this->delete_request( 'orders/' . $order_id ); + + $this->assertEquals( 200, $response->get_status() ); + + // Check that the response includes order data from the order (before deletion). + $data = $response->get_data(); + $this->assertArrayHasKey( 'id', $data ); + $this->assertEquals( $data['id'], $order_id ); + $this->assertEquals( 'completed', $data['status'] ); + + wp_cache_flush(); + + // Check the order was actually deleted. + $order = wc_get_order( $order_id ); + $this->assertEquals( 'trash', $order->get_status( 'edit' ) ); + } + + /** + * Test that the `include_meta` param filters the `meta_data` prop correctly. + */ + public function test_collection_param_include_meta() { + wp_set_current_user( $this->seller_id1 ); + + $order_params = array( + 'meta_data' => array( + array( + 'key' => 'test1', + 'value' => 'test1', + ), + array( + 'key' => 'test2', + 'value' => 'test2', + ), + ), + ); + + $this->factory()->order->set_seller_id( $this->seller_id1 )->create_many( 3, $order_params ); + + $response = $this->get_request( 'orders', array( 'include_meta' => 'test1' ) ); + $this->assertEquals( 200, $response->get_status() ); + + $response_data = $response->get_data(); + $this->assertCount( 3, $response_data ); + + foreach ( $response_data as $order ) { + $this->assertArrayHasKey( 'meta_data', $order ); + $this->assertEquals( 1, count( $order['meta_data'] ) ); + $meta_keys = array_map( + function ( $meta_item ) { + return $meta_item->get_data()['key']; + }, + $order['meta_data'] + ); + $this->assertContains( 'test1', $meta_keys ); + } + } + + /** + * Test that the `include_meta` param is skipped when empty. + */ + public function test_collection_param_include_meta_empty() { + wp_set_current_user( $this->seller_id1 ); + + $order_params = array( + 'meta_data' => array( + array( + 'key' => 'test1', + 'value' => 'test1', + ), + array( + 'key' => 'test2', + 'value' => 'test2', + ), + ), + ); + + $this->factory()->order->set_seller_id( $this->seller_id1 )->create_many( 3, $order_params ); + + $response = $this->get_request( 'orders', array( 'include_meta' => '' ) ); + + $this->assertEquals( 200, $response->get_status() ); + + $response_data = $response->get_data(); + $this->assertCount( 3, $response_data ); + + foreach ( $response_data as $order ) { + $this->assertArrayHasKey( 'meta_data', $order ); + $meta_keys = array_map( + function ( $meta_item ) { + return $meta_item->get_data()['key']; + }, + $order['meta_data'] + ); + $this->assertContains( 'test1', $meta_keys ); + $this->assertContains( 'test2', $meta_keys ); + } + } + + /** + * Test that the `exclude_meta` param filters the `meta_data` prop correctly. + */ + public function test_collection_param_exclude_meta() { + wp_set_current_user( $this->seller_id1 ); + + $order_params = array( + 'meta_data' => array( + array( + 'key' => 'test1', + 'value' => 'test1', + ), + array( + 'key' => 'test2', + 'value' => 'test2', + ), + ), + ); + + $this->factory()->order->set_seller_id( $this->seller_id1 )->create_many( 3, $order_params ); + + $response = $this->get_request( 'orders', array( 'exclude_meta' => 'test1' ) ); + $this->assertEquals( 200, $response->get_status() ); + + $response_data = $response->get_data(); + $this->assertCount( 3, $response_data ); + + foreach ( $response_data as $order ) { + $this->assertArrayHasKey( 'meta_data', $order ); + $meta_keys = array_map( + function ( $meta_item ) { + return $meta_item->get_data()['key']; + }, + $order['meta_data'] + ); + $this->assertContains( 'test2', $meta_keys ); + $this->assertNotContains( 'test1', $meta_keys ); + } + } + + /** + * Test that the `include_meta` param overrides the `exclude_meta` param. + */ + public function test_collection_param_include_meta_override() { + wp_set_current_user( $this->seller_id1 ); + + $order_params = array( + 'meta_data' => array( + array( + 'key' => 'test1', + 'value' => 'test1', + ), + array( + 'key' => 'test2', + 'value' => 'test2', + ), + ), + ); + + $this->factory()->order->set_seller_id( $this->seller_id1 )->create_many( 3, $order_params ); + + $response = $this->get_request( + 'orders', array( + 'include_meta' => 'test1', + 'exclude_meta' => 'test1', + ) + ); + $this->assertEquals( 200, $response->get_status() ); + + $response_data = $response->get_data(); + $this->assertCount( 3, $response_data ); + + foreach ( $response_data as $order ) { + $this->assertArrayHasKey( 'meta_data', $order ); + $this->assertEquals( 1, count( $order['meta_data'] ) ); + $meta_keys = array_map( + function ( $meta_item ) { + return $meta_item->get_data()['key']; + }, + $order['meta_data'] + ); + $this->assertContains( 'test1', $meta_keys ); + } + } + + /** + * Test that the meta_data property contains an array, and not an object, after being filtered. + */ + public function test_collection_param_include_meta_returns_array() { + wp_set_current_user( $this->seller_id1 ); + + $order_params = array( + 'meta_data' => array( + array( + 'key' => 'test1', + 'value' => 'test1', + ), + array( + 'key' => 'test2', + 'value' => 'test2', + ), + ), + ); + + $this->factory()->order->set_seller_id( $this->seller_id1 )->create( $order_params ); + + $response = $this->get_request( 'orders', array( 'include_meta' => 'test2' ) ); + $this->assertEquals( 200, $response->get_status() ); + + $response_data = $this->server->response_to_data( $response, false ); + $encoded_data_string = wp_json_encode( $response_data ); + $decoded_data_object = \json_decode( $encoded_data_string, false ); // Ensure object instead of associative array. + + $this->assertIsArray( $decoded_data_object[0]->meta_data ); + } + + /** + * Test that the `include_meta` param is skipped when empty. + */ + public function test_order_update_line_item_quantity_updates_product_stock() { + wp_set_current_user( $this->seller_id1 ); + + $product_id = $this->factory()->product->create(); + $product = wc_get_product( $product_id ); + $product->set_manage_stock( true ); + $product->set_stock_quantity( 10 ); + $product->save(); + + $customer = $this->factory()->customer->create_customer(); + + $order_params = array( + 'status' => 'on-hold', + 'customer_id' => $customer->get_id(), + 'line_items' => array( + array( + 'product_id' => $product_id, + 'quantity' => 4, + ), + ), + ); + + $order_id = $this->factory()->order->set_seller_id( $this->seller_id1 )->create( $order_params ); + $order = wc_get_order( $order_id ); + $items = $order->get_items(); + $item = reset( $items ); + wc_maybe_adjust_line_item_product_stock( $item ); + + $product = wc_get_product( $product->get_id() ); + $this->assertEquals( 6, $product->get_stock_quantity() ); + + $response = $this->post_request( + 'orders/' . $order->get_id(), + array( + 'line_items' => array( + array( + 'id' => $item->get_id(), + 'quantity' => 5, + ), + ), + ) + ); + $this->assertEquals( 200, $response->get_status() ); + + $product = wc_get_product( $product ); + $this->assertEquals( 5, $product->get_stock_quantity() ); + } + + /** + * @testdox When a line item in an order is removed via REST API, the product's stock should also be updated. + */ + public function test_order_remove_line_item_updates_product_stock() { + wp_set_current_user( $this->seller_id1 ); + + $product_id = $this->factory()->product->create(); + $product = wc_get_product( $product_id ); + $product->set_manage_stock( true ); + $product->set_stock_quantity( 10 ); + $product->save(); + + $customer = $this->factory()->customer->create_customer(); + + $order_params = array( + 'status' => 'on-hold', + 'customer_id' => $customer->get_id(), + 'line_items' => array( + array( + 'product_id' => $product_id, + 'quantity' => 4, + ), + ), + ); + + $order_id = $this->factory()->order->set_seller_id( $this->seller_id1 )->create( $order_params ); + $order = wc_get_order( $order_id ); + $items = $order->get_items(); + $item = reset( $items ); + wc_maybe_adjust_line_item_product_stock( $item ); + + $product = wc_get_product( $product->get_id() ); + $this->assertEquals( 6, $product->get_stock_quantity() ); + + $response = $this->post_request( + 'orders/' . $order->get_id(), + array( + 'line_items' => array( + array( + 'id' => $item->get_id(), + 'quantity' => 0, + ), + ), + ) + ); + $this->assertEquals( 200, $response->get_status() ); + + $order = wc_get_order( $order ); + $this->assertEmpty( $order->get_items() ); + + $product = wc_get_product( $product ); + $this->assertEquals( 10, $product->get_stock_quantity() ); + } + + /** + * When a line item in an order is updated via REST API, the product's stock should also be updated. + */ + public function test_order_commission_by_vendor() { + wp_set_current_user( $this->seller_id1 ); + + // Update commission settings for dokan. + $commission_options = array( + 'commission_type' => 'percentage', + 'admin_percentage' => 10, + 'additional_fee' => 0, + ); + update_option( 'dokan_selling', $commission_options ); + + $order_id = $this->factory()->order->set_seller_id( $this->seller_id1 )->create(); + $response = $this->get_request( 'orders/' . $order_id ); + + $this->assertEquals( 200, $response->get_status() ); + + $order = $response->get_data(); + + $this->assertArrayHasKey( 'line_items', $order ); + + foreach ( $order['line_items'] as $line_item ) { + $this->assertArrayHasKey( 'meta_data', $order ); + $this->assertGreaterThanOrEqual( 3, count( $order['meta_data'] ) ); + + $meta_keys = array_column( $line_item['meta_data'], 'key' ); + $this->assertContains( '_dokan_commission_rate', $meta_keys ); + $this->assertContains( '_dokan_commission_type', $meta_keys ); + $this->assertContains( '_dokan_additional_fee', $meta_keys ); + + // calculate commission for admin. + $vendor_earning = dokan()->commission->get_earning_by_product( $line_item['product_id'] ); + $this->assertEquals( 9.0, $vendor_earning ); + + // calculate commission for admin. + $admin_earning = dokan()->commission->get_earning_by_product( $line_item['product_id'], 'admin' ); + $this->assertEquals( 1.0, $admin_earning ); + } + } + + /** + * Tests creating an order with a vendor coupon using the REST API. + * + * @return void + */ + public function test_orders_create_with_vendor_coupon(): void { + wp_set_current_user( $this->seller_id1 ); + + // Create products + $product_id = $this->factory()->product->set_seller_id( $this->seller_id1 )->create( + array( + 'regular_price' => 100, + 'price' => 100, + ) + ); + + // Create a vendor coupon + $coupon_code = 'vendorcoupon'; + $coupon_id = $this->factory()->coupon->create( + [ + 'code' => $coupon_code, + 'amount' => 10, + 'discount_type' => 'percent', + 'product_ids' => [ $product_id ], + ] + ); + + // Set vendor-specific meta + $coupon = $this->factory()->coupon->get_object_by_id( $coupon_id ); + $coupon->update_meta_data( 'apply_before_tax', 'no' ); + $coupon->update_meta_data( 'apply_new_products', 'yes' ); + $coupon->update_meta_data( 'show_on_store', 'yes' ); + + // Order parameters including the coupon + $order_params = [ + 'payment_method' => 'bacs', + 'set_paid' => true, + 'line_items' => [ + [ + 'product_id' => $product_id, + 'quantity' => 2, + ], + ], + 'coupon_lines' => [ + [ + 'code' => $coupon_code, + ], + ], + ]; + + // Create the order + $response = $this->post_request( 'orders', $order_params ); + + // Validate the response + $this->assertEquals( 201, $response->get_status() ); + $data = $response->get_data(); + + // Check order details + $this->assertEquals( 'processing', $data['status'] ); + $this->assertCount( 1, $data['coupon_lines'] ); + $this->assertEquals( $coupon_code, $data['coupon_lines'][0]['code'] ); + + // Verify order total (assuming product price is 100) + $product = wc_get_product( $product_id ); + $expected_total = (float) ( $product->get_price() * 2 ) * 0.95; // 10% discount + $this->assertEquals( $expected_total, $data['total'] ); + + // Check coupon usage + $coupon = new WC_Coupon( $coupon_code ); + $this->assertEquals( 1, $coupon->get_usage_count() ); + + // Verify vendor ID + $order = wc_get_order( $data['id'] ); + $this->assertEquals( $this->seller_id1, $order->get_meta( '_dokan_vendor_id' ) ); + } +} diff --git a/tests/php/src/REST/PaymentGatewaysControllerTest.php b/tests/php/src/REST/PaymentGatewaysControllerTest.php new file mode 100644 index 0000000000..5466661e58 --- /dev/null +++ b/tests/php/src/REST/PaymentGatewaysControllerTest.php @@ -0,0 +1,54 @@ +server->get_routes( $this->namespace ); + $full_route = $this->get_route( 'payment_gateways' ); + + $this->assertArrayHasKey( $full_route, $routes ); + $this->assertNestedContains( [ 'methods' => [ 'GET' => true ] ], $routes[ $full_route ] ); + } + + /** + * Test getting payment gateways. + */ + public function test_get_payment_gateways() { + wp_set_current_user( $this->seller_id1 ); + + $response = $this->get_request( 'payment_gateways' ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertNotEmpty( $data ); + } + + /** + * Test getting a single payment gateway. + */ + public function test_get_payment_gateway() { + wp_set_current_user( $this->seller_id1 ); + + // Assuming 'bacs' is a valid payment gateway ID + $response = $this->get_request( 'payment_gateways/bacs' ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertEquals( 'bacs', $data['id'] ); + } +} diff --git a/tests/php/src/REST/ShippingMethodControllerTest.php b/tests/php/src/REST/ShippingMethodControllerTest.php new file mode 100644 index 0000000000..e8a3519bdc --- /dev/null +++ b/tests/php/src/REST/ShippingMethodControllerTest.php @@ -0,0 +1,57 @@ +server->get_routes( $this->namespace ); + $full_route = $this->get_route( 'shipping_methods' ); + + $this->assertArrayHasKey( $full_route, $routes ); + $this->assertNestedContains( [ 'methods' => [ 'GET' => true ] ], $routes[ $full_route ] ); + } + + /** + * Test getting shipping methods. + */ + public function test_get_shipping_methods() { + wp_set_current_user( $this->seller_id1 ); + + $response = $this->get_request( 'shipping_methods' ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertNotEmpty( $data ); + // Add more assertions based on expected shipping method data + } + + /** + * Test getting a single shipping method. + */ + public function test_get_shipping_method() { + wp_set_current_user( $this->seller_id1 ); + + // Assuming 'flat_rate' is a valid shipping method ID + $response = $this->get_request( 'shipping_methods/flat_rate' ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertEquals( 'flat_rate', $data['id'] ); + } +} diff --git a/tests/php/src/REST/TaxClassControllerTest.php b/tests/php/src/REST/TaxClassControllerTest.php new file mode 100644 index 0000000000..90566cb8f4 --- /dev/null +++ b/tests/php/src/REST/TaxClassControllerTest.php @@ -0,0 +1,39 @@ +server->get_routes( $this->namespace ); + $full_route = $this->get_route( 'taxes/classes' ); + + $this->assertArrayHasKey( $full_route, $routes ); + $this->assertNestedContains( [ 'methods' => [ 'GET' => true ] ], $routes[ $full_route ] ); + } + + /** + * Test getting tax classes. + */ + public function test_get_tax_classes() { + wp_set_current_user( $this->seller_id1 ); + + $response = $this->get_request( 'taxes/classes' ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertIsArray( $data ); + } +} diff --git a/tests/php/src/REST/TaxControllerTest.php b/tests/php/src/REST/TaxControllerTest.php new file mode 100644 index 0000000000..37cbf431c5 --- /dev/null +++ b/tests/php/src/REST/TaxControllerTest.php @@ -0,0 +1,40 @@ +server->get_routes( $this->namespace ); + $full_route = $this->get_route( 'taxes' ); + + $this->assertArrayHasKey( $full_route, $routes ); + $this->assertNestedContains( [ 'methods' => [ 'GET' => true ] ], $routes[ $full_route ] ); + } + + /** + * Test getting taxes. + */ + public function test_get_taxes() { + wp_set_current_user( $this->seller_id1 ); + + $response = $this->get_request( 'taxes' ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertIsArray( $data ); + // Add more assertions based on expected tax data + } +}