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:

  1. 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.

  2. 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.

views.py/finalize_payment
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.

views.py
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

views.py
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.

Desktop view
Mobile view

Here is the updated cart.html

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.

api_mercadopago.py
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