add( array( 'name' => 'woo_coupons', 'group' => 'woocommerce', 'title' => 'List coupons', 'description' => 'Coupons with code, discount, usage, limit, expiry and status. Filter with search (part of the code).', 'cap' => 'edit_shop_coupons', 'input' => array( 'properties' => array( 'search' => array( 'type' => 'string', 'maxLength' => 100 ), 'limit' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 200, 'default' => 50 ), ), ), 'run' => static function ( array $a ) { $args = array( 'post_type' => 'shop_coupon', 'post_status' => array( 'publish', 'draft', 'pending', 'future' ), 'posts_per_page' => (int) $a['limit'], 'orderby' => 'date', 'order' => 'DESC' ); if ( ! empty( $a['search'] ) ) { $args['s'] = sanitize_text_field( $a['search'] ); } $out = array(); foreach ( get_posts( $args ) as $p ) { $out[] = self::row( new \WC_Coupon( $p->ID ), $p->post_status ); } return array( 'coupons' => $out, 'count' => count( $out ) ); }, ) ); $props = array( 'id' => array( 'type' => 'integer', 'minimum' => 1, 'description' => 'Coupon to change. Leave out to create one.' ), 'code' => array( 'type' => 'string', 'minLength' => 1, 'maxLength' => 100 ), 'discount_type' => array( 'type' => 'string', 'enum' => $full ? array( 'percent', 'fixed_cart', 'fixed_product' ) : array( 'percent', 'fixed_cart' ) ), 'amount' => array( 'type' => 'number', 'minimum' => 0 ), 'expires' => array( 'type' => 'string', 'description' => 'YYYY-MM-DD, or empty to remove the expiry.' ), 'usage_limit' => array( 'type' => 'integer', 'minimum' => 0, 'description' => '0 means no limit.' ), 'usage_limit_per_user' => array( 'type' => 'integer', 'minimum' => 0 ), 'minimum_amount' => array( 'type' => 'number', 'minimum' => 0 ), 'free_shipping' => array( 'type' => 'boolean' ), 'description' => array( 'type' => 'string', 'maxLength' => 500 ), 'status' => array( 'type' => 'string', 'enum' => array( 'publish', 'draft' ) ), ); if ( $full ) { $props += array( 'individual_use' => array( 'type' => 'boolean' ), 'maximum_amount' => array( 'type' => 'number', 'minimum' => 0 ), 'product_ids' => array( 'type' => 'array', 'items' => array( 'type' => 'integer', 'minimum' => 1 ) ), 'excluded_product_ids' => array( 'type' => 'array', 'items' => array( 'type' => 'integer', 'minimum' => 1 ) ), 'product_categories' => array( 'type' => 'array', 'items' => array( 'type' => 'integer', 'minimum' => 1 ) ), 'email_restrictions' => array( 'type' => 'array', 'items' => array( 'type' => 'string', 'maxLength' => 190 ) ), ); } $r->add( array( 'name' => 'woo_save_coupon', 'group' => 'woocommerce', 'title' => 'Create or update a coupon', 'description' => 'Create a coupon (code, discount_type, amount) or change one by id. New coupons are live unless status is draft. Can be undone.' . ( $full ? '' : ' Product and category rules are part of HandyMCP Pro.' ), 'write' => true, 'scope' => 'commerce:write', 'cap' => 'edit_shop_coupons', 'input' => array( 'properties' => $props ), 'run' => static function ( array $a ) use ( $full ) { return self::save( $a, $full ); }, ) ); } /** * Save a coupon. * * @param array $a Args. * @param bool $full Pro fields. * @return array|\WP_Error */ private static function save( array $a, bool $full ) { $id = (int) ( $a['id'] ?? 0 ); if ( $id ) { $p = get_post( $id ); if ( ! $p || 'shop_coupon' !== $p->post_type ) { return new \WP_Error( 'not_found', 'Coupon not found.' ); } if ( ! current_user_can( 'edit_post', $id ) ) { return new \WP_Error( 'forbidden', 'Your account cannot edit this coupon.' ); } } elseif ( empty( $a['code'] ) ) { return new \WP_Error( 'invalid_arguments', 'A new coupon needs a code.' ); } $c = new \WC_Coupon( $id ); if ( isset( $a['code'] ) ) { $code = wc_format_coupon_code( sanitize_text_field( (string) $a['code'] ) ); if ( '' === $code ) { return new \WP_Error( 'invalid_arguments', 'The coupon code is empty after cleaning. Use letters, numbers or dashes.' ); } $taken = (int) wc_get_coupon_id_by_code( $code, $id ); if ( $taken ) { return new \WP_Error( 'code_exists', "Coupon $code already exists (id $taken). Change that one with its id, or pick another code." ); } } $type = isset( $a['discount_type'] ) ? (string) $a['discount_type'] : ( $id ? $c->get_discount_type() : 'percent' ); if ( ! $full && ! in_array( $type, array( 'percent', 'fixed_cart' ), true ) ) { return new \WP_Error( 'pro_feature', 'Fixed product discounts are part of HandyMCP Pro. Use percent or fixed_cart.' ); } $amount = isset( $a['amount'] ) ? (float) $a['amount'] : (float) $c->get_amount(); if ( 'percent' === $type && $amount > 100 ) { return new \WP_Error( 'invalid_arguments', 'A percent discount cannot be more than 100.' ); } $expires = null; if ( isset( $a['expires'] ) && '' !== $a['expires'] ) { $d = \DateTime::createFromFormat( '!Y-m-d', (string) $a['expires'], wp_timezone() ); if ( ! $d || $d->format( 'Y-m-d' ) !== $a['expires'] ) { return new \WP_Error( 'invalid_arguments', 'expires must be a date like 2026-12-31.' ); } $expires = $d->getTimestamp(); } if ( $id ) { History::post( $id ); } try { if ( isset( $code ) ) { $c->set_code( $code ); } $c->set_discount_type( $type ); $c->set_amount( $amount ); if ( isset( $a['expires'] ) ) { $c->set_date_expires( $expires ); } $plain = array( 'usage_limit' => 'set_usage_limit', 'usage_limit_per_user' => 'set_usage_limit_per_user', 'minimum_amount' => 'set_minimum_amount', 'free_shipping' => 'set_free_shipping' ); if ( $full ) { $plain += array( 'individual_use' => 'set_individual_use', 'maximum_amount' => 'set_maximum_amount', 'product_ids' => 'set_product_ids', 'excluded_product_ids' => 'set_excluded_product_ids', 'product_categories' => 'set_product_categories' ); } foreach ( $plain as $k => $m ) { if ( isset( $a[ $k ] ) ) { $c->{$m}( is_array( $a[ $k ] ) ? array_map( 'absint', $a[ $k ] ) : $a[ $k ] ); } } if ( $full && isset( $a['email_restrictions'] ) ) { $c->set_email_restrictions( array_values( array_filter( array_map( 'sanitize_email', (array) $a['email_restrictions'] ) ) ) ); } if ( isset( $a['description'] ) ) { $c->set_description( sanitize_textarea_field( (string) $a['description'] ) ); } if ( isset( $a['status'] ) ) { $c->set_status( (string) $a['status'] ); } $c->save(); } catch ( \Throwable $e ) { return new \WP_Error( 'save_failed', 'WooCommerce could not save the coupon: ' . $e->getMessage() ); } if ( ! $id ) { History::created_post( $c->get_id() ); } return array( 'coupon' => self::row( $c, (string) get_post_status( $c->get_id() ) ), 'message' => $id ? 'Coupon updated.' : 'Coupon created.' ); } /** * Coupon summary. * * @param \WC_Coupon $c Coupon. * @param string $status Post status. * @return array */ private static function row( \WC_Coupon $c, string $status ): array { return array( 'id' => $c->get_id(), 'code' => $c->get_code(), 'type' => $c->get_discount_type(), 'amount' => (float) $c->get_amount(), 'minimum_amount' => (float) $c->get_minimum_amount(), 'free_shipping' => $c->get_free_shipping(), 'usage' => $c->get_usage_count(), 'limit' => $c->get_usage_limit() ? $c->get_usage_limit() : null, 'expires' => $c->get_date_expires() ? $c->get_date_expires()->date( 'Y-m-d' ) : null, 'status' => $status, ); } }