Covivo/mobicoop

View on GitHub
api/src/DataProvider/Entity/MangoPayProvider.php

Summary

Maintainability
D
2 days
Test Coverage
<?php

/**
 * Copyright (c) 2020, MOBICOOP. All rights reserved.
 * This project is dual licensed under AGPL and proprietary licence.
 ***************************
 *    This program is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU Affero General Public License as
 *    published by the Free Software Foundation, either version 3 of the
 *    License, or (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <gnu.org/licenses>.
 ***************************
 *    Licence MOBICOOP described in the file
 *    LICENSE
 */

namespace App\DataProvider\Entity;

use App\DataProvider\Ressource\Hook;
use App\DataProvider\Ressource\MangoPayHook;
use App\DataProvider\Service\CurlDataProvider;
use App\DataProvider\Service\DataProvider;
use App\Geography\Entity\Address;
use App\Payment\Entity\CarpoolItem;
use App\Payment\Entity\CarpoolPayment;
use App\Payment\Entity\PaymentProfile;
use App\Payment\Entity\PaymentResult;
use App\Payment\Entity\Wallet;
use App\Payment\Entity\WalletBalance;
use App\Payment\Exception\PaymentException;
use App\Payment\Interfaces\PaymentProviderInterface;
use App\Payment\Repository\PaymentProfileRepository;
use App\Payment\Ressource\BankAccount;
use App\Payment\Ressource\ValidationDocument;
use App\User\Entity\User;

/**
 * Payment Provider for MangoPay.
 *
 * @author Maxime Bardot <maxime.bardot@mobicoop.org>
 * @author Remi Wortemann <remi.wortemann@mobicoop.org>
 */
class MangoPayProvider implements PaymentProviderInterface
{
    public const SERVER_URL_SANDBOX = 'https://api.sandbox.mangopay.com/';
    public const SERVER_URL = 'https://api.mangopay.com/';
    public const LANDING_AFTER_PAYMENT = 'paiements/paye';
    public const LANDING_AFTER_PAYMENT_MOBILE = '#/carpools/payment/paye';
    public const LANDING_AFTER_PAYMENT_MOBILE_SITE = '#/carpools/payment/paye';
    public const VERSION = 'V2.01';
    public const ACCESS_TOKEN_EXPIRATION_SECURITY_MARGIN_IN_SECONDS = 10;

    public const AUTH_TOKEN = 'oauth/token';

    public const COLLECTION_BANK_ACCOUNTS = 'bankaccounts';
    public const COLLECTION_WALLETS = 'wallets';

    public const ITEM_USER_NATURAL = 'natural';
    public const ITEM_WALLET = 'wallets';
    public const ITEM_PAYIN = 'payins/card/web';
    public const ITEM_TRANSFERS = 'transfers';
    public const ITEM_PAYOUT = 'payouts/bankwire';

    public const ITEM_KYC_CREATE_DOC = 'users/{userId}/KYC/documents/';
    public const ITEM_KYC_CREATE_PAGE = 'users/{userId}/KYC/documents/{KYCDocId}/pages';
    public const ITEM_KYC_PUT_DOC = 'users/{userId}/KYC/documents/{KYCDocId}';

    public const CARD_TYPE = 'CB_VISA_MASTERCARD';
    public const LANGUAGE = 'FR';
    public const VALIDATION_DOC_TYPE = 'IDENTITY_PROOF';
    public const VALIDATION_ASKED = 'VALIDATION_ASKED';

    public const OUT_OF_DATE = 'OUT_OF_DATE';
    public const UNDERAGE_PERSON = 'UNDERAGE_PERSON';
    public const DOCUMENT_FALSIFIED = 'DOCUMENT_FALSIFIED';
    public const DOCUMENT_MISSING = 'DOCUMENT_MISSING';
    public const DOCUMENT_HAS_EXPIRED = 'DOCUMENT_HAS_EXPIRED';
    public const DOCUMENT_NOT_ACCEPTED = 'DOCUMENT_NOT_ACCEPTED';
    public const DOCUMENT_DO_NOT_MATCH_USER_DATA = 'DOCUMENT_DO_NOT_MATCH_USER_DATA';
    public const DOCUMENT_UNREADABLE = 'DOCUMENT_UNREADABLE';
    public const DOCUMENT_INCOMPLETE = 'DOCUMENT_INCOMPLETE';
    public const SPECIFIC_CASE = 'SPECIFIC_CASE';

    public const RESULT_SUCCESS = 'SUCCESS';
    public const RESULT_FAILED = 'FAILED';
    public const RESULT_TYPE_TRANSFER = 'TRANSFER';
    public const RESULT_TYPE_PAYOUT = 'PAYOUT';

