Files
LCEssentials/API_Mobile_App.md
Daniel Arantes Loverde 35418cc4a2 docs(api): document pizza item payload format in Create Order endpoint
Add choices[] field documentation with format table, example payload
and business rules for size/flavor/dough/crust serialization.
2026-06-09 08:58:26 -03:00

34 KiB

Atomenta Mobile App API Documentation

🔐 Authentication & Headers

The mobile app accesses distinct sets of APIs:

  1. App APIs (/api/app/*): For general app features like Home Screen and Orders listing.
  2. Customer APIs (/api/customer/*): For User Registration, Login, and Profile Management.
  3. Store External APIs (/api/store/*): For store-specific operations like Catalog and Checkout.

Common Headers

All requests should ideally include:

Accept: application/json
Content-Type: application/json

Authentication Strategies

1. Customer Auth (for App & Customer APIs)

Used for everything related to the logged-in user (Profile, Orders, etc).

  • Header: Authorization: Bearer <Users_JWT_Token>
  • Note: Obtained via /api/customer/login.

3. Hybrid Store Auth (for Store External APIs)

Used when interacting with a specific store (Catalog, Checkout).

  • Header 1: Atomenta-Token: 550e8400-e29b-41d4-a716-446655440008 (Store Module ID)
  • Header 2: Authorization: Bearer <Users_JWT_Token> (Required for Checkout/Orders)

👤 Customer Management

1. Register Customer

POST /api/customer

Headers:

  • Atomenta-Token: 550e8400-e29b-41d4-a716-44665544000a (Customer Module ID)

Body:

{
  "name": "Daniel Loverde",
  "email": "daniel@example.com",
  "phoneNumber": "+5511999999999",
  "birthDate": "1990-01-01T00:00:00Z" // Opcional
}

Response:

{
  "error": false,
  "code": "CUSTOMER_CREATED",
  "result": {
    "id": "cust_uuid...",
    "name": "Daniel Loverde",
    "email": "daniel@example.com"
  }
}

2. Login (Get Token)

POST /api/customer/login

Body (email + phoneNumber are mandatory):

{
  "email": "daniel@example.com",
  "phoneNumber": "+5511999999999"
}

Fluxo:

  • Passo 1: enviar email + phoneNumber para receber código por email.
  • Passo 2: enviar email + phoneNumber e o OTP no header Authorization: Bearer <OTP>.

Response:

{
  "error": false,
  "code": "LOGIN_SUCCESS",
  "result": {
    "token": "eyJhbGciOi...", // <--- Use as Bearer Token for other requests
    "customer": {
      "id": "cust_uuid...",
      "name": "Daniel Loverde",
      "email": "daniel@example.com"
    }
  }
}

3. Get Profile

GET /api/customer/profile

Headers:

  • Authorization: Bearer <JWT_Token>
  • Atomenta-Token: 550e8400-e29b-41d4-a716-44665544000a

Response:

{
  "error": false,
  "code": "CUSTOMER_PROFILE_RETRIEVED",
  "result": {
    "id": "cust_uuid...",
    "name": "Daniel Loverde",
    "email": "daniel@example.com",
    "phoneNumber": "+5511999999999",
    "favorites": ["store_abc...", "store_xyz..."],
    "address_book": [],
    "verified": true,
    "biometricsEnabled": false,
    "createdAt": "2026-01-21T22:37:56.472Z",
    "updatedAt": "2026-02-13T19:00:00.000Z"
  }
}

Campos internos não retornados para o app: keyHash, strongHash, source, isShadow, profile_metrics, behavioral_stats, otpHash, otpHashParams, otpExpiresAt.

4. Update Profile

PATCH /api/customer/profile

Headers:

  • Authorization: Bearer <JWT_Token>
  • Atomenta-Token: 550e8400-e29b-41d4-a716-44665544000a
  • Content-Type: application/json

Notes:

  • Todos os campos são opcionais — envie apenas o que mudou.
  • profilePicture aceita base64 data URL (máx 2 MB decodificado).
  • A resposta inclui profilePictureUrl apenas quando imagem foi enviada.

Body:

{
  "name": "Daniel A. Loverde",
  "cpf": "12345678901",
  "phoneNumber": "5511988888888",
  "profilePicture": "data:image/jpeg;base64,/9j/4AAQSkZJRgAB..."
}
Campo Tipo Obrigatório Descrição
name string não Nome de exibição
cpf string não 11 dígitos, formatado ou raw
phoneNumber string não Com DDI, sem +: "5519991670000"
profilePicture string não base64 data URL — jpeg, png ou webp — máx 2 MB decoded

Formato profilePicture:

data:image/jpeg;base64,<base64data>
data:image/png;base64,<base64data>
data:image/webp;base64,<base64data>

Response (sucesso):

{
  "error": false,
  "code": "PROFILE_UPDATED",
  "profilePictureUrl": "/uploads/profiles/e30188cf_1780615191653.jpg"
}

profilePictureUrl é URL relativa. Prefixar com https://atomenta.com.br para exibir a imagem.

Erros possíveis:

Código HTTP Descrição
INVALID_CPF 400 CPF não tem 11 dígitos
INVALID_PROFILE_IMAGE 400 Não é data URL válido ou excede 2 MB
NO_FIELDS 400 Nenhum campo reconhecido no body
CUSTOMER_NOT_FOUND 404

5. Set Default Address

PATCH /api/customer/addresses/:addressId/default

Headers:

  • Authorization: Bearer <JWT_Token>
  • Atomenta-Token: 550e8400-e29b-41d4-a716-44665544000a

Notes:

  • Define um endereço como padrão pelo seu id.
  • Define isDefault: true no endereço alvo e isDefault: false em todos os outros.
  • O id de cada endereço é retornado no campo address_book do perfil (GET /api/customer/profile).
  • Ao salvar um endereço via PATCH /api/customer/profile, o backend gera automaticamente um id UUID caso o endereço não possua um.
  • Se nenhum endereço tiver isDefault: true, o primeiro do array é tratado como padrão.

Response 200:

{
  "error": false,
  "code": "ADDRESS_DEFAULT_SET"
}

Erros:

Código HTTP Descrição
ADDRESS_NOT_FOUND 404 Nenhum endereço com esse id no address_book
CUSTOMER_NOT_FOUND 404

Exemplo de address_book no perfil (com id e isDefault):

"address_book": [
  {
    "id": "a1b2c3d4-...",
    "label": "Casa",
    "type": "residential",
    "address": "Estrada Vicinal Nene Moro",
    "number": "SN",
    "neighborhood": "Curtume",
    "city": "Aguaí",
    "state": "SP",
    "zipCode": "13868070",
    "country": "Brasil",
    "lat_long": [-22.06743, -46.98018],
    "isDefault": true
  }
]

6. List Favorite Stores

GET /api/customer/favorites

Headers:

  • Authorization: Bearer <JWT_Token>
  • Atomenta-Token: 550e8400-e29b-41d4-a716-44665544000a

Notes:

  • Retorna as lojas favoritas do customer autenticado.
  • Apenas lojas públicas/visíveis são retornadas.
  • A ordem segue a ordem salva em favorites.

Response:

{
  "error": false,
  "code": "CUSTOMER_FAVORITES_RETRIEVED",
  "result": [
    {
      "id": "store_1775510226293_yn5ys",
      "name": "CPS Drinks",
      "logo": null,
      "cover": null,
      "category": "Doces & Bolos",
      "rating": 4.8,
      "totalReviews": 12,
      "isOpen": true,
      "statusLabel": "Aberto",
      "nextOpenLabel": null
    }
  ]
}

6. Add Store to Favorites

POST /api/customer/favorites/:storeId

Headers:

  • Authorization: Bearer <JWT_Token>
  • Atomenta-Token: 550e8400-e29b-41d4-a716-44665544000a

Notes:

  • Salva a loja nos favoritos do customer autenticado.
  • Se a loja já estiver favoritada, a operação continua idempotente.
  • A loja precisa existir e estar publicamente visível.

Response:

{
  "error": false,
  "code": "CUSTOMER_FAVORITE_SAVED",
  "result": {
    "favorites": ["store_1775510226293_yn5ys"],
    "store": {
      "id": "store_1775510226293_yn5ys",
      "name": "CPS Drinks",
      "logo": null,
      "cover": null,
      "category": "Doces & Bolos",
      "rating": 4.8,
      "totalReviews": 12,
      "isOpen": true,
      "statusLabel": "Aberto",
      "nextOpenLabel": null
    }
  }
}

7. Remove Store from Favorites

DELETE /api/customer/favorites/:storeId

Headers:

  • Authorization: Bearer <JWT_Token>
  • Atomenta-Token: 550e8400-e29b-41d4-a716-44665544000a

Notes:

  • Remove a loja dos favoritos do customer autenticado.
  • A operação é idempotente: se a loja não estiver favoritada, o array volta sem ela.

Response:

{
  "error": false,
  "code": "CUSTOMER_FAVORITE_REMOVED",
  "result": {
    "favorites": []
  }
}

🏠 App Home Screen

List Categories

GET /api/public/categories

Headers:

  • Accept: application/json

Response:

{
  "error": false,
  "result": [
    { "id": "all", "name": "Todas", "icon": "🏪" },
    { "id": "lanches", "name": "Lanches", "icon": "🍔" },
    { "id": "pizza", "name": "Pizza", "icon": "🍕" }
  ]
}

List Stores (Smart Listing)

GET /api/app/stores

Headers:

  • Authorization: Bearer <Users_JWT_Token> (Customer obrigatório)

Lists stores based on user location, filtered by distance.

Query Parameters:

  • lat (Required): User Latitude (e.g., -23.550520)
  • lng (Required): User Longitude (e.g., -46.633308)
  • category (Optional): Filter by category name (e.g., Lanches)
  • search (Optional): Search by store name

Notes:

  • Sem login do customer, a listagem é bloqueada.
  • Se lat/lng não forem enviados, a API tenta usar o endereço salvo no perfil do customer (address_book.lat_long).
  • Lojas fora do raio/bairro de entrega não são exibidas.

Response:

{
  "error": false,
  "result": [
    {
      "id": "store_123...",
      "name": "Burger King",
      "logo": "https://...",
      "cover": "https://...",
      "category": "Lanches",
      "rating": 4.8,
      "deliveryTime": "30-45 min",
      "deliveryFee": 5.99,
      "distance": 1.2, // km
      "isOpen": true,
      "statusLabel": "Aberto"
    }
  ]
}

List User Orders

GET /api/app/orders

Headers:

  • Authorization: Bearer <JWT>

Response:

{
  "error": false,
  "result": [
    {
      "id": "ord_123...",
      "total": 54.90,
      "status": "completed",
      "createdAt": "2024-03-20T10:00:00Z",
      "items": [...]
    }
  ]
}

🏪 Store Integration (External API)

Base URL: /api/store/:storeId

1. Store Identity & Status

GET /api/store/:storeId/identity

Headers/Query:

  • x-user-lat / lat: User Latitude
  • x-user-lng / lng: User Longitude

Response:

{
  "error": false,
  "result": {
    "fantasyName": "Mc Donalds",
    "distance": 2.5,
    "deliveryTime": "30-45 min",
    "minOrder": 15.00
  }
}

2. Full Store Info

GET /api/store/:storeId/info

Retorna versão pública para app (sem dados administrativos/financeiros da loja).

Response:

{
  "error": false,
  "code": "STORE_INFO_RETRIEVED",
  "result": {
    "isOpen": true,
    "statusLabel": "Aberto",
    "fantasyName": "Mc Donalds",
    "specialty": "Lanches",
    "minOrder": 15,
    "deliveryTime": "30-45 min",
    "logo": "https://...",
    "cover": "https://...",
    "paymentMethods": {
      "paymentOnDelivery": true,
      "paymentOnPickup": true,
      "acceptCash": true,
      "acceptPix": true
    },
    "openingHours": { "...": "..." },
    "address": {
      "street": "Rua X",
      "neighborhood": "Centro",
      "city": "Campinas",
      "state": "SP",
      "zipcode": "13000-000",
      "latitude": -22.9,
      "longitude": -47.0
    }
  }
}

3. Product Catalog

GET /api/store/:storeId/catalog

Retorna categorias com produtos, incluindo diferenciação explícita de pizza.

Response:

{
  "error": false,
  "result": [
    {
      "id": "cat_lanches",
      "name": "Lanches",
      "isPizzaCategory": false,
      "products": [
        {
          "id": "prod_xburger",
          "type": "prepared",
          "name": "X-Burger",
          "description": "Pão, carne e queijo",
          "image": "/uploads/products/xburger.jpg",
          "price": "25.00",
          "addons": "yes",
          "addonGroups": []
        }
      ]
    },
    {
      "id": "cat_pizzas",
      "name": "Pizzas",
      "isPizzaCategory": true,
      "pizzaConfig": {
        "sizes": [{ "id": "broto", "name": "Broto", "slices": 4, "maxFlavors": 1 }],
        "doughs": [{ "id": "tradicional", "name": "Tradicional", "active": true }],
        "crusts": [{ "id": "catupiry", "name": "Catupiry", "active": true, "priceModifier": "10.00" }]
      },
      "products": [
        {
          "id": "prod_pizza_calabresa",
          "type": "pizza",
          "name": "Pizza Calabresa",
          "description": "Molho, mussarela e calabresa",
          "image": "/uploads/products/pizza-calabresa.jpg",
          "price": "0.00",
          "pizzaPrices": {
            "broto": "35.00",
            "grande": "55.00"
          },
          "addons": "yes",
          "addonGroups": []
        }
      ]
    }
  ]
}

Como identificar pizza no app:

  • Produto pizza: products[].type === "pizza".
  • Configuração de montagem/preço por tamanho: category.isPizzaCategory === true + category.pizzaConfig + product.pizzaPrices.

4. Validate Delivery Address (Checkout)

POST /api/store/:storeId/delivery/validate-address

Use este endpoint ao trocar endereço na tela de pagamento para validar cobertura antes de finalizar.

Body:

{
  "address": {
    "street": "Avenida das Andorinhas",
    "number": "477",
    "neighborhood": "Jardim Andorinhas",
    "city": "Campinas",
    "state": "SP",
    "zip": "13101-400",
    "lat": -22.918,
    "lng": -47.01813
  }
}

Resposta Exemplo:

{
  "error": false,
  "code": "DELIVERY_ADDRESS_VALIDATED",
  "result": {
    "deliveryAllowed": true,
    "reasonCode": "DELIVERY_ALLOWED",
    "reasonMessage": "Endereço válido para entrega",
    "deliveryMode": "KM",
    "distance": 1.3,
    "deliveryFee": 8,
    "deliveryTime": "35 min",
    "sameCity": false,
    "matchedKmRange": { "from": 1, "to": 2, "price": 8, "time": 35 },
    "matchedNeighborhood": null
  }
}

Regras de validação:

  • KM: valida distância até o limite da última faixa + tolerância de 0.5 km.
  • NEIGHBORHOOD: reprova se cidade do endereço for diferente da cidade da loja.
  • NEIGHBORHOOD: aprova se bairro configurado bater, ou se for mesma cidade (fallback).

5. Create Order (Checkout)

POST /api/store/:storeId/orders

Body:

{
  "customer": {
    "name": "Daniel Loverde",
    "phone": "11999999999",
    "email": "daniel@example.com",
    "asaasId": "cus_000005165985" // Opcional.
  },
  // Nota: O usuário DEVE estar autenticado via Bearer Token.
  // Os dados de 'customer' enviados aqui são usados preferencialmente para
  // registrar os dados de entrega/cobrança deste pedido específico no Asaas,
  // mas o 'userId' e o histórico são vinculados ao token do usuário logado.
  "items": [
    {
      "productId": "prod_123",
      "name": "X-Burger",
      "qty": 2,
      "price": 25.00,
      "addons": [
        {
          "addonId": "addon_egg",
          "name": "Ovo",
          "price": 2.50,
          "qty": 2
        }
      ]
    }
  ],
  "total": 50.00,
  "paymentMethod": "PIX", // "PIX" | "CREDIT_CARD" | "DEBIT_CARD" | "MONEY"
  "deliveryType": "DELIVERY", // "DELIVERY" | "PICKUP"
  "address": {
    "street": "Rua Exemplo",
    "number": "123",
    "neighborhood": "Centro"
  }
}

Exemplo com pizza (tamanho + sabores + borda):

{
  "items": [
    {
      "productId": "prod_pizza_calabresa",
      "name": "Pizza Calabresa",
      "qty": 1,
      "price": 65.00,
      "choices": [
        "Tamanho: Grande (+R$ 55,00)",
        "Sabor: Calabresa",
        "Massa: Tradicional",
        "Borda: Catupiry (+R$ 10,00)"
      ]
    }
  ]
}

Como montar choices[] para pizza:

Linha Formato Obrigatório
Tamanho "Tamanho: <NomeDoTamanho> (+R$ X,XX)" Sim
Sabor (único) "Sabor: <NomeDeSabor>" Sim
Sabor N (múltiplos) "Sabor 1: <Sabor>", "Sabor 2: <Sabor>" Sim quando maxFlavors > 1
Massa "Massa: <NomeDaMassa>" Não
Borda sem custo "Borda: <NomeDaBorda>" Não
Borda com custo "Borda: <NomeDaBorda> (+R$ X,XX)" Não

Regras:

  • choices[] é um array de strings. O backend converte para options[] na normalização do pedido.
  • O price do item deve refletir o preço total da pizza (tamanho + modificador de borda).
  • Os nomes de tamanho, massa e borda devem corresponder aos id ou name retornados pelo catalog endpoint.
  • Máximo de sabores definido por pizzaConfig.sizes[n].maxFlavors.

Regras para items[].addons:

  • qty do adicional é aceito no backend (ex.: 2 ovos).
  • Se qty não for enviado ou vier inválido, a API assume 1.
  • qty mínimo efetivo é 1.

Response:

{
  "error": false,
  "code": "ORDER_CREATED",
  "result": {
    "id": "ord_987...",
    "shortId": "1234",
    "status": "PAYMENT_PENDING",
    "paymentStatus": "PENDING",
    "paymentMethod": "PIX",
    "paymentPayload": {
      "copyPaste": "000201010212...",
      "qrCodeImage": "iVBORw0KGgoAAAANSUhEUgAA...",
      "expirationDate": "2026-02-14T22:30:00.000Z"
    },
    "payment": {
      "method": "PIX",
      "status": "PENDING",
      "pix": {
        "copyPaste": "000201010212...",
        "qrCodeImage": "iVBORw0KGgoAAAANSUhEUgAA...",
        "expirationDate": "2026-02-14T22:30:00.000Z"
      }
    }
  }
}

Observação de segurança: o Atomenta não retorna mais links de checkout/fatura do Asaas no payload de criação do pedido.

6. Realtime de Status do Pedido (Socket.IO)

Use este canal para acompanhar mudança de status do pedido em tempo real (sem depender de push notification).

Socket Endpoint:

  • ws://<host>:<port>/socket.io/ (em produção, usar wss:// se houver TLS no balanceador)

Auth no handshake:

  • Enviar o JWT do customer em auth.token (mesmo token do Authorization: Bearer ... das APIs HTTP).

Exemplo de conexão (conceitual):

{
  "auth": {
    "token": "Bearer <Users_JWT_Token>"
  }
}

Eventos recebidos pelo app:

  • order_update: alteração geral de status do pedido.
  • order_cancelled: cancelamento confirmado, com motivo e metadados de estorno.
  • delivery_order_cancelled: evento dedicado para telas de OTP/entrega interromperem o fluxo imediatamente.

Payload típico de order_cancelled:

{
  "event": "order_cancelled",
  "emittedAt": "2026-03-05T18:00:00.000Z",
  "orderId": "ffdc47b4-6a55-4713-aea9-45169e4cf0d7",
  "shortId": "3778",
  "storeId": "store_1772117366848_wmqw4",
  "userId": "cust_uuid_001",
  "status": "CANCELED",
  "cancelledBy": "store",
  "cancelledAt": "2026-03-05T18:00:00.000Z",
  "cancelReasonCode": "STORE_UNAVAILABLE",
  "cancelReasonDetail": "Falta de insumo crítico para finalizar o pedido",
  "refundStatus": "failed",
  "refundIdempotencyKey": "refund:ffdc47b4-...",
  "refundProviderRef": null,
  "refundError": "Falha ao solicitar estorno automático"
}

7. Fallback oficial (Polling)

Se o socket cair, o app deve continuar consultando o status até estado final.

Endpoint:

  • GET /api/public/orders/:orderId

Sugestão de estratégia:

  • primeiros 60s: a cada 3s
  • próximos 2min: a cada 5s
  • depois: a cada 10s
  • parar em status final (COMPLETED, CANCELED, REFUNDED) ou ao sair da tela.

8. Cancelamento de Pedido (Loja/Customer)

Cancelamento pela loja (Store API):

  • POST /api/store/:storeId/orders/:orderId/cancel

Cancelamento pelo customer:

  • POST /api/customer/orders/:orderId/cancel

Request body (ambos):

{
  "reasonCode": "CUSTOMER_REQUEST",
  "reasonDetail": "Cliente solicitou cancelamento antes da entrega"
}

Motivos aceitos (reasonCode):

  • STORE_UNAVAILABLE
  • CUSTOMER_REQUEST
  • DELIVERY_ISSUE

Regras atuais de cancelamento:

  • Permitido em: PAYMENT_PENDING, PENDING, ACCEPTED, PREPARING, READY, DELIVERING.
  • Bloqueado em: COMPLETED e CANCELED.
  • Motoboy não cancela pedido.
  • Ao cancelar, o backend invalida otp, confirmOtp e customerOtp.

Concorrência e idempotência (ponta a ponta):

  • Cancelamento é serializado por orderId (lock por pedido).
  • Estorno automático usa lock + chave idempotente de estorno para evitar duplicidade.
  • Em segunda tentativa após cancelamento efetivado, a API retorna 409 ORDER_ALREADY_CANCELED.

Campos relevantes no retorno:

  • cancelledBy, cancelledAt, cancelReasonCode, cancelReasonDetail
  • refundStatus, refundIdempotencyKey, refundProviderRef

9. Catálogo Oficial de Tags de Review

9.1 Matriz Completa de Endpoints (Reviews + Tags)

Público/App (Customer):

  • GET /api/public/reviews/tags → catálogo oficial de tags e regras de validação.
  • POST /api/public/orders/:orderId/review → cria/atualiza avaliação do pedido concluído.
  • GET /api/public/store/:storeId/reviews → lista reviews públicos da loja.

Store/Backoffice (analytics e operação):

  • GET /api/store/:storeId/reviews → listagem detalhada + métricas de reviews da loja.
  • POST /api/store/:storeId/reviews/:reviewId/reply → resposta oficial da loja para avaliação.
  • POST /api/store/:storeId/reviews/:reviewId/dispute → contestação da avaliação.
  • GET /api/store/reviews/app/nps → NPS do app (geral/por filtros).
  • GET /api/store/reviews/app/nps/platform → NPS por plataforma (ios, android, web).
  • GET /api/store/reviews/observability/overview → funil técnico (sucesso/rejeição/erro).
  • GET /api/store/reviews/observability/alerts → alertas operacionais (ex.: pico de reviews negativas).

Observação de base URL:

  • No app, consumir sempre via domínio/API oficial do ecossistema (PediFoods/Atomenta), mantendo o Atomenta como orquestrador.

O app deve buscar este endpoint para renderizar as tags válidas e enviar somente os id retornados.

Endpoint:

  • GET /api/public/reviews/tags

Headers:

  • Accept: application/json

Response (resumo):

{
  "error": false,
  "result": {
    "version": "2026-03-01",
    "order": {
      "positive": [{ "id": "flavor", "label": "Sabor" }],
      "improvement": [{ "id": "wrong_items", "label": "Itens errados" }],
      "rules": {
        "positiveAllowedWhenRateGte": 5,
        "improvementAllowedWhenRateLte": 4
      }
    },
    "delivery": {
      "sentiments": [
        { "id": "positive", "allowedTags": ["politeness", "on_time"] },
        { "id": "negative", "allowedTags": ["delay", "rude"] }
      ],
      "positive": [{ "id": "politeness", "label": "Educação" }],
      "negative": [{ "id": "delay", "label": "Atraso" }]
    },
    "app": {
      "nps": { "min": 0, "max": 10 },
      "platforms": ["ios", "android", "web"]
    }
  }
}

Regra de integração:

  • Não hardcodear tags no app; usar o catálogo do backend.
  • Enviar no POST de review apenas os id retornados em cada bloco.

10. Enviar Avaliação do Pedido

Permite o customer enviar review da loja a partir de um pedido finalizado.

Endpoint:

  • POST /api/public/orders/:orderId/review

Headers:

  • Authorization: Bearer <Users_JWT_Token> (obrigatório)
  • Accept: application/json
  • Content-Type: application/json

Body:

{
  "orderRate": 5,
  "orderComment": "Pedido chegou certinho e bem embalado.",
  "orderPositiveTags": ["flavor", "temperature"],
  "orderImprovementTags": [],
  "deliverySentiment": "positive",
  "deliveryPositiveTags": ["on_time", "politeness"],
  "deliveryNegativeTags": [],
  "appNps": 10,
  "platform": "ios",

  "rate": 5,
  "message": "Pedido chegou certinho e bem embalado."
}

Regras de negócio atuais:

  • Só aceita review para pedido com status COMPLETED.
  • O orderId deve existir.
  • storeId, userId, clientName e orderId do review são derivados do pedido no backend (não enviar no body).
  • Campos obrigatórios para integração nova: orderRate, deliverySentiment, appNps, platform.
  • Compatibilidade: rate/message ainda são aceitos como fallback.
  • App deve manter o rascunho local do formulário em caso de erro HTTP para permitir reenvio sem redigitar.

Response (sucesso):

{
  "error": false,
  "result": {
    "id": "rev_abc123",
    "storeId": "store_1772117366848_wmqw4",
    "userId": "cust_uuid_001",
    "clientName": "Customer 002",
    "rate": 5,
    "message": "Pedido chegou certinho e bem embalado.",
    "orderRate": 5,
    "orderComment": "Pedido chegou certinho e bem embalado.",
    "deliveryFeedback": "positive",
    "platform": "ios",
    "editableUntil": "2026-03-06T17:15:00.000Z",
    "storeReplyUntil": "2026-03-06T17:15:00.000Z",
    "reviewWindowExpiresAt": "2026-03-06T16:10:00.000Z",
    "orderId": "ffdc47b4-6a55-4713-aea9-45169e4cf0d7",
    "date": "2026-03-01T17:15:00.000Z"
  }
}

Erros esperados:

  • 400 quando orderId não for enviado.
  • 400 quando o pedido não existir ou ainda não estiver concluído (Apenas pedidos concluídos podem ser avaliados).
  • 400 REVIEW_WINDOW_EXPIRED quando passou da janela inicial de 5 dias para criar review.
  • 400 REVIEW_EDIT_WINDOW_EXPIRED quando tentar alterar review após 5 dias da criação.
  • 400 INVALID_REVIEW_PAYLOAD / INVALID_REVIEW_TAGS / INVALID_DELIVERY_SENTIMENT_TAGS para payload inválido.
  • 401 se o token JWT for inválido/ausente.
  • 500 em falha interna (Erro ao enviar avaliação).

Exemplos de validação (cenários):

Exemplo válido (nota alta + entrega positiva):

{
  "orderRate": 5,
  "orderComment": "Perfeito.",
  "orderPositiveTags": ["flavor", "temperature"],
  "orderImprovementTags": [],
  "deliverySentiment": "positive",
  "deliveryPositiveTags": ["on_time", "politeness"],
  "deliveryNegativeTags": [],
  "appNps": 10,
  "platform": "android"
}

Exemplo inválido (nota 5 com orderImprovementTags):

{
  "orderRate": 5,
  "orderImprovementTags": ["temperature"],
  "deliverySentiment": "positive",
  "appNps": 8,
  "platform": "ios"
}

Retorno esperado: 400 INVALID_REVIEW_TAGS.

Exemplo inválido (entrega positiva com tags negativas):

{
  "orderRate": 4,
  "orderImprovementTags": ["temperature"],
  "deliverySentiment": "positive",
  "deliveryNegativeTags": ["delay"],
  "appNps": 7,
  "platform": "web"
}

Retorno esperado: 400 INVALID_DELIVERY_SENTIMENT_TAGS.

11. Analytics de Reviews (Store/API)

Para dashboard da loja e análise de produto, usar:

  • GET /api/store/:storeId/reviews

    • filtros opcionais: startDate, endDate, minRate, maxRate, deliverySentiment, platform
    • retorno inclui métricas de estrelas, blocos de pedido/entrega, janelas (editableUntil, storeReplyUntil) e appNps agregado.
  • POST /api/store/:storeId/reviews/:reviewId/reply

    • body obrigatório: reply (texto da resposta da loja).
    • regra: respeita janela de resposta (storeReplyUntil).
  • POST /api/store/:storeId/reviews/:reviewId/dispute

    • body obrigatório: reason (motivo da contestação).
  • GET /api/store/reviews/app/nps

    • filtros opcionais: startDate, endDate, storeId, platform
    • retorno: volume NPS, média e score.
  • GET /api/store/reviews/app/nps/platform

    • filtros opcionais: startDate, endDate, storeId
    • retorno: NPS separado em ios, android, web.
  • GET /api/store/reviews/observability/overview

    • filtros opcionais: startDate, endDate, storeId
    • retorno: tentativas, sucesso/rejeição/erro, taxa de sucesso e conversão por pedidos concluídos.
  • GET /api/store/reviews/observability/alerts

    • filtro opcional: storeId
    • retorno: alertas de pico, ex.: NEGATIVE_REVIEWS_SPIKE.

Compatibilidade Store:

  • O painel /store/reviews segue funcional com os campos legados (rate, message, itemFeedback, improvementFeedback, deliveryFeedback).
  • Campos novos coexistem para evolução gradual sem quebrar o fluxo atual.

Feature Control (Feature Flags)

O Feature Control permite ativar/desativar funcionalidades do app remotamente sem publicar uma nova versão. O app nunca chama o Atomenta diretamente — usa um BFF (Backend for Frontend) que protege o Atomenta-Token do cliente.

Arquitetura

App iOS  →  POST /feature-control/bootstrap  →  BFF  →  Atomenta /api/feature-control/evaluate

O BFF mantém cache de 60 segundos. Em caso de timeout ou erro upstream, retorna flags de fallback estáticas definidas nas variáveis de ambiente do BFF.


1. Bootstrap — Avaliar flags na inicialização

POST /feature-control/bootstrap

Deve ser chamado uma vez por sessão do app (ao abrir ou ao autenticar). Retorna os valores de todos os flags solicitados para o contexto do usuário.

Headers:

Content-Type: application/json
Authorization: Bearer <JWT_Token>   ← opcional, mas recomendado

Se o Authorization for enviado, o BFF extrai subjectId automaticamente do JWT (sub / customerId / id). Se não for enviado, context.subjectId é obrigatório.

Request body:

{
  "environment": "production",
  "keys": [
    "at.ios.only",
    "at.android.only",
    "at.promo",
    "at.city.aguai"
  ],
  "context": {
    "subjectType": "customer",
    "subjectId": "e30188cf-cf69-471e-bc59-4c92e0f9ad7b",
    "platform": "ios",
    "appVersion": "2.3.1",
    "attributes": {
      "city": "Aguaí"
    }
  }
}
Campo Tipo Obrigatório Descrição
environment string não "production" (default) ou "sandbox"
keys string[] sim Lista de flags a avaliar (máx 100)
context.subjectType string sim "customer" | "user" | "store" | "anonymous"
context.subjectId string sim* ID do usuário. *Dispensável se JWT enviado no header
context.platform string não "ios" ou "android"
context.appVersion string não Versão semântica, ex: "2.3.1"
context.storeId string não Para flags segmentadas por loja
context.attributes object não Atributos extras (ex: city, tier)

Response 200 — fonte live:

{
  "ok": true,
  "source": "live",
  "configVersion": 7,
  "evaluatedAt": "2026-06-05T12:00:00.000Z",
  "flags": {
    "atiosonly": true,
    "atandroidonly": false,
    "atpromo": true,
    "atcityaguai": true
  },
  "raw": {
    "at.ios.only": {
      "enabled": true,
      "variant": "on",
      "payload": null,
      "reason": "rollout"
    },
    "at.promo": {
      "enabled": true,
      "variant": "on",
      "payload": null,
      "reason": "rollout"
    },
    "at.city.aguai": {
      "enabled": true,
      "variant": "on",
      "payload": null,
      "reason": "segment"
    },
    "at.android.only": {
      "enabled": false,
      "variant": "off",
      "payload": null,
      "reason": "default"
    }
  }
}

Response 200 — fonte fallback (quando BFF não consegue atingir Atomenta):

{
  "ok": true,
  "source": "fallback",
  "configVersion": 0,
  "evaluatedAt": "2026-06-05T12:00:00.000Z",
  "flags": { "ios": true, "promo": true },
  "raw": { ... },
  "upstreamError": {
    "status": 504,
    "code": "FEATURE_CONTROL_UPSTREAM_TIMEOUT",
    "message": "Timeout no upstream"
  }
}

Como usar raw no app (Swift)

O app usa o campo raw, indexado pela chave original completa. O campo flags contém uma versão simplificada (pontos removidos), mas o app Swift usa raw para manter compatibilidade direta com os nomes de flag.

Chave original Acesso via raw flags simplificado
at.ios.only raw["at.ios.only"] flags["atiosonly"]
at.android.only raw["at.android.only"] flags["atandroidonly"]
at.promo raw["at.promo"] flags["atpromo"]
at.city.aguai raw["at.city.aguai"] flags["atcityaguai"]

Use sempre raw para acessar flags no app — as chaves simplificadas em flags são geradas mecanicamente e menos legíveis.

Regra de leitura em raw:

  • raw["at.promo"].enabled == true → flag habilitado
  • raw["at.promo"].variant == "on" → variante ativa
  • raw["at.promo"].payload → dados extras opcionais (JSON livre)

Exemplo Swift (padrão atual do app):

// FeatureFlagsState.isEnabled() já lê raw automaticamente
if appState.featureFlags.isEnabled("at.promo") {
    // mostrar módulo de promoções
}

if appState.featureFlags.isEnabled("at.ios.only") {
    // funcionalidade exclusiva iOS
}

source e estratégia de fallback

source Significado Ação recomendada
"live" Dados frescos do Atomenta Usar normalmente
"fallback" BFF usou defaults estáticos (timeout/erro) Usar com cautela; re-tentar na próxima sessão

2. Telemetria de Exposição

POST /feature-control/telemetry/exposure

Registra quais flags o usuário visualizou (para análise de rollout). Envio em batch, fire-and-forget — não bloquear UX aguardando resposta.

Headers:

Content-Type: application/json
Authorization: Bearer <JWT_Token>

Request body:

{
  "events": [
    {
      "featureKey": "at.promo",
      "variant": "on",
      "subjectType": "customer",
      "storeId": "store_1777215327582_akum6"
    },
    {
      "featureKey": "at.ios.only",
      "variant": "on",
      "subjectType": "customer"
    }
  ]
}
Campo Tipo Obrigatório Descrição
featureKey string sim Chave original com prefixo fc.
variant string não Variante exposta (default "off")
subjectType string não Default "customer"
storeId string não Quando relevante

Máximo de 100 eventos por request.

Response 200:

{
  "ok": true,
  "code": "FEATURE_CONTROL_EXPOSURE_ACCEPTED",
  "result": { "count": 2 }
}

3. Flags disponíveis (referência)

Padrão de nomenclatura: at.<domínio>.<descrição> (pontos como separador, sem hífens).

Flag Descrição Default
at.ios.only Funcionalidades exclusivas iOS true
at.android.only Funcionalidades exclusivas Android true
at.promo Módulo de promoções ativo true
at.city.aguai Expansão para cidade de Aguaí/SP true

Novos flags são criados pelo time Pedi Foods no painel /feature-control. Consulte o time antes de codificar um flag inexistente. A lista completa de flags ativos está disponível no painel admin → Feature Control.


4. Fluxo recomendado no app

1. App abre / usuário autentica
2. POST /feature-control/bootstrap  (com JWT no header)
3. Salvar flags em memória/UserDefaults para a sessão
4. Renderizar UI baseada nos flags
5. POST /feature-control/telemetry/exposure  (fire-and-forget, por flag visualizado)
6. Ao fechar sessão ou após 15 min, repetir passo 2 na próxima abertura

Não chamar bootstrap a cada tela — apenas na inicialização da sessão. O BFF tem cache de 60s no servidor; o app deve ter cache local mínimo de 15 minutos para evitar latência desnecessária.


Autor: Daniel Arantes Loverde