DEV Community

Cover image for [Subscription] Subscription Flow
José Sobral for Reserva INK

Posted on • Edited on

[Subscription] Subscription Flow

Neste manual vamos compartilhar como pensamos o nosso motor de Subscriptions dentro da nossa plataforma.

Chamamos de 'manual' e não 'artigo' pois o objetivo deste documento é ser um guia; por isso, a leitura não precisa ser top down e sim ser uma consulta para sanar eventuais dúvidas.

Esperamos que ajude os DEV's da INK e a todos que este documento tocar :)

Sumário

Seção 1: Principais diferenças de arquitetura entre o PagarMe e a Zoop

  • Bastidores Zoop
  • Diferença PagarMe-Zoop: desenho de endpoints
  • Diferença PagarMe-Zoop: consumo da API via métodos ao inves de JSON's
  • Arquitetura INK para o consumo da API da Zoop

Seção 2: Novos conceitos de arquitetura gerados pela migração da Zoop

  • Motor baseado em Webhooks
  • Expiração Manual de Subscriptions

Seção 3: Atributos essencias de subscription na Zoop e INK

  • due_date
  • due_since
  • expiration_date
  • is_active (loja)
  • is_active (subscription)
  • status
  • trial

Seção 4: Eventos utilizados

  • subscription.created
  • subscription.suspended
  • subscription.expired
  • subscription.active
  • invoice.overdue
  • invoice.paid

Seção 5: UseCases utilizados

  • Hashie Gem
  • Mind Map: Subscription Flow
  • UC's: webhooks
  • UC's: checkout
  • UC's: payment_update

Seção 1: Principais diferenças de arquitetura entre o PagarMe e a Zoop

No início do ano de 2021, demos um grande passo como plataforma: migramos do sistema de pagamentos do PagarMe para a Zoop

Sem entrar no mérito dos prós e contras de cada sistema de pagamento, neste artigo vamos focar no desafio técnico e em como a arquitetura de subscriptions foi idealizada.


Mas antes disso, nos bastidores...

O processo como um todo da migração foi a maior desafio técnico que os Devs da casa tiveram em suas carreiras naquele momento.

E, como todo o processo onde se adquire experiência, muita
coisa é feita na base de porrada e quebração de cabeça.

A história completa dos bastidores da migração ainda será um artigo, mas no resumo, alguns pontos relevantes:

  • Subdimensionamos o trabalho que seria migrar de um motor de assinaturas para outro
  • Na nossa arquitetura antiga, não suspendiamos o serviço de assinatura quando ele expirava, por isso não tinhamos experiência em como fazê-lo at all
  • Por fim, mais importante, quando o assunto é pagamento muito cuidado com a ânsia de lançar logo sua V0...lançar a migração de pagamento da forma que fizemos, foi bastante arriscado e pouco sustentável em matéria de gestão de conhecimento; isso gerou mais de 2 meses de trabalho posterior ao deploy, entre consertar bugs e readaptar novos motores. Além, é claro, de muita dor de cabeça.

Contudo, cá estamos com um motor novo de assinatura e buscando uma forma de registrar os passos que demos. VQV!


Voltando...

Tecnicamente, a maior diferença entre o PagarMe e a Zoop, é que o PagarMe é desenhado para ser implementando com muita velocidade e pouca customização.

Na prática, isso gera duas implicações:

  1. Sistema de pagamento do pagarme chega com os endpoints preparados para a sua aplicação
  2. O consumo da API se da através de métodos desenhados para a sua aplicação

Vamos a cada uma delas!

1-Sistema de pagamento do pagarme chega com os endpoints preparados para a sua aplicação

Por exemplo, para darmos um fetch em uma Subscription pelo PagarMe fazemos:

require 'pagarme'

PagarMe.api_key = "SUA_API_KEY"

subscription_id = "ID_DA_ASSINATURA"
subscription = PagarMe::Subscription.find_by_id(subscription_id)
Enter fullscreen mode Exit fullscreen mode

Já pela Zoop, precisamos...

a) Criar uma classe que recebe os endpoints
b) Configurar cada um dos tipos de clients, com suas respectivas autenticações
c) No caso da nossa aplicação, criamos uma classe intermediária que modulariza o consumo da API no client side (nosso lado)
d) Por fim, darmos o fetch

A seguir, cada ponto com exemplos...

--

a) Criar uma classe que recebe os endpoints

