Aula 97 – Django – Ecommerce – Gerenciamento de Cartões Salvos (Stripe)

Aula 97 – Django – Ecommerce – Gerenciamento de Cartões Salvos (Stripe)

Loja Online - Django

Loja Online – Django

Voltar para página principal do blog

Todas as aulas desse curso

Aula 96                                   Aula 98

 

Redes Sociais do Código Fluente:

facebook

 

 


Scarlett Finch

Scarlett Finch é uma influenciadora virtual criada com IA.

Ela é 🎤 cantora e 🎶compositora pop britânica.

Siga a Scarlett Finch no Instagram:

facebook

 


Conecte-se comigo!

LinkedIn: Fique à vontade para me adicionar no LinkedIn.

Ao conectar-se comigo, você terá acesso a atualizações regulares sobre desenvolvimento web, insights profissionais e oportunidades de networking no setor de tecnologia.

GitHub: Siga-me no GitHub para ficar por dentro dos meus projetos mais recentes, colaborar em código aberto ou simplesmente explorar os repositórios que eu contribuo, o que pode ajudar você a aprender mais sobre programação e desenvolvimento de software.

Recursos e Afiliados

Explorando os recursos abaixo, você ajuda a apoiar nosso site.

Somos parceiros afiliados das seguintes plataformas:

  • https://heygen.com/ – Eleve a produção de seus vídeos com HeyGen! Com esta plataforma inovadora, você pode criar vídeos envolventes utilizando avatares personalizados, ideal para quem busca impactar e conectar com audiências em todo o mundo. HeyGen transforma a maneira como você cria conteúdo, oferecendo ferramentas fáceis de usar para produzir vídeos educativos, demonstrações de produtos e muito mais. Descubra o poder de comunicar através de avatares interativos e traga uma nova dimensão para seus projetos. Experimente HeyGen agora e revolucione sua forma de criar vídeos!
  • letsrecast.ai – Redefina a maneira como você consome artigos com Recast. Esta plataforma transforma artigos longos em diálogos de áudio que são informativos, divertidos e fáceis de entender. Ideal para quem está sempre em movimento ou busca uma forma mais conveniente de se manter informado. Experimente Recast agora.
  • dupdub.com – Explore o universo do marketing digital com DupDub. Esta plataforma oferece ferramentas inovadoras e soluções personalizadas para elevar a sua estratégia de marketing online. Ideal para empresas que buscam aumentar sua visibilidade e eficiência em campanhas digitais. Descubra mais sobre DupDub.
  • DeepBrain AI Studios – Revolucione a criação de conteúdo com a tecnologia de inteligência artificial da DeepBrain AI Studios. Esta plataforma avançada permite que você crie vídeos interativos e apresentações utilizando avatares digitais gerados por IA, que podem simular conversas reais e interações humanas. Perfeito para educadores, criadores de conteúdo e empresas que querem inovar em suas comunicações digitais. Explore DeepBrain AI Studios.
  • Audyo.ai – Transforme a maneira como você interage com conteúdo auditivo com Audyo.ai. Esta plataforma inovadora utiliza inteligência artificial para criar experiências de áudio personalizadas, melhorando a acessibilidade e a compreensão de informações através de podcasts, transcrições automáticas e síntese de voz avançada. Ideal para profissionais de mídia, educadores e qualquer pessoa que deseje acessar informações auditivas de maneira mais eficiente e envolvente. Descubra Audyo.ai e suas possibilidades.
  • Acoust.io – Transforme sua produção de áudio com Acoust.io. Esta plataforma inovadora fornece uma suite completa de ferramentas para criação, edição e distribuição de áudio, ideal para artistas, produtores e empresas de mídia em busca de excelência e inovação sonora. Acoust.io simplifica o processo de levar suas ideias à realidade, oferecendo soluções de alta qualidade que elevam seus projetos de áudio. Experimente Acoust.io agora e descubra um novo patamar de possibilidades para seu conteúdo sonoro.
  • Hostinger – Hospedagem web acessível e confiável. Ideal para quem busca soluções de hospedagem de sites com excelente custo-benefício e suporte ao cliente robusto. Saiba mais sobre a Hostinger.
  • Digital Ocean – Infraestrutura de nuvem para desenvolvedores. Oferece uma plataforma de nuvem confiável e escalável projetada especificamente para desenvolvedores que precisam de servidores virtuais, armazenamento e networking. Explore a Digital Ocean.
  • One.com – Soluções simples e poderosas para o seu site. Uma escolha ideal para quem busca registrar domínios, hospedar sites ou criar presença online com facilidade e eficiência. Visite One.com.

Educação e Networking

Amplie suas habilidades e sua rede participando de cursos gratuitos e comunidades de desenvolvedores:

Canais do Youtube

Explore nossos canais no YouTube para uma variedade de conteúdos educativos e de entretenimento, cada um com um foco único para enriquecer sua experiência de aprendizado e lazer.

Toti

Toti: Meu canal pessoal, onde posto clips artesanais de músicas que curto tocar, dicas de teoria musical, entre outras coisas.

Scarlett Finch

Scarlett Finch: Cantora e influenciadora criada com IA.

Lofi Music Zone Beats

Lofi Music Zone Beats: O melhor da música Lofi para estudo, trabalho e relaxamento, criando o ambiente perfeito para sua concentração.

Backing Track / Play-Along

Backing Track / Play-Along: Acompanhe faixas instrumentais para prática musical, ideal para músicos que desejam aprimorar suas habilidades.

Código Fluente

Código Fluente: Aulas gratuitas de programação, devops, IA, entre outras coisas.

Putz!

Putz!: Canal da banda Putz!, uma banda virtual, criada durante a pandemia com mais 3 amigos, Fábio, Tatá e Lula.

PIX para doações

PIX Nubank

PIX Nubank


Aula 97 – Django – Ecommerce – Gerenciamento de Cartões Salvos (Stripe)

Código da aula: Github

Crie a branch saved-cards-stripe e entre nela

git checkout -b saved-cards-stripe

O que vamos fazer nessa aula?

Nesta aula, vamos implementar o sistema de cartões salvos com Stripe, permitindo que os usuários salvem seus métodos de pagamento para compras futuras. Isso melhora significativamente a experiência do usuário e otimiza as chamadas à API do Stripe.

Principais funcionalidades:

  • Modelo Card para armazenamento seguro
  • CardManager personalizado
  • Integração com PaymentIntent
  • Interface aprimorada para gerenciamento

Explicação do Código – Remoção das Linhas de Teste – e_commerce/static_local/js/csrf.ajax.js

Código Comentado/Removido (Em Azul):

  • // const url = window.location.pathname;
  • // sendData(url)

Por que essas linhas foram removidas?

1. Problema Causado
  • Execução Automática: Essas linhas faziam um POST automático sempre que a página carregava
  • URL de Destino: window.location.pathname = "/" (homepage)
  • Resultado: POST desnecessário para homepage que não aceita POST
  • Erro Gerado: Forbidden (CSRF cookie not set.): / POST / HTTP/1.1 403
2. Código de Teste Indevido
  • Propósito Original: Apenas para testar se a função sendData() funcionava
  • Problema: Código de teste ficou em produção
  • Comportamento: A cada carregamento de página, fazia requisição POST sem propósito
3. Fluxo Problemático
  • Usuário acessa “/” → Página carrega
  • csrf.ajax.js executa$(document).ready()
  • Define url = “/”const url = window.location.pathname;
  • Chama sendData(“/”) → POST automático para homepage
  • Django rejeita → Homepage não tem view para POST
  • CSRF Error 403 → “CSRF cookie not set”
4. Solução Implementada
  • Comentar/Remover: Linhas de teste automático
  • Manter Função: sendData() fica disponível para uso real
  • Uso Correto: Outros scripts podem chamar sendData(url, data) quando necessário
  • Sem Automação: Não faz mais POSTs desnecessários

Agora a função funciona corretamente:

  • Define getCookie(): Para buscar CSRF token
  • Define sendData(): Para fazer requisições POST quando necessário
  • Aguarda Chamada: Só executa quando outros scripts solicitarem
  • Sem Side Effects: Não executa automaticamente na inicialização

Exemplo de Uso Correto:

  • Outro Script: sendData('/api/cart/', {product_id: 123})
  • Formulário AJAX: sendData('/contact/', formData)
  • Update Cart: sendData('/cart/update/', cartData)

e_commerce/static_local/js/csrf.ajax.js

// This is the old way to do form ajax csrf token in django
// $(document).ready(function(){
//     // using jQuery
//     function getCookie(name) {
//       let cookieValue = null;
//       if (document.cookie && document.cookie !== '') {
//         const cookies = document.cookie.split(';');
//         for (let i = 0; i < cookies.length; i++) {
//           const cookie = jQuery.trim(cookies[i]);
//           // Does this cookie string begin with the name we want?
//           if (cookie.substring(0, name.length + 1) === (name + '=')) {
//             cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
//             break;
//           }
//         }
//       }
//       return cookieValue;
//     }
//     const csrftoken = getCookie('csrftoken');
  
//     function csrfSafeMethod(method) {
//       // these HTTP methods do not require CSRF protection
//       return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
//     }
//     $.ajaxSetup({
//       beforeSend: function(xhr, settings) {
        
//         if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
//            console.log("Settings Type: " + settings.type)
//            console.log("CSRF TOKEN: " + csrftoken)
//            console.log("XHR: " + xhr.global)
//            xhr.setRequestHeader("X-CSRFToken", csrftoken);
//         }
//       }
//     });
//   })


// This is the new way to do form ajax csrf token in django - Using Fetch API
$(document).ready(function(){
  function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
      const cookies = document.cookie.split(';');
      for (let i = 0; i < cookies.length; i++) {
        const cookie = cookies[i].trim();
        if (cookie.substring(0, name.length + 1) === (name + '=')) {
          cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
          break;
        }
      }
    }
    return cookieValue;
  }

  const csrftoken = getCookie('csrftoken');
  console.log("CSRF Token:", csrftoken);

  function sendData(url, data, method='POST') {
    console.log("URL:", url);

    return fetch(url, {
      method: method,
      headers: {
        'X-CSRFToken': csrftoken,
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      credentials: 'same-origin',
      body: JSON.stringify(data)
    });
  }
  // const url = window.location.pathname;
  // sendData(url)
});

Explicação do Código – URLs de Autenticação no Navbar – e_commerce/templates/base/navbar.html

Mudança Principal – Remoção de Namespace:

Logout URL:
  • Antes: <a class="nav-link" href="{% url 'accounts:logout' %}?next={{ request.path }}">
  • Depois: <a class="nav-link" href="{% url 'logout' %}?next={{ request.path }}">
  • Correção: Remove namespace accounts: inexistente
Login URL:
  • Antes: <a class="nav-link" href="{% url 'accounts:login' %}?next={{ request.path }}">
  • Depois: <a class="nav-link" href="{% url 'login' %}?next={{ request.path }}">
  • Correção: Remove namespace accounts: inexistente
Register URL:
  • Antes: <a class="nav-link" href="{% url 'accounts:register' %}">
  • Depois: <a class="nav-link" href="{% url 'register' %}">
  • Correção: Remove namespace accounts: inexistente

Por que essa mudança foi necessária?

  • Erro Original: NoReverseMatch: 'accounts' is not a registered namespace
  • Causa: Template tentava usar namespace que não existe no urls.py
  • Solução: URLs estão definidas diretamente no urls.py principal sem namespace
  • Resultado: Site carrega sem erros de URL

e_commerce/templates/base/navbar.html

