brokeyourbike/fcmb-api-client-php

View on GitHub
src/Client.php

Summary

Maintainability
A
2 hrs
Test Coverage
A
100%
<?php

// Copyright (C) 2021 Ivan Stasiuk <ivan@stasi.uk>.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

namespace BrokeYourBike\FirstCityMonumentBank;

use Psr\SimpleCache\CacheInterface;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\ClientInterface;
use BrokeYourBike\ResolveUri\ResolveUriTrait;
use BrokeYourBike\HttpEnums\HttpMethodEnum;
use BrokeYourBike\HttpClient\HttpClientTrait;
use BrokeYourBike\HttpClient\HttpClientInterface;
use BrokeYourBike\HasSourceModel\SourceModelInterface;
use BrokeYourBike\HasSourceModel\HasSourceModelTrait;
use BrokeYourBike\FirstCityMonumentBank\Models\ValidateRecipientResponse;
use BrokeYourBike\FirstCityMonumentBank\Models\PayoutTransactionResponse;
use BrokeYourBike\FirstCityMonumentBank\Models\FetchTransactionStatusResponse;
use BrokeYourBike\FirstCityMonumentBank\Models\FetchAuthTokenResponse;
use BrokeYourBike\FirstCityMonumentBank\Models\CancelTransactionResponse;
use BrokeYourBike\FirstCityMonumentBank\Interfaces\TransactionInterface;
use BrokeYourBike\FirstCityMonumentBank\Interfaces\SenderInterface;
use BrokeYourBike\FirstCityMonumentBank\Interfaces\RecipientInterface;
use BrokeYourBike\FirstCityMonumentBank\Interfaces\ConfigInterface;
use BrokeYourBike\FirstCityMonumentBank\Exceptions\PrepareRequestException;

/**
 * @author Ivan Stasiuk <ivan@stasi.uk>
 */
class Client implements HttpClientInterface
{
    use HttpClientTrait;
    use ResolveUriTrait;
    use HasSourceModelTrait;

    private ConfigInterface $config;
    private CacheInterface $cache;
    private int $ttlMarginInSeconds = 60;

    public function __construct(ConfigInterface $config, ClientInterface $httpClient, CacheInterface $cache)
    {
        $this->config = $config;
        $this->httpClient = $httpClient;
        $this->cache = $cache;
    }

    public function getConfig(): ConfigInterface
    {
        return $this->config;
    }

    public function getCache(): CacheInterface
    {
        return $this->cache;
    }

    public function authTokenCacheKey(): string
    {
        return get_class($this) . ':authToken:';
    }

    public function getAuthToken(): string
    {
        if ($this->cache->has($this->authTokenCacheKey())) {
            $cachedToken = $this->cache->get($this->authTokenCacheKey());

            if (is_string($cachedToken)) {
                return $cachedToken;
            }
        }

        $response = $this->fetchAuthTokenRaw();

        $this->cache->set(
            $this->authTokenCacheKey(),
            $response->accessToken,
            $response->expiresIn - $this->ttlMarginInSeconds
        );

        return $response->accessToken;
    }

    public function fetchAuthTokenRaw(): FetchAuthTokenResponse
    {
        $options = [
            \GuzzleHttp\RequestOptions::HEADERS => [
                'Accept' => 'application/json',
            ],
            \GuzzleHttp\RequestOptions::FORM_PARAMS => [
                'grant_type' => 'client_credentials',
                'client_id' => $this->config->getClientId(),
                'client_secret' => $this->config->getClientSecret(),
            ],
        ];

        $uri = (string) $this->resolveUriFor($this->config->getUrl(), 'auth');

        $response = $this->httpClient->request(
            HttpMethodEnum::POST->value,
            $uri,
            $options
        );

        return new FetchAuthTokenResponse($response);
    }

    public function validateRecipient(RecipientInterface $recipient): ValidateRecipientResponse
    {
        if ($recipient instanceof SourceModelInterface) {
            $this->setSourceModel($recipient);
        }

        $response = $this->performRequest(HttpMethodEnum::POST, 'customer/validate', [
            'publickey' => $this->config->getClientId(),
            'source' => [
                'operation' => 'account_enquiry',
                'recipient' => [
                    'accountnumber' => $recipient->getAccountNumber(),
                    'bankcode' => $recipient->getBankCode(),
                    'mobile' => $recipient->getPhoneNumber(),
                    'name' => $recipient->getName(),
                    'address' => $recipient->getAddress(),
                ],
            ],
            'order' => [
                'country' => $recipient->getCountryCode(),
            ],
        ]);

        return new ValidateRecipientResponse($response);
    }

