How to integrate a Stripe Terminal Reader to POS application using Django?

I am developing a POS system using Django. I have a Stripe account, and through the system I am developing, I can process payments using credit or debit cards, with the money being deposited into my Stripe account. This is done by typing card information such as the card number, CVV, and expiration date.

Now, I have decided to use a Stripe Terminal Reader to simplify the process. Instead of manually entering card details, customers can swipe, insert, or tap their card on the Terminal Reader for payment. The model I have ordered is the BBPOS WisePOS E. I powered it on, and it generated a code that I entered into my Stripe account. The terminal's online or offline status is displayed in my Stripe account.

The idea is that when I select 'Debit or Credit Card' as the payment method, the amount to be paid should be sent to the terminal. However, this process is not working.

The terminal still shows the screen displayed in the attached image."

Let me know if you'd like further refinements! enter image description here

I don't know if I miss some steps that need to be done in order for this to work.

Bellow are my functions:

@method_decorator(login_required)
def post(self, request, order_id):
    """Handles POST requests to process the checkout."""
    order = get_object_or_404(Order, id=order_id)

    # Ensure the order has items
    if not order.items.exists():
        modal_message = "Cette commande ne contient aucun produit. Le paiement ne peut pas être traité."
        return render(request, 'pos/orders/checkout.html', {
            'order': order,
            'modal_message': modal_message,
            'currency': None,
            'stripe_publishable_key': settings.STRIPE_PUBLISHABLE_KEY
        })

    # Fetch the active currency
    active_currency = Currency.objects.filter(is_active=True).first()
    if not active_currency:
        return render(request, 'pos/orders/checkout.html', {
            'order': order,
            'modal_message': 'Aucune devise active trouvée pour le magasin.',
            'currency': None,
            'stripe_publishable_key': settings.STRIPE_PUBLISHABLE_KEY
        })

    # Retrieve payment data
    payment_method = request.POST.get('payment_method')
    received_amount = request.POST.get('received_amount')
    stripe_payment_method_id = request.POST.get('stripe_payment_method_id')
    reader_id = request.POST.get('reader_id')  # Added for terminal payments
    discount_type = request.POST.get('discount_type')
    discount_amount = request.POST.get('discount_amount')

    # Convert received amount to Decimal
    try:
        received_amount = Decimal(received_amount) if received_amount else None
    except (ValueError, InvalidOperation):
        return render(request, 'pos/orders/checkout.html', {
            'order': order,
            'modal_message': 'Montant reçu invalide.',
            'currency': active_currency,
            'stripe_publishable_key': settings.STRIPE_PUBLISHABLE_KEY
        })

    # Apply discount if any
    try:
        if discount_type and discount_amount:
            discount_amount = Decimal(discount_amount)
            order.discount_type = discount_type
            order.discount_amount = discount_amount
            order.update_totals()  # Recalculate totals
        else:
            order.discount_type = None
            order.discount_amount = Decimal('0.00')
    except (ValueError, InvalidOperation):
        return render(request, 'pos/orders/checkout.html', {
            'order': order,
            'modal_message': 'Montant de remise invalide.',
            'currency': active_currency,
            'stripe_publishable_key': settings.STRIPE_PUBLISHABLE_KEY
        })

    # Ensure payment amount is rounded to 2 decimals
    payment_amount = round(order.total_amount_with_tax, 2)
    change = None

    try:
        if payment_method == 'cash':
            if received_amount is None or received_amount < payment_amount:
                raise ValueError("Le montant reçu est insuffisant.")

            change = received_amount - payment_amount
            order.status = 'completed'

        elif payment_method in ['credit_card', 'debit_card']:
            payment_service = PaymentService()

            # Create a PaymentIntent
            payment_intent = payment_service.create_payment_intent(
                amount=payment_amount,
                currency=active_currency.code,
                payment_method_types=["card_present"]
            )
            order.payment_intent_id = payment_intent["id"]

            # Send to terminal and process payment
            try:
                response = payment_service.send_to_terminal(payment_intent["id"])
                if response["status"] == "succeeded":
                    order.status = 'completed'
                    received_amount = payment_amount
                    change = Decimal('0.00')
                else:
                    raise ValueError("Échec du paiement par terminal.")
            except Exception as e:
                raise ValueError(f"Erreur lors du paiement avec le terminal: {str(e)}")


    except stripe.error.CardError as e:
        logging.error(f"Stripe Card Error: {e.error.message}")
        return render(request, 'pos/orders/checkout.html', {
            'order': order,
            'modal_message': f"Erreur Stripe: {e.error.message}",
            'currency': active_currency,
            'stripe_publishable_key': settings.STRIPE_PUBLISHABLE_KEY
        })
    except Exception as e:
        logging.error(f"Unexpected Error: {str(e)}")
        return render(request, 'pos/orders/checkout.html', {
            'order': order,
            'modal_message': f"Erreur lors du traitement du paiement: {str(e)}",
            'currency': active_currency,
            'stripe_publishable_key': settings.STRIPE_PUBLISHABLE_KEY
        })

    # Create the bill and update the order
    bill = Bill.objects.create(
        order=order,
        bill_id=f'{order.id}-{timezone.now().strftime("%Y%m%d%H%M%S")}',
        payment_method=payment_method,
        payment_amount=payment_amount,
        received_amount=received_amount,
        change_amount=change
    )

    order.user = request.user
    order.payment_method = payment_method
    order.save()

    # Update user profile and handle notifications
    self.update_user_profile_and_notifications(order, request.user)

    # Redirect to the checkout completed page
    return render(request, 'pos/orders/checkout_complete.html', {
        'order': order,
        'bill': bill,
        'received_amount': received_amount,
        'change': change,
        'currency': active_currency,
        'stripe_publishable_key': settings.STRIPE_PUBLISHABLE_KEY,
        'success_message': 'Transaction terminée avec succès.'
    })