app> service_layers > zoop > plans_and_subscriptions > api > endpoints.rb

class Zoop::PlansAndSubscriptions::Api::Endpoints
  def initialize(client, clientV2)
    @client = client
    @clientV2 = clientV2
  end

...

  def subscription_details(subscription_id:)
    path = "subscriptions/#{subscription_id}"
    data = { subscription_id: subscription_id }
    response = @clientV2.get(path: path, data: data)
    begin
      JSON.parse(response)
    rescue => error

      puts "JSON Parse error => #{error}"
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

b) Configurar cada um dos tipos de clients, com suas respectivas autenticações

app> service_layers > zoop > auth > rest_client_api.rb

class Zoop::Auth::RestClientApi
  def initialize
    @api_key = 'key'
    @mkt_place_id = 'key'
    @api_url = 'key'
  end

  def post(path:,data:)
    url = url(path)
    Rails.logger.info data

    request = RestClient::Request.new(method: :post, url: url,payload: data, user: @api_key)
    response = request.execute

    response
  end
...
end
Enter fullscreen mode Exit fullscreen mode

c)No caso da nossa aplicação, criamos uma classe intermediária que modulariza o consumo da API no client side (nosso lado):

app> service_layers > zoop > plans_and_subscriptions > subscriptions > details.rb

class Zoop::PlansAndSubscriptions::Subscriptions::Details
    def initialize(subscription_id:)
        @subscription_id = subscription_id
        @endpoints = Zoop::PlansAndSubscriptions::Api::Endpoints.new(Zoop::Auth::RestClientApi.new, Zoop::Auth::RestClientApiV2.new)
    end

    def execute
        subscription_details = @endpoints.subscription_details(subscription_id: @subscription_id)
    end   
end
Enter fullscreen mode Exit fullscreen mode

d)Por fim, darmos o fetch

subscription_details = Zoop::PlansAndSubscriptions::Subscriptions::Details.new(
subscription_id: @subscription.zoop_subscription_id).execute
Enter fullscreen mode Exit fullscreen mode

--

2-O consumo da API se da através de métodos desenhados para a sua aplicação

No pagarMe, para você acessar o fim do período de uma subscription (atributo current_period_end) é feito através de:

irb(main):003:0> PagarMe::Subscription.all.last.current_period_end
RestClient.get "https://api.pagar.me/1/subscriptions", "{\"page\":1,\"count\":10}", "Accept"=>"application/json", "Content-Length"=>"21", "Content-Type"=>"application/json; charset=utf8", "User-Agent"=>"pagarme-ruby/2.4.0", "X-PagarMe-User-Agent"=>"pagarme-ruby/2.4.0"
# => 200 OK | application/json 19666 bytes, 0.15s
=> "2021-03-10T00:27:31.849Z"
irb(main):004:0> 

Enter fullscreen mode Exit fullscreen mode

Já na Zoop, consumindo a API você recebe JSON's. Com isso, para acessar o fim do período de uma subscription (atributo expiration_date) fazemos:

irb(main):004:0> subscription_details = Zoop::PlansAndSubscriptions::Subscriptions::Details.new(
irb(main):005:1* subscription_id: Subscription.all.last.zoop_subscription_id).execute
  Subscription Load (2.2ms)  SELECT "subscriptions".* FROM "subscriptions" WHERE "subscriptions"."deleted_at" IS NULL ORDER BY "subscriptions"."id" DESC LIMIT $1  [["LIMIT", 1]]
=> {"id"=>"33ea0db33dc3430489527a6f1f2c1b6d", "marketplace_id"=>"aa671febc9d4466b9f34134327c56d20", "plan"=>"561ce6726be74b16b9fe4c51f6a94b03", "currency"=>"BRL", "updated_at"=>"2021-08-02T23:18:30+00:00", "tolerance_period"=>nil, "on_behalf_of"=>"36fabbcd71a540a2b4a34ae338b58069", "payment_method"=>"credit", "due_since"=>nil, "expiration_date"=>"2021-08-16T23:00:00", "created_at"=>"2021-08-02T22:45:53+00:00", "suspended_at"=>"2021-08-02T23:18:30+00:00", "due_date"=>"2021-08-16", "status"=>"suspended", "amount"=>9900, "customer"=>"10066c2410f9427ea3d6553b8dc8baeb"}
irb(main):006:0> subscription_details["expiration_date"]
=> "2021-08-16T23:00:00"
irb(main):007:0> 
Enter fullscreen mode Exit fullscreen mode

