wikimedia/mediawiki-extensions-DonationInterface

View on GitHub
gravy_gateway/gravy.adapter.php

Summary

Maintainability
F
3 days
Test Coverage
<?php

use MediaWiki\MediaWikiServices;
use Psr\Log\LogLevel;
use SmashPig\Core\PaymentError;
use SmashPig\Core\ValidationError;
use SmashPig\PaymentData\RecurringModel;
use SmashPig\PaymentData\ValidationAction;
use SmashPig\PaymentProviders\IPaymentProvider;
use SmashPig\PaymentProviders\PaymentProviderFactory;
use SmashPig\PaymentProviders\Responses\ApprovePaymentResponse;
use SmashPig\PaymentProviders\Responses\CreatePaymentResponse;
use SmashPig\PaymentProviders\Responses\CreatePaymentSessionResponse;
use SmashPig\PaymentProviders\Responses\PaymentDetailResponse;

class GravyAdapter extends GatewayAdapter implements RecurringConversion {
    use RecurringConversionTrait;

    /**
     * @var string
     */
    public const GATEWAY_NAME = 'Gravy';

    /**
     * @var string
     */
    public const IDENTIFIER = 'gravy';

    /**
     * @var string
     */
    public const GLOBAL_PREFIX = 'wgGravyGateway';

    /**
     * @inheritDoc
     */
    public function doPayment() {
        $this->ensureUniqueOrderID();
        $this->session_addDonorData();
        $this->runDoPaymentFilters();
        if ( !$this->filterActionIsProcess() ) {
            // Ensure IPVelocity filter session value is reset on error
            WmfFramework::setSessionValue( Gateway_Extras_CustomFilters_IP_Velocity::RAN_INITIAL, false );
            return $this->newFailureWithError( 'internal-0000', 'Failed pre-process checks for payment.' );
        }

        $paymentProvider = PaymentProviderFactory::getProviderForMethod( $this->getPaymentMethod() );

        $this->setCurrentTransaction( 'authorize' );

        if ( !$this->paymentMethodSupportsRecurring() ) {
            $this->addResponseData( [
                'recurring' => '',
            ] );
            // rerun staging helpers with new value (importantly, ReturnUrl helper)
            $this->stageData();
        }
        $createPaymentResponse = $this->callCreatePayment( $paymentProvider );
        // We increment the sequence number here, so the next time doPayment is called
        // in the same session we will get a new order ID in ensureUniqueOrderID.
        $this->incrementSequenceNumber();

        if ( count( $createPaymentResponse->getValidationErrors() ) > 0 ) {
            return $this->getLocalizedValidationErrorResult( $createPaymentResponse->getValidationErrors() );
        }

        // Add the gravy-generated transaction ID to the DonationData object
        // to be sent to the queues
        $this->addResponseData( [
            'gateway_txn_id' => $createPaymentResponse->getGatewayTxnId(),
        ] );

        if ( $createPaymentResponse->requiresRedirect() ) {
            // ... and ensure it is persisted in the php session
            $this->session_addDonorData();

            $redirectUrl = $createPaymentResponse->getRedirectUrl();
            $this->logger->info( "Redirecting to $redirectUrl" );
            return PaymentResult::newRedirect( $redirectUrl );
        }

        return $this->handleCreatedPayment( $paymentProvider, $createPaymentResponse );
    }

    public function getPaymentMethodsSupportingRecurringConversion(): array {
        return [ 'cc' ];
    }

    public function paymentMethodSupportsRecurring(): bool {
        return $this->payment_methods[$this->getPaymentMethod()]['recurring'];
    }

    public function processDonorReturn( $requestValues ) {
        $this->logger->info( "Handling redirectResult " . json_encode( $requestValues ) );
        $provider = PaymentProviderFactory::getProviderForMethod(
            $this->getPaymentMethod()
        );
        '@phan-var \SmashPig\PaymentProviders\IPaymentProvider $provider';

        $mappedResult = [];
        if ( isset( $requestValues['transaction_id'] ) ) {
            $mappedResult['gateway_txn_id'] = $requestValues['transaction_id'];
        } elseif ( isset( $requestValues['gr4vy_transaction_id'] ) ) {
            $mappedResult['gateway_txn_id'] = $requestValues['gr4vy_transaction_id'];
        }

        // @phan-suppress-next-line PhanUndeclaredMethod get Payment details is only declared in the gravy provider
        $detailsResult = $provider->getLatestPaymentStatus( $mappedResult );

        $this->logger->debug(
            'Gravy donor return response: ' . json_encode( $detailsResult->getRawResponse() )
        );

        return $this->handleCreatedPayment( $provider, $detailsResult );
    }

