base_url = rtrim( $base_url, '/' ); $this->username = $username; $this->password = $password; } public static function init( $base_url, $username, $password ) { if ( null === self::$instance ) { self::$instance = new self( $base_url, $username, $password ); } return self::$instance; } public static function get_instance() { return self::$instance; } /** * Native cURL request — mirrors softra-test.php requestJson(). */ private function curl_request( $method, $url, $payload = null, $extra_headers = array() ) { $ch = curl_init( $url ); if ( false === $ch ) { return new WP_Error( 'carei_curl_init', 'cURL init failed' ); } $headers = array_merge( array( 'Accept: application/json' ), $extra_headers ); if ( null !== $payload ) { $headers[] = 'Content-Type: application/json'; } curl_setopt_array( $ch, array( CURLOPT_CUSTOMREQUEST => strtoupper( $method ), CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 30, CURLOPT_CONNECTTIMEOUT => 15, ) ); if ( null !== $payload ) { curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( $payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) ); } $raw = curl_exec( $ch ); $err = curl_error( $ch ); $status = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE ); curl_close( $ch ); if ( false === $raw ) { return new WP_Error( 'carei_curl_error', 'cURL error: ' . $err ); } $decoded = json_decode( $raw, true ); if ( ! is_array( $decoded ) ) { $decoded = null; } return array( 'status' => $status, 'body' => $decoded, 'raw' => $raw, ); } /** * Get JWT token (cached in WP transient). */ public function get_token() { $token = get_transient( self::TOKEN_TRANSIENT ); if ( false !== $token ) { return $token; } $res = $this->curl_request( 'POST', $this->base_url . '/account/auth', array( 'login' => $this->username, 'password' => $this->password, ) ); if ( is_wp_error( $res ) ) { return new WP_Error( 'carei_auth_failed', 'Softra API auth failed: ' . $res->get_error_message() ); } if ( 200 !== $res['status'] || empty( $res['body']['token'] ) ) { $detail = isset( $res['raw'] ) ? $res['raw'] : ''; return new WP_Error( 'carei_auth_failed', 'Softra API auth failed (HTTP ' . $res['status'] . '): ' . $detail ); } $token = $res['body']['token']; set_transient( self::TOKEN_TRANSIENT, $token, self::TOKEN_EXPIRY ); return $token; } /** * Generic API request with auth. */ public function request( $method, $endpoint, $body = null, $query_params = array() ) { $token = $this->get_token(); if ( is_wp_error( $token ) ) { return $token; } $url = $this->base_url . $endpoint; if ( ! empty( $query_params ) ) { $url .= '?' . http_build_query( $query_params ); } $res = $this->curl_request( $method, $url, $body, array( 'Authorization: Bearer ' . $token ) ); if ( is_wp_error( $res ) ) { return $res; } if ( $res['status'] < 200 || $res['status'] >= 300 ) { return new WP_Error( 'carei_api_error', 'Softra API error: HTTP ' . $res['status'], array( 'status' => $res['status'], 'body' => $res['body'] ) ); } return $res['body']; } // ─── Public API Methods ─────────────────────────────────────── public function get_branches() { return $this->request( 'GET', '/branch/list' ); } public function get_all_car_classes() { return $this->request( 'GET', '/car/class/listAll' ); } /** * Get branches list with all segment names. * Returns branches + all segments. Frontend shows all branches for any segment * (actual availability verified at booking time by the API). * Cached as WP transient for 6 hours. */ public function get_segments_branches_map() { $cache_key = 'carei_segments_branches_map'; $cached = get_transient( $cache_key ); if ( false !== $cached ) { return $cached; } $branches = $this->get_branches(); if ( is_wp_error( $branches ) || ! is_array( $branches ) ) { return is_wp_error( $branches ) ? $branches : array(); } $all_classes = $this->get_all_car_classes(); $segments = array(); if ( ! is_wp_error( $all_classes ) && is_array( $all_classes ) ) { foreach ( $all_classes as $cls ) { $seg = is_string( $cls ) ? $cls : ( isset( $cls['name'] ) ? $cls['name'] : '' ); if ( '' !== $seg ) { $segments[] = $seg; } } } // All branches potentially have all segments. // Actual availability is checked at booking via /car/class/list. $branch_names = array(); foreach ( $branches as $b ) { if ( ! empty( $b['name'] ) ) { $branch_names[] = $b['name']; } } $map = array(); foreach ( $segments as $seg ) { $map[ $seg ] = $branch_names; } $result = array( 'segmentToBranches' => $map, 'branchToSegments' => array(), 'branches' => $branches, ); set_transient( $cache_key, $result, 6 * HOUR_IN_SECONDS ); return $result; } public function get_car_classes( $date_from, $date_to, $branch_name ) { return $this->request( 'POST', '/car/class/list', array( 'dateFrom' => $date_from, 'dateTo' => $date_to, 'branchName' => $branch_name, ) ); } public function get_car_models( $date_from, $date_to, $branch_name, $category = '' ) { return $this->request( 'POST', '/car/model/list', array( 'dateFrom' => $date_from, 'dateTo' => $date_to, 'branchName' => $branch_name, 'category' => $category, ), array( 'includeBrandDetails' => 'true' ) ); } public function get_pricelist( $category, $date_from, $date_to, $pickup_location, $language = 'pl', $currency = 'PLN' ) { return $this->request( 'POST', '/pricelist/list', array( 'category' => $category, 'dateFrom' => $date_from, 'dateTo' => $date_to, 'pickUpLocation' => $pickup_location, 'language' => $language, 'currency' => $currency, ) ); } public function get_pricing_summary( $params ) { return $this->request( 'POST', '/rent/princingSummary', $params ); } public function add_customer( $data ) { return $this->request( 'POST', '/customer/add', $data ); } public function make_booking( $data ) { return $this->request( 'POST', '/rent/makebooking', $data ); } public function confirm_booking( $reservation_id ) { return $this->request( 'POST', '/rent/confirm', array( 'reservationId' => $reservation_id, ) ); } public function cancel_booking( $reservation_id, $reason ) { return $this->request( 'POST', '/rent/cancel', array( 'reservationId' => $reservation_id, 'reason' => $reason, ) ); } public function get_agreements() { return $this->request( 'GET', '/agreement/def/list' ); } }