Capítulo 6

Criando o Carrinho

6.1 O Carrinho de compras

Algo bem comum em sistemas de vendas é podermos ter um carrinho de compras para o usuário. Nosso sistema não será diferente! Por isso, vamos criar nossa classe Carrinho :

public class Carrinho {



}

Agora, precisamos fazer com que o Spring consiga ter acesso a ele e, portanto, usaremos a anotação @Component. Contudo, o carrinho de compras geralmente fica vivo durante toda a sessão do usuário. Desse modo, vamos deixar ele com o escopo de sessão:

@Component
@SessionScoped
public class Carrinho {



}

Para deixarmos nosso carrinho mais inteligente, vamos dar algumas propriedades a ele! Por exemplo, precisaremos ter a lista de ingressos que o usuário vai querer comprar e também uma forma de podermos adicionar os ingressos nessa lista:

    public class Carrinho {

    private List<Ingresso> ingressos = new ArrayList<>();

    public void add(Ingresso ingresso){
        ingressos.add(ingresso);
    }

    //demais métodos e getters e setters
}

Agora, é necessário disponibilizar o carrinho para a tela, ou seja, quando pegarmos a sessão precisamos injetar nosso carrinho:

    @Controller
    public class SessaoController {

        @Autowired
        private Carrinho carrinho;

    }

Como na nossa requisição estamos enviando somente os id's da sessão, precisamos pegar esses dados do banco e retornar um ingresso válido para adicionar ao carrinho. Para isso, vamos criar a classe CarrinhoForm que representará nosso formulário. Essa classe será responsável por pegar os dados selecionados pelo usuário na tela e, a partir deles, nos fornecer um objeto Carrinho para podermos finalizar a compra.

    public class CarrinhoForm {

        private List<Ingresso> ingressos = new ArrayList<>();

        public List<Ingresso> getIngressos() {
            return ingressos;
        }

        public void setIngressos(List<Ingresso> ingressos) {
            this.ingressos = ingressos;
        }


        public List<Ingresso> toIngressos(SessaoDao sessaoDao, LugarDao lugarDao){

            return this.ingressos.stream().map(ingresso -> {
                Sessao sessao = sessaoDao.findOne(ingresso.getSessao().getId());
                Lugar lugar  = lugarDao.findOne(ingresso.getLugar().getId());
                TipoDeIngresso tipoDeIngresso = ingresso.getTipoDeIngresso();
                return new Ingresso(sessao, tipoDeIngresso, lugar);
            }).collect(Collectors.toList());

        }
    }

E, por fim, precisamos assim que o usuário decidir todos os ingressos que deseja, adicioná-los no nosso carrinho:

      @PostMapping("/compra/ingressos")
        public ModelAndView enviarParaPagamento(CarrinhoForm carrinhoForm){
            ModelAndView modelAndView = new ModelAndView("redirect:/compra");

            formulario.toIngressos(sessaoDao, lugarDao).forEach(carrinho::add);

            return modelAndView;
        }

Seus livros de tecnologia parecem do século passado?

Conheça a Casa do Código, uma nova editora, com autores de destaque no mercado, foco em ebooks (PDF, epub, mobi), preços imbatíveis e assuntos atuais.
Com a curadoria da Caelum e excelentes autores, é uma abordagem diferente para livros de tecnologia no Brasil.

Casa do Código, Livros de Tecnologia.