    /**
     * After a payment has been created and we have the processor-side fraud results
     * (AVS & CVV checks), run our fraud filters and capture the payment if needed.
     *
     * @param IPaymentProvider $provider
     * @param PaymentDetailResponse $authorizeResult
     * @return PaymentResult
     */
    protected function handleCreatedPayment(
        IPaymentProvider $provider, PaymentDetailResponse $authorizeResult
    ): PaymentResult {
        $transactionStatus = $authorizeResult->getStatus();

        // Ensure required DonationData information are filled
        $this->updateResponseData( $authorizeResult );

        // When authorization is successful but capture fails (or is not
        // attempted because our ValidationAction is 'review', we still
        // send the donor to the Thank You page. This is because the
        // donation can still be captured manually by Donor Relations and
        // we don't want the donor to try again.
        $paymentResult = PaymentResult::newSuccess();
        if ( !$authorizeResult->isSuccessful() ) {
            $paymentResult = PaymentResult::newFailure();
            // TODO: map any errors from $authorizeResult
            // log the error details on failure
            $errorLogMessage = 'Unsuccessful createPayment response from gateway: ';
            $errorLogMessage .= $authorizeResult->getStatus() . " : ";
            $errorLogMessage .= json_encode( $authorizeResult->getRawResponse() );
            $this->logger->info( $errorLogMessage );
        } elseif ( $authorizeResult->requiresApproval() ) {
            $this->runFraudFilters( $authorizeResult );
            switch ( $this->getValidationAction() ) {
                case ValidationAction::PROCESS:
                    // do approve payment request here.
                    $this->setCurrentTransaction( 'capture' );
                    $this->logger->info( "Calling approvePayment on PSP reference {$authorizeResult->getGatewayTxnId()}" );
                    $captureResult = $this->callApprovePayment( $provider );
                    $transactionStatus = $captureResult->getStatus();
                    if ( $captureResult->isSuccessful() ) {
                        $this->logger->info( "Returned PSP Reference {$captureResult->getGatewayTxnId()}" );
                        if ( $this->showMonthlyConvert() ) {
                            $this->logger->info( "Displaying monthly convert modal" );
                            $paymentResult = PaymentResult::newSuccess();
                        }
                    } else {
                        $this->logger->info( 'Capture call unsuccessful' );
                    }
                    break;
                case ValidationAction::REJECT:
                    $paymentResult = PaymentResult::newFailure();
                    $this->logger->info( 'Created payment rejected by our fraud filters' );
                    break;
                default:
                    $this->logger->info(
                        'Not capturing authorized payment - validation action is ' .
                        $this->getValidationAction()
                    );
            }
        }

        if ( $authorizeResult->isSuccessful() ) {
            // save donor data for recur conversion
            if ( $authorizeResult->getRecurringPaymentToken() && $this->showMonthlyConvert() ) {
                $this->session_addDonorData();
            }
        }

        // Log and send the payments-init message, and clean out the session
        $this->finalizeInternalStatus( $transactionStatus );

        $this->postProcessDonation();
        return $paymentResult;
    }

    /**
     * getAVSResult is intended to be used by the functions filter, to
     * determine if we want to fail the transaction ourselves or not.
     * @return int
     */
    public function getAVSResult() {
        return $this->getData_Unstaged_Escaped( 'avs_result' );
    }

    /**
     * getCVVResult is intended to be used by the functions filter, to
     * determine if we want to fail the transaction ourselves or not.
     * @return int
     */
    public function getCVVResult() {
        return $this->getData_Unstaged_Escaped( 'cvv_result' );
    }

    /**
     * Runs antifraud filters if the appropriate for the current payment method.
     * Sets $this->action to one of the ValidationAction constants.
     *
     * @param PaymentDetailResponse $authorizeResult
     */
    protected function runFraudFilters( PaymentDetailResponse $authorizeResult ): void {
        $riskScores = $authorizeResult->getRiskScores();
        $this->addResponseData( [
            'avs_result' => $riskScores['avs'] ?? 0,
            'cvv_result' => $riskScores['cvv'] ?? 0
        ] );
        $this->runAntifraudFilters();
    }

