📘 Aula 95 – Django – Ecommerce – Cálculo Dinâmico do Valor do Pedido e Integração com Frontend

📘 Aula 95 – Django – Ecommerce – Cálculo Dinâmico do Valor do Pedido e Integração com Frontend

Loja Online - Django

Loja Online – Django

Voltar para página principal do blog

Todas as aulas desse curso

Aula 94                                   Aula 96

 

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 95 – Django – Ecommerce – Cálculo Dinâmico do Valor do Pedido e Integração com Frontend

Código da aula: Github

Nesta aula, vamos aprimorar nosso sistema de pagamento implementando a lógica para calcular o valor total do pedido com base nos itens do carrinho e integrando essa funcionalidade ao frontend.

Vamos começar criando a branch dessa aula com o comando:

git checkout -b feature/stripe-integration-cart

Ou:

git checkout -b feature/dynamic-payment-calculation

Usar o prefixo feature/ é uma convenção comum em muitos fluxos de trabalho Git, como o Git Flow.

Isso ajuda a diferenciar branches de desenvolvimento de outras, como bugfix/ para correções de bugs ou hotfix/ para correções urgentes.

Atualizações no Código

É bom lembrar que já temos o #payment-form que garante que os estilos sejam aplicados apenas ao formulário de pagamento específico, evitando conflitos com outros formulários na página.

Adição de Estilos para Imagens e Cards

Foi adicionada a regra para .card-img-wrapper para ajustar as imagens dentro dos cards, garantindo que as imagens sejam redimensionadas corretamente e centralizadas dentro dos contêineres.

Centralização e Alinhamento de Conteúdo

A classe .card-body já está no card, mas não foi usada no css ainda.

Vamos usar ela agora para garantir que o título e a descrição dos produtos dentro dos cards estejam alinhados verticalmente de maneira consistente.

As classes .card-title.card-text, da mesma forma que a .card-body mencionada anteriormente, também já constavam no card, porém, não estavam sendo utilizada no css.

A card-footer não tá sendo usada no css, e por enquanto continuará assim.

Estilos Específicos para Botões no Formulário Stripe

As regras CSS para botões agora estão limitadas ao formulário do Stripe, com o seletor #payment-form button para evitar aplicar esses estilos globalmente.

Alinhamento Vertical do Conteúdo do Card

.card-title { margin-bottom: auto; }: Essa regra foi adicionada para garantir que o título (.card-title) dentro do card tenha um pouco de espaço abaixo dele. Usar margin-bottom: auto faz com que o título ocupe o espaço disponível acima do conteúdo seguinte, garantindo que o título tenha um alinhamento flexível.

.card-text { margin-top: auto; }: Esta regra foi adicionada para garantir que o texto da descrição (.card-text) comece após o título e ocupe o espaço disponível abaixo, ajudando a manter uma estrutura mais organizada e alinhada dentro do card.

Estilos de Fontes para Dispositivos Móveis

Foi adicionado um media query para ajustar o tamanho da fonte dos botões em telas pequenas, utilizando a classe .btn dentro de um @media (max-width: 576px).

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

/* Estilos aplicados apenas ao formulário do 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;
}

.hidden {
  display: none;
}

#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;
}

.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;
}

.card-img-wrapper {
  height: 250px; /* Aumentando a altura do contêiner da imagem */
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

.card-body {
  display: flex;
  flex-direction: column;
  justify-content: flex-start; /* Garante que o conteúdo comece no topo */
  min-height: 150px; /* Defina uma altura mínima para alinhar os textos */
}

.card-title {
  margin-bottom: auto; /* Permite que o título tenha um pouco de espaço */
}

.card-text {
  margin-top: auto; /* O texto começa após o título */
}

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

#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;
}

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

/* Buttons and links específicos do formulário 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;
}

/* spinner/processing state, errors */
.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;
}

@media (max-width: 576px) {
  .btn {
      font-size: 0.75rem; /* Reduz a fonte em telas pequenas */
  }
}

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

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

