Order bump e cross-sell no checkout

TLDR: conectar checkouts existentes entre si por uma tabela de relacionamento, permitindo oferecer produtos complementares (order bump), upgrades (upsell) ou alternativas mais baratas (downsell) durante o checkout.

Contexto

O order bump é uma funcionalidade de cross-selling que oferece produtos adicionais relacionados ao produto principal durante o checkout, com o objetivo de aumentar o ticket médio.

Objetivos

  • Um checkout principal pode ter múltiplos checkouts relacionados como cross-sell.
  • Cada relacionamento tem configuração própria de promoção, valor de referência, descrição personalizada e ordem de exibição.
  • Seguir o padrão MVC da aplicação Rails e os padrões já estabelecidos na codebase para relacionamentos e serialização.

Fora de escopo

— (não registrado na spec original)

Mudanças

Tabela checkout_cross_sells

```ruby class CreateCheckoutCrossSells < ActiveRecord::Migration[8.0] def change create_table :checkout_cross_sells do |t| t.references :primary_checkout, null: false, foreign_key: { to_table: :checkouts } t.references :cross_sell_checkout, null: false, foreign_key: { to_table: :checkouts } t.text :description t.decimal :reference_value, precision: 10, scale: 2 t.decimal :promotion_value, precision: 10, scale: 2 t.integer :kind, null: false t.integer :order, null: false, default: 0 t.string :pid, null: false

  t.timestamps

  t.index [:primary_checkout_id, :cross_sell_checkout_id],
          name: "index_checkout_cross_sells_unique", unique: true
  t.index :pid, unique: true
  t.index [:primary_checkout_id, :order]
end   end end ```

Modelo CheckoutCrossSell

Em app/models/checkout_cross_sell.rb, com include Pidable:

  • Enum kind: order_bump: 0, upsell: 1, downsell: 2
  • Relações: belongs_to :primary_checkout e belongs_to :cross_sell_checkout, ambos class_name: 'Checkout'
  • Validações: presença de description, reference_value, promotion_value, kind e order; reference_value e promotion_value maiores que 0; order maior ou igual a 0
  • Validações customizadas: different_checkouts (o cross-sell não pode ser o próprio checkout) e same_organization (ambos precisam pertencer à mesma organização)
  • Scopes: ordered (por order) e by_kind
  • to_payload: devolve pid, kind, order, description, promotion_value e reference_value

Modelo Checkout

```ruby has_many :checkout_cross_sells, foreign_key: :primary_checkout_id, dependent: :destroy has_many :cross_sell_checkouts, through: :checkout_cross_sells, source: :cross_sell_checkout has_many :parent_cross_sells, class_name: ‘CheckoutCrossSell’, foreign_key: :cross_sell_checkout_id, dependent: :destroy

accepts_nested_attributes_for :checkout_cross_sells, reject_if: :all_blank, allow_destroy: true

def cross_sell_items checkout_cross_sells.ordered.map(&:to_payload) end ```

API V1

Em app/controllers/api/v1/checkout_controller.rb#show, adicionar cross_sell_items à lista de methods do as_json do checkout.

ActiveAdmin

Nova aba “Cross Sell / Order Bump” em app/admin/checkouts.rb, exibida apenas quando o registro já existe, com f.has_many :checkout_cross_sells permitindo destruição. Campos do formulário: checkout de cross-sell (select limitado aos checkouts ativos da mesma organização, exceto o próprio), tipo, descrição, valor de referência, valor promocional e ordem de exibição.

Fluxos

Criação — o usuário seleciona o checkout principal no admin, adiciona checkouts relacionados com suas configurações, o sistema valida as regras de negócio e persiste em checkout_cross_sells.

Exibição — o cliente pede GET /api/v1/checkout/:id, o sistema busca o checkout e seus cross-sells, serializa e devolve o array populado.

Logging

  • Audit trail existente, rastreando CREATE, UPDATE e DELETE de checkout_cross_sells, com todos os campos auditados.
  • Log INFO na criação e edição; WARN em tentativas de violação de regra de negócio; ERROR em falhas de validação ou processamento.

Performance

  • Índices criados para as consultas frequentes, incluindo (primary_checkout_id, order) para as consultas ordenadas.
  • Prevenir N+1 com includes(:checkout_cross_sells) quando necessário.
  • Cache de fragmento do JSON de cross-sells por checkout, invalidado quando os cross-sells mudam.

Rollback

  • Implementar o método down na migration e fazer backup do banco antes de rodá-la em produção.
  • Feature toggle por variável de ambiente ENABLE_CROSS_SELL_FEATURE, com rollout gradual por organização.

Como verificar

Testes de modelo — validações de presença e numericalidade, validações customizadas (different_checkouts, same_organization), comportamento do enum, to_payload e as relações belongs_to. No Checkout: as relações has_many, o método cross_sell_items e a aceitação de nested attributes.

Testes de controller — a resposta da API V1 inclui o campo de cross-sell, com estrutura correta e ordenação por order. No admin: formulário com nested attributes e interface de criação, edição e exclusão.

Testes de integração — criação de checkout com cross-sells pelo admin, visualização pela API V1, edição pelo admin e validação das regras de negócio.

Factory

ruby FactoryBot.define do factory :checkout_cross_sell do association :primary_checkout, factory: :checkout association :cross_sell_checkout, factory: :checkout description { "Oferta especial!" } reference_value { 100.00 } promotion_value { 80.00 } kind { :order_bump } order { 1 } end end

Checklist de deploy — antes: rodar todos os testes, verificar conformidade com o RuboCop, review completo e backup do banco. Durante: migration em staging, testes de API em staging, deploy em produção, migration em produção e verificação dos logs. Depois: smoke tests das APIs, monitoramento dos logs de erro e validação com dados reais.

Documentação

Implementação: app/models/checkout_cross_sell.rb, app/admin/checkouts.rb, app/controllers/api/v1/checkout_controller.rb.

Cross-sell não recebe desconto de membro — ver as limitações em ../reference/checkout/member_discount.md.