    /**
     * @return void
     */
    protected function runDoPaymentFilters(): void {
        Gateway_Extras_CustomFilters::onGatewayReady( $this );
        $this->runSessionVelocityFilter();
    }

    /**
     * @return bool
     */
    protected function filterActionIsProcess(): bool {
        return $this->getValidationAction() === ValidationAction::PROCESS;
    }

    /**
     * @param string $errorCode
     * @param string $debugMessage
     * @return PaymentResult
     */
    protected function newFailureWithError( $errorCode, $debugMessage ): PaymentResult {
        $paymentError = new PaymentError(
            $errorCode,
            $debugMessage,
            LogLevel::INFO
        );
        return PaymentResult::newFailure( [ $paymentError ] );
    }

    /**
     * @param IPaymentProvider $paymentProvider
     * @return CreatePaymentResponse
     */
    protected function callCreatePayment( IPaymentProvider $paymentProvider ): CreatePaymentResponse {
        $createPaymentParams = $this->buildRequestArray();
        if ( $this->showMonthlyConvert() ) {
            $createPaymentParams['recurring'] = 1;
            $createPaymentParams['recurring_model'] = RecurringModel::CARD_ON_FILE;
        }
        $this->logger->info( "Calling createPayment for Gravy payment" );

        $createPaymentResponse = $paymentProvider->createPayment( $createPaymentParams );
        if ( $createPaymentResponse->getGatewayTxnId() !== null ) {
            $this->logger->info( "Returned Authorization ID {$createPaymentResponse->getGatewayTxnId()}" );
        }

        return $createPaymentResponse;
    }

    /**
     * @param IPaymentProvider $paymentProvider
     * @return ApprovePaymentResponse
     */
    protected function callApprovePayment( IPaymentProvider $paymentProvider ): ApprovePaymentResponse {
        $approvePaymentParams = $this->buildRequestArray();
        $this->logger->info( "Calling approvePayment for Gravy payment" );

        $approvePaymentResponse = $paymentProvider->approvePayment( $approvePaymentParams );
        if ( $approvePaymentResponse->getGatewayTxnId() !== null ) {
            $this->logger->info( "Returned Captured ID {$approvePaymentResponse->getGatewayTxnId()}" );
        }

        return $approvePaymentResponse;
    }

    /**
     * @inheritDoc
     */
    public function getCommunicationType() {
        // TODO: Implement getCommunicationType() method.
    }

    public function getCheckoutSession(): CreatePaymentSessionResponse {
        $paymentProvider = PaymentProviderFactory::getProviderForMethod( $this->getPaymentMethod() );
        // @phan-suppress-next-line PhanUndeclaredMethod the createPaymentSession variable is declared in Gravy Payment Provider class but not on the general interface
        return $paymentProvider->createPaymentSession();
    }

    /**
     * @inheritDoc
     */
    protected function defineTransactions() {
        $this->transactions = [
            'authorize' => [
                'request' => [
                    'city',
                    'country',
                    'currency',
                    'email',
                    'first_name',
                    'last_name',
                    'postal_code',
                    'phone',
                    'state_province',
                    'street_address',
                    'street_number',
                    'fiscal_number',
                    'amount',
                    'order_id',
                    'user_ip',
                    'recurring',
                    'payment_method',
                    'payment_submethod',
                    'user_name',
                    'description',
                    'return_url',
                    'use_3d_secure',
                    'gateway_session_id'
                ],
                'values' => [
                    'description' => WmfFramework::formatMessage( 'donate_interface-donation-description' )
                ]
            ],
            'capture' => [
                'request' => [
                    'amount',
                    'currency',
                    'gateway_txn_id'
                ]
            ]
        ];
    }

    /**
     * @inheritDoc
     */
    protected function defineAccountInfo() {
        // TODO: Implement defineAccountInfo() method.
        $this->accountInfo = $this->account_config;
    }

    /**
     * @inheritDoc
     */
    protected function defineReturnValueMap() {
        // TODO: Implement defineReturnValueMap() method.
    }

    /**
     * @inheritDoc
     */
    protected function defineOrderIDMeta() {
        $this->order_id_meta = [
            'ct_id' => true,
            'generate' => true,
        ];
    }

