Minicart | Frete grátis valor mínimo com base no CEP

Eai pessoal,
Gostaria de entender como funcionaria para exibir no minicart o valor mínimo a ser gasto para o cliente ganhar frete grátis com base no CEP (região específica).

Levantei algumas possibilidades mas gostaria de entender a melhor prática para fazer essa execução:

  • Usar a API Sessions para salvar o CEP da pessoa (pedir dentro do minicart ou em um modal ao acessar o site).

  • Criar promoções com o valor mínimo para frete em um range de CEPs ou regiões. Consumir no backend a API de Promotions para consultar o valor mínimo a ser gasto para que a promoção seja ativada.

  • Criar campanhas que façam essa condição de preço mínimo e salvo os valores por região hard code no meu projeto ou no master data e quando precisar alterar, altero a campanha e também os valores de preço no master data ou no projeto e subo uma nova versão.

O ponto do backend, no caso teria que criar um app de backend como um middleware para fazer esse processo visto que usar os tokens de api para a promotions do lado do front seria um problema de segurança, certo? Se for hard code o problema é que se o cliente alterar o valor do preço mínimo na vtex, isso não iria refletir a não ser que eu consulte de um master data.

Portanto, destaquei algumas ideias mas gostaria de entender qual a melhor maneira de executar isso.

Obrigado!

Opa @caiquelins tranquilo?

Dentro do Minicart você pode usar o próprio APP para fazer a custom. Não sei se precisa usar a API Sessions.

Veja esse doc: GitHub - vtex-apps/minicart-freeshipping-bar

Para o Modal provavelmente você vai usar o simulation para realizar o cálculo. https://developers.vtex.com/docs/api-reference/marketplace-protocol-external-marketplace-orders?endpoint=post-/api/checkout/pub/orderForms/simulation

Além disso, para salvar na sessão do usuário você pode usar a session, mas creio que não precisa usar API privada para isso, pois os inputs são públicos Session data available from VTEX apps

Veja se essas informações fazem sentido para o que você está tentando fazer.

Abs,
Estevão.

1 Like

Opa Estevão, obrigado pelo comentário. Análisei os processos, consegui fazer a captura do CEP e setar ele tanto com a API Sessions quanto no próprio orderform, porém o grande problema é identificar quais são os valores mínimos de gasto por região com base no frete, para adicionar na barra visual para o cliente.

Acessei o repositório do minicart mas fiquei com pé atrás de reproduzir, devido ao mesmo não ter mais suporte. A observação no readme do repositório é:
Starting June 1st 2023, this application will no longer be maintained by VTEX. The VTEX repository shall remain archived private and unchanged with the original version. Please don’t share this repository with anyone external to VTEX.

Portanto, o grande desafio está em obter os valores para que o frete seja ativado. Por isso havia pensado em usar a API de promotions, mas com isso, não posso consumir no front-end, pois com os tokens de api sendo expostos no front, isso acarretaria em um problema de segurança.

Teria outra ideia de como eu obtenho esses valores ou o jeito será partir para um middleware em back-end?

Obrigado!

Opa @caiquelins por nada!

Entendi. Mesmo a VTEX colocando esse alerta, este APP é um dos mais simples que você pode usar para fazer a regra do valor mínimo, pois ele usa a customização de animação da régua que já está direto nele, bastando só instalar e usar.

CRIE SEU PRÓPRIO COMPONENTE FRONTEND
Outro meio seria você criar seu próprio “mini app custom” dentro minicart apenas para capturar o valor mínimo para você usar como referência do cálculo. Você usaria o minicart normalmente que é o componente nativo, mas poderia criar um componente com Graphql para puxar campos customizados + cssHandle para poder exibir a animação.

Aqui vou deixar um exemplo, no entanto, eu peguei de referências de outros projetos + apoio com IA e você precisa readaptar tudo.

É somente uma ideia.

"settingsSchema": {
  "title": "Configurações do Frete Grátis",
  "type": "object",
  "properties": {
    "freeShippingThreshold": {
      "title": "Valor mínimo para frete grátis",
      "type": "number",
      "default": 399
    }
  }
}

Em seguida, você pode acessar essa configuração em seu componente React usando a função useRuntime do vtex.render-runtime:

import React from 'react';
import { useQuery } from '@apollo/react-hooks';
import { useRuntime } from 'vtex.render-runtime';
import gql from 'graphql-tag';

const GET_CART_VALUE = gql`
  query {
    orderForm {
      value
    }
  }
`;

