'POST', 'callback' => [ $this, 'sync_products' ], 'permission_callback' => [ $this, 'check_permission' ], ] ); // Get visibility state for given product IDs (from Platform API lookup). register_rest_route( self::NAMESPACE, '/products/visibility', [ 'methods' => 'GET', 'callback' => [ $this, 'get_visibility' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'product_ids' => [ 'required' => true, 'type' => 'string', 'description' => 'Comma-separated product IDs', ], ], ] ); // Enable or disable visibility for one or more products. register_rest_route( self::NAMESPACE, '/products/visibility', [ 'methods' => 'POST', 'callback' => [ $this, 'set_visibility' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'product_ids' => [ 'required' => true, 'type' => 'array', 'items' => [ 'type' => 'integer' ], 'description' => 'WC product IDs', ], 'enabled' => [ 'required' => true, 'type' => 'boolean', 'description' => 'Enable (true) or disable (false) AI visibility', ], ], ] ); // Get the stored MC4P certificate JSON for a single product. register_rest_route( self::NAMESPACE, '/products/(?P\d+)/snapshot', [ 'methods' => 'GET', 'callback' => [ $this, 'get_snapshot' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'id' => [ 'required' => true, 'type' => 'integer', 'validate_callback' => static fn ( $value ) => is_numeric( $value ), ], ], ] ); } /** * POST /mintall/v1/products/sync * * Bulk upsert all published WooCommerce products to the Platform API. */ public function sync_products( $request ) { $api_key = $this->get_api_key(); if ( is_wp_error( $api_key ) ) { return $api_key; } $product_ids = wc_get_products( [ 'status' => 'publish', 'limit' => -1, 'return' => 'ids', ] ); if ( empty( $product_ids ) ) { return new \WP_REST_Response( [ 'synced' => 0, 'message' => 'No published products.', ] ); } $chunks = array_chunk( $product_ids, self::BULK_CHUNK ); $total = 0; $errors = []; foreach ( $chunks as $ids ) { $items = []; foreach ( $ids as $id ) { $product = wc_get_product( $id ); if ( ! $product ) { continue; } $items[] = [ 'external_product_id' => (string) $id, 'product_metadata' => Mintall_Products::build_product_metadata( $product ), ]; } if ( empty( $items ) ) { continue; } $result = Mintall_Products::bulk_upsert( $api_key, $items ); if ( is_wp_error( $result ) ) { $errors[] = $result->get_error_message(); continue; } $total += count( $items ); } return new \WP_REST_Response( [ 'synced' => $total, 'errors' => $errors, ] ); } /** * GET /mintall/v1/products/visibility?product_ids=1,2,3 * * Returns visibility state from Platform API for given product IDs. */ public function get_visibility( $request ) { $api_key = $this->get_api_key(); if ( is_wp_error( $api_key ) ) { return $api_key; } $ids_param = $request->get_param( 'product_ids' ); $product_ids = array_filter( array_map( 'absint', explode( ',', $ids_param ) ) ); if ( empty( $product_ids ) ) { return new \WP_REST_Response( [] ); } $external_ids = array_map( 'strval', $product_ids ); $result = Mintall_Products::lookup( $api_key, $external_ids ); if ( is_wp_error( $result ) ) { $data = $result->get_error_data(); $status = is_array( $data ) && isset( $data['status'] ) ? $data['status'] : 500; return $this->error_response( $result->get_error_message(), $status ); } $product_data = []; foreach ( $result as $row ) { $ext_id = $row['external_product_id'] ?? null; if ( null === $ext_id ) { continue; } $readiness = $row['readiness'] ?? null; $is_visible = ! empty( $row['visible_at'] ); $product_data[ (int) $ext_id ] = [ 'visible' => $is_visible, 'readiness_score' => $readiness['score'] ?? 0, 'primary_issue' => $readiness['primary_issue'] ?? null, 'primary_issue_label' => $readiness['primary_issue_label'] ?? null, 'mintall_product_id' => $row['id'] ?? null, ]; // Self-heal: re-sync on a proof mismatch (toggled out-of-band via // the Mintall web app), or when a previously-synced product's // document has gone missing/unreadable — never for a product // that was simply never enabled. Reuses this lookup row. $existing_snapshot = get_post_meta( (int) $ext_id, '_mintall_snapshot', true ); $decoded = '' !== (string) $existing_snapshot ? json_decode( $existing_snapshot, true ) : null; $readable = is_array( $decoded ); $has_proof = $readable && isset( $decoded['proof'] ); $was_synced = '' !== (string) get_post_meta( (int) $ext_id, '_mintall_product_id', true ); $needs_resync = $is_visible ? ! $has_proof : ( $has_proof || ( ! $readable && $was_synced ) ); if ( $needs_resync ) { Mintall_MC4P::sync_product_certificate( $api_key, (int) $ext_id, $row ); } } // Ensure all requested ids have a key. foreach ( $product_ids as $id ) { if ( ! array_key_exists( $id, $product_data ) ) { $product_data[ $id ] = [ 'visible' => false, 'readiness_score' => 0, 'primary_issue' => null, 'primary_issue_label' => null, 'mintall_product_id' => null, ]; } } return new \WP_REST_Response( $product_data ); } /** * POST /mintall/v1/products/visibility * * Enable or disable AI visibility for given product IDs. */ public function set_visibility( $request ) { $api_key = $this->get_api_key(); if ( is_wp_error( $api_key ) ) { return $api_key; } $product_ids = array_map( 'absint', $request->get_param( 'product_ids' ) ); $product_ids = array_filter( $product_ids ); $enabled = (bool) $request->get_param( 'enabled' ); if ( empty( $product_ids ) ) { return $this->error_response( 'product_ids required.', 422 ); } $external_ids = array_map( 'strval', $product_ids ); if ( count( $external_ids ) === 1 ) { $id = $external_ids[0]; $result = $enabled ? Mintall_Products::enable_visibility( $api_key, $id ) : Mintall_Products::disable_visibility( $api_key, $id ); if ( is_wp_error( $result ) ) { $data = $result->get_error_data(); $status = is_array( $data ) && isset( $data['status'] ) ? $data['status'] : 500; return $this->error_response( $result->get_error_message(), $status ); } return new \WP_REST_Response( [ 'success' => true, 'enabled' => $enabled, 'count' => 1, ] ); } if ( ! $enabled ) { $result = Mintall_Products::bulk_disable_visibility( $api_key, $external_ids ); if ( is_wp_error( $result ) ) { $data = $result->get_error_data(); $status = is_array( $data ) && isset( $data['status'] ) ? $data['status'] : 500; return $this->error_response( $result->get_error_message(), $status ); } return new \WP_REST_Response( [ 'success' => true, 'enabled' => false, 'count' => count( $product_ids ), ] ); } // Bulk enable: the backend rejects the whole batch if it doesn't fit // the plan's remaining capacity, so this falls back to enabling as // many as fit rather than surfacing an all-or-nothing error - see // Mintall_Products::bulk_enable_visibility_with_fallback(). $bulk = Mintall_Products::bulk_enable_visibility_with_fallback( $api_key, $external_ids ); if ( is_wp_error( $bulk ) ) { $data = $bulk->get_error_data(); $status = is_array( $data ) && isset( $data['status'] ) ? $data['status'] : 500; return $this->error_response( $bulk->get_error_message(), $status ); } return new \WP_REST_Response( [ 'success' => true, 'enabled' => true, 'count' => count( $bulk['enabled_ids'] ), 'enabled_ids' => array_map( 'absint', $bulk['enabled_ids'] ), 'failed_count' => $bulk['failed_count'], ] ); } /** * GET /mintall/v1/products/{id}/snapshot * * Returns the stored MC4P certificate JSON for a single product. */ public function get_snapshot( $request ) { $product_id = (int) $request->get_param( 'id' ); $snapshot = get_post_meta( $product_id, '_mintall_snapshot', true ); $decoded = '' !== (string) $snapshot ? json_decode( $snapshot, true ) : null; if ( ! is_array( $decoded ) ) { return $this->error_response( 'No certificate data for this product.', 404 ); } return new \WP_REST_Response( $decoded ); } }