6.2 Exercício - Implementando a tela de compras

  1. Ao selecionar o lugar e o tipo de ingresso, vamos adicionar os ingressos no carrinho. Portanto, vamos criar a classe Carrinho no pacote de modelos, br.com.caelum.ingresso.model.

     public class Carrinho {
    
         private List<Ingresso> ingressos = new ArrayList<>();
    
         public void add(Ingresso ingresso){
             ingressos.add(ingresso);
         }
    
         //demais métodos e getters e setters
     }
    
  2. Altere o escopo do carrinho para SessionScope. Fique atento para pegar o import do Spring, do pacote org.springframework.web.context.annotation:

     @Component
     @SessionScope
     public class Carrinho {
    
         //restante da implementação
     }
    
  3. Vamos disponibilizar o carrinho para a tela de seleção de lugares:

     @Controller
     public class SessaoController {
         //Demais atributos omitidos
    
         @Autowired
         private Carrinho carrinho;
    
         @GetMapping("/admin/sessao/{id}/lugares")
         public ModelAndView lugaresNaSessao(@PathVariable("id") Integer sessaoId){
             ModelAndView modelAndView = new ModelAndView("sessao/lugares");
    
             Sessao sessao = sessaoDao.findOne(sessaoId);
    
             Optional<ImagemCapa> imagemCapa = client.request(sessao.getFilme(), ImagemCapa.class);
    
             modelAndView.addObject("sessao", sessao);
             modelAndView.addObject("carrinho", carrinho);
             modelAndView.addObject("imagemCapa", imagemCapa.orElse(new ImagemCapa()));
             modelAndView.addObject("tiposDeIngressos", TipoDeIngresso.values());
    
             return modelAndView;
         }
    
         //Demais métodos omitidos
     }
    
  4. Como na nossa requisição estamos enviando somente os id's da sessão, precisamos pegar esses dados do banco e retornar um ingresso válido para adicionar ao carrinho. Desse modo, no pacote br.com.caelum.ingresso.model.form, vamos criar a classe CarrinhoForm que representará nosso formulário. Nela, crie um método que irá retornar uma lista de ingressos válidos:

     public class CarrinhoForm {
    
         private List<Ingresso> ingressos = new ArrayList<>();
    
         public List<Ingresso> getIngressos() {
             return ingressos;
         }
    
         public void setIngressos(List<Ingresso> ingressos) {
             this.ingressos = ingressos;
         }
    
         public List<Ingresso> toIngressos(SessaoDao sessaoDao, LugarDao lugarDao){
    
             return this.ingressos.stream().map(ingresso -> {
                 Sessao sessao = sessaoDao.findOne(ingresso.getSessao().getId());
                 Lugar lugar  = lugarDao.findOne(ingresso.getLugar().getId());
                 TipoDeIngresso tipoDeIngresso = ingresso.getTipoDeIngresso();
                 return new Ingresso(sessao, tipoDeIngresso, lugar);
             }).collect(Collectors.toList());
    
         }
     }
    
  5. Adicione o método findOne na classe LugarDao, localizada no pacote br.com.caelum.ingresso.dao:

     public Lugar findOne(Integer id) {
         return manager.find(Lugar.class, id);
     }
    
  6. Vamos criar a classe CompraController no pacote br.com.caelum.ingresso.controller. Depois, na classe criada, adicione a action para a URI /compra/ingressos:

     @Controller
     public class CompraController {
    
         @Autowired
         private SessaoDao sessaoDao;
         @Autowired
         private LugarDao lugarDao;
    
         @Autowired
         private Carrinho carrinho;
    
         @PostMapping("/compra/ingressos")
         public ModelAndView enviarParaPagamento(CarrinhoForm carrinhoForm){
             ModelAndView modelAndView = new ModelAndView("redirect:/compra");
    
             carrinhoForm.toIngressos(sessaoDao, lugarDao).forEach(carrinho::add);
    
             return modelAndView;
         }
     }
    
  7. Para realizar a escolha de lugares na tela, vamos alterar o comportamento da tag svg adicionando a propriedade onclick. Quando o lugar for clicado, uma verificação deverá ser realizada para checar se o lugar já foi selecionado e alterar seu estado para Ocupado ou Disponível.

     <svg class="assento ${sessao.isDisponivel(lugar) ? "disponivel" : "ocupado"}" onclick="${sessao.isDisponivel(lugar) ? 'changeCheckbox(this)' : '' }" ...
     >
    

6.3 Melhorando a usabilidade do carrinho

