shop
Raz 8 months ago
parent 8777706865
commit 6272fcc3e6
  1. 37
      padelclub_backend/settings.py
  2. 10
      shop/context_processors.py
  3. 18
      shop/migrations/0017_order_stripe_mode.py
  4. 4
      shop/models.py
  5. 4
      shop/signals.py
  6. 103
      shop/stripe_utils.py
  7. 8
      shop/templates/shop/cart.html
  8. 43
      shop/templates/shop/payment.html
  9. 46
      shop/views.py

@ -72,6 +72,8 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'shop.context_processors.stripe_context',
],
},
},
@ -154,22 +156,37 @@ AUTHENTICATION_BACKENDS = [
CSRF_COOKIE_SECURE = True # if using HTTPS
SESSION_COOKIE_SECURE = True # Si vous utilisez HTTPS
# Stripe Settings
STRIPE_PUBLISHABLE_KEY = 'pk_test_51R4LrTPEZkECCx484C2KbmRpcO2ZkZb0NoNi8QJB4X3E5JFu3bvLk4JZQmz9grKbk6O40z3xI8DawHrGyUY0fOT600VEKC9ran' # Replace with your actual key
STRIPE_SECRET_KEY = 'sk_test_51R4LrTPEZkECCx48PkSbEYarhts7J7XNYpS1mJgows5z5dcv38l0G2tImvhXCjzvMgUH9ML0vLMOEPeyUBtYVf5H00Qvz8t3rE' # Replace with your actual key
STRIPE_WEBHOOK_SECRET = 'whsec_cbaa9c0c7b24041136e063a7d60fb674ec0646b2c4b821512c41a27634d7b1ba' # Optional for later
STRIPE_CURRENCY = 'eur' # Set to your preferred currency
# STRIPE_PUBLISHABLE_KEY = os.environ.get('STRIPE_PUBLISHABLE_KEY', 'your_test_publishable_key')
# STRIPE_SECRET_KEY = os.environ.get('STRIPE_SECRET_KEY', 'your_test_secret_key')
# STRIPE_WEBHOOK_SECRET = os.environ.get('STRIPE_WEBHOOK_SECRET', 'your_test_webhook_secret')
STRIPE_MODE = os.environ.get('STRIPE_MODE', 'test') # Default to test mode
# Depending on the mode, use appropriate keys
if STRIPE_MODE == 'test':
STRIPE_PUBLISHABLE_KEY = os.environ.get(
'STRIPE_TEST_PUBLISHABLE_KEY',
'pk_test_51R4LrTPEZkECCx484C2KbmRpcO2ZkZb0NoNi8QJB4X3E5JFu3bvLk4JZQmz9grKbk6O40z3xI8DawHrGyUY0fOT600VEKC9ran'
)
STRIPE_SECRET_KEY = os.environ.get(
'STRIPE_TEST_SECRET_KEY',
'sk_test_51R4LrTPEZkECCx48PkSbEYarhts7J7XNYpS1mJgows5z5dcv38l0G2tImvhXCjzvMgUH9ML0vLMOEPeyUBtYVf5H00Qvz8t3rE'
)
STRIPE_WEBHOOK_SECRET = os.environ.get(
'STRIPE_TEST_WEBHOOK_SECRET',
'whsec_cbaa9c0c7b24041136e063a7d60fb674ec0646b2c4b821512c41a27634d7b1ba'
)
else:
STRIPE_PUBLISHABLE_KEY = os.environ.get('STRIPE_LIVE_PUBLISHABLE_KEY', '')
STRIPE_SECRET_KEY = os.environ.get('STRIPE_LIVE_SECRET_KEY', '')
STRIPE_WEBHOOK_SECRET = os.environ.get('STRIPE_LIVE_WEBHOOK_SECRET', '')
STRIPE_CURRENCY = 'eur'
# Add managers who should receive internal emails
MANAGERS = [
SHOP_MANAGERS = [
('Razmig Sarkissian', 'razmig@padelclub.app'),
# ('Shop Admin', 'shop-admin@padelclub.app'),
# ('Laurent Morvillier', 'laurent@padelclub.app'),
# ('Xavier Rousset', 'xavier@padelclub.app'),
]
SITE_URL = 'https://padelclub.app'
SHOP_SUPPORT_EMAIL = 'shop-support@padelclub.app'
from .settings_local import *
from .settings_app import *

@ -0,0 +1,10 @@
from django.conf import settings
def stripe_context(request):
"""Add Stripe-related context variables to templates"""
stripe_mode = getattr(settings, 'STRIPE_MODE', 'test')
return {
'STRIPE_PUBLISHABLE_KEY': settings.STRIPE_PUBLISHABLE_KEY,
'STRIPE_MODE': stripe_mode,
'STRIPE_IS_TEST_MODE': stripe_mode == 'test',
}

@ -0,0 +1,18 @@
# Generated by Django 4.2.11 on 2025-03-21 08:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shop', '0016_order_webhook_processed'),
]
operations = [
migrations.AddField(
model_name='order',
name='stripe_mode',
field=models.CharField(choices=[('test', 'Test Mode'), ('live', 'Live Mode')], default='test', max_length=10),
),
]

@ -80,6 +80,10 @@ class Order(models.Model):
('FAILED', 'Failed'),
])
webhook_processed = models.BooleanField(default=False)
stripe_mode = models.CharField(max_length=10, default='test', choices=[
('test', 'Test Mode'),
('live', 'Live Mode'),
])
def __str__(self):
return f"Order #{self.id} - {self.status}"