No e_commerce/products/templates/products/snippets/card.html, removemos um estilo inline style="width: 18rem; e a formatação vai ser pelo css.

Adicionamos a classe h-100 para garantir que a altura do card seja preenchida completamente dentro de um layout de grid ou de contêiner, tornando os cartões uniformes em altura.

A imagem foi envolvida em uma div com a classe card-img-wrapper.

Antes o include {% include 'products/snippets/update-cart.html' with product=instance cart=cart %} estava dentro da <div class="card-body">.

Agora o include está dentro da <div class="card-footer d-flex p-2">.

Tem também essas classes já presentes no card, exceto a card-footer , e já mencionadas na parte do css:

  • card-body: d-flex flex-column, aplicam display flex e organizam os elementos em uma coluna.
  • card-text: a classe flex-grow-1, faz com que o texto ocupe o espaço disponível dentro do contêiner flex.
  • card-footer: as classes d-flex p-2, aplicam display flex e padding.

Essas classes adicionais no segundo bloco ajudam a controlar o layout e a distribuição dos elementos dentro do card, utilizando as propriedades do flexbox para uma organização mais flexível e responsiva.

O trecho em azul no código abaixo, o código da view do product, verifica se o usuário está autenticado e não é anônimo antes de registrar a visualização de um produto.

Se as condições forem atendidas, o sistema cria um registro no banco de dados para rastrear que o usuário autenticado visualizou o produto específico.

Esse registro é útil para analisar o comportamento dos usuários, oferecer recomendações personalizadas e manter um histórico de visualizações, permitindo que os usuários revisitem produtos que já acessaram.

Para testar o funcionamento desse trecho de código, siga estes passos:

  1. Acesse o Site como Usuário Autenticado:
    • Faça login no seu site com um usuário válido.
  2. Navegue até uma Página de Produto:
    • Após o login, visite a página de detalhes de um produto. O código será executado quando você acessar essa página.
  3. Verifique o Banco de Dados:
    • Acesse o banco de dados e verifique a tabela ObjectViewed para ver se um novo registro foi criado. Esse registro deve conter o ID do usuário autenticado e o produto visualizado.
    • Ou acesse o painel admin, analytics e objectviewed: http://localhost:8000/admin/analytics/objectviewed
    • Ou ainda o shell:
      python manage.py shell
      from analytics.models import ObjectViewed
      # Exibir todos os registros
      ObjectViewed.objects.all()
      # Filtrar registros por um campo específico
      ObjectViewed.objects.filter(user__username=”admin”)
      # Contar o número de registros

      ObjectViewed.objects.count()
      # Excluir um registro
      ObjectViewed.objects.filter(id=1).delete()
  1. Teste com um Usuário Anônimo:
    • Faça logout e tente acessar a mesma página de produto como um usuário anônimo.
    • Verifique novamente a tabela ObjectViewed. Não deve haver novos registros para o usuário anônimo, pois o código só registra visualizações de usuários autenticados.

Dessa forma, você pode confirmar que o código está funcionando corretamente para rastrear visualizações de produtos por usuários autenticados.

e_commerce/products/views.py

from django.http import Http404

from django.views.generic import ListView, DetailView
from django.shortcuts import render, get_object_or_404
from analytics.models import ObjectViewed
from analytics.mixin import ObjectViewedMixin
from carts.models import Cart
from .models import Product

class ProductFeaturedListView(ListView):
    template_name = "products/list.html"
    
    def get_queryset(self, *args, **kwargs):
        request = self.request
        return Product.objects.featured()

class ProductFeaturedDetailView(ObjectViewedMixin, DetailView):
    queryset = Product.objects.all().featured()
    template_name = "products/featured-detail.html"

