我正在使用 Django 开发 POS 系统。我有一个 Stripe 账户,通过我正在开发的系统,我可以使用信用卡或借记卡处理付款,并将资金存入我的 Stripe 账户。这是通过输入卡号、CVV 和到期日期等卡信息来完成的。
现在,我决定使用 Stripe Terminal Reader 来简化流程。客户无需手动输入卡详细信息,只需在终端读卡器上刷卡、插入或点击卡即可付款。我订购的型号是 BBPOS WisePOS E。我打开它,它生成了一个代码,我将其输入到我的 Stripe 帐户中。终端的在线或离线状态显示在我的 Stripe 账户中。
这个想法是,当我选择“借记卡或信用卡”作为付款方式时,要支付的金额应发送到终端。然而,这个过程不起作用。
终端仍然显示附图中显示的屏幕。”
我不知道我是否错过了一些需要完成的步骤才能使其发挥作用。
以下是我的功能:
@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 系统中的终端序列号或任何其他所需步骤?
配对阅读器并将其设置到某个位置后,“服务器驱动”集成无需其他设置步骤。然后,您应该能够按照指南使用服务器驱动的步骤收集卡付款。
但是,这个过程不起作用。
这并没有什么可继续的。我建议联系 Stripe 的支持团队,并分享详细说明,说明代码中的具体位置停止工作,以及您观察到的意外实际行为。特别是,您的 POS 设备或服务器日志中的任何错误都可能提供重要的上下文。