false, 'http' => 0, 'data' => null, ); // After a network failure or a server error, do not wait for the API again for a minute: // the shop keeps its normal speed and the checkout is never slowed down (degraded mode). if ( ! self::has_key() || get_transient( 'jsonpage_down' ) ) { return $failure; } $url = add_query_arg( $query, rtrim( JSONPAGE_API_BASE, '/' ) . $path ); $response = wp_remote_get( $url, array( 'timeout' => $timeout, 'redirection' => 0, 'headers' => array( 'X-API-Key' => (string) jsonpage_setting( 'api_key' ), 'Accept' => 'application/json', 'User-Agent' => 'Jsonpage-WordPress/' . JSONPAGE_VERSION, ), ) ); if ( is_wp_error( $response ) ) { set_transient( 'jsonpage_down', 1, MINUTE_IN_SECONDS ); return $failure; } $http = (int) wp_remote_retrieve_response_code( $response ); if ( $http >= 500 ) { set_transient( 'jsonpage_down', 1, MINUTE_IN_SECONDS ); } $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( 401 === $http || 403 === $http ) { // Shown once in the admin so that the shop owner learns about a wrong or revoked key. set_transient( 'jsonpage_key_rejected', 1, HOUR_IN_SECONDS ); } elseif ( 200 === $http ) { delete_transient( 'jsonpage_key_rejected' ); } return array( 'ok' => 200 === $http && is_array( $data ), 'http' => $http, 'data' => is_array( $data ) ? $data : null, ); } /** * Suggestions while the customer types. * * @param string $q Text typed, at least 3 characters. * @param string[] $countries Country codes. * @return array|null List of suggestions, or null when the API is not available. */ public static function autocomplete( $q, $countries ) { $status = 'allow' === jsonpage_setting( 'closed' ) || 'warn' === jsonpage_setting( 'closed' ) ? 'any' : 'active'; $key = 'jsonpage_ac_' . md5( strtolower( $q ) . '|' . implode( ',', $countries ) . '|' . $status ); $cached = get_transient( $key ); if ( is_array( $cached ) ) { return $cached; } $answer = self::request( '/autocomplete', array( 'q' => $q, 'country' => implode( ',', $countries ), 'status' => $status, 'limit' => 8, ), 5 ); if ( ! $answer['ok'] || ! isset( $answer['data']['data'] ) || ! is_array( $answer['data']['data'] ) ) { return null; } $out = array(); foreach ( $answer['data']['data'] as $row ) { if ( ! is_array( $row ) || empty( $row['id'] ) ) { continue; } $out[] = array( 'id' => (string) $row['id'], 'name' => isset( $row['name'] ) ? (string) $row['name'] : '', 'status' => isset( $row['status'] ) ? (string) $row['status'] : 'unknown', 'postal_code' => isset( $row['postal_code'] ) ? (string) $row['postal_code'] : '', 'city' => isset( $row['city'] ) ? (string) $row['city'] : '', 'kind' => isset( $row['kind'] ) ? (string) $row['kind'] : 'company', ); } set_transient( $key, $out, 10 * MINUTE_IN_SECONDS ); return $out; } /** * Company record (name, address, VAT number) from its global identifier. * * @param string $id Identifier such as FR-552032534. * @return array|null Record, or null when the API is not available. */ public static function company( $id ) { if ( ! preg_match( '/^[A-Z]{2}-[A-Za-z0-9]{3,20}$/', $id ) ) { return null; } $key = 'jsonpage_co_' . md5( $id ); $cached = get_transient( $key ); if ( is_array( $cached ) ) { return $cached; } $answer = self::request( '/companies/' . rawurlencode( $id ), array( 'include' => 'address,vat' ), 5 ); if ( ! $answer['ok'] || ! isset( $answer['data']['data'] ) || ! is_array( $answer['data']['data'] ) ) { return null; } $c = $answer['data']['data']; $address = isset( $c['address'] ) && is_array( $c['address'] ) ? $c['address'] : array(); $vat = isset( $c['vat'] ) && is_array( $c['vat'] ) ? $c['vat'] : array(); $out = array( 'id' => isset( $c['id'] ) ? (string) $c['id'] : $id, 'country' => isset( $c['country'] ) ? (string) $c['country'] : '', 'registry_id' => isset( $c['registry_id'] ) ? (string) $c['registry_id'] : '', 'name' => isset( $c['name'] ) ? (string) $c['name'] : '', 'status' => isset( $c['status'] ) ? (string) $c['status'] : 'unknown', 'address' => array( 'line' => isset( $address['line'] ) ? (string) $address['line'] : '', 'postal_code' => isset( $address['postal_code'] ) ? (string) $address['postal_code'] : '', 'city' => isset( $address['city'] ) ? (string) $address['city'] : '', 'country' => isset( $address['country'] ) ? (string) $address['country'] : '', ), 'vat' => array( 'number' => isset( $vat['number'] ) ? (string) $vat['number'] : '', 'status' => isset( $vat['status'] ) ? (string) $vat['status'] : '', ), ); set_transient( $key, $out, DAY_IN_SECONDS ); return $out; } /** * Checks a VAT number. The result is cached for 24 hours. * * Status: * valid the number is registered (the order can be exempted). * invalid the number is malformed or not registered. * pending the check is not finished yet. * unverified the check cannot be made (country not served, register unavailable). * error the API did not answer: degraded mode, nothing is blocked. * * @param string $vat Number typed by the customer. * @return array{status:string,number:string,closed:bool,checked_at:int,cached:bool} */ public static function verify_vat( $vat ) { $number = jsonpage_normalize_vat( $vat ); $result = array( 'status' => 'error', 'number' => $number, 'closed' => false, 'checked_at' => time(), 'cached' => false, ); if ( '' === $number ) { $result['status'] = 'unverified'; return $result; } static $memo = array(); if ( isset( $memo[ $number ] ) ) { return $memo[ $number ]; } $key = 'jsonpage_v_' . md5( $number ); $cached = get_transient( $key ); if ( is_array( $cached ) && isset( $cached['status'] ) ) { $cached['cached'] = true; $memo[ $number ] = $cached; return $cached; } $answer = self::request( '/validate', array( 'vat' => $number ), 4 ); if ( ! $answer['ok'] || ! isset( $answer['data']['data'] ) || ! is_array( $answer['data']['data'] ) ) { return $result; } $data = $answer['data']['data']; $checks = array(); foreach ( isset( $data['checks'] ) && is_array( $data['checks'] ) ? $data['checks'] : array() as $check ) { if ( is_array( $check ) && isset( $check['code'] ) ) { $checks[ $check['code'] ] = $check; } } $status = 'unverified'; if ( isset( $checks['vat_format'] ) && empty( $checks['vat_format']['ok'] ) ) { $status = 'invalid'; } elseif ( isset( $checks['vat_country_supported'] ) && empty( $checks['vat_country_supported']['ok'] ) ) { $status = 'unverified'; } elseif ( isset( $checks['vat_registered'] ) ) { $value = isset( $checks['vat_registered']['value'] ) ? $checks['vat_registered']['value'] : ''; if ( 'valid' === $value && ! empty( $checks['vat_registered']['ok'] ) ) { $status = 'valid'; } elseif ( 'invalid' === $value || empty( $checks['vat_registered']['ok'] ) ) { $status = 'invalid'; } elseif ( 'pending' === $value ) { $status = 'pending'; } } $closed = ( isset( $checks['company_active'] ) && empty( $checks['company_active']['ok'] ) ) || ( isset( $data['company']['status'] ) && 'closed' === $data['company']['status'] ); $result['status'] = $status; $result['closed'] = (bool) $closed; if ( 'pending' !== $status ) { set_transient( $key, $result, 'unverified' === $status ? HOUR_IN_SECONDS : DAY_IN_SECONDS ); $memo[ $number ] = $result; } return $result; } }