#Class Based View
class ProductListView(ListView):
    #traz todos os produtos do banco de dados sem filtrar nada 
    queryset = Product.objects.all()
    template_name = "products/list.html"
    
    # def get_context_data(self, *args, **kwargs):
    #     context = super(ProductListView, self).get_context_data(*args, **kwargs)
    #     print(context)
    #     return context
    def get_context_data(self, *args, **kwargs):
        context = super(ProductListView,  self).get_context_data(*args, **kwargs)
        cart_obj, new_obj = Cart.objects.new_or_get(self.request)
        context['cart'] = cart_obj
        return context

    def get_context_data(self, *args, **kwargs):
        context = super(ProductListView,  self).get_context_data(*args, **kwargs)
        cart_obj, new_obj = Cart.objects.new_or_get(self.request)
        context['cart'] = cart_obj
        return context

#Function Based View
def product_list_view(request):
    queryset = Product.objects.all()
    context = {
        'object_list': queryset
    }
    return render(request, "products/list.html", context)

class ProductDetailSlugView(ObjectViewedMixin, DetailView):
    queryset = Product.objects.all()
    template_name = "products/detail.html"

    def get_context_data(self, *args, **kwargs):
        context = super(ProductDetailSlugView, self).get_context_data(*args, **kwargs)
        cart_obj, new_obj = Cart.objects.new_or_get(self.request)
        context['cart'] = cart_obj
        return context

    def get_object(self, *args, **kwargs):
        slug = self.kwargs.get('slug')
        try:
            instance = Product.objects.get(slug=slug, active=True)
        except Product.DoesNotExist:
            raise Http404("Produto não encontrado!")
        except Product.MultipleObjectsReturned:
            qs = Product.objects.filter(slug=slug, active=True)
            instance = qs.first()

        # Verifique se o usuário está autenticado e não é um usuário anônimo antes de criar o ObjectViewed
        if self.request.user.is_authenticated and not self.request.user.is_anonymous:
            ObjectViewed.objects.create(
                user=self.request.user,
                content_object=instance  # Usar content_object como indicado no modelo
            )

        # Caso o usuário seja anônimo, não cria o ObjectViewed
        return instance

#Class Based View
class ProductDetailView(ObjectViewedMixin, DetailView):
    template_name = "products/detail.html"

    def get_context_data(self, *args, **kwargs):
        context = super(ProductDetailView, self).get_context_data(*args, **kwargs)
        print(context)
        return context

    def get_object(self, *args, **kwargs):
        pk = self.kwargs.get('pk')
        instance = Product.objects.get_by_id(pk)
        if instance is None:
            raise Http404("Esse produto não existe!")
        return instance

#Function Based View
def product_detail_view(request, pk = None, *args, **kwargs):
    instance = Product.objects.get_by_id(pk)
    print(instance)
    if instance is None:
        raise Http404("Esse produto não existe!")

    context = {
        'object': instance
    }
    return render(request, "products/detail.html", context)

O trecho do código abaixo, o código do django_ecommerce/e_commerce/analytics/models.py , a parte do user = request.user if request.user.is_authenticated else None verifica se o usuário está autenticado.

Se sim, atribui o usuário à variável user, caso contrário, user recebe None.

Depois, user=user passa essa informação ao criar um registro de visualização (ObjectViewed), associando a visualização ao usuário logado, ou deixando None se o usuário for anônimo.

Como testar:

Usuário autenticado: Faça login e depois visualize os detalhes de um produto qualquer clicando no botão Detalhe.

Usuário anônimo: Visualize os detalhes de um produto clicando no botão Detalhe e veja se dá erro.

django_ecommerce/e_commerce/analytics/models.py 


from django.conf import settings
from django.db import models
from django.db.models.signals import pre_save, post_save
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.models import Session
from accounts.signals import user_logged_in
from .signals import object_viewed_signal
from .utils import get_client_ip

User = settings.AUTH_USER_MODEL
FORCE_SESSION_TO_ONE = getattr(settings, 'FORCE_SESSION_TO_ONE', False)
FORCE_INACTIVE_USER_ENDSESSION = getattr(settings, 'FORCE_INACTIVE_USER_ENDSESSION', False)