const FreeShippingBar = () => {
  const { loading, error, data } = useQuery(GET_CART_VALUE);
  const { settings } = useRuntime();

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;

  const cartValue = data.orderForm.value;
  const freeShippingThreshold = settings['myAppName'].freeShippingThreshold;

  return (
    <div>
      {cartValue >= freeShippingThreshold ? (
        <p>Parabéns! Você ganhou frete grátis!</p>
      ) : (
        <p>Faltam R${freeShippingThreshold - cartValue} para você ganhar frete grátis!</p>
      )}
    </div>
  );
};

export default FreeShippingBar;

Sim, o componente FreeShippingBar deve ser colocado na pasta react/components do seu aplicativo VTEX IO.

A estrutura de pastas do seu APP pode se parecer com isto:

├── manifest.json
└── react
    ├── components
    │   └── FreeShippingBar.js
    └── index.js

No arquivo index.js, você deve importar e exportar o componente FreeShippingBar para que ele possa ser usado em outros lugares do seu aplicativo. Por exemplo:

import FreeShippingBar from './components/FreeShippingBar';

export default FreeShippingBar;

NO BACKEND
Optar pelo Backend seria uma customização muito complexa e não teria tanta vantagem entre tempo de execução vs benefício, porém também é um caminho.

Eu continuo sugerindo você seguir pelo frontend mesmo.

Abs,
Estevão.

Opa Estevão

Tentei sua sugestão mas não rolou :confused:
Pelo runtime só é possível trazer dados estáticos e não consigo obter dados de promoções dessa forma, notei que ao invés de setting, é getSettings, mas não retorna o que eu preciso. A doc do runtime é essa: GitHub - vtex-apps/render-runtime: The VTEX Render framework runtime

Atualmente já possuo o minicart com a lógica da barra de gasto para frete:

O problema é que está travado em R$ 700,00. O ideal é que com base no CEP, ele identificasse a informação correta de qual valor mínimo para cada região e não algo estático. Bom, tudo me leva a crer que terei que fazer um app back-end para isso.

Como complemento, achei esse artigo que executou algo parecido: Case funcionalidade VTEX IO: Free shipping infos – Codeby

Nele tem a seguinte informação:
Por isso, o Free shipping infos busca no back-end a promoção atrelada a região que o usuário se encontra e então é feita a verificação do valor que falta para o usuário receber o benefício. No front-end, é mostrado ao usuário no minicart, ou em qualquer outra página de preferência do lojista, uma régua de frete que mostra a evolução da compra até atingir o benefício de frete grátis, baseado no valor adicionado no carrinho.

1 Like

@caiquelins tudo certo?

Este valor está definido no APP dentro do admin nos apps instalados. Você pode alterar. Por exemplo:

Agora, no caso do valor mínimo para cada região, também é possível fazer no frontend.

Ex:

Régua Frete Grátis ativa no Minicart?
Sim ou Não

Régua Frete Grátis ativa no Checkout?
Sim ou Não

Blocos das regiões com valor mínimo de cada uma:
Região Sul
Valor mínimo: 300,00 (é possível trocar via ADMIN após o app custom ser instalado)
Ativo: Sim ou Não

Região Norte
Valor mínimo: 299,00 (é possível trocar via ADMIN após o app custom ser instalado)
Ativo: Sim ou Não

Essas ideias que te enviei fazem parte de uma versão customizada que conheço e não precisa do Backend. Eu sei que tem como realizar somente pelo frontend. Não posso te abrir muito porque é um app privado.

Mas se você quiser seguir pelo Backend, como mencionei também é possível conforme você já confirmou que conhece.

Abs,
Estevão.

Opa @estevao_santos obrigado pelos comentários, havia esquecido de retornar aqui para mostrar como resolvi esse problema e até para servir com documentação para outros usuários.

Eu já tinha uma lógica semelhante ao do app que tinha uma barra de progresso com base no valor gasto, o que fiz para ter o preço correto?

1 - Dentro do manifest.json, eu criei o objeto separado por regiões e estados, para receber o valor mínimo para frete grátis:

