<?php

/* 
 * File     : egwapi_class.php
 * Created  : Apr 16, 2026 2:32:19 PM
 * Author   : Thomas Kirby <tkirby at dynengsys.com> (Thomas)
 * Company  : Dynamic Engineering Systems
 * Rights   : Under license to assignee. Not for resale or redistribution.
 * Encoding : UTF-8
 */

require_once __DIR__ . '/egwapi_config.php';
require_once __DIR__ . '/egwapi_helpers.php';


class EgwApi {

	// --- Configuration -------------------------------------------------------

	const ENDPOINT_CREATE_PLAYER   = '/operator/g/create/';
	const ENDPOINT_CREATE_OPERATOR = '/operator/create/';
	const ENDPOINT_PURCHASE        = '/operator/g/purchase/';
	const ENDPOINT_REDEEM          = '/operator/g/redeem/';
	const ENDPOINT_GAMES                = '/operator/%s/games/';
	const ENDPOINT_AVAILABLE_GAMES      = '/operator/games/';
	const ENDPOINT_OPERATOR_GAME_CREATE = '/operator/%s/games/';
	const ENDPOINT_OPERATOR_GAME_PATCH  = '/operator/%s/games/%d/';
	const ENDPOINT_OPERATOR_GAMES_DISABLE_ALL = '/operator/%s/games/disable-all/';
	const ENDPOINT_OPERATOR_GAMES_ENABLE_ALL  = '/operator/%s/games/enable-all/';
	const CURRENCY_SYMBOL   = '$';

	// --- Instance state -------------------------------------------------------

	private $bearer_token;  // Authorization bearer token
	private $base_url;      // API base URL

	// Last call results
	public $json_response = '';
	public $response_data = null;
	public $curl_error    = '';
	public $curl_errno    = 0;
	public $http_code     = 0;

	// --------------------------------------------------------------------------
	// Constructor
	// --------------------------------------------------------------------------
	public function __construct( $mode=null )
	{
		egwapi_log("EgwApi::__construct($mode)");
		
		// get the fixed! bearer token
		$this->bearer_token = EGW_BEARER_TOKEN;
		
		// set default mode
		if ($mode == null) $mode = 'sandbox';
						
		if ($mode == 'local') {
			$this->base_url = EGW_LOCAL_BASEURL;
		}
		else if ($mode == 'sandbox') {
			$this->base_url = EGW_SANDBOX_BASEURL;
		}
		else if ($mode == 'live') {
			$this->base_url = EGW_LIVE_BASEURL;
		}
		
		egwapi_log("EgwApi::__construct: base_url=$this->base_url");
		egwapi_log("EgwApi::__construct: bearer_token=$this->bearer_token");
	}

	// --------------------------------------------------------------------------
	// Public API methods
	// --------------------------------------------------------------------------

	/**
	 * Create a new player (customer) and immediately add money to their wallet.
	 * NOT USED — Ember does not use this endpoint.
	 *
	 * @param  string  $operator_id          channel_partner_games.game_distributor_id
	 * @param  string  $payment_provider_id  Payment provider business name registered with EGW
	 * @param  string  $operator_wallet_id   Payment provider wallet ID registered with EGW
	 * @param  string  $player_wallet_id     Payment provider wallet ID for the player
	 * @param  float   $amount
	 * @param  int     $operator_game_id     channel_partner_games.game_id
	 * @param  string  $first_name
	 * @param  string  $last_name
	 * @param  string  $email
	 * @param  string  $date_of_birth        Format: YYYY-MM-DD
	 * @return array
	 * @throws RuntimeException on cURL failure or non-2xx response
	 */
	public function createPlayer( string $operator_id, string $player_wallet_id, float $amount, int $operator_game_id, string $first_name, string $last_name, string $email, string $date_of_birth ) : array
	{
		egwapi_log("EgwApi::createPlayer( $operator_id, $player_wallet_id, $amount, $operator_game_id, $email )");

		$this->validateAmount( $amount );

		$payload = [
			'operator_id'         => $operator_id,
			'payment_provider_id' => $payment_provider_id,
			'operator_wallet_id'  => $operator_wallet_id,
			'player_wallet_id'    => $player_wallet_id,
			'amount'              => $this->formatAmount( $amount ),
			'operator_game_id'    => $operator_game_id,
			'customer'            => [
				'first_name'    => $first_name,
				'last_name'     => $last_name,
				'email'         => $email,
				'date_of_birth' => $date_of_birth,
			],
		];

		return $this->post( self::ENDPOINT_CREATE_PLAYER, $payload );
	}

