69.3 Item stock not updating and overloading payments
Currently, our item stock is not being updated when a user completes a purchase. Additionally, there is no verification in place to check whether an item in the user's cart has run out of stock by the time they attempt to finalize their purchase. This could lead to inconsistencies in stock management and user experience.
Another issue arises when a new payment instance is created each time the client clicks the "Confirm Order" button. These instances remain in the database indefinitely, as users cannot access older, unfinished payments. To resolve this, we will implement two fixes:
Limit Unfinished Payments: Restrict the number of unfinished payments per user to a maximum of 5. Older unfinished payments will be automatically deleted if the limit is exceeded.
Payment Link Expiry: Set a 1-hour expiration period for each payment link, ensuring that outdated links cannot be reused. This will improve payment management and enhance system efficiency.
def finalize_payment(request) :
data = request.GET.dict()
status = data.get("status")
payment_id = data.get("preference_id")
if status == "approved" :
payment = Payment.objects.get(payment_id=payment_id) #? getting the already existing payment
payment.aproved = True
order = payment.order
order.finished = True
order.end_date = datetime.now()
#? updating the stock
items_ordered = OrderedItem.objects.filter(order=order)
for item_ordered in items_ordered :
item_stock = ItemStock.objects.get(product=item_ordered.itemstock.product, size=item_ordered.itemstock.size, color=item_ordered.itemstock.color)
item_stock.quantity -= item_ordered.quantity
item_stock.save()
order.save()
payment.save()
#? email system
send_purchase_email(order)
if request.user.is_authenticated :
return redirect("my_orders") #? show finished orders
else :
return redirect("order_aproved", order.id)
else :
return redirect("checkout")
In the checkout
function, we will introduce a verification step to ensure that each product in the order is available in stock. If any item in the cart has insufficient stock, the user will be notified with an appropriate error message, preventing the checkout process from proceeding. This step will help maintain inventory consistency and provide a better user experience.
def checkout(request):
#! getting the client
if request.user.is_authenticated:
client = request.user.client
else :
if request.COOKIES.get('id_session') :
id_session = request.COOKIES.get("id_session")
client, created = Client.objects.get_or_create(id_session=id_session)
else : #? if the client enters directly on the cart, whithout generating cookies
return redirect('store') #? return directly to the store as the cart should be empty
order, created = Order.objects.get_or_create(client=client, finished=False)
items_ordered = OrderedItem.objects.filter(order=order)
if not items_ordered :
return redirect(f'/cart/?error=quantity')
for item in items_ordered :
if item.quantity > item.itemstock.quantity:
return redirect(f'/cart/?error=quantity')
addresses = Adres.objects.filter(client=client) #? filters all adresses associated with the client
context = {"order" : order, "addresses" : addresses, "error" : None}
return render(request, 'checkout.html', context)
A similar validation will be implemented in the cart
function. If the stock quantity of any item is less than the quantity ordered, the number of products in the cart for that item will be automatically adjusted to match the available stock. This ensures that users cannot order more items than are currently available and provides a seamless shopping experience.
def cart(request):
#! getting the client
error = request.GET.get('error')
cart_exists = False
if request.user.is_authenticated:
client = request.user.client
else :
if request.COOKIES.get('id_session') :
id_session = request.COOKIES.get("id_session")
client, created = Client.objects.get_or_create(id_session=id_session)
else : #? if the client enters directly on the cart, whithout generating cookies
context = {"existing_client": False, "order" : None, "items_ordered" : None, "cart_exists" : cart_exists}
return render(request, 'cart.html', context)
order, created = Order.objects.get_or_create(client=client, finished=False)
items_ordered = OrderedItem.objects.filter(order = order)
for item in items_ordered :
if item.quantity > item.itemstock.quantity :
item.quantity = item.itemstock.quantity
item.save()
if item.quantity <= 0 :
item.delete()
if len(items_ordered) > 0:
cart_exists = True
context = {"order" : order, "items_ordered" : items_ordered, "existing_client": True, "cart_exists" : cart_exists, "error" : error}
return render(request, 'cart.html', context)
Update the add_to_cart function so the max number of products you can add is the quantity of item_stock
def add_to_cart(request, product_id):
if request.method == "POST" and product_id : #? if the user is sending a new product
data = request.POST.dict() #? converts the request data to a dictionary
size = data.get('size') #? used get instead of ['size'] as it wont return a error
color_id = data.get('color')
if not size: #? only check the size as it only appears after selecting the color
return redirect(f'/product/{product_id}/?error=size_required')
#!getting the client
answer = redirect('cart') #? to implement cookies we need to edit the redirect response
if request.user.is_authenticated:
client = request.user.client
else :
if request.COOKIES.get("id_session") : #? checks if there is already a registred anonymous session
id_session = request.COOKIES.get("id_session")
else :
id_session = str(uuid.uuid4()) #? uuid4 guarantees uniqueness and safety
answer.set_cookie(key="id_session", value=id_session, max_age=60*60*24*40) #? max age in seconds
client, created = Client.objects.get_or_create(id_session=id_session)
order, created = Order.objects.get_or_create(client=client, finished=False)
item_stock = ItemStock.objects.get(product__id=product_id, size=size, color=color_id) #? In the forms we enter the color, id, and the size
item_ordered, created = OrderedItem.objects.get_or_create(order=order, itemstock=item_stock) #? adding the product to the cart
if item_ordered.quantity < item_stock.quantity :
item_ordered.quantity += 1
item_ordered.save() #? Must save changes made directly to a element
return answer
else :
return redirect('store') #? redirect the user to the store if he didn't choose a product
Observation: A new context variable, cart_exists
, has been added to the cart
function. This variable is used to determine whether the cart contains any items. If the cart is empty, it will trigger the display of an "empty cart" screen, improving user feedback and interface clarity.


