<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Reserva INK</title>
    <description>The latest articles on DEV Community by Reserva INK (@reservaink).</description>
    <link>https://dev.to/reservaink</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3060%2F388583cf-d85c-4175-bb38-2b13aa06a9ce.png</url>
      <title>DEV Community: Reserva INK</title>
      <link>https://dev.to/reservaink</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/reservaink"/>
    <language>en</language>
    <item>
      <title>[Subscription] Subscription Flow</title>
      <dc:creator>José Sobral</dc:creator>
      <pubDate>Tue, 20 Dec 2022 19:19:08 +0000</pubDate>
      <link>https://dev.to/reservaink/subscription-subscription-flow-55li</link>
      <guid>https://dev.to/reservaink/subscription-subscription-flow-55li</guid>
      <description>&lt;p&gt;Neste manual vamos compartilhar como pensamos o nosso motor de Subscriptions dentro da nossa plataforma.&lt;/p&gt;

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

&lt;p&gt;Esperamos que ajude os DEV's da INK e a todos que este documento tocar :)&lt;/p&gt;

&lt;h1&gt;
  
  
  Sumário
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Seção 1: Principais diferenças de arquitetura entre o PagarMe e a Zoop
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Bastidores Zoop&lt;/li&gt;
&lt;li&gt;Diferença PagarMe-Zoop: desenho de endpoints&lt;/li&gt;
&lt;li&gt;Diferença PagarMe-Zoop: consumo da API via métodos ao inves de JSON's&lt;/li&gt;
&lt;li&gt;Arquitetura INK para o consumo da API da Zoop&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Seção 2: Novos conceitos de arquitetura gerados pela migração da Zoop
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Motor baseado em Webhooks&lt;/li&gt;
&lt;li&gt;Expiração Manual de Subscriptions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Seção 3: Atributos essencias de subscription na Zoop e INK
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;due_date&lt;/li&gt;
&lt;li&gt;due_since&lt;/li&gt;
&lt;li&gt;expiration_date&lt;/li&gt;
&lt;li&gt;is_active (loja)&lt;/li&gt;
&lt;li&gt;is_active (subscription)&lt;/li&gt;
&lt;li&gt;status&lt;/li&gt;
&lt;li&gt;trial&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Seção 4: Eventos utilizados
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;subscription.created&lt;/li&gt;
&lt;li&gt;subscription.suspended&lt;/li&gt;
&lt;li&gt;subscription.expired&lt;/li&gt;
&lt;li&gt;subscription.active&lt;/li&gt;
&lt;li&gt;invoice.overdue&lt;/li&gt;
&lt;li&gt;invoice.paid&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Seção 5: UseCases utilizados
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Hashie Gem&lt;/li&gt;
&lt;li&gt;Mind Map: Subscription Flow&lt;/li&gt;
&lt;li&gt;UC's: webhooks&lt;/li&gt;
&lt;li&gt;UC's: checkout&lt;/li&gt;
&lt;li&gt;UC's: payment_update&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Seção 1: Principais diferenças de arquitetura entre o PagarMe e a Zoop
&lt;/h2&gt;

&lt;p&gt;No início do ano de 2021, demos um grande passo como plataforma: &lt;strong&gt;migramos do sistema de pagamentos do &lt;a href="https://pagar.me/"&gt;PagarMe&lt;/a&gt; para a &lt;a href="https://zoop.com.br/"&gt;Zoop&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h4&gt;
  
  
  Mas antes disso, nos bastidores...
&lt;/h4&gt;

&lt;p&gt;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. &lt;/p&gt;

&lt;p&gt;E, como todo o processo onde se adquire experiência, muita &lt;br&gt;
coisa é feita na base de porrada e quebração de cabeça.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Subdimensionamos o trabalho que seria migrar de um motor de assinaturas para outro&lt;/li&gt;
&lt;li&gt;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 &lt;em&gt;at all&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Contudo, cá estamos com um motor novo de assinatura e buscando uma forma de registrar os passos que demos. VQV!&lt;/p&gt;


&lt;h4&gt;
  
  
  Voltando...
&lt;/h4&gt;

&lt;p&gt;Tecnicamente, a maior diferença entre o PagarMe e a Zoop, é que o PagarMe é &lt;strong&gt;desenhado para ser implementando com muita velocidade e pouca customização&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Na prática, isso gera duas implicações: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Sistema de pagamento do pagarme chega com os endpoints preparados para a sua aplicação&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;O consumo da API se da através de métodos desenhados para a sua aplicação&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Vamos a cada uma delas!&lt;/p&gt;

&lt;blockquote&gt;
&lt;h4&gt;
  
  
  1-Sistema de pagamento do pagarme chega com os endpoints preparados para a sua aplicação
&lt;/h4&gt;
&lt;/blockquote&gt;

&lt;p&gt;Por exemplo, para darmos um fetch em uma Subscription pelo PagarMe fazemos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require 'pagarme'

PagarMe.api_key = "SUA_API_KEY"

subscription_id = "ID_DA_ASSINATURA"
subscription = PagarMe::Subscription.find_by_id(subscription_id)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Já pela Zoop, precisamos...&lt;/p&gt;

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

