/** * This will process a report text file as taken from PayPal. This will create any applicable PaypalBatch entries * and correlate with existing CreditCardPayment records. This will *only* process "Delayed Capture" transactions, * since that's all we actually care about. This will create "as unlinked" any CreditCardPayment records that had * to be created because it didn't exist (which means that this is a bad thing -- we should not have any of those). * * If there are any issues with importing, this will throw an error * * @param string $strReportText the text of the report itself from paypal * @param integer $intEntriesModified return value of the number of entries that were modified * @param integer $intEntriesAdded return value of the number of entries that were created (hopefully shouuld be zero) * @throws QCallerException */ public static function ProcessReport($strReportText, &$intEntriesModified, &$intEntriesAdded) { $intEntriesModified = 0; $intEntriesAdded = 0; // Cleanup Linebreaks $strReportText = str_replace("\r", "\n", $strReportText); while (strpos($strReportText, "\n\n") !== false) { $strReportText = str_replace("\n\n", "\n", $strReportText); } $strReportText = trim($strReportText); // Pull out the First Line (column headers) from the rest of the report $intPosition = strpos($strReportText, "\n"); $strFirstLine = substr($strReportText, 0, $intPosition); $strReportText = substr($strReportText, $intPosition + 1); // Calculate the tokens $strTokenArray = array(); $intColumnIndex = 0; foreach (explode("\t", $strFirstLine) as $strToken) { if (array_key_exists($strToken, $strTokenArray)) { throw new QCallerException('Report has a duplicate column: ' . $strToken); } $strTokenArray[$strToken] = $intColumnIndex; $intColumnIndex++; } // Ensure that the required fields exist foreach (self::$RequiredFields as $strRequiredToken) { if (!array_key_exists($strRequiredToken, $strTokenArray)) { throw new QCallerException('Report is missing required column: ' . $strRequiredToken); } } // Go through each line of the report foreach (explode("\n", $strReportText) as $strLine) { // Break out all cells, and then create a specific Values array that contain only the cells we care about, indexed by name $strCellArray = explode("\t", $strLine); $strValuesArray = array(); foreach (self::$RequiredFields as $strRequiredToken) { $strValuesArray[$strRequiredToken] = $strCellArray[$strTokenArray[$strRequiredToken]]; } // Only process "Delayed Capture" if ($strValuesArray['Type'] == 'Delayed Capture') { if (strlen(trim($strValuesArray[self::PayPalOriginalTransactionId])) == 0) { throw new QCallerException('Transaction ' . $strValuesArray[self::PayPalTransactionId] . ' does not have an Original Transaction Id'); } // Can we find the linked CCPayment Record? $objCreditCardPayment = CreditCardPayment::LoadByTransactionCode($strValuesArray[self::PayPalOriginalTransactionId]); if (!$objCreditCardPayment) { // No -- let's create this as an UNLINKED one $intEntriesAdded++; $objCreditCardPayment = new CreditCardPayment(); $objCreditCardPayment->TransactionCode = $strValuesArray[self::PayPalOriginalTransactionId]; $objCreditCardPayment->CreditCardStatusTypeId = CreditCardStatusType::Captured; $objCreditCardPayment->CreditCardLastFour = substr($strValuesArray[self::PayPalAccountNumber], strlen($strValuesArray[self::PayPalAccountNumber]) - 4); foreach (CreditCardType::$NameArray as $intId => $strName) { if (strtolower($strName) == strtolower($strValuesArray[self::PayPalTenderType])) { $objCreditCardPayment->CreditCardTypeId = $intId; } } if (!$objCreditCardPayment->CreditCardTypeId) { throw new QCallerException('Unlinked transaction contains an unknown credit card type: ' . $strValuesArray[self::PayPalTenderType]); } $objCreditCardPayment->UnlinkedFlag = true; // Setup Fields $objCreditCardPayment->AuthorizationCode = $strValuesArray[self::PayPalAuthCode]; $objCreditCardPayment->AmountCharged = $strValuesArray[self::PayPalAmount]; } // Link in the Pay Pal Batch Info (if applicable) // if (SERVER_INSTANCE == 'dev') $strValuesArray[self::PayPalBatchId] = 789; if ($intBatchNumber = trim($strValuesArray[self::PayPalBatchId])) { $objPayPalBatch = PaypalBatch::LoadByNumber($intBatchNumber); if (!$objPayPalBatch) { $objPayPalBatch = new PaypalBatch(); $objPayPalBatch->Number = $intBatchNumber; $objPayPalBatch->DateReceived = new QDateTime($strValuesArray[self::PayPalSettledDate]); $objPayPalBatch->ReconciledFlag = false; $objPayPalBatch->Save(); } if ($objCreditCardPayment->PaypalBatchId != $objPayPalBatch->Id) { $objCreditCardPayment->PaypalBatch = $objPayPalBatch; if ($objCreditCardPayment->Id) { $intEntriesModified++; } } } else { if (!is_null($objCreditCardPayment->PaypalBatchId)) { $objCreditCardPayment->PaypalBatch = null; if ($objCreditCardPayment->Id) { $intEntriesModified++; } } } // TODO: How do we account fo a "reconciled" PayPal batch that unexpectatly received another transaction not previously accounted for? // Check Fields to ensure match if ($objCreditCardPayment->AuthorizationCode != $strValuesArray[self::PayPalAuthCode]) { throw new QCallerException(sprintf('Mismatch AuthCode for Transaction %s: %s vs. %s', $strValuesArray[self::PayPalOriginalTransactionId], $strValuesArray[self::PayPalAuthCode], $objCreditCardPayment->AuthorizationCode)); } if ($objCreditCardPayment->AmountCharged != str_replace(',', '', $strValuesArray[self::PayPalAmount])) { throw new QCallerException(sprintf('Mismatch Amount for Transaction %s: %s vs. %s', $strValuesArray[self::PayPalOriginalTransactionId], $strValuesArray[self::PayPalAmount], $objCreditCardPayment->AmountCharged)); } // Update Fields $objCreditCardPayment->DateCaptured = new QDateTime($strValuesArray[self::PayPalTime]); $objCreditCardPayment->Save(); } } }
/** * The following will synchronously perform an "Authorization" for an UNSAVED PaymentObject against * a given Name, Address, Cc Credentials and Amount. If the authorization succeeds, a valid * CreditCardPayment object is returned. Otherwise, an error message is presented in String form. * * The actual "Capture" will be performed asynchronously by a separate cron-based CLI process. * * @param mixed $objPaymentObject this should be either an OnlineDonation or a SignupPayment object * @param array $arrPaymentObjectSaveChildrenCallback a callback to a method that will perform any children-save to the PaymentObject sent in * @param string $strFirstName * @param string $strLastName * @param Address $objAddress does not have to be linked to an actual db row * @param float $fltAmount * @param string $strCcNumber * @param string $strCcExpiration four digits, MMYY format * @param string $strCcCsc * @param integer $intCreditCardTypeId * @return mixed a CreditCardPayment object if authorization successful, otherwise a string-based message on why it failed */ public static function PerformAuthorization($objPaymentObject, $arrPaymentObjectSaveChildrenCallback, $strFirstName, $strLastName, Address $objAddress, $fltAmount, $strCcNumber, $strCcExpiration, $strCcCsc, $intCreditCardTypeId) { // Ensure a "Valid" PaymentObject if (!$objPaymentObject instanceof SignupPayment && !$objPaymentObject instanceof OnlineDonation) { throw new QCallerException('Supplied PaymentObject is not an instance of SignupPayment or OnlineDonation'); } if ($objPaymentObject->Id) { throw new QCallerException('Supplied PaymentObject has already been saved'); } if ($objPaymentObject->CreditCardPaymentId) { throw new QCallerException('Supplied PaymentObject already has a linked CCPayment object'); } CreditCardPayment::GetDatabase()->TransactionBegin(); try { // Save the PaymentObject itself $objPaymentObject->Save(); // Make a call to save children (if applicable) call_user_func($arrPaymentObjectSaveChildrenCallback, $objPaymentObject); $strClassName = get_class($objPaymentObject); switch ($strClassName) { case 'SignupPayment': $strComment1 = 'Signup Payment ' . $objPaymentObject->Id; $strComment2 = 'SE' . $objPaymentObject->SignupEntry->Id . ' - ' . 'SF' . $objPaymentObject->SignupEntry->SignupFormId . ' - ' . 'P' . $objPaymentObject->SignupEntry->PersonId; $strInvoiceNumber = 'SP' . $objPaymentObject->Id; break; case 'OnlineDonation': $strComment1 = 'Online Donation ' . $objPaymentObject->Id; $strComment2 = 'P' . $objPaymentObject->PersonId; $strInvoiceNumber = 'OD' . $objPaymentObject->Id; break; default: throw new Exception('Unsupported: ' . $strClassName); } $strNvpRequestArray = self::PaymentGatewayGenerateAuthorizationPayload($strFirstName, $strLastName, $objAddress, $fltAmount, $strCcNumber, $strCcExpiration, $strCcCsc, $strComment1, $strComment2, $strInvoiceNumber); $strNvpResponseArray = self::PaymentGatewaySubmitRequest($strNvpRequestArray); if (!is_array($strNvpResponseArray)) { CreditCardPayment::GetDatabase()->TransactionRollBack(); return 'Could Not Connect to Payment Gateway'; } // Analyze the ResponseArray if (!array_key_exists('RESULT', $strNvpResponseArray)) { CreditCardPayment::GetDatabase()->TransactionRollBack(); return 'Missing Result Code from Payment Gateway'; } if (!array_key_exists('RESPMSG', $strNvpResponseArray)) { CreditCardPayment::GetDatabase()->TransactionRollBack(); return 'Missing Response from Payment Gateway'; } if (!array_key_exists('PNREF', $strNvpResponseArray)) { CreditCardPayment::GetDatabase()->TransactionRollBack(); return 'Missing Reference ID from Payment Gateway'; } // Fill in the blanks if (!array_key_exists('CVV2MATCH', $strNvpResponseArray)) { $strNvpResponseArray['CVV2MATCH'] = ''; } if (!array_key_exists('AVSADDR', $strNvpResponseArray)) { $strNvpResponseArray['AVSADDR'] = '?'; } if (!array_key_exists('AVSZIP', $strNvpResponseArray)) { $strNvpResponseArray['AVSZIP'] = '?'; } if (!array_key_exists('IAVS', $strNvpResponseArray)) { $strNvpResponseArray['IAVS'] = '?'; } if (!array_key_exists('AUTHCODE', $strNvpResponseArray)) { $strNvpResponseArray['AUTHCODE'] = ''; } // If Failure, cleanup and then report if ($strNvpResponseArray['RESULT'] != 0) { CreditCardPayment::GetDatabase()->TransactionRollBack(); return sprintf('%s (%s)', $strNvpResponseArray['RESPMSG'], $strNvpResponseArray['RESULT']); } // If CVV2 Failed, then Report if ($strNvpResponseArray['CVV2MATCH'] == 'N') { CreditCardPayment::GetDatabase()->TransactionRollBack(); $strNvpRequestArray = self::PaymentGatewayGenerateVoidPayload($strNvpResponseArray['PNREF']); $strNvpResponseArray = self::PaymentGatewaySubmitRequest($strNvpRequestArray); return 'The CVV2 code entered is invalid. Please double check the 3-digit CVV2 code on the back of your card. (' . $strNvpResponseArray['RESULT'] . ')'; } // If we are here, we had a successful authorization! $objCreditCardPayment = new CreditCardPayment(); $objCreditCardPayment->CreditCardStatusTypeId = CreditCardStatusType::Authorized; $objCreditCardPayment->CreditCardLastFour = substr($strCcNumber, strlen($strCcNumber) - 4); $objCreditCardPayment->CreditCardTypeId = $intCreditCardTypeId; $objCreditCardPayment->TransactionCode = $strNvpResponseArray['PNREF']; $objCreditCardPayment->AuthorizationCode = $strNvpResponseArray['AUTHCODE']; $objCreditCardPayment->AddressMatchCode = $strNvpResponseArray['AVSADDR'] . $strNvpResponseArray['AVSZIP'] . $strNvpResponseArray['IAVS']; $objCreditCardPayment->DateAuthorized = QDateTime::Now(); $objCreditCardPayment->AmountCharged = $fltAmount; $objCreditCardPayment->UnlinkedFlag = false; // Save, Commit and Return $objCreditCardPayment->Save(); $objPaymentObject->CreditCardPayment = $objCreditCardPayment; $objPaymentObject->Save(); CreditCardPayment::GetDatabase()->TransactionCommit(); } catch (Exception $objExc) { CreditCardPayment::GetDatabase()->TransactionRollBack(); throw $objExc; } $objPaymentObject->RefreshDetailsWithCreditCardPayment(); return $objCreditCardPayment; }