代码之家  ›  专栏  ›  技术社区  ›  Mohamed Abdillah

如何使用Django将Stripe终端阅读器集成到POS应用程序?

  •  0
  • Mohamed Abdillah  · 技术社区  · 1 月前

    我正在使用Django开发一个POS系统。我有一个Stripe账户,通过我正在开发的系统,我可以使用信用卡或借记卡处理付款,并将钱存入我的Stripe账户。这是通过键入卡号、CVV和到期日期等卡信息来完成的。

    现在,我决定使用Stripe终端阅读器来简化这个过程。客户无需手动输入卡详细信息,而是可以在终端阅读器上滑动、插入或点击他们的卡进行支付。我订购的型号是BBPOS WisePOS E。我打开它,它生成了一个代码,我将其输入了我的Stripe帐户。终端的在线或离线状态显示在我的Stripe帐户中。

    这个想法是,当我选择“借记卡或信用卡”作为支付方式时,要支付的金额应该发送到终端。然而,这一过程并未奏效。

    终端仍然显示所附图像中显示的屏幕。"

    如果你想进一步改进,请告诉我! enter image description here

    我不知道我是否错过了一些需要完成的步骤,才能使其发挥作用。

    以下是我的职责:

    @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)})
    

    我也有这个pyment服务的代码:

    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)}")
    

    这是我的结账模板的代码:

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

    在终端阅读器配置期间,我将注册码和位置ID添加到我的Stripe帐户中。是否需要配置其他设置,例如在POS系统中使用终端的序列号或任何其他所需步骤?

    1 回复  |  直到 1 月前
        1
  •  1
  •   Nolan H    1 月前

    在将阅读器配对并设置到某个位置后,“服务器驱动”集成没有其他设置步骤。然后,您应该能够按照指南 collect card payments 使用服务器驱动的步骤。

    然而,这一过程并未奏效。

    这并没有提供太多信息。我建议联系Stripe的支持团队,并分享一份详细的描述,说明这在代码中的确切位置停止了你所期望的工作,以及你观察到的意外实际行为。特别是,您的POS设备或服务器日志中的任何错误都可能提供重要的上下文。

    https://support.stripe.com/contact