&lt;p&gt;A seguir, cada ponto com exemplos...&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;
&lt;h4&gt;
  
  
  a) Criar uma classe que recebe os endpoints
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app&amp;gt; service_layers &amp;gt; zoop &amp;gt; plans_and_subscriptions &amp;gt; api &amp;gt; 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 =&amp;gt; error

      puts "JSON Parse error =&amp;gt; #{error}"
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  b) Configurar cada um dos tipos de clients, com suas respectivas autenticações
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app&amp;gt; service_layers &amp;gt; zoop &amp;gt; auth &amp;gt; 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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app&amp;gt; service_layers &amp;gt; zoop &amp;gt; plans_and_subscriptions &amp;gt; subscriptions &amp;gt; 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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  d)Por fim, darmos o fetch
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;subscription_details = Zoop::PlansAndSubscriptions::Subscriptions::Details.new(
subscription_id: @subscription.zoop_subscription_id).execute
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;--&lt;/p&gt;

&lt;blockquote&gt;
&lt;h4&gt;
  
  
  2-O consumo da API se da através de métodos desenhados para a sua aplicação
&lt;/h4&gt;
&lt;/blockquote&gt;

&lt;p&gt;No pagarMe, para você acessar o fim do período de uma subscription (atributo &lt;code&gt;current_period_end&lt;/code&gt;) é feito através de:&lt;br&gt;
&lt;/p&gt;

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

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;irb(main):004:0&amp;gt; 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]]
=&amp;gt; {"id"=&amp;gt;"33ea0db33dc3430489527a6f1f2c1b6d", "marketplace_id"=&amp;gt;"aa671febc9d4466b9f34134327c56d20", "plan"=&amp;gt;"561ce6726be74b16b9fe4c51f6a94b03", "currency"=&amp;gt;"BRL", "updated_at"=&amp;gt;"2021-08-02T23:18:30+00:00", "tolerance_period"=&amp;gt;nil, "on_behalf_of"=&amp;gt;"36fabbcd71a540a2b4a34ae338b58069", "payment_method"=&amp;gt;"credit", "due_since"=&amp;gt;nil, "expiration_date"=&amp;gt;"2021-08-16T23:00:00", "created_at"=&amp;gt;"2021-08-02T22:45:53+00:00", "suspended_at"=&amp;gt;"2021-08-02T23:18:30+00:00", "due_date"=&amp;gt;"2021-08-16", "status"=&amp;gt;"suspended", "amount"=&amp;gt;9900, "customer"=&amp;gt;"10066c2410f9427ea3d6553b8dc8baeb"}
irb(main):006:0&amp;gt; subscription_details["expiration_date"]
=&amp;gt; "2021-08-16T23:00:00"
irb(main):007:0&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;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&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;renovação de pagamento &lt;/li&gt;
&lt;li&gt;expiração de subscription&lt;/li&gt;
&lt;li&gt;exclusão de multiplas subscriptions em caso de upsell&lt;/li&gt;
&lt;li&gt;etc &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;pois no PagarMe, esses processos eram realizados automaticamente no lado do PagarMe.&lt;/p&gt;

&lt;p&gt;Com isso, vamos aos principais conceitos de arquitetura trazidos pela escolha da Zoop&lt;/p&gt;




&lt;h2&gt;
  
  
  Seção 2: Novos conceitos gerados pela migração da Zoop
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Motor baseado em Webhooks
&lt;/h4&gt;

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

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

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

&lt;p&gt;--&lt;/p&gt;

&lt;h4&gt;
  
  
  Expiração Manual de Subscriptions
&lt;/h4&gt;

&lt;p&gt;Na Zoop, a expiração de uma subscription é feita manualmente. &lt;/p&gt;

&lt;p&gt;O que isso significa?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Simples, nós que precisamos dizer quando uma assinatura expira&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Na prática isso trouxe uma grande dor de cabeça:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Como vamos setar e atualizar o campo de expiração para que o fluxo de renovação e suspensão de assinatura funcione corretamente?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A resposta à esta pergunta virá na explicação dos UseCases utilizados na nossa aplicação&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Seção 3: Atributos essencias de subscription na Zoop e INK
&lt;/h2&gt;

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

