wikimedia/mediawiki-extensions-DonationInterface

View on GitHub
adyen_gateway/adyen_submit_payment.api.php

Summary

Maintainability
C
1 day
Test Coverage
<?php
use SmashPig\Core\DataStores\QueueWrapper;
use SmashPig\Core\SequenceGenerators;
use SmashPig\Core\UtcDate;
use SmashPig\PaymentData\FinalStatus;
use SmashPig\PaymentProviders\PaymentProviderFactory;
use Wikimedia\ParamValidator\ParamValidator;

class AdyenSubmitPaymentApi extends ApiBase {
    private const GATEWAY = 'adyen';
    private const STATUS_ERROR = 'error';
    private const STATUS_SUCCESS = 'success';
    // For payments init, always process as we aren't doing fraud checks here
    private const VALIDATION_ACTION = 'process';

    /**
     * @var string
     */
    public string $contributionTrackingId;

    /**
     * @var array
     */
    public array $donationData;

    /**
     * @var string
     */
    public string $gateway = self::GATEWAY;

    /**
     * @var string
     */
    public string $gatewayAccount;

    /**
     * @var string
     */
    public string $gatewayTransactionId;

    /**
     * @var string
     */
    public string $orderId;

    /**
     * @var \Psr\Log\LoggerInterface
     */
    public $logger;

    public function execute() {
        if ( RequestContext::getMain()->getUser()->pingLimiter( 'submitpayment' ) ) {
            // Allow rate limiting by setting e.g. $wgRateLimits['submitpayment']['ip']
            return;
        }

        // Get the data
        $this->donationData = $this->extractRequestParams();

        // Create a contribution tracking id
        $generator = SequenceGenerators\Factory::getSequenceGenerator( 'contribution-tracking' );
        $this->contributionTrackingId = (string)$generator->getNext();

        // Create orderId
        $this->orderId = $this->contributionTrackingId . '.1';
        $this->donationData['order_id'] = $this->orderId;

        // Set up logging
        $className = DonationInterface::getAdapterClassForGateway( $this->gateway );
        $this->logger = DonationLoggerFactory::getLoggerForType(
            $className,
            $this->orderId . ':' . $this->orderId
        );

        if ( $this->donationData['payment_method'] == 'applepay' ) {
            $this->donationData['payment_method'] = 'apple';
            $this->donationData['utm_campaign'] = 'iOS';
        } elseif ( $this->donationData['payment_method'] == 'paywithgoogle' ) {
            $this->donationData['payment_method'] = 'google';
            $this->donationData['utm_campaign'] = 'Android';
        } else {
            $this->logger->error( 'Payment method of ' . $this->donationData['payment_method'] . ' not available' );
            $response = [
                'status' => self::STATUS_ERROR,
                'error_message' => $this->generateErrorMessage(),
                'order_id' => $this->orderId,
            ];
            $this->getResult()->addValue( null, 'response', $response );
            return;
        }

        // App is sending payment_network which is our payment_submethod, match what we do in the adyen form
        $this->donationData['payment_submethod'] = $this->mapNetworktoSubmethod( $this->donationData['payment_network'] );

        $debugparams = $this->donationData;
        unset( $debugparams['payment_token'] );
        $this->logger->info( ' Starting submitPayment request with: ' . json_encode( $debugparams ) );

        // Set up gateway
        DonationInterface::setSmashPigProvider( $this->gateway );

        // Get the account
        $accountInfo = array_keys( $this->getConfig()->get( 'AdyenCheckoutGatewayAccountInfo' ) );
        $this->gatewayAccount = array_shift( $accountInfo );

        $this->sendToContributionTracking();

        $paymentProvider = PaymentProviderFactory::getProviderForMethod( $this->donationData['payment_method'] );

        try {
            $createPaymentResponse = $paymentProvider->createPayment( $this->donationData );

            if ( !$createPaymentResponse->isSuccessful() ) {
                $this->returnError( $createPaymentResponse->getRawResponse() );
                return;
            }
            $response = [
                'status' => self::STATUS_SUCCESS,
            ];
        } catch ( Exception $e ) {
            $this->returnError( $e );
            return;
        }

        $this->gatewayTransactionId = $createPaymentResponse->getGatewayTxnId();

        // Need to grab the token if it's recurring
        if ( $this->donationData['recurring'] ) {
            $this->donationData['recurring_payment_token'] = $createPaymentResponse->getRecurringPaymentToken();
            $this->donationData['processor_contact_id'] = $createPaymentResponse->getProcessorContactID();
        }

        // Approve (capture) if needed
        if ( $createPaymentResponse->getStatus() === FinalStatus::PENDING_POKE ) {
            try {
                $approvePaymentResponse = $paymentProvider->approvePayment( [
                    'amount' => $this->donationData['amount'],
                    'currency' => $this->donationData['currency'],
                    'gateway_txn_id' => $this->gatewayTransactionId,
                ] );

                if ( !$approvePaymentResponse->isSuccessful() ) {
                    $this->returnError( $approvePaymentResponse->getRawResponse() );
                    return;
                }

                $response['status'] = self::STATUS_SUCCESS;
                $this->sendToPaymentsInit( 'complete' );
            } catch ( Exception $e ) {
                $this->returnError( $e );
                return;
            }
        }

        $this->sendToDonations();

        // Build the response
        $response['gateway_transaction_id'] = $this->gatewayTransactionId;
        $response['order_id'] = $this->orderId;

        $this->getResult()->addValue( null, 'response', $response );
    }