	/**
	 * Add money to an existing customer's wallet.
	 * @param  string  $operator_id          channel_partner_games.game_distributor_id (73b83a81-0293-45ae-99ba-bdd7f3555aa4)
	 * @param  string  $operator_game_id     channel_partner_games.game_id
	 * @param  string  $operator_wallet_id   partner_id?
	 * @param  string  $player_game_user_id  EGW-assigned customer ID (from webhook after /create/)
	 * @param  string  $player_wallet_id     Payment provider wallet ID for the player
	 * @param  string  $payment_provider_id  'ember'
	 * @param  float   $amount
	 * @return array
	 * @throws RuntimeException on cURL failure or non-2xx response
	 */
	public function purchase( string $operator_id, string $operator_game_id, string $operator_wallet_id, string $player_game_user_id, string $player_wallet_id, string $payment_provider_id, float $amount ) : array
	{
		egwapi_log("EgwApi::purchase( $operator_id, $operator_game_id, $operator_wallet_id, $player_game_user_id, $player_wallet_id, $payment_provider_id, $amount ) ");
		
		$this->validateAmount( $amount );

		$payload = [
			'operator_id'         => $operator_id,
			'operator_game_id'    => $operator_game_id,
			'operator_wallet_id'  => $operator_wallet_id,
			'player_game_user_id' => $player_game_user_id,
			'player_wallet_id'    => $player_wallet_id,
			'payment_provider_id' => $payment_provider_id,
			'amount'              => $amount,
		];

		$response = $this->post( self::ENDPOINT_PURCHASE, $payload );
		//var_dump($response);

		// after you call create, redeem or purchase you will get session_id, after this you need to wait webhook in which
		// we will send you information about status of this session(success, failed) and additional information, such as created customer id		
		// we will send you information about status of this session(success, failed) and additional information, such as created customer id
		
		return $response;
	}

	/**
	 * Withdraw money from an existing customer's wallet.
	 *
	 * @param  string  $operator_id          channel_partner_games.game_distributor_id
	 * @param  int     $operator_game_id     channel_partner_games.game_id
	 * @param  string  $game_name            channel_partner_games.game_name
	 * @param  float   $amount
	 * @param  string  $player_wallet_id
	 * @param  string  $player_game_user_id  EGW-assigned customer ID (from webhook after /create/)
	 * @return array
	 * @throws RuntimeException on cURL failure or non-2xx response
	 */
	public function redeem( string $operator_id, string $operator_game_id, string $operator_wallet_id, string $player_game_user_id, string $player_wallet_id, string $payment_provider_id, float $amount ) : array
	{
		egwapi_log("EgwApi::redeem( $operator_id, $operator_game_id, $operator_wallet_id, $player_game_user_id, $player_wallet_id, $payment_provider_id, $amount ) ");
		
		$this->validateAmount( $amount );

		$payload = [
			'operator_id'         => $operator_id,
			'operator_game_id'    => $operator_game_id,
			'operator_wallet_id'  => $operator_wallet_id,
			'player_game_user_id' => $player_game_user_id,
			'player_wallet_id'    => $player_wallet_id,
			'payment_provider_id' => $payment_provider_id,
			'amount'              => $amount
		];

		return $this->post( self::ENDPOINT_REDEEM, $payload );
	}

	/**
	 * Get active games for an operator.
	 *
	 * @param  string  $operator_id  UUID returned by /operator/create/
	 * @return array
	 * @throws RuntimeException on cURL failure or non-2xx response
	 */
	public function getActiveGames( string $operator_id ) : array
	{
		$endpoint = sprintf( self::ENDPOINT_GAMES, $operator_id );
		return $this->get( $endpoint );
	}

	/**
	 * Get all games assigned to the authenticated processor.
	 * GET /operator/games/
	 *
	 * @return array
	 * @throws RuntimeException on cURL failure or non-2xx response
	 */
	public function getAvailableGames() : array
	{
		egwapi_log("EgwApi::getAvailableGames()");
		return $this->get( self::ENDPOINT_AVAILABLE_GAMES );
	}

