wikimedia/wikimedia-fundraising-tools

View on GitHub
audit/payflow/PayflowReports.php

Summary

Maintainability
C
7 hrs
Test Coverage
<?php error_reporting (E_ALL);

/*
 * Base class for a returned Payflow Report. This class is used by all others to access Payflow report data.
 *
 */

class PayflowReport {
    public $reportName;
    public $results = array();
    protected $columnFilters = array();
    protected $reportResponse;
    
    public function __construct( $report ) {
        $this->reportName = $report;
    }

    /** 
     * Add the fully parsed and preapred report response
     * @param ReportResponse $reportResponse
     */
    public function addReportResponse( ReportResponse $reportResponse ) {
        $this->reportResponse = $reportResponse;
    }

    public function getReportResponse() {
        return $this->reportResponse;
    }
    
    /**
     * Specify the columns filters you wish to apply when viewing the report 
     * @param array $filters
     * @TODO perhaps do some validity checking of requested filters
     */
    public function setColumnFilters( array $filters ) {
        $this->columnFilters = $filters;
    }
    
    /**
     * Fetch an iterable object of column data
     * 
     * Sets any defined column filters on objects returned.
     * 
     * @return object
     */
    public function getColumns() {
        if ( !$this->checkReportResponse() ) {
            throw new Exception( 'ReportResponse does not exist.' );
        }
        
        if ( count( $this->columnFilters )) {
            $this->reportResponse->columns->setFilterKeys( $this->columnFilters );
        }
        
        return $this->reportResponse->columns;
    }
    
    /**
     * Fetch an array of iterable data row objects
     * 
     * Sets any defined column filters on objects returned.
     * 
     * @return array
     */
    public function getData() {
        if ( !$this->checkReportResponse() ) {
            throw new Exception( 'ReportResponse does not exist.' );
        }
        
        if ( count( $this->columnFilters )) {
            foreach( $this->reportResponse->rows as $row ) {
                $row->setFilterKeys( $this->columnFilters );
            }
        }
        
        return $this->reportResponse->rows;
    }
    
    /**
     * Check existence of report response
     * @TODO add additional checks for things like data and columns
     * @return bool
     */
    public function checkReportResponse() {
        if ( !$this->reportResponse ) {
            return false;
        }
        
        return true;
    }
}

/*
 * Subclass for sending a report specific query to PayFlow
 *
 */

class PayflowReportQuery extends PayflowQuery {

    protected $reportResponse;
    public $pageSize = 50;
    
    public function __construct( $date = null ) {
        require_once( dirname(__FILE__) . "/PayflowReportResponse.php" );
        $this->date['start'] = isset( $date[0] ) ? $date[0] : date( 'Y-m-d' ); 
        $this->date['end'] = isset( $date[1] ) ? $date[1] : date( 'Y-m-d' );
    }

    // Get the results of a previous run report from payflow
    public function getReportData( $id = null ) {
        $id = ( is_null( $id ) ) ? $this->reportId : $id;
        $xml  = '<getResultsRequest>';
        $xml .= $this->xmlTag( 'reportId', $id );
        $xml .= '</getResultsRequest>';

        $request = $this->createRequest( $xml );

        $rc = $this->sendRequest( $request );

        return $rc;
    }

    // Retrieve all the metadata for a given report
    public function getMetadataRequest() {
        $xml  = '<getMetaDataRequest>';
        $xml .= self::xmlTag( 'reportId', $this->reportId );
        $xml .= '</getMetaDataRequest>';

        $request = $this->createRequest( $xml );

        $rc = $this->sendRequest( $request );

        return $rc;
    }

    // Retrieves a specfic page from a given report
    public function getDataRequest( $id, $page ) {
        $xml  = '<getDataRequest>';
        $xml .= $this->xmlTag( 'reportId', $id );
        $xml .= $this->xmlTag( 'pageNum' , $page ); // Will this ever not be 1
        $xml .= '</getDataRequest>';

        $request = $this->createRequest( $xml );
        $response = $this->sendRequest( $request );

        return $response;
    }

