From 53db5ff4c8f2ab3605019e394d7cc75a3eca9cba Mon Sep 17 00:00:00 2001 From: Claudio Sanches Date: Fri, 26 Feb 2016 17:24:33 -0300 Subject: [PATCH] Created coupons POST and PUT endpoints and coupons schema --- .../abstract-wc-rest-posts-controller.php | 220 ++++++++++- includes/api/wc-rest-coupons-controller.php | 370 ++++++++++++++++++ 2 files changed, 588 insertions(+), 2 deletions(-) diff --git a/includes/abstracts/abstract-wc-rest-posts-controller.php b/includes/abstracts/abstract-wc-rest-posts-controller.php index e70c256450d..89151ac7218 100644 --- a/includes/abstracts/abstract-wc-rest-posts-controller.php +++ b/includes/abstracts/abstract-wc-rest-posts-controller.php @@ -51,6 +51,23 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return true; } + /** + * 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 ) { + + $post_type = get_post_type_object( $this->post_type ); + + if ( ! current_user_can( $post_type->cap->create_posts ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', sprintf( __( 'Sorry, you are not allowed to create a new %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + /** * Check if a given request has access to read items. * @@ -63,6 +80,23 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return current_user_can( $post_type->cap->read_private_posts ); } + /** + * Check if a given request has access to update an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $post = get_post( $request['id'] ); + $post_type = get_post_type_object( $this->post_type ); + + if ( $post && ! $this->check_update_permission( $post ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', sprintf( __( 'Sorry, you are not allowed to update this %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) );; + } + + return true; + } + /** * Check if a given request has access to delete an item. * @@ -70,7 +104,6 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { * @return bool|WP_Error */ public function delete_item_permissions_check( $request ) { - $post = get_post( $request['id'] ); if ( $post && ! $this->check_delete_permission( $post ) ) { @@ -94,7 +127,18 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { } /** - * Check if we can delete a post. + * Check if we can edit an item. + * + * @param object $post Post object. + * @return boolean Can we edit it? + */ + protected function check_update_permission( $post ) { + $post_type = get_post_type_object( $this->post_type ); + return current_user_can( $post_type->cap->edit_post, $post->ID ); + } + + /** + * Check if we can delete an item. * * @param object $post Post object. * @return boolean Can we delete it? @@ -128,6 +172,178 @@ abstract class WC_REST_Posts_Controller extends WP_REST_Controller { return $response; } + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $post = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $post ) ) { + return $post; + } + + $post->post_type = $this->post_type; + $post_id = wp_insert_post( $post, true ); + + if ( is_wp_error( $post_id ) ) { + + if ( in_array( $post_id->get_error_code(), array( 'db_insert_error' ) ) ) { + $post_id->add_data( array( 'status' => 500 ) ); + } else { + $post_id->add_data( array( 'status' => 400 ) ); + } + return $post_id; + } + $post->ID = $post_id; + + $schema = $this->get_item_schema(); + + // if ( ! empty( $schema['properties']['sticky'] ) ) { + // if ( ! empty( $request['sticky'] ) ) { + // stick_post( $post_id ); + // } else { + // unstick_post( $post_id ); + // } + // } + + // if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) { + // $this->handle_featured_media( $request['featured_media'], $post->ID ); + // } + + // $terms_update = $this->handle_terms( $post->ID, $request ); + // if ( is_wp_error( $terms_update ) ) { + // return $terms_update; + // } + + $post = get_post( $post_id ); + $this->update_additional_fields_for_object( $post, $request ); + + // Add meta fields. + $meta_fields = $this->add_post_meta_fields( $post, $request ); + if ( is_wp_error( $meta_fields ) ) { + // Remove post. + wp_delete_post( $post->ID, true ); + + return $meta_fields; + } + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param object $post Inserted object (not a WP_Post object). + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', WC_API::REST_API_NAMESPACE, $this->rest_base, $post_id ) ) ); + + return $response; + } + + /** + * Add post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + return true; + } + + /** + * 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 ) { + $id = (int) $request['id']; + $post = get_post( $id ); + + if ( empty( $id ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $post = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $post ) ) { + return $post; + } + // Convert the post object to an array, otherwise wp_update_post will expect non-escaped input. + $post_id = wp_update_post( (array) $post, true ); + if ( is_wp_error( $post_id ) ) { + if ( in_array( $post_id->get_error_code(), array( 'db_update_error' ) ) ) { + $post_id->add_data( array( 'status' => 500 ) ); + } else { + $post_id->add_data( array( 'status' => 400 ) ); + } + return $post_id; + } + + $schema = $this->get_item_schema(); + + // if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) { + // $this->handle_featured_media( $request['featured_media'], $post_id ); + // } + + // if ( ! empty( $schema['properties']['sticky'] ) && isset( $request['sticky'] ) ) { + // if ( ! empty( $request['sticky'] ) ) { + // stick_post( $post_id ); + // } else { + // unstick_post( $post_id ); + // } + // } + + // $terms_update = $this->handle_terms( $post->ID, $request ); + // if ( is_wp_error( $terms_update ) ) { + // return $terms_update; + // } + + $post = get_post( $post_id ); + $this->update_additional_fields_for_object( $post, $request ); + + // Update meta fields. + $meta_fields = $this->update_post_meta_fields( $post, $request ); + if ( is_wp_error( $meta_fields ) ) { + return $meta_fields; + } + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param object $post Inserted object (not a WP_Post object). + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + return rest_ensure_response( $response ); + } + + /** + * Update post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + return true; + } + /** * Get a collection of posts. * diff --git a/includes/api/wc-rest-coupons-controller.php b/includes/api/wc-rest-coupons-controller.php index 5ecabc0942f..1eab89d3ef4 100644 --- a/includes/api/wc-rest-coupons-controller.php +++ b/includes/api/wc-rest-coupons-controller.php @@ -47,6 +47,13 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => $this->get_collection_params(), ), + 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 ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), ) ); register_rest_route( WC_API::REST_API_NAMESPACE, '/' . $this->rest_base . '/(?P[\d]+)', array( @@ -58,6 +65,12 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { 'context' => $this->get_context_param( array( 'default' => 'view' ) ), ), ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), @@ -69,6 +82,7 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { ), ), ), + 'schema' => array( $this, 'get_public_item_schema' ), ) ); } @@ -131,4 +145,360 @@ class WC_REST_Coupons_Controller extends WC_REST_Posts_Controller { */ return apply_filters( 'woocommerce_rest_prepare_' . $this->post_type, $response, $post, $request ); } + + /** + * Prepare a single coupon for create or update. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|stdClass $data Post object. + */ + protected function prepare_item_for_database( $request ) { + global $wpdb; + + $data = new stdClass; + + // ID. + if ( isset( $request['id'] ) ) { + $data->ID = absint( $request['id'] ); + } + + $schema = $this->get_item_schema(); + + // Validate required POST fields. + if ( 'POST' === $request->get_method() && empty( $data->ID ) ) { + if ( empty( $request['code'] ) ) { + return new WP_Error( 'woocommerce_rest_missing_parameter', sprintf( __( 'Missing parameter %s.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); + } + } + + // Code. + if ( ! empty( $schema['properties']['code'] ) && ! empty( $request['code'] ) ) { + $coupon_code = apply_filters( 'woocommerce_coupon_code', $request['code'] ); + $id = isset( $data->ID ) ? $data->ID : 0; + + // Check for duplicate coupon codes. + $coupon_found = $wpdb->get_var( $wpdb->prepare( " + SELECT $wpdb->posts.ID + FROM $wpdb->posts + WHERE $wpdb->posts.post_type = 'shop_coupon' + AND $wpdb->posts.post_status = 'publish' + AND $wpdb->posts.post_title = '%s' + AND $wpdb->posts.ID != %s + ", $coupon_code, $id ) ); + + if ( $coupon_found ) { + return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $data->post_title = $coupon_code; + } + + // Content. + $data->post_content = ''; + + // Excerpt. + if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['description'] ) ) { + $data->post_excerpt = wp_filter_post_kses( $request['description'] ); + } + + // Post type. + $data->post_type = $this->post_type; + + // Post status. + $data->post_status = 'publish'; + + // Comment status. + $data->comment_status = 'closed'; + + // Ping status. + $data->ping_status = 'closed'; + + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for insertion. + * + * @param stdClass $data An object representing a single item prepared + * for inserting or updating the database. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request ); + } + + /** + * Expiry date format. + * + * @param string $expiry_date + * @return string + */ + protected function get_coupon_expiry_date( $expiry_date ) { + if ( '' != $expiry_date ) { + return date( 'Y-m-d', strtotime( $expiry_date ) ); + } + + return ''; + } + + /** + * Add post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + $data = $request->get_json_params(); + + $defaults = array( + 'type' => 'fixed_cart', + 'amount' => 0, + 'individual_use' => false, + 'product_ids' => array(), + 'exclude_product_ids' => array(), + 'usage_limit' => '', + 'usage_limit_per_user' => '', + 'limit_usage_to_x_items' => '', + 'usage_count' => '', + 'expiry_date' => '', + 'enable_free_shipping' => false, + 'product_category_ids' => array(), + 'exclude_product_category_ids' => array(), + 'exclude_sale_items' => false, + 'minimum_amount' => '', + 'maximum_amount' => '', + 'customer_emails' => array(), + 'description' => '' + ); + + $data = wp_parse_args( $data, $defaults ); + + // Set coupon meta. + update_post_meta( $post->ID, 'discount_type', $data['type'] ); + update_post_meta( $post->ID, 'coupon_amount', wc_format_decimal( $data['amount'] ) ); + update_post_meta( $post->ID, 'individual_use', ( true === $data['individual_use'] ) ? 'yes' : 'no' ); + update_post_meta( $post->ID, 'product_ids', implode( ',', array_filter( array_map( 'intval', $data['product_ids'] ) ) ) ); + update_post_meta( $post->ID, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $data['exclude_product_ids'] ) ) ) ); + update_post_meta( $post->ID, 'usage_limit', absint( $data['usage_limit'] ) ); + update_post_meta( $post->ID, 'usage_limit_per_user', absint( $data['usage_limit_per_user'] ) ); + update_post_meta( $post->ID, 'limit_usage_to_x_items', absint( $data['limit_usage_to_x_items'] ) ); + update_post_meta( $post->ID, 'usage_count', absint( $data['usage_count'] ) ); + update_post_meta( $post->ID, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $data['expiry_date'] ) ) ); + update_post_meta( $post->ID, 'free_shipping', ( true === $data['enable_free_shipping'] ) ? 'yes' : 'no' ); + update_post_meta( $post->ID, 'product_categories', array_filter( array_map( 'intval', $data['product_category_ids'] ) ) ); + update_post_meta( $post->ID, 'exclude_product_categories', array_filter( array_map( 'intval', $data['exclude_product_category_ids'] ) ) ); + update_post_meta( $post->ID, 'exclude_sale_items', ( true === $data['exclude_sale_items'] ) ? 'yes' : 'no' ); + update_post_meta( $post->ID, 'minimum_amount', wc_format_decimal( $data['minimum_amount'] ) ); + update_post_meta( $post->ID, 'maximum_amount', wc_format_decimal( $data['maximum_amount'] ) ); + update_post_meta( $post->ID, 'customer_email', array_filter( array_map( 'sanitize_email', $data['customer_emails'] ) ) ); + + return true; + } + + /** + * Update post meta fields. + * + * @param WP_Post $post + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + if ( isset( $request['amount'] ) ) { + update_post_meta( $post->ID, 'coupon_amount', wc_format_decimal( $request['amount'] ) ); + } + + if ( isset( $request['individual_use'] ) ) { + update_post_meta( $post->ID, 'individual_use', ( true === $request['individual_use'] ) ? 'yes' : 'no' ); + } + + if ( isset( $request['product_ids'] ) ) { + update_post_meta( $post->ID, 'product_ids', implode( ',', array_filter( array_map( 'intval', $request['product_ids'] ) ) ) ); + } + + if ( isset( $request['exclude_product_ids'] ) ) { + update_post_meta( $post->ID, 'exclude_product_ids', implode( ',', array_filter( array_map( 'intval', $request['exclude_product_ids'] ) ) ) ); + } + + if ( isset( $request['usage_limit'] ) ) { + update_post_meta( $post->ID, 'usage_limit', absint( $request['usage_limit'] ) ); + } + + if ( isset( $request['usage_limit_per_user'] ) ) { + update_post_meta( $post->ID, 'usage_limit_per_user', absint( $request['usage_limit_per_user'] ) ); + } + + if ( isset( $request['limit_usage_to_x_items'] ) ) { + update_post_meta( $post->ID, 'limit_usage_to_x_items', absint( $request['limit_usage_to_x_items'] ) ); + } + + if ( isset( $request['usage_count'] ) ) { + update_post_meta( $post->ID, 'usage_count', absint( $request['usage_count'] ) ); + } + + if ( isset( $request['expiry_date'] ) ) { + update_post_meta( $post->ID, 'expiry_date', $this->get_coupon_expiry_date( wc_clean( $request['expiry_date'] ) ) ); + } + + if ( isset( $request['enable_free_shipping'] ) ) { + update_post_meta( $post->ID, 'free_shipping', ( true === $request['enable_free_shipping'] ) ? 'yes' : 'no' ); + } + + if ( isset( $request['product_category_ids'] ) ) { + update_post_meta( $post->ID, 'product_categories', array_filter( array_map( 'intval', $request['product_category_ids'] ) ) ); + } + + if ( isset( $request['exclude_product_category_ids'] ) ) { + update_post_meta( $post->ID, 'exclude_product_categories', array_filter( array_map( 'intval', $request['exclude_product_category_ids'] ) ) ); + } + + if ( isset( $request['exclude_sale_items'] ) ) { + update_post_meta( $post->ID, 'exclude_sale_items', ( true === $request['exclude_sale_items'] ) ? 'yes' : 'no' ); + } + + if ( isset( $request['minimum_amount'] ) ) { + update_post_meta( $post->ID, 'minimum_amount', wc_format_decimal( $request['minimum_amount'] ) ); + } + + if ( isset( $request['maximum_amount'] ) ) { + update_post_meta( $post->ID, 'maximum_amount', wc_format_decimal( $request['maximum_amount'] ) ); + } + + if ( isset( $request['customer_emails'] ) ) { + update_post_meta( $post->ID, 'customer_email', array_filter( array_map( 'sanitize_email', $request['customer_emails'] ) ) ); + } + } + + /** + * Get the Post's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the object.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'required' => true, + ), + 'type' => array( + 'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_coupon_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'created_at' => array( + 'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'updated_at' => array( + 'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'amount' => array( + 'description' => __( 'The amount of discount.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + ), + 'individual_use' => array( + 'description' => __( 'Whether coupon can only be used individually.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'product_ids' => array( + 'description' => __( "List of product ID's the coupon can be used on.", 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'exclude_product_ids' => array( + 'description' => __( "List of product ID's the coupon cannot be used on.", 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit' => array( + 'description' => __( 'How many times the coupon can be used.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit_per_user' => array( + 'description' => __( 'How many times the coupon can be user per customer.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'limit_usage_to_x_items' => array( + 'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'usage_count' => array( + 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'expiry_date' => array( + 'description' => __( 'UTC DateTime when the coupon expires.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'enable_free_shipping' => array( + 'description' => __( 'Define if can be applied for free shipping.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'product_category_ids' => array( + 'description' => __( "List of category ID's the coupon applies to.", 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'exclude_product_category_ids' => array( + 'description' => __( "List of category ID's the coupon does not apply to.", 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'exclude_sale_items' => array( + 'description' => __( 'Define if should not apply when have sale items.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'minimum_amount' => array( + 'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + ), + 'maximum_amount' => array( + 'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ), + 'type' => 'float', + 'context' => array( 'view', 'edit' ), + ), + 'customer_emails' => array( + 'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Coupon description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema );; + } }