De um catálogo estático, para um catálogo global real

Uma visão do próximo passo na arquitetura de produtos: utilizar o pipeline existente de verificação e ingestão de CSV/JSON para construir um catálogo global de produtos validado e independente da fonte.

PromobetArquiteturaIngestão de DadosValidaçãoCatálogo de ProdutosBackendMarketing de AfiliadosFirebase

De Produtos Mockados para um Catálogo Global Real

Quando a experiência global de produtos foi implementada pela primeira vez, utilizar um conjunto de dados mockados da Lomadee fazia todo sentido.

Naquele momento, o objetivo não era construir um sistema completo de gerenciamento de produtos. Precisávamos de produtos para fazer a experiência funcionar, testar as mecânicas da roleta, validar a interface e começar a pensar em como os catálogos interagiriam com os tenants.

Uma pequena camada de transformação era suficiente.

A aplicação carregava um arquivo JSON da Lomadee, extraía os campos que precisávamos, calculava informações como desconto e tier e disponibilizava o array resultante para a experiência do catálogo.

Era simples.

E, naquela época, simplicidade era exatamente o que precisávamos.

Mas o projeto evoluiu desde então.

Agora temos algo muito mais valioso do que um conjunto de dados de produtos mockados: um pipeline de ingestão capaz de transformar diferentes representações de produtos em uma estrutura interna de produto validada.

Isso muda a forma como a experiência global deve funcionar.

A Situação Atual

No momento, o catálogo global ainda depende, em última instância, de um conjunto de dados local da Lomadee.

O resolvedor de catálogo efetivamente possui duas possibilidades:

text
Tenant possui produtos

Utiliza o catálogo do tenant

Tenant não possui produtos

Utiliza os produtos da Lomadee

Sem tenant

Utiliza os produtos da Lomadee

A abstração em si é útil.

O problema é a fonte.

A experiência global não deveria depender fundamentalmente da Lomadee.

A Lomadee foi útil como fonte inicial de dados, mas agora nossa aplicação possui seu próprio modelo de produtos e suas próprias regras para determinar se um produto é válido.

Isso significa que podemos mover essa responsabilidade para uma etapa anterior do processo.

Em vez de transformar um conjunto de dados externo e mockado toda vez que a aplicação é iniciada, podemos ingerir os produtos uma vez, validá-los, persistí-los e permitir que a experiência global consuma a mesma estrutura canônica de produtos utilizada pelo restante do sistema.


O Pipeline de Ingestão Já Existe

Essa é uma das razões pelas quais essa transição é relativamente simples.

Já estamos trabalhando para separar as diferentes responsabilidades envolvidas na ingestão de produtos.

Atualmente, um CSV passa por um processo aproximadamente equivalente a:

text
CSV

csvRowToProduct

enrichProduct

validateProduct

produto verificado

persistência

O JSON segue a mesma ideia geral:

text
JSON

jsonProductToProduct

enrichProduct

validateProduct

produto verificado

persistência

Os adapters lidam com as diferenças entre as representações externas.

A camada de enriquecimento normaliza e preenche os valores necessários.

A camada de validação determina se o produto resultante atende aos requisitos da nossa aplicação.

Essa separação é importante.

O catálogo em si não deveria se importar se um produto originalmente veio de um arquivo CSV, documento JSON, provedor de afiliados ou qualquer outra fonte.

Depois que a ingestão termina, ele deve simplesmente ser um produto.


Uma Estrutura de Produto, Múltiplas Fontes

Essa é a direção arquitetural que queremos preservar.

Imagine que os produtos cheguem de várias fontes:

text
CSV da Shopee


CSV Adapter

     ├──────────────┐

JSON de parceiro   │
     │             │
     ▼             │
JSON Adapter       │
     │             │
     └──────┬──────┘

       Enriquecimento


         Validação


      Produto Canônico

Ao final desse processo, a origem passa a ser secundária.

