feat: Coupons CRUD - Backend API (Phase 1)

Implemented CouponsController with full CRUD operations

Created: CouponsController.php
- GET /coupons - List coupons with pagination and filtering
- GET /coupons/{id} - Get single coupon
- POST /coupons - Create new coupon
- PUT /coupons/{id} - Update coupon
- DELETE /coupons/{id} - Delete coupon

Features:
- Pagination support (page, per_page)
- Search by coupon code
- Filter by discount_type
- Full coupon data (all WooCommerce fields)
- Validation (code required, duplicate check)
- Error handling (user-friendly messages)

Coupon Fields Supported:
- Basic: code, amount, discount_type, description
- Usage: usage_count, usage_limit, usage_limit_per_user
- Restrictions: product_ids, categories, email_restrictions
- Limits: minimum_amount, maximum_amount, date_expires
- Options: individual_use, free_shipping, exclude_sale_items

Registered in Routes.php:
- Added CouponsController to route registration
- Follows API_ROUTES.md standards

Following PROJECT_SOP.md:
- Consistent error responses
- Permission checks (manage_woocommerce)
- User-friendly error messages
- Standard REST patterns

Next Steps:
- Frontend list page with submenu tabs
- Frontend create/edit form
- Update API_ROUTES.md
- Update NavigationRegistry.php
This commit is contained in:
dwindown
2025-11-20 13:52:12 +07:00
parent afb54b962e
commit 249505ddf3
2 changed files with 347 additions and 0 deletions

View File

