Skip to content

Commit b20bc98

Browse files
committed
fixes handling unsetting ot_cc and validating cc_exp
1 parent e7ba94e commit b20bc98

1 file changed

Lines changed: 45 additions & 0 deletions

File tree

src/cc.inc.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,33 @@ function can_use_cc($data, $ccData = false, $check_disabled_cc = true, $cc_field
218218
}
219219
}
220220
}
221+
// A missing or expired expiration date makes a card unusable regardless of whitelist/fraud status.
222+
// Without this, get_next_cc() can hand back an expired or exp-less backup card whose charge then
223+
// falls through to the fabricated fallback expiration date and is guaranteed to decline.
224+
if (is_array($ccData)) {
225+
$cc_exp_raw = isset($ccData['cc_exp']) ? trim((string) $ccData['cc_exp']) : '';
226+
if ($cc_exp_raw === '') {
227+
$reason .= ' No Credit-Card expiration date set.';
228+
$cc_usable = false;
229+
} else {
230+
if (mb_strpos($cc_exp_raw, '/') === false) {
231+
$cc_exp_raw = mb_substr($cc_exp_raw, 0, 2).'/'.mb_substr($cc_exp_raw, 2);
232+
}
233+
$exp_parts = explode('/', str_replace(' ', '', $cc_exp_raw));
234+
$exp_month = isset($exp_parts[0]) && $exp_parts[0] !== '' ? (int) $exp_parts[0] : 0;
235+
$exp_year = $exp_parts[1] ?? '';
236+
if (mb_strlen($exp_year) == 2) {
237+
$exp_year = '20'.$exp_year;
238+
}
239+
$exp_year = (int) $exp_year;
240+
// only flag when the date parses cleanly and is in the past — never reject an unparseable format
241+
if ($exp_month >= 1 && $exp_month <= 12 && $exp_year >= 2000
242+
&& ($exp_year < (int) date('Y') || ($exp_year == (int) date('Y') && $exp_month < (int) date('n')))) {
243+
$reason .= ' Credit-Card has expired ('.$exp_month.'/'.$exp_year.').';
244+
$cc_usable = false;
245+
}
246+
}
247+
}
221248
if ($set_global_reason === true) {
222249
$GLOBALS['cc_reason'] = trim($reason);
223250
}
@@ -345,6 +372,20 @@ function charge_card($custid, $amount = false, $invoice = false, $module = 'defa
345372
$response['code'] = 0;
346373
//$cc = $data['cc'];
347374
$ccs = parse_ccs($data);
375+
// The "No CC Expiration Date On File" guard above only fires on the primary-card path (no ot_cc).
376+
// Mirror it for the backup-card (ot_cc) retry path: if the selected backup card has no usable
377+
// expiration date, abort instead of charging with the fabricated fallback exp (which always declines).
378+
if (isset(App::variables()->request['ot_cc'])) {
379+
$ot_cc_idx = App::variables()->request['ot_cc'];
380+
if (!isset($ccs[$ot_cc_idx]) || !is_array($ccs[$ot_cc_idx]) || !isset($ccs[$ot_cc_idx]['cc_exp']) || trim((string) $ccs[$ot_cc_idx]['cc_exp']) == '') {
381+
global $webpage;
382+
if (isset($webpage) && $webpage == true) {
383+
add_output('<div class="container alert alert-danger"><strong>Error! No CC Expiration Date On File! </strong>We have no credit-card exp date on file. Please go to Billing -> Manage Credit Cards and set one or contact support for assistance.</div>');
384+
}
385+
myadmin_log('billing', 'notice', "Aborting charge for customer {$custid}: selected backup card (ot_cc={$ot_cc_idx}) has no expiration date on file", __LINE__, __FILE__);
386+
return $retval;
387+
}
388+
}
348389
$cc = isset(App::variables()->request['ot_cc']) && isset($ccs[App::variables()->request['ot_cc']]) && isset($ccs[App::variables()->request['ot_cc']]['cc']) ? App::decrypt($ccs[App::variables()->request['ot_cc']]['cc']) : (isset($data['cc']) ? App::decrypt($data['cc']) : null);
349390
if (is_null($cc)) {
350391
if (isset($webpage) && $webpage == true) {
@@ -784,6 +825,10 @@ function retry_charge_card($custid, $amount = false, $invoice = false, $module =
784825
App::variables()->request['ot_cc'] = $next_cc;
785826
App::variables()->request['retry_cc'] = 1;
786827
$success = charge_card($custid, $amount, $invoice, $module, $returnURL, $useHandlePayment, $queue);
828+
// Clear the retry markers so they cannot leak into the next customer charged in the same
829+
// process (e.g. the billingd loop), which would otherwise index into the wrong customer's
830+
// ccs array and charge their primary card with the fabricated fallback expiration date.
831+
unset(App::variables()->request['ot_cc'], App::variables()->request['retry_cc']);
787832
if ($success) {
788833
myadmin_log('billing', 'info', "Retrying BackupCC - Success for $custid, Amount: $amount, CC ID - $next_cc", __LINE__, __FILE__);
789834
return true;

0 commit comments

Comments
 (0)