A aplicação recebe uma representação consistente do produto contendo informações como:

  • nome
  • imagem
  • URL
  • URL da oferta
  • preço
  • comissão
  • taxa de comissão
  • loja
  • categoria
  • afiliado
  • descrição
  • tier
  • prioridade
  • estoque

O princípio arquitetural mais importante aqui é que produtos globais e produtos de tenants não deveriam exigir schemas de produto diferentes simplesmente porque pertencem a catálogos diferentes.

Eles continuam sendo produtos.

O que muda é onde eles são armazenados e como são selecionados.


Introduzindo o Global Product Pool

O próximo passo é introduzir uma coleção persistente de produtos globais.

Conceitualmente:

text
tenants/
  tenantA/
    products/
  tenantB/
    products/

globalProducts/
  productA
  productB
  productC

Os produtos dos tenants continuam sendo específicos de cada tenant.

Os produtos globais passam a ser um pool compartilhado que pode ser utilizado pela experiência global e, quando apropriado, como catálogo de fallback para os tenants.

Isso nos dá um modelo muito mais claro:

text
                 Ingestão de Produtos

               ┌────────┴────────┐
               │                 │
            Tenant             Global
               │                 │
               ▼               ▼
        Produtos do Tenant   Produtos Globais

O processo de ingestão continua sendo o mesmo.

Apenas o destino da persistência muda.


O Catalog Resolver Não Precisa Mudar Muito

Uma das coisas mais interessantes da arquitetura atual é que o consumidor já possui uma abstração útil.

Atualmente, a aplicação solicita:

js
const { products: catalog, source: catalogSource } =
  await getCatalogForTenant(tenant);

A experiência global não precisa saber de onde esses produtos vieram.

Esse contrato pode permanecer.

A diferença está no que acontece por trás dele.

O fluxo futuro passa a ser:

text
Experiência Global


getCatalogForTenant(null)


Global Product Pool

Para um tenant:

text
Experiência do Tenant


getCatalogForTenant(tenant)

       ├── Produtos do tenant disponíveis
       │          ↓
       └──  Catálogo do tenant

Essa é uma mudança pequena, mas com uma consequência arquitetural bastante significativa.

O catálogo global passa a ser um recurso real do domínio, em vez de um fixture de desenvolvimento.


Colocando a Lomadee de Volta no Lugar ao Qual Ela Pertence

O código atual de transformação da Lomadee foi útil porque adaptava uma representação externa para o formato esperado pela aplicação.

Mas agora temos um sistema de ingestão mais genérico.

Em vez de manter uma lógica especial de catálogo como:

text
JSON da Lomadee

mapLomadeeProduct()

catálogo global

podemos eventualmente tratar a Lomadee apenas como mais uma fonte possível:

text
Lomadee

Lomadee Adapter

Produto Canônico

O mesmo poderia futuramente acontecer com outros provedores.

Isso nos dá um modelo muito mais escalável:

text
                 ┌── CSV

                 ├── JSON

Fontes externas  ├── Lomadee

                 ├── Shopee

                 └── Futuros provedores


                   Camada de Ingestão


                    Produto Canônico

                  ┌──────┴──────┐
                  ▼             ▼
             Pool do Tenant  Pool Global

A camada de experiência não precisa mudar quando uma nova fonte é introduzida.


Um Fluxo de Importação Administrativo

Essa arquitetura também abre caminho para algo que intencionalmente evitamos até que o pipeline subjacente estivesse pronto: gerenciar o catálogo global através de uma interface administrativa.

O fluxo pretendido é aproximadamente:

text
Admin

 │ upload CSV / JSON

Autenticação


Verificação

 ├── produtos válidos
 ├── avisos
 └── produtos inválidos


Preview


Commit


Global Product Pool

É aqui que a funcionalidade de verificação existente se torna particularmente útil.

Um administrador não deveria precisar simplesmente enviar um arquivo e torcer para que os dados estejam corretos.