class ObjectViewed(models.Model):
    user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE)
    ip_address = models.CharField(max_length=220, blank=True, null=True)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')
    timestamp = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.content_object} viewed on {self.timestamp}"

    class Meta:
        ordering = ['-timestamp']
        verbose_name = 'Object viewed'
        verbose_name_plural = 'Objects viewed'

def object_viewed_receiver(sender, instance, request, *args, **kwargs):
    c_type = ContentType.objects.get_for_model(sender)
    user = request.user if request.user.is_authenticated else None
    new_view_obj = ObjectViewed.objects.create(
        user=user,
        content_type=c_type,
        object_id=instance.id,
        ip_address=get_client_ip(request)
    )

object_viewed_signal.connect(object_viewed_receiver)

class UserSession(models.Model):
    user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE)
    ip_address = models.CharField(max_length=220, blank=True, null=True)
    session_key = models.CharField(max_length=100, blank=True, null=True)
    timestamp = models.DateTimeField(auto_now_add=True)
    active = models.BooleanField(default=True)
    ended = models.BooleanField(default=False)

    def end_session(self):
        session_key = self.session_key
        try:
            Session.objects.get(pk=session_key).delete()
        except Session.DoesNotExist:
            pass
        except Exception as e:
            print(f"Unexpected error ending session: {e}")
        self.active = False
        self.ended = True
        self.save()

    def save(self, *args, **kwargs):
        if not self.active and not self.ended:
            self.end_session()
        super().save(*args, **kwargs)

def post_save_session_receiver(sender, instance, created, *args, **kwargs):
    if created:
        qs = UserSession.objects.filter(user=instance.user, ended=False, active=False).exclude(id=instance.id)
        for i in qs:
            i.end_session()
    if not instance.active and not instance.ended:
        instance.end_session()

if FORCE_SESSION_TO_ONE:
    post_save.connect(post_save_session_receiver, sender=UserSession)

def post_save_user_changed_receiver(sender, instance, created, *args, **kwargs):
    if not created:
        if instance.is_active == False:
            qs = UserSession.objects.filter(user=instance.user, ended=False, active=False)
            for i in qs:
                i.end_session()

if FORCE_INACTIVE_USER_ENDSESSION:
    post_save.connect(post_save_user_changed_receiver, sender=User)

def user_logged_in_receiver(sender, instance, request, *args, **kwargs):
    user = instance
    ip_address = get_client_ip(request)
    session_key = request.session.session_key
    UserSession.objects.create(
        user=user,
        ip_address=ip_address,
        session_key=session_key
    )

user_logged_in.connect(user_logged_in_receiver)

Função de Cálculo do Valor do Pedido

Primeiramente, vamos implementar a função na views.py do billing, que calculará o valor total do pedido com base nos itens do carrinho.

Temos o import da classe Decimal para cálculos precisos e o modelo Cart para acessar o carrinho de compras.

Na função calculate_order_amount, ela pega o carrinho do banco de dados, calcula o valor total dos itens no carrinho multiplicando o preço pela quantidade de cada item e ajusta o total com base na relação entre o total e o subtotal do carrinho.

Finalmente, o valor é convertido para centavos, que é o formato exigido pela API do Stripe.

django_ecommerce/e_commerce/billing/views.py


from decimal import Decimal
from django.shortcuts import render
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 products.models import Product
from carts.models import Cart
from django.conf import settings
import stripe
import json

stripe.api_key = settings.STRIPE_API_KEY

# View para renderizar a página de sucesso do pagamento
def payment_success_view(request):
    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):
    # Adicione um log para verificar se a chave está sendo passada
    # print(f"Publish Key na view: {settings.STRIPE_PUB_KEY}")
    context = {'publish_key': settings.STRIPE_PUB_KEY}
    return render(request, 'billing/payment-method.html', context)

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

Atualização no JavaScript