    public function fetchTransactionStatus(TransactionInterface $transaction): FetchTransactionStatusResponse
    {
        if ($transaction instanceof SourceModelInterface) {
            $this->setSourceModel($transaction);
        }

        return $this->fetchTransactionStatusRaw($transaction->getReference());
    }

    public function fetchTransactionStatusRaw(string $reference): FetchTransactionStatusResponse
    {
        $response = $this->performRequest(HttpMethodEnum::GET, 'payout/status', [
            'reference' => $reference,
        ]);

        return new FetchTransactionStatusResponse($response);
    }

    public function cancelTransaction(TransactionInterface $transaction): CancelTransactionResponse
    {
        if ($transaction instanceof SourceModelInterface) {
            $this->setSourceModel($transaction);
        }

        return $this->cancelTransactionRaw($transaction->getReference());
    }

    public function cancelTransactionRaw(string $reference): CancelTransactionResponse
    {
        $response = $this->performRequest(HttpMethodEnum::POST, 'payout/cancel', [
            'publickey' => $this->config->getClientId(),
            'transaction' => [
                'reference' => $reference,
            ],
        ]);

        return new CancelTransactionResponse($response);
    }

    public function payoutTransaction(TransactionInterface $transaction): PayoutTransactionResponse
    {
        $sender = $transaction->getSender();
        $recipient = $transaction->getRecipient();

        if (!$sender instanceof SenderInterface) {
            throw PrepareRequestException::noSender($transaction);
        }

        if (!$recipient instanceof RecipientInterface) {
            throw PrepareRequestException::noRecipient($transaction);
        }

        if ($transaction instanceof SourceModelInterface) {
            $this->setSourceModel($transaction);
        }

        $senderIdExpiry = $sender->getIdentificationExpiry() !== null
            ? $sender->getIdentificationExpiry()->format('Y-m-d')
            : null;

        $recipientIdExpiry = $recipient->getIdentificationExpiry() !== null
            ? $recipient->getIdentificationExpiry()->format('Y-m-d')
            : null;

        $data = [
            'publickey' => $this->config->getClientId(),
            'transaction' => [
                'reference' => $transaction->getReference(),
            ],
            'source' => [
                'operation' => $transaction->getTransactionType()->value,
                'sender' => [
                    'name' => $sender->getName(),
                    'address' => $sender->getAddress(),
                    'mobile' => $sender->getPhoneNumber(),
                    'country' => $sender->getCountryCode(),
                    'idtype' => $sender->getIdentificationType()?->value,
                    'idnumber' => $sender->getIdentificationNumber(),
                    'idexpiry' => $senderIdExpiry,
                ],
                'recipient' => [
                    'name' => $recipient->getName(),
                    'address' => $recipient->getAddress(),
                    'mobile' => $recipient->getPhoneNumber(),
                    'country' => $recipient->getCountryCode(),
                    'idtype' => $recipient->getIdentificationType()?->value,
                    'idnumber' => $recipient->getIdentificationNumber(),
                    'idexpiry' => $recipientIdExpiry,
                    'accountnumber' => $recipient->getAccountNumber(),
                    'bankcode' => $recipient->getBankCode(),
                ],
            ],
            'order' => [
                'amount' => (string) $transaction->getAmount(),
                'country' => $transaction->getCountryCode(),
                'currency' => $transaction->getCurrencyCode(),
                'reason' => (string) $transaction->getReason(),
                'description' => (string) $transaction->getDescription(),
            ],
        ];

        if ($transaction->getSecretQuestion() && $transaction->getSecretAnswer()) {
            $data['order']['secretquestion'] = $transaction->getSecretQuestion();
            $data['order']['secretanswer'] = $transaction->getSecretAnswer();
        }

        $response = $this->performRequest(HttpMethodEnum::POST, 'account/payout', $data);
        return new PayoutTransactionResponse($response);
    }

    /**
     * @param HttpMethodEnum $method
     * @param string $uri
     * @param array<mixed> $data
     * @return ResponseInterface
     */
    private function performRequest(HttpMethodEnum $method, string $uri, array $data): ResponseInterface
    {
        $options = [
            \GuzzleHttp\RequestOptions::HEADERS => [
                'Accept' => 'application/json',
                'Authorization' => 'Bearer ' . (string) $this->getAuthToken(),
            ],
        ];

        $option = match ($method) {
            HttpMethodEnum::GET => \GuzzleHttp\RequestOptions::QUERY,
            default => \GuzzleHttp\RequestOptions::JSON,
        };

        $options[$option] = $data;

        if ($this->getSourceModel()) {
            $options[\BrokeYourBike\HasSourceModel\Enums\RequestOptions::SOURCE_MODEL] = $this->getSourceModel();
        }

        $uri = (string) $this->resolveUriFor($this->config->getUrl(), $uri);
        return $this->httpClient->request($method->value, $uri, $options);
    }
}