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

问题描述 投票:0回答:1

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

现在,我决定使用 Stripe Terminal Reader 来简化流程。客户无需手动输入卡详细信息,只需在终端读卡器上刷卡、插入或点击卡即可付款。我订购的型号是 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 系统中的终端序列号或任何其他所需步骤?

python django stripe-payments
1个回答
0
投票

配对阅读器并将其设置到某个位置后,“服务器驱动”集成无需其他设置步骤。然后,您应该能够按照指南使用服务器驱动的步骤收集卡付款

但是,这个过程不起作用。

这并没有什么可继续的。我建议联系 Stripe 的支持团队,并分享详细说明,说明代码中的具体位置停止工作,以及您观察到的意外实际行为。特别是,您的 POS 设备或服务器日志中的任何错误都可能提供重要的上下文。

https://support.stripe.com/contact

© www.soinside.com 2019 - 2024. All rights reserved.