feat: Newsletter system improvements and validation framework

- Fix: Marketing events now display in Staff notifications tab
- Reorganize: Move Coupons to Marketing/Coupons for better organization
- Add: Comprehensive email/phone validation with extensible filter hooks
  - Email validation with regex pattern (xxxx@xxxx.xx)
  - Phone validation with WhatsApp verification support
  - Filter hooks for external API integration (QuickEmailVerification, etc.)
- Fix: Newsletter template routes now use centralized notification email builder
- Add: Validation.php class for reusable validation logic
- Add: VALIDATION_HOOKS.md documentation with integration examples
- Add: NEWSLETTER_CAMPAIGN_PLAN.md architecture for future campaign system
- Fix: API delete method call in Newsletter.tsx (delete -> del)
- Remove: Duplicate EmailTemplates.tsx (using notification system instead)
- Update: Newsletter controller to use centralized Validation class

Breaking changes:
- Coupons routes moved from /routes/Coupons to /routes/Marketing/Coupons
- Legacy /coupons routes maintained for backward compatibility
This commit is contained in:
Dwindi Ramadhana
2025-12-26 10:59:48 +07:00
parent 0b08ddefa1
commit 0b2c8a56d6
23 changed files with 1132 additions and 232 deletions

View File

@@ -4,6 +4,7 @@ namespace WooNooW\API;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use WooNooW\Core\Validation;
class NewsletterController {
const API_NAMESPACE = 'woonoow/v1';
@@ -112,8 +113,11 @@ class NewsletterController {
public static function subscribe(WP_REST_Request $request) {
$email = sanitize_email($request->get_param('email'));
if (!is_email($email)) {
return new WP_Error('invalid_email', 'Invalid email address', ['status' => 400]);
// Use centralized validation with extensible filter hooks
$validation = Validation::validate_email($email, 'newsletter_subscribe');
if (is_wp_error($validation)) {
return $validation;
}
// Get existing subscribers (now stored as objects with metadata)

View File

@@ -68,19 +68,12 @@ class ProductsController {
* Register REST API routes
*/
public static function register_routes() {
error_log('WooNooW ProductsController::register_routes() START');
// List products
$callback = [__CLASS__, 'get_products'];
$is_callable = is_callable($callback);
error_log('WooNooW ProductsController: Callback is_callable: ' . ($is_callable ? 'YES' : 'NO'));
$result = register_rest_route('woonoow/v1', '/products', [
register_rest_route('woonoow/v1', '/products', [
'methods' => 'GET',
'callback' => $callback,
'callback' => [__CLASS__, 'get_products'],
'permission_callback' => [Permissions::class, 'check_admin_permission'],
]);
error_log('WooNooW ProductsController: GET /products registered: ' . ($result ? 'SUCCESS' : 'FAILED'));
// Get single product
register_rest_route('woonoow/v1', '/products/(?P<id>\d+)', [
@@ -136,8 +129,6 @@ class ProductsController {
* Get products list with filters
*/
public static function get_products(WP_REST_Request $request) {
error_log('WooNooW ProductsController::get_products() CALLED - START');
try {
$page = max(1, (int) $request->get_param('page'));
$per_page = min(100, max(1, (int) ($request->get_param('per_page') ?: 20)));
@@ -206,12 +197,7 @@ class ProductsController {
foreach ($query->posts as $post) {
$product = wc_get_product($post->ID);
if ($product) {
$formatted = self::format_product_list_item($product);
// Debug: Log first product to verify structure
if (empty($products)) {
error_log('WooNooW Debug - First product data: ' . print_r($formatted, true));
}
$products[] = $formatted;
$products[] = self::format_product_list_item($product);
}
}
@@ -228,14 +214,10 @@ class ProductsController {
$response->header('Cache-Control', 'no-cache, no-store, must-revalidate');
$response->header('Pragma', 'no-cache');
$response->header('Expires', '0');
$response->header('X-WooNooW-Version', '2.0'); // Debug header
error_log('WooNooW ProductsController::get_products() CALLED - END SUCCESS');
return $response;
} catch (\Exception $e) {
error_log('WooNooW ProductsController::get_products() ERROR: ' . $e->getMessage());
error_log('WooNooW ProductsController::get_products() TRACE: ' . $e->getTraceAsString());
return new WP_Error('products_error', $e->getMessage(), ['status' => 500]);
}
}

View File

@@ -268,17 +268,14 @@ class StoreController extends WP_REST_Controller {
* @return WP_REST_Response|WP_Error Response object or error
*/
public function get_customer_settings(WP_REST_Request $request) {
error_log('WooNooW: get_customer_settings called');
try {
$settings = CustomerSettingsProvider::get_settings();
error_log('WooNooW: Customer settings retrieved: ' . print_r($settings, true));
$response = rest_ensure_response($settings);
$response->header('Cache-Control', 'max-age=60');
return $response;
} catch (\Exception $e) {
error_log('WooNooW: get_customer_settings exception: ' . $e->getMessage());
return new WP_Error(
'get_customer_settings_failed',
$e->getMessage(),