Here is the updated cart.html
{% extends 'base.html' %}
{% load static %}
{% block body %}
<main class="principal">
<title>Cart | Reserva</title>
{% if existing_client and cart_exists%}
<section class="carrinho">
<div class="sacola">
<div class="sacola__titulos">
<h1 class="sacola__titulo">Cart</h1>
<p>
Order ID: <b>{{ order.id }}</b>
</p>
</div>
<table class="tabela">
<tr>
<th>Products</th>
<th>Unit Price</th>
<th>Quantity</th>
<th>Total Cost</th>
</tr>
{% for item in items_ordered %}
<tr>
<td class="tabela__produto">
<div class="tabela__imagem">
<img
src="{{ item.itemstock.product.image.url }}"
alt="{{ item.itemstock.product.name }}"
/>
</div>
<div class="tabela__produto-textos">
<p><b>{{ item.itemstock.product.name }}</b></p>
<p><b>Size:</b> {{ item.itemstock.size }}</p>
<p><b>Color:</b> {{ item.itemstock.color.name }}</p>
<p><b>Quantity in stock:</b> {{ item.itemstock.quantity }}</p>
</div>
</td>
<td class="tabela__preco-unit">
<p class="tabela__preco">R$ {{ item.itemstock.product.price }}</p>
</td>
<td class="tabela__qtd">
<div class="tabela__quantidade">
<!--! REMOVE BUTTON -->
<form method = "POST" action = "{% url 'remove_from_cart' item.itemstock.product.id %}"> <!--? Changed the product reference to the one being used in the code above-->
{% csrf_token %} <!--Protects (by generating a unique token) the forms from hackers trying to replicate it-->
<input type="hidden" name="size" value="{{ item.itemstock.size }}">
<input type="hidden" name="color" value="{{ item.itemstock.color.id }}">
<button type="submit">-</button>
</form>
{{ item.quantity}}
<!--! ADD BUTTON -->
<form method = "POST" action = "{% url 'add_to_cart' item.itemstock.product.id %}"> <!--? Changed the product reference to the one being used in the code above-->
{% csrf_token %} <!--Protects (by generating a unique token) the forms from hackers trying to replicate it-->
<input type="hidden" name="size" value="{{ item.itemstock.size }}">
<input type="hidden" name="color" value="{{ item.itemstock.color.id }}">
<button type="submit">+</button>
</form>
</div>
</td>
<td>
<p class="tabela__preco tabela__preco--total">R$ {{ item.total_price }}</p>
</td>
</tr>
{% endfor %}
</table>
</div>
<div class="subtotal">
<div class="subtotal__infos">
<p>Quantidade de Produtos</p>
<p>{{ order.total_quantity }}</p>
</div>
<div class="subtotal__infos subtotal__infos--sborda">
<p>Total</p>
<p>R$ {{ order.total_cost }}</p>
</div>
<a href="{% url 'checkout' %}" class="subtotal__botao">Go to checkout</a>
{% if error == "quantity" %}
<p style="margin-top:2rem;"class="checkout_erro">Stock quantity changed, revise the cart.</p>
{% endif %}
</div>
</section>
{% else %}
<section class="conta">
<div style="align-items: center;" class="conta__container">
<img
class="cabecalho__menu-icone"
src="{% static 'images/empty_cart.png' %}"
alt="Ícone Menu"
style="width: 6vw; height: 6vw; max-width:70px; max-height:70px; min-width:40px; min-height:40px;"
/>
<div class="checkout__titulos">
<p class="checkout__titulo">Your cart is empty</p>
</div>
<button class="subtotal__botao" type="button" onclick="location.href='{% url 'store' %}'">
Visit our store
</button>
</div>
</section>
{% endif %}
</main>
{% endblock %}
The cart
functionality also includes error messages for scenarios where the stock quantity of an item is insufficient. These messages will inform users of stock limitations, ensuring they understand why the cart has been adjusted.
Finally, in the api_mercadopago.py
file, we will implement a 1-hour expiration time for each purchase link. This ensures that payment links are valid only for a limited duration, enhancing security and maintaining accurate order processing workflows.
import mercadopago
from dotenv import load_dotenv
import os
from datetime import datetime, timedelta, timezone
load_dotenv()
public_key = os.getenv('mercado_public_key')
access_token = os.getenv('mercado_access_token')
def create_payment(items_ordered, link):
# Configure suas credenciais
sdk = mercadopago.SDK(access_token) # Validando access token
# Items que o usuário está comprando
items = []
for item in items_ordered:
quantity = int(item.quantity)
product_name = item.itemstock.product.name
unit_price = float(item.itemstock.product.price)
items.append({
"title": product_name,
"quantity": quantity,
"unit_price": unit_price
})
# Calcular a data de expiração (1 hora a partir do horário atual)
now_utc = datetime.now(timezone.utc)
expiration_time = now_utc + timedelta(hours=1)# Adicionar 1 hora
expiration_iso = expiration_time.isoformat(timespec='milliseconds') # Formatar em ISO 8601
# Criar os dados de preferência
preference_data = {
"items": items,
"auto_return": "all", # Retorno automático para o site após pagamento
"back_urls": { # Links que serão carregados em cada cenário de pagamento
"success": link,
"failure": link,
"pending": link,
},
"expires": True,
"expiration_date_to": expiration_iso # Adicionando a data de expiração
}
# Criar uma preferência
preference_response = sdk.preference().create(preference_data) # Criando requisição com várias informações sobre o pagamento
payment_link = preference_response["response"]["init_point"]
payment_id = preference_response["response"]["id"] # Obtendo o ID do pagamento para tratá-lo
return payment_link, payment_id
Last updated