&lt;h4&gt;
  
  
  Atributos Zoop
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;due_date&lt;/code&gt;: Data da próxima cobrança para a Subscription&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expiration_date&lt;/code&gt;: Data de expiração para a Subscription&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;due_since&lt;/code&gt;: Data do último pagamento atribuido a subscription&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;status&lt;/code&gt;: Status que se encontra a Subscription (&lt;em&gt;active&lt;/em&gt;, &lt;em&gt;suspended&lt;/em&gt;, &lt;em&gt;expired&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Atributos INK
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;expiration_date&lt;/code&gt;: Reflexo direto do expiration_date da Zoop&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;is_active (store)&lt;/code&gt;: 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. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;is_active (subscription)&lt;/code&gt;: Booleano que indica se a subscripton está ativa ou não; caso o usuário não possua nenhuma subscription ativa (&lt;code&gt;is_active = true&lt;/code&gt;), a loja terá, em tese, um &lt;code&gt;is_active = false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;trial&lt;/code&gt;: Booleano que diz se a condição de trial para subscription é &lt;code&gt;true&lt;/code&gt; ou &lt;code&gt;false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;status&lt;/code&gt;: Reflexo direto do &lt;code&gt;status&lt;/code&gt; vindo da Zoop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A interface entre os atributos ficará clara na seção de &lt;strong&gt;UseCases Utilizados&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Seção 4: Eventos utilizados
&lt;/h2&gt;

&lt;p&gt;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&lt;/p&gt;

&lt;h4&gt;
  
  
  subscription.created
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Payload
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; {
            "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"
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Trigger&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No checkout de subscription (&lt;code&gt;app&amp;gt;app_core&amp;gt;subscription&amp;gt;use_cases&amp;gt;checkout&amp;gt;process_subscription_checkout.rb&lt;/code&gt;), há a criação da subscription da Zoop e neste momento imaginamos que este evento será triggado.&lt;/p&gt;

&lt;h4&gt;
  
  
  subscription.suspended
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Payload
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; {
            "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"
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Trigger&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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 (&lt;code&gt;app&amp;gt;app_core&amp;gt;subscription&amp;gt;use_cases&amp;gt;webhooks&amp;gt;handle_subscription_suspended_event.rb&lt;/code&gt;)&lt;/p&gt;

&lt;h4&gt;
  
  
  subscription.expired
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Payload
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; {
            "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"
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Trigger&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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 &lt;em&gt;active&lt;/em&gt; para &lt;em&gt;expired&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  subscription.active
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Payload
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  {
            "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"
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Trigger&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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&lt;/p&gt;

&lt;h4&gt;
  
  
  invoice.overdue
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Payload
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  {
            "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
            }
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Trigger&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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ê. &lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;O &lt;code&gt;invoice.overdue&lt;/code&gt; 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. &lt;/p&gt;

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

&lt;h4&gt;
  
  
  invoice.paid
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Payload
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
            "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
            }
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Trigger&lt;/li&gt;
&lt;/ul&gt;

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




&lt;h2&gt;
  
  
  Seção 5: UseCases utilizados
&lt;/h2&gt;

&lt;p&gt;Antes de começarmos a falar sobre os UC, precisamos falar de dois pontos essenciais:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hashie Gem&lt;/li&gt;
&lt;li&gt;Como projetamos cada UC&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  1-&lt;a href="https://github.com/hashie/hashie"&gt;Hashie Gem&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Esta é uma Gem que usamos no Flow de Subscription que facilitou muito o nosso lado na hora de manipular os &lt;code&gt;payloads&lt;/code&gt; chegados pela Zoop através dos Webhooks&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ywK8g064--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mvn5pk1ycfgx1gf9n84c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ywK8g064--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mvn5pk1ycfgx1gf9n84c.png" alt="Alt Text" width="800" height="195"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Dor resolvida
Imagina que, recebendo um &lt;code&gt;hash&lt;/code&gt; do evento &lt;code&gt;invoice.paid&lt;/code&gt;, por exemplo, queiramos acessar a &lt;code&gt;transaction_id&lt;/code&gt; desta fatura.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para isso, quando chega o payload de exemplo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; {
            "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
            }
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Podemos colocar este numa variável &lt;code&gt;event&lt;/code&gt; e extrair o &lt;code&gt;transaction_id&lt;/code&gt; através de:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event["payload"]["transactions"][0]["id"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;No exemplo do payload, com o Hashie, poderíamos extrair o &lt;code&gt;transaction_id&lt;/code&gt; fazendo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@event_params = event_params
@transactions = (@event_params.deep_find("transactions"))[0]
@transactions["id"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;E por isso usamos o Hashie! Para facilitar a sintax de manipulação de objetos com estrutura em &lt;code&gt;hash&lt;/code&gt; :)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Implementação no Projeto&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A implementação aconteceu em 3 etapas&lt;/p&gt;

&lt;p&gt;a) Adicionar no projeto os módulos associados ao Hashie.&lt;br&gt;
&lt;code&gt;app&amp;gt;services&amp;gt;hashie&amp;gt;deep_find.rb&lt;/code&gt;&lt;br&gt;
&lt;code&gt;app&amp;gt;services&amp;gt;hashie&amp;gt;deep_locate.rb&lt;/code&gt;&lt;br&gt;
&lt;code&gt;app&amp;gt;services&amp;gt;hashie&amp;gt;deep_fetch.rb&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;b) Criamos um Presenter* que recebe o &lt;code&gt;hash&lt;/code&gt; como &lt;code&gt;params&lt;/code&gt; na chamada da nossa aplicação pelo webhook da Zoop chamado &lt;code&gt;ParametersPresenter&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class ParametersPresenter &amp;lt; 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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este &lt;code&gt;Presenter&lt;/code&gt; tem como finalidade adaptar um objeto que chega como &lt;code&gt;ActionController::Paramaters&lt;/code&gt; para um objeto &lt;code&gt;Hash&lt;/code&gt; com métodos do &lt;code&gt;Hashie&lt;/code&gt; como o &lt;code&gt;deep_find&lt;/code&gt; mostrado anteriormente.&lt;/p&gt;

&lt;p&gt;c) Damos um build em cada UC para transformar um &lt;code&gt;ActionController::Parameters&lt;/code&gt; um &lt;code&gt;Hash&lt;/code&gt; com PowerUps da gem do Hashie&lt;/p&gt;

&lt;p&gt;Exemplo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module Subscription::UseCases::Webhooks
  class HandleInvoicePaidEvent
    def self.build(event_params:)
      new(
        event_params: ParametersPresenter.new(event_params: event_params).build
      )
    end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pronto! Agora já sabemos como usar o &lt;code&gt;Hashie&lt;/code&gt; :)&lt;/p&gt;

&lt;h4&gt;
  
  
  2-&lt;a href="https://miro.com/app/board/o9J_lGnhzCo=/"&gt;Como projetamos cada UC&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Para este projeto, estreiamos o conceito de Mind Map na INK.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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&lt;/p&gt;

&lt;p&gt;Hora dos UC's...&lt;/p&gt;

&lt;h4&gt;
  
  
  use_cases&amp;gt;webhooks
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;handle_invoice_paid_event.rb&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Abaixo cada um dos métodos que são executados no evento &lt;code&gt;invoice.paid&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app&amp;gt;app_core&amp;gt;subscription&amp;gt;use_cases&amp;gt;webhooks&amp;gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interfaces:&lt;br&gt;
a) Atributo &lt;code&gt;status&lt;/code&gt; sendo setado &lt;code&gt;active&lt;/code&gt; na Zoop e na INK&lt;br&gt;
b) Atributo &lt;code&gt;is_active&lt;/code&gt; da loja sendo setado para &lt;code&gt;true&lt;/code&gt;&lt;br&gt;
c) Atributo &lt;code&gt;is_active&lt;/code&gt; da subscription sendo setado para &lt;code&gt;true&lt;/code&gt;&lt;br&gt;
d) Atributo &lt;code&gt;trial&lt;/code&gt; sendo setado para &lt;code&gt;false&lt;/code&gt; pois houve pagamento&lt;br&gt;
e) Criação de um &lt;code&gt;SubscriptionInvoice&lt;/code&gt; para acessarmos o invoice na nossa base com mais facilidade.&lt;br&gt;
f) Atributo &lt;code&gt;expiration_date&lt;/code&gt; 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 &lt;code&gt;expiration_date&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;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&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;handle_invoice_overdue.rb&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Abaixo cada um dos métodos que são executados no evento &lt;code&gt;invoice.overdue&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      def execute
        notify_invalid_credit_card_to_user
      end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;handle_subscription_active_event.rb&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Abaixo cada um dos métodos que são executados no evento &lt;code&gt;subscription.active&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    def execute
      set_status
    end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interface&lt;br&gt;
