Repository: Attendize/Attendize (language: php)

Issue:
Customers charged multiple times
**Describe the bug**
We have now had chance to test this system properly and it is now being used in a live environment where upwards of 10,000 tickets will be sold over the next 6 weeks. We have sold over 800 in last week and have noticed a recurring problem.

We had an order come through and it had repeated itself and charged the customer 4 times. The payment had gone through stripe, new tickets and new orders were created. We thought it was a one off but it has happened at least 6 times now, each time the customers order getting repeated 4 times and being charged 4 times. With the refund / cancel tickets also experiencing a bug where we can not cancel an order all refunds are having to be processed manually and the orders deleted directly from within phpmyadmin.





Discussion / hints:
Have the issue too on my side. Some customers are billed 2, 3, or 4 times.
I think it's something to do with high traffic, I see that only when there's a spike in the traffic on my instance of Attendize.

The refund function is broken so far with stripe integration. That's a known issue. You have to manually refund it in Stripe (enjoy keeping the fees to pay) and then mark them as refunds in the order table AND the attendee table. Beware. Otherwise, people will still be able to use the tickets.

Fix patch:
diff --git a/app/Services/PaymentGateway/StripeSCA.php b/app/Services/PaymentGateway/StripeSCA.php
index 354934395..38bd704b3 100644
--- a/app/Services/PaymentGateway/StripeSCA.php
+++ b/app/Services/PaymentGateway/StripeSCA.php
@@ -104,16 +104,15 @@ public function storeAdditionalData()
     public function refundTransaction($order, $refund_amount, $refund_application_fee)
     {
 
-        $request = $this->gateway->cancel([
+        $request = $this->gateway->refund([
             'transactionReference' => $order->transaction_id,
             'amount' => $refund_amount,
-            'refundApplicationFee' => $refund_application_fee,
-            'paymentIntentReference' => $order->payment_intent
+            'refundApplicationFee' => $refund_application_fee
         ]);
 
         $response = $request->send();
 
-        if ($response->isCancelled()) {
+        if ($response->isSuccessful()) {
             $refundResponse['successful'] = true;
         } else {
             $refundResponse['successful'] = false;
diff --git a/resources/views/Public/ViewEvent/Partials/PaymentStripeSCA.blade.php b/resources/views/Public/ViewEvent/Partials/PaymentStripeSCA.blade.php
index 3cf0e2734..1f96d0f5a 100644
--- a/resources/views/Public/ViewEvent/Partials/PaymentStripeSCA.blade.php
+++ b/resources/views/Public/ViewEvent/Partials/PaymentStripeSCA.blade.php
@@ -1,83 +1,148 @@
-<form class="online_payment" action="<?php echo route('postCreateOrder', ['event_id' => $event->id]); ?>" method="post" id="stripe-sca-payment-form">
-    <div class="form-row">
-        <label for="card-element">
-            @lang("Public_ViewEvent.stripe_credit_or_debit_card")
-        </label>
-        <div id="card-element">
-
-        </div>
-
-        <div id="card-errors" role="alert"></div>
-    </div>
-    {!! Form::token() !!}
-
-    <input class="btn btn-lg btn-success card-submit" style="width:100%;" type="submit" value="@lang("Public_ViewEvent.complete_payment")">
-
+<form class="online_payment" id='payment-form'>
+    <label>
+      Card details
+    </label>
+    <!-- placeholder for Elements -->
+    <div id="card-element"></div>
+    <br>
+    <button id="submit-button" type="submit" class="btn bt-nlg btn-success card-submit">Submit Payment</button>
+    <br><br>
+    <div id="card-errors" role="alert"></div>
 </form>
-<script type="text/javascript" src="https://js.stripe.com/v3/"></script>
-<script type="text/javascript">
 
+<script src="https://js.stripe.com/v3/"></script>
+<script type="text/javascript">
     var stripe = Stripe('<?php echo $account_payment_gateway->config['publishableKey']; ?>');
+
     var elements = stripe.elements();
 
+    // Set up Stripe.js and Elements to use in checkout form
     var style = {
-        base: {
-            color: '#32325d',
-            fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
-            fontSmoothing: 'antialiased',
-            fontSize: '16px',
-            '::placeholder': {
-                color: '#aab7c4'
-            }
-        },
-        invalid: {
-            color: '#fa755a',
-            iconColor: '#fa755a'
+    base: {
+        color: "#32325d",
+        fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
+        fontSmoothing: "antialiased",
+        fontSize: "16px",
+        "::placeholder": {
+        color: "#aab7c4"
         }
+    },
+    invalid: {
+        color: "#fa755a",
+        iconColor: "#fa755a"
+    },
     };
 
-    var cardElement = elements.create('card', {hidePostalCode: true, style: style});
+    var cardElement = elements.create('card', {hidePostalCode: true, style:style});
     cardElement.mount('#card-element');
 
-    cardElement.addEventListener('change', function(event) {
-        var displayError = document.getElementById('card-errors');
-        if (event.error) {
-            displayError.textContent = event.error.message;
-        } else {
-            displayError.textContent = '';
-        }
+    var form = document.getElementById('payment-form');
+
+    form.addEventListener('submit', function(event) {
+    document.getElementById("submit-button").disabled = true;
+    // We don't want to let default form submission happen here,
+    // which would refresh the page.
+    event.preventDefault();
+
+    stripe.createPaymentMethod({
+        type: 'card',
+        card: cardElement
+        /*billing_details: {
+        // Include any additional collected billing details.
+        name: 'Jenny Rosen',
+        },*/
+    }).then(stripePaymentMethodHandler);
     });
 
+    function stripePaymentMethodHandler(result) {
+    if (result.error) {
+        // Show error in payment form
+        //Wait a bit, then reactivate the button
+        setTimeout(function (){
+            document.getElementById("submit-button").disabled = false;
+        }, 3000);
 
-</script>
-<style type="text/css">
-
-    .StripeElement {
-        box-sizing: border-box;
-
-        height: 40px;
-
-        padding: 10px 12px;
-
-        border: 1px solid #e0e0e0 !important;
-        border-radius: 4px;
-        background-color: white;
+        var displayError = document.getElementById('card-errors');
+        if(result.message)
+        {
+        displayError.textContent = result.message;
+        }
+        else if(result.error.message)
+        {
+        displayError.textContent = result.error.message;
+        }
 
-        box-shadow: 0 1px 3px 0 #e6ebf1;
-        -webkit-transition: box-shadow 150ms ease;
-        transition: box-shadow 150ms ease;
-        margin-bottom: 20px;
+    } else {
+        // Otherwise send paymentMethod.id to your server (see Step 4)
+        fetch("<?php echo route('postCreateOrder', ['event_id' => $event->id]); ?>", {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN':'{{ csrf_token() }}' },
+        body: JSON.stringify({
+            paymentMethod: result.paymentMethod.id,
+        })
+        }).then(function(result) {
+        // Handle server response (see Step 4)
+        result.json().then(function(json) {
+            handleServerResponse(json);
+        })
+        });
     }
-
-    .StripeElement--focus {
-        box-shadow: 0 1px 3px 0 #cfd7df;
     }
 
-    .StripeElement--invalid {
-        border-color: #fa755a;
-    }
+    var retries = 0;
 
-    .StripeElement--webkit-autofill {
-        background-color: #fefde5 !important;
+    function handleServerResponse(json)
+    {
+        if(json.status == "success")
+        {
+            window.location.href = json.redirectUrl;
+        }
+        else if(json.status == "error")
+        {
+            retries = retries + 1;
+            var displayError = document.getElementById('card-errors');
+            displayError.textContent = json.message + " (" + String(5 - retries) + " retries left)";
+            if(retries < 5)
+            {
+                var btn = document.getElementById("submit-button");
+                btn.disabled = false;
+            }
+            else
+            {
+                displayError.textContent = "Too much retries, please refresh the page";
+            }
+        }
+        else if(json.error.code == "card_declined")
+        {
+        displayError.textContent = json.error.message;
+        }
     }
-</style>
+</script>
+
+<style type="text/css">
+    .StripeElement {
+  height: 40px;
+  padding: 10px 12px;
+  width: 100%;
+  color: #32325d;
+  background-color: white;
+  border: 1px solid transparent;
+  border-radius: 4px;
+
+  box-shadow: 0 1px 3px 0 #e6ebf1;
+  -webkit-transition: box-shadow 150ms ease;
+  transition: box-shadow 150ms ease;
+}
+
+.StripeElement--focus {
+  box-shadow: 0 1px 3px 0 #cfd7df;
+}
+
+.StripeElement--invalid {
+  border-color: #fa755a;
+}
+
+.StripeElement--webkit-autofill {
+  background-color: #fefde5 !important;
+}
+</style>
\ No newline at end of file


Test patch: