Add basic modelling
This commit is contained in:
parent
9f05b3d9c9
commit
cb631823b2
@ -166,7 +166,7 @@ class ProductVariant(models.Model):
|
|||||||
)
|
)
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
sku = models.CharField(max_length=255, blank=True)
|
sku = models.CharField(max_length=255, blank=True)
|
||||||
stripe_id = models.CharField(max_length=255, blank=True)
|
stripe_id = models.CharField(max_length=255, blank=True, db_index=True)
|
||||||
price = models.DecimalField(
|
price = models.DecimalField(
|
||||||
max_digits=settings.DEFAULT_MAX_DIGITS,
|
max_digits=settings.DEFAULT_MAX_DIGITS,
|
||||||
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
|
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
|
||||||
@ -199,6 +199,44 @@ class ProductVariant(models.Model):
|
|||||||
ordering = ['sorting', 'weight']
|
ordering = ['sorting', 'weight']
|
||||||
|
|
||||||
|
|
||||||
|
class StripePrice:
|
||||||
|
# When this model is updated, it will query stripe and update all of the instances of this model
|
||||||
|
DAY = 'day'
|
||||||
|
WEEK = 'week'
|
||||||
|
MONTH = 'month'
|
||||||
|
YEAR = 'year'
|
||||||
|
INTERVAL_CHOICES = [
|
||||||
|
(DAY, 'Days'),
|
||||||
|
(WEEK, 'Weeks'),
|
||||||
|
(MONTH, 'Month'),
|
||||||
|
(YEAR, 'Year'),
|
||||||
|
]
|
||||||
|
|
||||||
|
stripe_id = models.CharField(max_length=255, blank=True, db_index=True)
|
||||||
|
variant = models.ForeignKey(
|
||||||
|
ProductVariant,
|
||||||
|
related_name='stripe_prices',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
unit_amount = models.DecimalField(
|
||||||
|
max_digits=settings.DEFAULT_MAX_DIGITS,
|
||||||
|
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
interval = models.CharField(
|
||||||
|
max_length=16,
|
||||||
|
choices=INTERVAL_CHOICES,
|
||||||
|
default=MONTH
|
||||||
|
)
|
||||||
|
interval_count = models.PositiveIntegerField(default=1)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|
||||||
class ProductOption(models.Model):
|
class ProductOption(models.Model):
|
||||||
"""
|
"""
|
||||||
Description: Consistent accross all variants
|
Description: Consistent accross all variants
|
||||||
|
|||||||
@ -121,6 +121,19 @@ class Subscription {
|
|||||||
this.items.push(item)
|
this.items.push(item)
|
||||||
return this.items
|
return this.items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createSubscription() {
|
||||||
|
fetch('/create-subscription', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
priceId: priceId,
|
||||||
|
customerId: customerId,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
112
src/storefront/templates/storefront/sub_payment.html
Normal file
112
src/storefront/templates/storefront/sub_payment.html
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script src="https://js.stripe.com/v3/"></script>
|
||||||
|
<script>const stripe = Stripe({{ STRIPE_API_KEY }});</script>
|
||||||
|
<script src="{% static 'scripts/subscriptions.js' %}" defer></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="site__banner site__banner--site">
|
||||||
|
<h1>Subscriptions</h1>
|
||||||
|
<h4>SUBSCRIBE AND SAVE</h4>
|
||||||
|
</div>
|
||||||
|
<article>
|
||||||
|
<section>
|
||||||
|
<form id="payment-form">
|
||||||
|
<div id="payment-element">
|
||||||
|
<!-- Elements will create form elements here -->
|
||||||
|
</div>
|
||||||
|
<button id="submit">Subscribe</button>
|
||||||
|
<div id="error-message">
|
||||||
|
<!-- Display error message to your customers here -->
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
<script>
|
||||||
|
const appearance = {
|
||||||
|
theme: 'flat',
|
||||||
|
variables: {
|
||||||
|
fontFamily: ' "Inter", sans-serif',
|
||||||
|
fontLineHeight: '1.75',
|
||||||
|
borderRadius: '10px',
|
||||||
|
colorBackground: '#fffbf8',
|
||||||
|
colorPrimaryText: '#34201a'
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'.Block': {
|
||||||
|
backgroundColor: 'var(--colorBackground)',
|
||||||
|
boxShadow: 'none',
|
||||||
|
padding: '12px'
|
||||||
|
},
|
||||||
|
'.Input': {
|
||||||
|
padding: '12px'
|
||||||
|
},
|
||||||
|
'.Input:disabled, .Input--invalid:disabled': {
|
||||||
|
color: 'lightgray'
|
||||||
|
},
|
||||||
|
'.Tab': {
|
||||||
|
padding: '10px 12px 8px 12px',
|
||||||
|
border: 'none'
|
||||||
|
},
|
||||||
|
'.Tab:hover': {
|
||||||
|
border: 'none',
|
||||||
|
boxShadow: '0px 1px 1px rgba(0, 0, 0, 0.03), 0px 3px 7px rgba(18, 42, 66, 0.04)'
|
||||||
|
},
|
||||||
|
'.Tab--selected, .Tab--selected:focus, .Tab--selected:hover': {
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
boxShadow: '0 0 0 1.5px var(--colorPrimaryText), 0px 1px 1px rgba(0, 0, 0, 0.03), 0px 3px 7px rgba(18, 42, 66, 0.04)'
|
||||||
|
},
|
||||||
|
'.Label': {
|
||||||
|
fontWeight: '500'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pass the appearance object to the Elements instance
|
||||||
|
const elements = stripe.elements({clientSecret, appearance});
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
clientSecret: '{{ CLIENT_SECRET }}',
|
||||||
|
// Fully customizable with appearance API.
|
||||||
|
appearance: {/*...*/},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set up Stripe.js and Elements to use in checkout form, passing the client secret obtained in step 5
|
||||||
|
const elements = stripe.elements(options);
|
||||||
|
|
||||||
|
// Create and mount the Payment Element
|
||||||
|
const paymentElement = elements.create('payment');
|
||||||
|
paymentElement.mount('#payment-element');
|
||||||
|
|
||||||
|
const form = document.getElementById('payment-form');
|
||||||
|
|
||||||
|
form.addEventListener('submit', async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const {error} = await stripe.confirmPayment({
|
||||||
|
//`Elements` instance that was used to create the Payment Element
|
||||||
|
elements,
|
||||||
|
confirmParams: {
|
||||||
|
return_url: "https://example.com/order/123/complete",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
// This point will only be reached if there is an immediate error when
|
||||||
|
// confirming the payment. Show error to your customer (for example, payment
|
||||||
|
// details incomplete)
|
||||||
|
const messageContainer = document.querySelector('#error-message');
|
||||||
|
messageContainer.textContent = error.message;
|
||||||
|
} else {
|
||||||
|
// Your customer will be redirected to your `return_url`. For some payment
|
||||||
|
// methods like iDEAL, your customer will be redirected to an intermediate
|
||||||
|
// site first to authorize the payment, then redirected to the `return_url`.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@ -2,6 +2,8 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
|
<script src="https://js.stripe.com/v3/"></script>
|
||||||
|
<script>const stripe = Stripe({{ STRIPE_API_KEY }});</script>
|
||||||
<script src="{% static 'scripts/subscriptions.js' %}" defer></script>
|
<script src="{% static 'scripts/subscriptions.js' %}" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -11,6 +13,11 @@
|
|||||||
<h4>SUBSCRIBE AND SAVE</h4>
|
<h4>SUBSCRIBE AND SAVE</h4>
|
||||||
</div>
|
</div>
|
||||||
<article>
|
<article>
|
||||||
|
<section>
|
||||||
|
{% for product in stripe_products %}
|
||||||
|
{{ product }}
|
||||||
|
{% endfor %}
|
||||||
|
</section>
|
||||||
<section class="">
|
<section class="">
|
||||||
<form action="" class="subscription-create-form">
|
<form action="" class="subscription-create-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|||||||
@ -379,6 +379,57 @@ class OrderCreateView(CreateView):
|
|||||||
return JsonResponse(data)
|
return JsonResponse(data)
|
||||||
|
|
||||||
|
|
||||||
|
# stripe listen --forward-to localhost:8000/stripe-webhook
|
||||||
|
@csrf_exempt
|
||||||
|
@require_POST
|
||||||
|
def stripe_webhook(request):
|
||||||
|
# You can use webhooks to receive information about asynchronous payment events.
|
||||||
|
# For more about our webhook events check out https://stripe.com/docs/webhooks.
|
||||||
|
webhook_secret = os.getenv('STRIPE_WEBHOOK_SECRET')
|
||||||
|
request_data = json.loads(request.data)
|
||||||
|
|
||||||
|
if webhook_secret:
|
||||||
|
# Retrieve the event by verifying the signature using the raw body and secret if webhook signing is configured.
|
||||||
|
signature = request.headers.get('stripe-signature')
|
||||||
|
try:
|
||||||
|
event = stripe.Webhook.construct_event(
|
||||||
|
payload=request.data, sig_header=signature, secret=webhook_secret)
|
||||||
|
data = event['data']
|
||||||
|
except Exception as e:
|
||||||
|
return e
|
||||||
|
# Get the type of webhook event sent - used to check the status of PaymentIntents.
|
||||||
|
event_type = event['type']
|
||||||
|
else:
|
||||||
|
data = request_data['data']
|
||||||
|
event_type = request_data['type']
|
||||||
|
|
||||||
|
data_object = data['object']
|
||||||
|
|
||||||
|
if event_type == 'invoice.paid':
|
||||||
|
# Used to provision services after the trial has ended.
|
||||||
|
# The status of the invoice will show up as paid. Store the status in your
|
||||||
|
# database to reference when a user accesses your service to avoid hitting rate
|
||||||
|
# limits.
|
||||||
|
messages.success(request, 'Paid')
|
||||||
|
logger.warning(data)
|
||||||
|
|
||||||
|
if event_type == 'invoice.payment_failed':
|
||||||
|
# If the payment fails or the customer does not have a valid payment method,
|
||||||
|
# an invoice.payment_failed event is sent, the subscription becomes past_due.
|
||||||
|
# Use this webhook to notify your user that their payment has
|
||||||
|
# failed and to retrieve new card details.
|
||||||
|
messages.warning(request, 'Payment failed')
|
||||||
|
logger.warning(data)
|
||||||
|
|
||||||
|
if event_type == 'customer.subscription.deleted':
|
||||||
|
# handle subscription canceled automatically based
|
||||||
|
# upon your subscription settings. Or if the user cancels it.
|
||||||
|
messages.error(request, 'Deleted')
|
||||||
|
logger.warning(data)
|
||||||
|
|
||||||
|
return jsonify({'status': 'success'})
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@require_POST
|
@require_POST
|
||||||
def paypal_order_transaction_capture(request, transaction_id):
|
def paypal_order_transaction_capture(request, transaction_id):
|
||||||
@ -539,13 +590,47 @@ class SubscriptionCreateView(FormView):
|
|||||||
success_url = reverse_lazy('storefront:payment-done')
|
success_url = reverse_lazy('storefront:payment-done')
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
stripe.api_key = settings.STRIPE_API_KEY
|
||||||
context = super().get_context_data(*args, **kwargs)
|
context = super().get_context_data(*args, **kwargs)
|
||||||
context['STRIPE_API_KEY'] = settings.STRIPE_API_KEY
|
context['STRIPE_API_KEY'] = settings.STRIPE_API_KEY
|
||||||
context['product_list'] = Product.objects.filter(
|
context['product_list'] = Product.objects.filter(
|
||||||
visible_in_listings=True
|
visible_in_listings=True
|
||||||
)
|
)
|
||||||
|
context['stripe_products'] = stripe.Price.list()
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
# TODO: Construct items element
|
||||||
|
items = []
|
||||||
|
subscription = self.create_subscription(items)
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def create_subscription(self, items):
|
||||||
|
# items=[{
|
||||||
|
# 'price': price_id,
|
||||||
|
# 'quantity': quantity
|
||||||
|
# }, {
|
||||||
|
# 'price': next_price_id,
|
||||||
|
# 'quantity': quantity
|
||||||
|
# }]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create the subscription. Note we're expanding the Subscription's
|
||||||
|
# latest invoice and that invoice's payment_intent
|
||||||
|
# so we can pass it to the front end to confirm the payment
|
||||||
|
subscription = stripe.Subscription.create(
|
||||||
|
customer=self.request.user.stripe_id,
|
||||||
|
items=items,
|
||||||
|
payment_behavior='default_incomplete',
|
||||||
|
payment_settings={'save_default_payment_method': 'on_subscription'},
|
||||||
|
expand=['latest_invoice.payment_intent'],
|
||||||
|
)
|
||||||
|
# TODO: pass this secret to the sub_payment.html as 'CLIENT_SECRET'
|
||||||
|
# clientSecret=subscription.latest_invoice.payment_intent.client_secret
|
||||||
|
return subscription
|
||||||
|
except Exception as e:
|
||||||
|
return messages.error(self.request, e.user_message)
|
||||||
|
|
||||||
|
|
||||||
class CreatePayment(View):
|
class CreatePayment(View):
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user