manager = new RunManager( Plugin::instance()->operations() ); } /** * Registers routes. * * @return void */ public function register_routes() { $this->route( '/runs', array( array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create' ), 'permission_callback' => array( Auth::class, 'can_read' ), 'args' => array( 'spec' => array( 'required' => true, 'type' => 'object', ), 'operation' => array( 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'params' => array( 'type' => 'object', 'default' => array(), ), 'suppress_emails' => array( 'type' => 'boolean', 'default' => false, ), ), ), array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'list_runs' ), 'permission_callback' => array( Auth::class, 'can_read' ), 'args' => array( 'page' => $this->int_param( false, 1 ), 'per_page' => array( 'type' => 'integer', 'default' => 20, 'minimum' => 1, 'maximum' => 100, ), 'status' => array( 'type' => 'string' ), 'operation' => array( 'type' => 'string' ), 'relation' => array( 'type' => 'string' ), 'from' => array( 'type' => 'string' ), 'to' => array( 'type' => 'string' ), ), ), ) ); $this->route( '/runs/(?P\d+)', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'get_run' ), 'args' => array( 'id' => $this->int_param( true ) ), ) ); $this->route( '/runs/(?P\d+)/items', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'items' ), 'args' => array( 'id' => $this->int_param( true ), 'page' => $this->int_param( false, 1 ), 'per_page' => array( 'type' => 'integer', 'default' => 25, 'minimum' => 1, 'maximum' => 200, ), 'changed' => array( 'type' => 'boolean', 'default' => false, ), 'status' => array( 'type' => 'string' ), 'subscription_id' => $this->int_param( false, 0 ), ), ) ); $this->route( '/runs/(?P\d+)/cancel', array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $this, 'cancel' ), 'args' => array( 'id' => $this->int_param( true ) ), ) ); $this->route( '/subscriptions/(?P\d+)/history', array( 'methods' => \WP_REST_Server::READABLE, 'callback' => array( $this, 'history' ), 'args' => array( 'id' => $this->int_param( true ) ), ) ); } /** * GET /subscriptions/{id}/history. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response */ public function history( \WP_REST_Request $request ) { return $this->respond( \Recast\WCS\Journal\JournalReader::history( (int) $request['id'] ) ); } /** * Run JSON with labels. * * @param Run $run Run. * @return array */ public function present( Run $run ) { $data = $run->to_array(); $data['confirmation_labels'] = array_intersect_key( Thresholds::labels(), array_flip( $data['confirmation_required'] ) ); // The batch that put the old values back, so an undone batch can link to it. $data['undone_by'] = null; if ( in_array( $run->status, array( RunStatus::UNDONE, RunStatus::PARTIALLY_UNDONE ), true ) ) { $children = $this->manager->runs()->list( array( 'parent_run_id' => $run->id, 'relation' => 'undo', 'status' => array( RunStatus::APPLIED, RunStatus::APPLIED_WITH_ISSUES, RunStatus::CANCELLED ), ), 1, 1 ); $data['undone_by'] = $children['runs'] ? $children['runs'][0]->id : null; } $data['product_names'] = (object) array(); try { $descriptor = Plugin::instance()->operations()->get( $run->operation_id )->describe(); $data['operation_label'] = $descriptor->label; $data['operation_description'] = $descriptor->description; $data['product_names'] = (object) self::product_names( $descriptor->schema->fields(), $run->operation_params ); } catch ( \Exception $e ) { $data['operation_label'] = $run->operation_id; $data['operation_description'] = ''; } /** * Filters a batch as the REST API returns it. * * @param array $data Batch data. * @param Run $run Batch. */ return apply_filters( 'recast_run_response', $data, $run ); } /** * Names of the products and variations that the options of a batch refer to, so the app can * show names instead of IDs. A product that no longer exists has no name. * * @param array> $fields Param schema fields. * @param array $params Stored params. * @return array Product or variation ID => name. */ public static function product_names( array $fields, array $params ) { $names = array(); foreach ( $fields as $field ) { if ( ! in_array( $field['type'], array( 'product', 'product_ids', 'variation_ids' ), true ) || ! isset( $params[ $field['key'] ] ) ) { continue; } foreach ( (array) $params[ $field['key'] ] as $id ) { $id = (int) $id; if ( $id <= 0 || isset( $names[ $id ] ) ) { continue; } $product = wc_get_product( $id ); if ( $product ) { $names[ $id ] = wp_strip_all_tags( $product->get_name() ); } } } return $names; } /** * Loads a run or errors. * * @param \WP_REST_Request $request Request. * @return Run|\WP_Error */ protected function load( \WP_REST_Request $request ) { $run = $this->manager->runs()->get( (int) $request['id'] ); if ( ! $run ) { return $this->error( 'recast_run_not_found', __( 'Batch not found.', 'recast-subscriptions' ), 404 ); } return $run; } /** * POST /runs. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response|\WP_Error */ public function create( \WP_REST_Request $request ) { $spec = CohortController::spec_from_request( $request ); if ( is_wp_error( $spec ) ) { return $spec; } $params = is_array( $request['params'] ) ? $request['params'] : array(); $run = $this->manager->create( $spec, (string) $request['operation'], $params, get_current_user_id(), array( 'suppress_emails' => (bool) $request['suppress_emails'] ) ); if ( is_wp_error( $run ) ) { return $run; } return $this->respond( $this->present( $run ), in_array( $run->status, RunStatus::previewing(), true ) ? 202 : 201 ); } /** * GET /runs. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response */ public function list_runs( \WP_REST_Request $request ) { $filters = array(); foreach ( array( 'operation', 'relation', 'from', 'to' ) as $key ) { if ( ! empty( $request[ $key ] ) ) { $filters[ $key ] = sanitize_text_field( (string) $request[ $key ] ); } } if ( ! empty( $request['status'] ) ) { $filters['status'] = array_map( 'sanitize_key', explode( ',', (string) $request['status'] ) ); } $result = $this->manager->runs()->list( $filters, (int) $request['page'], (int) $request['per_page'] ); return $this->respond( array( 'runs' => array_map( array( $this, 'present' ), $result['runs'] ), 'total' => $result['total'], 'page' => (int) $request['page'], 'per_page' => (int) $request['per_page'], ) ); } /** * GET /runs/{id}. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response|\WP_Error */ public function get_run( \WP_REST_Request $request ) { $run = $this->load( $request ); if ( is_wp_error( $run ) ) { return $run; } return $this->respond( $this->present( $run ) ); } /** * GET /runs/{id}/items. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response|\WP_Error */ public function items( \WP_REST_Request $request ) { $run = $this->load( $request ); if ( is_wp_error( $run ) ) { return $run; } $filters = array( 'changed' => (bool) $request['changed'], 'subscription_id' => (int) $request['subscription_id'], ); if ( ! empty( $request['status'] ) ) { $filters['status'] = array_map( 'sanitize_key', explode( ',', (string) $request['status'] ) ); } $result = $this->manager->items()->list( $run->id, $filters, (int) $request['page'], (int) $request['per_page'] ); return $this->respond( array( 'items' => array_map( array( $this, 'present_item' ), $result['items'] ), 'total' => $result['total'], 'page' => (int) $request['page'], 'per_page' => (int) $request['per_page'], ) ); } /** * Item JSON with the customer and products, so each row says whose subscription it is. * * @param \Recast\WCS\Run\RunItem $item Item. * @return array */ public function present_item( $item ) { $data = $item->to_array(); $data['customer'] = ''; $data['products'] = array(); $subscription = wcs_get_subscription( $item->subscription_id ); if ( $subscription instanceof \WC_Subscription ) { $row = RowFormatter::format( $subscription ); $data['customer'] = '' !== $row['customer']['name'] ? $row['customer']['name'] : $row['customer']['email']; $data['products'] = array_values( wp_list_pluck( $row['items'], 'name' ) ); } return $data; } /** * POST /runs/{id}/cancel. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response|\WP_Error */ public function cancel( \WP_REST_Request $request ) { $run = $this->load( $request ); if ( is_wp_error( $run ) ) { return $run; } // Any reader can discard a preview. To stop a batch that is writing changes, the user needs // the write capability. if ( in_array( $run->status, array( RunStatus::APPLYING, RunStatus::PAUSED, RunStatus::CANCELLING ), true ) ) { $allowed = Auth::can_change(); if ( true !== $allowed ) { return $allowed; } } /** * Lets an extension handle cancellation of an applying or paused run. * * @param bool|null $handled Null when not handled. * @param Run $run Run. */ $handled = apply_filters( 'recast_cancel_run', null, $run ); if ( null === $handled ) { $handled = $this->manager->cancel( $run ); } if ( ! $handled ) { return $this->error( 'recast_cannot_cancel', __( 'You cannot stop this batch in its current state.', 'recast-subscriptions' ), 409 ); } return $this->respond( $this->present( $this->manager->runs()->get( $run->id ) ) ); } }