	/**
	 * Create an OperatorGame for the given operator.
	 * POST /operator/{operator_id}/games/
	 *
	 * @param  string  $operator_id  UUID returned by /operator/create/
	 * @param  array   $fields       Optional payload fields per spec (game_id, display_name,
	 *                               pos_username, pos_password, publish_site, drawer,
	 *                               kiosk_number, station_number, order, pos_category)
	 * @return array
	 * @throws RuntimeException on cURL failure or non-2xx response
	 */
	public function createOperatorGame( string $operator_id, array $fields = [] ) : array
	{
		egwapi_log("EgwApi::createOperatorGame( $operator_id )");
		$endpoint = sprintf( self::ENDPOINT_OPERATOR_GAME_CREATE, $operator_id );
		return $this->post( $endpoint, $fields );
	}

	/**
	 * Update an existing OperatorGame.
	 * PATCH /operator/{operator_id}/games/{id}/
	 *
	 * @param  string  $operator_id       UUID returned by /operator/create/
	 * @param  int     $operator_game_id  Integer operator game id
	 * @param  array   $fields            Fields to update (only non-blank fields should be passed)
	 * @return array
	 * @throws RuntimeException on cURL failure or non-2xx response
	 */
	public function updateOperatorGame( string $operator_id, int $operator_game_id, array $fields = [] ) : array
	{
		egwapi_log("EgwApi::updateOperatorGame( $operator_id, $operator_game_id )");
		$endpoint = sprintf( self::ENDPOINT_OPERATOR_GAME_PATCH, $operator_id, $operator_game_id );
		return $this->patch( $endpoint, $fields );
	}

	/**
	 * Disable all operator games for the given operator (sets processor_publish_site=false).
	 * POST /operator/{operator_id}/games/disable-all/
	 *
	 * @param  string  $operator_id  UUID returned by /operator/create/
	 * @return array   Contains 'detail' (string) and 'updated' (int)
	 * @throws RuntimeException on cURL failure or non-2xx response
	 */
	public function disableAllGames( string $operator_id ) : array
	{
		egwapi_log("EgwApi::disableAllGames( $operator_id )");
		$endpoint = sprintf( self::ENDPOINT_OPERATOR_GAMES_DISABLE_ALL, $operator_id );
		return $this->post( $endpoint, [] );
	}

	/**
	 * Enable all operator games for the given operator (sets processor_publish_site=true).
	 * POST /operator/{operator_id}/games/enable-all/
	 *
	 * @param  string  $operator_id  UUID returned by /operator/create/
	 * @return array   Contains 'detail' (string) and 'updated' (int)
	 * @throws RuntimeException on cURL failure or non-2xx response
	 */
	public function enableAllGames( string $operator_id ) : array
	{
		egwapi_log("EgwApi::enableAllGames( $operator_id )");
		$endpoint = sprintf( self::ENDPOINT_OPERATOR_GAMES_ENABLE_ALL, $operator_id );
		return $this->post( $endpoint, [] );
	}

	/**
	 * Create a new operator account.
	 * Auth: Processor Bearer token (Authorization header).
	 *
	 * @param  string  $username
	 * @param  string  $email
	 * @param  string  $business_name
	 * @param  string  $contact_first_name
	 * @param  string  $contact_last_name
	 * @param  string  $address
	 * @param  string  $contact_phone
	 * @param  string  $operator_site_url
	 * @return array   Contains operator_id (UUID) and username
s	 * @throws RuntimeException on cURL failure or non-2xx response
	 */
	public function createOperator( string $username, string $email, string $business_name, string $contact_first_name, string $contact_last_name,
			string $address, string $contact_phone, string $operator_site_url ) : array
	{
		egwapi_log("EgwApi::createOperator( $username, $email, $business_name )");
		
		// Guard clause to enforce business logic
		if ($username === '') {
			throw new \InvalidArgumentException("@@@ Argument 'username' cannot be empty.");
		}

		$payload = [
			'username'           => $username,
			'email'              => $email,
			'business_name'      => $business_name,
			'contact_first_name' => $contact_first_name,
			'contact_last_name'  => $contact_last_name,
			'address'            => $address,
			'contact_phone'      => $contact_phone,
			'operator_site_url'  => $operator_site_url,
		];

		return $this->post( self::ENDPOINT_CREATE_OPERATOR, $payload );
	}
	
	
	/**
	 * Get an operator.
	 *
	 * @param  string  $username  assigned by user
	 * @return array
	 * @throws RuntimeException on cURL failure or non-2xx response
	 */
	 //qqq
	public function getOperator( string $username ) : array
	{
		$endpoint = sprintf( self::ENDPOINT_GAMES, $operator_id );
		return $this->get( $endpoint );
	}