Agora vamos adicionar uma função no JavaScript para obter os itens do carrinho e enviá-los para a função de cálculo no backend.

A função getCartItems faz uma requisição AJAX síncrona (usando async: false) para a URL /cart/get-items/, buscando os itens no carrinho de compras.

Se a requisição for bem-sucedida, ela armazena os itens recebidos na variável items.

Se houver um erro, ele é registrado no console.

No final, a função retorna a lista de itens do carrinho.

static_local/js/stripe_payment.js

$(document).ready(function () {
  console.log("Documento pronto");

  function initializeStripe() {
      const stripeKeyElement = $("#stripe-key");

      if (stripeKeyElement.length === 0) {
          console.error("Elemento stripe-key não encontrado");
          return;
      }

      const publishKey = stripeKeyElement.data("publishKey");

      if (!publishKey) {
          console.error("Chave pública do Stripe não encontrada");
          return;
      }

      const stripe = Stripe(publishKey);
      let elements, cardElement, clientSecret = null;

      function initialize() {
          const items = getCartItems();  // Obtém os itens do carrinho

          $.ajax({
              url: "/create-payment-intent",
              method: "POST",
              contentType: "application/json",
              headers: {
                  "X-CSRFToken": getCookie("csrftoken"),
              },
              data: JSON.stringify({ items: items }),
              success: function (data) {
                  if (data.error) {
                      console.error("Falha ao criar o intent de pagamento:", data.error);
                      return;
                  }
                  clientSecret = data.clientSecret;

                  elements = stripe.elements();
                  cardElement = elements.create("card");
                  cardElement.mount("#payment-element");
                  console.log("Elemento do cartão montado");
              },
              error: function (error) {
                  console.error("Erro ao inicializar os elementos do Stripe:", error);
              },
          });
      }

      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;
      }

      function getCartItems() {
          let items = [];
          $.ajax({
              url: "/cart/get-items/",  // Certifique-se que a URL está correta
              method: "GET",
              async: false, // Garante que a requisição seja concluída antes de prosseguir
              success: function (data) {
                  items = data.items;
              },
              error: function (xhr, status, error) {
                  console.error("Erro ao obter itens do carrinho:", error);
              }
          });
          return items;
      }

      const paymentForm = $("#payment-form");

      if (paymentForm.length > 0) {
          paymentForm.on("submit", function (event) {
              event.preventDefault();

              if (!elements || !cardElement) {
                  showMessage("Elemento do cartão não inicializado corretamente.", true);
                  return;
              }

              setLoading(true);

              stripe
                  .confirmCardPayment(clientSecret, {
                      payment_method: {
                          card: cardElement,
                          billing_details: {
                              // Inclua os detalhes de faturamento se necessário
                          },
                      },
                  })
                  .then(function (result) {
                      if (result.error) {
                          showMessage(result.error.message, true);
                          setTimeout(function () {
                              window.location.href = "/billing/payment-failed/";
                          }, 3000);
                      } else if (result.paymentIntent && result.paymentIntent.status === "succeeded") {
                          showMessage("Pagamento realizado com sucesso!", false);
                          setTimeout(function () {
                              window.location.href = "/billing/payment-success/";
                          }, 3000);
                      } else {
                          showMessage("Pagamento não foi bem-sucedido. Tente novamente.", true);
                          setTimeout(function () {
                              window.location.href = "/billing/payment-failed/";
                          }, 3000);
                      }
                      setLoading(false);
                  })
                  .catch(function (error) {
                      showMessage("Ocorreu um erro inesperado.", true);
                      setTimeout(function () {
                          window.location.href = "/billing/payment-failed/";
                      }, 3000);
                      setLoading(false);
                  });
          });
      } else {
          console.error("Formulário de pagamento não encontrado");
      }

      function showMessage(messageText, isError) {
          const messageContainer = $("#payment-message");
          messageContainer.text(messageText).removeClass("hidden");

          if (isError) {
              messageContainer.addClass("error-message");
          } else {
              messageContainer.removeClass("error-message");
          }

          setTimeout(function () {
              messageContainer.addClass("hidden").text("");
          }, 4000);
      }

      function setLoading(isLoading) {
          const submitButton = $("#submit");
          submitButton.prop("disabled", isLoading);
          $("#spinner").toggleClass("hidden", !isLoading);
          $("#button-text").toggleClass("hidden", isLoading);
      }

      initialize();
  }

  // Inicializa o Stripe diretamente se o elemento estiver presente
  const stripeKeyElement = $("#stripe-key");

  if (stripeKeyElement.length > 0) {
      initializeStripe();
  }
});

