'POST', 'callback' => [ $this, 'mint' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'product_id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', ], ], ] ); register_rest_route( self::NAMESPACE, '/mint/bulk', [ 'methods' => 'POST', 'callback' => [ $this, 'mint_bulk' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'product_ids' => [ 'required' => true, 'type' => 'array', 'items' => [ 'type' => 'integer' ], ], ], ] ); register_rest_route( self::NAMESPACE, '/mint/status', [ 'methods' => 'GET', 'callback' => [ $this, 'get_status' ], 'permission_callback' => [ $this, 'check_permission' ], 'args' => [ 'product_id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', ], ], ] ); } /** * POST /mintall/v1/mint * * Enqueues a MANUAL protection job for every image the product protects — * its featured image, gallery, and any images embedded in the description. * The backend queue owns the actual certify/replace work and drives it via * signed callbacks; this just hands off the request. */ public function mint( $request ) { $api_key = $this->get_api_key(); if ( is_wp_error( $api_key ) ) { return $api_key; } $product_id = $request->get_param( 'product_id' ); $product = wc_get_product( $product_id ); if ( ! $product ) { return $this->error_response( 'Product not found.', 404 ); } $file_ids = Product_Images::collect_file_ids( $product ); if ( empty( $file_ids ) ) { return $this->error_response( 'Product has no images to protect.', 400 ); } // Keep the platform's product record populated. Best-effort: the backend // otherwise lazily creates a bare product row (null metadata) when the // first protection job completes. Mintall_Products::sync_metadata( $api_key, $product ); $result = Mintall_Protection::enqueue( $api_key, 'MANUAL', [ [ 'external_product_id' => (string) $product_id, 'external_file_ids' => $file_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 ); } // Auto-enable AI visibility on protect if setting is on. if ( get_option( 'mintall_keep_visibility_on_protect', false ) ) { Mintall_Products::enable_visibility( $api_key, (string) $product_id ); } return new \WP_REST_Response( $result ); } /** * POST /mintall/v1/mint/bulk * * Enqueues MANUAL protection jobs for many products in one batched request, * rather than looping the single-item /mint endpoint client-side. Products * that can't be protected (missing, or no images) are skipped and reported * back rather than failing the whole request; only a failure of the batched * enqueue call itself (e.g. REAUTH_REQUIRED) errors the request. */ public function mint_bulk( $request ) { $api_key = $this->get_api_key(); if ( is_wp_error( $api_key ) ) { return $api_key; } $product_ids = $request->get_param( 'product_ids' ); $to_enqueue = []; $skipped = []; foreach ( $product_ids as $product_id ) { $product = wc_get_product( $product_id ); if ( ! $product ) { $skipped[] = [ 'product_id' => $product_id, 'reason' => 'not_found', ]; continue; } $file_ids = Product_Images::collect_file_ids( $product ); if ( empty( $file_ids ) ) { $skipped[] = [ 'product_id' => $product_id, 'reason' => 'no_images', ]; continue; } // Best-effort, same as the single-item /mint route. Mintall_Products::sync_metadata( $api_key, $product ); $to_enqueue[] = [ 'external_product_id' => (string) $product_id, 'external_file_ids' => $file_ids, ]; } if ( empty( $to_enqueue ) ) { return new \WP_REST_Response( [ 'requested' => $product_ids, 'enqueued' => [], 'skipped' => $skipped, ] ); } $result = Mintall_Protection::enqueue( $api_key, 'MANUAL', $to_enqueue ); 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 ); } $enqueued_ids = array_map( static fn ( $p ) => (int) $p['external_product_id'], $to_enqueue ); // Auto-enable AI visibility on protect if setting is on — one batched // call for the whole request, mirroring the single-item route's intent. if ( get_option( 'mintall_keep_visibility_on_protect', false ) ) { Mintall_Products::bulk_enable_visibility( $api_key, array_column( $to_enqueue, 'external_product_id' ) ); } return new \WP_REST_Response( [ 'requested' => $product_ids, 'enqueued' => $enqueued_ids, 'skipped' => $skipped, ] ); } /** * GET /mintall/v1/mint/status?product_id=123 * * Reads protection state from the backend queue rather than local postmeta. * Returns one of UNPROTECTED / IN_PROGRESS / PROTECTED / NEEDS_ATTENTION * plus the job counts. Falls back to UNPROTECTED on any error so the * metabox renders a sane default instead of breaking. */ public function get_status( $request ) { $unprotected = [ 'status' => 'UNPROTECTED', 'counts' => null, ]; $api_key = $this->get_api_key(); if ( is_wp_error( $api_key ) ) { return new \WP_REST_Response( $unprotected ); } $product_id = $request->get_param( 'product_id' ); $result = Mintall_Protection::status( $api_key, [ (string) $product_id ] ); if ( is_wp_error( $result ) ) { return new \WP_REST_Response( $unprotected ); } $products = isset( $result['products'] ) && is_array( $result['products'] ) ? $result['products'] : []; foreach ( $products as $entry ) { if ( (string) ( $entry['external_product_id'] ?? '' ) === (string) $product_id ) { return new \WP_REST_Response( [ 'status' => $entry['status'] ?? 'UNPROTECTED', 'counts' => $entry['counts'] ?? null, ] ); } } return new \WP_REST_Response( $unprotected ); } }