@ -115,7 +115,7 @@ Ceci est un message automatique. Merci de ne pas répondre.
"""
# Send internal email
recipient_list = [email for name, email in settings.MANAGERS]
recipient_list = [email for name, email in settings.SHOP_MANAGERS]
if not recipient_list:
recipient_list = [settings.DEFAULT_FROM_EMAIL]
@ -131,7 +131,7 @@ Ceci est un message automatique. Merci de ne pas répondre.
if status == OrderStatus.PAID and customer_email and instance.payment_status == "PAID":
# Generate customer-facing URLs
shop_url = f"{settings.SITE_URL}/shop"
contact_email = "support@padelclub.app"
contact_email = f"{settings.SHOP_SUPPORT_EMAIL}"
# Create a customer receipt email
customer_subject = f"Confirmation de votre commande #{order_id} - PadelClub"

@ -1,47 +1,90 @@
import stripe
from django.conf import settings
import logging
# Configure Stripe with your secret key
stripe.api_key = settings.STRIPE_SECRET_KEY
logger = logging.getLogger(__name__)
def create_payment_intent(amount, currency=settings.STRIPE_CURRENCY, metadata=None):
"""
Create a payment intent with Stripe
class StripeService:
"""Service class to manage Stripe operations with mode awareness"""
Args:
amount: Amount in cents (e.g., 1000 for 10.00)
currency: Currency code (default: settings.STRIPE_CURRENCY)
metadata: Additional info to attach to the payment intent
def __init__(self):
# Determine if we're in test mode
self.mode = getattr(settings, 'STRIPE_MODE', 'test')
self.is_test_mode = self.mode == 'test'
Returns:
The created payment intent object
"""
intent = stripe.PaymentIntent.create(
# Get appropriate keys based on mode
self.api_key = settings.STRIPE_SECRET_KEY
self.publishable_key = settings.STRIPE_PUBLISHABLE_KEY
self.webhook_secret = settings.STRIPE_WEBHOOK_SECRET
self.currency = getattr(settings, 'STRIPE_CURRENCY', 'eur')
# Configure Stripe library
stripe.api_key = self.api_key
# Log initialization in debug mode
mode_str = "TEST" if self.is_test_mode else "LIVE"
logger.debug(f"Initialized StripeService in {mode_str} mode")
def create_checkout_session(self, line_items, success_url, cancel_url, metadata=None):
"""Create a Stripe Checkout Session for one-time payments"""
if self.is_test_mode:
logger.info(f"Creating checkout session in TEST mode with metadata: {metadata}")
session = stripe.checkout.Session.create(
payment_method_types=['card'],
line_items=line_items,
mode='payment',
success_url=success_url,
cancel_url=cancel_url,
metadata=metadata or {},
)
return session
def verify_webhook_signature(self, payload, signature):
"""Verify webhook signature using mode-appropriate secret"""
try:
event = stripe.Webhook.construct_event(
payload, signature, self.webhook_secret
)
return event
except stripe.error.SignatureVerificationError as e:
logger.error(f"Webhook signature verification failed: {str(e)}")
raise
def construct_event_for_testing(self, payload, signature):
"""Helper method to construct events during testing"""
return stripe.Webhook.construct_event(
payload, signature, self.webhook_secret
)
def get_checkout_session(self, session_id):
"""Retrieve a checkout session by ID"""
return stripe.checkout.Session.retrieve(session_id)
def get_payment_intent(self, payment_intent_id):
"""Retrieve a payment intent by ID"""
return stripe.PaymentIntent.retrieve(payment_intent_id)
# Create a singleton instance for import and use throughout the app
stripe_service = StripeService()
# For backward compatibility, expose some functions directly
def create_payment_intent(amount, currency=None, metadata=None):
"""Legacy function for backward compatibility"""
if currency is None:
currency = stripe_service.currency
return stripe.PaymentIntent.create(
amount=amount,
currency=currency,
metadata=metadata or {},
)
return intent
def create_checkout_session(line_items, success_url, cancel_url, metadata=None):
"""
Create a Stripe Checkout Session for one-time payments
Args:
line_items: List of items to purchase
success_url: URL to redirect on successful payment
cancel_url: URL to redirect on canceled payment
metadata: Additional info to attach to the session
Returns:
The created checkout session
"""
session = stripe.checkout.Session.create(
payment_method_types=['card'],
"""Legacy function for backward compatibility"""
return stripe_service.create_checkout_session(
line_items=line_items,
mode='payment',
success_url=success_url,
cancel_url=cancel_url,
metadata=metadata or {},
)
return session

@ -16,6 +16,14 @@
<a href="{% url 'login' %}">Se connecter</a>
{% endif %}
</nav>
{% if STRIPE_IS_TEST_MODE %}
<div class="alert alert-warning" style="background-color: #fff3cd; color: #856404; padding: 10px; margin: 10px 0; border-radius: 5px; border: 1px solid #ffeeba;">
<strong> Test Mode:</strong> Stripe is currently in test mode. No real payments will be processed.
<br>
<small>Use test card: 4242 4242 4242 4242 with any future date and any CVC.</small>
</div>
{% endif %}
<div class="grid-x">
<div class="small-12 medium-9 large-6 my-block">
<h1 class="club my-block topmargin20">Votre panier</h1>

@ -5,6 +5,13 @@
{% block second_title %}La Boutique{% endblock %}
{% block content %}
{% if STRIPE_IS_TEST_MODE %}
<div class="alert alert-warning" style="background-color: #fff3cd; color: #856404; padding: 10px; margin: 10px 0; border-radius: 5px; border: 1px solid #ffeeba;">
<strong> Test Mode:</strong> Stripe is currently in test mode. No real payments will be processed.
<br>
<small>Use test card: 4242 4242 4242 4242 with any future date and any CVC.</small>
</div>
{% endif %}
<nav class="margin10">
<a href="{% url 'shop:product_list' %}">La Boutique</a>
<a href="{% url 'index' %}" class="orange">Accueil</a>
@ -19,40 +26,10 @@
<div class="grid-x">
<div class="cell medium-6 large-6 my-block">
<h1 class="club my-block topmargin20">Paiement</h1>
<h1 class="club my-block topmargin20">Résumé de votre commande</h1>
<div class="bubble">
<h2>Résumé de votre commande</h2>
<!-- Display order items in a table -->
<table class="cart-table">
<thead>
<tr>
<th class="text-left">Produit</th>
<th>Couleur</th>
<th>Taille</th>
<th>Quantité</th>
<th class="price-column">Prix</th>
</tr>
</thead>
<tbody>
{% for item in order_items %}
<tr class="{% cycle 'odd' 'even' %}">
<td class="text-left">{{ item.product.title }}</td>
<td>{{ item.color.name|default:"N/A" }}</td>
<td>{{ item.size.name|default:"N/A" }}</td>
<td>{{ item.quantity }}</td>
<td class="price-column">{{ item.get_total_price }} €</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="3" class="total-label text-left">Total:</td>
<td class="total-quantity">{{ order_items.count }}</td>
<td class="price-column total-price">{{ order.total_price }} €</td>
</tr>
</tfoot>
</table>
<h3>Détails de la commande</h3>
{% include 'shop/partials/order_items_display.html' with items=display_data.items total_quantity=display_data.total_quantity total_price=display_data.total_price edit_mode=False %}
<!-- Stripe checkout button -->
<div id="payment-container">

@ -1,3 +1,4 @@
from .stripe_utils import stripe_service
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from .models import Product, Order, OrderItem, GuestUser, OrderStatus
@ -46,7 +47,7 @@ def _create_stripe_checkout_session(request, order, line_items):
# Create metadata to identify this order
metadata = {
'order_id': str(order.id), # Convert to string to be safe
'order_id': str(order.id),
}
# Add user info to metadata if available
@ -56,14 +57,9 @@ def _create_stripe_checkout_session(request, order, line_items):
metadata['guest_email'] = request.session.get('guest_email', '')
try:
# Initialize Stripe with your secret key
stripe.api_key = settings.STRIPE_SECRET_KEY
# Create session
checkout_session = stripe.checkout.Session.create(
payment_method_types=['card'],
# Use the service to create the session
checkout_session = stripe_service.create_checkout_session(
line_items=line_items,
mode='payment',
success_url=success_url,
cancel_url=cancel_url,
metadata=metadata,
@ -76,7 +72,6 @@ def _create_stripe_checkout_session(request, order, line_items):
return checkout_session
except Exception as e:
# Log error and re-raise
print(f"Stripe error: {str(e)}")
raise
@ -159,7 +154,9 @@ def create_order(request):
# Authenticated user order
order = Order.objects.create(
user=request.user,
total_price=total_price
total_price=total_price,
stripe_mode=stripe_service.mode # Add this line
)
else:
# Guest user order
@ -167,12 +164,15 @@ def create_order(request):
guest_user = GuestUser.objects.get(email=request.session['guest_email'])
order = Order.objects.create(
guest_user=guest_user,
total_price=total_price
total_price=total_price,
stripe_mode=stripe_service.mode # Add this line
)
except (KeyError, GuestUser.DoesNotExist):
# No guest user information, create order without user
order = Order.objects.create(
total_price=total_price
total_price=total_price,
stripe_mode=stripe_service.mode # Add this line
)
# Create order items
@ -282,10 +282,12 @@ def payment(request, order_id):
messages.error(request, f"Payment processing error: {str(e)}")
return redirect('shop:view_cart')
display_data = prepare_item_display_data(order_items, is_cart=False)
# Render payment page
return render(request, 'shop/payment.html', {
'order': order,
'order_items': order_items,
'display_data': display_data,
'checkout_session_id': checkout_session_id,
'stripe_publishable_key': settings.STRIPE_PUBLISHABLE_KEY,
})
@ -474,7 +476,7 @@ def simulate_payment_failure(request):
@require_POST
@csrf_exempt # Stripe can't provide CSRF tokens
@csrf_exempt
def stripe_webhook(request):
"""Handle Stripe webhook events"""
payload = request.body
@ -488,23 +490,17 @@ def stripe_webhook(request):
return HttpResponse(status=400)
try:
# Initialize Stripe
stripe.api_key = settings.STRIPE_SECRET_KEY
# Verify the event using webhook signing secret
event = stripe.Webhook.construct_event(
payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
)
# Use the service to verify the webhook
event = stripe_service.verify_webhook_signature(payload, sig_header)
# Log the event type
print(f"Webhook event type: {event['type']}")
# Log the event type and mode
mode = "TEST" if stripe_service.is_test_mode else "LIVE"
print(f"{mode} webhook event type: {event['type']}")
except ValueError as e:
# Invalid payload
print(f"Invalid payload: {str(e)}")
return HttpResponse(status=400)
except stripe.error.SignatureVerificationError as e:
# Invalid signature
print(f"Invalid signature: {str(e)}")
return HttpResponse(status=400)

Loading…
Cancel
Save