    private $user;
    private $clientId;
    private $apikey;
    private $serverUrl;
    private $serverUrlNoClientId;
    private $authChain;
    private $currency;
    private $paymentProfileRepository;
    private $validationDocsPath;
    private $baseUri;
    private $baseMobileUri;
    private $curlDataProvider;
    private $access_token;
    private $accessTokenExpireIn;
    private $startTime;

    public function __construct(
        ?User $user,
        string $clientId,
        string $apikey,
        bool $sandBoxMode,
        string $currency,
        string $validationDocsPath,
        string $baseUri,
        string $baseMobileUri,
        PaymentProfileRepository $paymentProfileRepository
    ) {
        ($sandBoxMode) ? $this->serverUrl = self::SERVER_URL_SANDBOX : $this->serverUrl = self::SERVER_URL;
        $this->serverUrlNoClientId = $this->serverUrl;
        $this->user = $user;
        $this->clientId = $clientId;
        $this->apikey = $apikey;
        $this->serverUrl .= self::VERSION.'/'.$clientId.'/';
        $this->serverUrlNoClientId .= self::VERSION.'/';
        $this->currency = $currency;
        $this->paymentProfileRepository = $paymentProfileRepository;
        $this->validationDocsPath = $validationDocsPath;
        $this->baseUri = $baseUri;
        $this->baseMobileUri = $baseMobileUri;
        $this->curlDataProvider = new CurlDataProvider();
        $this->access_token = null;
        $this->accessTokenExpireIn = null;
        $this->startTime = new \DateTime('now');
    }

    private function __treatReturn(User $debtor, array $creditor, string $result): PaymentResult
    {
        $return = new PaymentResult();
        $arrayResult = json_decode($result, true);
        $return->setDebtorId($debtor->getId());
        $return->setCreditorId($creditor['user']->getId());
        if (isset($creditor['carpoolItemId'])) {
            $return->setCarpoolItemId($creditor['carpoolItemId']);
        }

        if (isset($arrayResult['Type']) && self::RESULT_TYPE_TRANSFER == $arrayResult['Type']) {
            $return->setType(PaymentResult::RESULT_ONLINE_PAYMENT_TYPE_TRANSFER);
        }
        if (isset($arrayResult['Type']) && self::RESULT_TYPE_PAYOUT == $arrayResult['Type']) {
            $return->setType(PaymentResult::RESULT_ONLINE_PAYMENT_TYPE_PAYOUT);
        }

        if (isset($arrayResult['Status']) && self::RESULT_FAILED == $arrayResult['Status']) {
            $return->setStatus(PaymentResult::RESULT_ONLINE_PAYMENT_STATUS_FAILED);
        } else {
            $return->setStatus(PaymentResult::RESULT_ONLINE_PAYMENT_STATUS_SUCCESS);
        }

        return $return;
    }

    /**
     * Returns a collection of Bank accounts.
     *
     * @param PaymentProfile $paymentProfile The User's payment profile related to the Bank accounts
     * @param bool           $onlyActive     By default, only the active bank accounts are returned
     *
     * @return BankAccount[]
     */
    public function getBankAccounts(PaymentProfile $paymentProfile, bool $onlyActive = true)
    {
        $this->_auth();

        $dataProvider = new DataProvider($this->serverUrl.'users/'.$paymentProfile->getIdentifier().'/', self::COLLECTION_BANK_ACCOUNTS);
        $getParams = [
            'per_page' => 100,
            'sort' => 'creationdate:desc',
        ];

        if ($onlyActive) {
            $getParams['Active'] = 'true';
        }

        $headers = [
            'Authorization' => $this->authChain,
        ];
        $response = $dataProvider->getCollection($getParams, $headers);

        $bankAccounts = [];
        if (200 == $response->getCode()) {
            $data = json_decode($response->getValue(), true);
            foreach ($data as $account) {
                $bankAccount = $this->deserializeBankAccount($account);
                $bankAccount->setValidationAskedDate($paymentProfile->getValidationAskedDate());
                $bankAccount->setValidatedDate($paymentProfile->getValidatedDate());
                $bankAccount->setValidationOutdatedDate($paymentProfile->getValidationOutdatedDate());
                $bankAccounts[] = $bankAccount;
            }
        }

        return $bankAccounts;
    }