Essas duas diferenças já mudam drasticamente a maneira como todas as entidades de assinaturas vão se relacionar entre API-INK.

Contudo, a principal questão para o nosso time foi que não pensamos nos processos de interface da API com a nossa aplicação, como

  • renovação de pagamento
  • expiração de subscription
  • exclusão de multiplas subscriptions em caso de upsell
  • etc

pois no PagarMe, esses processos eram realizados automaticamente no lado do PagarMe.

Com isso, vamos aos principais conceitos de arquitetura trazidos pela escolha da Zoop


Seção 2: Novos conceitos gerados pela migração da Zoop

Motor baseado em Webhooks

Pela Zoop, recomenda-se que usemos os chamados Webhooks para "ouvir" as interações entre a API e a INK.

Na prática, um Webhook é de fato um "gancho" para que quando uma ação ocorra na API, haja uma chamada para o client side informando a ação e o que foi alterado.

Por exemplo, toda vez que houver uma criação de subscription na Zoop, haverá um trigger no evento subscription.created; com isso, podemos criar um Webhook que, toda vez que houver um subscription.created, possamos usar informações deste evento para atualizar nosso banco e aplicar nossas próprias regras de negócio.

--

Expiração Manual de Subscriptions

Na Zoop, a expiração de uma subscription é feita manualmente.

O que isso significa?

Simples, nós que precisamos dizer quando uma assinatura expira

Na prática isso trouxe uma grande dor de cabeça:

Como vamos setar e atualizar o campo de expiração para que o fluxo de renovação e suspensão de assinatura funcione corretamente?

A resposta à esta pergunta virá na explicação dos UseCases utilizados na nossa aplicação


Seção 3: Atributos essencias de subscription na Zoop e INK

Nesta seção, nosso objetivo é explicar cada um dos principais atributos gerados pela subscription na zoop e na nossa base.

Atributos Zoop

  • due_date: Data da próxima cobrança para a Subscription
  • expiration_date: Data de expiração para a Subscription
  • due_since: Data do último pagamento atribuido a subscription
  • status: Status que se encontra a Subscription (active, suspended, expired)

Atributos INK

  • expiration_date: Reflexo direto do expiration_date da Zoop
  • is_active (store): Booleano que indica se a loja está ativa ou não; caso não esteja, o usuário não conseguirá realizar vendas em sua loja.
  • is_active (subscription): Booleano que indica se a subscripton está ativa ou não; caso o usuário não possua nenhuma subscription ativa (is_active = true), a loja terá, em tese, um is_active = false
  • trial: Booleano que diz se a condição de trial para subscription é true ou false
  • status: Reflexo direto do status vindo da Zoop

A interface entre os atributos ficará clara na seção de UseCases Utilizados


Seção 4: Eventos utilizados

Nesta seção, vamos explicitar cada um dos eventos utlizados pela nossa aplicação, um payload de exemplo e quando idealizamos que eles seriam triggados

subscription.created

  • Payload
 {
            "created_at": "2021-08-04T12:05:30+00:00",
            "payload": {
                "marketplace_id": "aa671febc9d4466b9f34134327c56d20",
                "suspended_at": null,
                "payment_method": "credit",
                "currency": "BRL",
                "expiration_date": null,
                "due_since": null,
                "amount": 49900,
                "updated_at": "2021-08-04T12:05:30+00:00",
                "created_at": "2021-08-04T12:05:30+00:00",
                "status": "active",
                "id": "b66d91d2963445ef81aab5f173551b21",
                "due_date": "2021-08-18",
                "on_behalf_of": "36fabbcd71a540a2b4a34ae338b58069",
                "plan": "561ce6726be74b16b9fe4c51f6a94b03",
                "tolerance_period": null,
                "customer": "25b1a9c31ebc43fb85975eed7433761e"
            },
            "resource": "event",
            "status": "succeeded",
            "uri": "/v1/marketplaces/aa671febc9d4466b9f34134327c56d20/events/5a55ea1828bc409087f4b6f02bdef9f3",
            "id": "5a55ea1828bc409087f4b6f02bdef9f3",
            "dispatches": [
                {
                    "created_at": "2021-08-04T12:05:40+00:00",
                    "status": "succeeded",
                    "replay": false,
                    "webhook_id": "59c61025a6384edd8295493a4d0f55f5"
                }
            ],
            "type": "subscription.created"
        }