"settingsSchema": {
    "title": "Configuração | Minicart Valor Mínimo para Frete Grátis",
    "type": "object",
    "access": "public",
    "bindingBounded": true,
    "properties": {
      "freeShippingRegions": {
        "title": "Frete Grátis por Região",
        "type": "object",
        "properties": {
          "Norte": {
            "title": "Norte",
            "type": "object",
            "properties": {
              "AC": {
                "title": "Acre (AC)",
                "type": "number"
              },
              "AP": {
                "title": "Amapá (AP)",
                "type": "number"
              },
              "AM": {
                "title": "Amazonas (AM)",
                "type": "number"
              },
              "PA": {
                "title": "Pará (PA)",
                "type": "number"
              },
              "RO": {
                "title": "Rondônia (RO)",
                "type": "number"
              },
              "RR": {
                "title": "Roraima (RR)",
                "type": "number"
              },
              "TO": {
                "title": "Tocantins (TO)",
                "type": "number"
              }
            }
          },
          "Nordeste": {
            "title": "Nordeste",
            "type": "object",
            "properties": {
              "AL": {
                "title": "Alagoas (AL)",
                "type": "number"
              },
              "BA": {
                "title": "Bahia (BA)",
                "type": "number"
              },
              "CE": {
                "title": "Ceará (CE)",
                "type": "number"
              },
              "MA": {
                "title": "Maranhão (MA)",
                "type": "number"
              },
              "PB": {
                "title": "Paraíba (PB)",
                "type": "number"
              },
              "PE": {
                "title": "Pernambuco (PE)",
                "type": "number"
              },
              "PI": {
                "title": "Piauí (PI)",
                "type": "number"
              },
              "RN": {
                "title": "Rio Grande do Norte (RN)",
                "type": "number"
              },
              "SE": {
                "title": "Sergipe (SE)",
                "type": "number"
              }
            }
          },
          "Centro-Oeste": {
            "title": "Centro-Oeste",
            "type": "object",
            "properties": {
              "DF": {
                "title": "Distrito Federal (DF)",
                "type": "number"
              },
              "GO": {
                "title": "Goiás (GO)",
                "type": "number"
              },
              "MT": {
                "title": "Mato Grosso (MT)",
                "type": "number"
              },
              "MS": {
                "title": "Mato Grosso do Sul (MS)",
                "type": "number"
              }
            }
          },
          "Sudeste": {
            "title": "Sudeste",
            "type": "object",
            "properties": {
              "ES": {
                "title": "Espírito Santo (ES)",
                "type": "number"
              },
              "MG": {
                "title": "Minas Gerais (MG)",
                "type": "number"
              },
              "RJ": {
                "title": "Rio de Janeiro (RJ)",
                "type": "number"
              },
              "SP": {
                "title": "São Paulo (SP)",
                "type": "number"
              }
            }
          },
          "Sul": {
            "title": "Sul",
            "type": "object",
            "properties": {
              "PR": {
                "title": "Paraná (PR)",
                "type": "number"
              },
              "RS": {
                "title": "Rio Grande do Sul (RS)",
                "type": "number"
              },
              "SC": {
                "title": "Santa Catarina (SC)",
                "type": "number"
              }
            }
          }
        }
      }
    }
  },

2 - Criei uma query graphql que faz busca em vtex.apps-graphql:

query AppSettings {
  publicSettingsForApp(app: "colocar_vendor_e_name_aqui", version: "x")
    @context(provider: "vtex.apps-graphql") {
    message
  }
}

3 - usei o endpoint que é usado no checkout para busca o cep:
/api/checkout/pub/postal-code/BRA/CEP_AQUI

4 - Fiz o uso da api do checkout (attach) para adicionar o endereço após digitar o cep:
/api/checkout/pub/orderForm/${orderForm?.id}/attachments/shippingData

A implementação total ficou mais ou menos assim:

/* eslint-disable no-restricted-globals */
import React, { useState, useEffect } from 'react';
import { useOrderForm } from 'vtex.order-manager/OrderForm';
import { useCssHandles } from 'vtex.css-handles';
import { useQuery } from 'react-apollo';

import MinicartFreightBarHandles from './MinicartFreightBar.handles';
import SearchIcon from '../Icons/SearchIcon';
import AppSettings from './minicartbarSettings.graphql';
import formatMoney from '../../utils/formatMoney';
import styles from './MinicartFreeshipping.css';