    // Actually run the report
    public function runReport() {
        $xml = $this->runReportRequest();
        $request = $this->createRequest( $xml );

        $rc = $this->sendRequest( $request );
        $xml_response = new SimpleXMLElement( $rc );

        $responseCode = $xml_response->baseResponse->responseCode;

        switch ( $responseCode ) {
            case 100:
                $this->reportId = $xml_response->runReportResponse->reportId;
                $response = $this->parseReportResponse( $xml_response );
                // $response->printEntries();
                break;
            case 101:
                die( "Request has failed\n" );
            case 102:
                die( "An internal scheduler error has occurred\n" );
            case 103:
                die( "Unknown report requested\n" );
            case 104:
                die( "Invalid Report ID\n" );
            case 105: 
                die( "A system error has occurred\n" );
            case 106:
                die( "A database error has occurred\n" );
            case 107:
                die( "Invalid XML request\n" );
            case 108:
                die( "User authentication failed\n" );
            case 109:
                die( "Invalid report parameters provided\n" );
            case 110:
                die( "Invalid merchant account\n" );
            case 111:
                die( "Invalid page number\n" );
            case 112:
                die( "Template already exists\n" );
            case 113:
                die( "Unknown template requested\n" );
            case 129: 
                $this->splitReport();
                break;
            case 131:
                print "Warning: Waiting as Report is still being created\n";
                sleep(10);
                $this->runReport();
                break;
            default:
                print_r($xml_response);
                die( "Blowing up as we got response code $responseCode\n" );
        }
        return $response;
    }

    // Parse the results from a payflow report
    public function parseReportResponse ( $xml_response ) {
        $statusCode = $xml_response->runReportResponse->statusCode;

        switch ( $statusCode ) {
            case 1: // Created
                return;
            case 2: // Still executing
                while(1) {
                    print( "Waiting as report is still running $this->reportId \n" );
                    sleep(60);
                    if ( $this->isReportReady() ) {
                        break;
                    }
                }
                break;
            case 3: // Done
                break;    
            case 4: // Failed
                die( "Report Creation Failed\n" );
            case 5: // Expired
                die( "Expired" );
            case 6: // Expired?
                die( "Expired" );
            default:
        }
    
        $this->saveReportInfo();
        
        // return the metadata as a SimpleXMLEelement object
        $simple_xml_metadata = new SimpleXMLElement( $this->getMetadataRequest() );
        
        /**
         * begin building a report response object
         * this will contain parsed report data for us to use later.
         */ 
        if ( !$this->reportResponse ) {
            $this->reportResponse = new ReportResponse();
        }

        // parse the metadata
        $this->reportResponse->parseMetaData( $simple_xml_metadata );
        
        for( $i = 1; $i <= $this->reportResponse->getNumberOfPages(); $i++ ) {
            // loop through pages and fetch their XML
            $response = $this->getDataRequest( $this->reportId, $i );

            try {
                $xml_response = new SimpleXMLElement( $response );
                $this->reportResponse->addData( $xml_response );
            } catch ( Exception $e ) {
                echo "Bad XML. Skipping page $i from report: $this->reportId\n";
            }
        }
        
        return true;
    }

    // Query Payflow to find out if our report has finished running. Some reports take a while to run.
    public function isReportReady() {
        $rc = $this->getReportData();    

        $xml = new SimpleXMLElement( $rc );
        
        if ( $xml->runReportResponse->statusCode == 3 ) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Save what we've been doing to a log
     * 
     * @TODO make this better, perhaps with syslog support
     */
    public function saveReportInfo() {
        $string = $this->reportName . ',' . $this->reportId . ',' . $this->date['start'] . ',' . 
              $this->date['end'] . "\n";

        if ( $fp = @fopen( '/var/log/fundraising/contrib_audit/log', 'a' )) {
            fwrite( $fp , $string );
            fclose( $fp );
        }
    }

    // If paypal reports that our query is too big we have to split it up. Divide the time by 2 and try again
    // till we get it right
    public function splitReport() {
        $start_end = strtotime( $this->date['start'] );
        $split_end = strtotime( $this->date['end'] );

        $diff = $split_end - $start_end;
        $middle = date( 'Y-m-d H:i:s', ( $start_end + ( $diff / 2 ) ) );

        print "Warning: Halving report length from: " . date( 'Y-m-d H:i:s', $start_end ) . " to: $middle\n";

        $report = new CustomReport( array( $this->date['start'], $middle ) );
        $report->runReport();
        $report2 = new CustomReport( array( $middle, $this->date['end'] ) );
        $report2->runReport();
    }

    /**
     * Fetch the results as a monolitich report object.
     */
    public function getResults() {
        // instantiate our report object
        $results = new PayflowReport( $this->reportName );
        
        // feed it the reportResponse object, 
        //which contains all of the data from which to build the report
        $results->addReportResponse( $this->reportResponse );
        return $results;
    }
}

/*
 * Top level Class to Query Payflow
 *
 */

class PayflowQuery {