    /**
     * Add a BankAccount.
     *
     * @param BankAccount $bankAccount The BankAccount to create
     *
     * @return null|BankAccount
     */
    public function addBankAccount(BankAccount $bankAccount)
    {
        // Build the body
        $body['OwnerName'] = $this->user->getGivenName().' '.$this->user->getFamilyName();
        $body['IBAN'] = $bankAccount->getIban();
        if (!null == $bankAccount->getBic()) {
            $body['BIC'] = $bankAccount->getBic();
        }

        $street = '';
        if ('' != $bankAccount->getAddress()->getStreetAddress()) {
            $street = $bankAccount->getAddress()->getStreetAddress();
        } else {
            $street = trim($bankAccount->getAddress()->getHouseNumber().' '.$bankAccount->getAddress()->getStreet());
        }

        $body['OwnerAddress'] = [
            'AddressLine1' => $street,
            'City' => $bankAccount->getAddress()->getAddressLocality(),
            'Region' => $bankAccount->getAddress()->getRegion(),
            'PostalCode' => $bankAccount->getAddress()->getPostalCode(),
            'Country' => substr($bankAccount->getAddress()->getCountryCode(), 0, 2),
        ];

        // Get the identifier
        $paymentProfiles = $this->paymentProfileRepository->findBy(['user' => $this->user]);
        $identifier = $paymentProfiles[0]->getIdentifier();

        if (is_null($identifier)) {
            throw new PaymentException(PaymentException::NO_IDENTIFIER);
        }

        $this->_auth();
        $dataProvider = new DataProvider($this->serverUrl.'users/'.$identifier.'/', self::COLLECTION_BANK_ACCOUNTS.'/iban');
        $headers = [
            'Authorization' => $this->authChain,
        ];
        $response = $dataProvider->postCollection($body, $headers);

        if (200 == $response->getCode()) {
            $data = json_decode($response->getValue(), true);
            $bankAccount = $this->deserializeBankAccount($data);
        } else {
            throw new PaymentException(PaymentException::ERROR_CREATING);
        }

        return $bankAccount;
    }

    /**
     * Disable a BankAccount (Only IBAN/BIC and active/inactive).
     *
     * @param BankAccount $bankAccount The BankAccount to disable
     *
     * @return null|BankAccount
     */
    public function disableBankAccount(BankAccount $bankAccount)
    {
        // Build the body
        $body['Active'] = 'false';

        // Get the identifier
        $paymentProfiles = $this->paymentProfileRepository->findBy(['user' => $this->user]);
        $identifier = $paymentProfiles[0]->getIdentifier();

        if (is_null($identifier)) {
            throw new PaymentException(PaymentException::NO_IDENTIFIER);
        }

        $this->_auth();
        $dataProvider = new DataProvider($this->serverUrl.'users/'.$identifier.'/', self::COLLECTION_BANK_ACCOUNTS.'/'.$bankAccount->getId());
        $headers = [
            'Authorization' => $this->authChain,
        ];
        $response = $dataProvider->putItem($body, $headers);

        if (200 == $response->getCode()) {
            $data = json_decode($response->getValue(), true);
            $bankAccount = $this->deserializeBankAccount($data);
        }

        return $bankAccount;
    }

    /**
     * Returns a collection of Wallet.
     *
     * @param PaymentProfile $paymentProfile The User's payment profile related to the wallets
     *
     * @return Wallet[]
     */
    public function getWallets(PaymentProfile $paymentProfile)
    {
        $this->_auth();

        $wallets = [new Wallet()];

        $dataProvider = new DataProvider($this->serverUrl.'users/'.$paymentProfile->getIdentifier().'/', self::COLLECTION_WALLETS);
        $getParams = null;
        $headers = [
            'Authorization' => $this->authChain,
        ];
        $response = $dataProvider->getCollection($getParams, $headers);

        $wallets = [];
        if (200 == $response->getCode()) {
            $data = json_decode($response->getValue(), true);
            foreach ($data as $wallet) {
                $wallet = $this->deserializeWallet($wallet);
                $wallet->setOwnerIdentifier($paymentProfile->getIdentifier());
                $wallets[] = $wallet;
            }
        }

        return $wallets;
    }

    /**
     * Add a Wallet.
     *
     * @return null|Wallet
     */
    public function addWallet(Wallet $wallet): Wallet
    {
        $this->_auth();

        // Build the body
        $body['Description'] = $wallet->getDescription();
        $body['Currency'] = $wallet->getCurrency();
        $body['Tag'] = $wallet->getComment();
        $body['Owners'] = [$wallet->getOwnerIdentifier()];

        $dataProvider = new DataProvider($this->serverUrl, self::ITEM_WALLET);
        $headers = [
            'Authorization' => $this->authChain,
        ];
        $response = $dataProvider->postCollection($body, $headers);

        $wallet = new Wallet();
        if (200 == $response->getCode()) {
            $data = json_decode($response->getValue(), true);
            $wallet->setId($data['Id']);
            $wallet->setDescription($data['Description']);
            $wallet->setOwnerIdentifier($data['Owners'][0]);
        } else {
            throw new PaymentException(PaymentException::ADD_WALLET_USER_FAILED);
        }

        return $wallet;
    }