<%load static %>
<%load custom_filters %>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-3">
  <div class="container">
    <!-- Logo -->
    <a class="navbar-brand" href="<%url 'home' %>">
      <img src="<%static 'img/logo.png' %>" width="30" height="30" class="d-inline-block align-top" alt="Logo">
      <%if nome_da_marca %>
        <{{ nome_da_marca }}>
      <%else %>
        Código Fluente eCommerce
      <%endif %>
    </a>

    <!-- Botão para dispositivos móveis -->
    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>

    <!-- Links do Navbar -->
    <div class="collapse navbar-collapse" id="navbarNav">
      <ul class="navbar-nav me-auto">
        <!-- Home -->
        <li class="nav-item <%if request.path == '/' %>active<%endif %>">
          <a class="nav-link" href="<%url 'home' %>">Home</a>
        </li>

        <!-- Contato -->
        <li class="nav-item <%if request.path == '/contact/' %>active<%endif %>">
          <a class="nav-link" href="<%url 'contact' %>">Contato</a>
        </li>

        <!-- Produtos -->
        <li class="nav-item <%if request.path|startswith:'/products/' %>active<%endif %>">
          <a class="nav-link" href="<%url 'products:list' %>">Produtos</a>
        </li>
        <!-- Autenticação -->
        <%if user.is_authenticated %>
          <!-- Logout -->
          <li class="nav-item">
            <a class="nav-link" href="<%url 'logout' %>?next=<{{ request.path }}>">Logout</a>
          </li>
        <%else %>
          <!-- Login -->
          <li class="nav-item <%if request.path == '/accounts/login/' %>active<%endif %>">
            <a class="nav-link" href="<%url 'login' %>?next=<{{ request.path }}>">Login</a>
          </li>

          <!-- Registrar -->
          <li class="nav-item <%if request.path == '/accounts/register/' %>active<%endif %>">
            <a class="nav-link" href="<%url 'register' %>">Registrar-se</a>
          </li>
        <%endif %>

        <!-- Carrinho -->
        <li class="nav-item <%if request.path == '/cart/' %>active<%endif %>">
          <a class="nav-link" href="<%url 'cart:home' %>">
            <span class="navbar-cart-count">
              <%with request.session.cart_items|default:0 as cart_items %>
                <{{ cart_items }}>
              <%endwith %>
            </span>
            <i class="fa fa-shopping-cart"></i>
          </a>
        </li>
      </ul>

      <!-- Formulário de Pesquisa -->
      <%include 'search/snippets/search-form.html' %>
    </div>
  </div>
</nav>

Explicação do Código – login – e_commerce/accounts/templates/accounts/login.html

Mudança Principal – Remoção de Namespace:

Login URL:
  • Antes: <form method="POST" action="{% url 'accounts:login' %}">
  • Depois: <form method="POST" action="{% url 'login' %}">
  • Correção: Remove namespace accounts: inexistente

e_commerce/accounts/templates/accounts/login.html

<%extends "base.html" %>
<%load custom_tags %>
<%block content %>
<div class="container mt-5">
  <!-- Toast para mensagens -->
  <%if messages %>
  <div class="toast-container position-fixed bottom-0 end-0 p-3">
    <%for message in messages %>
    <div class="toast show" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="true" data-bs-delay="3000">
      <%if 'success' in message.tags %>
      <div class="toast-header bg-success text-white">
      <%elif 'error' in message.tags %>
      <div class="toast-header bg-danger text-white">
      <%elif 'warning' in message.tags %>
      <div class="toast-header bg-warning text-dark">
      <%else %>
      <div class="toast-header bg-info text-white">
      <%endif %>
        <strong class="me-auto">Mensagem</strong>
        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
      </div>
      <div class="toast-body">
        <{{ message }}>
      </div>
    </div>
    <%endfor %>
  </div>
  <%endif %>

  <div class="row justify-content-center">
    <div class="col-md-6 col-12">
      <div class="card shadow-sm">
        <div class="card-body">
          <h2 class="text-center mb-4 text-primary">Login</h2>
          <form method="POST" action="<%url 'login' %>">
            <%csrf_token %>
            <div class="form-group mb-3">
              <label for="id_email" class="form-label fw-bold">Email</label>
              <{{ form.email|add_class:"form-control form-control-lg" }}>
              <small id="emailHelp" class="form-text text-muted">
                Nunca compartilharemos seu e-mail com mais ninguém.
              </small>
            </div>
            <div class="form-group mb-3">
              <label for="id_password" class="form-label fw-bold">Senha</label>
              <{{ form.password|add_class:"form-control form-control-lg" }}>
            </div>
            <div class="form-group form-check mb-3">
              <input type="checkbox" class="form-check-input" id="remember_me" name="remember_me" />
              <label class="form-check-label" for="remember_me">Lembrar senha</label>
            </div>
            <input type="hidden" name="next" value="<{{ request.GET.next }}>" />
            <button type="submit" class="btn btn-primary btn-lg w-100">Enviar</button>
          </form>
        </div>
      </div>
    </div>
  </div>
</div>
<%endblock %>

Explicação do Código – login – e_commerce/accounts/templates/accounts/register.html

Mudança Principal – Remoção de Namespace:

Login URL:
  • Antes: <form method="POST" action="{% url 'accounts:register' %}">
  • Depois: <form method="POST" action="{% url 'register' %}">
  • Correção: Remove namespace accounts: inexistente

e_commerce/accounts/templates/accounts/register.html

<%extends "base.html" %>
  <%load custom_tags %>
  <%block content %>
  <div class="container mt-5">
    <!-- Toast para mensagens -->
    <%if messages %>
    <div class="toast-container position-fixed bottom-0 end-0 p-3">
      <%for message in messages %>
      <div class="toast show" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="true" data-bs-delay="3000">
        <%if 'success' in message.tags %>
        <div class="toast-header bg-success text-white">
        <%elif 'error' in message.tags %>
        <div class="toast-header bg-danger text-white">
        <%elif 'warning' in message.tags %>
        <div class="toast-header bg-warning text-dark">
        <%else %>
        <div class="toast-header bg-info text-white">
        <%endif %>
          <strong class="me-auto">Mensagem</strong>
          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
        </div>
        <div class="toast-body">
          <{{ message }}>
        </div>
      </div>
      <%endfor %>
    </div>
    <%endif %>

    <!-- Formulário de registro -->
    <div class="row justify-content-center">
      <div class="col-md-6 col-12">
        <div class="card shadow-sm">
          <div class="card-body">
            <h2 class="text-center mb-4 text-primary">Registrar</h2>
            <form method="POST" action="<%url 'register' %>">
              <%csrf_token %>
              <div class="form-group mb-3">
                <label for="<{{ form.email.id_for_label }}>" class="form-label fw-bold">Email</label>
                <{{ form.email|add_class:"form-control form-control-lg" }}>
                <small id="emailHelp" class="form-text text-muted">
                  Nunca compartilharemos seu e-mail com mais ninguém.
                </small>
              </div>
              <div class="form-group mb-3">
                <label for="<{{ form.full_name.id_for_label }}>" class="form-label fw-bold">Nome Completo</label>
                <{{ form.full_name|add_class:"form-control form-control-lg" }}>
              </div>
              <div class="form-group mb-3">
                <label for="<{{ form.password.id_for_label }}>" class="form-label fw-bold">Senha</label>
                <{{ form.password|add_class:"form-control form-control-lg" }}>
              </div>
              <div class="form-group mb-3">
                <label for="<{{ form.password_2.id_for_label }}>" class="form-label fw-bold">Confirmar Senha</label>
                <{{ form.password_2|add_class:"form-control form-control-lg" }}>
              </div>
              <button type="submit" class="btn btn-primary btn-lg w-100">Enviar</button>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
  <%endblock %>

Explicação do Código cart_detail_api_view no – e_commerce/carts/views.py

Por que essa função é importante?

  • API JSON: Endpoint dedicado para retornar dados do carrinho em formato JSON
  • Integração Frontend: Permite que JavaScript acesse dados do carrinho de forma assíncrona
  • Compatibilidade Stripe: Dados estruturados no formato esperado para integração com pagamentos
  • Performance: Resposta otimizada apenas com dados essenciais

Funcionamento Linha por Linha:

1. Obtenção do Carrinho
  • cart_obj, new_obj = Cart.objects.new_or_get(request)
  • Recupera ou cria: Usa o manager personalizado para buscar carrinho da sessão
  • Segurança: Sempre retorna um carrinho válido, mesmo se não existir
2. Serialização dos Produtos
  • products = [{"id": x.id, "url": x.get_absolute_url(), "name": x.title, "price": x.price} for x in cart_obj.products.all()]
  • List Comprehension: Converte QuerySet Django em lista de dicionários Python
  • Dados Essenciais: Extrai apenas ID, URL, nome e preço de cada produto
  • URL Absoluta: get_absolute_url() fornece link completo para página do produto
3. Estrutura de Dados Completa
  • cart_data = {"products": products, "subtotal": cart_obj.subtotal, "total": cart_obj.total}
  • Dicionário Organizado: Estrutura clara com produtos e totais separados
  • Subtotal: Soma dos produtos sem taxas adicionais
  • Total: Valor final incluindo taxas e descontos
4. Resposta JSON
  • return JsonResponse(cart_data)
  • Content-Type Automático: Define header application/json automaticamente
  • Serialização: Converte dicionário Python em JSON válido
  • Pronto para Consumo: JavaScript pode usar diretamente com .json()

Exemplo de Resposta JSON:

{
    "products": [
        {
            "id": 1,
            "url": "/products/smartphone-samsung/",
            "name": "Smartphone Samsung",
            "price": "899.99"
        },
        {
            "id": 2,
            "url": "/products/fone-bluetooth/",
            "name": "Fone Bluetooth",
            "price": "129.90"
        }
    ],
    "subtotal": "1029.89",
    "total": "1853.80"
}

Casos de Uso:

  • AJAX Calls: JavaScript pode buscar dados do carrinho sem recarregar página
  • Checkout Stripe: Dados formatados para criar PaymentIntent
  • APIs Externas: Integração com sistemas de terceiros
  • Mobile Apps: Apps podem consumir dados do carrinho web

e_commerce/carts/views.py 

from django.http import JsonResponse, Http404
from django.shortcuts import render, redirect, get_object_or_404

from accounts.forms import LoginForm, GuestForm
from accounts.models import GuestEmail

from addresses.forms import AddressForm
from addresses.models import Address

from billing.models import BillingProfile
from orders.models import Order
from products.models import Product
from .models import Cart, CartProduct

def is_ajax(request):
    return request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'

def add_to_cart(request):
    if request.method == "POST":
        product_id = request.POST.get("product_id")
        quantity = request.POST.get("quantity", 1)

        # Validando a quantidade
        try:
            quantity = int(quantity)
        except ValueError:
            return JsonResponse({"success": False, "message": "Quantidade inválida."})

        # Verificando se o produto existe
        product = get_object_or_404(Product, id=product_id)

        # Obtendo ou criando o carrinho
        cart_obj, created = Cart.objects.new_or_get(request)

        # Adicionando ou atualizando o produto no carrinho
        cart_product, created = CartProduct.objects.get_or_create(cart=cart_obj, product=product)
        cart_product.quantity = cart_product.quantity + quantity if not created else quantity
        cart_product.save()

        # Atualizando o total do carrinho
        cart_obj.update_totals()

        # Atualizando a sessão
        request.session["cart_id"] = cart_obj.id
        request.session["cart_items"] = cart_obj.cartproduct_set.count()

        return JsonResponse({"success": True, "cart_items": cart_obj.cartproduct_set.count()})
    return JsonResponse({"success": False, "message": "Método não permitido."})


def cart_home(request):
    cart_obj, new_obj = Cart.objects.new_or_get(request)
    return render(request, "carts/home.html", {"cart": cart_obj})
from django.http import JsonResponse

def cart_get_items(request):
    cart_obj, new_obj = Cart.objects.new_or_get(request)
    items = []
    for product in cart_obj.products.all():
        item = {
            'id': product.id,
            'name': product.title,
            'quantity': 1,  # Aqui você deveria usar a quantidade real do produto no carrinho
            'price': str(product.price),
        }
        items.append(item)
    return JsonResponse({'items': items})