    private $payflowTest = 'https://payments-reports.paypal.com/test-reportingengine';
    private $payflowLive = 'https://payments-reports.paypal.com/reportingengine';

    private $header = '<?xml version="1.0" encoding="UTF-8"?>';

    /**
     * An array containing the input params for a particular report
     *     $options[<param name>] = <param value>
     * @var array
     */
    protected $options = array();
    
    /**
     * An array containing required parameter names for a particular report
     *     $reqd_options = array( 'start_date', 'end_date' );
     * @var array
     */
    protected $reqd_options = array();
    
    public function __construct() {
        $this->loadAuthData();
    }

    public function loadAuthData() {
        $config = '/etc/fundraising/auth.cfg';
        $auth = parse_ini_file( $config );

        $this->vendor = $auth['vendor'];
        $this->partner = $auth['partner'];
        $this->user = $auth['user'];
        $this->password = $auth['password'];
    }

    // Escape and all approprite tags
    protected function xmlTag( $tag, $value ) {
        $xml  = "<$tag>" . htmlspecialchars( $value ) . "</$tag>";

        return $xml;
    }

    // Create new reporting engine request
    public function createRequest( $data ) {
        $xml  = $this->header;
        $xml .= '<reportingEngineRequest>';
        $xml .= $this->authRequest();
        $xml .= $data;
        $xml .= '</reportingEngineRequest>';
        
        return $xml;
    }

    // Create a new auth request
    public function authRequest() {
        $xml  = '<authRequest>';
        $xml .= '<user>' . $this->user . '</user>';
        $xml .= '<vendor>' . $this->vendor . '</vendor>';
        $xml .= '<partner>' . $this->partner . '</partner>';
        $xml .= '<password>' . $this->password . '</password>';
        $xml .= '</authRequest>';
        
        return $xml;
    }

    // Build the report request
    public function runReportRequest() {
        $xml  = '<runReportRequest>';
        $xml .= self::xmlTag( 'reportName', $this->reportName );
        $xml .= $this->addParams( $this->getOptions() );
        $xml .= '</runReportRequest>';

        return $xml;
    }
    
    // Send a request to paypal
    public function sendRequest( $xml ) {
        //print "Sending: $xml\n";

        $ch = curl_init();
        curl_setopt( $ch, CURLOPT_URL, $this->payflowLive );
        curl_setopt( $ch, CURLOPT_HEADER, 0 );
        curl_setopt( $ch, CURLOPT_POSTFIELDS, $xml );
        curl_setopt( $ch, CURLOPT_POST, 1 );
        curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, FALSE );
        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, TRUE );
        
        $response = curl_exec( $ch );
        $headers = curl_getinfo( $ch );
        
        //print "Got $response\n";

        curl_close( $ch );

        return $response; 
    }

    // Given an options array create the paramName and paramValue parts of a report
    public function addParams( $options = array() ) {
        $xml = '';

        foreach( $options as $name => $value ) {
            $xml .= '<reportParam>';
            $xml .= self::xmlTag( 'paramName', $name );
            $xml .= self::xmlTag( 'paramValue', $value );
            $xml .= '</reportParam>'; 
        }
        return $xml;
    }
    
    public function setOptions( array $options ) {
        $this->options = $options;
    }
    
    /**
     * Fetch the parameters and options for a query
     * @param bool $ignore_reqd Wheh set to true, sanity check will not be performed
     * @throws Exception
     * @return array
     */
    public function getOptions( $ignore_reqd=false ) {
        if ( !$ignore_reqd && !$this->checkSanityOptions()) {
            throw new Exception( 'Missing required parameter in report: ' . get_class( $this ));
        }
        
        return $this->options;
    }
    
    /**
     * Perform sanity check on options array
     * 
     * Compares keys in options array against list of required params
     * @return bool
     */
    public function checkSanityOptions() {
        foreach ( $this->reqd_options as $reqd_option ) {
            if ( !in_array( $reqd_option, array_keys( $this->options ))) {
                return false;                
            }
        }
        
        return true;
    }
    
    public function setReqdOptions( array $reqd_options ) {
        $this->reqd_options = $reqd_options;
    }
    
    public function getReqdOptions() {
        return $this->reqd_options;
    }
}
?>