    public function getAllowedParams() {
        return [
            'amount' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'app_version' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'banner' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'city' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'country' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'currency' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'donor_country' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'email' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'first_name' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'full_name' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'language' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'last_name' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'recurring' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'payment_token' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'opt_in' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'pay_the_fee' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'payment_method' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'payment_network' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'postal_code' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'state_province' => [ ParamValidator::PARAM_TYPE => 'string' ],
            'street_address' => [ ParamValidator::PARAM_TYPE => 'string' ],
        ];
    }

    protected function generateErrorMessage() {
        $text = $this->msg( 'donate_interface-error-msg-general' )->inLanguage( $this->donationData['language'] )->text();
        $reference = $this->msg( 'donate_interface-error-reference', $this->orderId )->inLanguage( $this->donationData['language'] )->text();
        // There was an error processing your request. Error reference: 1234.5
        return $text . ' ' . $reference;
    }

    /**
     * This allows the api to be hit without being logged in
     * @return false
     */
    public function isReadMode() {
        return false;
    }

    /**
     * This function also exists in the frontend JS in adyen.js
     * @param string $network
     * @return string
     */
    protected function mapNetworktoSubmethod( $network ) {
        $network = strtolower( $network );
        switch ( $network ) {
            case 'amex':
            case 'discover':
            case 'jcb':
            case 'visa':
                return $network;
            case 'cartesbancaires':
                return 'cb';
            case 'electron':
                return 'visa-electron';
            case 'mastercard':
                return 'mc';
            default:
                return '';
        }
    }

    public function mustBePosted() {
        return true;
    }

    protected function sendToContributionTracking() {
        // build utm key, looks like pay the fee is still taken from that
        if ( $this->donationData['pay_the_fee'] === 1 ) {
            $this->donationData['utm_key'] = 'ptf_1';
        }

        // Temporary until this is fixed on the app side, but if there is no banner set, set it to appmenu
        // TODO: Remove when the work in T350919 is done
        if ( !isset( $this->donationData['banner'] ) ) {
            $this->donationData['banner'] = 'appmenu';
        }

        $message = [
            'amount' => $this->donationData['amount'],
            'banner' => $this->donationData['banner'],
            'browser' => 'app',
            'browser_version' => $this->donationData['app_version'],
            'country' => $this->donationData['country'],
            'currency' => $this->donationData['currency'],
            'form_amount' => $this->donationData['currency'] . ' ' . $this->donationData['amount'],
            'gateway' => $this->gateway,
            'id' => $this->contributionTrackingId,
            'is_recurring' => $this->donationData['recurring'],
            'language' => $this->donationData['language'],
            'os' => $this->donationData['utm_campaign'],
            'payment_method' => $this->donationData['payment_method'],
            'payment_submethod' => $this->donationData['payment_submethod'],
            'ts' => wfTimestamp( TS_MW ),
            'utm_key' => $this->donationData['utm_key'],
            // the utm_source from the form has banner.landingpage.payment_method
            'utm_source' => $this->donationData['banner'] . '.' . 'inapp' . '.' . $this->donationData['payment_method'],
            'utm_medium' => 'WikipediaApp',
            'utm_campaign' => $this->donationData['utm_campaign']
        ];

        QueueWrapper::push( 'contribution-tracking', $message );
    }

    protected function sendToDonations() {
        $message = [
            'contribution_tracking_id' => $this->contributionTrackingId,
            'date' => UtcDate::getUtcTimestamp(),
            'gateway' => $this->gateway,
            // Pulling from the config
            'gateway_account' => $this->gatewayAccount,
            'gateway_txn_id' => $this->gatewayTransactionId,
            'order_id' => $this->orderId,
            'user_ip' => WmfFramework::getIP(),
            // donationData that needs to be renamed
            'country' => $this->donationData['donor_country'],
            'gross' => $this->donationData['amount']
        ];

        // Add in donationData
        $keysToCopy = [
            'city',
            'currency',
            'language',
            'email',
            'first_name',
            'full_name',
            'last_name',
            'payment_method',
            'payment_submethod',
            'processor_contact_id',
            'postal_code',
            'opt_in',
            'recurring',
            'recurring_payment_token',
            'street_address',
            'state_province',
            'utm_key',
            'utm_medium',
            'utm_source'
        ];

        foreach ( $keysToCopy as $key ) {
            if ( isset( $this->donationData[$key] ) ) {
                $message[$key] = $this->donationData[$key];
            }
        }

        QueueWrapper::push( 'donations', $message );
    }

    protected function sendToPaymentsInit( $status ) {
        $message = [
            'amount' => $this->donationData['amount'],
            'contribution_tracking_id' => $this->contributionTrackingId,
            'country' => $this->donationData['country'],
            'currency' => $this->donationData['currency'],
            'date' => UtcDate::getUtcTimestamp(),
            'gateway' => $this->gateway,
            // @phan-suppress-next-line PhanCoalescingNeverNull Property maybe unset
            'gateway_txn_id' => $this->gatewayTransactionId ?? '',
            'order_id' => $this->orderId,
            'payment_method' => $this->donationData['payment_method'],
            'payment_submethod' => $this->donationData['payment_submethod'],
            'payments_final_status' => $status,
            'server' => gethostname(),
            'validation_action' => self::VALIDATION_ACTION
        ];

        QueueWrapper::push( 'payments-init', $message );
    }

    protected function returnError( $error ) {
        $this->logger->error( (string)$error );
        $response = [];
        $response['status'] = self::STATUS_ERROR;
        $response['error_message'] = $this->generateErrorMessage();
        $response['order_id'] = $this->orderId;
        $this->sendToPaymentsInit( 'failed' );
        $this->getResult()->addValue( null, 'response', $response );
    }
}