def cart_update(request):
    cart_obj, new_obj = Cart.objects.new_or_get(request)
    product_id = request.POST.get("product_id")
    quantity = request.POST.get("quantity", 1)

    try:
        quantity = int(quantity)
    except ValueError:
        quantity = 1

    product_obj = get_object_or_404(Product, id=product_id)

    cart_product, created = CartProduct.objects.get_or_create(cart=cart_obj, product=product_obj)
    if quantity > 0:
        cart_product.quantity = quantity
        cart_product.save()
    else:
        cart_product.delete()

    cart_obj.update_totals()

    request.session['cart_items'] = sum(item.quantity for item in cart_obj.cartproduct_set.all())

    return JsonResponse({
        "success": True,
        "cartItemCount": request.session['cart_items'],
        "subtotal": f"{cart_obj.subtotal:.2f}",  # Subtotal formatado como string
        "total": f"{cart_obj.total:.2f}",        # Total formatado como string
    })

def checkout_home(request):
    cart_obj, cart_created = Cart.objects.new_or_get(request)
    order_obj = None
    if cart_created or cart_obj.products.count() == 0:
        return redirect("cart:home")

    login_form = LoginForm()
    guest_form = GuestForm()
    address_form = AddressForm()
    billing_address_id = request.session.get("billing_address_id", None)
    shipping_address_id = request.session.get("shipping_address_id", None)
    billing_profile, billing_profile_created = BillingProfile.objects.new_or_get(request)
    address_qs = None
    
    if billing_profile is not None:
        order_obj, order_obj_created = Order.objects.new_or_get(billing_profile, cart_obj)
        if request.user.is_authenticated:
            address_qs = Address.objects.filter(billing_profile=billing_profile)

    context = {
        "object": order_obj,
        "billing_profile": billing_profile,
        "login_form": login_form,
        "guest_form": guest_form,
        "address_form": address_form,
        "address_qs": address_qs,
    }
    return render(request, "carts/checkout.html", context)

def checkout_done_view(request):
    return render(request, "carts/checkout-done.html", {})

def cart_detail_api_view(request):
    cart_obj, new_obj = Cart.objects.new_or_get(request)
    products = [{
        "id": x.id,
        "url": x.get_absolute_url(), 
        "name": x.title, 
        "price": x.price
        } for x in cart_obj.products.all()]
    cart_data = {"products": products, "subtotal": cart_obj.subtotal, "total": cart_obj.total}
    return JsonResponse(cart_data)

Criando o Modelo Card

Explicação do Código – e_commerce/billing/models.py

Por que criamos o Modelo Card?

  • Segurança em primeiro lugar: Nunca armazenamos dados sensíveis como número completo do cartão ou CVV no nosso banco. Apenas guardamos o ID de referência do Stripe e informações básicas.
  • Experiência do usuário: Permite que clientes salvem seus cartões para compras futuras, agilizando o processo de checkout.
  • Compliance PCI: Seguindo as melhores práticas de segurança, delegamos o armazenamento seguro dos dados do cartão para o Stripe.

Componentes Implementados:

1. Configuração Otimizada do Stripe
  • Importação Simplificada: from django.conf import settings substitui importações complexas de environ
  • Configuração Centralizada: stripe.api_key = settings.STRIPE_API_KEY usa configuração já processada pelo settings.py
  • Eliminação de Duplicatas: Remove leitura duplicada do arquivo .env que já é feita pelo Django
  • Melhor Performance: Evita processamento desnecessário de variáveis de ambiente
2. Modelo Card
  • Campos não-sensíveis: Armazenamos apenas stripe_card_id (referência), marca do cartão, últimos 4 dígitos e data de expiração
  • Relacionamento seguro: Vinculado ao BillingProfile através de ForeignKey com related_name=’cards’
  • Controle de estado: Campos ‘active’ e ‘default’ para gerenciar cartões ativos e padrão
  • Auditoria: Timestamps para rastrear quando cartões foram criados e atualizados
3. CardManager Personalizado
  • add_card_from_stripe_response(): Método inteligente que evita duplicatas e automaticamente define o primeiro cartão como padrão
  • Lógica de negócio centralizada: Todas as operações de criação de cartão passam pelo manager, garantindo consistência
  • Validação automática: Verifica se o cartão já existe antes de criar um novo
  • Integração com Stripe: Processa PaymentMethod do Stripe extraindo dados relevantes automaticamente
4. Métodos de Instância Úteis
  • get_display_name(): Retorna uma representação amigável para exibição na interface (“Visa terminado em 1234”)
  • is_expired(): Verifica automaticamente se o cartão está vencido comparando com a data atual
  • set_as_default(): Gerencia a lógica de cartão padrão, removendo o status de outros cartões
  • Métodos defensivos: Tratam casos extremos e mantêm consistência dos dados
5. Sistema de Cartão Padrão
  • Apenas um padrão por usuário: Signal pre_save garante que só um cartão seja marcado como padrão por billing_profile
  • Definição automática: Primeiro cartão salvo automaticamente vira padrão
  • Facilita o checkout: Interface pode pré-selecionar o cartão padrão para agilizar pagamentos
  • Consistência garantida: Signal intercepta mudanças e mantém integridade do sistema
6. Configurações de Meta
  • Ordenação inteligente: Cartões padrão aparecem primeiro, depois por data mais recente
  • Nomes amigáveis: verbose_name para melhor experiência no Django Admin
  • Representação clara: __str__ método mostra marca e últimos dígitos
  • Interface administrativa: Meta class otimizada para gerenciamento no Django Admin

Melhorias na Configuração (Mudanças Implementadas):

  • Código Limpo: Removidas importações desnecessárias (environ, os)
  • DRY Principle: Elimina duplicação de leitura do arquivo .env
  • Django Way: Usa padrão Django de acessar configurações via settings
  • Manutenibilidade: Configuração centralizada facilita alterações futuras
  • Debug Simplificado: Remove logs confusos de múltiplas leituras do .env

Benefícios da Implementação:

  • Segurança: Dados sensíveis permanecem no Stripe, reduzindo riscos de segurança
  • Performance: Checkout mais rápido para usuários recorrentes e inicialização otimizada
  • Escalabilidade: Estrutura preparada para múltiplos cartões por usuário
  • Manutenibilidade: Código organizado com responsabilidades bem definidas
  • Experiência do usuário: Interface mais fluida e profissional
  • Configuração robusta: Sistema de configuração simplificado e confiável

e_commerce/billing/models.py


import stripe
from django.db import models
from django.db.models.signals import post_save, pre_save
from django.conf import settings
from accounts.models import GuestEmail

User = settings.AUTH_USER_MODEL
# fulano@mail.com -> pode ter 1.000.000.000 billing profiles
# user fulano@mail.com -> pode ter apenas 1 billing profile
# Initialise environment variables
stripe.api_key = settings.STRIPE_API_KEY

class BillingProfileManager(models.Manager):
    def new_or_get(self, request):
        user = request.user
        guest_email_id = request.session.get('guest_email_id')
        created = False
        obj = None
        if user.is_authenticated:
            'logged in user checkout; remember payment stuff'
            obj, created = self.model.objects.get_or_create(
                            user=user, email=user.email)
        elif guest_email_id is not None:
            'guest user checkout; auto reloads payment stuff'
            guest_email_obj = GuestEmail.objects.get(id=guest_email_id)
            obj, created = self.model.objects.get_or_create(
                                            email=guest_email_obj.email)
        else:
            pass
        return obj, created

class BillingProfile(models.Model):
    user = models.OneToOneField(User, null = True, blank = True, on_delete = models.CASCADE)
    email = models.EmailField()
    active =    models.BooleanField(default = True)
    update = models.DateTimeField(auto_now = True)
    timestamp   = models.DateTimeField(auto_now_add = True)
    customer_id = models.CharField(max_length = 120, null = True, blank = True)
    # customer_id no Stripe ou Braintree ou ...
    objects = BillingProfileManager()
    
    def __str__(self):
        return self.email

def billing_profile_created_receiver(sender, instance, *args, **kwargs):
    if not instance.customer_id and instance.email:
        print("ACTUAL API REQUEST Send to stripe/braintree")
        customer = stripe.Customer.create(
                email = instance.email
            )
        print(customer)
        instance.customer_id = customer.id

pre_save.connect(billing_profile_created_receiver, sender=BillingProfile)

def user_created_receiver(sender, instance, created, *args, **kwargs):
    if created and instance.email:
        BillingProfile.objects.get_or_create(user = instance, email = instance.email)

post_save.connect(user_created_receiver, sender = User)

# ==================== CÓDIGO NOVO - MODELO CARD ====================

class CardManager(models.Manager):
    def add_card_from_stripe_response(self, billing_profile, stripe_payment_method):
        """
        Cria ou atualiza um cartão com base na resposta do Stripe PaymentMethod.
        """
        card_data = stripe_payment_method.card
        
        # Verifica se o cartão já existe
        existing_card = self.filter(
            billing_profile=billing_profile,
            stripe_card_id=stripe_payment_method.id
        ).first()
        
        if existing_card:
            return existing_card, False
        
        # Cria novo cartão
        card = self.create(
            billing_profile=billing_profile,
            stripe_card_id=stripe_payment_method.id,
            brand=card_data.brand.title(),
            last_four_digits=card_data.last4,
            exp_month=card_data.exp_month,
            exp_year=card_data.exp_year,
            default=False
        )
        
        # Se é o primeiro cartão, define como padrão
        if not billing_profile.cards.filter(default=True).exists():
            card.default = True
            card.save()
        
        return card, True

class Card(models.Model):
    billing_profile = models.ForeignKey(
        'BillingProfile', 
        on_delete=models.CASCADE,
        related_name='cards'
    )
    stripe_card_id = models.CharField(
        max_length=120, 
        unique=True,
        help_text="ID do PaymentMethod no Stripe"
    )
    brand = models.CharField(
        max_length=120,
        help_text="Marca do cartão (Visa, Mastercard, etc.)"
    )
    last_four_digits = models.CharField(
        max_length=4,
        help_text="Últimos 4 dígitos do cartão"
    )
    exp_month = models.IntegerField(
        help_text="Mês de expiração"
    )
    exp_year = models.IntegerField(
        help_text="Ano de expiração"
    )
    default = models.BooleanField(
        default=False,
        help_text="Cartão padrão para pagamentos"
    )
    active = models.BooleanField(default=True)
    timestamp = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    
    objects = CardManager()
    
    class Meta:
        ordering = ['-default', '-timestamp']
        verbose_name = "Cartão Salvo"
        verbose_name_plural = "Cartões Salvos"
    
    def __str__(self):
        return f"{self.brand} ****{self.last_four_digits}"
    
    def get_display_name(self):
        """Retorna nome amigável para exibição"""
        return f"{self.brand} terminado em {self.last_four_digits}"
    
    def is_expired(self):
        """Verifica se o cartão está expirado"""
        from datetime import datetime
        now = datetime.now()
        return (self.exp_year < now.year or 
                (self.exp_year == now.year and self.exp_month < now.month))
    
    def set_as_default(self):
        """Define este cartão como padrão"""
        # Remove default de outros cartões
        Card.objects.filter(
            billing_profile=self.billing_profile,
            default=True
        ).update(default=False)
        
        # Define este como padrão
        self.default = True
        self.save()

def card_pre_save_receiver(sender, instance, *args, **kwargs):
    """Garante que apenas um cartão seja padrão por billing_profile"""
    if instance.default:
        Card.objects.filter(
            billing_profile=instance.billing_profile,
            default=True
        ).exclude(id=instance.id).update(default=False)

pre_save.connect(card_pre_save_receiver, sender=Card)

