Add basic modelling

This commit is contained in:
Nathan Chapman 2022-11-28 06:30:43 -07:00
parent 9f05b3d9c9
commit cb631823b2
5 changed files with 256 additions and 1 deletions

View File

@ -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

View File

@ -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,
}),
})
}
}

View 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 %}

View File

@ -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 %}

View File

@ -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):