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:
- Sistema de pagamento do pagarme chega com os endpoints preparados para a sua aplicação
- 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)
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
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
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
d)Por fim, darmos o fetch
subscription_details = Zoop::PlansAndSubscriptions::Subscriptions::Details.new(
subscription_id: @subscription.zoop_subscription_id).execute
--
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>
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>
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, umis_active = false
-
trial
: Booleano que diz se a condição de trial para subscription étrue
oufalse
-
status
: Reflexo direto dostatus
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"
}
- 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"
}
- 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"
}
- 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"
}
- 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
}
}
- 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
}
}
- 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:
- Hashie Gem
- 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
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 eventoinvoice.paid
, por exemplo, queiramos acessar atransaction_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
}
}
Podemos colocar este numa variável event
e extrair o transaction_id
através de:
event["payload"]["transactions"][0]["id"]
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"]
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
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
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
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
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
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
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
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
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
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"
}
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
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
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
Top comments (0)