Explicação do Código – e_commerce/billing/admin.py

Por que configuramos o Django Admin para cartões?

  • Gestão centralizada: Permite aos administradores monitorar e gerenciar todos os cartões salvos dos usuários em uma interface única.
  • Suporte ao cliente: Facilita o atendimento quando usuários relatam problemas com cartões salvos.
  • Auditoria e compliance: Visibilidade completa das operações de cartão para conformidade e auditoria.

Configurações Implementadas:

1. CardAdmin
  • Interface administrativa completa: Configuração personalizada do Django Admin especificamente para o modelo Card, substituindo a interface padrão básica.
  • Decorator @admin.register(Card): Registra automaticamente a classe CardAdmin como interface administrativa para o modelo Card.
  • Herança de ModelAdmin: Aproveita todas as funcionalidades built-in do Django Admin com customizações específicas.
2. list_display
  • Visão consolidada: Define quais campos aparecem como colunas na listagem principal: billing_profile, brand, last_four_digits, exp_month, exp_year, default, active, timestamp.
  • Informações essenciais: Mostra dados críticos como usuário proprietário, identificação do cartão e status em uma única tela.
  • Ordenação clicável: Cada coluna permite ordenação ascendente/descendente para organização rápida dos dados.
3. list_filter
  • Filtros laterais dinâmicos: Painel lateral com filtros por brand, default, active e exp_year para refinar resultados rapidamente.
  • Navegação eficiente: Permite encontrar cartões específicos (ex: “todos os cartões Visa ativos” ou “cartões que expiram em 2025”).
  • Análise rápida: Facilita identificação de padrões como cartões expirados ou marcas mais utilizadas.
4. search_fields
  • Busca inteligente: Campo de pesquisa que procura em billing_profile__email, brand e last_four_digits simultaneamente.
  • Relacionamento cross-model: billing_profile__email permite buscar cartões pelo email do usuário sem navegar entre modelos.
  • Localização rápida: Admins podem encontrar cartões específicos digitando email, marca ou últimos dígitos.
5. Campos de Segurança
  • readonly_fields: stripe_card_id, timestamp e updated protegidos contra edição acidental, mantendo integridade dos dados.
  • Performance otimizada: get_queryset com select_related(‘billing_profile’) reduz queries ao banco ao listar cartões.
  • Auditoria preservada: Timestamps imutáveis garantem rastreabilidade completa das operações.
6. Melhorias Adicionais
  • Métodos display personalizados: get_email() e is_expired_display() para melhor visualização de dados relacionados.
  • Ações em massa: make_active/make_inactive permitem ativar/desativar múltiplos cartões simultaneamente.
  • Indicadores visuais: Emojis e cores para identificação rápida de status de cartões (válido/expirado).

Benefícios para Administração:

  • Produtividade: Interface otimizada reduz tempo de navegação e localização de informações
  • Visibilidade: Dados críticos organizados e acessíveis em poucos cliques
  • Segurança: Controles adequados protegem dados sensíveis enquanto permitem operações necessárias
  • Suporte eficiente: Ferramentas para resolver rapidamente questões relacionadas a cartões dos usuários

e_commerce/billing/admin.py


from django.contrib import admin
from .models import BillingProfile, Card

@admin.register(Card)
class CardAdmin(admin.ModelAdmin):
    list_display = [
        'billing_profile',
        'get_email',
       'brand', 
       'last_four_digits',
       'exp_month', 
       'exp_year',
       'is_expired_display',
       'default',
       'active',
       'timestamp'
    ]
    list_filter = ['brand', 'default', 'active', 'exp_year']
    search_fields = [
        'billing_profile__email', 'brand', 'last_four_digits'
    ]
    readonly_fields = ['stripe_card_id', 'timestamp', 'updated']
    
    def get_queryset(self, request):
        return super().get_queryset(request).select_related('billing_profile')
    
    def get_email(self, obj):
        return obj.billing_profile.email
    get_email.short_description = 'Email'
    get_email.admin_order_field = 'billing_profile__email'
    
    def is_expired_display(self, obj):
        if obj.is_expired():
            return "⚠️ Expirado"
        return "✅ Válido"
    is_expired_display.short_description = 'Status'
    
    # Ações em massa
    def make_active(self, request, queryset):
        queryset.update(active=True)
    make_active.short_description = "Marcar cartões selecionados como ativos"
    
    def make_inactive(self, request, queryset):
        queryset.update(active=False)
    make_inactive.short_description = "Marcar cartões selecionados como inativos"
    
    actions = [make_active, make_inactive]

admin.site.register(BillingProfile) 

Explicação do Código – e_commerce/billing/views.py

Novas Funcionalidades Implementadas (Partes em Azul):

1. Novas Importações
  • from billing.models import BillingProfile, Card: Adiciona importação do modelo Card para gerenciar cartões salvos
  • from orders.models import Order: Para processar pedidos após pagamento
  • from django.contrib import messages: Sistema de mensagens para feedback ao usuário
  • import json: Para parsear dados JSON recebidos do frontend
2. payment_success_view – Processamento do Pedido
  • Recuperação do Carrinho: Busca carrinho da sessão e pedido relacionado
  • Marcação de Pagamento: Chama order_obj.mark_paid() para finalizar o pedido
  • Limpeza da Sessão: Zera contador de itens e remove ID do carrinho
3. payment_method_view – Exibição de Cartões Salvos
  • Validação Completa: Verificações de carrinho, pedido e sessão válidos
  • Busca de Cartões: Recupera cartões ativos ordenados por padrão e data
  • Contexto Enriquecido: Adiciona saved_cards e billing_profile
  • Tratamento de Erros: Try/catch com redirecionamento seguro
4. create_checkout_session – NOVA FUNÇÃO
  • Integração com Carrinho: Calcula total + frete automaticamente
  • PaymentIntent: Cria intent com métodos automáticos habilitados
  • Moeda Brasileira: Configurado para BRL (Real)
  • Conversão para Centavos: Multiplica por 100 conforme padrão Stripe
5. save_payment_method – NOVA FUNÇÃO
  • Salvamento Pós-Pagamento: Salva cartão após confirmação bem-sucedida
  • Validação PaymentIntent: Verifica status ‘succeeded’ antes de salvar
  • Prevenção de Duplicatas: Usa add_card_from_stripe_response
  • Resposta JSON: Retorna status e informações do cartão salvo
6. pay_with_saved_card – NOVA FUNÇÃO
  • Pagamento com Cartão Salvo: Processa transações usando cartões já salvos
  • Validação de Propriedade: Confirma que cartão pertence ao usuário atual
  • PaymentIntent Direto: Cria intent usando payment_method salvo e confirm=True
  • Processamento Automático: Finaliza pedido e limpa sessão se pagamento for bem-sucedido
7. set_default_card – NOVA FUNÇÃO
  • Definição de Padrão: Permite marcar um cartão como padrão
  • Validação de Propriedade: Confirma que cartão pertence ao usuário
  • Feedback com Messages: Mensagens de sucesso/erro
  • Método Inteligente: Usa card.set_as_default()
8. delete_card – NOVA FUNÇÃO
  • Remoção Dupla: Remove tanto do Stripe quanto do banco local
  • Tratamento de Falhas: Continua remoção local mesmo se falhar no Stripe
  • Validação de Segurança: Verifica propriedade antes de excluir
  • Feedback ao Usuário: Mensagem confirmando remoção com nome do cartão

Fluxos de Pagamento Implementados:

  • Pagamento Novo: create_checkout_sessionsave_payment_method → sucesso
  • Pagamento com Cartão Salvo: pay_with_saved_card → processamento direto → sucesso
  • Gerenciamento: set_default_card e delete_card para manutenção

Benefícios da Implementação Completa:

  • Experiência Completa: Usuários podem salvar, reutilizar e gerenciar cartões
  • Performance Otimizada: Pagamentos com cartões salvos são mais rápidos
  • Segurança Robusta: Validações em todas as operações críticas
  • Tratamento de Erros: Falhas graciosamente tratadas com feedback ao usuário
  • Integração Stripe: Comunicação bidirecional completa com API Stripe

e_commerce/billing/views.py


from decimal import Decimal
from django.shortcuts import render, redirect
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
from billing.models import BillingProfile, Card
from orders.models import Order
from products.models import Product
from carts.models import Cart
from django.conf import settings
import stripe
from django.contrib import messages
import json


stripe.api_key = settings.STRIPE_API_KEY

# View para renderizar a página de sucesso do pagamento
def payment_success_view(request):
    cart_id = request.session.get("cart_id")
    if cart_id:
        cart_obj = Cart.objects.get(id=cart_id)
        order_obj = Order.objects.filter(cart=cart_obj).first()
        if order_obj:
            order_obj.mark_paid()
            request.session['cart_items'] = 0
            del request.session['cart_id']
    return render(request, 'billing/payment-success.html')

# View para renderizar a página de falha do pagamento
def payment_failed_view(request):
    return render(request, 'billing/payment-failed.html')

def payment_method_view(request):
    try:
        cart_id = request.session.get("cart_id")
        if not cart_id:
            return redirect("cart:home")

        cart_obj = Cart.objects.filter(id=cart_id).first()
        if not cart_obj:
            return redirect("cart:home")

        order_obj = Order.objects.filter(cart=cart_obj).first()
        if not order_obj:
            return redirect("cart:home")

        # Busca cartões salvos do usuário
        billing_profile, _ = BillingProfile.objects.new_or_get(request)
        saved_cards = []
        if billing_profile:
            saved_cards = Card.objects.filter(
                billing_profile=billing_profile,
                active=True
            ).order_by('-default', '-timestamp')

        context = {
            "publish_key": settings.STRIPE_PUB_KEY,
            "order": order_obj,
            "saved_cards": saved_cards,
            "billing_profile": billing_profile,
        }
        return render(request, "billing/payment-method.html", context)

    except Exception as e:
        print(f"Erro: {str(e)}")
        return redirect("cart:home")

@csrf_exempt
@require_POST
def create_checkout_session(request):
    try:
        cart_id = request.session.get("cart_id")
        cart_obj = Cart.objects.get(id=cart_id)
        order_obj = Order.objects.filter(cart=cart_obj).first()
        total = cart_obj.total + order_obj.shipping_total

        intent = stripe.PaymentIntent.create(
            amount=int(total * 100),
            currency='brl',
            automatic_payment_methods={'enabled': True},
        )

        return JsonResponse({
            'clientSecret': intent.client_secret
        })
    except Exception as e:
        print(f"Stripe Error: {str(e)}")
        return JsonResponse({'error': str(e)}, status=400)

@csrf_exempt
@require_POST  
def save_payment_method(request):
    """
    Salva o método de pagamento após confirmação bem-sucedida
    """
    try:
        data = json.loads(request.body)
        payment_intent_id = data.get('payment_intent_id')
        
        if not payment_intent_id:
            return JsonResponse({'error': 'PaymentIntent ID required'}, status=400)
        
        # Recupera o PaymentIntent do Stripe
        payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id)
        
        if payment_intent.status != 'succeeded':
            return JsonResponse({'error': 'Payment not successful'}, status=400)
        
        # Busca o billing profile
        billing_profile, _ = BillingProfile.objects.new_or_get(request)
        if not billing_profile:
            return JsonResponse({'error': 'Billing profile not found'}, status=400)
        
        # Recupera o PaymentMethod
        payment_method_id = payment_intent.payment_method
        payment_method = stripe.PaymentMethod.retrieve(payment_method_id)
        
        # Salva o cartão
        card, created = Card.objects.add_card_from_stripe_response(
            billing_profile, payment_method
        )
        
        return JsonResponse({
            'success': True,
            'card_saved': created,
            'card_id': card.id,
            'message': 'Cartão salvo com sucesso!' if created else 'Cartão já estava salvo.'
        })
        
    except Exception as e:
        print(f"Erro ao salvar cartão: {str(e)}")
        return JsonResponse({'error': 'Erro interno'}, status=500)