	// --------------------------------------------------------------------------
	// Result accessors
	// --------------------------------------------------------------------------

	/** Returns the session_id from the last successful response, or null. */
	public function getSessionId() : ?string
	{
		return $this->response_data['session_id'] ?? null;
	}

	/** Returns the status string from the last response, or null. */
	public function getStatus() : ?string
	{
		return $this->response_data['status'] ?? null;
	}

	/** True if last call succeeded (HTTP 2xx).
	 * 200 OK: The standard response for successful HTTP requests. The actual response depends on the request method used (e.g., data is returned for a GET request).
	 * 201 Created: The request was successful, and a new resource was created as a result.
	 * 202 Accepted: The request was accepted for processing, but the processing is not yet complete. This is commonly used for asynchronous or long-running tasks.
	 * 203 Non-Authoritative Information: The server successfully processed the request, but is returning information that may have been modified by a transforming proxy from the origin server's 200 OK response.
	 * 204 No Content: The server successfully processed the request, but there is no additional content or body to return in the response.
	 * 205 Reset Content: The server successfully processed the request and requires that the requester reset the document view (e.g., clear a form). No content is returned in the response body.
	 * 206 Partial Content: The server is delivering only part of the resource due to a range header sent by the client. This is frequently used for video streaming or multi-part downloads.
	 * 207 Multi-Status: Formally defined in the WebDAV standard, this code conveys information about multiple independent operations or resources inside an XML payload.
	 * 208 Already Reported: Used inside a WebDAV 207 Multi-Status response to avoid repeatedly enumerating the internal members of a single binding.
	 * 226 IM Used: The server has fulfilled a GET request for the resource, and the response is a representation of the result of one or more instance-manipulations applied to the current instance
	 */
	public function succeeded() : bool
	{
		return $this->http_code >= 200 && $this->http_code < 300;
	}

	// --------------------------------------------------------------------------
	// DB methods — each opens and closes its own connection
	// --------------------------------------------------------------------------