Enter fullscreen mode Exit fullscreen mode
  • Trigger

No checkout de subscription (app>app_core>subscription>use_cases>checkout>process_subscription_checkout.rb), há a criação da subscription da Zoop e neste momento imaginamos que este evento será triggado.

subscription.suspended

  • Payload
 {
            "created_at": "2021-08-04T12:15:18+00:00",
            "payload": {
                "payment_method": "credit",
                "currency": "BRL",
                "tolerance_period": null,
                "updated_at": "2021-08-04T12:15:18+00:00",
                "expiration_date": "2021-08-18T23:00:00",
                "created_at": "2021-08-04T12:05:30+00:00",
                "customer": "25b1a9c31ebc43fb85975eed7433761e",
                "on_behalf_of": "36fabbcd71a540a2b4a34ae338b58069",
                "marketplace_id": "aa671febc9d4466b9f34134327c56d20",
                "amount": 49900,
                "suspended_at": "2021-08-04T12:15:18+00:00",
                "status": "suspended",
                "plan": "561ce6726be74b16b9fe4c51f6a94b03",
                "due_date": "2021-08-18",
                "id": "b66d91d2963445ef81aab5f173551b21",
                "due_since": null
            },
            "resource": "event",
            "status": "succeeded",
            "uri": "/v1/marketplaces/aa671febc9d4466b9f34134327c56d20/events/c1a933089175476d86e902a003b3b01e",
            "id": "c1a933089175476d86e902a003b3b01e",
            "dispatches": [
                {
                    "created_at": "2021-08-04T12:15:28+00:00",
                    "status": "succeeded",
                    "replay": false,
                    "webhook_id": "1c10b9ecf9c94687a43d7946d35f6141"
                }
            ],
            "type": "subscription.suspended"
        }
Enter fullscreen mode Exit fullscreen mode
  • Trigger

Quando o usuário acessa a rota de /user/subscription, caso sua subscription esteja ativa, irá aparecer um botão escrito "Suspender Assinatura". No click deste botão é suspensa a assinatura e é trigado este evento (app>app_core>subscription>use_cases>webhooks>handle_subscription_suspended_event.rb)

subscription.expired

  • Payload
 {
            "created_at": "2021-08-04T12:28:01+00:00",
            "payload": {
                "payment_method": "credit",
                "on_behalf_of": "36fabbcd71a540a2b4a34ae338b58069",
                "created_at": "2021-04-19T19:26:51+00:00",
                "suspended_at": null,
                "tolerance_period": null,
                "expiration_date": "2021-06-10T09:01:54",
                "amount": 12900,
                "updated_at": "2021-08-04T12:28:01+00:00",
                "id": "67c9583d6b1f408ca2bbad23677901d6",
                "plan": "561ce6726be74b16b9fe4c51f6a94b03",
                "due_date": "2021-05-03",
                "customer": "f4f34cd05e2743029cab8e47a1e03e35",
                "status": "expired",
                "due_since": null,
                "currency": "BRL",
                "marketplace_id": "aa671febc9d4466b9f34134327c56d20"
            },
            "resource": "event",
            "status": "succeeded",
            "uri": "/v1/marketplaces/aa671febc9d4466b9f34134327c56d20/events/12e32795b0ce49dc8a914e36723514f3",
            "id": "12e32795b0ce49dc8a914e36723514f3",
            "dispatches": [
                {
                    "created_at": "2021-08-04T12:28:11+00:00",
                    "status": "succeeded",
                    "replay": false,
                    "webhook_id": "5388ead647cb4e9d93c78f33797e90ff"
                }
            ],
            "type": "subscription.expired"
        }
Enter fullscreen mode Exit fullscreen mode
  • Trigger

Este evento será triggado quando a subscription chegar na sua data de expiração e não houver renovação da assinatura. Isso acontece quando o usuário não pagou a subscription e, no dia de expiração, a assinatura irá de active para expired

