diff --git a/config/payment.php b/config/payment.php index 0d980cb..abd0899 100644 --- a/config/payment.php +++ b/config/payment.php @@ -41,8 +41,7 @@ ], 'wave' => [ - 'client_key' => '', - 'client_secret' => '', + 'api_key' => '', // Your Wave API key (starts with wave_sn_prod_ or wave_sn_sandbox_) 'webhook_secret' => '' ], diff --git a/src/IvoryCost/OrangeMoney/OrangeMoneyGateway.php b/src/IvoryCost/OrangeMoney/OrangeMoneyGateway.php index 9c94e53..9f17fd2 100644 --- a/src/IvoryCost/OrangeMoney/OrangeMoneyGateway.php +++ b/src/IvoryCost/OrangeMoney/OrangeMoneyGateway.php @@ -37,9 +37,17 @@ public function payment(...$args) // Set the right production endpoint $payment->setPaymentEndpoint('/orange-money-webpay/v1/webpayment'); - $payment->setNotifyUrl($args['notif_url']); - $payment->setCancelUrl($args['cancel_url']); - $payment->setReturnUrl($args['return_url']); + if (isset($args['notif_url'])) { + $payment->setNotifyUrl($args['notif_url']); + } + + if (isset($args['cancel_url'])) { + $payment->setCancelUrl($args['cancel_url']); + } + + if (isset($args['return_url'])) { + $payment->setReturnUrl($args['return_url']); + } $amount = $args['amount']; $reference = $args['reference']; diff --git a/src/IvoryCost/Wave/WaveCheckoutSession.php b/src/IvoryCost/Wave/WaveCheckoutSession.php new file mode 100644 index 0000000..9a9afe2 --- /dev/null +++ b/src/IvoryCost/Wave/WaveCheckoutSession.php @@ -0,0 +1,249 @@ +id; + } + + /** + * Get amount + * + * @return string + */ + public function getAmount(): string + { + return $this->amount; + } + + /** + * Get checkout status + * + * @return string + */ + public function getCheckoutStatus(): string + { + return $this->checkoutStatus; + } + + /** + * Get payment status + * + * @return string + */ + public function getPaymentStatus(): string + { + return $this->paymentStatus; + } + + /** + * Get Wave launch URL + * + * @return string + */ + public function getWaveLaunchUrl(): string + { + return $this->waveLaunchUrl; + } + + /** + * Get transaction ID + * + * @return string|null + */ + public function getTransactionId(): ?string + { + return $this->transactionId; + } + + /** + * Get client reference + * + * @return string|null + */ + public function getClientReference(): ?string + { + return $this->clientReference; + } + + /** + * Check if checkout is open + * + * @return bool + */ + public function isOpen(): bool + { + return $this->checkoutStatus === 'open'; + } + + /** + * Check if checkout is complete + * + * @return bool + */ + public function isComplete(): bool + { + return $this->checkoutStatus === 'complete'; + } + + /** + * Check if checkout is expired + * + * @return bool + */ + public function isExpired(): bool + { + return $this->checkoutStatus === 'expired'; + } + + /** + * Check if payment succeeded + * + * @return bool + */ + public function isPaymentSucceeded(): bool + { + return $this->paymentStatus === 'succeeded'; + } + + /** + * Check if payment is processing + * + * @return bool + */ + public function isPaymentProcessing(): bool + { + return $this->paymentStatus === 'processing'; + } + + /** + * Check if payment is cancelled + * + * @return bool + */ + public function isPaymentCancelled(): bool + { + return $this->paymentStatus === 'cancelled'; + } + + /** + * Get last payment error + * + * @return array|null + */ + public function getLastPaymentError(): ?array + { + return $this->lastPaymentError; + } + + /** + * Convert to array + * + * @return array + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'amount' => $this->amount, + 'checkout_status' => $this->checkoutStatus, + 'payment_status' => $this->paymentStatus, + 'currency' => $this->currency, + 'business_name' => $this->businessName, + 'success_url' => $this->successUrl, + 'error_url' => $this->errorUrl, + 'wave_launch_url' => $this->waveLaunchUrl, + 'transaction_id' => $this->transactionId, + 'client_reference' => $this->clientReference, + 'aggregated_merchant_id' => $this->aggregatedMerchantId, + 'restrict_payer_mobile' => $this->restrictPayerMobile, + 'last_payment_error' => $this->lastPaymentError, + 'when_created' => $this->whenCreated, + 'when_completed' => $this->whenCompleted, + 'when_expires' => $this->whenExpires, + ]; + } +} diff --git a/src/IvoryCost/Wave/WaveClient.php b/src/IvoryCost/Wave/WaveClient.php new file mode 100644 index 0000000..703e2e1 --- /dev/null +++ b/src/IvoryCost/Wave/WaveClient.php @@ -0,0 +1,382 @@ +apiKey = $apiKey; + $this->http = new HttpClient([ + 'base_uri' => self::BASE_URL, + 'headers' => [ + 'Authorization' => 'Bearer ' . $apiKey, + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ], + ]); + } + + /** + * Create a checkout session + * + * @param array $data + * @param string|null $idempotencyKey Unique key to prevent duplicate payments + * @return WaveCheckoutSession + * @throws PaymentRequestException + */ + public function createCheckoutSession(array $data, ?string $idempotencyKey = null): WaveCheckoutSession + { + try { + $headers = []; + if ($idempotencyKey !== null) { + $headers['Idempotency-Key'] = $idempotencyKey; + } + + $response = $this->http->post('/v1/checkout/sessions', [ + 'json' => $this->buildCheckoutPayload($data), + 'headers' => $headers, + ]); + + $content = $response->getBody()->getContents(); + $sessionData = json_decode($content, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new PaymentRequestException('Invalid JSON response from Wave API'); + } + + return WaveCheckoutSession::fromResponse($sessionData); + } catch (\GuzzleHttp\Exception\ClientException $e) { + return $this->handleClientException($e); + } catch (\GuzzleHttp\Exception\ServerException $e) { + throw new PaymentRequestException( + 'Wave API server error: ' . $e->getMessage(), + $e->getCode(), + $e + ); + } catch (\Exception $e) { + throw new PaymentRequestException( + 'Failed to create Wave checkout session: ' . $e->getMessage(), + 0, + $e + ); + } + } + + /** + * Retrieve a checkout session by ID + * + * @param string $sessionId + * @return WaveCheckoutSession + * @throws PaymentRequestException + */ + public function retrieveCheckoutSession(string $sessionId): WaveCheckoutSession + { + try { + $response = $this->http->get("/v1/checkout/sessions/{$sessionId}"); + + $content = $response->getBody()->getContents(); + $sessionData = json_decode($content, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new PaymentRequestException('Invalid JSON response from Wave API'); + } + + return WaveCheckoutSession::fromResponse($sessionData); + } catch (\GuzzleHttp\Exception\ClientException $e) { + return $this->handleClientException($e); + } catch (\GuzzleHttp\Exception\ServerException $e) { + throw new PaymentRequestException( + 'Wave API server error: ' . $e->getMessage(), + $e->getCode(), + $e + ); + } catch (\Exception $e) { + throw new PaymentRequestException( + 'Failed to retrieve Wave checkout session: ' . $e->getMessage(), + 0, + $e + ); + } + } + + /** + * Retrieve a checkout session by transaction ID + * + * @param string $transactionId + * @return WaveCheckoutSession + * @throws PaymentRequestException + */ + public function retrieveCheckoutByTransactionId(string $transactionId): WaveCheckoutSession + { + try { + $response = $this->http->get('/v1/checkout/sessions', [ + 'query' => ['transaction_id' => $transactionId], + ]); + + $content = $response->getBody()->getContents(); + $sessionData = json_decode($content, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new PaymentRequestException('Invalid JSON response from Wave API'); + } + + return WaveCheckoutSession::fromResponse($sessionData); + } catch (\GuzzleHttp\Exception\ClientException $e) { + return $this->handleClientException($e); + } catch (\GuzzleHttp\Exception\ServerException $e) { + throw new PaymentRequestException( + 'Wave API server error: ' . $e->getMessage(), + $e->getCode(), + $e + ); + } catch (\Exception $e) { + throw new PaymentRequestException( + 'Failed to retrieve Wave checkout session: ' . $e->getMessage(), + 0, + $e + ); + } + } + + /** + * Search for checkout sessions by client reference + * + * @param string $clientReference + * @return array + * @throws PaymentRequestException + */ + public function searchCheckoutSessions(string $clientReference): array + { + try { + $response = $this->http->get('/v1/checkout/sessions/search', [ + 'query' => ['client_reference' => $clientReference], + ]); + + $content = $response->getBody()->getContents(); + $data = json_decode($content, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new PaymentRequestException('Invalid JSON response from Wave API'); + } + + $sessions = []; + foreach ($data['result'] ?? [] as $sessionData) { + $sessions[] = WaveCheckoutSession::fromResponse($sessionData); + } + + return $sessions; + } catch (\GuzzleHttp\Exception\ClientException $e) { + return $this->handleClientException($e); + } catch (\GuzzleHttp\Exception\ServerException $e) { + throw new PaymentRequestException( + 'Wave API server error: ' . $e->getMessage(), + $e->getCode(), + $e + ); + } catch (\Exception $e) { + throw new PaymentRequestException( + 'Failed to search Wave checkout sessions: ' . $e->getMessage(), + 0, + $e + ); + } + } + + /** + * Refund a checkout session + * + * @param string $sessionId + * @return bool + * @throws PaymentRequestException + */ + public function refundCheckoutSession(string $sessionId): bool + { + try { + $response = $this->http->post("/v1/checkout/sessions/{$sessionId}/refund"); + return $response->getStatusCode() === 200; + } catch (\GuzzleHttp\Exception\ClientException $e) { + return $this->handleClientException($e); + } catch (\GuzzleHttp\Exception\ServerException $e) { + throw new PaymentRequestException( + 'Wave API server error: ' . $e->getMessage(), + $e->getCode(), + $e + ); + } catch (\Exception $e) { + throw new PaymentRequestException( + 'Failed to refund Wave checkout session: ' . $e->getMessage(), + 0, + $e + ); + } + } + + /** + * Expire a checkout session + * + * @param string $sessionId + * @return bool + * @throws PaymentRequestException + */ + public function expireCheckoutSession(string $sessionId): bool + { + try { + $response = $this->http->post("/v1/checkout/sessions/{$sessionId}/expire"); + return $response->getStatusCode() === 200; + } catch (\GuzzleHttp\Exception\ClientException $e) { + return $this->handleClientException($e); + } catch (\GuzzleHttp\Exception\ServerException $e) { + throw new PaymentRequestException( + 'Wave API server error: ' . $e->getMessage(), + $e->getCode(), + $e + ); + } catch (\Exception $e) { + throw new PaymentRequestException( + 'Failed to expire Wave checkout session: ' . $e->getMessage(), + 0, + $e + ); + } + } + + /** + * Build checkout payload from input data + * + * @param array $data + * @return array + */ + private function buildCheckoutPayload(array $data): array + { + $payload = [ + 'amount' => $this->formatAmount($data['amount']), + 'currency' => $data['currency'] ?? 'XOF', + 'error_url' => $data['error_url'], + 'success_url' => $data['success_url'], + ]; + + // Optional fields + if (isset($data['client_reference'])) { + $payload['client_reference'] = substr($data['client_reference'], 0, 255); + } + + if (isset($data['restrict_payer_mobile'])) { + $payload['restrict_payer_mobile'] = $this->formatPhoneNumber($data['restrict_payer_mobile']); + } + + if (isset($data['aggregated_merchant_id'])) { + $payload['aggregated_merchant_id'] = $data['aggregated_merchant_id']; + } + + return $payload; + } + + /** + * Format amount according to Wave API requirements + * + * @param mixed $amount + * @return string + */ + private function formatAmount($amount): string + { + // Convert to string and ensure proper decimal formatting + $formatted = (string) $amount; + + // Remove any leading zeros (except for values < 1) + if (floatval($formatted) >= 1) { + $formatted = ltrim($formatted, '0'); + } + + // Ensure max 2 decimal places + if (strpos($formatted, '.') !== false) { + $parts = explode('.', $formatted); + if (strlen($parts[1]) > 2) { + $formatted = $parts[0] . '.' . substr($parts[1], 0, 2); + } + } + + return $formatted; + } + + /** + * Format phone number to E.164 standard + * + * @param string $phone + * @return string + */ + private function formatPhoneNumber(string $phone): string + { + // If already starts with +, return as is + if (strpos($phone, '+') === 0) { + return $phone; + } + + // Add country code if missing (default to Ivory Coast +225) + if (strpos($phone, '225') !== 0) { + $phone = '225' . ltrim($phone, '0'); + } + + return '+' . $phone; + } + + /** + * Handle client exceptions from Wave API + * + * @param \GuzzleHttp\Exception\ClientException $e + * @throws PaymentRequestException + * @return never + */ + private function handleClientException(\GuzzleHttp\Exception\ClientException $e): never + { + $response = $e->getResponse(); + $statusCode = $response->getStatusCode(); + $body = $response->getBody()->getContents(); + + $errorData = json_decode($body, true); + $errorMessage = $errorData['error_message'] ?? $e->getMessage(); + $errorCode = $errorData['error_code'] ?? 'unknown'; + + $message = match ($statusCode) { + 400 => "Bad request: {$errorMessage} (Code: {$errorCode})", + 401 => "Authentication failed: {$errorMessage}", + 403 => "Access forbidden: {$errorMessage}", + 404 => "Checkout session not found: {$errorMessage}", + 409 => "Conflict: {$errorMessage}", + default => "Wave API error ({$statusCode}): {$errorMessage}", + }; + + throw new PaymentRequestException($message, $statusCode, $e); + } +} diff --git a/src/IvoryCost/Wave/WaveGateway.php b/src/IvoryCost/Wave/WaveGateway.php index b5ce036..c2ed756 100644 --- a/src/IvoryCost/Wave/WaveGateway.php +++ b/src/IvoryCost/Wave/WaveGateway.php @@ -4,10 +4,12 @@ use Bow\Payment\Common\ProcessorGatewayInterface; use Bow\Payment\Exceptions\PaymentRequestException; +use Bow\Payment\Exceptions\ConfigurationException; /** * Wave Gateway - * Note: This is a placeholder implementation pending official Wave API documentation + * Implementation of Wave Checkout API + * @link https://docs.wave.com/checkout */ class WaveGateway implements ProcessorGatewayInterface { @@ -16,34 +18,129 @@ class WaveGateway implements ProcessorGatewayInterface * * @var array */ - private $config; + private array $config; + + /** + * Wave API client + * + * @var WaveClient + */ + private WaveClient $client; /** * Create a new Wave gateway * * @param array $config + * @throws ConfigurationException */ public function __construct(array $config) { $this->config = $config; + $this->validateConfig(); + $this->client = new WaveClient($this->config['api_key']); } /** - * Make payment + * Make payment - Create a Wave checkout session * * @param mixed ...$args - * @return mixed + * @return array * @throws PaymentRequestException + * + * Expected parameters: + * - amount: (required) Amount to collect + * - notify_url: (required) Redirect URL on notification + * - success_url: (required) Redirect URL on success + * - error_url: (required) Redirect URL on error + * - currency: (optional) Currency code (default: XOF) + * - client_reference: (optional) Your unique reference (max 255 chars) + * - restrict_payer_mobile: (optional) Phone number (E.164 format) + * - aggregated_merchant_id: (optional) For aggregators only + * - idempotency_key: (optional) Unique key to prevent duplicate payments (auto-generated if not provided) */ public function payment(...$args) { - throw new PaymentRequestException( - 'Wave payment gateway is not yet implemented. Implementation pending official API documentation.' + // Validate required fields + $this->validatePaymentData($args); + + // Generate idempotency key if not provided (prevents duplicate payments) + $idempotencyKey = $args['idempotency_key'] ?? $this->generateIdempotencyKey(); + + // Create checkout session + $session = $this->client->createCheckoutSession( + [ + 'amount' => $args['amount'], + 'currency' => $args['currency'] ?? 'XOF', + 'notify_url' => $args['notify_url'], + 'success_url' => $args['success_url'], + 'error_url' => $args['error_url'], + 'client_reference' => $args['client_reference'] ?? null, + 'restrict_payer_mobile' => $args['restrict_payer_mobile'] ?? null, + 'aggregated_merchant_id' => $args['aggregated_merchant_id'] ?? null, + ], + $idempotencyKey ); + + return [ + 'success' => true, + 'session_id' => $session->getId(), + 'wave_launch_url' => $session->getWaveLaunchUrl(), + 'amount' => $session->getAmount(), + 'currency' => $args['currency'] ?? 'XOF', + 'checkout_status' => $session->getCheckoutStatus(), + 'payment_status' => $session->getPaymentStatus(), + 'transaction_id' => $session->getTransactionId(), + 'client_reference' => $session->getClientReference(), + 'when_expires' => $session->toArray()['when_expires'], + 'idempotency_key' => $idempotencyKey, + 'session' => $session, + ]; } /** - * Make transfer + * Verify payment - Retrieve checkout session status + * + * @param mixed ...$args + * @return WavePaymentStatus + * @throws PaymentRequestException + * + * Expected parameters: + * - session_id: (optional) Checkout session ID + * - transaction_id: (optional) Transaction ID + * - client_reference: (optional) Your unique reference + * + * Note: Provide at least one of the above identifiers + */ + public function verify(...$args) + { + $session = null; + + // Try to retrieve by session ID + if (isset($args['session_id'])) { + $session = $this->client->retrieveCheckoutSession($args['session_id']); + } + // Try to retrieve by transaction ID + elseif (isset($args['transaction_id'])) { + $session = $this->client->retrieveCheckoutByTransactionId($args['transaction_id']); + } + // Try to search by client reference + elseif (isset($args['client_reference'])) { + $sessions = $this->client->searchCheckoutSessions($args['client_reference']); + if (empty($sessions)) { + throw new PaymentRequestException('No checkout session found with the provided client reference'); + } + $session = $sessions[0]; // Get the first matching session + } else { + throw new PaymentRequestException( + 'Please provide one of: session_id, transaction_id, or client_reference' + ); + } + + return new WavePaymentStatus($session); + } + + /** + * Make transfer - Not supported by Wave Checkout API * * @param mixed ...$args * @return mixed @@ -52,12 +149,12 @@ public function payment(...$args) public function transfer(...$args) { throw new PaymentRequestException( - 'Wave transfer is not yet implemented.' + 'Wave transfer is not supported by the Checkout API. Use Wave Business API instead.' ); } /** - * Get balance + * Get balance - Not supported by Wave Checkout API * * @param mixed ...$args * @return mixed @@ -66,21 +163,128 @@ public function transfer(...$args) public function balance(...$args) { throw new PaymentRequestException( - 'Wave balance inquiry is not yet implemented.' + 'Wave balance inquiry is not supported by the Checkout API. Use Wave Business API instead.' ); } /** - * Verify payment + * Refund a checkout session * - * @param mixed ...$args - * @return mixed + * @param string $sessionId + * @return bool * @throws PaymentRequestException */ - public function verify(...$args) + public function refund(string $sessionId): bool { - throw new PaymentRequestException( - 'Wave payment verification is not yet implemented.' - ); + return $this->client->refundCheckoutSession($sessionId); + } + + /** + * Expire a checkout session + * + * @param string $sessionId + * @return bool + * @throws PaymentRequestException + */ + public function expire(string $sessionId): bool + { + return $this->client->expireCheckoutSession($sessionId); + } + + /** + * Search for checkout sessions by client reference + * + * @param string $clientReference + * @return array + * @throws PaymentRequestException + */ + public function search(string $clientReference): array + { + return $this->client->searchCheckoutSessions($clientReference); + } + + /** + * Generate a unique idempotency key + * Uses UUID v4 format to ensure uniqueness and prevent duplicate payments + * + * @return string + */ + private function generateIdempotencyKey(): string + { + // Generate UUID v4 format + $data = random_bytes(16); + $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // Set version to 0100 + $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // Set bits 6-7 to 10 + + return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); + } + + /** + * Validate configuration + * + * @throws ConfigurationException + */ + private function validateConfig(): void + { + if (empty($this->config['api_key'])) { + throw new ConfigurationException( + 'Wave API key is required. Please set "api_key" in your Wave configuration.' + ); + } + + // Validate API key format + if (!str_starts_with($this->config['api_key'], 'wave_')) { + throw new ConfigurationException( + 'Invalid Wave API key format. API key should start with "wave_".' + ); + } + } + + /** + * Validate payment data + * + * @param array $data + * @throws PaymentRequestException + */ + private function validatePaymentData(array $data): void + { + if (!isset($data['amount']) || empty($data['amount'])) { + throw new PaymentRequestException('Amount is required for Wave payment'); + } + + if (!isset($data['success_url']) || empty($data['success_url'])) { + throw new PaymentRequestException('Success URL is required for Wave payment'); + } + + if (!isset($data['error_url']) || empty($data['error_url'])) { + throw new PaymentRequestException('Error URL is required for Wave payment'); + } + + // Validate URLs are HTTPS + if (!str_starts_with($data['success_url'], 'https://')) { + throw new PaymentRequestException('Success URL must use HTTPS protocol'); + } + + if (!str_starts_with($data['error_url'], 'https://')) { + throw new PaymentRequestException('Error URL must use HTTPS protocol'); + } + + // Validate amount is positive + if (floatval($data['amount']) <= 0) { + throw new PaymentRequestException('Amount must be greater than zero'); + } + + // Validate currency if provided + if (isset($data['currency'])) { + $currency = strtoupper($data['currency']); + if ($currency === 'XOF') { + // XOF doesn't allow decimals + if (strpos((string) $data['amount'], '.') !== false) { + throw new PaymentRequestException( + 'XOF currency does not allow decimal places. Amount must be a whole number.' + ); + } + } + } } } diff --git a/src/IvoryCost/Wave/WavePaymentStatus.php b/src/IvoryCost/Wave/WavePaymentStatus.php new file mode 100644 index 0000000..cd417cf --- /dev/null +++ b/src/IvoryCost/Wave/WavePaymentStatus.php @@ -0,0 +1,249 @@ +session = $session; + } + + /** + * Check if payment is successful + * + * @return bool + */ + public function isSuccess(): bool + { + return $this->session->isComplete() && $this->session->isPaymentSucceeded(); + } + + /** + * Check if payment is pending + * + * @return bool + */ + public function isPending(): bool + { + return $this->session->isOpen() || $this->session->isPaymentProcessing(); + } + + /** + * Check if payment failed + * + * @return bool + */ + public function isFail(): bool + { + return $this->session->isPaymentCancelled() || + ($this->session->isExpired() && !$this->session->isPaymentSucceeded()); + } + + /** + * Check if payment failed (alias for isFail) + * + * @return bool + */ + public function isFailed(): bool + { + return $this->isFail(); + } + + /** + * Check if transaction is initiated + * + * @return bool + */ + public function isInitiated(): bool + { + return $this->session->isOpen(); + } + + /** + * Check if transaction is expired + * + * @return bool + */ + public function isExpired(): bool + { + return $this->session->isExpired(); + } + + /** + * Get payment status string + * + * @return string + */ + public function getStatus(): string + { + if ($this->isSuccess()) { + return 'success'; + } + + if ($this->isPending()) { + return 'pending'; + } + + if ($this->isFail()) { + return 'failed'; + } + + return 'unknown'; + } + + /** + * Get transaction ID + * + * @return string|null + */ + public function getTransactionId(): ?string + { + return $this->session->getTransactionId(); + } + + /** + * Get checkout session ID + * + * @return string + */ + public function getSessionId(): string + { + return $this->session->getId(); + } + + /** + * Get amount + * + * @return string + */ + public function getAmount(): string + { + return $this->session->getAmount(); + } + + /** + * Get checkout status + * + * @return string + */ + public function getCheckoutStatus(): string + { + return $this->session->getCheckoutStatus(); + } + + /** + * Get payment status + * + * @return string + */ + public function getPaymentStatus(): string + { + return $this->session->getPaymentStatus(); + } + + /** + * Get client reference + * + * @return string|null + */ + public function getClientReference(): ?string + { + return $this->session->getClientReference(); + } + + /** + * Get error details if payment failed + * + * @return array|null + */ + public function getError(): ?array + { + return $this->session->getLastPaymentError(); + } + + /** + * Get error message + * + * @return string|null + */ + public function getErrorMessage(): ?string + { + $error = $this->session->getLastPaymentError(); + return $error['message'] ?? null; + } + + /** + * Get error code + * + * @return string|null + */ + public function getErrorCode(): ?string + { + $error = $this->session->getLastPaymentError(); + return $error['code'] ?? null; + } + + /** + * Get the underlying checkout session + * + * @return WaveCheckoutSession + */ + public function getSession(): WaveCheckoutSession + { + return $this->session; + } + + /** + * Convert to array + * + * @return array + */ + public function toArray(): array + { + return [ + 'status' => $this->getStatus(), + 'is_success' => $this->isSuccess(), + 'is_pending' => $this->isPending(), + 'is_failed' => $this->isFail(), + 'is_expired' => $this->isExpired(), + 'is_initiated' => $this->isInitiated(), + 'session_id' => $this->getSessionId(), + 'transaction_id' => $this->getTransactionId(), + 'amount' => $this->getAmount(), + 'checkout_status' => $this->getCheckoutStatus(), + 'payment_status' => $this->getPaymentStatus(), + 'client_reference' => $this->getClientReference(), + 'error' => $this->getError(), + 'session' => $this->session->toArray(), + ]; + } + + /** + * String representation + * + * @return string + */ + public function __toString(): string + { + return json_encode($this->toArray()); + } +}