class CheckoutCompleteView(View):
    @method_decorator(login_required)
    def get(self, request, order_id):
        order = get_object_or_404(Order, id=order_id)
        
        # Get the currency from the first order item
        currency = None
        if order.items.exists():
            first_order_item = order.items.first()
            if first_order_item and first_order_item.batch.product.currency:
                currency = first_order_item.batch.product.currency

        return render(request, 'pos/orders/checkout_complete.html', {
            'order': order,
            'currency': currency
        })

    @method_decorator(login_required)
    def post(self, request, order_id):
        order = get_object_or_404(Order, id=order_id)
        print_receipt = request.POST.get('print_receipt') == 'yes'

        if print_receipt:
            return redirect('posApp:generate_pdf_receipt', order_id=order.id)

        # If not printing the receipt, just render the checkout complete page
        context = {
            'order': order,
            'currency': order.items.first().batch.product.currency if order.items.exists() else None,
        }
        return render(request, 'pos/checkout_complete.html', context)


# Backend Endpoint (send_to_terminal)
# Creating a backend view (send_to_terminal) to handle the terminal communication"

@login_required
def send_to_terminal(request, order_id):
    """
    Send the payment amount to the terminal.
    """
    if request.method == "POST":
        try:
            amount = Decimal(request.POST.get('amount', 0))
            if amount <= 0:
                return JsonResponse({'success': False, 'error': 'Montant non valide.'})

            # Create a PaymentIntent
            payment_service = PaymentService()
            payment_intent = payment_service.create_payment_intent(
                amount=amount,
                currency="CAD",
                payment_method_types=["card_present"]
            )

            # Fetch the online reader dynamically
            readers = stripe.Terminal.Reader.list(status="online").data
            if not readers:
                return JsonResponse({'success': False, 'error': 'Aucun lecteur en ligne trouvé.'})
            reader = readers[0]  # Use the first online reader

            # Send the payment to the terminal
            response = stripe.Terminal.Reader.process_payment_intent(
                reader["id"], {"payment_intent": payment_intent["id"]}
            )

            if response.get("status") == "succeeded":
                return JsonResponse({'success': True, 'payment_intent_id': payment_intent["id"]})
            else:
                return JsonResponse({'success': False, 'error': response.get("error", "Erreur du terminal.")})
        except Exception as e:
            return JsonResponse({'success': False, 'error': str(e)})