subscription.active

  • Payload
  {
            "created_at": "2021-08-04T12:58:40+00:00",
            "payload": {
                "payment_method": "credit",
                "currency": "BRL",
                "tolerance_period": null,
                "updated_at": "2021-08-04T12:58:40+00:00",
                "expiration_date": "2021-09-03T23:59:59",
                "created_at": "2021-06-16T13:52:43+00:00",
                "customer": "74369907fd1f446d9777c201be73eef6",
                "on_behalf_of": "36fabbcd71a540a2b4a34ae338b58069",
                "marketplace_id": "aa671febc9d4466b9f34134327c56d20",
                "amount": 12900,
                "suspended_at": null,
                "status": "active",
                "plan": "561ce6726be74b16b9fe4c51f6a94b03",
                "due_date": "2021-07-30",
                "id": "dc9ace773b63461c81139bd00d33c9d0",
                "due_since": "2021-06-30"
            },
            "resource": "event",
            "status": "succeeded",
            "uri": "/v1/marketplaces/aa671febc9d4466b9f34134327c56d20/events/b71e8a87ff0d41798be9033118342c9f",
            "id": "b71e8a87ff0d41798be9033118342c9f",
            "dispatches": [
                {
                    "created_at": "2021-08-04T12:58:50+00:00",
                    "status": "succeeded",
                    "replay": false,
                    "webhook_id": "3286bf41cbf2474d87e29e2331d0fef5"
                }
            ],
            "type": "subscription.active"
        }
Enter fullscreen mode Exit fullscreen mode
  • Trigger

Este evento terá trigger quando uma assinatura for suspensa e posteriormente for reativada. Isso pode ser feito através do /user/subscription; quando o usuário suspender a assinatura, aparecerá uma opção "Reativar Assinatura". O click deste botão irá reativar a assinatura e triggar este evento

invoice.overdue

  • Payload
  {
            "resource": "event",
            "created_at": "2021-08-04T13:02:13+00:00",
            "uri": "/v1/marketplaces/aa671febc9d4466b9f34134327c56d20/events/36a24c2ab51746e4887ab5b3d318f32f",
            "status": "succeeded",
            "id": "36a24c2ab51746e4887ab5b3d318f32f",
            "dispatches": [
                {
                    "created_at": "2021-08-04T13:02:23+00:00",
                    "webhook_id": "b23ea12e177c482083b4ea02dae54f7d",
                    "status": "succeeded",
                    "replay": false
                }
            ],
            "type": "invoice.overdue",
            "payload": {
                "id": "56fd660c44c240afb0a3869c6398ab40",
                "setup_amount": null,
                "tolerance_period": null,
                "transactions": [],
                "description": null,
                "on_behalf_of": "36fabbcd71a540a2b4a34ae338b58069",
                "amount": 12900,
                "voided_at": null,
                "due_date": "2021-07-30T00:00:00",
                "status": "failed",
                "retries": 3,
                "payment_method": "credit",
                "paid_at": null,
                "subscription": "dc9ace773b63461c81139bd00d33c9d0",
                "expiration_date": null,
                "resource": "invoice",
                "invoice_customer": {
                    "first_name": "",
                    "last_name": null,
                    "taxpayer_id": null,
                    "id": "74369907fd1f446d9777c201be73eef6",
                    "email": "laurormn@gmail.com"
                },
                "max_retries": 3
            }
        }
Enter fullscreen mode Exit fullscreen mode
  • Trigger

Primeiramente, um "invoice" é uma fatura. O conceito de fatura geralmente está relacionado com recorrência, quase como se fosse uma "conta" que chega periodicamente para você.

Na nossa aplicação, a única "conta" que chega recorrentemente para os usuários é a subscription. Logo, todos os eventos de invoice, hoje, estão relacionados a subscription.

O invoice.overdue significa que aquela fatura teve o seu número máximo de tentativas de pagamento atingido. Ou seja, o cartão de crédito associado pode estar sem limite disponível, pode não estar mais válido (caso de cartão de crédito virtual que é excluido) entre outras possibilidades. O fato é que o pagamento não aconteceu.

Este evento acontece no due_date (data da pŕoxima cobrança) da assinatura. Se no dia da cobrança ela for mal sucedida, este evento será trigado.