Ainda precisamos remover do carrinho os lugares que foram marcados e logo em seguida desmarcados, para isso vamos criar um método que faça a validação se já está dentro do carrinho. Usaremos o método anyMatch da classe Stream para fazer a verificação de algum ingresso já presente no carrinho :

    public boolean isSelecionado(Lugar lugar){
        return ingressos.stream().map(Ingresso::getLugar).anyMatch(l -> l.equals(lugar));
    }

Agora, basta usar esse nosso método em nossa jsp, no próprio item que representa o lugar, para que a verificação seja feita antes de adicionar um ingresso e não tenhamos a seleção de lugares duplicada :


   <svg class="assento ${sessao.isDisponivel(lugar) && !carrinho.isSelecionado(lugar) ? "disponivel" : "ocupado" }"
        onclick="${sessao.isDisponivel(lugar) && !carrinho.isSelecionado(lugar) ? 'changeCheckbox(this)' : '' }"
        ...>
          <!-- Restante -->
    </svg>

6.4 Exercício - Desabilitando a seleção do lugar que já está no carrinho

  1. Vamos adicionar um método no carrinho para verificar se um lugar está no carrinho ou não:

     public boolean isSelecionado(Lugar lugar){
         return ingressos.stream().map(Ingresso::getLugar).anyMatch(l -> l.equals(lugar));
     }
    
  2. Vamos alterar a tela de seleção de lugares/lista, para que não seja possível selecionar os lugares que já foram adicionados no carrinho. Para isso, altere a tag svg já presente na nossa jsp e adicione a nova verificação do carrinho nas propriedades class e onclick :

     <svg class="assento ${sessao.isDisponivel(lugar) && !carrinho.isSelecionado(lugar) ? "disponivel" : "ocupado" }"
         onclick="${sessao.isDisponivel(lugar) && !carrinho.isSelecionado(lugar) ? 'changeCheckbox(this)' : '' }"
         ...>
         <!-- Restante -->
     </svg>
    

Agora é a melhor hora de respirar mais tecnologia!

Se você está gostando dessa apostila, certamente vai aproveitar os cursos online que lançamos na plataforma Alura. Você estuda a qualquer momento com a qualidade Caelum. Programação, Mobile, Design, Infra, Front-End e Business! Ex-aluno da Caelum tem 15% de desconto, siga o link!

Conheça a Alura Cursos Online.

6.5 Exibindo os detalhes da compra

Antes de fecharmos a nossa compra, nosso usuário quer checar se as informações estão corretas para poder fazer o pagamento.

Para fazermos essa nossa nova tela, precisaremos ter um método em nosso Controller responsável por nos informar o estado da Compra :


    @GetMapping("/compra")
    public ModelAndView  checkout(){

        ModelAndView modelAndView = new ModelAndView("compra/pagamento");

        modelAndView.addObject("carrinho", carrinho);
        return modelAndView;
    }

Precisaremos ter no nosso carrinho um método para poder disponibilizar o valor total para que ele seja exibido na tela. Primeiramente obteremos todos os valores dos ingressos do carrinho usando um objeto Stream proveniente da lista de ingressos. Então, através do método map poderemos invocar o getPreco de cada um dos ingressos, obtendo assim um Stream<Bigdecimal>. Usaremos, novamente, um método da classe Stream para obtermos a somatória de todos os valores contidos no mesmo :

public BigDecimal getTotal(){
    return ingressos.stream().map(Ingresso::gerPreco).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
}

Estamos pegando todos os valores, somando e então devolvendo apenas o resultado, caso não tenha nenhum valor para ser somado, estamos devolvendo um BigDecimal de valor zero.

6.6 Exercício - Implementando a tela de checkout

  1. Na classe CompraController adicione uma action para /compra e disponibilize o carrinho para a jsp:

     @GetMapping("/compra")
     public ModelAndView  checkout(){
    
         ModelAndView modelAndView = new ModelAndView("compra/pagamento");
    
         modelAndView.addObject("carrinho", carrinho);
         return modelAndView;
     }
    
  2. Altere a classe carrinho e adicione um método que retorne o total a pagar:

     public BigDecimal getTotal(){
         return ingressos.stream().map(Ingresso::getPreco).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
     }
    
  3. Reinicie o sistema, selecione um ingresso para alguma sessão, precione o botão para finalizar a compra e visualize a exibição da tela de detalhes da compra.

