Capítulo 5

Iniciando o processo de Venda

5.1 Criando tela para escolha de lugar

Naturalmente o usuário do nosso sistema vai desejar comprar um ingresso. Para isso, precisamos deixar disponível para ele quais são os lugares que ele pode comprar assim que ele decidir qual será a sessão que ele deseja ver.

Criaremos então uma nova tela que exibirá alguns detalhes do filme, desta forma:

Para chegarmos nesse resultado, teremos que fazer algumas refatorações no nosso sistema. A primeira é reaproveitar a nossa classe ImdbClient para continuar pegando as informações dos filmes, contudo, na segunda tela só queremos pegar o banner. Devemos então criar outra classe que vai mapear exatamente o que queremos:

public class ImagemCapa {

    @JsonProperty("Poster")
    String url;

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }
}

Precisamos deixar a classe ImdbClient genérica, ou seja, fazer com que ela possa devolver tanto o DetalhesDoFilme quanto a nossa nova classe ImagemCapa, para isso temos que fazer essa sútil alteração :

public  <T> Optional<T>  request(Filme filme, Class<T> tClass){

    RestTemplate client = new RestTemplate();

    String titulo = filme.getNome().replace(" ", "+");

    String url = String.format("https://imdb-fj22.herokuapp.com/imdb?title=%s", titulo);

    try {
        return Optional.of(client.getForObject(url, tClass));
    }catch (RestClientException e){
        logger.error(e.getMessage(), e);
        return Optional.empty();
    }
}

Agora precisamos disponibilizar essas informações para a tela:


@GetMapping("/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("imagemCapa", imagemCapa.orElse(new ImagemCapa()));

    return modelAndView;
}

Vamos deixar os lugares disponíveis para a tela, para isso precisamos fazer uma pequena alteraçao na Sessão para que ela forneça para a tela os lugares:

public Map<String, List<Lugar>> getMapaDeLugares(){
    return sala.getMapaDeLugares();
}

E por fim vamos disponibilizar nossos lugares para a tela :