I've this pyment service's code as well:

import stripe
import logging
from decimal import Decimal
from django.conf import settings

class PaymentService:
    def __init__(self):
        """Initialize the PaymentService with the Stripe API key."""
        stripe.api_key = settings.STRIPE_SECRET_KEY
        self.logger = logging.getLogger(__name__)

    def get_online_reader(self):
        """
        Fetch the first online terminal reader from Stripe.
        :return: Stripe Terminal Reader object.
        :raises: ValueError if no online reader is found.
        """
        try:
            readers = stripe.Terminal.Reader.list(status="online").data
            if not readers:
                self.logger.error("Aucun lecteur de terminal en ligne trouvé.")
                raise ValueError("Aucun lecteur de terminal en ligne trouvé.")
            return readers[0]  # Return the first online reader
        except stripe.error.StripeError as e:
            self.logger.error(f"Erreur Stripe lors de la récupération des lecteurs: {str(e)}")
            raise Exception(f"Erreur Stripe: {str(e)}")

    def create_payment_intent(self, amount, currency="CAD", payment_method_types=None):
        """
        Create a payment intent for a terminal transaction.
        :param amount: Decimal, total amount to charge.
        :param currency: str, currency code (default: "CAD").
        :param payment_method_types: list, payment methods (default: ["card_present"]).
        :param capture_method: str, capture method for the payment intent.
        :return: Stripe PaymentIntent object.
        """
        try:
            if payment_method_types is None:
                payment_method_types = ["card_present"]

            payment_intent = stripe.PaymentIntent.create(
                amount=int(round(amount, 2) * 100),  # Convert to cents
                currency=currency.lower(),
                payment_method_types=payment_method_types,
                capture_method=capture_method
            )
            self.logger.info(f"PaymentIntent created: {payment_intent['id']}")
            return payment_intent
        except stripe.error.StripeError as e:
            self.logger.error(f"Stripe error while creating PaymentIntent: {str(e)}")
            raise Exception(f"Stripe error: {str(e)}")
        except Exception as e:
            self.logger.error(f"Unexpected error while creating PaymentIntent: {str(e)}")
            raise Exception(f"Unexpected error: {str(e)}")

    
    def send_to_terminal(self, payment_intent_id):
        """
        Send a payment intent to the online terminal reader for processing.
        :param payment_intent_id: str, ID of the PaymentIntent.
        :return: Stripe response from the terminal reader.
        """
        try:
            # Retrieve the Reader ID from settings
            reader_id = settings.STRIPE_READER_ID  # Ensure this is correctly set in your configuration
            
            # Send the payment intent to the terminal
            response = stripe.Terminal.Reader.process_payment_intent(
                reader_id, {"payment_intent": payment_intent_id}
            )
            
            self.logger.info(f"PaymentIntent {payment_intent_id} sent to reader {reader_id}.")
            return response
        except stripe.error.StripeError as e:
            self.logger.error(f"Erreur Stripe lors de l'envoi au terminal: {str(e)}")
            raise Exception(f"Erreur Stripe: {str(e)}")
        except Exception as e:
            self.logger.error(f"Unexpected error while sending to terminal: {str(e)}")
            raise Exception(f"Unexpected error: {str(e)}")