6.7 Realizando a compra

Ainda é necessário podermos finalizar a compra. para isso precisaremos adicionar o cartão como forma pagamento. Criaremos uma classe para representa-lo :

public class Cartao {

    private String numero;
    private Integer cvv;
    private YearMonth vencimento;


    //getters e setters
}

Nosso controller precisa saber que assim que ele entra na página de pagamento ele precisa ter um cartão, para isso faremos com que ele nos forneça um cartão :


@GetMapping("/compra")
public ModelAndView  checkout(Cartao cartao){
    //restante do código
}

Como nosso data de vencimento do cartão é representada pelo objeto YearMonth e na nossa tela inserimos apenas com String, precisamos informar como deve ser feita a conversão. Para isso será necessário criarmos nosso próprio conversor, o que pode ser feito implementando a interface Converter :

public class YearMonthConverter implements Converter<String, YearMonth> {

    @Override
    public YearMonth convert(String text) {
        return YearMonth.parse(text, DateTimeFormatter.ofPattern("MM/yyyy"));
    }
}

Com nosso conversor criado, precisamos avisar ao Spring que ele existe, para isso teremos que fazer algumas mudanças no nosso spring-context.xml :

<mvc:annotation-driven conversion-service="conversionService"/>
    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <property name="converters">
            <set merge="true">
                <bean class="br.com.caelum.ingresso.converter.YearMonthConverter"/>
            </set>
        </property>
    </bean>

Para saber se o cartão informado pelo usuário é válido, podemos criar um pequeno método de validação no mesmo, que checará se a data de expiração já está vencida ou não:

    public boolean isValido(){
        return vencimento.isAfter(YearMonth.now());
    }

Criaremos um objeto que represente a compra dos ingressos em si, para que possamos persistí-la:

@Entity
public class Compra {

    @Id
    @GeneratedValue
    private Long id;

    @OneToMany(cascade = CascadeType.PERSIST)
    List<Ingresso> ingressos = new ArrayList<>();

    /**
    * @deprecated hibernate only
    */
    public Compra() {
    }

    public Compra(List<Ingresso> ingressos) {
        this.ingressos = ingressos;
    }

    // getters e setters
}

Como a compra é realizada através dos métodos do próprio Carrinho, podemos fazer com que o mesmo já nos forneça um objeto do tipo Compra baseado nos ingressos já inseridos no carrinho pelo cliente:

  public class Carrinho{

    // Atributos e demais métodos omitidos

    public Compra toCompra(){
        return  new Compra(ingressos);
    }
  }

Por fim, basta criarmos um novo método no nosso CompraController que irá realizar a persistência da compra do usuário quando o mesmo finalizar a compra na tela:

    @PostMapping("/compra/comprar")
    @Transactional
    public ModelAndView comprar(@Valid  Cartao cartao, BindingResult result){
        ModelAndView modelAndView = new ModelAndView("redirect:/");

        if (cartao.isValido()){
            compraDao.save(carrinho.toCompra());
        }else{
            result.rejectValue("vencimento", "Vencimento inválido");
            return checkout(cartao);
        }

        return modelAndView;
    }

Editora Casa do Código com livros de uma forma diferente

Editoras tradicionais pouco ligam para ebooks e novas tecnologias. Não dominam tecnicamente o assunto para revisar os livros a fundo. Não têm anos de experiência em didáticas com cursos.
Conheça a Casa do Código, uma editora diferente, com curadoria da Caelum e obsessão por livros de qualidade a preços justos.

Casa do Código, ebook com preço de ebook.