    /**
     * Register a User to the provider and create a PaymentProfile.
     *
     * @param null|Address $address The address to use to the registration
     *
     * @return string The identifier
     */
    public function registerUser(User $user, ?Address $address = null)
    {
        // Build the body
        $body['FirstName'] = $user->getGivenName();
        $body['LastName'] = $user->getFamilyName();
        $body['Email'] = $user->getEmail();

        if (is_null($user->getBirthDate())) {
            throw new PaymentException(PaymentException::NO_BIRTHDATE);
        }
        $body['Birthday'] = (int) $user->getBirthDate()->format('U');

        if (is_null($address)) {
            // Address of the user
            foreach ($user->getAddresses() as $homeAddress) {
                if ($homeAddress->isHome()) {
                    $address = $homeAddress;

                    break;
                }
            }
        }

        if (!is_null($address)) {
            // the address of a user is optionnal we sent it only if it's a full address
            if (
                '' !== $address->getStreetAddress()
                && '' !== $address->getStreet()
                && '' !== $address->getAddressLocality()
                && '' !== $address->getRegion()
                && '' !== $address->getPostalCode()
                && '' !== $address->getCountryCode()
            ) {
                $street = '';
                if ('' != $address->getStreetAddress()) {
                    $street = $address->getStreetAddress();
                } else {
                    $street = trim($address->getHouseNumber().' '.$address->getStreet());
                }
                $body['Address'] = [
                    'AddressLine1' => $street,
                    'City' => $address->getAddressLocality(),
                    'Region' => $address->getRegion(),
                    'PostalCode' => $address->getPostalCode(),
                    'Country' => substr($address->getCountryCode(), 0, 2),
                ];
            }
            // the Nationality and the country of residence are required
            $body['Nationality'] = substr($address->getCountryCode(), 0, 2);
            $body['CountryOfResidence'] = substr($address->getCountryCode(), 0, 2);
        } else {
            throw new PaymentException(PaymentException::NO_ADDRESS);
        }

        $body['KYCLevel'] = 'LIGHT';
        $body['TermsAndConditionsAccepted'] = true;
        $body['UserCategory'] = 'OWNER';

        $this->_auth();
        $dataProvider = new DataProvider($this->serverUrl.'users/', self::ITEM_USER_NATURAL);
        $headers = [
            'Authorization' => $this->authChain,
        ];
        $response = $dataProvider->postCollection($body, $headers);

        if (200 == $response->getCode()) {
            $data = json_decode($response->getValue(), true);
        } else {
            throw new PaymentException(PaymentException::REGISTER_USER_FAILED);
        }

        return $data['Id'];
    }

    /**
     * Update a User to the provider and create a PaymentProfile.
     *
     * @return string The identifier
     */
    public function updateUser(User $user)
    {
        // We check first if the user have an identifier
        $paymentProfiles = $this->paymentProfileRepository->findBy(['user' => $this->user]);
        $identifier = $paymentProfiles[0]->getIdentifier();

        if (is_null($identifier)) {
            throw new PaymentException(PaymentException::NO_IDENTIFIER);
        }

        // Build the body
        $body['FirstName'] = $user->getGivenName();
        $body['LastName'] = $user->getFamilyName();
        $body['Email'] = $user->getEmail();

        if (is_null($user->getBirthDate())) {
            throw new PaymentException(PaymentException::NO_BIRTHDATE);
        }
        $body['Birthday'] = (int) $user->getBirthDate()->format('U');

        $body['KYCLevel'] = 'LIGHT';

        $this->_auth();
        $dataProvider = new DataProvider($this->serverUrl.'users/', self::ITEM_USER_NATURAL.'/'.$identifier);
        $headers = [
            'Authorization' => $this->authChain,
        ];
        $response = $dataProvider->putItem($body, $headers);

        if (200 == $response->getCode()) {
            $data = json_decode($response->getValue(), true);
        } else {
            throw new PaymentException(PaymentException::UPDATE_USER_FAILED);
        }

        return $data['Id'];
    }

    /**
     * Get a User to the provider.
     */
    public function getUser(string $identifier)
    {
        $this->_auth();

        $dataProvider = new DataProvider($this->serverUrl.'users/'.$identifier);
        $headers = [
            'Authorization' => $this->authChain,
        ];

        $response = $dataProvider->getCollection(null, $headers);

        if (200 == $response->getCode()) {
            $data = json_decode($response->getValue(), true);
        }

        return $data;
    }