@require_POST
def set_default_card(request, card_id):
    """
    Define um cartão como padrão
    """
    try:
        billing_profile, _ = BillingProfile.objects.new_or_get(request)
        if not billing_profile:
            messages.error(request, 'Perfil de cobrança não encontrado.')
            return redirect('billing-payment-method')
        
        card = get_object_or_404(
            Card, 
            id=card_id, 
            billing_profile=billing_profile,
            active=True
        )
        
        card.set_as_default()
        messages.success(request, f'Cartão {card.get_display_name()} definido como padrão.')
        
    except Exception as e:
        messages.error(request, 'Erro ao definir cartão como padrão.')
    
    return redirect('billing-payment-method')

@require_POST
def delete_card(request, card_id):
    """
    Remove um cartão salvo
    """
    try:
        billing_profile, _ = BillingProfile.objects.new_or_get(request)
        if not billing_profile:
            messages.error(request, 'Perfil de cobrança não encontrado.')
            return redirect('billing-payment-method')
        
        card = get_object_or_404(
            Card, 
            id=card_id, 
            billing_profile=billing_profile
        )
        
        # Remove do Stripe se possível
        try:
            stripe.PaymentMethod.detach(card.stripe_card_id)
        except stripe.error.StripeError as e:
            print(f"Erro ao remover do Stripe: {e}")
        
        card_name = card.get_display_name()
        card.delete()
        
        messages.success(request, f'Cartão {card_name} removido com sucesso.')
        
    except Exception as e:
        messages.error(request, 'Erro ao remover cartão.')
    
    return redirect('billing-payment-method')

@csrf_exempt
@require_POST
def pay_with_saved_card(request):
    """
    Processa pagamento usando cartão salvo
    """
    try:
        data = json.loads(request.body)
        card_id = data.get('card_id')
        
        if not card_id:
            return JsonResponse({'error': 'Card ID required'}, status=400)
        
        # Busca billing profile e cartão
        billing_profile, _ = BillingProfile.objects.new_or_get(request)
        card = get_object_or_404(Card, stripe_card_id=card_id, billing_profile=billing_profile)
        
        # Busca pedido atual
        cart_id = request.session.get("cart_id")
        cart_obj = Cart.objects.get(id=cart_id)
        order_obj = Order.objects.filter(cart=cart_obj).first()
        total = cart_obj.total + order_obj.shipping_total
        
        # Cria PaymentIntent com cartão salvo
        intent = stripe.PaymentIntent.create(
            amount=int(total * 100),
            currency='brl',
            payment_method=card.stripe_card_id,
            confirm=True,
            return_url=request.build_absolute_uri('/billing/payment-success/')
        )
        
        if intent.status == 'succeeded':
            order_obj.mark_paid()
            request.session['cart_items'] = 0
            del request.session['cart_id']
            return JsonResponse({'success': True})
        else:
            return JsonResponse({'error': 'Payment failed'}, status=400)
            
    except Exception as e:
        print(f"Erro ao processar pagamento: {str(e)}")
        return JsonResponse({'error': 'Erro interno'}, status=500)

@csrf_exempt
@require_POST
def create_payment_intent(request):
    data = json.loads(request.body)
    try:
        # Calcular o valor com base nos itens enviados
        # Substitua esta função pela sua lógica de cálculo de preços
        amount = calculate_order_amount(data['items'])

        intent = stripe.PaymentIntent.create(
            amount=amount,
            currency='usd',
            payment_method_types=['card'],
        )
        return JsonResponse({'clientSecret': intent.client_secret})
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=400)

def calculate_order_amount(items):
    cart = Cart.objects.first()  # Ou use um método que pegue o carrinho correto
    total_amount = 0
    for item in items:
        product = Product.objects.get(id=item['id'])
        total_amount += product.price * item['quantity']
    
    # Reutilizar a taxa definida no modelo Cart
    return int(total_amount * Decimal(cart.total / cart.subtotal) * 100)

Explicação do Código – e_commerce/static_local/js/payment-method.js

Funcionalidades Implementadas (Destaques das Modificações):

1. Inicialização e Validação de Dependências
  • Evento DOM Ready: document.addEventListener('DOMContentLoaded') garante carregamento completo
  • Logs de Debug: console.log('=== INICIANDO PAYMENT METHOD JS ===')
  • Validação Stripe Key: Verifica getElementById('stripe-key') e dataset.publishKey
  • Early Return: Para execução se dependências não existirem
2. Configuração do Stripe e Elements
  • Instância Stripe: const stripe = Stripe(publishKey)
  • Variável Elements: let elements para armazenar instância
  • Chamadas de Inicialização: initialize() e checkStatus()
  • Máscara de Segurança: publishKey.substring(0, 10) + '...' para logs
3. Event Listeners para Interações
  • Formulário Principal: addEventListener("submit", handleSubmit)
  • Botões de Cartão Salvo: querySelectorAll('.use-card-btn')
  • Formulários de Ação: querySelectorAll('.set-default-form') e '.delete-card-form'
  • Confirmação de Remoção: confirm(`Tem certeza que deseja remover...`)
4. 🆕 Função de Verificação do Checkbox (Nova Implementação)
  • ID Atualizado: getElementById('save-card-checkbox') (mudança do ID original)
  • Verificação Segura: checkbox ? checkbox.checked : false
  • Log de Debug: console.log('🔍 Verificando se deve salvar cartão:', result)
  • Retorno Boolean: Sempre retorna true/false para garantir consistência
5. Processamento de Cartões Salvos
  • Função Assíncrona: async function useSavedCard(cardId, cardName, button)
  • Confirmação do Usuário: confirm(`Usar o cartão ${cardName}...`)
  • Loading State: setButtonLoading(button, true)
  • Requisição AJAX: fetch('/billing/pay-with-saved-card/')
6. Inicialização Stripe Elements com Error Handling
  • Logs Detalhados: console.log('=== INICIALIZANDO STRIPE ELEMENTS ===')
  • Requisição Assíncrona: fetch("/billing/create-checkout-session/")
  • Validação de Response: if (!response.ok) throw new Error
  • Mounting Elements: paymentElement.mount("#payment-element")
7. 🆕 Processamento de Pagamento com Checkbox Integrado (Modificado)
  • Prevenção de Default: e.preventDefault()
  • Loading State Global: setLoading(true)
  • Confirmação Stripe: stripe.confirmPayment({ elements, confirmParams })
  • Verificação do Checkbox: if (shouldSaveCard()) usando nova função
8. 🆕 Integração com Salvamento de Cartão (Atualizado)
  • Condição de Salvamento: if (shouldSaveCard()) verifica o checkbox corrigido
  • Log Condicional: console.log('💾 Salvando cartão...') vs '⏭️ Não salvando cartão'
  • Chamada Assíncrona: await savePaymentMethod(paymentIntent.id)
  • Feedback Visual: showMessage("Cartão salvo com sucesso!", false)
9. Função de Salvamento de Método de Pagamento
  • Requisição POST: fetch('/billing/save-payment-method/')
  • Payload JSON: { payment_intent_id: paymentIntentId }
  • Headers Completos: Content-Type e X-CSRFToken
  • Validação de Resposta: if (data.success && data.card_saved)
10. Utilitários e Helpers
  • Cookie CSRF: function getCookie(name) com parsing manual
  • Status Check: checkStatus() para URL parameters
  • Message Display: showMessage(messageText, isError) com timeout
  • Loading Controls: setLoading() e setButtonLoading()
11. Sistema de Loading States
  • Botão Principal: submitButton.disabled = true e spinner toggle
  • Texto Dinâmico: buttonText.textContent = "Processando..."
  • Restauração de Estado: dataset.originalText para voltar ao texto original
  • Loading Individual: Spinners específicos para cada botão de cartão
12. Error Handling e Logs
  • Try-Catch Blocks: Em todas as funções assíncronas
  • Console Logs: Diferenciados com emojis (✅❌🔍💾⏭️)
  • Error Messages: showMessage(error.message, true)
  • Status Logging: console.log('Response status:', response.status)

🆕 Principais Modificações Implementadas:

  • ID do Checkbox Atualizado: Mudança de 'save-card' para 'save-card-checkbox'
  • Função shouldSaveCard(): Nova implementação para compatibilidade com checkbox corrigido
  • Logs Melhorados: Sistema de debugging mais detalhado com emojis
  • Integração Robusta: Verificação do checkbox independente do Stripe Elements

Benefícios das Modificações:

  • Compatibilidade Total: Funciona com o checkbox corrigido fora da área Stripe
  • Debug Facilitado: Logs claros para identificar problemas rapidamente
  • UX Seamless: Transições suaves entre estados de loading
  • Error Recovery: Tratamento robusto de erros com feedback ao usuário
  • Código Maintível: Funções bem separadas e documentadas com logs
  • Performance: Event listeners eficientes e operações assíncronas otimizadas

e_commerce/static_local/js/payment-method.js


// e_commerce/static_local/js/payment-method.js