a) Atualizamos o atributo &lt;code&gt;status&lt;/code&gt; na nossa base para &lt;code&gt;active&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;handle_subscription_created_event.rb&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Abaixo cada um dos métodos que são executados no evento &lt;code&gt;subscription.created&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    def execute
      set_trial
      set_status
      set_db_expiration_date
      set_zoop_expiration_date
    end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interfaces&lt;br&gt;
a) Setamos o atributo &lt;code&gt;trial&lt;/code&gt; para &lt;code&gt;true&lt;/code&gt; dado que a subscription acabou de ser criada&lt;br&gt;
b) Setamos o atributo &lt;code&gt;status&lt;/code&gt; para &lt;code&gt;active&lt;/code&gt; &lt;br&gt;
c) Setamos o atributo &lt;code&gt;expiration_date&lt;/code&gt; para o &lt;code&gt;due_date&lt;/code&gt; da subscription, no final do dia.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;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&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;handle_subscription_expired_event.rb&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Abaixo cada um dos métodos que são executados no evento &lt;code&gt;subscription.expired&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    def execute
      set_status
      set_subscription_is_active
      handle_store_is_active
    end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;handle_subscription_suspended_event.rb&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Abaixo cada um dos métodos que são executados no evento &lt;code&gt;subscription.suspended&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    def execute
      set_status
      handle_store_block
    end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;Exemplo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        {
            "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"
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h4&gt;
  
  
  Checkout
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;process_subscription_checkout.rb
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        def execute
            create_buyer_id
            suspend_old_subscription
            create_zoop_subscription
            associate_credit_card_to_customer
            create_subscription_database_subscription
            create_store
        end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Payment Update
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;handle_subscription_reactivation_process.rb
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        def execute
            associate_new_credit_card_to_zoop_customer
            associate_new_credit_card_token_to_subscription
            create_single_invoice
        end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aqui, para reativarmos uma loja que está desativada, há um checkout especial (&lt;a href="https://reserva.ink/subscriptions/reactivation/checkout"&gt;https://reserva.ink/subscriptions/reactivation/checkout&lt;/a&gt;).&lt;/p&gt;

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

&lt;p&gt;Quando ela for paga, o evento &lt;code&gt;invoice.paid&lt;/code&gt; será triggado e a loja será reativada.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;handle_credit_card_change_process.rb
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        def execute
            associate_new_credit_card_to_zoop_customer
            associate_new_credit_card_token_to_subscription
        end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






</description>
      <category>rails</category>
      <category>ruby</category>
      <category>webhooks</category>
      <category>zoop</category>
    </item>
    <item>
      <title>Shrine Photo Uploader – Galeria de Fotos</title>
      <dc:creator>José Sobral</dc:creator>
      <pubDate>Mon, 18 Jan 2021 22:36:31 +0000</pubDate>
      <link>https://dev.to/reservaink/shrine-photo-uploader-galeria-de-fotos-2c21</link>
      <guid>https://dev.to/reservaink/shrine-photo-uploader-galeria-de-fotos-2c21</guid>
      <description>&lt;p&gt;Você já tentou criar uma aplicação web que permitisse upload de fotos? Nós, da Reserva INK já e gostaríamos de partilhar sobre a experiência de como criar uma feature iradíssima em Rails =)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Usuários fazem upload de suas fotos&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--pwd0tSU0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/pkff9u5avun3hpvca5vz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--pwd0tSU0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/pkff9u5avun3hpvca5vz.png" alt="Alt Text" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Relacionam à suas camisetas&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oVzbvNlk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/imyv9kxkmrxjri33u4kg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oVzbvNlk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/imyv9kxkmrxjri33u4kg.png" alt="Alt Text" width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Exibimos uma galeria de fotos e as respectivas imagens associadas a cada foto&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--T_js5iuQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/4kjrv96q661r3fimlqor.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--T_js5iuQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/4kjrv96q661r3fimlqor.png" alt="Alt Text" width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--I7OYw6kn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/h16f075hzaz461obd90o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--I7OYw6kn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/h16f075hzaz461obd90o.png" alt="Alt Text" width="800" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Toolkit:&lt;/strong&gt; &lt;a href="https://github.com/shrinerb/shrine"&gt;Shrine&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Neste artigo, contaremos um pouco mais sobre como instalar o Shrine e como utilizamos ele para criar a Galeria de Fotos da INK. Toda a aplicação desta feature foi escrita no paradigma MVC (Model View Controller), em Rails e tentaremos passar em detalhes a construção de cada parte do código. &lt;/p&gt;

&lt;p&gt;Lembrando que a nossa intenção é muito mais compartilhar o conhecimento do que se mostrar referência na linguagem. Dito isso, se você tiver quaisquer sugestões de como poderíamos fazer melhor cada um dos passos a seguir ou apenas quiser conversar sobre a feature, estamos de braços abertos para receber feedbacks através do &lt;a href="mailto:jose.sobral@reserva.ink"&gt;jose.sobral@reserva.ink&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1-Como instalar o shrine do zero&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;2-Como configuramos em nossa aplicação&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;a)Model&lt;br&gt;
I-Modelagem de dados&lt;br&gt;
II-Criando os models no rails&lt;br&gt;
III-Utilizando o shrine para guardar as imagens&lt;/p&gt;