    /**
     * Get the secured form's url for electronic payment.
     *
     * @return CarpoolPayment With redirectUrl filled
     */
    public function generateElectronicPaymentUrl(CarpoolPayment $carpoolPayment): CarpoolPayment
    {
        $user = $carpoolPayment->getUser();
        $paymentProfiles = $this->paymentProfileRepository->findBy(['user' => $user]);

        if (is_null($paymentProfiles) || 0 == count($paymentProfiles)) {
            // No active payment profile. The User need at least a Wallet to pay so we register him and create a Wallet
            $identifier = $this->registerUser($user);
            $wallet = new Wallet();
            $wallet->setComment('');
            $wallet->setCurrency($this->currency);
            $wallet->setDescription('From Mobicoop');
            $wallet->setOwnerIdentifier($identifier);
            $wallet = $this->addWallet($wallet);
            $carpoolPayment->setCreateCarpoolProfileIdentifier($identifier); // To persist the paymentProfile in PaymentManager
        } else {
            $identifier = $paymentProfiles[0]->getIdentifier();
            $wallet = $this->getWallets($paymentProfiles[0])[0];
        }

        $returnUrl = $this->baseUri.''.self::LANDING_AFTER_PAYMENT;
        if (CarpoolPayment::ORIGIN_MOBILE == $carpoolPayment->getOrigin()) {
            $returnUrl = $this->baseMobileUri.self::LANDING_AFTER_PAYMENT_MOBILE;
        } elseif (CarpoolPayment::ORIGIN_MOBILE_SITE == $carpoolPayment->getOrigin()) {
            $returnUrl = $this->baseMobileUri.self::LANDING_AFTER_PAYMENT_MOBILE;
        }

        $body = [
            'AuthorId' => $identifier,
            'DebitedFunds' => [
                'Currency' => $this->currency,
                'Amount' => (int) ($carpoolPayment->getAmountOnline() * 100),
            ],
            'Fees' => [
                'Currency' => $this->currency,
                'Amount' => 0,
            ],
            'CreditedWalletId' => $wallet->getId(),
            'ReturnURL' => $returnUrl.'?paymentPaymentId='.$carpoolPayment->getId(),
            'CardType' => self::CARD_TYPE,
            'Culture' => self::LANGUAGE,
        ];

        $this->_auth();
        $dataProvider = new DataProvider($this->serverUrl, self::ITEM_PAYIN);
        $headers = [
            'Authorization' => $this->authChain,
        ];
        $response = $dataProvider->postCollection($body, $headers);

        if (200 == $response->getCode()) {
            $data = json_decode($response->getValue(), true);
        } else {
            throw new PaymentException(PaymentException::GET_URL_PAYIN_FAILED);
        }

        $carpoolPayment->setTransactionid($data['Id']);
        $carpoolPayment->setRedirectUrl($data['RedirectURL']);
        $carpoolPayment->setTransactionPostData($carpoolPayment->getTransactionPostData().((!is_null($carpoolPayment->getTransactionPostData())) ? '|' : '').json_encode($body));

        return $carpoolPayment;
    }

    /**
     * Process an asynchronous electronic payment between the $debtor and the $creditors.
     *
     * array of creditors are like this :
     * $creditors = [
     *  "userId" => [
     *      "user" => User object
     *      "amount" => float
     *      "carpoolItemId => int
     *      "creditorStatus" => int
     *      "debtorStatus" => int
     *  ]
     * ]
     */
    public function processAsyncElectronicPayment(User $debtor, array $creditors): array
    {
        $return = [];

        // Get the wallet of the debtor and his identifier
        $debtorPaymentProfile = $this->paymentProfileRepository->find($debtor->getPaymentProfileId());

        // Transfer to the creditors wallets and payout
        foreach ($creditors as $creditor) {
            if (CarpoolItem::DEBTOR_STATUS_PENDING_ONLINE == $creditor['debtorStatus']) {
                $creditorWallet = $creditor['user']->getWallets()[0];
                $result = $this->transferWalletToWallet($debtorPaymentProfile->getIdentifier(), $debtorPaymentProfile->getWallets()[0], $creditorWallet, $creditor['amount']);
                $treatedResult = $this->__treatReturn($debtor, $creditor, $result);
                $return[] = $treatedResult;
            }

            if (CarpoolItem::DEBTOR_STATUS_ONLINE == $creditor['debtorStatus']) {
                // Do the payout to the default bank account
                $creditorWallet = $creditor['user']->getWallets()[0];
                $creditorPaymentProfile = $this->paymentProfileRepository->find($creditor['user']->getPaymentProfileId());
                $creditorBankAccount = $creditor['user']->getBankAccounts()[0];
                $result = $this->triggerPayout($creditorPaymentProfile->getIdentifier(), $creditorWallet, $creditorBankAccount, $creditor['amount']);
                $treatedResult = $this->__treatReturn($debtor, $creditor, $result);
                $return[] = $treatedResult;
            }
        }

        return $return;
    }