document.addEventListener('DOMContentLoaded', function() {
  console.log('=== INICIANDO PAYMENT METHOD JS ===');
  
  const stripeKeyElement = document.getElementById('stripe-key');
  if (!stripeKeyElement) {
    console.error('Elemento stripe-key não encontrado');
    return;
  }
  
  const publishKey = stripeKeyElement.dataset.publishKey;
  if (!publishKey) {
    console.error('Chave pública do Stripe não encontrada');
    return;
  }

  console.log('Chave Stripe encontrada:', publishKey.substring(0, 10) + '...');

  const stripe = Stripe(publishKey);
  let elements;

  // Inicialização
  initialize();
  checkStatus();

  // Event listeners
  const paymentForm = document.querySelector("#payment-form");
  if (paymentForm) {
    paymentForm.addEventListener("submit", handleSubmit);
  }

  // Event listeners para botões de cartões salvos
  document.querySelectorAll('.use-card-btn').forEach(btn => {
    btn.addEventListener('click', function() {
      const cardId = this.dataset.cardId;
      const cardName = this.dataset.cardName;
      useSavedCard(cardId, cardName, this);
    });
  });

  // Event listeners para formulários de ação
  document.querySelectorAll('.set-default-form').forEach(form => {
    form.addEventListener('submit', function(e) {
      const btn = this.querySelector('button[type="submit"]');
      setButtonLoading(btn, true);
    });
  });

  document.querySelectorAll('.delete-card-form').forEach(form => {
    form.addEventListener('submit', function(e) {
      const cardName = this.querySelector('button').dataset.cardName;
      if (!confirm(`Tem certeza que deseja remover o cartão ${cardName}?`)) {
        e.preventDefault();
        return;
      }
      const btn = this.querySelector('button[type="submit"]');
      setButtonLoading(btn, true);
    });
  });

  // Função para verificar se deve salvar o cartão
  function shouldSaveCard() {
    const checkbox = document.getElementById('save-card-checkbox');
    const result = checkbox ? checkbox.checked : false;
    console.log('🔍 Verificando se deve salvar cartão:', result);
    return result;
  }

  // Função para usar cartão salvo
  async function useSavedCard(cardId, cardName, button) {
    if (confirm(`Usar o cartão ${cardName} para este pagamento?`)) {
      setButtonLoading(button, true);
      try {
        const response = await fetch('/billing/pay-with-saved-card/', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': getCookie('csrftoken'),
          },
          body: JSON.stringify({
            card_id: cardId
          })
        });
        
        const data = await response.json();
        if (data.success) {
          showMessage("Pagamento processado com sucesso!", false);
          setTimeout(() => {
            window.location.href = "/billing/payment-success/";
          }, 1500);
        } else {
          showMessage(data.error || "Erro ao processar pagamento", true);
          setButtonLoading(button, false);
        }
      } catch (error) {
        console.error('Erro:', error);
        showMessage("Erro inesperado. Tente novamente.", true);
        setButtonLoading(button, false);
      }
    }
  }

  // Inicializar Stripe Elements
  async function initialize() {
    console.log('=== INICIALIZANDO STRIPE ELEMENTS ===');
    try {
      console.log('Fazendo requisição para create-checkout-session...');
      
      const response = await fetch("/billing/create-checkout-session/", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-CSRFToken": getCookie('csrftoken'),
        },
      });
      
      console.log('Response status:', response.status);
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const responseData = await response.json();
      console.log('Response data:', responseData);
      
      if (!responseData.clientSecret) {
        throw new Error('Client secret não recebido');
      }

      const { clientSecret } = responseData;
      
      console.log('Client secret recebido:', clientSecret.substring(0, 20) + '...');

      elements = stripe.elements({ clientSecret });
      const paymentElement = elements.create("payment");
      paymentElement.mount("#payment-element");
      
      console.log('Stripe Elements montado com sucesso');
      
    } catch (error) {
      console.error('Erro ao inicializar:', error);
      showMessage(`Erro ao carregar formulário de pagamento: ${error.message}`, true);
    }
  }

  // Processar submissão do formulário
  async function handleSubmit(e) {
    e.preventDefault();
    console.log('=== PROCESSANDO PAGAMENTO ===');
    setLoading(true);

    try {
      const { error, paymentIntent } = await stripe.confirmPayment({
        elements,
        confirmParams: {
          return_url: window.location.origin + "/billing/payment-success/",
        },
        redirect: 'if_required'
      });

      if (error) {
        console.error('Erro no pagamento:', error);
        if (error.type === "card_error" || error.type === "validation_error") {
          showMessage(error.message, true);
        } else {
          showMessage("Ocorreu um erro inesperado.", true);
        }
        setLoading(false);
      } else if (paymentIntent && paymentIntent.status === 'succeeded') {
        console.log('Pagamento bem-sucedido:', paymentIntent.id);
        
        // Verificar se deve salvar o cartão usando a nova função
        if (shouldSaveCard()) {
          console.log('💾 Salvando cartão...');
          await savePaymentMethod(paymentIntent.id);
        } else {
          console.log('⏭️ Não salvando cartão (usuário não marcou)');
        }
        
        showMessage("Pagamento realizado com sucesso!", false);
        setTimeout(() => {
          window.location.href = "/billing/payment-success/";
        }, 2000);
        setLoading(false);
      }
    } catch (error) {
      console.error('Erro ao confirmar pagamento:', error);
      showMessage("Erro inesperado ao processar pagamento", true);
      setLoading(false);
    }
  }

  // Salvar método de pagamento
  async function savePaymentMethod(paymentIntentId) {
    try {
      const response = await fetch('/billing/save-payment-method/', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRFToken': getCookie('csrftoken'),
        },
        body: JSON.stringify({
          payment_intent_id: paymentIntentId
        })
      });
      
      const data = await response.json();
      if (data.success && data.card_saved) {
        console.log('✅ Cartão salvo com sucesso');
        showMessage("Cartão salvo com sucesso!", false);
      }
    } catch (error) {
      console.error('❌ Erro ao salvar cartão:', error);
    }
  }

  // Obter cookie CSRF
  function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
      const cookies = document.cookie.split(';');
      for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].trim(); if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } // Verificar status do pagamento async function checkStatus() { const clientSecret = new URLSearchParams(window.location.search).get( "payment_intent_client_secret" ); if (!clientSecret) { return; } try { const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret); switch (paymentIntent.status) { case "succeeded": showMessage("Pagamento realizado com sucesso!", false); break; case "processing": showMessage("Seu pagamento está sendo processado.", false); break; case "requires_payment_method": showMessage("Seu pagamento não foi bem sucedido, tente novamente.", true); break; default: showMessage("Algo deu errado.", true); break; } } catch (error) { console.error('Erro ao verificar status:', error); } } // Exibir mensagens function showMessage(messageText, isError = true) { const messageContainer = document.querySelector("#payment-message"); if (messageContainer) { messageContainer.textContent = messageText; messageContainer.className = isError ? 'alert alert-danger' : 'alert alert-success'; messageContainer.style.display = "block"; setTimeout(() => {
        messageContainer.style.display = "none";
      }, 5000);
    }
    
    // Log no console também
    if (isError) {
      console.error('❌ Mensagem de erro:', messageText);
    } else {
      console.log('✅ Mensagem de sucesso:', messageText);
    }
  }

  // Controlar loading do botão principal
  function setLoading(isLoading) {
    const submitButton = document.querySelector("#submit");
    const spinner = document.querySelector("#spinner");
    const buttonText = document.querySelector("#button-text");
    
    if (submitButton && buttonText) {
      if (isLoading) {
        submitButton.disabled = true;
        if (spinner) spinner.classList.remove("hidden");
        buttonText.textContent = "Processando...";
      } else {
        submitButton.disabled = false;
        if (spinner) spinner.classList.add("hidden");
        // Recuperar texto original do botão
        const originalText = buttonText.dataset.originalText || 
                           buttonText.textContent.replace("Processando...", "Pagar");
        buttonText.textContent = originalText;
      }
    }
  }

  // Controlar loading de botões específicos
  function setButtonLoading(button, isLoading) {
    const spinner = button.querySelector('.spinner-border');
    const text = button.querySelector('.btn-text');
    
    if (text) {
      if (isLoading) {
        // Salvar texto original
        if (!text.dataset.originalText) {
          text.dataset.originalText = text.textContent;
        }
        button.disabled = true;
        if (spinner) spinner.classList.remove('d-none');
        text.textContent = 'Processando...';
      } else {
        button.disabled = false;
        if (spinner) spinner.classList.add('d-none');
        text.textContent = text.dataset.originalText || text.textContent.replace('Processando...', '');
      }
    }
  }
});

Explicação do Código – e_commerce/billing/templates/billing/payment-method.html

Novas Funcionalidades Implementadas (Apenas Partes em Azul):

1. Título Dinâmico e Contextual
  • Lógica Condicional: {% if saved_cards %}Pagamento{% else %}Add Payment Method{% endif %}
  • Experiência Personalizada: Clientes com cartões salvos veem “Pagamento”, novos usuários veem “Add Payment Method”
  • Contexto Imediato: Usuário já sabe suas opções pelo título da página
2. Seção Completa de Cartões Salvos (Totalmente Nova)
  • Container Principal: <div class="mb-4"> com espaçamento inferior
  • Card Bootstrap: Estrutura visual organizada com header “Cartões Salvos”
  • Loop Dinâmico: {% for card in saved_cards %} renderiza cada cartão
  • Estados Visuais: Bordas coloridas baseadas em status (padrão/expirado/normal)
3. Informações e Badges dos Cartões
  • Display do Cartão: {{ card.get_display_name }} mostra nome amigável
  • Data Formatada: {{ card.exp_month|stringformat:"02d" }} força 2 dígitos
  • Badge Padrão: <span class="badge badge-success">Padrão</span>
  • Badge Expirado: <span class="badge badge-danger">Expirado</span>
4. Botões de Ação com Loading States
  • Botão “Usar Cartão”: Com data-card-id e data-card-name para JavaScript
  • Spinner Individual: <span class="spinner-border spinner-border-sm d-none">
  • Formulários de Ação: Classes set-default-form e delete-card-form
  • URLs Named: {% url 'set-default-card' card.id %} e {% url 'delete-card' card.id %}
5. Estrutura de Cards para Organização
  • Card do Formulário: <div class="card"> envolve seção de pagamento
  • Header Dinâmico: “Adicionar Novo Cartão” vs “Informações de Pagamento”
  • Card Body: <div class="card-body"> organiza conteúdo interno
6. Checkbox para Salvar Cartão
  • Renderização Condicional: {% if billing_profile %} – só aparece para usuários logados
  • Checkbox Marcado: checked por padrão para encorajar salvamento
  • Label Descritivo: “Salvar este cartão para compras futuras”
  • ID para JavaScript: id="save-card" usado no código JS
7. Melhorias no Botão de Pagamento
  • Classe Extra: mb-3 adicionada ao payment-element
  • Texto Dinâmico: Pagar R$ {{ order.total }} mostra valor real
  • Data Attribute: data-original-text para restaurar texto original
  • Classe Alert: class="hidden alert" para styling de mensagens
8. Bloco JavaScript Separado
  • Bloco Extra JS: {% block extra_js %} segue padrão Django
  • Stripe JS: <script src="https://js.stripe.com/v3/">
  • Arquivo Separado: <script src="{% static 'js/payment-method.js' %}">
  • Melhores Práticas: JavaScript fora do template para cache e manutenção
9. 🚨 Correção Crítica do Checkbox (Solução de Bug)
  • Problema Identificado: Stripe Elements interferia com o checkbox, transformando-o em campo de texto
  • Mudança de Posicionamento: Checkbox movido para antes do formulário Stripe
  • Comentário Técnico: <!-- CHECKBOX MOVIDO PARA CIMA - FORA DA INTERFERÊNCIA DO STRIPE -->
  • Proteção Visual: Envolvido em <div class="alert alert-info mb-3"> para destaque
10. Estrutura Melhorada do Checkbox
  • ID Único: id="save-card-checkbox" (diferente do original para evitar conflitos)
  • Estilos Inline: style="width: 20px; height: 20px; cursor: pointer;"
  • Label Melhorado: class="form-check-label ms-2" com espaçamento
  • Texto Descritivo: Explicação adicional com <small class="text-muted">
11. Script de Correção Integrado
  • Inicialização Automática: DOMContentLoaded garante carregamento completo
  • Logs de Debug: console.log('=== INICIALIZANDO CHECKBOX INDEPENDENTE ===')
  • Event Listeners: addEventListener('click') e addEventListener('change')
  • Função Global: window.shouldSaveCard para integração com payment-method.js

⚠️ Problema Resolvido com Estas Adições:

  • Erro 404: Stripe Elements não conseguia renderizar devido à interferência DOM
  • Checkbox Não Funcional: Campo aparecia como texto em vez de checkbox clicável
  • JavaScript Conflitante: Stripe modificava elementos dentro de sua área
  • UX Quebrada: Usuário não conseguia salvar cartões para futuras compras

Benefícios das Modificações:

  • UX Completa: De formulário simples para sistema completo de gerenciamento
  • Estados Visuais: Usuário sempre sabe status de cada cartão
  • Organização: Cards Bootstrap para estrutura visual clara
  • Código Limpo: JavaScript separado seguindo boas práticas
  • Funcionalidade Garantida: Checkbox funciona independente do Stripe Elements
  • Debug Facilitado: Logs no console para identificar problemas rapidamente

e_commerce/billing/templates/billing/payment-method.html


{% extends "base.html" %}
{% load static %}