&lt;p&gt;b)View&lt;br&gt;
I-Como criar o layout do form que guardará suas imagens    II-Relacionar fotos e camisetas&lt;/p&gt;

&lt;p&gt;c)Controller&lt;br&gt;
I-Criando a galeria de cada loja&lt;br&gt;
II-CRUD de fotos&lt;br&gt;
III-Relacionamento de fotos e camisetas&lt;/p&gt;


&lt;h2&gt;
  
  
  1-Como instalar
&lt;/h2&gt;

&lt;p&gt;A instalação do Shrine é bem tranquila e segue basicamente dois passos:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a) gemfile&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;gem "shrine", "~&amp;gt; 3.0"&lt;/code&gt; &lt;/p&gt;

&lt;p&gt;&lt;em&gt;b) config &amp;gt; initializers &amp;gt; shrine.rb&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require "shrine"
require "shrine/storage/file_system"

if Rails.env.development? 
    Shrine.storages = {
        cache:  Shrine::Storage::FileSystem.new(“public”, prefix: “uploads/cache”)
        store: Shrine::Storage::FileSystem.new(“public”, prefix: “uploads”),


elsif Rails.env.Production? 
    Require ‘shrine/storage/s3’

    s3_options = {
        ***Suas credenciais do S3***
    }
    Shrine.storages = {
        cache: Shrine::Storage::S3.new(prefix: ‘cache’, **s3_options), # temporary
        store: Shrine::Storage::S3.new(prefix: ‘store, **s3options),       # permanent
}

end

Shrine.plugin :activerecord           # loads Active Record integration
Shrine.plugin :cached_attachment_data # enables retaining cached file across form redisplays
Shrine.plugin :restore_cached_data    # extracts metadata for assigned cached files

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Diferenciamos o Storage para o ambiente de Desenvolvimento e para o ambiente de Produção.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Quando a feature está no ar, o armazenamento de dados ocorre através do S3.&lt;/li&gt;
&lt;li&gt;Em desenvolvimento, o upload é realizado para um banco local.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2-Como configuraramos em nossa aplicação
&lt;/h2&gt;

&lt;h4&gt;
  
  
  a)Model
&lt;/h4&gt;

&lt;p&gt;i-Modelagem de dados&lt;/p&gt;

&lt;p&gt;Para início de conversa, falemos sobre como a modelagem de dados foi feita. No ínicio, sabíamos que teriam dois fluxos de dados através das entidades:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Galeria&lt;/li&gt;
&lt;li&gt;Fotos&lt;/li&gt;
&lt;li&gt;Relacionamento fotos-produto&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Por isso, quando começamos o projeto pensamos em criar três Models: Picture e GalleryRelatedProducts e Gallery, da seguinte forma:&lt;/p&gt;

&lt;h4&gt;
  
  
  Gallery
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Estruturando o problema:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cada seller teria sua própria galeria&lt;/li&gt;
&lt;li&gt;Por enquanto, na nossa arquitetura atual, cada loja teria apenas uma galeria&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Decisão&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;&lt;code&gt;ID      Store_id&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Com esta modelagem, cada loja teria sua própria galeria (Store_id) e, no futuro, se quisermos que cada loja tenha mais de uma galeria, não teríamos que remodelar todo nosso Model pois poderíamos ter ID’s diferentes para o mesmo Store_id.&lt;/p&gt;

&lt;h4&gt;
  
  
  Picture
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Estruturando o problema:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cada loja precisava possuir suas próprias fotos&lt;/li&gt;
&lt;li&gt;A mesma loja pode ter N fotos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Decisão:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ID  Gallery_id  Image_data&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Com essa modelagem, cada foto teria seu próprio ID e saberíamos qual o owner da foto através do gallery_id&lt;/p&gt;

&lt;h4&gt;
  
  
  GalleryRelatedProducts
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Estruturando o problema:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cada loja poderia se relacionar N fotos ao mesmo produto&lt;/li&gt;
&lt;li&gt;Cada Foto, poderia ser associada a N fotos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Decisão&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ID   Picture_id   Art_id&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Com isso, conseguimos criar um model N-N, que permite que a mesma foto (picture_id único) se relacione com tantas artes quanto necessário (Art_id) e vice e versa.&lt;/p&gt;