        /**
         * @param ValidationError[] $validationErrors
         * @return PaymentResult
         */
    protected function getLocalizedValidationErrorResult( array $validationErrors ): PaymentResult {
        $localizedErrors = [];
        foreach ( $validationErrors as $error ) {
            $field = $error->getField();
            $debugMessage = $error->getDebugMessage();
            if ( $field === 'payment_token' ) {
                // This means the generated token was invalid.
                $urlParameterKeys = [
                    'payment_method',
                    'recurring',
                    'uselang',
                    'language',
                    'currency',
                    'amount',
                    'country',
                    'utm_source',
                    'utm_medium',
                    'utm_campaign'
                ];
                $urlParameters = [];
                foreach ( $urlParameterKeys as $key ) {
                    if ( isset( $this->unstaged_data[$key] ) ) {
                        $urlParameters[$key] = $this->unstaged_data[$key];
                    }
                }
                $messageKey = 'donate_interface-donate-error-try-again-html';
                $messageParams = [
                    GatewayChooser::buildGatewayPageUrl(
                        'gravy',
                        $urlParameters,
                        MediaWikiServices::getInstance()->getMainConfig()
                    ),
                    $this->localizeGlobal( 'OtherWaysURL' ),
                    self::getGlobal( 'ProblemsEmail' )
                ];
            } else {
                if ( in_array( $field, [ 'currency', 'fiscal_number' ] ) ) {
                    $messageKey = "donate_interface-error-msg-invalid-$field";
                } else {
                    $messageKey = 'donate_interface-error-msg-' . $field;
                }
                $messageParams = [];
            }
            $localizedErrors[] = new ValidationError(
                $field, $messageKey, $messageParams
            );
            $this->logger->info(
                'createPayment call came back with validation error in ' . $field . ( $debugMessage
                    ? ' with message: ' . $debugMessage : '' )
            );
        }
        return PaymentResult::newRefresh( $localizedErrors );
    }

    protected function updateResponseData( PaymentDetailResponse $authorizeResult ) {
        $responseData = [];

        // Add the gravy-generated transaction ID to the DonationData object
        // to be sent to the queues
        if ( $authorizeResult->isSuccessful() ) {
            $responseData['gateway_txn_id'] = $authorizeResult->getGatewayTxnId();
            $responseData['backend_processor'] = $authorizeResult->getBackendProcessor();
            $responseData['backend_processor_txn_id'] = $authorizeResult->getBackendProcessorTransactionId();

            $responseData['payment_orchestrator_reconciliation_id'] = $authorizeResult->getPaymentOrchestratorReconciliationId();

            if ( $authorizeResult->getRecurringPaymentToken() != null ) {
                $responseData['recurring_payment_token'] = $authorizeResult->getRecurringPaymentToken();
            } elseif ( $this->getData_Unstaged_Escaped( 'recurring' ) && $authorizeResult->isSuccessful() ) {
                $this->logger->warning( 'No token found on successful recurring payment authorization response.' );
            }

            if ( $authorizeResult->getProcessorContactID() != null ) {
                $responseData['processor_contact_id'] = $authorizeResult->getProcessorContactID();
            }
            if ( $authorizeResult->getDonorDetails() !== null && $authorizeResult->getDonorDetails()->getUserName() !== '' ) {
                $responseData['user_name'] = $authorizeResult->getDonorDetails()->getUserName();
            }
            if ( !$this->getPaymentSubmethod() ) {
                $responseData['payment_submethod'] = $authorizeResult->getPaymentSubmethod() ?? '';
            }
            if ( !$this->getPaymentMethod() ) {
                $responseData['payment_method'] = $authorizeResult->getPaymentMethod();
            }
        }

        $this->addResponseData( $responseData );
    }

    protected function getQueueDonationMessage(): array {
        $message = parent::getQueueDonationMessage();
        // save external_identifier as gravy buyer id
        if ( isset( $this->unstaged_data['processor_contact_id'] ) ) {
            $message['external_identifier'] = $this->unstaged_data['processor_contact_id'];
        }

        if ( isset( $this->unstaged_data['user_name'] ) ) {
            $message['external_identifier'] = $this->unstaged_data['user_name'];
        }

        return $message;
    }

}