O sistema pode primeiro apresentar:

text
Total de produtos:   1.000
Válidos:               963
Avisos:                 21
Inválidos:              37

Os produtos válidos podem então ser visualizados antes que qualquer coisa seja efetivamente gravada no catálogo global.


Verificação Antes da Persistência

Essa separação é importante o suficiente para ser considerada parte da arquitetura, e não apenas uma funcionalidade da interface.

O ciclo de vida desejado é:

text
Dados Brutos

Parse

Normalização

Enriquecimento

Verificação

Preview

Commit

E não:

text
Upload

Gravar tudo em produção

Isso nos dá a oportunidade de rejeitar produtos malformados antes que eles façam parte da experiência.

Também significa que as mesmas regras de verificação serão aplicadas independentemente de os dados virem de CSV ou JSON.


Produtos Globais Devem Ser Independentes da Fonte

Talvez a consequência mais importante dessa mudança seja que a experiência global se torna independente do provedor original.

Hoje, o modelo mental ainda é, de certa forma:

text
Produtos globais = produtos da Lomadee

O objetivo é mudar isso para:

text
Produtos globais = produtos validados disponíveis para o catálogo global

São conceitos muito diferentes.

A Lomadee se torna uma fonte.

CSV se torna um formato de transporte.

JSON se torna um formato de transporte.

O catálogo global se torna um conceito próprio do domínio.


Mantendo um Fallback Temporário

Também não há motivo para tornar essa migração desnecessariamente arriscada.

Durante a transição, o conjunto de dados existente da Lomadee pode permanecer como um fallback legado.

A lógica de resolução pode efetivamente se tornar:

text
Tentar o Global Product Pool

        ├── produtos disponíveis
        │       ↓
        │   catálogo global

        └── vazio

        fallback legado da Lomadee

Isso nos proporciona um caminho seguro de migração.

Podemos fazer o upload de um conjunto de dados real e validado, testar a experiência global, monitorá-la e só remover a fonte legada quando tivermos confiança de que o novo catálogo está funcionando corretamente.


A Arquitetura Para a Qual Estamos Caminhando

O cenário completo começa a ficar assim:

text
                       SISTEMA ADMIN

                    Upload autenticado

                     Arquivos CSV / JSON


                    ┌───────────────┐
                    │    Adapters   │
                    └───────┬───────┘


                    ┌───────────────┐
                    │ Enriquecimento│
                    └───────┬───────┘


                    ┌───────────────┐
                    │   Verificação │
                    └───────┬───────┘


                    Produtos Canônicos



                       Global Pool



                        Global UX

E o serviço de catálogo continua sendo a fronteira entre toda essa complexidade e a experiência propriamente dita.


O Que Vem a Seguir

O próximo passo de implementação não é redesenhar o pipeline de ingestão.

É reutilizá-lo.

O principal trabalho será fazer com que a camada de persistência compreenda um destino:

global\

Em seguida, vamos introduzir um endpoint administrativo autenticado capaz de enviar dados CSV e JSON através do pipeline de verificação existente.

A primeira versão não precisa resolver todos os problemas futuros do catálogo.

Ela precisa apenas estabelecer esta relação:

text
Produto Validado

Global Product Pool

Experiência Global

Uma vez que isso exista, teremos uma base muito mais sólida para tudo o que vier depois.

Podemos futuramente pensar em composição de catálogos, múltiplos provedores de afiliados, catálogos sazonais, priorização, deduplicação, gerenciamento do ciclo de vida dos produtos e regras de seleção global mais sofisticadas.

Mas esses recursos passam a ser extensões de um catálogo de produtos real, em vez de soluções alternativas construídas em torno de um dataset mockado.

E esse provavelmente é o ponto de mudança arquitetural mais importante aqui.

A experiência global não vai mais consumir uma fonte mockada que, por acaso, contém produtos. Ela vai consumir o nosso próprio catálogo de produtos validados.