	/**
	 * Look up the EGW-assigned player_game_user_id for a given game_username.
	 *
	 * @return string
	 * @throws RuntimeException if the record is not found or query fails
	 */
	public function getPlayerGameUserId( string $game_username ) : string
	{
		global $sysfiles;
		require($sysfiles."/connect_to_subscribers.php");  // provides $dbc

		$stmt = $dbc->prepare("
			SELECT player_game_user_id
			FROM   egw_sessions
			WHERE  game_username = ?
			  AND  player_game_user_id IS NOT NULL
			ORDER  BY created_at DESC
			LIMIT  1
		");

		if ( ! $stmt ) {
			$dbc->close();
			throw new RuntimeException( 'EgwApi::getPlayerGameUserId: prepare failed: ' . $dbc->error );
		}

		$stmt->bind_param( 's', $game_username );
		$stmt->execute();
		$stmt->bind_result( $player_game_user_id );
		$found = $stmt->fetch();
		$stmt->close();
		$dbc->close();

		if ( ! $found ) {
			throw new RuntimeException( "EgwApi::getPlayerGameUserId: no record found for game_username '{$game_username}'" );
		}

		return (string) $player_game_user_id;
	}

	/**
	 * Store the EGW-assigned player_game_user_id against a session.
	 *
	 * @throws RuntimeException if the update fails
	 */
	public function updatePlayerGameUserId( string $session_id, string $player_game_user_id ) : void
	{
		global $sysfiles;
		require($sysfiles."/connect_to_subscribers.php");  // provides $dbc

		$stmt = $dbc->prepare("
			UPDATE egw_sessions
			SET    player_game_user_id = ?
			WHERE  session_id = ?
			LIMIT  1
		");

		if ( ! $stmt ) {
			$dbc->close();
			throw new RuntimeException( 'EgwApi::updatePlayerGameUserId: prepare failed: ' . $dbc->error );
		}

		$stmt->bind_param( 'ss', $player_game_user_id, $session_id );
		$ok = $stmt->execute();
		$stmt->close();
		$dbc->close();

		if ( ! $ok ) {
			throw new RuntimeException( "EgwApi::updatePlayerGameUserId: update failed for session_id '{$session_id}'" );
		}
	}

	/**
	 * Update egw_sessions.status when EGW webhook confirms a session result.
	 *
	 * @throws RuntimeException if the update fails
	 */
	public function updateSessionStatus( string $session_id, string $status ) : void
	{
		global $sysfiles;
		require($sysfiles."/connect_to_subscribers.php");  // provides $dbc

		$stmt = $dbc->prepare("
			UPDATE egw_sessions
			SET    status = ?
			WHERE  session_id = ?
			LIMIT  1
		");

		if ( ! $stmt ) {
			$dbc->close();
			throw new RuntimeException( 'EgwApi::updateSessionStatus: prepare failed: ' . $dbc->error );
		}

		$stmt->bind_param( 'ss', $status, $session_id );
		$ok = $stmt->execute();
		$stmt->close();
		$dbc->close();

		if ( ! $ok ) {
			throw new RuntimeException( "EgwApi::updateSessionStatus: update failed for session_id '{$session_id}'" );
		}
	}

	/**
	 * Insert a new EGW session record into egw_sessions.
	 *
	 * @throws RuntimeException if the insert fails
	 */
	public function insertSession( array $data ) : void
	{
		global $sysfiles;
		require($sysfiles."/connect_to_subscribers.php");  // provides $dbc

		$stmt = $dbc->prepare("
			INSERT INTO egw_sessions
				( rtp_reference_number, session_id, game_distributor_id,
				  game_id, game_name, game_username, status )
			VALUES
				( ?, ?, ?, ?, ?, ?, ? )
		");

		if ( ! $stmt ) {
			$dbc->close();
			throw new RuntimeException( 'EgwApi::insertSession: prepare failed: ' . $dbc->error );
		}

		$stmt->bind_param(
			'sssssss',
			$data['rtp_reference_number'],
			$data['session_id'],
			$data['game_distributor_id'],
			$data['game_id'],
			$data['game_name'],
			$data['game_username'],
			$data['status']
		);

		$ok = $stmt->execute();
		$stmt->close();
		$dbc->close();

		if ( ! $ok ) {
			throw new RuntimeException( 'EgwApi::insertSession: insert failed' );
		}
	}

	// --------------------------------------------------------------------------
	// Internal helpers
	// --------------------------------------------------------------------------

	/**
	 * @return array
	 * @throws RuntimeException on cURL failure or non-2xx response
	 */
	private function get( string $endpoint ) : array
	{
		egwapi_log("get( $endpoint )");

		$this->clearState();

		$url = $this->base_url . $endpoint;
//zzz get		

		$ch = curl_init( $url );

		curl_setopt_array( $ch, [
			CURLOPT_HTTPGET        => true,
			CURLOPT_RETURNTRANSFER => true,
			CURLOPT_CONNECTTIMEOUT => 10,
			CURLOPT_TIMEOUT        => 10,
			CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
			CURLOPT_HTTPHEADER     => [
				//'Content-Type: application/json',	// not sending, not needed
				'Authorization: Bearer ' . $this->bearer_token,
			],
		]);

		$this->json_response = curl_exec( $ch );
		$this->curl_errno    = curl_errno( $ch );
		$this->curl_error    = curl_error( $ch );
		$this->http_code     = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE );
		curl_close( $ch );
		
		egwapi_log( 'get json_response [' . $this->http_code . ']: ' . $this->json_response );

		if ( $this->curl_errno ) {
			throw new RuntimeException( "EgwApi::get: cURL error ({$this->curl_errno}): {$this->curl_error}" );
		}

		$this->response_data = json_decode( $this->json_response, true );
		
		egwapi_log( 'get response decoded = ' . var_export($this->response_data,true) );

		if ( ! $this->succeeded() ) {
			//throw new RuntimeException( "EgwApi::get: HTTP {$this->http_code}: {$this->json_response}" );
			throw new RuntimeException( $this->unscrew_errors($this->response_data), $this->http_code );
		}

		return $this->response_data;
	}

	/**
	 * @return array
	 * @throws RuntimeException on cURL failure or non-2xx response
	 */
	private function post( string $endpoint, array $payload ) : array
	{
		egwapi_log("post( $endpoint, " . var_export($payload,true) . " )");

		$this->clearState();

		$url = $this->base_url . $endpoint;
//zzz post

		$body = json_encode( $payload );

		$ch = curl_init( $url );
		curl_setopt_array( $ch, [
			CURLOPT_POST           => true,
			CURLOPT_POSTFIELDS     => $body,
			CURLOPT_RETURNTRANSFER => true,
			CURLOPT_CONNECTTIMEOUT => 10,
			CURLOPT_TIMEOUT        => 10,
			CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
			CURLOPT_HTTPHEADER     => [
				'Content-Type: application/json',
				'Authorization: Bearer ' . $this->bearer_token,
			],
		]);

		$this->json_response = curl_exec( $ch );
		$this->curl_errno    = curl_errno( $ch );
		$this->curl_error    = curl_error( $ch );
		$this->http_code     = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE );
		curl_close( $ch );

		egwapi_log( 'post json_response [' . $this->http_code . ']: ' . $this->json_response );

		if ( $this->curl_errno ) {
			throw new RuntimeException( "EgwApi::post: cURL error ({$this->curl_errno}): {$this->curl_error}" );
		}

		$this->response_data = json_decode( $this->json_response, true );

		egwapi_log( 'post response decoded = ' . var_export($this->response_data,true) );
		
		if ( ! $this->succeeded() ) {
			throw new RuntimeException( $this->unscrew_errors($this->response_data), $this->http_code );
		}

		return (array) $this->response_data;
	}

	/**
	 * @return array
	 * @throws RuntimeException on cURL failure or non-2xx response
	 */
	private function patch( string $endpoint, array $payload ) : array
	{
		egwapi_log("patch( $endpoint, " . var_export($payload,true) . " )");

		$this->clearState();

		$url  = $this->base_url . $endpoint;
//zzz patch

		$body = json_encode( $payload );

		$ch = curl_init( $url );
		curl_setopt_array( $ch, [
			CURLOPT_CUSTOMREQUEST  => 'PATCH',
			CURLOPT_POSTFIELDS     => $body,
			CURLOPT_RETURNTRANSFER => true,
			CURLOPT_CONNECTTIMEOUT => 10,
			CURLOPT_TIMEOUT        => 10,
			CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
			CURLOPT_HTTPHEADER     => [
				'Content-Type: application/json',
				'Authorization: Bearer ' . $this->bearer_token,
			],
		]);

		$this->json_response = curl_exec( $ch );
		$this->curl_errno    = curl_errno( $ch );
		$this->curl_error    = curl_error( $ch );
		$this->http_code     = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE );
		curl_close( $ch );

		egwapi_log( 'patch json_response [' . $this->http_code . ']: ' . $this->json_response );

		if ( $this->curl_errno ) {
			throw new RuntimeException( "EgwApi::patch: cURL error ({$this->curl_errno}): {$this->curl_error}" );
		}

		$this->response_data = json_decode( $this->json_response, true );

		egwapi_log( 'patch response decoded = ' . var_export($this->response_data,true) );

		if ( ! $this->succeeded() ) {
			//throw new RuntimeException( "EgwApi::patch: HTTP {$this->http_code}: {$this->json_response}" );
			//throw new RuntimeException( "EgwApi::get  : HTTP {$this->http_code}: {$this->json_response}" );
			throw new RuntimeException( $this->unscrew_errors($this->response_data), $this->http_code );
		}

		return (array) $this->response_data;
	}

	private function unscrew_errors( $data ) : string
	{
		$msg = "";

		foreach( $data as $key => $errs ) {
			foreach( $errs as $err ) {
				//print "$err<br>";
				if ($msg != "") $msg .= " ";
				$msg .= $err;
			}
		}

		return $msg;
	}
	
	
	private function clearState() : void
	{
		$this->json_response = '';
		$this->response_data = null;
		$this->curl_error    = '';
		$this->curl_errno    = 0;
		$this->http_code     = 0;
	}

	
	private function validateAmount( float $amount ) : void
	{
		if ( $amount < 0.01 ) {
			throw new InvalidArgumentException( 'EgwApi: amount must be >= 0.01' );
		}
	}

	/** Format a float as a decimal string with exactly 2 decimal places. */
	private function formatAmount( float $amount ) : string
	{
		return number_format( $amount, 2, '.', '' );
	}
}