    /**
     * Process an electronic payment between the $debtor and the $creditors.
     *
     * array of creditors are like this :
     * $creditors = [
     *  "userId" => [
     *      "user" => User object
     *      "amount" => float
     *  ]
     * ]
     */
    public function processElectronicPayment(User $debtor, array $creditors): array
    {
        $return = [];

        // Get the wallet of the debtor and his identifier
        $debtorPaymentProfile = $this->paymentProfileRepository->find($debtor->getPaymentProfileId());

        // Transfer to the creditors wallets and payout
        foreach ($creditors as $creditor) {
            $creditorWallet = $creditor['user']->getWallets()[0];
            $return[] = $this->transferWalletToWallet($debtorPaymentProfile->getIdentifier(), $debtorPaymentProfile->getWallets()[0], $creditorWallet, $creditor['amount']);

            // Do the payout to the default bank account
            $creditorPaymentProfile = $this->paymentProfileRepository->find($creditor['user']->getPaymentProfileId());
            $creditorBankAccount = $creditor['user']->getBankAccounts()[0];
            $return[] = $this->triggerPayout($creditorPaymentProfile->getIdentifier(), $creditorWallet, $creditorBankAccount, $creditor['amount']);
        }

        return $return;
    }

    /**
     * Transfer founds bewteen two wallets.
     *
     * @param string $debtorIdentifier MangoPay's identifier of the debtor
     * @param Wallet $walletFrom       Wallet of the debtor
     * @param Wallet $walletTo         Wallet of the creditor
     * @param float  $amount           Amount of the transaction
     */
    public function transferWalletToWallet(string $debtorIdentifier, Wallet $walletFrom, Wallet $walletTo, float $amount, string $tag = ''): ?string
    {
        $body = [
            'AuthorId' => $debtorIdentifier,
            'DebitedFunds' => [
                'Currency' => $this->currency,
                'Amount' => (int) ($amount * 100),
            ],
            'Fees' => [
                'Currency' => $this->currency,
                'Amount' => 0,
            ],
            'DebitedWalletId' => $walletFrom->getId(),
            'CreditedWalletId' => $walletTo->getId(),
            'Tag' => $tag,
        ];

        $this->_auth();
        $dataProvider = new DataProvider($this->serverUrl, self::ITEM_TRANSFERS);
        $headers = [
            'Authorization' => $this->authChain,
        ];
        $response = $dataProvider->postCollection($body, $headers);

        if (200 == $response->getCode()) {
            return $response->getValue();
        }

        return null;
    }

    /**
     * Trigger a payout from a Wallet to a Bank Account.
     */
    public function triggerPayout(string $authorIdentifier, Wallet $wallet, BankAccount $bankAccount, float $amount, string $reference = ''): ?string
    {
        $body = [
            'AuthorId' => $authorIdentifier,
            'DebitedFunds' => [
                'Currency' => $this->currency,
                'Amount' => (int) ($amount * 100),
            ],
            'Fees' => [
                'Currency' => $this->currency,
                'Amount' => 0,
            ],
            'DebitedWalletId' => $wallet->getId(),
            'BankAccountId' => $bankAccount->getId(),
            'BankWireRef' => $reference,
        ];

        $this->_auth();
        $dataProvider = new DataProvider($this->serverUrl, self::ITEM_PAYOUT);
        $headers = [
            'Authorization' => $this->authChain,
        ];
        $response = $dataProvider->postCollection($body, $headers);

        if (200 == $response->getCode()) {
            return $response->getValue();
        }

        return null;
    }

    /**
     * Handle a payment web hook.
     *
     * @var Hook The mangopay hook
     *
     * @return Hook with status and transaction id
     */
    public function handleHook(Hook $hook): Hook
    {
        switch ($hook->getEventType()) {
            case MangoPayHook::PAYIN_SUCCEEDED:
            case MangoPayHook::VALIDATION_SUCCEEDED:
                $hook->setStatus(Hook::STATUS_SUCCESS);

                break;

            case MangoPayHook::VALIDATION_OUTDATED:
                $hook->setStatus(Hook::STATUS_OUTDATED_RESSOURCE);

                break;

            default:
                $hook->setStatus(Hook::STATUS_FAILED);
        }

        return $hook;
    }

