📘 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
Voltar para página principal do blog
Todas as aulas desse curso
Aula 94 Aula 96
Redes Sociais do Código Fluente:
Scarlett Finch
Scarlett Finch é uma influenciadora virtual criada com IA.
Ela é 🎤 cantora e 🎶compositora pop britânica.
Siga a Scarlett Finch no Instagram:
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:
- Digital Innovation One – Cursos gratuitos com certificado.
- Workover – Aprenda Python3 gratuitamente.
- Comunidades de desenvolvedores para networking:
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
📘 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
e .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 classeflex-grow-1
, faz com que o texto ocupe o espaço disponível dentro do contêiner flex.card-footer
: as classesd-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:
- Acesse o Site como Usuário Autenticado:
- Faça login no seu site com um usuário válido.
- 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.
- 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()
- Acesse o banco de dados e verifique a tabela
- 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