PaymentIntent fix guide

User
<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

Assistant
Got it, Neeraj — this is a classic **“double‑POST + PaymentMethod already attached”** tangle. Here’s a tight fix that (1) makes duplicate submits safe, (2) stops the illegal re‑attach, and (3) cleans up a couple of edge cases you’re hitting.

# 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.

User
<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

Assistant
Perfect — I see exactly where you’d drop the new, safe Stripe flow.

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.

User
first repalce in testing envrin ment

<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

Assistant
nice — here’s a clean, copy‑paste set of **surgical replaces** for your testing file. I’ve kept your structure, only touching what’s needed:

# 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.

Leave a Comment

Your email address will not be published. Required fields are marked *