    /**
     * Upload an identity validation document
     * The document is not stored on the platform. It has to be deleted.
     */
    public function uploadValidationDocument(ValidationDocument $validationDocument): ValidationDocument
    {
        $user = $validationDocument->getUser();
        $paymentProfiles = $this->paymentProfileRepository->findBy(['user' => $user]);
        if (is_null($paymentProfiles) || 0 == count($paymentProfiles)) {
            throw new PaymentException(PaymentException::CARPOOL_PAYMENT_NOT_FOUND);
        }
        $identifier = $paymentProfiles[0]->getIdentifier();

        // $fileContent = base64_encode(file_get_contents(self::VALIDATION_DOCUMENTS_PATH."".$validationDocument->getFileName()));

        // General header for all 3 requests
        $this->_auth();
        $headers = [
            'Authorization' => $this->authChain,
        ];

        // Creation of the doc
        $urlPost = str_replace('{userId}', $identifier, self::ITEM_KYC_CREATE_DOC);
        $body = [
            'Type' => self::VALIDATION_DOC_TYPE,
            'Tag' => 'Automatic',
        ];

        $dataProvider = new DataProvider($this->serverUrl, $urlPost);
        $response = $dataProvider->postCollection($body, $headers);
        if (200 == $response->getCode()) {
            $data = json_decode($response->getValue(), true);
            $docId = $data['Id'];
        } else {
            throw new PaymentException(PaymentException::ERROR_CREATING_DOC_TO_PROVIDER);
        }

        // Creation of pages
        $this->_uploadPage($docId, $identifier, $headers, $validationDocument->getFileName());

        if (!is_null($validationDocument->getFile2())) {
            $this->_uploadPage($docId, $identifier, $headers, $validationDocument->getFileName2());
        }

        // Asking validation
        $urlPost = str_replace('{KYCDocId}', $docId, str_replace('{userId}', $identifier, self::ITEM_KYC_PUT_DOC));

        $body = [
            'Status' => self::VALIDATION_ASKED,
        ];

        $dataProvider = new DataProvider($this->serverUrl, $urlPost);
        $response = $dataProvider->putItem($body, $headers);
        if (200 == $response->getCode()) {
            $data = json_decode($response->getValue(), true);
            if (self::VALIDATION_ASKED !== $data['Status']) {
                throw new PaymentException(PaymentException::ERROR_VALIDATION_ASK_DOC_BAD_STATUS);
            }
            $validationDocument->setIdentifier($docId);
        } else {
            throw new PaymentException(PaymentException::ERROR_VALIDATION_ASK_DOC);
        }

        return $validationDocument;
    }

    /**
     * Deserialize a BankAccount.
     *
     * @param array $account The account to deserialize
     */
    public function deserializeBankAccount(array $account): BankAccount
    {
        $bankAccount = new BankAccount();
        $bankAccount->setId($account['Id']);
        $bankAccount->setUserLitteral($account['OwnerName']);
        $bankAccount->setIban($account['IBAN']);
        $bankAccount->setBic($account['BIC']);
        $bankAccount->setCreatedDate(\DateTime::createFromFormat('U', $account['CreationDate']));
        $bankAccount->setComment($account['Tag']);
        $bankAccount->setStatus($account['Active']);

        if (!empty($account['OwnerAddress'])) {
            $address = new Address();
            $streetAddress = $account['OwnerAddress']['AddressLine1'];
            if ('' !== trim($account['OwnerAddress']['AddressLine2'])) {
                $streetAddress .= ' '.$account['OwnerAddress']['AddressLine2'];
            }

            $address->setStreetAddress($streetAddress);
            $address->setAddressLocality($account['OwnerAddress']['City']);
            $address->setRegion($account['OwnerAddress']['Region']);
            $address->setPostalCode($account['OwnerAddress']['PostalCode']);
            $address->setCountryCode($account['OwnerAddress']['Country']);

            $bankAccount->setAddress($address);
        }

        return $bankAccount;
    }

    /**
     * Deserialize a Wallet.
     *
     * @param array $data The wallet to deserialize
     */
    public function deserializeWallet(array $data): Wallet
    {
        $wallet = new Wallet();
        $wallet->setId($data['Id']);
        $wallet->setDescription($data['Description']);
        $wallet->setComment($data['Tag']);
        $wallet->setCreatedDate(\DateTime::createFromFormat('U', $data['CreationDate']));
        $wallet->setCurrency($data['Currency']);

        $balance = new WalletBalance();
        $balance->setCurrency($data['Balance']['Currency']);
        $balance->setAmount($data['Balance']['Amount']);
        $wallet->setBalance($balance);

        // Get the Users matching the Owners of this wallet
        $paymentProfiles = [];
        foreach ($data['Owners'] as $owner) {
            $paymentProfile = $this->paymentProfileRepository->findOneBy(['identifier' => $owner]);
            if (!is_null($paymentProfile)) {
                $paymentProfiles[] = $paymentProfile;
            }
        }

        return $wallet;
    }

