api_key = $api_key; } /** * Create a one-time checkout. * * @param array $body Request body (amount, currency, description, buyer fields, URLs, metadata). * @param string $idempotency_key Key that makes a retried request return the same checkout. * @return array Decoded checkout, including session_id and checkoutUrl. * @throws Exception When the request fails or the response is not a checkout. */ public function create_checkout( array $body, $idempotency_key ) { $checkout = $this->request( 'POST', '/gateway/sessions', $body, array( 'Idempotency-Key' => $idempotency_key ) ); if ( empty( $checkout['checkoutUrl'] ) || empty( $checkout['session_id'] ) ) { throw new Exception( 'AgentaOS did not return a checkout URL.' ); } return $checkout; } /** * Read a checkout by its public session ID. * * @param string $session_id Session ID returned when the checkout was created. * @return array Decoded checkout, including status. * @throws Exception When the request fails. */ public function get_checkout( $session_id ) { return $this->request( 'GET', '/gateway/sessions/' . rawurlencode( $session_id ) ); } /** * Create an AgentaOS product (a "payment link"), used here for subscription plans. * * @param array $body Product fields. * @param string $idempotency_key Key that makes a retried request return the same product. * @return array Decoded product, including id (the linkId). * @throws Exception When the request fails. */ public function create_product( array $body, $idempotency_key ) { $product = $this->request( 'POST', '/gateway/payment-links', $body, array( 'Idempotency-Key' => $idempotency_key ) ); if ( empty( $product['id'] ) ) { throw new Exception( 'AgentaOS did not return a product id.' ); } return $product; } /** * Cancel a subscription at the end of its current period. * * @param string $subscription_id AgentaOS subscription id. * @throws Exception When AgentaOS refuses. */ public function cancel_subscription( $subscription_id ) { $this->request( 'POST', '/gateway/subscriptions/' . rawurlencode( $subscription_id ) . '/cancel', array(), array( 'Idempotency-Key' => 'cancel-' . $subscription_id ) ); } /** * Ask AgentaOS to refund a sale in full. It reviews the request; no money moves yet. * * @param string $transaction_id The sale's transaction id. * @throws Exception When AgentaOS refuses the request. */ public function request_refund( $transaction_id ) { $this->request( 'POST', '/gateway/transactions/' . rawurlencode( $transaction_id ) . '/refund-request', array(), array( 'Idempotency-Key' => 'refund-' . $transaction_id ) ); } /** * @return array Sale id => mor_status for the most recent sales. * @throws Exception When the request fails. */ public function sale_statuses() { $page = $this->request( 'GET', '/gateway/all-transactions?limit=5000' ); $status = array(); foreach ( isset( $page['items'] ) && is_array( $page['items'] ) ? $page['items'] : array() as $sale ) { if ( isset( $sale['id'], $sale['mor_status'] ) ) { $status[ (string) $sale['id'] ] = (string) $sale['mor_status']; } } return $status; } /** * The PDF receipt of a paid invoice: AgentaOS's invoice to the buyer, marked paid. * * @param string $invoice_id AgentaOS invoice id. * @return string PDF bytes. * @throws Exception When the request fails or the response is not a PDF. */ public function invoice_receipt( $invoice_id ) { list( $status, $body, $type ) = $this->send( 'GET', '/gateway/invoices/' . rawurlencode( $invoice_id ) . '/receipt' ); if ( 200 !== $status || 0 !== strpos( $type, 'application/pdf' ) ) { throw new Exception( 'AgentaOS did not return the receipt (HTTP ' . (int) $status . ').' ); } return $body; } /** * Cheap authenticated call used by the settings page to check a key. * * @throws Exception When the key is rejected or the API is unreachable. */ public function ping() { $this->request( 'GET', '/gateway/sessions?limit=1' ); } /** * @param string $method HTTP method. * @param string $path Path under the API base URL. * @param array $body JSON body for POST requests. * @param array $headers Extra headers. * @return array Decoded JSON response. * @throws Exception When the request fails or returns an error status. */ private function request( $method, $path, array $body = array(), array $headers = array() ) { list( $status, $raw ) = $this->send( $method, $path, $body, $headers ); $decoded = json_decode( $raw, true ); if ( $status < 200 || $status >= 300 ) { $message = is_array( $decoded ) && isset( $decoded['message'] ) ? $decoded['message'] : 'HTTP ' . $status; throw new Exception( esc_html( is_array( $message ) ? implode( ' ', $message ) : (string) $message ) ); } if ( ! is_array( $decoded ) ) { throw new Exception( 'AgentaOS returned an unreadable response.' ); } return $decoded; } /** * @param string $method HTTP method. * @param string $path Path under the API base URL. * @param array $body JSON body for POST requests. * @param array $headers Extra headers. * @return array{0:int,1:string,2:string} Status, raw body and content type. * @throws Exception When AgentaOS cannot be reached. */ private function send( $method, $path, array $body = array(), array $headers = array() ) { $args = array( 'method' => $method, 'timeout' => 30, 'headers' => array_merge( array( 'x-api-key' => $this->api_key, 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'User-Agent' => 'agentaos-woocommerce/' . AGENTAOS_PAYMENTS_VERSION, ), $headers ), ); if ( 'POST' === $method ) { $args['body'] = wp_json_encode( $body ); } // Retry brief outages: reads, and new checkouts, whose Idempotency-Key returns the same checkout on // a retry. Other writes are sent once, so a retry can never create a second plan or request. $can_retry = 'GET' === $method || '/gateway/sessions' === $path; $attempts = $can_retry ? 3 : 1; for ( $attempt = 1; $attempt <= $attempts; $attempt++ ) { $response = wp_remote_request( self::BASE_URL . $path, $args ); $status = is_wp_error( $response ) ? 0 : (int) wp_remote_retrieve_response_code( $response ); $transient = 0 === $status || 429 === $status || $status >= 500; if ( ! $transient || $attempt === $attempts ) { break; } usleep( 500000 * $attempt ); } if ( is_wp_error( $response ) ) { throw new Exception( esc_html( $response->get_error_message() ) ); } return array( $status, (string) wp_remote_retrieve_body( $response ), (string) wp_remote_retrieve_header( $response, 'content-type' ) ); } }