const MinicartFreightBar = () => {
  const [cep, setCep] = useState('');
  const [state, setState] = useState('');
  const [freeShippingAmount, setFreeShippingAmount] = useState(0);
  const [isNewCep, setIsNewCep] = useState(false);
  const minValue = freeShippingAmount;

  const { orderForm } = useOrderForm();
  const [remainingValue, setRemainigValue] = useState(0);
  const [percentage, setPercentage] = useState(0);

  const { handles } = useCssHandles(MinicartFreightBarHandles);

  const { data } = useQuery(AppSettings, {
    ssr: false,
    fetchPolicy: 'no-cache',
  });

  useEffect(() => {
    if (!orderForm) return;

    if (orderForm?.shipping?.selectedAddress?.state && !isNewCep) {
      setState(orderForm?.shipping?.selectedAddress?.state);
    }

    const minicartTotalValue = orderForm.value / 100;

    setRemainigValue(minValue - minicartTotalValue);

    if ((minicartTotalValue * 100) / Number(minValue) > 100)
      return setPercentage(100);

    setPercentage((minicartTotalValue * 100) / Number(minValue));
  }, [isNewCep, minValue, orderForm]);

  useEffect(() => {
    if (!data?.publicSettingsForApp?.message) return;

    const settings = JSON.parse(data.publicSettingsForApp.message);
    const regions = settings.freeShippingRegions;

    if (Object.keys(regions).length === 0) {
      console.warn('No free shipping regions found');

      return;
    }

    const states = Object?.keys(regions)?.reduce((acc, region) => {
      const statesInRegion = regions[region];

      Object?.keys(statesInRegion)?.forEach((statee) => {
        acc[statee] = statesInRegion[statee];
      });

      return acc;
    }, {} as { [state: string]: number });

    const amount = states[state];

    setFreeShippingAmount(amount);
  }, [state, data]);

  const handleChangeCep = (event: React.ChangeEvent<HTMLInputElement>) => {
    let { value } = event.target;

    value = value.replace(/\D/g, '');

    if (value.length > 8) {
      value = value.slice(0, 8);
    }

    if (value.length > 5) {
      value = value.replace(/(\d{5})(\d)/, '$1-$2');
    }

    setCep(value);
  };

  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    try {
      const response = await fetch(
        `/api/checkout/pub/postal-code/BRA/${cep.replace(/\D/g, '')}`,
      );

      const dataCepInfos = await response.json();

      if (dataCepInfos?.state?.length === 2) {
        setIsNewCep(true);
        setState(dataCepInfos?.state);

        try {
          const body = JSON.stringify({
            clearAddressIfPostalCodeNotFound: false,
            selectedAddresses: [
              {
                addressId: null,
                addressType: 'search',
                receiverName: '',
                addressQuery: '',
                ...dataCepInfos,
                isDisposable: true,
              },
            ],
          });

          await fetch(
            `/api/checkout/pub/orderForm/${orderForm?.id}/attachments/shippingData`,
            { body, method: 'POST' },
          );
        } catch (error) {
          setIsNewCep(false);
          console.error('Ocorreu um erro ao atualizar o orderform:', error);
        }
      }
    } catch (error) {
      setIsNewCep(false);
      console.error('Ocorreu um erro ao buscar o endereço:', error);
    }
  };

  return (
    <>
      <div className={handles.freightBarContainer}>
        {remainingValue <= 0 ? (
          <>
            <div className={handles.animatedBarContainer}>
              <div
                style={{ width: `${percentage}%` }}
                className={handles.animatedBar}
              />
            </div>
            <p className={handles.freightMessage}>
              Parabéns, você ganhou <strong>FRETE GRÁTIS</strong>
            </p>
          </>
        ) : (
          <div>
            {isNaN(remainingValue) ? (
              <p className={handles.freightMessage}>Digite um CEP válido</p>
            ) : (
              <>
                <div className={handles.animatedBarContainer}>
                  <div
                    style={{ width: `${percentage}%` }}
                    className={handles.animatedBar}
                  />
                </div>
                <p className={handles.freightMessage}>
                  Faltam <strong>{formatMoney(remainingValue)}</strong> para{' '}
                  <strong>FRETE GRÁTIS</strong>
                </p>
              </>
            )}
          </div>
        )}
      </div>
      <article className={styles.freeShippingCep__container}>
        <section className={styles.freeShippingCep__textCep}>
          <p>
            Consulte seu CEP para <strong>FRETE GRÁTIS</strong>
          </p>
        </section>
        <form
          className={styles.freeShippingCep__content}
          onSubmit={handleSubmit}
        >
          <input
            type="text"
            name="x-cep"
            id="x-cep"
            placeholder="Digite seu CEP"
            className={styles.freeShippingCep__inputCep}
            onChange={handleChangeCep}
            value={cep}
          />
          <button type="submit" className={styles.freeShippingCep__submit}>
            <SearchIcon />
          </button>
        </form>
        <section className={styles.freeShippingCep__DontKnowCep}>
          <a
            target="_blank"
            href="http://buscacepinter.correios.com.br/app/endereco/index.php?t"
            rel="noreferrer"
          >
            Não sei meu cep!
          </a>
        </section>
      </article>
    </>
  );
};

export default MinicartFreightBar;

Sendo assim, dentro do admin, no meu app, consigo ajustar o valor do frete individualmente por estado:

E ao buscar no front, ele retorna os valores para que eu faça a conta:

Obrigado!!

1 Like

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.