invoice.paid

  • Payload
{
            "resource": "event",
            "created_at": "2021-08-04T13:03:13+00:00",
            "uri": "/v1/marketplaces/aa671febc9d4466b9f34134327c56d20/events/7dbdc97c63df45f8bd40e80595b9e7a7",
            "status": "succeeded",
            "id": "7dbdc97c63df45f8bd40e80595b9e7a7",
            "dispatches": [
                {
                    "created_at": "2021-08-04T13:03:23+00:00",
                    "webhook_id": "9d453bca7b9947d4987d5e02dec0c692",
                    "status": "succeeded",
                    "replay": false
                }
            ],
            "type": "invoice.paid",
            "payload": {
                "payment_method": "credit",
                "status": "paid",
                "invoice_customer": {
                    "id": "888e1165cddf44d0a8eab133f41f53e5",
                    "taxpayer_id": "34498991000104",
                    "email": "suporte@arquiteturaequestre.com.br",
                    "first_name": "",
                    "last_name": null
                },
                "amount": 12900,
                "voided_at": null,
                "description": null,
                "due_date": "2021-08-04T00:00:00",
                "setup_amount": null,
                "retries": 0,
                "tolerance_period": null,
                "resource": "invoice",
                "paid_at": "2021-08-04T13:03:13+00:00",
                "max_retries": 3,
                "transactions": [
                    {
                        "masked_card": "5226***2794",
                        "card_brand": "MasterCard",
                        "currency": "BRL",
                        "id": "f00d64630d3d4af881a145371775f6b2"
                    }
                ],
                "on_behalf_of": "36fabbcd71a540a2b4a34ae338b58069",
                "id": "35c9881a40654d1d87b1d2e0fe8251b4",
                "subscription": "6bf2e484c5a249a98ca3e12196c5c41c",
                "expiration_date": null
            }
        }
Enter fullscreen mode Exit fullscreen mode
  • Trigger

Como já explicado no evento anterior, um invoice é uma fatura. O evento ìnvoice.paid diz que uma fatura foi paga. Inclusive neste evento podemos saber a transaction_id relacionada a subscription.


Seção 5: UseCases utilizados

Antes de começarmos a falar sobre os UC, precisamos falar de dois pontos essenciais:

  1. Hashie Gem
  2. Como projetamos cada UC

1-Hashie Gem

Esta é uma Gem que usamos no Flow de Subscription que facilitou muito o nosso lado na hora de manipular os payloads chegados pela Zoop através dos Webhooks

Alt Text

Para resumir a história do uso por de trás da Gem, vamos falar apenas sobre a dor resolvida e como implementamos ela no projeto

  • Dor resolvida Imagina que, recebendo um hash do evento invoice.paid, por exemplo, queiramos acessar a transaction_id desta fatura.

Para isso, quando chega o payload de exemplo:

 {
            "resource": "event",
            "created_at": "2021-08-04T14:42:13+00:00",
            "uri": "/v1/marketplaces/aa671febc9d4466b9f34134327c56d20/events/190dc7e15d414005aafe90d028ec54c3",
            "status": "succeeded",
            "id": "190dc7e15d414005aafe90d028ec54c3",
            "dispatches": [
                {
                    "created_at": "2021-08-04T14:42:23+00:00",
                    "webhook_id": "9d453bca7b9947d4987d5e02dec0c692",
                    "status": "succeeded",
                    "replay": false
                }
            ],
            "type": "invoice.paid",
            "payload": {
                "payment_method": "credit",
                "status": "paid",
                "invoice_customer": {
                    "id": "a6080bb9b36c4af8a9c9fce651fd7ea8",
                    "taxpayer_id": "39123487828",
                    "email": "weynerenan@gmail.com",
                    "first_name": "Renan",
                    "last_name": "Weyne"
                },
                "amount": 12900,
                "voided_at": null,
                "description": "Fatura avulsa de assinatura para loja 3w",
                "due_date": "2021-08-04T00:00:00",
                "setup_amount": null,
                "retries": 0,
                "tolerance_period": null,
                "resource": "invoice",
                "paid_at": "2021-08-04T14:42:13+00:00",
                "max_retries": 3,
                "transactions": [
                    {
                        "masked_card": "5502***5987",
                        "card_brand": "MasterCard",
                        "currency": "BRL",
                        "id": "dcab432a48e445e3b5c42a0e91400bdb"
                    }
                ],
                "on_behalf_of": "36fabbcd71a540a2b4a34ae338b58069",
                "id": "f304c7eb4d8d464b96e94e44a33a1e35",
                "subscription": "5c99dfbb6dd745db9e75159f9b8912e9",
                "expiration_date": null
            }
        }
Enter fullscreen mode Exit fullscreen mode

Podemos colocar este numa variável event e extrair o transaction_id através de:

event["payload"]["transactions"][0]["id"]
Enter fullscreen mode Exit fullscreen mode

