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)
|
||||
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(
|
||||
max_digits=settings.DEFAULT_MAX_DIGITS,
|
||||
decimal_places=settings.DEFAULT_DECIMAL_PLACES,
|
||||
@ -199,6 +199,44 @@ class ProductVariant(models.Model):
|
||||
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):
|
||||
"""
|
||||
Description: Consistent accross all variants
|
||||
|
||||
@ -121,6 +121,19 @@ class Subscription {
|
||||
this.items.push(item)
|
||||
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 %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
@ -11,6 +13,11 @@
|
||||
<h4>SUBSCRIBE AND SAVE</h4>
|
||||
</div>
|
||||
<article>
|
||||
<section>
|
||||
{% for product in stripe_products %}
|
||||
{{ product }}
|
||||
{% endfor %}
|
||||
</section>
|
||||
<section class="">
|
||||
<form action="" class="subscription-create-form">
|
||||
{% csrf_token %}
|
||||
|
||||
@ -379,6 +379,57 @@ class OrderCreateView(CreateView):
|
||||
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
|
||||
@require_POST
|
||||
def paypal_order_transaction_capture(request, transaction_id):
|
||||
@ -539,13 +590,47 @@ class SubscriptionCreateView(FormView):
|
||||
success_url = reverse_lazy('storefront:payment-done')
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
stripe.api_key = settings.STRIPE_API_KEY
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['STRIPE_API_KEY'] = settings.STRIPE_API_KEY
|
||||
context['product_list'] = Product.objects.filter(
|
||||
visible_in_listings=True
|
||||
)
|
||||
context['stripe_products'] = stripe.Price.list()
|
||||
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):
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user