&lt;p&gt;ii-Criando os models no rails&lt;/p&gt;

&lt;p&gt;Para criar os models executamos através do cmd,&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ rails generate model Gallery store:references&lt;/code&gt;&lt;br&gt;
&lt;code&gt;$ rails generate model Picture gallery:references image_data:text&lt;/code&gt;&lt;br&gt;
&lt;code&gt;$ rails generate model PictureRelatedProduct picture:references art:references&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;PS1: O comando “references” cria automaticamente uma foreign_key com o model que veio chamado antes dos “:”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;PS2: O campo “image_data” não foi escolhido por acaso, a documentação do shrine pede que utilizemos o nome do tipo de arquivo seguido de “data” para este atributo. Genericamente: _data&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;iii-Utilizando o shrine para guardar as imagens&lt;/p&gt;

&lt;p&gt;Neste passo você começará a sentir a potência do Shrine. Descreveremos em 3 passos como fazer esta configuração&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Primeiro passo:&lt;/strong&gt; Criamos uma classe uploader, herdando métodos do Shrine&lt;/p&gt;

&lt;p&gt;&lt;em&gt;app &amp;gt; uploaders &amp;gt; store_picture_uploader.rb&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Class StorePictureUploader &amp;lt; Shrine
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Segundo passo:&lt;/strong&gt; Na model que receberemos os dados do Upload (picture, no nosso caso), faremos uma chamada desta classe que criamos em (i)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;app &amp;gt; models &amp;gt; picture.rb&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Picture &amp;lt; ApplicationRecord
    include StorePictureUploader::Attachment(:image) 
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Com isso, garantimos que os dados que chegarão neste atributo (image_data) chegarão através do Shrine. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;PS: Usamos :image pois foi o nome do campo que criamos no model Picture (image_data), tirando a palavra “data”.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terceiro passo:&lt;/strong&gt; Não tem passo 3, é só isso mesmo :)&lt;/p&gt;




&lt;h4&gt;
  
  
  b)View
&lt;/h4&gt;

&lt;p&gt;Arquivos utilizados:&lt;br&gt;
&lt;em&gt;app &amp;gt; views &amp;gt; user &amp;gt; dashboard &amp;gt; galleries &amp;gt; index.html.erb&lt;/em&gt;&lt;br&gt;
&lt;em&gt;app &amp;gt; views &amp;gt; user &amp;gt; dashboard &amp;gt; galleries &amp;gt; edit_image.html.erb&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;O usuário terá acesso a três interfaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upload de fotos&lt;/li&gt;
&lt;li&gt;Alterar e Deletar fotos / associar foto a um produto&lt;/li&gt;
&lt;li&gt;Galeria que exibirá suas fotos os clientes de uma loja&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;PS: A interface de exibição das fotos não entrará neste artigo pois utilizamos uma arquitetura DDD (Domain Driven Design). Posteriormente, em outro artigo falaremos sobre como utilizamos esta arquitetura.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sendo assim, vamos explorar as duas primeiras interfaces&lt;/p&gt;

&lt;p&gt;I-Como criar o layout do form que guardará suas imagens&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Upload de fotos&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;app &amp;gt; views &amp;gt; user &amp;gt; dashboard &amp;gt; galleries &amp;gt; index.html.erb&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;%= form_for :picture, url: gallery_upload_path do |f| %&amp;gt;
    &amp;lt;%= f.hidden_field :image %&amp;gt;
    &amp;lt;%= f.file_field :image %&amp;gt;
    &amp;lt;%= f.submit “Enviar foto” %&amp;gt;
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Debugando:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;:picture&lt;/code&gt; -&amp;gt; Model que estaremos enviando as informações do form&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;:image&lt;/code&gt; -&amp;gt; Atributo que estaremos enviando a imagem. Lembrando que, mesmo o nome do campo sendo image_data, deixamos apenas image segunda a documentação do Shrine&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gallery_upload_path&lt;/code&gt; -&amp;gt; Rota de post para onde enviaremos as informações de upload
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;post '/user/dashboard/gallery/', to: 'user/dashboard/galleries#upload', as: :gallery_upload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adicionando um CSS e helpers de bootstrap…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7j3-9X5j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/ogjtvejkavkbypzw1m90.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7j3-9X5j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/ogjtvejkavkbypzw1m90.png" alt="Alt Text" width="469" height="80"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Deletar fotos&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;app &amp;gt; views &amp;gt; user &amp;gt; dashboard &amp;gt; galleries &amp;gt; edit_image.html.erb&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;%= link_to gallery_delete_picture_path(@image.id) do%&amp;gt;
    &amp;lt;button&amp;gt; Deletar Foto da Galeria &amp;lt;/button&amp;gt;
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Debugando:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@image = Picture.find(params[:pic_id])&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gallery_delete_picture_path&lt;/code&gt; -&amp;gt; Rota para deletar uma imagem no banco. Ela pede um parâmetro que é a ID da foto
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;get '/user/dashboard/gallery/delete/:id', to: 'user/dashboard/galleries#delete', as: :gallery_delete_picture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Debugando:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;:picture&lt;/code&gt; -&amp;gt; model para onde enviaremos o form&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onchange: ‘this.form.submit()’;&lt;/code&gt; -&amp;gt; com isso, fazemos que o próprio botão de escolha da foto seja o mesmo de submit.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gallery_update_picture_path&lt;/code&gt; -&amp;gt; Rota para atualizarmos uma imagem no banco
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;post '/user/dashboard/gallery/update/:pic_id', to: 'user/dashboard/galleries#update', as: :gallery_update_picture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GXvaYOvZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/w0fj23t8lkdchx4kbtr8.png" alt="Alt Text" width="558" height="455"&gt;
&lt;/h2&gt;