O que mudou no static_local/js/ecommerce.js

Uso de Template Strings: A mudança principal é a utilização de template strings (delimitadas por crases “) em vez de strings tradicionais (delimitadas por aspas simples ' ou duplas ") para construir o HTML. As template strings permitem a inserção de múltiplas linhas e são mais fáceis de ler quando se constrói estruturas HTML dinâmicas.

Botões Dinâmicos: Dependendo se o produto foi adicionado ou removido do carrinho, o botão dentro do submitSpan é alterado dinamicamente entre um botão de “Remover” (com estilo btn-outline-danger) e um botão de “Adicionar” (com estilo btn-success).

static_local/js/ecommerce.js

$(document).ready(function () {
  // Contact Form Handler
  var contactForm = $(".contact-form");
  var contactFormMethod = contactForm.attr("method");
  var contactFormEndpoint = contactForm.attr("action");
  function displaySubmitting(submitBtn, defaultText, doSubmit) {
    if (doSubmit) {
      submitBtn.addClass("disabled");
      submitBtn.html("<i class='fa fa-spin fa-spinner'></i> Enviando...");
    } else {
      submitBtn.removeClass("disabled");
      submitBtn.html(defaultText);
    }
  }
  contactForm.submit(function (event) {
    event.preventDefault();
    const contactFormSubmitBtn = contactForm.find("[type='submit']");
    const contactFormSubmitBtnTxt = contactFormSubmitBtn.text();
    const contactFormData = contactForm.serialize();
    const thisForm = $(this);
    displaySubmitting(contactFormSubmitBtn, "", true);
    $.ajax({
      method: contactFormMethod,
      url: contactFormEndpoint,
      data: contactFormData,
      success: function (data) {
        contactForm[0].reset();
        $.alert({
          title: "Success!",
          content: data.message,
          theme: "modern",
        });
        setTimeout(function () {
          displaySubmitting(
            contactFormSubmitBtn,
            contactFormSubmitBtnTxt,
            false
          );
        }, 500);
      },
      error: function (error) {
        console.log(error.responseJSON);
        const jsonData = error.responseJSON;
        let msg = "";
        $.each(jsonData, function (key, value) {
          msg += key + ": " + value[0].message + "<br/>";
        });
        $.alert({
          title: "Oops!",
          content: msg,
          theme: "modern",
        });
        setTimeout(function () {
          displaySubmitting(
            contactFormSubmitBtn,
            contactFormSubmitBtnTxt,
            false
          );
        }, 500);
      },
    });
  });
  // Auto Search
  const searchForm = $(".search-form");
  const searchInput = searchForm.find("[name='q']"); // input name='q'
  const typingTimer = 0;
  const typingInterval = 500; // .5 seconds
  const searchBtn = searchForm.find("[type='submit']");
  searchInput.keyup(function (event) {
    //key released
    clearTimeout(typingTimer);
    typingTimer = setTimeout(performSearch, typingInterval);
  });
  searchInput.keydown(function (event) {
    // key pressed
    clearTimeout(typingTimer);
  });
  function displaySearching() {
    searchBtn.addClass("disabled");
    searchBtn.html("<i class='fa fa-spin fa-spinner'></i> Searching...");
  }
  function performSearch() {
    displaySearching();
    var query = searchInput.val();
    setTimeout(function () {
      window.location.href = "/search/?q=" + query;
    }, 1000);
  }
  //Cart + Add Product
  const productForm = $(".form-product-ajax");
  productForm.submit(function (event) {
    event.preventDefault();
    // console.log("O formulário não foi enviado!");
    // o this pega os dados relacionados a esse form
    const thisForm = $(this);
    //const actionEndpoint = thisForm.attr("action");
    const actionEndpoint = thisForm.attr("data-endpoint");
    const httpMethod = thisForm.attr("method");
    const formData = thisForm.serialize();
    $.ajax({
      url: actionEndpoint,
      method: httpMethod,
      data: formData,
      success: function (data) {
        // console.log("Sucesso")
        // console.log(data)
        // console.log("Adicionado", data.added)
        // console.log("Removido", data.removed)
        const submitSpan = thisForm.find(".submit-span");
        if (data.added) {
          submitSpan.html(`
            <button type='submit' class='btn btn-outline-danger btn-sm w-100'>Remover</button>
          `);
        } else {
          submitSpan.html(`
           <button type='submit' class='btn btn-success btn-sm w-100'>Adicionar</button>
          `);
        }

        const navbarCount = $(".navbar-cart-count");
        navbarCount.text(data.cartItemCount);
        const currentPath = window.location.href;
        if (currentPath.indexOf("cart") != -1) {
          refreshCart();
        }
      },
      error: function (errorData) {
        $.alert({
          title: "Oops!",
          content: "Ocorreu um erro, tente novamente mais tarde!",
          theme: "modern",
        });
      },
    });
  });
  function refreshCart() {
    //console.log("Excluído do carrinho atual!")
    const cartTable = $(".cart-table");
    const cartBody = cartTable.find(".cart-body");
    //cartBody.html("<h1>Mudou!</h1>")
    const productsRow = cartBody.find(".cart-product");
    const currentUrl = window.location.href;
    const refreshCartUrl = "/api/cart/";
    const refreshCartMethod = "GET";
    const data = {};
    $.ajax({
      url: refreshCartUrl,
      method: refreshCartMethod,
      data: data,
      success: function (data) {
        console.log(data);
        const hiddenCartItemRemoveForm = $(".cart-item-remove-form");
        if (data.products.length > 0) {
          productsRow.html(" ");
          let i = data.products.length;
          $.each(data.products, function (index, value) {
            const newCartItemRemove = hiddenCartItemRemoveForm.clone();
            newCartItemRemove.css("display", "block");
            newCartItemRemove.find(".cart-item-product-id").val(value.id);
            cartBody.prepend(
              '<tr><th scope="row">' +
                i +
                "</th><td><a href='" +
                value.url +
                "'>" +
                value.name +
                "</a>" +
                newCartItemRemove.html() +
                "</td><td>" +
                value.price +
                "</td></tr>"
            );
            i--;
          });
          cartBody.find(".cart-subtotal").text(data.subtotal);
          cartBody.find(".cart-total").text(data.total);
        } else {
          window.location.href = currentUrl;
        }
      },
      error: function (errorData) {
        console.log("Erro");
        console.log(errorData);
      },
    });
  }
});

No e_commerce/carts/urls.py o cart_get_items é uma função de visualização (view) no Django que lida com a requisição para obter os itens do carrinho.

A linha path('get-items/', cart_get_items, name='cart-get-items') mapeia a URL /get-items/ para essa função, permitindo que o frontend acesse os itens do carrinho através dessa URL.

e_commerce/carts/urls.py

from django.urls import path

from .views import (
    cart_home, 
    checkout_home,
    cart_update,
    checkout_done_view,
    cart_get_items,
)

app_name = "carts"

urlpatterns = [
    path('', cart_home, name='home'),
    path('checkout/success/', checkout_done_view, name='success'),
    path('checkout/', checkout_home, name='checkout'),
    path('update/', cart_update, name='update'),
    path('get-items/', cart_get_items, name='cart-get-items'),
]

Testando as Novas Funcionalidades

Suba o servidor e teste as novas funcionalidades para garantir que o valor do pedido seja calculado dinamicamente com base nos itens do carrinho.

Suba o servidor

python manage.py runserver

Acesse: 127.0.0.1:8000/billing/payment-method

Pra você testar se o cálculo do valor do pedido e a integração com o frontend estão rodando certinho, depois de acessar 127.0.0.1:8000/billing/payment-method, é só seguir esses passos aqui:

1. Dá uma conferida no formulário de pagamento

Primeiro de tudo, quando você abrir a URL, tem que aparecer o formulário de pagamento bonitinho na tela, com aquele campo pro cartão de crédito (o famoso Stripe Elements) e um botão pra enviar a parada.

Se não aparecer, algo tá errado.

2. Adiciona uns itens no carrinho

Antes de sair testando o pagamento, dá um rolê no site e coloca uns produtos no carrinho?

Isso é importante porque a mágica do cálculo do valor do pedido só rola se tiver coisa no carrinho.

3. Preenche o formulário e manda ver

Preencha o formulário de pagamento.

Usa esses dados aqui, que são de teste do Stripe:

  • Número do Cartão: 4242 4242 4242 4242
  • Data de Expiração: Coloca qualquer data no futuro (tipo 05/34)
  • CVC: Qualquer código de 3 dígitos (sei lá, 123, por exemplo)

Depois disso, clica no botão de enviar.

4. Fica de olho na requisição do valor do pedido

Quando você manda o formulário, o JavaScript (aquele que você botou lá, o getCartItems) vai entrar em ação, mandando uma requisição AJAX pra /cart/get-items/ e depois passando esses itens pro backend calcular o valor total do pedido.

Quer ver se isso rolou certinho? Abre as ferramentas de desenvolvedor no seu navegador (aquele F12 básico), vai na aba “Network” (Rede) e dá uma olhada na requisição pra /cart/get-items/. Se a resposta vier certinha, com os itens que você colocou no carrinho, tamo no caminho certo.

5. Confere o processamento do pagamento

Se a requisição AJAX rolou de boa e o cálculo foi feito corretamente, o pagamento deve seguir sem problema nenhum.

Daí, você vai ser redirecionado pra página de sucesso ou de falha de pagamento, dependendo do resultado.

Se der ruim, não se desespera, é só seguir as próximas dicas.

6. Dá um check no backend e no banco de dados (opcional)

Se quiser garantir que tá tudo nos conformes, dá uma conferida nos logs do Django pra ver se a função calculate_order_amount foi chamada corretamente e se não rolou nenhum erro no processo.

Se precisar, dá uma olhada no banco de dados também pra ver se o pagamento foi registrado como esperado.

Tranquilo?

7. Fica ligado na mensagem de feedback

No frontend, você deve ver uma mensagem dizendo se a transação foi sucesso ou não, dependendo do que rolou, conforme você configurou lá no JavaScript (stripe_payment.js).

Se tudo estiver certo, a parada tá funcionando lindamente.

Se não, bora dar uma olhada nos logs de erro do Django e na resposta da requisição AJAX pra entender o que deu ruim para corrigir.

E é isso aí!

Segue esses passos e você vai estar pronto pra garantir que o sistema de pagamento tá tinindo!

Se der ruim em algum momento, só voltar aqui e revisar cada detalhe.

Agora é com você! Boa sorte!

Conclusão

Nesta aula, implementamos a lógica para calcular o valor total do pedido com base nos itens do carrinho de forma dinâmica e integramos essa funcionalidade com o frontend.

Na próxima aula, vamos focar em criar uma API para obter os itens do carrinho em tempo real e continuar aprimorando o fluxo de pagamento.

Nos vemos na próxima aula!
Bons estudos!

Voltar para página principal do blog

Aula 94                                   Aula 96

 

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>