| Server IP : 10.200.247.200 / Your IP : 216.73.217.19 Web Server : Apache System : Linux synergy-usa-sites 6.8.0-138-generic #138-Ubuntu SMP PREEMPT_DYNAMIC Fri Jul 31 22:41:49 UTC 2026 x86_64 User : jeremy ( 1001) PHP Version : 8.4.25 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/usa_sites/governmentgrants.us/vx/lib/ |
Upload File : |
<?php
/**
* FluidPayHelper.php
*
* Shared helpers for the NMI -> FluidPay migration. Everything in here is the
* logic that would otherwise be copy/pasted between lcapi/inc/Billing.php,
* lcapi/CreateUser.php, pci_form/index.php and pci_form/update.php.
*
* The existing FluidPay blocks in lcapi/ChargeUser.php and
* cronscripts/monthly_billing/queue_process.php still have their own inline
* copies - they work and were left alone on purpose.
*/
//__DIR__ rather than DOCUMENT_ROOT: the monthly billing cron runs from the CLI,
//where DOCUMENT_ROOT is not set. FluidPay.php sits next to this file either way.
require_once(__DIR__.'/FluidPay.php');
if(!defined('FLUIDPAY_API_KEY')) { define('FLUIDPAY_API_KEY','api_3C36R2RndSzIAlwWX2WYC74PxMs'); }
if(!defined('FLUIDPAY_API_URL')) { define('FLUIDPAY_API_URL','https://app.fluidpay.com/api'); }
//if(!defined('FLUIDPAY_API_URL')) { define('FLUIDPAY_API_URL','https://sandbox.fluidpay.com/api'); } //Dev
/**
* Kill switch for the new signup path. Return false to send new accounts back
* through NMI without having to roll anything back.
*/
function fluidpay_enabled() {
return true;
}
function fluidpay_client() {
return new FluidPay(FLUIDPAY_API_KEY, FLUIDPAY_API_URL);
}
/**
* FluidPay wants MM/YY. NMI wanted MMYY.
*/
function fluidpay_expiration($month,$year) {
$month = preg_replace('/[^0-9]/','',$month);
$month = str_pad($month,2,'0',STR_PAD_LEFT);
$year = preg_replace('/[^0-9]/','',$year);
return $month.'/'.substr($year,-2);
}
/**
* FluidPay rejects the whole request with 'invalid Postal Code Length' rather than
* just failing AVS, so a ZIP+4 - '84604-1234', or '846041234' when they typed it
* without the dash - kills the sale outright. Force US codes down to 5 digits.
*
* Padding matters as much as truncating: a New England zip that has been through an
* int cast anywhere comes back as '2134', which is the same length error. This is the
* same normalization SiteDB::CreateLead() already does with sprintf('%05d'), except
* that sprintf pads without truncating, so 9 digit zips got through it untouched.
*
* Non-US codes are passed through trimmed only - Canadian and UK postal codes are
* alphanumeric and are not 5 of anything.
*/
function fluidpay_postal_code($zip,$country='US') {
$zip = trim((string) $zip);
//LoadUser() defaults missing address fields to '-'
if($zip=='' || $zip=='-') {
return '';
}
$country = strtoupper(trim((string) $country));
if($country!='' && $country!='US' && $country!='USA') {
return $zip;
}
$digits = preg_replace('/[^0-9]/','',$zip);
if($digits=='') {
return '';
}
return str_pad(substr($digits,0,5),5,'0',STR_PAD_LEFT);
}
/**
* Normalize an expiration year to 4 digits for storage.
*/
function fluidpay_exp_year($year) {
$year = preg_replace('/[^0-9]/','',$year);
if(strlen($year)==2) {
$year = '20'.$year;
}
return $year;
}
/**
* Map a legacy proc_id to a FluidPay processor.
*
* $proc_id_hint is the client's sticky lc_reseller.rebill_proc_id, $exclude is
* a proc_id we already know is bad (UserSignup::ChargeUser retries with one).
* Returns the FluidPay.proc_ids row (stdClass) or false when nothing is active.
*/
function fluidpay_pick_processor($db,$proc_id_hint='',$exclude='') {
if(empty($db)) {
return false;
}
if(!empty($proc_id_hint) && $proc_id_hint!=$exclude) {
$res = $db->get_results("SELECT * FROM FluidPay.proc_ids
WHERE proc_id='".$db->clean($proc_id_hint)."'
AND active=1
LIMIT 1");
if(!empty($res)) {
return $res[0];
}
}
$sql = "SELECT * FROM FluidPay.proc_ids WHERE active=1";
if(!empty($exclude)) {
$sql .= " AND proc_id!='".$db->clean($exclude)."'";
}
$sql .= " ORDER BY RAND() LIMIT 1";
$res = $db->get_results($sql);
if(!empty($res)) {
return $res[0];
}
return false;
}
/**
* Build a FluidPay billing_address out of a UserSignup object, applying the
* cardholder_name / billing_* overrides the NMI path used.
*
* LoadUser() defaults address/city/state to '-' when they weren't posted;
* sending that through as AVS data is worse than sending nothing.
*/
function fluidpay_billing_address($user) {
$first = $user->fname;
$last = $user->lname;
if(!empty($user->cardholder_name)) {
$name_arr = explode(' ',trim($user->cardholder_name));
$last = $name_arr[count($name_arr)-1];
$first = trim(str_replace(' '.$last,'',trim($user->cardholder_name)));
}
$street = (!empty($user->billing_street_address)) ? $user->billing_street_address : $user->address;
$city = (!empty($user->billing_city)) ? $user->billing_city : $user->city;
$state = (!empty($user->billing_state)) ? $user->billing_state : $user->state;
$zip = (!empty($user->billing_zip)) ? $user->billing_zip : $user->zip;
return array(
'first_name' => $first
,'last_name' => $last
,'address_line_1' => ($street=='-') ? '' : $street
,'city' => ($city=='-') ? '' : $city
,'state' => ($state=='-') ? '' : $state
,'postal_code' => fluidpay_postal_code($zip,$user->country)
,'country' => (!empty($user->country)) ? $user->country : 'US'
,'phone' => $user->phone
,'email' => $user->email
);
}
/**
* Vault a card without charging it.
*
* Pass an existing $customer_id to add another card to a customer we already
* have; leave it empty to create the customer first. The new card is always
* made the customer's default so a customerToken() charge without an explicit
* payment_method_id still hits the right card.
*
* Returns array('customer_id'=>..,'payment_id'=>..,'error'=>..).
*/
function fluidpay_enroll_card($fp,$description,$card,$billing,$customer_id='') {
$result = array('customer_id'=>'','payment_id'=>'','error'=>'');
if(empty($customer_id)) {
$customer = $fp->createCustomer(array(
'description' => $description
,'default_billing_address' => $billing
));
if(empty($customer['data']['id'])) {
$result['error'] = fluidpay_error_message($customer,'Unable to save your billing information. Please try again.');
return $result;
}
$customer_id = $customer['data']['id'];
}
$result['customer_id'] = $customer_id;
$card_result = $fp->addCustomerCard($customer_id,$card);
if(empty($card_result['data']['created_payment_method_id'])) {
$result['error'] = fluidpay_error_message($card_result,'Unable to save your credit card. Please double check the card number and try again.');
return $result;
}
$result['payment_id'] = $card_result['data']['created_payment_method_id'];
$fp->setDefaultPaymentMethod($customer_id,$result['payment_id'],'card');
return $result;
}
/**
* Pull something human readable out of a failed FluidPay response. The inline
* copies of this logic elsewhere just report 'Error', which makes a 401 or a
* 500 indistinguishable from a decline.
*/
function fluidpay_error_message($response,$default='') {
if(empty($response)) {
return ($default!='') ? $default : 'No response from the payment gateway.';
}
if(!empty($response['data']['response_body']['card']['processor_response_text'])) {
return $response['data']['response_body']['card']['processor_response_text'];
}
if(!empty($response['msg']) && $response['msg']!='success') {
return $response['msg'];
}
if(!empty($response['http_code']) && $response['http_code']>=400) {
return 'Payment gateway error (HTTP '.$response['http_code'].').';
}
return ($default!='') ? $default : 'Error';
}
/**
* Is this response text the processor/merchant account failing rather than the
* card being declined? Those are worth retrying on a different proc_id - nothing
* was authorized, so there is no double charge risk - and worth alerting on.
*
* The NMI wording is on the left, FluidPay's on the right. FluidPay prefixes its
* messages ("general error: daily transaction limit exceeded"), so this matches
* on substring rather than equality.
*
* Deliberately NOT in here: timeouts and empty responses. Those may have
* authorized before we lost the connection, so retrying them can double charge.
*/
function fluidpay_processor_level_failure($responsetext) {
if(trim($responsetext)=='') {
return false;
}
$processor_errors = array(
'invalid processor'
,'invalid merchant id'
,'authentication failed'
,'bad bin or host disconnect'
,'daily threshold exceeded' //NMI
,'daily transaction limit exceeded' //FluidPay
,'monthly transaction limit exceeded'
,'transaction limit exceeded'
,'processor not found'
,'processor is not active'
);
$responsetext = strtolower($responsetext);
foreach($processor_errors as $needle) {
if(strpos($responsetext,$needle)!==false) {
return true;
}
}
return false;
}
/**
* Flatten a FluidPay transaction response into the NMI-shaped response array
* the rest of the CRM expects: response 1 = approved, 2 = declined, 3 = error.
*/
function fluidpay_normalize_response($response) {
$normalized = array(
'response' => '3'
,'transactionid' => ''
,'responsetext' => 'Error'
,'authcode' => ''
,'avsresponse' => ''
,'cvvresponse' => ''
,'customer_id' => ''
,'payment_id' => ''
);
if(empty($response)) {
$normalized['responsetext'] = 'No response from the payment gateway.';
return $normalized;
}
if(!empty($response['data'])) {
$data = $response['data'];
$normalized['transactionid'] = (!empty($data['id'])) ? $data['id'] : '';
$normalized['customer_id'] = (!empty($data['customer_id'])) ? $data['customer_id'] : '';
$normalized['payment_id'] = (!empty($data['customer_payment_id'])) ? $data['customer_payment_id'] : '';
if(!empty($data['response_body']['card'])) {
$card = $data['response_body']['card'];
$normalized['authcode'] = (!empty($card['auth_code'])) ? $card['auth_code'] : '';
$normalized['avsresponse'] = (!empty($card['avs_response_code'])) ? $card['avs_response_code'] : '';
$normalized['cvvresponse'] = (!empty($card['cvv_response_code'])) ? $card['cvv_response_code'] : '';
if(!empty($card['response']) && $card['response']=='approved') {
$normalized['response'] = '1';
$normalized['responsetext'] = 'Approved';
} else {
$normalized['response'] = '2';
$normalized['responsetext'] = (!empty($card['processor_response_text'])) ? $card['processor_response_text'] : 'Declined';
}
return $normalized;
}
}
$normalized['responsetext'] = fluidpay_error_message($response,'Error');
return $normalized;
}
/**
* Read back the newest vaulted card for a client, or false when they have none.
*
* Newest row by ts wins - pci_form writes a new row every time somebody updates
* their card rather than replacing the old one, so the rebillers have to sort.
*/
function fluidpay_get_vault($db,$site_code,$client_id) {
if(empty($db) || empty($site_code) || empty($client_id)) {
return false;
}
$res = $db->get_results("SELECT * FROM FluidPay.customer_vault
WHERE site_code='".$db->clean($site_code)."'
AND client_id='".$db->clean($client_id)."'
ORDER BY ts DESC LIMIT 1");
if(empty($res)) {
return false;
}
return $res[0];
}
/**
* Store the FluidPay token for a client. This is what queue_process.php and
* ChargeUser.php read back to rebill them.
*
* $cc_num may be a full PAN or a masked '558958******0900' - both give the
* right bin/last 4 once the non digits are stripped.
*/
function fluidpay_save_vault($db,$site_code,$client_id,$customer_id,$payment_id,$cc_num,$exp_month,$exp_year) {
if(empty($db) || empty($site_code) || empty($client_id) || empty($customer_id)) {
return false;
}
$cc_num = preg_replace('/[^0-9]/','',$cc_num);
return $db->query("INSERT INTO FluidPay.customer_vault
(
site_code
,client_id
,customer_id
,payment_id
,bin_number
,last_4
,exp_month
,exp_year
,ts
) VALUES (
'".$db->clean($site_code)."'
,'".$db->clean($client_id)."'
,'".$db->clean($customer_id)."'
,'".$db->clean($payment_id)."'
,'".$db->clean(substr($cc_num,0,6))."'
,'".$db->clean(substr($cc_num,-4))."'
,'".$db->clean(str_pad(preg_replace('/[^0-9]/','',$exp_month),2,'0',STR_PAD_LEFT))."'
,'".$db->clean(fluidpay_exp_year($exp_year))."'
,NOW()
)");
}
/**
* Same as fluidpay_save_vault() but keyed on the allocator lead id, for cards
* captured through pci_form before the client account exists. CreateUser.php
* reads this back when it is called with an allocatorid and no cc_num.
*/
function fluidpay_save_vault_lead($db,$allocator_id,$customer_id,$payment_id,$cc_num,$exp_month,$exp_year) {
if(empty($db) || empty($allocator_id) || empty($customer_id)) {
return false;
}
$cc_num = preg_replace('/[^0-9]/','',$cc_num);
return $db->query("INSERT INTO FluidPay.customer_vault_leads
(
allocator_id
,customer_id
,payment_id
,bin_number
,last_4
,exp_month
,exp_year
,ts
) VALUES (
'".$db->clean($allocator_id)."'
,'".$db->clean($customer_id)."'
,'".$db->clean($payment_id)."'
,'".$db->clean(substr($cc_num,0,6))."'
,'".$db->clean(substr($cc_num,-4))."'
,'".$db->clean(str_pad(preg_replace('/[^0-9]/','',$exp_month),2,'0',STR_PAD_LEFT))."'
,'".$db->clean(fluidpay_exp_year($exp_year))."'
,NOW()
)");
}