    public function getDocument($validationDocumentId)
    {
        $this->_auth();
        $dataProvider = new DataProvider($this->serverUrl.'kyc/documents/'.$validationDocumentId.'/');
        $headers = [
            'Authorization' => $this->authChain,
        ];

        $validationDocument = new ValidationDocument();
        $response = $dataProvider->getCollection(null, $headers);

        if (200 == $response->getCode()) {
            $data = json_decode($response->getValue(), true);

            switch ($data['RefusedReasonType']) {
                case self::OUT_OF_DATE:
                    $validationDocument->setStatus(ValidationDocument::OUT_OF_DATE);

                    break;

                case self::UNDERAGE_PERSON:
                    $validationDocument->setStatus(ValidationDocument::UNDERAGE_PERSON);

                    break;

                case self::DOCUMENT_FALSIFIED:
                    $validationDocument->setStatus(ValidationDocument::DOCUMENT_FALSIFIED);

                    break;

                case self::DOCUMENT_MISSING:
                    $validationDocument->setStatus(ValidationDocument::DOCUMENT_MISSING);

                    break;

                case self::DOCUMENT_HAS_EXPIRED:
                    $validationDocument->setStatus(ValidationDocument::DOCUMENT_HAS_EXPIRED);

                    break;

                case self::DOCUMENT_NOT_ACCEPTED:
                    $validationDocument->setStatus(ValidationDocument::DOCUMENT_NOT_ACCEPTED);

                    break;

                case self::DOCUMENT_DO_NOT_MATCH_USER_DATA:
                    $validationDocument->setStatus(ValidationDocument::DOCUMENT_DO_NOT_MATCH_USER_DATA);

                    break;

                case self::DOCUMENT_UNREADABLE:
                    $validationDocument->setStatus(ValidationDocument::DOCUMENT_UNREADABLE);

                    break;

                case self::DOCUMENT_INCOMPLETE:
                    $validationDocument->setStatus(ValidationDocument::DOCUMENT_INCOMPLETE);

                    break;

                case self::SPECIFIC_CASE:
                    $validationDocument->setStatus(ValidationDocument::SPECIFIC_CASE);

                    break;
            }
        } else {
            throw new PaymentException(PaymentException::ERROR_DOC);
        }

        return $validationDocument;
    }

    public function getKycDocument(string $kycDocumentId)
    {
        $this->_auth();
        $dataProvider = new DataProvider($this->serverUrl.'kyc/documents/'.$kycDocumentId.'/');
        $headers = [
            'Authorization' => $this->authChain,
        ];

        $response = $dataProvider->getCollection(null, $headers);

        if (200 == $response->getCode()) {
            $data = json_decode($response->getValue(), true);

            if (isset($data['Status']) && !is_null($data['Status'])) {
                return $data;
            }
        } else {
            throw new PaymentException(PaymentException::ERROR_DOC);
        }
    }

    private function _uploadPage(string $docId, string $identifier, array $headers, string $fileName)
    {
        $urlPost = str_replace('{KYCDocId}', $docId, str_replace('{userId}', $identifier, self::ITEM_KYC_CREATE_PAGE));

        $body = [
            'File' => base64_encode(file_get_contents($this->validationDocsPath.''.$fileName)),
        ];
        $dataProvider = new DataProvider($this->serverUrl, $urlPost);
        $response = $dataProvider->postCollection($body, $headers);
        if (204 !== $response->getCode()) {
            throw new PaymentException(PaymentException::ERROR_CREATING_DOC_PAGE_TO_PROVIDER);
        }
    }

    private function _refreshToken()
    {
        $this->curlDataProvider->setUrl($this->serverUrlNoClientId.''.self::AUTH_TOKEN);
        $headers = [
            'Authorization: Basic '.base64_encode($this->clientId.':'.$this->apikey),
        ];
        $response = $this->curlDataProvider->post($headers, '{}');
        if (200 == $response->getCode()) {
            $data = json_decode($response->getValue(), true);
            if (isset($data['access_token'], $data['token_type'], $data['expires_in'])) {
                $this->access_token = $data['access_token'];
                $this->accessTokenExpireIn = $data['expires_in'];
                $this->authChain = 'Bearer '.$this->access_token;
            }
        } else {
            throw new PaymentException(PaymentException::ERROR_GETTING_ACCESS_TOKEN);
        }
    }

    private function _auth()
    {
        if (is_null($this->access_token)) {
            $this->_refreshToken();

            return;
        }

        $currentTime = new \DateTime('now');
        $diff = $currentTime->getTimestamp() - $this->startTime->getTimestamp();

        if ($diff >= $this->accessTokenExpireIn - self::ACCESS_TOKEN_EXPIRATION_SECURITY_MARGIN_IN_SECONDS) {
            $this->_refreshToken();
        }
    }
}