@@ -0,0 +1,343 @@
<?php
namespace WooNooW\Api;
use WP_REST_Request;
use WP_REST_Response;
use WC_Coupon;
/**
* CouponsController
*
* Handles coupon CRUD operations via REST API
*
* @package WooNooW\Api
*/
class CouponsController {
/**
* Register REST API routes
*/
public static function register_routes() {
// List coupons
register_rest_route('woonoow/v1', '/coupons', [
'methods' => 'GET',
'callback' => [__CLASS__, 'list_coupons'],
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
]);
// Get single coupon
register_rest_route('woonoow/v1', '/coupons/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_coupon'],
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
]);
// Create coupon
register_rest_route('woonoow/v1', '/coupons', [
'methods' => 'POST',
'callback' => [__CLASS__, 'create_coupon'],
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
]);
// Update coupon
register_rest_route('woonoow/v1', '/coupons/(?P<id>\d+)', [
'methods' => 'PUT',
'callback' => [__CLASS__, 'update_coupon'],
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
]);
// Delete coupon
register_rest_route('woonoow/v1', '/coupons/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => [__CLASS__, 'delete_coupon'],
'permission_callback' => function () { return current_user_can('manage_woocommerce'); },
]);
}
/**
* GET /woonoow/v1/coupons
* List all coupons with pagination and filtering
*/
public static function list_coupons(WP_REST_Request $request): WP_REST_Response {
$page = max(1, (int) $request->get_param('page') ?: 1);
$per_page = max(1, min(100, (int) $request->get_param('per_page') ?: 20));
$search = sanitize_text_field($request->get_param('search') ?: '');
$discount_type = sanitize_text_field($request->get_param('discount_type') ?: '');
$args = [
'post_type' => 'shop_coupon',
'post_status' => 'publish',
'posts_per_page' => $per_page,
'paged' => $page,
'orderby' => 'date',
'order' => 'DESC',
];
// Search by code
if ($search) {
$args['s'] = $search;
}
// Filter by discount type
if ($discount_type) {
$args['meta_query'] = [
[
'key' => 'discount_type',
'value' => $discount_type,
'compare' => '=',
],
];
}
$query = new \WP_Query($args);
$coupons = [];
foreach ($query->posts as $post) {
$coupon = new WC_Coupon($post->ID);
$coupons[] = self::format_coupon($coupon);
}
return new WP_REST_Response([
'coupons' => $coupons,
'total' => $query->found_posts,
'page' => $page,
'per_page' => $per_page,
'total_pages' => $query->max_num_pages,
], 200);
}
/**
* GET /woonoow/v1/coupons/{id}
* Get single coupon
*/
public static function get_coupon(WP_REST_Request $request): WP_REST_Response {
$id = (int) $request->get_param('id');
$coupon = new WC_Coupon($id);
if (!$coupon->get_id()) {
return new WP_REST_Response([
'error' => 'not_found',
'message' => __('Coupon not found', 'woonoow'),
], 404);
}
return new WP_REST_Response(self::format_coupon($coupon, true), 200);
}
/**
* POST /woonoow/v1/coupons
* Create new coupon
*/
public static function create_coupon(WP_REST_Request $request): WP_REST_Response {
$data = $request->get_json_params();
// Validate required fields
if (empty($data['code'])) {
return new WP_REST_Response([
'error' => 'missing_code',
'message' => __('Coupon code is required', 'woonoow'),
], 400);
}
// Check if code already exists
$existing = get_page_by_title($data['code'], OBJECT, 'shop_coupon');
if ($existing) {
return new WP_REST_Response([
'error' => 'code_exists',
'message' => __('Coupon code already exists', 'woonoow'),
], 400);
}
try {
$coupon = new WC_Coupon();
self::update_coupon_data($coupon, $data);
$coupon->save();
return new WP_REST_Response(self::format_coupon($coupon, true), 201);
} catch (\Exception $e) {
return new WP_REST_Response([
'error' => 'create_failed',
'message' => __('Failed to create coupon', 'woonoow'),
], 500);
}
}
/**
* PUT /woonoow/v1/coupons/{id}
* Update coupon
*/
public static function update_coupon(WP_REST_Request $request): WP_REST_Response {
$id = (int) $request->get_param('id');
$data = $request->get_json_params();
$coupon = new WC_Coupon($id);
if (!$coupon->get_id()) {
return new WP_REST_Response([
'error' => 'not_found',
'message' => __('Coupon not found', 'woonoow'),
], 404);
}
try {
self::update_coupon_data($coupon, $data);
$coupon->save();
return new WP_REST_Response(self::format_coupon($coupon, true), 200);
} catch (\Exception $e) {
return new WP_REST_Response([
'error' => 'update_failed',
'message' => __('Failed to update coupon', 'woonoow'),
], 500);
}
}
/**
* DELETE /woonoow/v1/coupons/{id}
* Delete coupon
*/
public static function delete_coupon(WP_REST_Request $request): WP_REST_Response {
$id = (int) $request->get_param('id');
$force = $request->get_param('force') === 'true';
$coupon = new WC_Coupon($id);
if (!$coupon->get_id()) {
return new WP_REST_Response([
'error' => 'not_found',
'message' => __('Coupon not found', 'woonoow'),
], 404);
}
$result = wp_delete_post($id, $force);
if (!$result) {
return new WP_REST_Response([
'error' => 'delete_failed',
'message' => __('Failed to delete coupon', 'woonoow'),
], 500);
}
return new WP_REST_Response([
'success' => true,
'id' => $id,
], 200);
}
/**
* Format coupon for API response
*
* @param WC_Coupon $coupon
* @param bool $full Include full details
* @return array
*/
private static function format_coupon(WC_Coupon $coupon, bool $full = false): array {
$data = [
'id' => $coupon->get_id(),
'code' => $coupon->get_code(),
'amount' => (float) $coupon->get_amount(),
'discount_type' => $coupon->get_discount_type(),
'description' => $coupon->get_description(),
'usage_count' => $coupon->get_usage_count(),
'usage_limit' => $coupon->get_usage_limit() ?: null,
'date_expires' => $coupon->get_date_expires() ? $coupon->get_date_expires()->date('Y-m-d') : null,
];
if ($full) {
$data = array_merge($data, [
'individual_use' => $coupon->get_individual_use(),
'product_ids' => $coupon->get_product_ids(),
'excluded_product_ids' => $coupon->get_excluded_product_ids(),
'usage_limit_per_user' => $coupon->get_usage_limit_per_user() ?: null,
'limit_usage_to_x_items' => $coupon->get_limit_usage_to_x_items() ?: null,
'free_shipping' => $coupon->get_free_shipping(),
'product_categories' => $coupon->get_product_categories(),
'excluded_product_categories' => $coupon->get_excluded_product_categories(),
'exclude_sale_items' => $coupon->get_exclude_sale_items(),
'minimum_amount' => $coupon->get_minimum_amount() ? (float) $coupon->get_minimum_amount() : null,
'maximum_amount' => $coupon->get_maximum_amount() ? (float) $coupon->get_maximum_amount() : null,
'email_restrictions' => $coupon->get_email_restrictions(),
]);
}
return $data;
}
/**
* Update coupon data from request
*
* @param WC_Coupon $coupon
* @param array $data
*/
private static function update_coupon_data(WC_Coupon $coupon, array $data) {
if (isset($data['code'])) {
$coupon->set_code(sanitize_text_field($data['code']));
}
if (isset($data['amount'])) {
$coupon->set_amount((float) $data['amount']);
}
if (isset($data['discount_type'])) {
$coupon->set_discount_type(sanitize_text_field($data['discount_type']));
}
if (isset($data['description'])) {
$coupon->set_description(wp_kses_post($data['description']));
}
if (isset($data['date_expires'])) {
$coupon->set_date_expires($data['date_expires'] ? $data['date_expires'] : null);
}
if (isset($data['individual_use'])) {
$coupon->set_individual_use((bool) $data['individual_use']);
}
if (isset($data['product_ids'])) {
$coupon->set_product_ids(array_map('intval', (array) $data['product_ids']));
}
if (isset($data['excluded_product_ids'])) {
$coupon->set_excluded_product_ids(array_map('intval', (array) $data['excluded_product_ids']));
}
if (isset($data['usage_limit'])) {
$coupon->set_usage_limit($data['usage_limit'] ? (int) $data['usage_limit'] : null);
}
if (isset($data['usage_limit_per_user'])) {
$coupon->set_usage_limit_per_user($data['usage_limit_per_user'] ? (int) $data['usage_limit_per_user'] : null);
}
if (isset($data['limit_usage_to_x_items'])) {
$coupon->set_limit_usage_to_x_items($data['limit_usage_to_x_items'] ? (int) $data['limit_usage_to_x_items'] : null);
}
if (isset($data['free_shipping'])) {
$coupon->set_free_shipping((bool) $data['free_shipping']);
}
if (isset($data['product_categories'])) {
$coupon->set_product_categories(array_map('intval', (array) $data['product_categories']));
}
if (isset($data['excluded_product_categories'])) {
$coupon->set_excluded_product_categories(array_map('intval', (array) $data['excluded_product_categories']));
}
if (isset($data['exclude_sale_items'])) {
$coupon->set_exclude_sale_items((bool) $data['exclude_sale_items']);
}
if (isset($data['minimum_amount'])) {
$coupon->set_minimum_amount($data['minimum_amount'] ? (float) $data['minimum_amount'] : '');
}
if (isset($data['maximum_amount'])) {
$coupon->set_maximum_amount($data['maximum_amount'] ? (float) $data['maximum_amount'] : '');
}
if (isset($data['email_restrictions'])) {
$coupon->set_email_restrictions(array_map('sanitize_email', (array) $data['email_restrictions']));
}
}
}

View File

@@ -18,6 +18,7 @@ use WooNooW\Api\SystemController;
use WooNooW\Api\NotificationsController;
use WooNooW\Api\ActivityLogController;
use WooNooW\Api\ProductsController;
use WooNooW\Api\CouponsController;
class Routes {
public static function init() {
@@ -108,6 +109,9 @@ class Routes {
error_log('WooNooW Routes: Registering ProductsController routes');
ProductsController::register_routes();
error_log('WooNooW Routes: ProductsController routes registered');
// Coupons controller
CouponsController::register_routes();
});
}
}