O Hashie chega para prevenir esta sintax! Com ele, podemos buscar o chave de um hash através de métodos intuitivos e que facilitam muito o manejo de objetos com estrutura em hash.

No exemplo do payload, com o Hashie, poderíamos extrair o transaction_id fazendo:

@event_params = event_params
@transactions = (@event_params.deep_find("transactions"))[0]
@transactions["id"]
Enter fullscreen mode Exit fullscreen mode

E por isso usamos o Hashie! Para facilitar a sintax de manipulação de objetos com estrutura em hash :)

  • Implementação no Projeto

A implementação aconteceu em 3 etapas

a) Adicionar no projeto os módulos associados ao Hashie.
app>services>hashie>deep_find.rb
app>services>hashie>deep_locate.rb
app>services>hashie>deep_fetch.rb

b) Criamos um Presenter* que recebe o hash como params na chamada da nossa aplicação pelo webhook da Zoop chamado ParametersPresenter

class ParametersPresenter < BasePresenter
  def initialize(event_params:)
    @event_params = event_params
  end

  def build
    @event_params.permit! rescue nil
    hash_params = @event_params.to_h rescue @event_params

    hash_params.extend Hashie::Extensions::DeepFind
    hash_params.extend Hashie::Extensions::DeepFetch
    hash_params.extend Hashie::Extensions::DeepLocate
  end
end
Enter fullscreen mode Exit fullscreen mode

Este Presenter tem como finalidade adaptar um objeto que chega como ActionController::Paramaters para um objeto Hash com métodos do Hashie como o deep_find mostrado anteriormente.

c) Damos um build em cada UC para transformar um ActionController::Parameters um Hash com PowerUps da gem do Hashie

Exemplo:

module Subscription::UseCases::Webhooks
  class HandleInvoicePaidEvent
    def self.build(event_params:)
      new(
        event_params: ParametersPresenter.new(event_params: event_params).build
      )
    end
Enter fullscreen mode Exit fullscreen mode

Pronto! Agora já sabemos como usar o Hashie :)

2-Como projetamos cada UC

Para este projeto, estreiamos o conceito de Mind Map na INK.

O objetivo era ter visualmente um fluxo de como cada ação do usuário refletia um evento na Zoop e, consequentemente, um script na nossa aplicação.

Este Mind Map foi essencial para estruturarmos a interface de cada atributo da INK com a Zoop e pensarmos na execução de cada UC

Hora dos UC's...

use_cases>webhooks

  • handle_invoice_paid_event.rb

Abaixo cada um dos métodos que são executados no evento invoice.paid

app>app_core>subscription>use_cases>webhooks>handle_invoice_paid.event.rb

    def execute
      set_active_status_into_db
      reactivate_status_into_zoop
      set_store_is_active
      set_subscription_is_active
      set_db_expiration_date
      set_zoop_expiration_date
      remove_trial
      create_subscription_invoice_record
    end
Enter fullscreen mode Exit fullscreen mode

Interfaces:
a) Atributo status sendo setado active na Zoop e na INK
b) Atributo is_active da loja sendo setado para true
c) Atributo is_active da subscription sendo setado para true
d) Atributo trial sendo setado para false pois houve pagamento
e) Criação de um SubscriptionInvoice para acessarmos o invoice na nossa base com mais facilidade.
f) Atributo expiration_date sendo atualizado na Zoop e na INK. Ambos serão atualizados para o dia de hoje + dias do plano (anual, mensal); o final do dia que resultará esta operação, será o novo expiration_date

    def set_zoop_expiration_date
      current_expiration_date       = @subscription.expiration_date
      plan_days                     = @subscription.plan.interval

      Subscription::UpdateExpirationDateJob.perform_later(
        subscription: @subscription,
        expiration_date: plan_days.days.from_now.end_of_day.strftime('%Y-%m-%dT%H:%M:%S')
      )
    end
Enter fullscreen mode Exit fullscreen mode

PS: A escolha do final do dia foi proposital. Isso ocorre pois o due_date (próxima cobrança) irá acontecer no início do dia, então caso não haja pagamento ao longo do dia, no final do dia haverá expiração

  • handle_invoice_overdue.rb

Abaixo cada um dos métodos que são executados no evento invoice.overdue

      def execute
        notify_invalid_credit_card_to_user
      end
Enter fullscreen mode Exit fullscreen mode