&lt;p&gt;II-Relacionamento de fotos e artes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adicionar relação&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;%= link_to gallery_insert_related_path(art.id, @image.id) %&amp;gt;
    &amp;lt;button&amp;gt; Adicionar &amp;lt;/button&amp;gt;
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Debugando:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;art&lt;/code&gt; -&amp;gt; Para exibimos todas as artes do usuário no carrosel, fizemos um:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;% arts.each do |art| %&amp;gt;
&amp;lt;%= link_to gallery_insert_related_path(art.id, @image.id) %&amp;gt;
&amp;lt;button&amp;gt; Adicionar &amp;lt;/button&amp;gt;
&amp;lt;% end %&amp;gt;
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;PS: Por isso que acessamos a variável “art”. Além disso, &lt;a class="mentioned-user" href="https://dev.to/arts"&gt;@arts&lt;/a&gt; = @store.arts, sendo @store a variável que instanciamos através do before_action&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gallery_insert_related_path&lt;/code&gt; -&amp;gt; Rota para chamarmos a action de criação do relacionamento
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;get '/user/dashboard/gallery/insert_related/:art_id/:pic_id', to: 'user/dashboard/galleries#insert_related', as: :gallery_insert_related
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Remover relação&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;%= link_to gallery_remove_related_path(art.id, @image.id) %&amp;gt;
    &amp;lt;button&amp;gt; Remove &amp;lt;/button&amp;gt;
&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Debugando:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gallery_remove_related_path&lt;/code&gt; -&amp;gt; Rota deletarmos uma associação do model &lt;code&gt;GalleryRelatedPicture&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;get '/user/dashboard/remove_related/:art_id/:pic_id', to: 'user/dashboard/galleries#remove_related', as: :gallery_remove_related
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h4&gt;
  
  
  c)Controller
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;app &amp;gt; controllers &amp;gt; user &amp;gt; dashboard &amp;gt; galleries_controller.rb&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I-Criando a galeria de cada loja&lt;/p&gt;

&lt;p&gt;Para criarmos uma galeria para o usuário, utilizamos o método before_action do rails. Com ele, assim que o usuário acessa a página do seu respectivo controller, antes de tudo, é executada uma ação. &lt;/p&gt;

&lt;p&gt;Nossa lógica então foi fazer o seguinte: Assim que o usuário acessar a galeria através de seu dashboard, faremos:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;app/controllers/galleries_controller.rb&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class User::Dashboard::GalleriesController &amp;gt; ApplicationController
    before_action :load_store_and_galley
….

private
def load_store_and_gallery
    load_gallery
    load_store
end 

def  load_gallery
    @gallery = current_user&amp;amp;.store&amp;amp;.gallery || Gallery.new(store: current_user&amp;amp;.store)
end

def load_store
    @store = current_user&amp;amp;.store
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Debugando:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;current_user&lt;/code&gt; -&amp;gt; &lt;a href="https://stackoverflow.com/questions/12719958/rails-where-does-the-infamous-current-user-come-from"&gt;método nativo do rails&lt;/a&gt; para sabermos qual o usuário atual que está na nossa aplicação&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;amp;&lt;/code&gt; -&amp;gt; Com ele, a nossa aplicação não irá quebrar caso uma das chamadas seja “nil”&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;II-CRUD de fotos&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Insert&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Como mostramos na view, estamos usando um form_for, passando a rota gallery_upload_path como argumento&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;form_for&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;%= form_for :picture, url: gallery_upload_path do |f| %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gallery_upload_path&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;post '/user/dashboard/gallery/', to: 'user/dashboard/galleries#upload', as: :gallery_upload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A action upload, ficou assim:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def upload
    @picture = Picture.new(post_params)
    @picture.gallery = @gallery

    if !@picture.save
        flash[:error] = @picture.errors.messages[:image] 
    end
end


private
def post_params
    params.require(:picture).permit(:image)
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Debugando:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@gallery&lt;/code&gt; -&amp;gt; conseguimos acessar a variável de instância @gallery mesmo sem tê-la chamado neste action pois criamos ele usando o before_action. Logo, todas as actions do controller gallery terão acesso a esta variável &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flash[:error] = @picture.errors.message[:image]&lt;/code&gt; -&amp;gt; em &lt;code&gt;@picture&lt;/code&gt;, estamos instanciando um novo objeto do model Picture, logo ele retornará um &lt;code&gt;ActiveRecord&lt;/code&gt;. Com isso, conseguimos acessar alguns métodos de objeto ActiveRecord, entre eles o objeto errors.message. Mas antes de falar sobre este método, mostraremos uma das validações que fizemos neste model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;app &amp;gt; models &amp;gt; picture.rb&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;validate :validate_image
...
def validate_image
if image.blank?
Errors.add(:image, ‘Por favor, adicione uma imagem’)
return
end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Com isso, fazemos com que, caso a imagem venha vazia (submit sem upload), adicionemos um “error” ao objeto &lt;code&gt;Picture&lt;/code&gt; que foi instanciado (&lt;code&gt;@picture = Picture.new&lt;/code&gt; no nossso caso) e  não salvemos a imagem vazia no banco. &lt;/p&gt;