@GetMapping("/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("imagemCapa", imagemCapa.orElse(new ImagemCapa()));

    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.

5.2 Exercício - Criando tela para seleção de lugares

  1. Dentro da classe SessaoController, crie uma action para acessar /sessao/{id}/lugares:

     @GetMapping("/sessao/{id}/lugares")
     public ModelAndView lugaresNaSessao(@PathVariable("id") Integer sessaoId){
         ModelAndView modelAndView = new ModelAndView("sessao/lugares");
    
         return modelAndView;
     }
    
  2. Adicione o método para buscar a sessão por id:

     @Repository
     public class SessaoDao {
    
       //Demais métodos omitidos
    
       public Sessao findOne(Integer id) {
           return manager.find(Sessao.class, id);
       }
     }
    
  3. Disponibilize a sessão para a jsp:

     @GetMapping("/sessao/{id}/lugares")
     public ModelAndView lugaresNaSessao(@PathVariable("id") Integer sessaoId){
         ModelAndView modelAndView = new ModelAndView("sessao/lugares");
    
         Sessao sessao = sessaoDao.findOne(sessaoId);
    
         modelAndView.addObject("sessao", sessao);
    
         return modelAndView;
     }
    
  4. Crie a classe ImagemCapa no pacote de br.com.caelum.ingresso.modelo. Usaremos ela para pegar somente a imagem de capa do filme retornado pela api https://imdb-fj22.herokuapp.com/imdb?title=NOME+DO+FILME:

     public class ImagemCapa {
    
         @JsonProperty("Poster")
         String url;
    
         public String getUrl() {
             return url;
         }
    
         public void setUrl(String url) {
             this.url = url;
         }
     }
    
  5. Altere a classe ImdbClient no pacote br.com.caelum.ingresso.rest para que ela seja genérica, ou seja indiferente da representação (DetalheDoFilme ou ImagemCapa), dessa forma será possível consumir a api e retornar de forma correta:

     @Component
     public class ImdbClient {
    
         private Logger logger = Logger.getLogger(ImdbClient.class);
    
         public  <T> Optional<T>  request(Filme filme, Class<T> tClass){
    
             RestTemplate client = new RestTemplate();
    
             String titulo = filme.getNome().replace(" ", "+");
    
             String url = String.format("https://imdb-fj22.herokuapp.com/imdb?title=%s", titulo);
    
             try {
                 return Optional.of(client.getForObject(url, tClass));
             }catch (RestClientException e){
                 logger.error(e.getMessage(), e);
                 return Optional.empty();
             }
         }
    
     }
    
  6. Com essa alteração, quebramos o método detalhes na classe FilmeController. Vamos corrigi-lo informando qual a classe será utilizada no método request:

     @GetMapping("/filme/{id}/detalhe")
     public ModelAndView detalhes(@PathVariable("id") Integer id){
         ModelAndView modelAndView = new ModelAndView("/filme/detalhe");
    
         Filme filme = filmeDao.findOne(id);
         List<Sessao> sessoes = sessaoDao.buscaSessoesDoFilme(filme);
    
         Optional<DetalhesDoFilme> detalhesDoFilme = client.request(filme, DetalhesDoFilme.class);
    
         modelAndView.addObject("sessoes", sessoes);
         modelAndView.addObject("detalhes", detalhesDoFilme.orElse(new DetalhesDoFilme()));
    
         return modelAndView;
     }
    
  7. Vamos alterar o método lugaresNaSessao da classe SessaoController para disponibilizar a imagem de capa para nossa jsp. Não se esqueça de injetar o ImdbClient :

     @GetMapping("/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("imagemCapa", imagemCapa.orElse(new ImagemCapa()));
    
         return modelAndView;
     }
    
  8. Crie o método getMapaDeLugares na classe Sessao, para podermos disponibilizar os lugares para tela :

     public Map<String, List<Lugar>> getMapaDeLugares(){
         return sala.getMapaDeLugares();
     }
    
  9. Acesse a página de detalhes de algum filme e clique no botão para comprar alguma sessão disponível. A tela de seleção de lugares deverá ser exibida corretamente.

5.3 Selecionando Ingresso

Agora precisamos deixar a opção de nosso usuário poder de fato comprar o seu ingresso, contudo ainda precisamos definir qual é o tipo de Ingresso que ele está querendo comprar, com seu respectivo desconto.

Para sabermos disso vamos deixar mais evidente para o usuário, primeiro vamos criar um Enum que tenha todos esses tipo :

public enum TipoDeIngresso {

    INTEIRO(new SemDesconto()),
    ESTUDANTE(new DescontoEstudante()),
    BANCO(new DescontoDeTrintaPorCentoParaBancos());

    private final Desconto desconto;

    TipoDeIngresso(Desconto desconto) {
        this.desconto = desconto;
    }

    public BigDecimal aplicaDesconto(BigDecimal valor){
        return desconto.aplicarDescontoSobre(valor);
    }

    public String getDescricao(){
        return desconto.getDescricao();
    }

}

Precisamos agora definir na nossa interface Desconto o método getDescricao() para que todas as classes que implementem sejam obrigadas a ter este método :

public interface Desconto {

    BigDecimal aplicarDescontoSobre(BigDecimal precoOriginal);
    String getDescricao();
}

Desta forma, precisamos fazer uma pequena alteração na nossa classe Ingresso, para que tenha o TipoDeIngresso invés do Desconto :



public class Ingresso {

    private Sessao sessao;

    private BigDecimal preco;

    public Ingresso(Sessao sessao, TipoDeIngresso tipoDeDesconto) {
        this.sessao = sessao;
        this.preco = tipoDeDesconto.aplicarDescontoSobre(sessao.getPreco());

    }

    // getters
}

Além disso nosso usuário vai precisar ter no seu ingresso, qual foi o Lugar que ele comprou, para isso vamos adicionar esse atributo ao no Ingresso :



public class Ingresso {

    private Sessao sessao;

    private BigDecimal preco;

    private Lugar lugar;

    public Ingresso(Sessao sessao, TipoDeIngresso tipoDeDesconto, Lugar lugar) {
        this.sessao = sessao;
        this.preco = tipoDeDesconto.aplicarDescontoSobre(sessao.getPreco());
        this.lugar = lugar;

    }

    // getters
}

5.4 Exercício - Implementando a seleção de lugares, ingressos e tipo de ingressos.

  1. Nosso ingresso além de ter um preço e uma sessão deve ter um lugar. Altere a classe Ingresso, adicione um atributo para o Lugar e receba-o no construtor:

     public class Ingresso {
    
     private Sessao sessao;
    
     private Lugar lugar;
    
     private BigDecimal preco;
    
     public Ingresso(Sessao sessao, Desconto tipoDeDesconto, Lugar lugar) {
         this.sessao = sessao;
         this.preco = tipoDeDesconto.aplicarDescontoSobre(sessao.getPreco());
         this.lugar = lugar;
     }
    
     // getters
    
  2. Adicione na interface Desconto um método para pegar a descrição do desconto:

     public interface Desconto {
    
         BigDecimal aplicarDescontoSobre(BigDecimal precoOriginal);
         String getDescricao();
     }
    
  3. Implemente o método getDescricao em todos os descontos:

     public class SemDesconto implements Desconto {
    
         @Override
         public String getDescricao() {
             return "Normal";
         }
     }
    
     public class DescontoEstudante implements Desconto {
    
         @Override
         public String getDescricao() {
             return "Estudante";
         }
     }
    
     public class DescontoDeTrintaPorCentoParaBancos implements Desconto {
    
         @Override
         public String getDescricao() {
             return "Desconto Banco";
         }
     }
    
  4. Ao invés de receber um desconto nosso ingresso deve receber um TipoDeIngresso, crie esse enum no pacote br.com.caelum.ingresso.model:

     public enum TipoDeIngresso {
    
         INTEIRO(new SemDesconto()),
         ESTUDANTE(new DescontoEstudante()),
         BANCO(new DescontoDeTrintaPorCentoParaBancos());
    
         private final Desconto desconto;
    
         TipoDeIngresso(Desconto desconto) {
             this.desconto = desconto;
         }
    
         public BigDecimal aplicaDesconto(BigDecimal valor){
             return desconto.aplicarDescontoSobre(valor);
         }
    
         public String getDescricao(){
             return desconto.getDescricao();
         }
    
     }
    
  5. Crie um atributo TipoDeIngresso na classe Ingresso e altere o ingresso para receber um TipoDeIngresso ao invés do desconto.

     public class Ingresso {
    
         private Sessao sessao;
    
         private Lugar lugar;
    
         private BigDecimal preco;
    
         private TipoDeIngresso tipoDeIngresso;
    
         public Ingresso(Sessao sessao, TipoDeIngresso tipoDeIngresso, Lugar lugar) {
                 this.sessao = sessao;
                 this.tipoDeIngresso = tipoDeIngresso;
                 this.preco = this.tipoDeIngresso.aplicaDesconto(sessao.getPreco());
    
                 this.lugar = lugar;
             }
    
             //demais métodos
    
     }
    
  6. Como vamos persistir um ingresso no banco de dados para simular uma compra do usuário, adicione os mapeamentos do Hibernate nos atributos da classe. Inclua, também, um atributo do tipo Integer para representar o id do Ingresso:

     @Entity
     public class Ingresso {
    
         @Id
         @GeneratedValue
         private Integer id;
    
         @ManyToOne
         private Sessao sessao;
    
         @ManyToOne
         private Lugar lugar;
    
         private BigDecimal preco;
    
         @Enumerated(EnumType.STRING)
         private TipoDeIngresso tipoDeIngresso;
    
         public Ingresso(Sessao sessao, TipoDeIngresso tipoDeIngresso, Lugar lugar) {
                 this.sessao = sessao;
                 this.tipoDeIngresso = tipoDeIngresso;
                 this.preco = this.tipoDeIngresso.aplicaDesconto(sessao.getPreco());
                 this.lugar = lugar;
             }
    
             //demais métodos
    
     }
    
  7. Após a última alteração os testes da classe DescontoTeste quebraram por conta do construtor. Corrija os testes fazendo-os receber um TipoDeIngresso e um Lugar no construtor do ingresso:

     public class DescontoTest {
    
         @Test
         public void deveConcederDescontoDe30PorcentoParaIngressosDeClientesDeBancos(){
    
             Lugar lugar = new Lugar("A",1);
             Sala sala = new Sala("Eldorado - IMAX", new BigDecimal("20.5"));
             Filme filme = new Filme("Rogue One", Duration.ofMinutes(120),  
                             "SCI-FI", new BigDecimal("12"));
             Sessao sessao = new Sessao(LocalTime.parse("10:00:00"), filme, sala);
             Ingresso ingresso = new Ingresso(sessao, TipoDeIngresso.BANCO, lugar);
    
             BigDecimal precoEsperado = new BigDecimal("22.75");
    
             Assert.assertEquals(precoEsperado, ingresso.getPreco());
    
         }
    
         @Test
         public void deveConcederDescontoDe50PorcentoParaIngressoDeEstudante(){
    
             Lugar lugar = new Lugar("A",1);
             Sala sala = new Sala("Eldorado - IMAX", new BigDecimal("20.5"));
             Filme filme = new Filme("Rogue One", Duration.ofMinutes(120),  
                             "SCI-FI", new BigDecimal("12"));
             Sessao sessao = new Sessao(LocalTime.parse("10:00:00"), filme, sala);
             Ingresso ingresso = new Ingresso(sessao, TipoDeIngresso.ESTUDANTE, lugar);
    
             BigDecimal precoEsperado = new BigDecimal("16.25");
    
             Assert.assertEquals(precoEsperado, ingresso.getPreco());
    
         }
    
         @Test
         public void naoDeveConcederDescontoParaIngressoNormal(){
             Lugar lugar = new Lugar("A",1);
             Sala sala = new Sala("Eldorado - IMAX", new BigDecimal("20.5"));
             Filme filme = new Filme("Rogue One", Duration.ofMinutes(120),  
                             "SCI-FI", new BigDecimal("12"));
             Sessao sessao = new Sessao(LocalTime.parse("10:00:00"), filme, sala);
             Ingresso ingresso = new Ingresso(sessao, TipoDeIngresso.INTEIRO,lugar);
    
             BigDecimal precoEsperado = new BigDecimal("32.5");
    
             Assert.assertEquals(precoEsperado, ingresso.getPreco());
    
         }
     }
    
  8. Rode os testes e garanta que todos estão funcionando corretamente.

  9. Precisamos saber quais lugares já foram ocupados em uma determinada sessão. Vamos adicionar um atributo do tipo Set<Ingresso> chamado ingressos na classe Sessao, para obtermos todos os ingressos que já foram vendidos para uma determinada sessão e seus respectivos lugares. Adicione no novo atributo a anotação @OneToMany para mapea-lo para o Hibernate, bem como as propriedades da anotação mappedBy="sessao" e fetch=FetchType.EAGER, para dizer que a relação já foi mapeada pelo atributo sessao da classe Ingresso e para o Hibernate já trazer os ingressos do banco de dados quando buscarmos por uma Sessao, respectivamente.

     @Entity
     public class Sessao{
       //Demais atributos omitidos
    
       @OneToMany(mappedBy = "sessao", fetch = FetchType.EAGER)
       private Set<Ingresso> ingressos = new HashSet<>();
    
       //Demais métodos omitidos
     }
    
  10. Crie um método isDisponivel na classe Sessao, que deve verificar se o lugar está ou não disponivel:

     public boolean isDisponivel(Lugar lugarSelecionado) {
         return ingressos.stream().map(Ingresso::getLugar).noneMatch(lugar -> lugar.equals(lugarSelecionado));
     }
    
  11. Vamos criar um teste na classe SessaoTest que garanta que nossa implementação do método isDisponivel está correto:

     @Test
     public void garanteQueOLugarA1EstaOcupadoEOsLugaresA2EA3Disponiveis(){
    
         Lugar a1 = new Lugar("A", 1);
         Lugar a2 = new Lugar("A", 2);
         Lugar a3 = new Lugar("A", 3);
    
         Filme rogueOne = new Filme("Rogue One", Duration.ofMinutes(120),  
                         "SCI_FI", new BigDecimal("12.0"));
    
         Sala eldorado7 = new Sala("Eldorado 7", new BigDecimal("8.5"));
    
         Sessao sessao = new Sessao(LocalTime.parse("10:00:00"), rogueOne, eldorado7);
    
         Ingresso ingresso = new Ingresso(sessao, TipoDeIngresso.INTEIRO, a1);
    
         Set<Ingresso> ingressos = Stream.of(ingresso).collect(Collectors.toSet());
    
         sessao.setIngressos(ingressos);
    
         Assert.assertFalse(sessao.isDisponivel(a1));
         Assert.assertTrue(sessao.isDisponivel(a2));
         Assert.assertTrue(sessao.isDisponivel(a3));
     }
    
  12. Altere o método lugaresNaSessao na SessaoController e disponibilize para a jsp a lista de tipos de ingressos, para seleção de tipos de ingressos:

     @Controller
     public class SessaoController {
    
         //demais métodos
    
         @GetMapping("/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("imagemCapa", imagemCapa.orElse(new ImagemCapa()));
             modelAndView.addObject("tiposDeIngressos", TipoDeIngresso.values());
    
             return modelAndView;
         }
     }
    

Já conhece os cursos online Alura?

A Alura oferece centenas de cursos online em sua plataforma exclusiva de ensino que favorece o aprendizado com a qualidade reconhecida da Caelum. Você pode escolher um curso nas áreas de Programação, Front-end, Mobile, Design & UX, Infra e Business, com um plano que dá acesso a todos os cursos. Ex aluno da Caelum tem 15% de desconto neste link!

Conheça os cursos online Alura.