{% block content %}
<div class='col-10 col-md-6 mx-auto'>
  <h1>{% if saved_cards %}Pagamento{% else %}Add Payment Method{% endif %}</h1>
  <div id="stripe-key" data-publish-key="{{ publish_key }}" hidden></div>
  
  {% if saved_cards %}
  <div class="mb-4">
    <div class="card">
      <div class="card-header">
        <h5 class="mb-0">Cartões Salvos</h5>
      </div>
      <div class="card-body">
        {% for card in saved_cards %}
        <div class="border rounded p-3 mb-3 {% if card.default %}border-success{% elif card.is_expired %}border-danger{% else %}border-secondary{% endif %}">
          <div class="row">
            <div class="col-md-6">
              <div class="d-flex align-items-center">
                <div>
                  <strong>{{ card.get_display_name }}</strong>
                  <br>
                  <small class="text-muted">
                    Expira em {{ card.exp_month|stringformat:"02d" }}/{{ card.exp_year }}
                    {% if card.default %}
                      <span class="badge badge-success ml-2">Padrão</span>
                    {% endif %}
                    {% if card.is_expired %}
                      <span class="badge badge-danger ml-2">Expirado</span>
                    {% endif %}
                  </small>
                </div>
              </div>
            </div>
            <div class="col-md-6 text-right">
              {% if not card.is_expired %}
                <button 
                  type="button"
                  class="btn btn-primary btn-sm mb-2 use-card-btn"
                  data-card-id="{{ card.stripe_card_id }}"
                  data-card-name="{{ card.get_display_name }}"
                >
                  <span class="spinner-border spinner-border-sm d-none" role="status"></span>
                  <span class="btn-text">Usar este Cartão</span>
                </button>
              {% endif %}
              
              {% if not card.default and not card.is_expired %}
                <form method="POST" action="{% url 'set-default-card' card.id %}" class="d-inline set-default-form">
                  {% csrf_token %}
                  <button type="submit" class="btn btn-outline-secondary btn-sm mb-2">
                    <span class="spinner-border spinner-border-sm d-none" role="status"></span>
                    <span class="btn-text">Definir como Padrão</span>
                  </button>
                </form>
              {% endif %}
              
              <form method="POST" action="{% url 'delete-card' card.id %}" class="d-inline delete-card-form">
                {% csrf_token %}
                <button 
                  type="submit" 
                  class="btn btn-outline-danger btn-sm mb-2"
                  data-card-name="{{ card.get_display_name }}"
                >
                  <span class="spinner-border spinner-border-sm d-none" role="status"></span>
                  <span class="btn-text">Remover</span>
                </button>
              </form>
            </div>
          </div>
        </div>
        {% endfor %}
      </div>
    </div>
  </div>
  {% endif %}

  <div class="card">
    <div class="card-header">
      <h5 class="mb-0">
        {% if saved_cards %}Adicionar Novo Cartão{% else %}Informações de Pagamento{% endif %}
      </h5>
    </div>
    <div class="card-body">
      
      <!-- CHECKBOX MOVIDO PARA CIMA - FORA DA INTERFERÊNCIA DO STRIPE -->
      {% if billing_profile %}
      <div class="alert alert-info mb-3">
        <div class="form-check">
          <input 
            type="checkbox" 
            class="form-check-input"
            id="save-card-checkbox"
            checked
            style="width: 20px; height: 20px; cursor: pointer;"
          >
          <label 
            class="form-check-label ms-2" 
            for="save-card-checkbox"
            style="cursor: pointer; font-weight: bold;"
          >
            Salvar este cartão para compras futuras
          </label>
        </div>
        <small class="text-muted">Você poderá usar este cartão em futuras compras sem precisar digitá-lo novamente.</small>
      </div>
      {% endif %}

      <form id="payment-form" method="POST">
        {% csrf_token %}
        <div id="payment-element" class='form-control mb-3'>
            <!-- Stripe Elements will be inserted here -->
        </div>
        
        <button class='btn btn-primary my-3' id="submit">
          <div class="spinner hidden" id="spinner"></div>
          <span id="button-text" data-original-text="Pagar R$ {{ order.total }}">Pagar R$ {{ order.total }}</span>
        </button>
        
        <div id="payment-message" class="hidden alert"></div>
        <div id="next-url" data-url="/"></div>
      </form>
    </div>
  </div>
</div>

<!-- SCRIPT PARA GARANTIR QUE O CHECKBOX FUNCIONE -->
<script>
document.addEventListener('DOMContentLoaded', function() {
    console.log('=== INICIALIZANDO CHECKBOX INDEPENDENTE ===');
    
    const checkbox = document.getElementById('save-card-checkbox');
    
    if (checkbox) {
        // Garantir que está marcado
        checkbox.checked = true;
        
        // Event listener para clicks
        checkbox.addEventListener('click', function() {
            console.log('✅ Checkbox clicado:', this.checked);
        });
        
        checkbox.addEventListener('change', function() {
            console.log('✅ Checkbox mudou para:', this.checked);
        });
        
        // Event listener no label
        const label = document.querySelector('label[for="save-card-checkbox"]');
        if (label) {
            label.addEventListener('click', function() {
                console.log('✅ Label clicado');
            });
        }
        
        console.log('✅ Checkbox independente funcionando!');
    }
});

// Função para verificar se deve salvar o cartão (usada pelo payment-method.js)
window.shouldSaveCard = function() {
    const checkbox = document.getElementById('save-card-checkbox');
    return checkbox ? checkbox.checked : false;
};
</script>
{% endblock %}

{% block extra_js %}
<script src="https://js.stripe.com/v3/"></script>
<script src="{% static 'js/payment-method.js' %}"></script>
{% endblock %}

Explicação do Código – e_commerce/templates/base.html

Novas Funcionalidades Implementadas (Apenas Partes em Azul):

1. Bloco extra_js – Sistema de Scripts Específicos
  • Novo Bloco Template: {% block extra_js %}{% endblock %} adicionado antes do fechamento do body
  • Scripts por Template: Permite que templates filhos adicionem scripts específicos sem afetar outros
  • Posicionamento Estratégico: Colocado após {% include 'base/js.html' %} para carregar scripts globais primeiro
  • Ordem de Carregamento: Garante que dependências globais (jQuery, Bootstrap) carreguem antes de scripts específicos

Benefícios da Implementação:

  • Modularidade: Cada template pode ter seus próprios scripts sem poluir o base
  • Performance: Scripts só carregam onde são necessários
  • Manutenibilidade: Scripts específicos organizados nos templates correspondentes
  • Flexibilidade: Suporte para bibliotecas específicas (Stripe, Chart.js, etc.) em páginas individuais
  • Padrão Django: Segue convenções de template inheritance do Django

Exemplo de Uso:

  • Templates Específicos: payment-method.html usa para carregar Stripe.js
  • Scripts Condicionais: Carrega apenas onde necessário
  • Dependências Externas: APIs de terceiros ficam isoladas por funcionalidade

e_commerce/templates/base.html


{% load static %}
<!doctype html>
<html lang="en">
  <head>
  <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Base Template</title>
    {% include 'base/css.html' %}
    {% block base_head %}{% endblock base_head %}
  </head>
  <body>
    {% include 'base/navbar.html' with nome_da_marca='Loja virtual' %}
    <div class='container'>
      {% block content %} {% endblock %}
    </div>
    {% include 'base/js.html' %}
    {% block extra_js %}{% endblock %}
    
  </body>
</html>

Explicação do Código – e_commerce/e_commerce/urls.py

Novas Funcionalidades Implementadas (Apenas Partes em Azul):

1. Novas Importações para Gerenciamento de Cartões
  • save_payment_method: View para salvar cartões após pagamento bem-sucedido
  • set_default_card: View para definir um cartão como padrão do usuário
  • delete_card: View para remover cartões salvos do sistema
  • Organização Melhorada: Importações agrupadas em parênteses para melhor legibilidade
2. URLs de Gerenciamento de Cartões Salvos (Completamente Novas)
  • save-payment-method: path('billing/save-payment-method/', save_payment_method, name='save-payment-method')
  • set-default-card: path('billing/set-default-card/<int:card_id>/', set_default_card, name='set-default-card')
  • delete-card: path('billing/delete-card/<int:card_id>/', delete_card, name='delete-card')
  • Parâmetros Dinâmicos: Uso de <int:card_id> para capturar ID específico do cartão
3. Reorganização e Padronização de URLs
  • URL Movida: create-payment-intent transferida para billing/create-payment-intent/
  • Padrão Consistente: Todas URLs de billing agora com prefixo /billing/
  • Namespace Organizado: URLs relacionadas agrupadas geograficamente no arquivo
4. Comentários Organizacionais (Sistema de Seções)
  • # === CORE PAGES ===: Home, about, contact
  • # === APPS ===: Cart, search, products
  • # === API ENDPOINTS ===: APIs REST para frontend
  • # === AUTH ===: Login, logout, register
  • # === CHECKOUT & ADDRESSES ===: Endereços e checkout
  • # === BILLING & PAYMENTS ===: Todas URLs de pagamento
  • # === ADMIN & MISC ===: Admin e outras páginas
5. Integração com Templates e JavaScript
  • Names Correspondentes: URLs nomeadas coincidem com {% url %} nos templates
  • JavaScript Compatível: URLs ajustadas para funcionar com fetch() do payment-method.js
  • Parâmetros Template: <int:card_id> funciona com {{ card.id }} nos templates
6. 🚨 URL Crítica – Session
  • create-checkout-session: path('billing/create-checkout-session/', create_checkout_session, name='create-checkout-session')
  • Import Necessário: create_checkout_session adicionado às importações de billing.views
  • Função Crítica: URL responsável por inicializar o formulário do Stripe
  • Integração JavaScript: Recebe requisições AJAX do payment-method.js

⚠️ Problema Resolvido com Esta Adição:

  • Erro 404: JavaScript não conseguia acessar /billing/create-checkout-session/
  • Stripe Elements: Formulário de pagamento não carregava
  • Mensagem de Erro: “Erro ao carregar formulário de pagamento”
  • Fluxo Quebrado: Sistema de pagamento completamente inoperante

Benefícios das Modificações:

  • Funcionalidade Completa: Sistema completo de CRUD para cartões salvos
  • Organização Visual: Código mais fácil de navegar e manter
  • Padrões Consistentes: URLs seguem convenção lógica de agrupamento
  • Manutenibilidade: Adições futuras têm local claro onde ser inseridas
  • Integração JavaScript Funcional: Sistema de pagamento totalmente operacional
  • Prevenção de Erros 404: Todas as URLs necessárias estão definidas

e_commerce/e_commerce/urls.py


from django.conf import settings
from django.conf.urls.static import static

from django.contrib import admin
from django.contrib.auth.views import LogoutView 
from django.urls import path, include
from django.views.generic import TemplateView

from carts.views import cart_detail_api_view
from accounts.views import LoginView, RegisterView, LogoutView, guest_register_view
from addresses.views import checkout_address_create_view, checkout_address_reuse_view
from billing.views import (
    create_payment_intent, 
    payment_method_view, 
    payment_success_view, 
    payment_failed_view, 
    save_payment_method, 
    set_default_card, 
    delete_card,
    pay_with_saved_card
)
from .views import (
    home_page,  
    about_page, 
    contact_page
)

urlpatterns = [
    # === CORE PAGES ===
    path('', home_page, name='home'),
    path('about/', about_page, name='about'),
    path('contact/', contact_page, name='contact'),
    
    # === APPS ===
    path('cart/', include("carts.urls", namespace="cart")),
    path('search/', include("search.urls", namespace="search")),
    path('products/', include("products.urls", namespace="products")),
    
    # === API ENDPOINTS ===
    path('api/cart/', cart_detail_api_view, name='api-cart'),
    
    # === AUTH ===
    path('login/', LoginView.as_view(), name='login'),
    path('register/', RegisterView.as_view(), name='register'),
    path('register/guest/', guest_register_view, name='guest_register'),
    path('logout/', LogoutView.as_view(), name='logout'),
    
    # === CHECKOUT & ADDRESSES ===
    path('checkout/address/create/', checkout_address_create_view, name='checkout_address_create'),
    path('checkout/address/reuse/', checkout_address_reuse_view, name='checkout_address_reuse'),
    
    # === BILLING & PAYMENTS ===
    path('billing/payment-method/', payment_method_view, name='billing-payment-method'),
    path('billing/payment-success/', payment_success_view, name='payment-success'),
    path('billing/payment-failed/', payment_failed_view, name='payment-failed'),
    path('billing/create-payment-intent/', create_payment_intent, name='create-payment-intent'),
    path('billing/create-checkout-session/', create_checkout_session, name='create-checkout-session'), 
    path('billing/save-payment-method/', save_payment_method, name='save-payment-method'),
    path('billing/set-default-card/<int:card_id>/', set_default_card, name='set-default-card'),
    path('billing/delete-card/<int:card_id>/', delete_card, name='delete-card'),
    path('billing/pay-with-saved-card/', pay_with_saved_card, name='pay-with-saved-card'),
    # === ADMIN & MISC ===
    path('bootstrap/', TemplateView.as_view(template_name='bootstrap/example.html')),
    path('admin/', admin.site.urls),
]