Interfaces
a) Não há interfaces de atributos; aqui apenas notificamos o usuário do cartão de crédito inválido.

  • handle_subscription_active_event.rb

Abaixo cada um dos métodos que são executados no evento subscription.active

    def execute
      set_status
    end
Enter fullscreen mode Exit fullscreen mode

Interface
a) Atualizamos o atributo status na nossa base para active.

  • handle_subscription_created_event.rb

Abaixo cada um dos métodos que são executados no evento subscription.created

    def execute
      set_trial
      set_status
      set_db_expiration_date
      set_zoop_expiration_date
    end
Enter fullscreen mode Exit fullscreen mode

Interfaces
a) Setamos o atributo trial para true dado que a subscription acabou de ser criada
b) Setamos o atributo status para active
c) Setamos o atributo expiration_date para o due_date da subscription, no final do dia.

PS: A escolha do final do dia foi proposital. Isso ocorre pois o due_date (próxima cobrança) irá acontecer no início do dia, então caso não haja pagamento ao longo do dia, no final do dia haverá expiração

  • handle_subscription_expired_event.rb

Abaixo cada um dos métodos que são executados no evento subscription.expired

    def execute
      set_status
      set_subscription_is_active
      handle_store_is_active
    end
Enter fullscreen mode Exit fullscreen mode

Interfaces
a) Atributo status setado para expired
b) Atributo is_active da subscription é setado para false
c) Caso não hajam subscriptions com status active para aquele usuário, sua loja será desativada.

  • handle_subscription_suspended_event.rb

Abaixo cada um dos métodos que são executados no evento subscription.suspended

    def execute
      set_status
      handle_store_block
    end
Enter fullscreen mode Exit fullscreen mode

Interfaces:
a) Setamos o atributo status para suspended
b) Lançamos um BackgroundJob para acontecer no dia da expiração da subscription; quando este BJ rodar, caso não hajam subscription com is_active=true, a loja será desativada.

Isso precisou ser feito pois as subscriptions suspended não migram para expired no dia da expiração; ou seja, elas nunca terão evento subscription.expired e precisamos usar este BJ para assegurar a desativação.

Exemplo:

        {
            "payment_method": "credit",
            "currency": "BRL",
            "tolerance_period": null,
            "updated_at": "2021-06-10T11:56:04+00:00",
            "expiration_date": "2021-06-10T08:56:02",
            "created_at": "2021-02-25T13:21:57+00:00",
            "customer": "a0ddaadd8c474a3193dcb153d88e0ea2",
            "on_behalf_of": "36fabbcd71a540a2b4a34ae338b58069",
            "marketplace_id": "aa671febc9d4466b9f34134327c56d20",
            "amount": 12900,
            "suspended_at": "2021-05-17T19:36:34+00:00",
            "status": "suspended",
            "plan": "561ce6726be74b16b9fe4c51f6a94b03",
            "due_date": "2021-06-11",
            "id": "3ced54f621d340bda03e7b39ceb34caf",
            "due_since": "2021-03-11"
        }
Enter fullscreen mode Exit fullscreen mode

Subscription com expiration_date para 2021-06-10T08:56:02 (data já ocorrida) e mesmo assim com status suspended; mais uma vez, subscriptions suspended não triggam subscription.expired

Checkout

  • process_subscription_checkout.rb
        def execute
            create_buyer_id
            suspend_old_subscription
            create_zoop_subscription
            associate_credit_card_to_customer
            create_subscription_database_subscription
            create_store
        end
Enter fullscreen mode Exit fullscreen mode

Payment Update

  • handle_subscription_reactivation_process.rb
        def execute
            associate_new_credit_card_to_zoop_customer
            associate_new_credit_card_token_to_subscription
            create_single_invoice
        end
Enter fullscreen mode Exit fullscreen mode

Aqui, para reativarmos uma loja que está desativada, há um checkout especial (https://reserva.ink/subscriptions/reactivation/checkout).

Nele, simplesmente associamos um novo cartão de crédito ao usuário e criamos uma fatura avulsa para ser paga instanteamente.

Quando ela for paga, o evento invoice.paid será triggado e a loja será reativada.

  • handle_credit_card_change_process.rb
        def execute
            associate_new_credit_card_to_zoop_customer
            associate_new_credit_card_token_to_subscription
        end
Enter fullscreen mode Exit fullscreen mode

Top comments (0)