&lt;p&gt;Por isso podemos combinar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;if !@picture.save&lt;/code&gt; → caso a imagem não seja salva no banco &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@picture.errors.message[:image]&lt;/code&gt; → chamemos o     log de erro que causou o não-salvamento do atributo :image&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Também mostrado na view, usamos um form_for passando a rota &lt;code&gt;gallery_update_picture_path&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;form_for&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;%= form_for :picture, url: gallery_update_picture_path(@image.id) do |f| %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gallery_update_picture_path&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;post '/user/dashboard/gallery/update/:pic_id', to: 'user/dashboard/galleries#update', as: :gallery_update_picture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;app &amp;gt; controllers &amp;gt; galleries_controller.rb&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def update
@picture = Picture.find_by(id: params[:pic_id])

if @picture.update(post_params)
    flash[:error] = @picture.errors.full_messages.first
    end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Debugando:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@picture.update&lt;/code&gt; -&amp;gt; Como &lt;code&gt;@picture&lt;/code&gt; é um objeto ActiveRecord, conseguimos utiizar o método udpate&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@picture.errors.full_messages.first&lt;/code&gt; -&amp;gt; Como não estamos instanciando um objeto novo no model Picture, a validação validate_image mostrada é executada de forma diferente. A saída que achamos para conseguimos carregar o log dos erros gerados foi usar este método chamado &lt;code&gt;full_messages.first&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Delete&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Esse foi o mais simples. Tivemos apenas que criar um link_to para a rota de delete – &lt;code&gt;gallery_delete_picture_path&lt;/code&gt; e executar os devidos comandos no controller&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;link_to&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;get '/user/dashboard/gallery/delete/:id', to: 'user/dashboard/galleries#delete', as: :gallery_delete_picture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gallery_delete_picture&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;get '/user/dashboard/gallery/delete/:id', to: 'user/dashboard/galleries#delete', as: :gallery_delete_picture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;app &amp;gt; controllers &amp;gt; galleries_controller.rb&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def delete
    @gallery_picture = Picture.find_by(id: params[:id])

    @gallery_picture.destroy

    redirect_to user_dashboard_gallery_path
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Debugando:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;params[:id]&lt;/code&gt; -&amp;gt; ID da imagem que desejamos excluir&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.destroy&lt;/code&gt; -&amp;gt; método para deletar um objeto ActiveRecord&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;redirect_to&lt;/code&gt; -&amp;gt; método para redirecionamos o usuário para uma rota&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user_dashboard_gallery_path&lt;/code&gt; -&amp;gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;get '/user/dashboard/gallery(/:page)', to: 'user/dashboard/galleries#index', as: :user_dashboard_gallery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;III-Relacionamento de fotos e artes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adicionar relação foto – arte&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Foi bem simples fazer. Colocamos aquele botão “adicionar” que mostramos na view e, por trás do panos do controller fizemos&lt;/p&gt;

&lt;p&gt;&lt;em&gt;app &amp;gt; controllers &amp;gt; galleries_controller.rb&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def insert_related
    @picture = Picture.find_by(id: params[:id])
    @related = GalleryRelatedPicture.new(art_id: params[:art_id], picture: @picture, gallery: @gallery)

    @related.save

    flash[:error] = @related.errors.full_messages.first if @related.errors

    redirect_to request.referrer 
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Debugando:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;request.referrer&lt;/code&gt; -&amp;gt; Com este método conseguimos chamar a rota que o usuário estava antes da sua atual. Em outras palavras, é como dar um “click to go back” na seta para esquerda do seu navegador&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Removendo relação foto – arte&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A lógica foi bem parecida com adição: colocamos um &lt;code&gt;link_to&lt;/code&gt; para a rota de remover relação (&lt;code&gt;gallery_remove_related_path&lt;/code&gt;) passando os atributos art.id e &lt;code&gt;@image.id&lt;/code&gt; nesta rota.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gallery_remove_related_path&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;get '/user/dashboard/remove_related/:art_id/:pic_id', to: 'user/dashboard/galleries#remove_related', as: :gallery_remove_related
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;app &amp;gt; controllers &amp;gt; galleries_controller.rb&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def remove_related
    @related = GalleryRelatedPicture.find_by(art_id: params[:art_id], picture_id: params[:pic_id], gallery_id: @gallery.id)

    @related.destroy

    redirect_back(fallback_locatin: root_path)
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Debugando:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;redirect_back(fallback_location: root_path)&lt;/code&gt; -&amp;gt; método semalhante ao request.referrer. Caso tenha alguma diferença gritante, gostaríamos de ouvi-la leitor =)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Depois poucas semanas desta feature no ar já temos mais de 1000 fotos no model Picture. A percepção dos usuário foi excelente e temos muito orgulho do impacto dela na loja de cada usuário. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.reserva.ink/owm/gallery"&gt;https://www.reserva.ink/owm/gallery&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.reserva.ink/blogdocavaco/gallery"&gt;https://www.reserva.ink/blogdocavaco/gallery&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.reserva.ink/inspiringgirls/gallery"&gt;https://www.reserva.ink/inspiringgirls/gallery&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ficou alguma dúvida, crítica ou sugestão? Fala com a gente, estamos em busca de criar uma rede de Devs que programem em Rails para compartilhamos cada vez mais nossos aprendizados =) &lt;/p&gt;

&lt;p&gt;Abração!&lt;/p&gt;

</description>
      <category>rails</category>
      <category>shrine</category>
      <category>ruby</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