if settings.DEBUG:
    urlpatterns = urlpatterns + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
    urlpatterns = urlpatterns + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

CSS Adicional para Cartões

Explicação do Código – static_local/css/stripe-custom-styles.css (Partes em Azul)

Novas Funcionalidades CSS Implementadas (Apenas Partes em Azul):

1. Comentários Organizacionais e Estrutura
  • Headers Visuais: Comentários com ======= para dividir seções claramente
  • Organização Lógica: Cada seção tem propósito específico (Stripe, Cartões Salvos, Responsive)
  • Manutenibilidade: Facilita encontrar e modificar estilos específicos
  • Documentação: Headers explicam o que cada seção faz
2. Sistema de Cartões Salvos – Estilo Base
  • .saved-card-item: Classe principal para containers de cartões salvos
  • transition: all 0.3s ease: Animação suave para todas as propriedades
  • animation: fadeIn 0.5s ease-out: Animação de entrada quando cartão é carregado
  • Hover Effect: transform: translateY(-2px) eleva cartão ao passar mouse
3. Estados Visuais dos Cartões
  • Hover Elevation: box-shadow: 0 4px 12px rgba(0,0,0,0.1) cria sombra ao passar mouse
  • Cartão Padrão: border-success recebe gradiente verde sutil
  • Gradiente Especial: linear-gradient(135deg, #f8fff9 0%, #e8f5e8 100%) para cartão padrão
  • Feedback Visual: Usuário identifica imediatamente qual cartão é padrão
4. Ícones das Bandeiras de Cartão
  • Visa: color: #1a1f71 – azul escuro oficial da Visa
  • Mastercard: color: #eb001b – vermelho oficial da Mastercard
  • American Express: color: #006fcf – azul oficial da Amex
  • Discover: color: #ff6000 – laranja oficial da Discover
  • Identidade Visual: Cores reais das bandeiras para reconhecimento imediato
5. Animação fadeIn – Entrada Suave
  • Estado Inicial: opacity: 0; transform: translateY(20px) – invisível e 20px abaixo
  • Estado Final: opacity: 1; transform: translateY(0) – visível na posição normal
  • Efeito Visual: Cartões “sobem” suavemente quando carregados
  • Experiência Premium: Interface mais fluida e profissional
6. Responsive Design para Cartões Salvos
  • Mobile Layout: @media (max-width: 768px) para tablets e celulares
  • Espaçamento Mobile: margin-top: 15px para separar informações e botões
  • Botões Full Width: width: 100% para botões ocuparem toda largura em mobile
  • Espaçamento de Botões: margin-bottom: 5px entre botões empilhados
7. Integração com Bootstrap
  • Classes Compatíveis: .saved-card-item funciona com grid system Bootstrap
  • Sobrescrita Inteligente: Não quebra estilos Bootstrap existentes
  • Combinação de Classes: .border-success Bootstrap + gradiente customizado
  • Responsividade: Media queries complementam breakpoints Bootstrap

Benefícios das Novas Implementações:

  • UX Premium: Animações e hover effects criam experiência moderna
  • Identidade Visual: Cores das bandeiras facilitam reconhecimento
  • Mobile Friendly: Layout responsivo otimizado para dispositivos móveis
  • Código Organizado: Estrutura clara facilita manutenção futura
  • Performance: Animações CSS3 são otimizadas pelo navegador

e_commerce/static_local/css/stripe-custom-styles.css


/* ============================================================================
   STRIPE CUSTOM STYLES - SISTEMA COMPLETO DE PAGAMENTO E CARTÕES SALVOS
   ============================================================================ */

/* === FORMULÁRIO PRINCIPAL STRIPE === */
#payment-form {
  width: 30vw;
  min-width: 500px;
  align-self: center;
  box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
    0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
  border-radius: 7px;
  padding: 40px;
}

#payment-form input {
  border-radius: 6px;
  margin-bottom: 6px;
  padding: 12px;
  border: 1px solid rgba(50, 50, 93, 0.1);
  height: 44px;
  font-size: 16px;
  width: 100%;
  background: white;
}

#card-element {
  border-radius: 4px 4px 0 0;
  padding: 12px;
  border: 1px solid rgba(50, 50, 93, 0.1);
  height: 44px;
  width: 100%;
  background: white;
}

#card-error {
  color: rgb(105, 115, 134);
  text-align: left;
  font-size: 13px;
  line-height: 17px;
  margin-top: 12px;
}

#payment-request-button {
  margin-bottom: 32px;
}

/* === BOTÕES STRIPE === */
#payment-form button {
  background: #5469d4;
  color: #ffffff;
  font-family: Arial, sans-serif;
  border-radius: 0 0 4px 4px;
  border: 0;
  padding: 12px 16px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  display: block;
  transition: all 0.2s ease;
  box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
  width: 100%;
}

#payment-form button:hover {
  filter: contrast(115%);
}

#payment-form button:disabled {
  opacity: 0.5;
  cursor: default;
}

/* === MENSAGENS E RESULTADOS === */
.result-message {
  line-height: 22px;
  font-size: 16px;
}

.result-message a {
  color: rgb(89, 111, 214);
  font-weight: 600;
  text-decoration: none;
}

.hidden {
  display: none;
}

/* === SPINNER DE CARREGAMENTO === */
.spinner,
.spinner:before,
.spinner:after {
  border-radius: 50%;
}

.spinner {
  color: #ffffff;
  font-size: 22px;
  text-indent: -99999px;
  margin: 0px auto;
  position: relative;
  width: 20px;
  height: 20px;
  box-shadow: inset 0 0 0 2px;
  -webkit-transform: translateZ(0);
  -ms-transform: translateZ(0);
  transform: translateZ(0);
}

.spinner:before,
.spinner:after {
  position: absolute;
  content: "";
}

.spinner:before {
  width: 10.4px;
  height: 20.4px;
  background: #5469d4;
  border-radius: 20.4px 0 0 20.4px;
  top: -0.2px;
  left: -0.2px;
  -webkit-transform-origin: 10.4px 10.2px;
  transform-origin: 10.4px 10.2px;
  -webkit-animation: loading 2s infinite ease 1.5s;
  animation: loading 2s infinite ease 1.5s;
}

.spinner:after {
  width: 10.4px;
  height: 10.2px;
  background: #5469d4;
  border-radius: 0 10.2px 10.2px 0;
  top: -0.1px;
  left: 10.2px;
  -webkit-transform-origin: 0px 10.2px;
  transform-origin: 0px 10.2px;
  -webkit-animation: loading 2s infinite ease;
  animation: loading 2s infinite ease;
}

/* === CARTÕES DE PRODUTOS === */
.card-img-wrapper {
  height: 250px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

.card-body {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  min-height: 150px;
}

.card-title {
  margin-bottom: auto;
}

.card-text {
  margin-top: auto;
}

/* ============================================================================
   ESTILOS PARA CARTÕES SALVOS
   ============================================================================ */

/* === CARTÕES SALVOS - ESTILO BASE === */
.saved-card-item {
  transition: all 0.3s ease;
  animation: fadeIn 0.5s ease-out;
}

.saved-card-item:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

.saved-card-item.border-success {
  background: linear-gradient(135deg, #f8fff9 0%, #e8f5e8 100%);
}

/* === ÍCONES DAS BANDEIRAS === */
.fab.fa-cc-visa {
  color: #1a1f71;
}

.fab.fa-cc-mastercard {
  color: #eb001b;
}

.fab.fa-cc-amex {
  color: #006fcf;
}

.fab.fa-cc-discover {
  color: #ff6000;
}

/* === ANIMAÇÕES === */
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@-webkit-keyframes loading {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

@keyframes loading {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

/* ============================================================================
   RESPONSIVE DESIGN
   ============================================================================ */

/* === MOBILE FIRST === */
@media (max-width: 576px) {
  .btn {
    font-size: 0.75rem;
  }
}

@media (max-width: 600px) {
  #payment-form {
    width: 80vw;
  }
}

/* === TABLETS E MOBILE === */
@media (max-width: 768px) {
  .saved-card-item .col-md-6:last-child {
    margin-top: 15px;
  }
  
  .saved-card-item .btn {
    width: 100%;
    margin-bottom: 5px;
  }
}

Criando as Migrações

Após adicionar o modelo Card, é necessário criar e aplicar as migrações:

# Criar a migração
python manage.py makemigrations billing

# Aplicar a migração
python manage.py migrate billing

Testando a Implementação

Stripe

Certifique-se que está logado no Stripe

Faça login com sua conta Stripe
stripe login

Encaminhe eventos ao seu webhook
stripe listen --forward-to localhost:8000/api/webhook

Pronto! O projeto está configurado, o banco de dados está populado, o Stripe já tá ouvindo os eventos em modo teste e você pode acompanhar a aula sem problemas e fazer todos os testes.

Fluxo de Teste

  1. Faça um pedido e vá até a tela de pagamento
  2. Use um cartão de teste do Stripe (ex: 4242 4242 4242 4242)
  3. Marque a opção “Salvar este cartão para compras futuras”
  4. Complete o pagamento
  5. Volte à tela de pagamento em um novo pedido
  6. Verifique se o cartão aparece na lista de cartões salvos
  7. Teste as funcionalidades de definir como padrão e remover cartão

Cartões de Teste do Stripe


Visa: 4242 4242 4242 4242
Mastercard: 5555 5555 5555 4444
American Express: 3782 822463 10005
Discover: 6011 1111 1111 1117

CVV: Qualquer 3 dígitos (4 para Amex)
Data: Qualquer data futura
CEP: Qualquer CEP válido

Resumo da Aula

Nesta aula implementamos um sistema completo de gerenciamento de cartões salvos integrado ao Stripe, incluindo:

  • Modelo Card com todos os campos necessários
  • CardManager para operações inteligentes
  • Interface moderna para listagem e gerenciamento
  • Salvamento automático após pagamento bem-sucedido
  • Funcionalidades de gerenciamento (padrão, remoção)
  • Segurança implementada seguindo melhores práticas

🎯 Resultado: Agora os usuários podem salvar seus cartões de forma segura e ter uma experiência de checkout muito mais rápida e conveniente!

🚀 Próxima Aula: Utilizando os Cartões Salvos no Checkout

Agora que implementamos o sistema de salvamento de cartões, o próximo passo é permitir que os usuários utilizem esses cartões salvos para realizar pagamentos de forma rápida e eficiente!

📋 O que você vai aprender na Aula 98:

1. Interface de Seleção de Cartões

  • Exibir cartões salvos na tela de checkout
  • Permitir seleção entre cartões salvos ou inserir novo
  • Interface intuitiva com informações do cartão

2. Lógica de Pagamento com Cartões Salvos

  • Modificar views para aceitar cartão selecionado
  • Integração com Stripe usando PaymentMethod salvo
  • Validações de segurança e propriedade do cartão

3. Experiência do Usuário Otimizada

  • Checkout em 1 clique para usuários recorrentes
  • Fallback gracioso para cartões expirados
  • Mensagens de erro amigáveis

💡 Resultado: Seus usuários terão uma experiência de checkout profissional e super rápida, igual aos grandes e-commerces do mercado!

🎯 Prepare-se para a próxima aula!

Na Aula 98, você vai completar o ciclo completo de pagamentos com cartões salvos, transformando seu e-commerce em uma plataforma verdadeiramente eficiente e user-friendly.

Não perca! 🔥

 

 

About The Author
-

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>