Here is my checkout template's code:

    <!-- Content Section -->
    <div class="content">
        <div class="row">
            <div class="col-md-8">
                <label align="center">Commande N° {{ order.id }}</label>
                <div class="table-responsive">
                    <table class="table table-striped">
                        <thead>
                            <tr>
                                <th>Produit</th>
                                <th>Quantité</th>
                                <th>Prix unitaire</th>
                                <th>Total</th>
                            </tr>
                        </thead>
                        <tbody>
                            {% for item in order.items.all %}
                            <tr>
                                <td>{{ item.product_batch.product.name }}</td>
                                <td>{{ item.quantity }}</td>
                                <td>
                                    {% if item.product_batch.discounted_price %}
                                        {{ item.product_batch.discounted_price }} {{ currency.symbol }}
                                    {% else %}
                                        {{ item.product_batch.price }} {{ currency.symbol }}
                                    {% endif %}
                                </td>
                                <td>
                                    {% if item.product_batch.discounted_price %}
                                        {{ item.quantity|multiply:item.product_batch.discounted_price|floatformat:2 }} {{ currency.symbol }}
                                    {% else %}
                                        {{ item.quantity|multiply:item.product_batch.price|floatformat:2 }} {{ currency.symbol }}
                                    {% endif %}
                                </td>
                            </tr>
                            {% endfor %}
                        </tbody>
                        <tfoot>
                            <tr>
                                <td colspan="3" class="text-right"><strong>Total à payer:</strong></td>
                                <td><strong>{{ order.total_amount_with_tax|floatformat:2 }} {{ currency.symbol }}</strong></td>
                            </tr>
                        </tfoot>
                    </table>
                </div>
            </div>

            <!-- Payment Section -->
            <div class="col-md-4">
                <form id="checkout-form" method="post">
                    <input type="hidden" id="stripe_payment_method_id" name="stripe_payment_method_id" value="">
                    {% csrf_token %}
                    <!-- Mode de Paiement -->
                    <div class="form-group">
                        <label for="payment_method">Mode de Paiement</label>
                        <select class="form-control" id="payment_method" name="payment_method" required>
                            <option value="cash" selected>Cash</option>
                            <option value="credit_card">Credit Card</option>
                            <option value="debit_card">Debit Card</option>
                            <option value="holo">Holo</option>
                            <option value="huri_money">Huri Money</option>
                        </select>
                    </div>

                    <!-- Discount Type -->
                    <div class="form-group">
                        <label for="discount_type">Type de réduction</label>
                        <select class="form-control" id="discount_type" name="discount_type">
                            <option value="">Aucune</option>
                            <option value="rabais">Rabais</option>
                            <option value="remise">Remise</option>
                            <option value="ristourne">Ristourne</option>
                        </select>
                    </div>

                    <!-- Discount Amount -->
                    <div class="form-group">
                        <label for="discount_amount">Montant de la réduction</label>
                        <input type="number" class="form-control" id="discount_amount" name="discount_amount" min="0" step="0.01" value="0.00">
                    </div>

                    <!-- Montant reçu (for cash payment) -->
                    <div class="form-group" id="cash-payment">
                        <label for="received_amount">Montant reçu</label>
                        <input type="number" class="form-control" id="received_amount" name="received_amount" min="0" step="0.01">
                        <small id="change" class="form-text text-muted"></small>
                    </div>

                    <!-- Payment card fields for Stripe -->
                    <div id="card-element" class="form-group" style="display:none;">
                        <!-- A Stripe Element will be inserted here. -->
                    </div>
                    <div id="card-errors" role="alert" class="form-text text-danger"></div>
                    <button type="submit" class="btn btn-success btn-block">Confirmer la commande</button>
                </form>
            </div>
        </div>
    </div>

    <!-- Stripe Integration & Checkout Form Handling -->
    <script src="https://js.stripe.com/v3/"></script>
    <script src="https://js.stripe.com/terminal/v1/"></script>
    <script>
    $(document).ready(function () {
        console.log("Initializing Stripe...");

        try {
            // Initialize Stripe
            const stripe = Stripe("{{ stripe_publishable_key }}");
            const elements = stripe.elements();

            // Create a card element
            const card = elements.create('card', {
                style: {
                    base: {
                        fontSize: '16px',
                        color: '#32325d',
                        '::placeholder': { color: '#aab7c4' }
                    },
                    invalid: {
                        color: '#fa755a',
                        iconColor: '#fa755a'
                    }
                }
            });

            // Mount the card element
            card.mount('#card-element');
            console.log("Card element mounted successfully.");

            // Function to toggle payment fields
            function togglePaymentFields() {
                const paymentMethod = $('#payment_method').val();
                console.log("Selected payment method:", paymentMethod);

                if (paymentMethod === 'cash') {
                    $('#cash-payment').show();
                    $('#card-element').hide();
                    card.unmount(); // Ensure card fields are unmounted
                    $('#received_amount').val('');
                    $('#change').text('');
                } else if (paymentMethod === 'credit_card' || paymentMethod === 'debit_card') {
                    $('#cash-payment').hide();
                    $('#card-element').show();
                    card.mount('#card-element'); // Remount card fields
                } else {
                    $('#cash-payment').hide();
                    $('#card-element').hide();
                    card.unmount();
                }
            }

            // Initialize the field toggle
            togglePaymentFields();

            // Trigger toggle on payment method change
            $('#payment_method').change(function () {
                togglePaymentFields();
            });

            // Update change amount dynamically
            $('#received_amount').on('input', function () {
                const received = parseFloat($(this).val());
                const total = parseFloat("{{ order.total_amount_with_tax }}");

                if (!isNaN(received) && received >= total) {
                    const change = received - total;
                    $('#change').text('Montant à retourner: ' + change.toFixed(2) + ' {{ currency.symbol }}');
                } else {
                    $('#change').text('');
                }
            });

            $(document).ready(function () {
                $('#checkout-form').on('submit', function (e) {
                    e.preventDefault();

                    const paymentMethod = $('#payment_method').val();
                    console.log("Form submission triggered. Selected payment method:", paymentMethod);

                    // Handle cash payment
                    if (paymentMethod === 'cash') {
                        const received = parseFloat($('#received_amount').val());
                        const total = parseFloat("{{ order.total_amount_with_tax }}");
                        const discountAmount = parseFloat($('#discount_amount').val()) || 0;

                        console.log("Received amount:", received, "Total amount:", total, "Discount amount:", discountAmount);

                        const finalTotal = total - discountAmount;

                        if (isNaN(received) || received < finalTotal) {
                            alert('Le montant reçu est insuffisant.');
                            return; // Prevent form submission
                        }

                        console.log("Cash payment validated. Submitting form...");
                        this.submit(); // Proceed with the form submission for cash payment
                    } 
                    // Handle credit or debit card payment
                    else if (paymentMethod === 'credit_card' || paymentMethod === 'debit_card') {
                        const totalAmount = parseFloat("{{ order.total_amount_with_tax }}");

                        console.log("Initiating terminal payment for:", totalAmount);

                        // Send the payment amount to the terminal
                        $.ajax({
                            type: 'POST',
                            url: "{% url 'posApp:send_to_terminal' order.id %}",
                            data: {
                                'csrfmiddlewaretoken': '{{ csrf_token }}',
                                'amount': totalAmount
                            },
                            success: function (response) {
                                if (response.success) {
                                    console.log("Amount successfully sent to terminal. Proceeding with payment confirmation...");

                                    alert('Paiement traité avec succès.');
                                    window.location.href = "{% url 'posApp:checkout_complete' order.id %}";
                                } else {
                                    alert(response.error || "Une erreur s'est produite lors de l'envoi au terminal.");
                                }
                            },
                            error: function (xhr, status, error) {
                                console.error("Error sending payment to terminal:", error);
                                alert('Une erreur s\'est produite lors de l\'envoi au terminal: ' + error);
                            }
                        });
                    } 
                    // Handle invalid payment method
                    else {
                        alert('Méthode de paiement invalide.');
                        console.error("Invalid payment method selected:", paymentMethod);
                    }
                });
            });

        } catch (err) {
            console.error("Error initializing Stripe or setting up payment fields:", err);
            alert("An error occurred while setting up the payment system. Check the console for details.");
        }
    });
</script>

During the Terminal Reader configuration, I added the registration code and location ID to my Stripe account. Are there any other settings that need to be configured, such as using the terminal's serial number in the POS system or any other required steps?

There are no other setup steps for the "server-driven" integration after you're paired your reader and set it to a location. You should then be able to follow the guide to collect card payments using the server-driven steps.

However, this process is not working.

This doesn't really give much to go on. I would recommend contact Stripe's support team and share a detailed description of where exactly in the code this stops working as you expected, and the unexpected actual behaviour that you observe. In particular, any errors in your POS device or server logs will likely provide important context.

https://support.stripe.com/contact

Вернуться на верх