6.8 Exercícios - Implementando a compra

  1. Crie a classe Cartao no pacote br.com.caelum.ingresso.model:

     public class Cartao {
    
         private String numero;
         private Integer cvv;
         private YearMonth vencimento;
    
         //getters e setters
     }
    
  2. Faça com que o método checkout da classe CompraController receba um objeto Cartao:

     @GetMapping("/compra")
     public ModelAndView  checkout(Cartao cartao){
    
         ModelAndView modelAndView = new ModelAndView("compra/pagamento");
    
         modelAndView.addObject("carrinho", carrinho);
         return modelAndView;
     }
    
  3. Crie o conversor para YearMonth no pacote br.com.caelum.ingresso.converter:

     public class YearMonthConverter implements Converter<String, YearMonth> {
    
         @Override
         public YearMonth convert(String text) {
             return YearMonth.parse(text, DateTimeFormatter.ofPattern("MM/yyyy"));
         }
     }
    
  4. Altere a declaração da tag annotation-driven no arquivo spring-context.xml:

     <mvc:annotation-driven conversion-service="conversionService"/>
    
  5. Registre o conversor no spring-context.xml:

     <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
         <property name="converters">
             <set merge="true">
                 <bean class="br.com.caelum.ingresso.converter.YearMonthConverter"/>
             </set>
         </property>
     </bean>
    
  6. Crie a classe Compra no pacote br.com.caelum.ingresso.model:

     @Entity
     public class Compra {
    
         @Id
         @GeneratedValue
         private Long id;
    
         @OneToMany(cascade = CascadeType.PERSIST)
         List<Ingresso> ingressos = new ArrayList<>();
    
         /**
         * @deprecated hibernate only
         */
         public Compra() {
         }
    
         public Compra(List<Ingresso> ingressos) {
             this.ingressos = ingressos;
         }
    
         public Long getId() {
             return id;
         }
    
         public void setId(Long id) {
             this.id = id;
         }
    
         public List<Ingresso> getIngressos() {
             return ingressos;
         }
    
         public void setIngressos(List<Ingresso> ingressos) {
             this.ingressos = ingressos;
         }
     }
    
  7. Adicione um método no carrinho para criar uma compra:

     public Compra toCompra(){
         return  new Compra(ingressos);
     }
    
  8. Adicione um método no cartão que retorne se o cartão é válido ou não:

     public boolean isValido(){
         return vencimento.isAfter(YearMonth.now());
     }
    
  9. Adicione uma action em CompraController que deverá atender a URI /compra/comprar e nele implemente a persistencia da compra:

     @PostMapping("/compra/comprar")
     @Transactional
     public ModelAndView comprar(@Valid  Cartao cartao, BindingResult result){
         ModelAndView modelAndView = new ModelAndView("redirect:/");
    
         if (cartao.isValido()){
             compraDao.save(carrinho.toCompra());
         }else{
             result.rejectValue("vencimento", "Vencimento inválido");
             return checkout(cartao);
         }
    
         return modelAndView;
     }
    
  10. Injete um objeto CompraDao em CompraController:

     @Controller
     public class CompraController {
    
         @Autowired
         private SessaoDao sessaoDao;
         @Autowired
         private LugarDao lugarDao;
    
         @Autowired
         private CompraDao compraDao;
    
         @Autowired
         private Carrinho carrinho;
    
         //demais métodos
     }
    
  11. Como ainda não criamos a classe CompraDao, crie-a no pacote br.com.caelum.ingresso.dao para corrigir os erros existentes:

     @Repository
     public class CompraDao {
    
         @PersistenceContext
         private EntityManager manager;
    
         public void save(Compra compra) {
             manager.persist(compra);
         }
     }
    
  12. Adicione na página pagamento.jsp o seguinte div antes do botão de submit :

     <div class="form-group">
         <div class="col-md-6">
             <label for="vencimento">Vencimento:</label>
             <input id="vencimento" type="text" name="vencimento" class="form-control">
         </div>˜
     </div>
    
  13. Rode a aplicação e verifique se a compra está sendo persistida.