PaymentIntent fix guide
August 25, 2025 · AI Conversations
<script>
window.location='https://fossettscircus.com/success/?eid=" . $order_info->event_date_id . "&oc=" . $order_info->order_code . "';
</script>
' ";
goto continue_with_wordpress;
exit();
}
else
{
sendErrorMail( "Order not saved", "Order not saved but charge paid" . var_export( $order_info, true ) );
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Order could not be saved'
]);
exit;
}
}
}
elseif ($intent->status == 'processing') {
echo json_encode([
'success' => false,
'message' => 'Payment is processing. Please wait a moment and try again.',
'status' => 'processing'
]);
exit;
} elseif ($intent->status == 'requires_payment_method') {
echo json_encode([
'success' => false,
'message' => 'Payment method failed. Please try again with a different card.',
'status' => 'requires_payment_method'
]);
exit;
} elseif ($intent->status == 'requires_confirmation') {
echo json_encode([
'success' => false,
'message' => 'Payment requires confirmation. Please try again.',
'status' => 'requires_confirmation'
]);
exit;
} else {
echo json_encode([
'success' => false,
'message' => 'Payment failed: ' . $intent->status,
'status' => $intent->status,
'errors' => ['There was an error during the transaction, charge was not paid.']
]);
exit;
}
}
catch(StripeErrorCard $e)
{
// Log payment failure
$debug_logger->logPaymentFailure($_POST['payment_token'] ?? 'unknown', $e->getMessage(), 'card_error');
$body = $e->getJsonBody();
$err = $body['error'];
array_push( $order_errors, $err['message'] );
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => $err['message'],
'errors' => $order_errors
]);
exit;
}
}
catch (Exception $e)
{
// Log payment failure
$debug_logger->logPaymentFailure($_POST['payment_token'] ?? 'unknown', $e->getMessage(), 'general_error');
if ( is_object( $e ) && method_exists( $e, 'getJsonBody' ) )
{
$body = $e->getJsonBody();
$err = $body['error'];
array_push( $order_errors, $err['message'] );
}
array_push( $order_errors, 'There was an error during the transaction. Payment not performed.' );
sendErrorMail( "Exception", var_export( $e, true ) );
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Payment processing error',
'errors' => $order_errors
]);
exit;
}
}
} // /Process payment
} // /Form submitted
?>
<style>
<?php echo file_get_contents( __DIR__ . '/css/style.css'); ?>
/*.div-wrapper{
padding: 0em 1em !important;
}*/
.payment-fields {
display: block !important;
padding: 1em !important;
border-radius: 5px;
width: 100%;
height: 3em;
color: rgb(66, 109, 143);
background-color: rgb(255,255,255);
font-weight: bold;
}
</style>
<script>
<?php echo file_get_contents( __DIR__ . '/js/hashes.js'); ?>
</script>
<script>
</script>
<script>
var event_seat_location_options;
var eventData = <?php echo json_encode($event_data); ?>;
jQuery(document).ready(function() {
if (!eventData || !eventData.event_dates) {
console.error('Event data not available');
return;
}
var latest_available_seat = jQuery('#event-order [name="event_seat_location"] option:selected').val();
event_seat_location_options = jQuery('#event-order [name="event_seat_location"]').html();
jQuery('#event-order [name="event_seat_location"]').html('<option value="">Pick a seat location</option>');
updateAvailableLocations();
jQuery('#event-order [name="event_seat_location"]').val(latest_available_seat);
});
function updateFormSent()
{
var selectedDatetime = jQuery('#event-order [name="event_datetime"]').val();
jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').removeAttr('disabled');
jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').show();
updateTotal();
}
function updateAvailableLocations() {
if (!eventData || !eventData.event_dates) {
console.error('Event data not available');
return;
}
// reset options
jQuery('#event-order [name="event_seat_location"]').html(event_seat_location_options);
var selectedDatetime = jQuery('#event-order [name="event_datetime"]').val();
jQuery('#event-order [name="event_seat_location"] option').prop('disabled', true).hide();
jQuery('#event-order [name="event_seat_location"] option:first').prop('disabled', false).show();
jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').prop('disabled', false).show();
// remove unwanted options
jQuery('#event-order [name="event_seat_location"]').find('option:disabled').remove();
jQuery('#event-order [name="event_seat_location"]').val('');
}
function updateAvailableSeats()
{
var seatsLeft = parseInt( jQuery('#event-order [name="event_seat_location"] option:selected').attr('data-left') );
var seatsSelected = parseInt( jQuery('#event-order [name="event_seat_full"]').val() ) + parseInt( jQuery("#event-order [name='event_seat_child']").val() ) + parseInt( jQuery("#event-order [name='event_seat_senior']").val() );
if ( seatsSelected > seatsLeft )
{
jQuery('#event-order .warning-seats').html('You selected more tickets than are available right now in ' + jQuery("#event-order [name='event_seat_location'] option:selected").text() + ', please pick a different location or reduce the number of tickets by ' + ( seatsSelected - seatsLeft ) + '.' );
jQuery('#event-order .warning-seats').show();
return false;
}
else
{
jQuery('#event-order .warning-seats').hide();
jQuery('#event-order .warning-seats').html('');
return true;
}
}
var V = JSON.parse( '<?php echo $event_data->event_vouchers; ?>' );
var D = 0;
var T = '';
function voucherValid()
{
var SHA1 = new Hashes.SHA1;
var customer_voucher = SHA1.hex( jQuery("#event-order [name='voucher']").val().toLowerCase() );
for (var i = 0; i < V.length; i++)
{
if ( customer_voucher == V[i].voucher_code )
{
D = V[i].voucher_discount;
T = V[i].voucher_discount_type;
return true;
}
}
return false;
}
function updateTotal()
{
var seatsFull = parseInt( jQuery('#event-order [name="event_seat_full"]').val() );
var seatsChild = parseInt( jQuery('#event-order [name="event_seat_child"]').val() );
var seatsSenior = parseInt( jQuery('#event-order [name="event_seat_senior"]').val() );
var priceFull = parseFloat( jQuery('#event-order [name="event_seat_location"]').find('option:selected').attr( 'data-price-full' ) );
var priceReduced = parseFloat( jQuery('#event-order [name="event_seat_location"]').find('option:selected').attr( 'data-price-reduced' ) );
var total = ( seatsFull * priceFull ) + ( ( seatsChild + seatsSenior ) * priceReduced );
if ( voucherValid() )
{
if ( T == 'percentage' )
{
total = total * ( 1 - ( D / 100 ) );
}
else if ( T == 'amount' )
{
total = total - D;
}
}
var currency = '<?php echo $currency_symbol; ?>';
if ( isNaN( total ) )
{
jQuery('#event-order .total').html('');
}
else
{
jQuery('#event-order .total').html('Total: '+ currency + total );
}
}
jQuery(document).ready(function()
{
/*var stripe = Stripe('pk_test_VBObL72a6bVf4DAEpzOKvlh1', { // thenet
stripeAccount: 'acct_1C7mK9IQtQgABICR' // fossetts
});*/
var stripe = Stripe('pk_live_KSh8MRi76Kliuk4fUpBZ23GS', { // thenet live mode
stripeAccount: 'acct_1C7mK9IQtQgABICR' // fossetts
});
<?php if ( isset( $_POST['payment_intent_client_secret'] ) ): ?>
stripe.handleCardAction(
'<?php echo $_POST['payment_intent_client_secret']; ?>'
).then(function(result)
{
if (result.error)
{
jQuery('#event-order .warning-payment').html( result.error.message );
jQuery('#event-order .warning-payment').show();
}
else
{
jQuery('[name="payment_intent_id"]').val( result.paymentIntent.id );
jQuery('#event-order .warning-payment').hide();
jQuery('#event-order .warning-payment').html( '' );
jQuery("#event-order #event-order-form").trigger('submit');
}
});
<?php else: ?>
var elements = stripe.elements();
var paymentStyle = {
base: {
color: 'rgb(66, 109, 143)',
'::placeholder': {
color: 'rgba(66, 109, 143, 0.7)',
},
}
};
// var card = elements.create('card', { hidePostalCode: true, style: paymentStyle });
var card = elements.create('cardNumber', { showIcon: true, style: paymentStyle });
var cardExpiry = elements.create('cardExpiry', { style: paymentStyle });
var cardCvc = elements.create('cardCvc', { style: paymentStyle });
card.addEventListener('change', function( event )
{
if ( event.error )
{
jQuery('#event-order .warning-payment').html( event.error.message );
jQuery('#event-order .warning-payment').show();
}
else
{
jQuery('#event-order .warning-payment').hide();
jQuery('#event-order .warning-payment').html( '' );
}
});
// card.mount('#event-order #payment-fields');
card.mount('#event-order #card-number');
cardExpiry.mount('#event-order #card-expiry');
cardCvc.mount('#event-order #card-cvc');
<?php endif; ?>
jQuery("#event-order [name='event_datetime']").on("change", function()
{
updateAvailableLocations();
updateTotal();
});
jQuery("#event-order [name='event_seat_location'], #event-order [name='event_seat_full'], #event-order [name='event_seat_child'], #event-order [name='event_seat_senior']").on("change", function()
{
updateAvailableSeats();
updateTotal();
});
jQuery("#event-order [name='voucher']").on('input',function()
{
if ( voucherValid() )
{
jQuery('.warning-voucher').hide();
jQuery('.success-voucher').show();
}
else
{
if ( jQuery(this).val() == '' )
{
jQuery('.success-voucher').hide();
jQuery('.warning-voucher').hide();
}
else
{
jQuery('.success-voucher').hide();
jQuery('.warning-voucher').show();
}
}
updateTotal();
});
jQuery("#event-order .order-submit").on('click', function()
{
jQuery("#event-order .order-submit").prop('disabled', true);
if ( updateAvailableSeats() )
{
// Stripe SCA
stripe.createPaymentMethod('card', card, {
billing_details: { name: jQuery('[name="customer_name"]').val() }
})
//stripe.createToken( card )
.then( function(result)
{
if ( result.error )
{
jQuery('#event-order .warning-payment').html( result.error.message );
jQuery('#event-order .warning-payment').show();
jQuery("#event-order .order-submit").prop('disabled', false);
}
else
{
jQuery('#event-order [name="payment_token"]').val( result.paymentMethod.id );
//jQuery('#event-order [name="payment_token"]').val( result.token.id );
jQuery('#event-order .warning-payment').hide();
jQuery('#event-order .warning-payment').html( '' );
jQuery("#event-order #event-order-form").trigger('submit');
}
});
}
else
{
jQuery("#event-order .order-submit").prop('disabled', false);
}
});
/*jQuery("#event-order #event-order-form").on("submit", function(e)
{
jQuery("#event-order .order-submit").trigger('click');
e.preventDefault();
});*/
updateFormSent();
});
</script>
<script>
jQuery(document).ready(function()
{
jQuery('html, body').animate(
{
scrollTop: ( jQuery('.form-errors').first().offset().top - 10 )
}, 500 );
});
</script>
<script>
jQuery(document).ready(function()
{
function checkEmailValue(value, arr) {
var status = 'not_exist';
for (var i = 0; i < arr.length; i++) {
var name = arr[i];
if (name == value) {
status = 'exist';
break;
}
}
return status;
}
//code to check if a value exists in an array
var email_pool = ['gmail.com','yahoo.com','hotmail.com','aol.com','hotmail.co.uk','hotmail.fr','msn.com','yahoo.fr','wanadoo.fr','orange.fr','comcast.net','yahoo.co.uk','yahoo.com.br','yahoo.co.in','live.com','rediffmail.com','free.fr','gmx.de','web.de','yandex.ru','ymail.com','libero.it','outlook.com','uol.com.br','bol.com.br','mail.ru','cox.net','hotmail.it','sbcglobal.net','sfr.fr','live.fr','verizon.net','live.co.uk','googlemail.com','yahoo.es','ig.com.br','live.nl','bigpond.com','terra.com.br','yahoo.it','neuf.fr','yahoo.de','alice.it','rocketmail.com','att.net','laposte.net','facebook.com','bellsouth.net','yahoo.in','hotmail.es','charter.net','yahoo.ca','yahoo.com.au','rambler.ru','hotmail.de','tiscali.it','shaw.ca','yahoo.co.jp','sky.com','earthlink.net','optonline.net','freenet.de','t-online.de','aliceadsl.fr','virgilio.it','home.nl','qq.com','telenet.be','me.com','yahoo.com.ar','tiscali.co.uk','yahoo.com.mx','voila.fr','gmx.net','mail.com','planet.nl','tin.it','live.it','ntlworld.com','arcor.de','yahoo.co.id','frontiernet.net','hetnet.nl','live.com.au','yahoo.com.sg','zonnet.nl','club-internet.fr','juno.com','optusnet.com.au','blueyonder.co.uk','bluewin.ch','skynet.be','sympatico.ca','windstream.net','mac.com','centurytel.net','chello.nl','live.ca','aim.com','bigpond.net.au'];
jQuery( "#confirm_email_address" ).focusout(function() {
jQuery("#confirm_email_error").css("display", (jQuery("#email_address").val() != jQuery("#confirm_email_address").val()) ? "inline-block" : "none");
var user_email = jQuery("#email_address").val();
var domain_name = user_email.indexOf("@");
var slice_email = user_email.slice((domain_name + 1), user_email.length);
if(jQuery("#email_address").val() == jQuery("#confirm_email_address").val()){
// if(!jQuery.inArray(slice_email, email_pool)){
if(checkEmailValue(slice_email, email_pool) == 'not_exist'){
jQuery("#confirm_email_error2").css("display", "inline-block");
jQuery("#confirm_email_error2").html("Hmmm, are you sure... <strong>"+user_email+"</strong> is the correct email address?");
}else{
jQuery("#confirm_email_error2").css("display", "none");
}
}
});
});
</script>
<style>
.email_address_error{
margin-top: 5px !important;
color: rgb(255, 255, 255);
background-color: rgb(237, 30, 38);
font-weight: normal;
padding: 0.3em 15px;
}.email_address_warning{
margin-top: 5px !important;
color: rgb(255, 255, 255);
background-color: #25a828;
font-weight: normal;
padding: 0.3em 15px;
}
</style>
Contact details
—
# What to change (quick hits)
1) **Use an idempotency key when creating the PaymentIntent**
Prevents two near‑simultaneous POSTs from creating/confirming two different Intents.
2) **Stop blindly attaching the PaymentMethod**
Only attach if it’s **not** already attached; if it is attached to a *different* customer, use **that** customer instead of creating a new one.
(Alternative: don’t set `customer` at all for one‑off payments.)
3) **Normalize the flow**
– If you already have a `payment_intent_id`, **retrieve + confirm** it.
– If you only have a `payment_token` (pm_…), **create** a PI **with idempotency**, then confirm.
4) **Fix your logs and guards**
– Validate that a “PaymentIntent ID” starts with `pi_`.
– Log `pm_…` as **PaymentMethod ID**, not `payment_intent_id`.
– If `pm.customer` exists and differs, **do not** attempt `attach` (that’s the error you saw).
5) **Optional hardening**
– Reuse a single Customer per email on the **connected account**.
– Consider skipping `customer` entirely if you don’t need to save the card.
– Move live keys to env vars.
—
# Drop‑in patch (replace your “create PI” block)
> Replace the section where you currently:
> `Customer::create(…)` → `PaymentMethod::retrieve(…)->attach(…)` → `PaymentIntent::create(…, confirm => true)`
// --- Build a stable idempotency key for this cart to prevent double-charges ---
$cart_fingerprint = json_encode([
'event_id' => $event_id,
'event_date_id' => $event_date_id,
'email' => $_POST['customer_email'] ?? '',
'amount_cents' => $amount_in_cents,
'tickets' => $ticket_quantities, // seating_area_ticket_type => qty
'addons' => processAddons($_POST) // keep same logic you have
]);
$idempotencyKey = 'fos-' . hash('sha256', $cart_fingerprint);
// --- Connected account & API key ---
StripeStripe::setApiKey(getenv('STRIPE_SECRET') ?: "sk_live_AhoYJCZ6tXIi9HRzh6XY7nqO");
$connectOpts = [ 'stripe_account' => "acct_1C7mK9IQtQgABICR", 'idempotency_key' => $idempotencyKey ];
// --- If this is a 3DS continuation, prefer retrieving the existing PI ---
if (!empty($_POST['payment_intent_id']) && str_starts_with($_POST['payment_intent_id'], 'pi_')) {
$intent = StripePaymentIntent::retrieve($_POST['payment_intent_id'], $connectOpts);
$intent->confirm([], $connectOpts);
} else {
// Fresh attempt with a PaymentMethod
if (empty($_POST['payment_token']) || !str_starts_with($_POST['payment_token'], 'pm_')) {
throw new Exception('Invalid payment token.');
}
// Retrieve the PaymentMethod ON THE CONNECTED ACCOUNT
$pm = StripePaymentMethod::retrieve($_POST['payment_token'], [ 'stripe_account' => $connectOpts['stripe_account'] ]);
// Decide which customer to use
$customerId = null;
// (A) If PM already linked to a customer, reuse that customer (don’t reattach!)
if (!empty($pm->customer)) {
$customerId = $pm->customer;
} else {
// (B) Otherwise, try to find or create a customer for this email (on connected account)
$email = trim($_POST['customer_email'] ?? '');
$name = trim($_POST['customer_name'] ?? '');
try {
// Stripe Search API (available in PHP SDK). If not enabled, fallback to Customer::create directly.
$found = StripeCustomer::search(
['query' => "email:'{$email}'"],
[ 'stripe_account' => $connectOpts['stripe_account'] ]
);
if (!empty($found->data) && isset($found->data[0])) {
$customerId = $found->data[0]->id;
}
} catch (Exception $e) {
// swallow and create below
}
if (!$customerId) {
$customer = StripeCustomer::create(
['email' => $email, 'name' => $name],
[ 'stripe_account' => $connectOpts['stripe_account'] ]
);
$customerId = $customer->id;
}
// Attach only if PM has no customer yet
StripePaymentMethod::retrieve($_POST['payment_token'], [ 'stripe_account' => $connectOpts['stripe_account'] ])
->attach(['customer' => $customerId], [ 'stripe_account' => $connectOpts['stripe_account'] ]);
}
// Create + confirm the PaymentIntent atomically with idempotency
$intent = StripePaymentIntent::create([
'amount' => $amount_in_cents,
'currency' => $event_data->event['currency'],
'payment_method' => $pm->id,
'customer' => $customerId, // if you don’t need to save cards, you could omit this line
'application_fee_amount' => $fee_in_cents,
'description' => "Fossetts charge for " . ($_POST['customer_email'] ?? ''),
'metadata' => ['Location' => $event_data->event['area']],
'confirm' => true,
'return_url' => 'https://fossettscircus.com/success/',
'automatic_payment_methods' => ['enabled' => true, 'allow_redirects' => 'never'],
], $connectOpts);
}
// --- Handle statuses exactly once ---
if ($intent->status === 'requires_action' && $intent->next_action->type === 'use_stripe_sdk') {
echo json_encode([
'requires_action' => true,
'payment_intent_client_secret' => $intent->client_secret,
'payment_intent_id' => $intent->id,
'status' => 'requires_action'
]);
exit;
}
if ($intent->status === 'succeeded') {
// proceed with your existing addon/ticket/order save flow
}
### Notes on the patch
– **Idempotency**: the `$idempotencyKey` ensures a second POST (reload/double‑tap) returns the **same** PI rather than creating a new one.
– **No illegal attach**: if the `pm` is already attached to a customer, we **reuse that customer** and skip `attach()`.
– **Guarding IDs**: we check prefixes (`pi_`, `pm_`) to avoid mislabeling in logs.
– **Connected account**: every Stripe call uses `['stripe_account' => 'acct_…']`.
– **Search API**: if Customer Search isn’t enabled, just create the customer (or maintain your own email→customer map in DB).
—
# Fix your logging (quick snippet)
Change your logging block where you currently log a `payment_intent_id` but pass a `pm_…`:
$debug_logger->logPaymentIntent(
$intent->id, // real PI id: pi_...
$amount_in_cents,
$event_data->event['currency'],
$_POST['customer_email'],
['Location' => $event_data->event['area'], 'payment_method_id' => $_POST['payment_token'] ?? null]
);
And anywhere you print a **PaymentIntent ID**, ensure it **starts with `pi_`**. If it’s `pm_`, log it as **PaymentMethod ID**.
—
# Make the frontend retry path clean
Your `requires_action` branch is fine — just be sure the client passes back the **same** `payment_intent_id` you returned (not a PM id), and you **only** run the “create PI” path when there’s **no** `payment_intent_id` present.
—
# Optional: simpler, even safer flow
If you don’t need to save cards:
– **Do not create a Customer** and **do not attach** the PaymentMethod.
– Just create PI with `payment_method` and `confirm: true`.
That alone will avoid the “already attached” error class because there’s no cross‑customer constraint.
—
# Session/nonce note
You mentioned `session_id` empty in both posts. In many in‑app browsers (FB/IG), cookies can get lost. Add a hidden nonce and verify it server‑side, or switch to a “create PI server‑side → confirm with Payment Element client‑side” flow. Idempotency already saves you from accidental double‑charges.
—
# TL;DR
– Add **idempotency key** ✅
– **Reuse** existing PM’s customer, **don’t reattach** ✅
– Guard **pi_** vs **pm_** ✅
– (Optionally) **skip customers** for one‑off payments ✅
If you want, paste me the latest error line after this patch and I’ll sanity‑check the final statuses you should handle.
<script>
window.location='https://fossettscircus.com/success/?eid=" . $order_info->event_date_id . "&oc=" . $order_info->order_code . "';
</script>
' ";
goto continue_with_wordpress;
exit();
}
else
{
sendErrorMail( "Order not saved", "Order not saved but charge paid" . var_export( $order_info, true ) );
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Order could not be saved'
]);
exit;
}
}
}
elseif ($intent->status == 'processing') {
echo json_encode([
'success' => false,
'message' => 'Payment is processing. Please wait a moment and try again.',
'status' => 'processing'
]);
exit;
} elseif ($intent->status == 'requires_payment_method') {
echo json_encode([
'success' => false,
'message' => 'Payment method failed. Please try again with a different card.',
'status' => 'requires_payment_method'
]);
exit;
} elseif ($intent->status == 'requires_confirmation') {
echo json_encode([
'success' => false,
'message' => 'Payment requires confirmation. Please try again.',
'status' => 'requires_confirmation'
]);
exit;
} else {
echo json_encode([
'success' => false,
'message' => 'Payment failed: ' . $intent->status,
'status' => $intent->status,
'errors' => ['There was an error during the transaction, charge was not paid.']
]);
exit;
}
}
catch(StripeErrorCard $e)
{
// Log payment failure
$debug_logger->logPaymentFailure($_POST['payment_token'] ?? 'unknown', $e->getMessage(), 'card_error');
$body = $e->getJsonBody();
$err = $body['error'];
array_push( $order_errors, $err['message'] );
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => $err['message'],
'errors' => $order_errors
]);
exit;
}
}
catch (Exception $e)
{
// Log payment failure
$debug_logger->logPaymentFailure($_POST['payment_token'] ?? 'unknown', $e->getMessage(), 'general_error');
if ( is_object( $e ) && method_exists( $e, 'getJsonBody' ) )
{
$body = $e->getJsonBody();
$err = $body['error'];
array_push( $order_errors, $err['message'] );
}
array_push( $order_errors, 'There was an error during the transaction. Payment not performed.' );
sendErrorMail( "Exception", var_export( $e, true ) );
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Payment processing error',
'errors' => $order_errors
]);
exit;
}
}
} // /Process payment
} // /Form submitted
?>
<style>
<?php echo file_get_contents( __DIR__ . '/css/style.css'); ?>
/*.div-wrapper{
padding: 0em 1em !important;
}*/
.payment-fields {
display: block !important;
padding: 1em !important;
border-radius: 5px;
width: 100%;
height: 3em;
color: rgb(66, 109, 143);
background-color: rgb(255,255,255);
font-weight: bold;
}
</style>
<script>
<?php echo file_get_contents( __DIR__ . '/js/hashes.js'); ?>
</script>
<script>
</script>
<script>
var event_seat_location_options;
var eventData = <?php echo json_encode($event_data); ?>;
jQuery(document).ready(function() {
if (!eventData || !eventData.event_dates) {
console.error('Event data not available');
return;
}
var latest_available_seat = jQuery('#event-order [name="event_seat_location"] option:selected').val();
event_seat_location_options = jQuery('#event-order [name="event_seat_location"]').html();
jQuery('#event-order [name="event_seat_location"]').html('<option value="">Pick a seat location</option>');
updateAvailableLocations();
jQuery('#event-order [name="event_seat_location"]').val(latest_available_seat);
});
function updateFormSent()
{
var selectedDatetime = jQuery('#event-order [name="event_datetime"]').val();
jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').removeAttr('disabled');
jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').show();
updateTotal();
}
function updateAvailableLocations() {
if (!eventData || !eventData.event_dates) {
console.error('Event data not available');
return;
}
// reset options
jQuery('#event-order [name="event_seat_location"]').html(event_seat_location_options);
var selectedDatetime = jQuery('#event-order [name="event_datetime"]').val();
jQuery('#event-order [name="event_seat_location"] option').prop('disabled', true).hide();
jQuery('#event-order [name="event_seat_location"] option:first').prop('disabled', false).show();
jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').prop('disabled', false).show();
// remove unwanted options
jQuery('#event-order [name="event_seat_location"]').find('option:disabled').remove();
jQuery('#event-order [name="event_seat_location"]').val('');
}
function updateAvailableSeats()
{
var seatsLeft = parseInt( jQuery('#event-order [name="event_seat_location"] option:selected').attr('data-left') );
var seatsSelected = parseInt( jQuery('#event-order [name="event_seat_full"]').val() ) + parseInt( jQuery("#event-order [name='event_seat_child']").val() ) + parseInt( jQuery("#event-order [name='event_seat_senior']").val() );
if ( seatsSelected > seatsLeft )
{
jQuery('#event-order .warning-seats').html('You selected more tickets than are available right now in ' + jQuery("#event-order [name='event_seat_location'] option:selected").text() + ', please pick a different location or reduce the number of tickets by ' + ( seatsSelected - seatsLeft ) + '.' );
jQuery('#event-order .warning-seats').show();
return false;
}
else
{
jQuery('#event-order .warning-seats').hide();
jQuery('#event-order .warning-seats').html('');
return true;
}
}
var V = JSON.parse( '<?php echo $event_data->event_vouchers; ?>' );
var D = 0;
var T = '';
function voucherValid()
{
var SHA1 = new Hashes.SHA1;
var customer_voucher = SHA1.hex( jQuery("#event-order [name='voucher']").val().toLowerCase() );
for (var i = 0; i < V.length; i++)
{
if ( customer_voucher == V[i].voucher_code )
{
D = V[i].voucher_discount;
T = V[i].voucher_discount_type;
return true;
}
}
return false;
}
function updateTotal()
{
var seatsFull = parseInt( jQuery('#event-order [name="event_seat_full"]').val() );
var seatsChild = parseInt( jQuery('#event-order [name="event_seat_child"]').val() );
var seatsSenior = parseInt( jQuery('#event-order [name="event_seat_senior"]').val() );
var priceFull = parseFloat( jQuery('#event-order [name="event_seat_location"]').find('option:selected').attr( 'data-price-full' ) );
var priceReduced = parseFloat( jQuery('#event-order [name="event_seat_location"]').find('option:selected').attr( 'data-price-reduced' ) );
var total = ( seatsFull * priceFull ) + ( ( seatsChild + seatsSenior ) * priceReduced );
if ( voucherValid() )
{
if ( T == 'percentage' )
{
total = total * ( 1 - ( D / 100 ) );
}
else if ( T == 'amount' )
{
total = total - D;
}
}
var currency = '<?php echo $currency_symbol; ?>';
if ( isNaN( total ) )
{
jQuery('#event-order .total').html('');
}
else
{
jQuery('#event-order .total').html('Total: '+ currency + total );
}
}
jQuery(document).ready(function()
{
/*var stripe = Stripe('pk_test_VBObL72a6bVf4DAEpzOKvlh1', { // thenet
stripeAccount: 'acct_1C7mK9IQtQgABICR' // fossetts
});*/
var stripe = Stripe('pk_live_KSh8MRi76Kliuk4fUpBZ23GS', { // thenet live mode
stripeAccount: 'acct_1C7mK9IQtQgABICR' // fossetts
});
<?php if ( isset( $_POST['payment_intent_client_secret'] ) ): ?>
stripe.handleCardAction(
'<?php echo $_POST['payment_intent_client_secret']; ?>'
).then(function(result)
{
if (result.error)
{
jQuery('#event-order .warning-payment').html( result.error.message );
jQuery('#event-order .warning-payment').show();
}
else
{
jQuery('[name="payment_intent_id"]').val( result.paymentIntent.id );
jQuery('#event-order .warning-payment').hide();
jQuery('#event-order .warning-payment').html( '' );
jQuery("#event-order #event-order-form").trigger('submit');
}
});
<?php else: ?>
var elements = stripe.elements();
var paymentStyle = {
base: {
color: 'rgb(66, 109, 143)',
'::placeholder': {
color: 'rgba(66, 109, 143, 0.7)',
},
}
};
// var card = elements.create('card', { hidePostalCode: true, style: paymentStyle });
var card = elements.create('cardNumber', { showIcon: true, style: paymentStyle });
var cardExpiry = elements.create('cardExpiry', { style: paymentStyle });
var cardCvc = elements.create('cardCvc', { style: paymentStyle });
card.addEventListener('change', function( event )
{
if ( event.error )
{
jQuery('#event-order .warning-payment').html( event.error.message );
jQuery('#event-order .warning-payment').show();
}
else
{
jQuery('#event-order .warning-payment').hide();
jQuery('#event-order .warning-payment').html( '' );
}
});
// card.mount('#event-order #payment-fields');
card.mount('#event-order #card-number');
cardExpiry.mount('#event-order #card-expiry');
cardCvc.mount('#event-order #card-cvc');
<?php endif; ?>
jQuery("#event-order [name='event_datetime']").on("change", function()
{
updateAvailableLocations();
updateTotal();
});
jQuery("#event-order [name='event_seat_location'], #event-order [name='event_seat_full'], #event-order [name='event_seat_child'], #event-order [name='event_seat_senior']").on("change", function()
{
updateAvailableSeats();
updateTotal();
});
jQuery("#event-order [name='voucher']").on('input',function()
{
if ( voucherValid() )
{
jQuery('.warning-voucher').hide();
jQuery('.success-voucher').show();
}
else
{
if ( jQuery(this).val() == '' )
{
jQuery('.success-voucher').hide();
jQuery('.warning-voucher').hide();
}
else
{
jQuery('.success-voucher').hide();
jQuery('.warning-voucher').show();
}
}
updateTotal();
});
jQuery("#event-order .order-submit").on('click', function()
{
jQuery("#event-order .order-submit").prop('disabled', true);
if ( updateAvailableSeats() )
{
// Stripe SCA
stripe.createPaymentMethod('card', card, {
billing_details: { name: jQuery('[name="customer_name"]').val() }
})
//stripe.createToken( card )
.then( function(result)
{
if ( result.error )
{
jQuery('#event-order .warning-payment').html( result.error.message );
jQuery('#event-order .warning-payment').show();
jQuery("#event-order .order-submit").prop('disabled', false);
}
else
{
jQuery('#event-order [name="payment_token"]').val( result.paymentMethod.id );
//jQuery('#event-order [name="payment_token"]').val( result.token.id );
jQuery('#event-order .warning-payment').hide();
jQuery('#event-order .warning-payment').html( '' );
jQuery("#event-order #event-order-form").trigger('submit');
}
});
}
else
{
jQuery("#event-order .order-submit").prop('disabled', false);
}
});
/*jQuery("#event-order #event-order-form").on("submit", function(e)
{
jQuery("#event-order .order-submit").trigger('click');
e.preventDefault();
});*/
updateFormSent();
});
</script>
<script>
jQuery(document).ready(function()
{
jQuery('html, body').animate(
{
scrollTop: ( jQuery('.form-errors').first().offset().top - 10 )
}, 500 );
});
</script>
<script>
jQuery(document).ready(function()
{
function checkEmailValue(value, arr) {
var status = 'not_exist';
for (var i = 0; i < arr.length; i++) {
var name = arr[i];
if (name == value) {
status = 'exist';
break;
}
}
return status;
}
//code to check if a value exists in an array
var email_pool = ['gmail.com','yahoo.com','hotmail.com','aol.com','hotmail.co.uk','hotmail.fr','msn.com','yahoo.fr','wanadoo.fr','orange.fr','comcast.net','yahoo.co.uk','yahoo.com.br','yahoo.co.in','live.com','rediffmail.com','free.fr','gmx.de','web.de','yandex.ru','ymail.com','libero.it','outlook.com','uol.com.br','bol.com.br','mail.ru','cox.net','hotmail.it','sbcglobal.net','sfr.fr','live.fr','verizon.net','live.co.uk','googlemail.com','yahoo.es','ig.com.br','live.nl','bigpond.com','terra.com.br','yahoo.it','neuf.fr','yahoo.de','alice.it','rocketmail.com','att.net','laposte.net','facebook.com','bellsouth.net','yahoo.in','hotmail.es','charter.net','yahoo.ca','yahoo.com.au','rambler.ru','hotmail.de','tiscali.it','shaw.ca','yahoo.co.jp','sky.com','earthlink.net','optonline.net','freenet.de','t-online.de','aliceadsl.fr','virgilio.it','home.nl','qq.com','telenet.be','me.com','yahoo.com.ar','tiscali.co.uk','yahoo.com.mx','voila.fr','gmx.net','mail.com','planet.nl','tin.it','live.it','ntlworld.com','arcor.de','yahoo.co.id','frontiernet.net','hetnet.nl','live.com.au','yahoo.com.sg','zonnet.nl','club-internet.fr','juno.com','optusnet.com.au','blueyonder.co.uk','bluewin.ch','skynet.be','sympatico.ca','windstream.net','mac.com','centurytel.net','chello.nl','live.ca','aim.com','bigpond.net.au'];
jQuery( "#confirm_email_address" ).focusout(function() {
jQuery("#confirm_email_error").css("display", (jQuery("#email_address").val() != jQuery("#confirm_email_address").val()) ? "inline-block" : "none");
var user_email = jQuery("#email_address").val();
var domain_name = user_email.indexOf("@");
var slice_email = user_email.slice((domain_name + 1), user_email.length);
if(jQuery("#email_address").val() == jQuery("#confirm_email_address").val()){
// if(!jQuery.inArray(slice_email, email_pool)){
if(checkEmailValue(slice_email, email_pool) == 'not_exist'){
jQuery("#confirm_email_error2").css("display", "inline-block");
jQuery("#confirm_email_error2").html("Hmmm, are you sure... <strong>"+user_email+"</strong> is the correct email address?");
}else{
jQuery("#confirm_email_error2").css("display", "none");
}
}
});
});
</script>
<style>
.email_address_error{
margin-top: 5px !important;
color: rgb(255, 255, 255);
background-color: rgb(237, 30, 38);
font-weight: normal;
padding: 0.3em 15px;
}.email_address_warning{
margin-top: 5px !important;
color: rgb(255, 255, 255);
background-color: #25a828;
font-weight: normal;
padding: 0.3em 15px;
}
</style>
Contact details
Below are surgical “replace-here” markers for **both** places you currently create/confirm PaymentIntents (AJAX path and legacy/non‑AJAX path), plus a couple of small correctness fixes you’ll want.
—
# 1) Replace in the **AJAX (XHR) path**
**Find this block** (inside the first big `if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) … )` section):
if (isset($_POST['payment_token'])) {
try {
$customer = StripeCustomer::create([...]);
StripePaymentMethod::retrieve($_POST['payment_token'], ['stripe_account' => "acct_1C7mK9IQtQgABICR"])
->attach(['customer' => $customer->id], ['stripe_account' => "acct_1C7mK9IQtQgABICR"]);
$intent = StripePaymentIntent::create([... 'confirm' => true ...], ['stripe_account' => "acct_1C7mK9IQtQgABICR"]);
...
**Replace that whole “create customer + attach + create PI” block with this:**
// ---------- New safe, idempotent flow (AJAX path) ----------
StripeStripe::setApiKey(getenv('STRIPE_SECRET') ?: "sk_live_AhoYJCZ6tXIi9HRzh6XY7nqO");
$connect = "acct_1C7mK9IQtQgABICR";
// Build a deterministic idempotency key for this cart
$cart_fingerprint = json_encode([
'event_id' => $event_id,
'event_date_id' => $event_date_id,
'email' => $_POST['customer_email'] ?? '',
'amount_cents' => $amount_in_cents,
'tickets' => $ticket_quantities,
'addons' => processAddons($_POST),
]);
$idempotencyKey = 'fos-' . hash('sha256', $cart_fingerprint);
// Continuation after 3DS?
if (!empty($_POST['payment_intent_id']) && strpos($_POST['payment_intent_id'], 'pi_') === 0) {
$intent = StripePaymentIntent::retrieve($_POST['payment_intent_id'], ['stripe_account' => $connect]);
$intent->confirm([], ['stripe_account' => $connect]);
} else {
// Fresh attempt with a PaymentMethod
if (empty($_POST['payment_token']) || strpos($_POST['payment_token'], 'pm_') !== 0) {
throw new Exception('Invalid payment token.');
}
// Retrieve PM on the connected account
$pm = StripePaymentMethod::retrieve($_POST['payment_token'], ['stripe_account' => $connect]);
// Decide customer: reuse if already attached; otherwise create (or search) and attach
$customerId = null;
if (!empty($pm->customer)) {
$customerId = $pm->customer; // already attached somewhere; reuse that customer
} else {
$email = trim($_POST['customer_email'] ?? '');
$name = trim($_POST['customer_name'] ?? '');
// Try search (ok if not enabled — fallback to create)
try {
$found = StripeCustomer::search(
['query' => "email:'{$email}'"],
['stripe_account' => $connect]
);
if (!empty($found->data) && isset($found->data[0])) {
$customerId = $found->data[0]->id;
}
} catch (Exception $e) { /* ignore */ }
if (!$customerId) {
$customer = StripeCustomer::create(
['email' => $email, 'name' => $name],
['stripe_account' => $connect]
);
$customerId = $customer->id;
}
// Attach only if PM has no customer yet
$pm->attach(['customer' => $customerId], ['stripe_account' => $connect]);
}
// Create + confirm PI with idempotency to defeat double‑POSTs
$intent = StripePaymentIntent::create([
'amount' => $amount_in_cents,
'currency' => $event_data->event['currency'],
'payment_method' => $pm->id,
'customer' => $customerId, // omit if you don't want to save cards
'application_fee_amount' => (int)round($amount_in_cents * 0.05), // 5% in CENTS (see note below)
'description' => "Fossets charge for " . ($_POST['customer_email'] ?? ''),
'metadata' => ['Location' => $event_data->event['area']],
'confirm' => true,
'return_url' => 'https://fossettscircus.com/success/',
'automatic_payment_methods' => ['enabled' => true, 'allow_redirects' => 'never'],
], ['stripe_account' => $connect, 'idempotency_key' => $idempotencyKey]);
}
// The same status handling you already have (requires_action/succeeded/others) continues below…
> **Important:** you currently compute:
>
>
> $fee_in_cents = (int)($amount_in_eur * 5);
>
>
> That’s **wrong units** (it’s 5 *euros*, not 5%). Either keep using your `$fee_in_cents` variable **but set it correctly** *before* the PI call:
>
>
> $fee_in_cents = (int)round($amount_in_cents * 0.05); // 5% of total, in cents
>
>
> …and then pass `$fee_in_cents` into `application_fee_amount`.
You can delete your old `Customer::create()` + `PaymentMethod::attach()` block entirely; the new logic handles “already attached” gracefully and is idempotent.
—
# 2) Replace in the **legacy/non‑AJAX** path
You have the same “create customer + attach + create PI” pattern later (in the big “Non‑AJAX request handling” section). **Do the same replacement** there:
– Replace the block under `if ( isset( $_POST['payment_token'] ) ) { … }` with **the very same new flow** above (you can reuse it as a small helper function to avoid duplication).
– Keep the status handling exactly as you already do.
—
# 3) Quick correctness fixes you should do now
– ✅ **Fix fee math** (explained above).
– ✅ **Guard ID types in logs**: anywhere you log a “PaymentIntent ID”, ensure it starts with `pi_`. If it’s `pm_…`, log it as **PaymentMethod ID** instead.
– ✅ **Undefined `$response`** in your dates “ loop:
You reference `$response['ticket_seats_left']` without defining `$response`. Either remove that conditional or actually load it:
// $response = $fossets_database->getNumberOfAvailableTicketsByDate($date['event_date_id']);
if (!empty($response) && isset($response['ticket_seats_left']) && $response['ticket_seats_left'] === '0') {
echo " - SOLD OUT";
}
(Don’t echo HTML “ inside an “.)
– ✅ **Duplicate `ob_start()`**: you call it twice near the top. One is enough.
– ✅ **`ticket_num_price_*` fields in `createOrderInfo()`**:
Your new `$_POST` uses keys like `event_seat_ringside_adult`, so these summaries should aggregate by suffix rather than look for plain `adult/child/senior` keys. Replace:
$order_info->ticket_num_price_full = isset($ticket_quantities['adult']) ? $ticket_quantities['adult'] : 0;
with:
$sumBySuffix = function(array $q, string $suffix): int {
$n = 0;
foreach ($q as $k => $v) {
if ($v > 0 && substr($k, -strlen($suffix)) === $suffix) { $n += (int)$v; }
}
return $n;
};
$order_info->ticket_num_price_full = $sumBySuffix($ticket_quantities, 'adult');
$order_info->ticket_num_price_child = $sumBySuffix($ticket_quantities, 'child');
$order_info->ticket_num_price_senior= $sumBySuffix($ticket_quantities, 'senior');
– ✅ **PHP version guard**: if you’re not on PHP 8, avoid `str_starts_with`. The snippets above use `strpos(…) === 0`, which is PHP‑7 safe.
– ✅ **Keys in code**: your live keys are hard‑coded. Move them to env (`getenv('STRIPE_SECRET')`, `getenv('STRIPE_PUBLISHABLE')`) and your Connect account id likewise.
– ✅ **One‑off payments?** If you don’t need to save cards, you can simplify: skip creating/attaching customers entirely. Just create the PI with the `payment_method` and `confirm: true` (still keep the **idempotency key**).
—
# 4) (Optional) Extract to helpers to DRY both paths
To avoid duplicate logic in AJAX + legacy sections:
function create_or_confirm_intent_idempotent(array $ctx) {
// ctx: amount_in_cents, currency, payment_token (pm_), email, name, metadata, stripe_account, idempotency_key
// return: StripePaymentIntent
...
}
Call that in both places; keep your existing status branching (`requires_action` → send client_secret; `succeeded` → build order, etc.).
—
If you paste me the next error line **after** this swap (if any), I’ll read it and point at the exact branch to tweak.
<script>
window.location='https://fossettscircus.com/success/?eid=" . $order_info->event_date_id . "&oc=" . $order_info->order_code . "';
</script>
' ";
goto continue_with_wordpress;
exit();
}
else
{
sendErrorMail( "Order not saved", "Order not saved but charge paid" . var_export( $order_info, true ) );
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Order could not be saved'
]);
exit;
}
}
}
elseif ($intent->status == 'processing') {
echo json_encode([
'success' => false,
'message' => 'Payment is processing. Please wait a moment and try again.',
'status' => 'processing'
]);
exit;
} elseif ($intent->status == 'requires_payment_method') {
echo json_encode([
'success' => false,
'message' => 'Payment method failed. Please try again with a different card.',
'status' => 'requires_payment_method'
]);
exit;
} elseif ($intent->status == 'requires_confirmation') {
echo json_encode([
'success' => false,
'message' => 'Payment requires confirmation. Please try again.',
'status' => 'requires_confirmation'
]);
exit;
} else {
echo json_encode([
'success' => false,
'message' => 'Payment failed: ' . $intent->status,
'status' => $intent->status,
'errors' => ['There was an error during the transaction, charge was not paid.']
]);
exit;
}
}
catch(StripeErrorCard $e)
{
$body = $e->getJsonBody();
$err = $body['error'];
array_push( $order_errors, $err['message'] );
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => $err['message'],
'errors' => $order_errors
]);
exit;
}
}
catch (Exception $e)
{
if ( is_object( $e ) && method_exists( $e, 'getJsonBody' ) )
{
$body = $e->getJsonBody();
$err = $body['error'];
array_push( $order_errors, $err['message'] );
}
array_push( $order_errors, 'There was an error during the transaction. Payment not performed.' );
sendErrorMail( "Exception", var_export( $e, true ) );
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Payment processing error',
'errors' => $order_errors
]);
exit;
}
}
} // /Process payment
} // /Form submitted
?>
<style>
<?php echo file_get_contents( __DIR__ . '/css/style.css'); ?>
/*.div-wrapper{
padding: 0em 1em !important;
}*/
.payment-fields {
display: block !important;
padding: 1em !important;
border-radius: 5px;
width: 100%;
height: 3em;
color: rgb(66, 109, 143);
background-color: rgb(255,255,255);
font-weight: bold;
}
</style>
<script>
<?php echo file_get_contents( __DIR__ . '/js/hashes.js'); ?>
</script>
<script>
</script>
<script>
var event_seat_location_options;
var eventData = <?php echo json_encode($event_data); ?>;
jQuery(document).ready(function() {
if (!eventData || !eventData.event_dates) {
console.error('Event data not available');
return;
}
var latest_available_seat = jQuery('#event-order [name="event_seat_location"] option:selected').val();
event_seat_location_options = jQuery('#event-order [name="event_seat_location"]').html();
jQuery('#event-order [name="event_seat_location"]').html('<option value="">Pick a seat location</option>');
updateAvailableLocations();
jQuery('#event-order [name="event_seat_location"]').val(latest_available_seat);
});
function updateFormSent()
{
var selectedDatetime = jQuery('#event-order [name="event_datetime"]').val();
jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').removeAttr('disabled');
jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').show();
updateTotal();
}
function updateAvailableLocations() {
if (!eventData || !eventData.event_dates) {
console.error('Event data not available');
return;
}
// reset options
jQuery('#event-order [name="event_seat_location"]').html(event_seat_location_options);
var selectedDatetime = jQuery('#event-order [name="event_datetime"]').val();
jQuery('#event-order [name="event_seat_location"] option').prop('disabled', true).hide();
jQuery('#event-order [name="event_seat_location"] option:first').prop('disabled', false).show();
jQuery('#event-order [name="event_seat_location"] option[data-date-id="' + selectedDatetime + '"]').prop('disabled', false).show();
// remove unwanted options
jQuery('#event-order [name="event_seat_location"]').find('option:disabled').remove();
jQuery('#event-order [name="event_seat_location"]').val('');
}
function updateAvailableSeats()
{
var seatsLeft = parseInt( jQuery('#event-order [name="event_seat_location"] option:selected').attr('data-left') );
var seatsSelected = parseInt( jQuery('#event-order [name="event_seat_full"]').val() ) + parseInt( jQuery("#event-order [name='event_seat_child']").val() ) + parseInt( jQuery("#event-order [name='event_seat_senior']").val() );
if ( seatsSelected > seatsLeft )
{
jQuery('#event-order .warning-seats').html('You selected more tickets than are available right now in ' + jQuery("#event-order [name='event_seat_location'] option:selected").text() + ', please pick a different location or reduce the number of tickets by ' + ( seatsSelected - seatsLeft ) + '.' );
jQuery('#event-order .warning-seats').show();
return false;
}
else
{
jQuery('#event-order .warning-seats').hide();
jQuery('#event-order .warning-seats').html('');
return true;
}
}
var V = JSON.parse( '<?php echo $event_data->event_vouchers; ?>' );
var D = 0;
var T = '';
function voucherValid()
{
var SHA1 = new Hashes.SHA1;
var customer_voucher = SHA1.hex( jQuery("#event-order [name='voucher']").val().toLowerCase() );
for (var i = 0; i < V.length; i++)
{
if ( customer_voucher == V[i].voucher_code )
{
D = V[i].voucher_discount;
T = V[i].voucher_discount_type;
return true;
}
}
return false;
}
function updateTotal()
{
var seatsFull = parseInt( jQuery('#event-order [name="event_seat_full"]').val() );
var seatsChild = parseInt( jQuery('#event-order [name="event_seat_child"]').val() );
var seatsSenior = parseInt( jQuery('#event-order [name="event_seat_senior"]').val() );
var priceFull = parseFloat( jQuery('#event-order [name="event_seat_location"]').find('option:selected').attr( 'data-price-full' ) );
var priceReduced = parseFloat( jQuery('#event-order [name="event_seat_location"]').find('option:selected').attr( 'data-price-reduced' ) );
var total = ( seatsFull * priceFull ) + ( ( seatsChild + seatsSenior ) * priceReduced );
if ( voucherValid() )
{
if ( T == 'percentage' )
{
total = total * ( 1 - ( D / 100 ) );
}
else if ( T == 'amount' )
{
total = total - D;
}
}
var currency = '<?php echo $currency_symbol; ?>';
if ( isNaN( total ) )
{
jQuery('#event-order .total').html('');
}
else
{
jQuery('#event-order .total').html('Total: '+ currency + total );
}
}
jQuery(document).ready(function()
{
/*var stripe = Stripe('pk_test_VBObL72a6bVf4DAEpzOKvlh1', { // thenet
stripeAccount: 'acct_1C7mK9IQtQgABICR' // fossetts
});*/
var stripe = Stripe('pk_live_KSh8MRi76Kliuk4fUpBZ23GS', { // thenet live mode
stripeAccount: 'acct_1C7mK9IQtQgABICR' // fossetts
});
<?php if ( isset( $_POST['payment_intent_client_secret'] ) ): ?>
stripe.handleCardAction(
'<?php echo $_POST['payment_intent_client_secret']; ?>'
).then(function(result)
{
if (result.error)
{
jQuery('#event-order .warning-payment').html( result.error.message );
jQuery('#event-order .warning-payment').show();
}
else
{
jQuery('[name="payment_intent_id"]').val( result.paymentIntent.id );
jQuery('#event-order .warning-payment').hide();
jQuery('#event-order .warning-payment').html( '' );
jQuery("#event-order #event-order-form").trigger('submit');
}
});
<?php else: ?>
var elements = stripe.elements();
var paymentStyle = {
base: {
color: 'rgb(66, 109, 143)',
'::placeholder': {
color: 'rgba(66, 109, 143, 0.7)',
},
}
};
// var card = elements.create('card', { hidePostalCode: true, style: paymentStyle });
var card = elements.create('cardNumber', { showIcon: true, style: paymentStyle });
var cardExpiry = elements.create('cardExpiry', { style: paymentStyle });
var cardCvc = elements.create('cardCvc', { style: paymentStyle });
card.addEventListener('change', function( event )
{
if ( event.error )
{
jQuery('#event-order .warning-payment').html( event.error.message );
jQuery('#event-order .warning-payment').show();
}
else
{
jQuery('#event-order .warning-payment').hide();
jQuery('#event-order .warning-payment').html( '' );
}
});
// card.mount('#event-order #payment-fields');
card.mount('#event-order #card-number');
cardExpiry.mount('#event-order #card-expiry');
cardCvc.mount('#event-order #card-cvc');
<?php endif; ?>
jQuery("#event-order [name='event_datetime']").on("change", function()
{
updateAvailableLocations();
updateTotal();
});
jQuery("#event-order [name='event_seat_location'], #event-order [name='event_seat_full'], #event-order [name='event_seat_child'], #event-order [name='event_seat_senior']").on("change", function()
{
updateAvailableSeats();
updateTotal();
});
jQuery("#event-order [name='voucher']").on('input',function()
{
if ( voucherValid() )
{
jQuery('.warning-voucher').hide();
jQuery('.success-voucher').show();
}
else
{
if ( jQuery(this).val() == '' )
{
jQuery('.success-voucher').hide();
jQuery('.warning-voucher').hide();
}
else
{
jQuery('.success-voucher').hide();
jQuery('.warning-voucher').show();
}
}
updateTotal();
});
jQuery("#event-order .order-submit").on('click', function()
{
jQuery("#event-order .order-submit").prop('disabled', true);
if ( updateAvailableSeats() )
{
// Stripe SCA
stripe.createPaymentMethod('card', card, {
billing_details: { name: jQuery('[name="customer_name"]').val() }
})
//stripe.createToken( card )
.then( function(result)
{
if ( result.error )
{
jQuery('#event-order .warning-payment').html( result.error.message );
jQuery('#event-order .warning-payment').show();
jQuery("#event-order .order-submit").prop('disabled', false);
}
else
{
jQuery('#event-order [name="payment_token"]').val( result.paymentMethod.id );
//jQuery('#event-order [name="payment_token"]').val( result.token.id );
jQuery('#event-order .warning-payment').hide();
jQuery('#event-order .warning-payment').html( '' );
jQuery("#event-order #event-order-form").trigger('submit');
}
});
}
else
{
jQuery("#event-order .order-submit").prop('disabled', false);
}
});
/*jQuery("#event-order #event-order-form").on("submit", function(e)
{
jQuery("#event-order .order-submit").trigger('click');
e.preventDefault();
});*/
updateFormSent();
});
</script>
<script>
jQuery(document).ready(function()
{
jQuery('html, body').animate(
{
scrollTop: ( jQuery('.form-errors').first().offset().top - 10 )
}, 500 );
});
</script>
<script>
jQuery(document).ready(function()
{
function checkEmailValue(value, arr) {
var status = 'not_exist';
for (var i = 0; i < arr.length; i++) {
var name = arr[i];
if (name == value) {
status = 'exist';
break;
}
}
return status;
}
//code to check if a value exists in an array
var email_pool = ['gmail.com','yahoo.com','hotmail.com','aol.com','hotmail.co.uk','hotmail.fr','msn.com','yahoo.fr','wanadoo.fr','orange.fr','comcast.net','yahoo.co.uk','yahoo.com.br','yahoo.co.in','live.com','rediffmail.com','free.fr','gmx.de','web.de','yandex.ru','ymail.com','libero.it','outlook.com','uol.com.br','bol.com.br','mail.ru','cox.net','hotmail.it','sbcglobal.net','sfr.fr','live.fr','verizon.net','live.co.uk','googlemail.com','yahoo.es','ig.com.br','live.nl','bigpond.com','terra.com.br','yahoo.it','neuf.fr','yahoo.de','alice.it','rocketmail.com','att.net','laposte.net','facebook.com','bellsouth.net','yahoo.in','hotmail.es','charter.net','yahoo.ca','yahoo.com.au','rambler.ru','hotmail.de','tiscali.it','shaw.ca','yahoo.co.jp','sky.com','earthlink.net','optonline.net','freenet.de','t-online.de','aliceadsl.fr','virgilio.it','home.nl','qq.com','telenet.be','me.com','yahoo.com.ar','tiscali.co.uk','yahoo.com.mx','voila.fr','gmx.net','mail.com','planet.nl','tin.it','live.it','ntlworld.com','arcor.de','yahoo.co.id','frontiernet.net','hetnet.nl','live.com.au','yahoo.com.sg','zonnet.nl','club-internet.fr','juno.com','optusnet.com.au','blueyonder.co.uk','bluewin.ch','skynet.be','sympatico.ca','windstream.net','mac.com','centurytel.net','chello.nl','live.ca','aim.com','bigpond.net.au'];
jQuery( "#confirm_email_address" ).focusout(function() {
jQuery("#confirm_email_error").css("display", (jQuery("#email_address").val() != jQuery("#confirm_email_address").val()) ? "inline-block" : "none");
var user_email = jQuery("#email_address").val();
var domain_name = user_email.indexOf("@");
var slice_email = user_email.slice((domain_name + 1), user_email.length);
if(jQuery("#email_address").val() == jQuery("#confirm_email_address").val()){
// if(!jQuery.inArray(slice_email, email_pool)){
if(checkEmailValue(slice_email, email_pool) == 'not_exist'){
jQuery("#confirm_email_error2").css("display", "inline-block");
jQuery("#confirm_email_error2").html("Hmmm, are you sure... <strong>"+user_email+"</strong> is the correct email address?");
}else{
jQuery("#confirm_email_error2").css("display", "none");
}
}
});
});
</script>
<style>
.email_address_error{
margin-top: 5px !important;
color: rgb(255, 255, 255);
background-color: rgb(237, 30, 38);
font-weight: normal;
padding: 0.3em 15px;
}.email_address_warning{
margin-top: 5px !important;
color: rgb(255, 255, 255);
background-color: #25a828;
font-weight: normal;
padding: 0.3em 15px;
}
</style>
Contact details
—
# A) Fix the fee math (do this in both AJAX + legacy paths)
**Find** (right after you compute `$amount_in_eur`):
// Get 5%
$fee_in_cents = (int)($amount_in_eur * 5);
// Convert to cents for stripe
$amount_in_cents = (int)($amount_in_eur * 100);
**Replace with:**
// Convert to cents first
$amount_in_cents = (int) round($amount_in_eur * 100);
// 5% platform fee, in cents
$fee_in_cents = (int) round($amount_in_cents * 0.05);
—
# B) Replace the Stripe create/confirm block in the **AJAX** section
**Find** this whole block inside the XHR handler (from `if (isset($_POST['payment_token'])) {` down to its `catch`es, **and** the small “retrieve + confirm” block for `payment_intent_id` just below it):
if (isset($_POST['payment_token'])) {
try {
$customer = StripeCustomer::create([...]);
StripePaymentMethod::retrieve(...)->attach([...], [...]);
$intent = StripePaymentIntent::create([...], [...]);
...
} catch (StripeExceptionCardException $e) { ... }
catch (Exception $e) { ... }
}
if (isset($_POST['payment_intent_id'])) {
$intent = StripePaymentIntent::retrieve(...);
$intent->confirm();
}
**Replace the entire thing with:**
$connect = "acct_1C7mK9IQtQgABICR"; // Fossetts Connect account
// Optional: use env secret in testing; fallback to your current key
StripeStripe::setApiKey(getenv('STRIPE_SECRET') ?: "sk_live_AhoYJCZ6tXIi9HRzh6XY7nqO");
// Build an idempotency key for this exact cart to avoid double-charges
$cart_fingerprint = json_encode([
'event_id' => $event_id,
'event_date_id' => $event_date_id,
'email' => $_POST['customer_email'] ?? '',
'amount_cents' => $amount_in_cents,
'tickets' => $ticket_quantities,
'addons' => processAddons($_POST),
]);
$idempotencyKey = 'fos-test-' . hash('sha256', $cart_fingerprint);
try {
if (!empty($_POST['payment_intent_id']) && strpos($_POST['payment_intent_id'], 'pi_') === 0) {
// 3DS continuation: just confirm again on the connected account
$intent = StripePaymentIntent::retrieve($_POST['payment_intent_id'], ['stripe_account' => $connect]);
$intent->confirm([], ['stripe_account' => $connect]);
} else {
// Fresh attempt using a PaymentMethod id (pm_...)
if (empty($_POST['payment_token']) || strpos($_POST['payment_token'], 'pm_') !== 0) {
throw new Exception('Invalid payment token.');
}
// Retrieve the PM on the connected account
$pm = StripePaymentMethod::retrieve($_POST['payment_token'], ['stripe_account' => $connect]);
// Pick/reuse a Customer (attach only if PM not attached yet)
$customerId = null;
if (!empty($pm->customer)) {
$customerId = $pm->customer;
} else {
$email = trim($_POST['customer_email'] ?? '');
$name = trim($_POST['customer_name'] ?? '');
// Try to find an existing customer by email (safe if search disabled: we fall back)
try {
$found = StripeCustomer::search(['query' => "email:'{$email}'"], ['stripe_account' => $connect]);
if (!empty($found->data) && isset($found->data[0])) {
$customerId = $found->data[0]->id;
}
} catch (Exception $e) { /* ignore if search not enabled */ }
if (!$customerId) {
$customer = StripeCustomer::create(
['email' => $email, 'name' => $name],
['stripe_account' => $connect]
);
$customerId = $customer->id;
}
// Attach the PM now that we have a customer
$pm->attach(['customer' => $customerId], ['stripe_account' => $connect]);
}
// Create & confirm the PaymentIntent with idempotency
$intent = StripePaymentIntent::create([
'amount' => $amount_in_cents,
'currency' => $event_data->event['currency'],
'payment_method' => $pm->id,
'customer' => $customerId, // keep if you want to reuse cards
'application_fee_amount' => $fee_in_cents,
'description' => "Fossets charge for " . ($_POST['customer_email'] ?? ''),
'metadata' => ['Location' => $event_data->event['area']],
'confirm' => true,
'return_url' => 'https://fossettscircus.com/success/',
'automatic_payment_methods' => ['enabled' => true, 'allow_redirects' => 'never'],
], ['stripe_account' => $connect, 'idempotency_key' => $idempotencyKey]);
}
} catch (StripeExceptionCardException $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage(),
'status' => 'card_error'
]);
exit;
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => $e->getMessage(),
'status' => 'error'
]);
exit;
}
✅ Your existing status handling right **after** this (the `if ($intent->status == 'requires_action') … elseif ('succeeded') …`) stays exactly the same and will work as‑is.
—
# C) Replace the Stripe create/confirm block in the **legacy / non‑AJAX** section
Do the same replacement there: in the large “Process payment if no errors” try/catch, **replace**:
if ( isset( $_POST['payment_token'] ) ) {
$customer = StripeCustomer::create([...]);
StripePaymentMethod::retrieve(...)->attach([...], [...]);
$intent = StripePaymentIntent::create([...], [...]);
}
if ( isset( $_POST['payment_intent_id'] ) ) {
$intent = StripePaymentIntent::retrieve(...);
$intent->confirm();
}
**with the same idempotent block** you used above (you can paste it verbatim; it only depends on `$amount_in_cents`, `$fee_in_cents`, `$event_id`, `$event_date_id`, `$ticket_quantities`, `$event_data`, etc., which you already have in scope).
Your downstream status branches (`requires_action` → send client_secret, `succeeded` → save order, etc.) remain unchanged.
—
# D) Make `createOrderInfo()` sum adult/child/senior across seating
Right now it looks for plain `adult/child/senior` keys, but your POST keys are `seating_ticketType`. **Find** in `createOrderInfo()`:
$order_info->ticket_num_price_full = isset($ticket_quantities['adult']) ? $ticket_quantities['adult'] : 0;
$order_info->ticket_num_price_child = isset($ticket_quantities['child']) ? $ticket_quantities['child'] : 0;
$order_info->ticket_num_price_senior= isset($ticket_quantities['senior']) ? $ticket_quantities['senior'] : 0;
**Replace with:**
$sumBySuffix = function(array $q, string $suffix): int {
$n = 0;
foreach ($q as $k => $v) {
if ($v > 0 && substr($k, -strlen($suffix)) === $suffix) { $n += (int)$v; }
}
return $n;
};
$order_info->ticket_num_price_full = $sumBySuffix($ticket_quantities, 'adult');
$order_info->ticket_num_price_child = $sumBySuffix($ticket_quantities, 'child');
$order_info->ticket_num_price_senior = $sumBySuffix($ticket_quantities, 'senior');
—
# E) Two tiny safety fixes
1) **Duplicate `ob_start()`**
You call `ob_start();` at the very top **and** again before `date_default_timezone_set(…)`. Keep the first one; remove the second.
2) **SOLD OUT in “**
You can’t render a “ inside an “. Change:
if ($response['ticket_seats_left'] === '0') {
echo "- <p style='color:red;'>SOLD OUT</p>";
}
to:
if (!empty($response) && isset($response['ticket_seats_left']) && $response['ticket_seats_left'] === '0') {
echo " - SOLD OUT";
}
(or just drop it during testing).
—
That’s it. Paste those replaces and you’ll have the safer, idempotent, test‑ready flow without changing your UI or downstream logic. If you hit a specific error after swapping, send me the exact line/Stripe error JSON and I’ll point to the precise fix.