<?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: José Sobral</title>
    <description>The latest articles on DEV Community by José Sobral (@jsobralgitpush).</description>
    <link>https://dev.to/jsobralgitpush</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F474685%2F3a9c4c01-f22d-4291-9e2a-a5600098f7a3.png</url>
      <title>DEV Community: José Sobral</title>
      <link>https://dev.to/jsobralgitpush</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jsobralgitpush"/>
    <language>en</language>
    <item>
      <title>Component Unit Test fundamentals</title>
      <dc:creator>José Sobral</dc:creator>
      <pubDate>Mon, 02 Oct 2023 22:20:27 +0000</pubDate>
      <link>https://dev.to/jsobralgitpush/component-unit-test-fundamentals-3hc</link>
      <guid>https://dev.to/jsobralgitpush/component-unit-test-fundamentals-3hc</guid>
      <description>&lt;p&gt;I have to be honest with you: on my previous experiences working with React, I've procrastinated a lot on deep dive at component unit testing. Unlike other concepts (like states, props, life cycles), test is a knowledge which you wont learn only by doing over and over again: &lt;strong&gt;know the fundamentals of testing is essential to know what to test&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Recently, I've started to work on an application using Vue.js and despite of my lack of knowledge on component testing, I knew something was wrong on its tests. I know that because software testing is something that i’m studying a lot in the previous months and I knew the difference of concepts like &lt;a href="https://testing.googleblog.com/2013/08/testing-on-toilet-test-behavior-not.html"&gt;behaviour vs implementation&lt;/a&gt; tests to evaluate the quality of current tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PS:&lt;/strong&gt; &lt;em&gt;If you want to know more about the overall concepts of tests, check out my recent article about Glenford Myers books, The Art of Software Testing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Right now, working on an application with a need to component test improving, I had to study some of the fundamentals of component unit testing to start a new history on this company. This article is intend to provide an overview and give some references of how you can go further on your own journey on component unit testing. The main references of this articles are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vue Mastery: &lt;a href="https://www.vuemastery.com/courses/real-world-testing/getting-started/"&gt;Real World Testing&lt;/a&gt;, with &lt;a href="https://jess.sh/"&gt;Jessica Sachs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.manning.com/books/testing-vue-js-applications"&gt;Testing Vue.js Applications&lt;/a&gt;, from Edd Yerburgh&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://lmiller1990.github.io/vue-testing-handbook/"&gt;Vue Testing Handbook&lt;/a&gt;, from Lachlan Miller&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kentcdodds.com/blog"&gt;Kent C. Dodds’s Blog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Purpose of Test
&lt;/h2&gt;

&lt;p&gt;When it comes to test, starts on its philosophies is a perfect start. Just to give an example of the power of tests philosophy, one that really changed my mind, on Glenford books, was a shift from thinking in test as a process to show that a program works correctly to a greater definition:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Testing is the process of executing a program with the intent of finding errors.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Saying that, on components world, our main philosophy on test &lt;strong&gt;is to ensure that the component holds its contracts for the end user&lt;/strong&gt;. From the Testing Vue.js Applications’ book:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Deciding what unit tests to write is important. If you wrote tests for every property of a component, you would slow development and create an inefficient test suite. One method for deciding what parts of a component should be tested is to use the concept of a component contract. A component contract is the agreement between a component and the rest of the application. &lt;br&gt;
...&lt;br&gt;
The idea of input and output is important in component contracts. A good component unit test should always trigger an input and assert that the component generates the correct output. You should write tests from the perspective of a developer who’s using the component but is unaware of how the component functionality is implemented.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is NOT the purpose of test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The fallacy of 100% code coverage&lt;/strong&gt;: Sometimes the test coverage is the only metric that we have to ensure if our test suite are in state of excellence. But we have to be careful because this metric can be misleading. Things like the component contract mentioned above and a user perspective (what are the most crucial parts of your app) are far more effective than be based only on test coverage. Another idea, explained in &lt;a href="https://kentcdodds.com/blog/common-testing-mistakes"&gt;this article of Kent C. Odds&lt;/a&gt;, is that for test coverage an “About” page and a “Checkout” one would be the same thing, but for our end users one is far more important than the other one. This is explained as well by Edd, on Part 1.2.7 of his book:&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Not only is it time-consuming to reach the fabled 100% code coverage, but even with 100% code coverage, tests do not always catch bugs. Sometimes you make the wrong assumptions. Maybe you’re testing code that calls an API, and you assume that the API never returns an error; when the API does return an error in production, your app will implode.&lt;br&gt;
You don’t become a test master by striving for 100% code coverage on every app. Like a good MMA fighter who knows when to walk away from a fight, a true testing master knows when to write tests and when not to write tests.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Implementation details&lt;/strong&gt;: Things like our implementation process (variable names, hard coded label names, etc) are just implementation details, it does not necessary aims to ensure the component contract. Take a look of a test that I've found on my recent Vue.js experience
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;describe("CarForm", () =&amp;gt; {
  it("contains correct form labels", async () =&amp;gt; {
    const { queryByText } = renderComponent();

    expect(queryByText("Brand")).toBeTruthy();
    expect(queryByText("Price")).toBeTruthy();
    expect(queryByText("Year")).toBeTruthy();

    expect(queryByText("Submit")).toBeTruthy();
    expect(queryByText("Cancel")).toBeTruthy();
  });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test aims to test form labels; its all implementation details, these names can change easily. When comes to form, things like state changes on submit, submit function being called correctly and submit experience (successful, error) are contracts from forms that are far more important, like this example from &lt;a href="https://lmiller1990.github.io/vue-testing-handbook/#what-is-this-guide"&gt;Vue Testing Handbook&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;the framework itself or third party libraries&lt;/strong&gt;: We do not have to test methods and function from the framework itself neither from third party libraries. You have to trust that the core team from these tools have already done this job.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to test
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The more your tests resemble the way of your software is used, the more confidence they can give you — Kent C. Dodds&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On writing component tests, you have to think gradually to test. Use the follow mentality:&lt;/p&gt;

&lt;p&gt;1) &lt;strong&gt;Create a test suite (describe (…) ) and setup your tests (test(…))&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This part is the easiest one: create a describe and a test to ensure your test setup is working correctly. This could be unnecessary but if you do not do that and, for any reason, and there is a problem in the setup, it will make the debug process harder.&lt;/p&gt;

&lt;p&gt;2) &lt;strong&gt;Mount the component&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;According to Jessica Sachs, this is the hardest part on writing component unit tests. In this step, your only concern is to make sure your component will be mounted without any error. The difficult can vary from a lot of things, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plug dependencies correctly (like redux, pinia, router, etc) on component;&lt;/li&gt;
&lt;li&gt;Set props correctly;&lt;/li&gt;
&lt;li&gt;Build mocks correctly;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To go further on this step, read this section called &lt;a href="https://v1.test-utils.vuejs.org/api/options.html#context"&gt;Mounting Options&lt;/a&gt; from Vue Test Utils. I’ve tried to find the equivalence for React, but I did not. But &lt;a href="https://wasuradananjith.medium.com/testing-with-jest-and-enzyme-in-react-part-4-shallow-vs-mount-in-enzyme-d60cad73f85c"&gt;this article&lt;/a&gt;, talking about Enzyme gave me a good reference.&lt;/p&gt;

&lt;p&gt;3) &lt;strong&gt;Leave your IDE and think about component contracts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As mentioned above, test is not a part of the software that you simple learn by doing, you have to think about what is going to be tested. At this point of this article, I hope you already built a mentality that your unit tests should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;guarantee that your &lt;strong&gt;component contract&lt;/strong&gt; is going to be satisfied, independent of its implementations&lt;/li&gt;
&lt;li&gt;End-user mindset to know the most fundamental contracts️.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Saying that, in this part you will think and write your component contracts — which will be each test case. This will make the processes to build tests easier. An example of this could be checked on &lt;a href="https://www.vuemastery.com/courses/unit-testing/Testing-Props-and-User-Interaction/"&gt;Lesson 3&lt;/a&gt;, from Vue Mastery Unit Test Course. This lesson is only available for premium accounts but the repo from this lesson is available for free &lt;a href="https://github.com/Code-Pop/Unit-Testing-Vue2/releases/tag/Lesson3_END"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For a &lt;code&gt;RandomNumber&lt;/code&gt; component:&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;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;span&amp;gt;{{ randomNumber }}&amp;lt;/span&amp;gt;
    &amp;lt;button @click="getRandomNumber"&amp;gt;Generate Random Number&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&amp;lt;script&amp;gt;
export default {
  props: {
    min: {
      type: Number,
      default: 1
    },
    max: {
      type: Number,
      default: 10
    }
  },
  data() {
    return {
      randomNumber: 0
    }
  },
  methods: {
    getRandomNumber() {
      this.randomNumber =
        Math.floor(Math.random() * (this.max - this.min + 1)) + this.min
    }
  }
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The author draw three contracts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import RandomNumber from '@/components/RandomNumber'
import { mount } from '@vue/test-utils'

describe('RandomNumber', () =&amp;gt; {
  test('By default, randomNumber data value should be 0', () =&amp;gt; {
  })
  test('If button is clicked, randomNumber should be between 1 and 10', () =&amp;gt; {
  })
  test('If button is clicked (with right props), randomNumber should be between 200 and 300', () =&amp;gt; {
  })
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4) Write your tests&lt;/p&gt;

&lt;p&gt;When all the previous parts of this guide are done, write tests should be easy. Here are some final tips and a work material to go further:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;data-testid&lt;/code&gt; to grab selectors&lt;/strong&gt;: This selectors is a good option because, as a tester, you know for sure that this won’t change; different from classes, ids, names and etc. Read &lt;a href="https://kentcdodds.com/blog/making-your-ui-tests-resilient-to-change"&gt;this article&lt;/a&gt; to know more.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Think of all the relevant inputs from a end user&lt;/strong&gt;: This means that you want to check component props, actions (click, mouse over, etc), changing on states and etc. Read &lt;a href="https://lmiller1990.github.io/vue-testing-handbook/simulating-user-input.html#triggering-events"&gt;this section&lt;/a&gt; from Vue Testing Handbook to know more.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test cases when user do the wrong stuff&lt;/strong&gt;: You should write tests for situations where your uses calls a form with wrong data, badly formatted data or any other user errors (this is called fault injection). Remember: &lt;em&gt;Testing is the process of executing a program with the intent of finding errors&lt;/em&gt;, do not think your user is always do the right thing. Read &lt;a href="https://www.code-intelligence.com/blog/negative-testing-in-software-testing"&gt;this article&lt;/a&gt; to know more.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Specialists section
&lt;/h2&gt;

&lt;p&gt;From &lt;a href="https://github.com/vuejs/core"&gt;Vue.js repository&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This test ensures the agreement that the computed property will fires reactivity and update the value&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages &amp;gt; reactivity &amp;gt; __tests__ &amp;gt; computed.spec.ts

describe('reactivity/computed', () =&amp;gt; {
  it('should return updated value', () =&amp;gt; {
    const value = reactive&amp;lt;{ foo?: number }&amp;gt;({})
    const cValue = computed(() =&amp;gt; value.foo)
    expect(cValue.value).toBe(undefined)
    value.foo = 1
    expect(cValue.value).toBe(1)
  })
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From &lt;a href="https://github.com/facebook/react"&gt;React.js repository&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This test even add comments about the agreements from react states&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages &amp;gt; react-refresh &amp;gt; src &amp;gt; __tests__ &amp;gt; ReactFresh-test.js

  it('can preserve state for compatible types', () =&amp;gt; {
    if (__DEV__) {
      const HelloV1 = render(() =&amp;gt; {
        function Hello() {
          const [val, setVal] = React.useState(0);
          return (
            &amp;lt;p style={{color: 'blue'}} onClick={() =&amp;gt; setVal(val + 1)}&amp;gt;
              {val}
            &amp;lt;/p&amp;gt;
          );
        }
        $RefreshReg$(Hello, 'Hello');
        return Hello;
      });

      // Bump the state before patching.
      const el = container.firstChild;
      expect(el.textContent).toBe('0');
      expect(el.style.color).toBe('blue');
      act(() =&amp;gt; {
        el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
      });
      expect(el.textContent).toBe('1');

      // Perform a hot update.
      const HelloV2 = patch(() =&amp;gt; {
        function Hello() {
          const [val, setVal] = React.useState(0);
          return (
            &amp;lt;p style={{color: 'red'}} onClick={() =&amp;gt; setVal(val + 1)}&amp;gt;
              {val}
            &amp;lt;/p&amp;gt;
          );
        }
        $RefreshReg$(Hello, 'Hello');
        return Hello;
      });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>testing</category>
      <category>unittest</category>
      <category>frontend</category>
    </item>
    <item>
      <title>The Art of Software Testing, from Glenford Myers</title>
      <dc:creator>José Sobral</dc:creator>
      <pubDate>Fri, 01 Sep 2023 12:21:01 +0000</pubDate>
      <link>https://dev.to/jsobralgitpush/the-art-of-software-testing-from-glenford-myers-55c</link>
      <guid>https://dev.to/jsobralgitpush/the-art-of-software-testing-from-glenford-myers-55c</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--s-dBsX0M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0vj97m9oe7143sglbrkz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--s-dBsX0M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0vj97m9oe7143sglbrkz.jpg" alt="Image description" width="200" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The rise of AIs has revolutionized numerous industries, among them, our software industry couldn't be an exception: a &lt;a href="https://github.blog/2023-06-13-survey-reveals-ais-impact-on-the-developer-experience/"&gt;survey from Github&lt;/a&gt; shows that over 90% of developers have already used some type of AI and 40% of them use AIs on a daily routine.&lt;/p&gt;

&lt;p&gt;However, despite seeming and indeed being a significant transformation, we need to be aware of some current &lt;a href="https://www.forbes.com/sites/bernardmarr/2023/03/03/the-top-10-limitations-of-chatgpt/?sh=6878409b8f35"&gt;model limitations&lt;/a&gt;, like the &lt;strong&gt;lack of understanding of question full context&lt;/strong&gt;, the &lt;strong&gt;biases on responses&lt;/strong&gt; and the &lt;strong&gt;model knowledge limitations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When it comes to software testing, these limitations of current models turns out to be even more significant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lack of Context&lt;/strong&gt;: When designing tests, it is crucial to have a deep understanding of the application's purpose, its target audience, the most common usage scenarios, and the potential risk areas. For example, a banking app and a casual game will have vastly different risk profiles and user behaviors. A human tester brings to the table a wealth of contextual knowledge, such as which inputs might be more commonly used, which flow controls are more at risk, or which areas of an app are more crucial to its function.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Biases on Responses&lt;/strong&gt;: The AI model is only as good as the data it was trained on. If the model was trained using data from sources that consistently follow poor practices or outdated methodologies, the AI would reflect those biases. For instance, if all sources suggest an outdated testing approach, the model might recommend it even if it's not the best choice for a given scenario.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Knowledge Limitation&lt;/strong&gt;: AI models, like ChatGPT, have a "knowledge cutoff", meaning they are not aware of developments, techniques, tools, or best practices introduced after this date. Software testing is a field that evolves rapidly, with new methodologies, tools, and best practices emerging frequently. Human testers can continuously learn and adapt, absorbing the latest knowledge from conferences, workshops, forums, and community interactions. Right now, the model can not.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's look at a typical example of a suggestion for unit tests from ChatGPT for a controller name &lt;code&gt;Stores::CollectionsController&lt;/code&gt; , which is responsible to fetch &lt;code&gt;collections&lt;/code&gt; and associated &lt;code&gt;products&lt;/code&gt; based on &lt;code&gt;store&lt;/code&gt; entity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RSpec.describe Stores::CollectionsController, type: :controller do
  let(:store) { create(:store) }
  let(:collection) { create(:collection, store: store) }
  let(:product) { create(:product, collection: collection) }

  describe "GET #show" do
    context "when collection exists" do
      before { get :show, params: { store_id: store.id, collection_id: collection.id } }

      it "assigns the collection" do
        expect(assigns(:collection)).to eq(collection)
      end

      it "assigns the products" do
        expect(assigns(:products)).to include(product)
      end

      it "renders the show template" do
        expect(response).to render_template(:show)
      end
    end

    context "when collection does not exist" do
      before { get :show, params: { store_id: store.id, collection_id: -1 } }

      it "redirects to store path" do
        expect(response).to redirect_to(store_path(store.slug))
      end
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are several mistakes in this code provided by AI, lets take a look on two of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;render_template&lt;/code&gt; is deprecated&lt;/strong&gt;: render_template has been deprecated in more recent versions of RSpec, &lt;a href="https://gitlab.com/gitlab-org/gitlab/-/issues/16260"&gt;since 2015&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;assigns&lt;/code&gt; is an implementation test&lt;/strong&gt;: &lt;code&gt;assigns&lt;/code&gt; was traditionally used to access the instance variables set in the controller. The problem with this is that you're testing the &lt;a href="https://testing.googleblog.com/2013/08/testing-on-toilet-test-behavior-not.html"&gt;internal implementation of the controller rather than its external output or behavior&lt;/a&gt;. Unit tests should focus on the expected behavior and not on how this behavior is internally achieved.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given this kind of limitations on tests generated by AI's, it's necessary to understand the fundamentals beyond the test writing.&lt;/p&gt;

&lt;p&gt;This article aims to provide a summary of concepts presented in the book &lt;a href="https://www.amazon.com/Art-Software-Testing-Glenford-Myers/dp/1118031962"&gt;"The Art of Software Testing" by Glenford Myers&lt;/a&gt;, in an attempt to shed light on and introduce the science of testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Psychology of Testing
&lt;/h2&gt;

&lt;p&gt;In the software world, it's common for us to learn by solving problems. This approach is usually very effective, and there's a plethora of articles that correlate problem-solving with software.&lt;/p&gt;

&lt;p&gt;However, a thesis raised by the author in Chapter 2 - "The Psychology and Economics of Software Testing" - is that there's a philosophy behind the thinking of constructing tests. Often, this philosophy gets misconstrued, and problem-solving is grounded on incorrect premises. The author states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The purpose of testing is to show that a program performs its intended functions correctly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing is the process of establishing confidence that a program does what it's supposed to do.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This portrays a testing psychology focused on ensuring that your system works correctly.&lt;/p&gt;

&lt;p&gt;However, according to the author, the real psychology behind testing isn't to ensure your system works correctly, but rather to break it. In the author's words:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Testing is the process of executing a program with the intent of finding errors.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This entirely shifts our problem-solving perspective; when our goal is to demonstrate that a program is error-free, we subconsciously select data that has a low likelihood of causing the program to fail.&lt;/p&gt;

&lt;p&gt;Another way to spot this testing psychology is the use of the terms "&lt;em&gt;successful&lt;/em&gt;" and "&lt;em&gt;unsuccessful&lt;/em&gt;", especially when used by product managers. In most projects, PMs categorize a "&lt;em&gt;successful&lt;/em&gt;" test when the test finds no errors. We need to start with the assumption that all programs contain errors and the purpose of testing is to locate them.&lt;/p&gt;

&lt;p&gt;In summary, it's as if the process of software testing is seen as a process of trying to constantly break your code. In the next section, we'll explore methods to help structure our thinking when attempting to "break our code", that is, to test it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Black Box Testing
&lt;/h2&gt;

&lt;p&gt;The author introduces two techniques to assist developers in the testing process and on "break things". In the first one, black box testing, we operate under the assumption that our code is a "black box" and we don't test control flows and business rules; we only test how our "black box" handles inputs and what its outputs are.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;also known as data driven or input/output-driven testing&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thus, the goal of black box testing is to attempt to break our objects and methods based on different inputs. Some strategies to apply black box testing include:&lt;/p&gt;

&lt;h3&gt;
  
  
  Equivalence partitioning
&lt;/h3&gt;

&lt;p&gt;This technique divides the software's input space into partitions of equivalent values. The idea here is that if one input in a given partition is valid, then all inputs from that partition are. So, instead of testing each input individually, testers can simply test one representative input from each partition.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;in practice&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Suppose you have a registration form in your application that accepts an age range of users between 18 and 65 years. The equivalent partitions here would be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Under 18 years old&lt;/li&gt;
&lt;li&gt;Between 18 and 65 years old&lt;/li&gt;
&lt;li&gt;Over 65 years old&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To test, you can choose a representative age from each partition (for instance, 15, 30, and 70) and check if the system correctly processes each entry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.test import TestCase
from .models import User

class EquivalencePartitioningTest(TestCase):
    def test_underage(self):
        response = self.client.post('/signup/', {'age': 15})
        self.assertContains(response, 'Age is too low')

    def test_valid_age(self):
        response = self.client.post('/signup/', {'age': 30})
        self.assertContains(response, 'Congratulations!')

    def test_over_age(self):
        response = self.client.post('/signup/', {'age': 70})
        self.assertContains(response, 'Age is too high.')

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Boundary value analysis
&lt;/h3&gt;

&lt;p&gt;In this technique, the focus is on the boundary values or edge points of the input ranges. Errors commonly occur at these extremities, which is why it's crucial to test values just above, below, and right at the acceptance limits.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;in practice&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Using the same registration form example mentioned above, the boundary values would be 17, 18, 65, and 66. The goal would be to test these specific values because it's at these boundaries or extremities that errors are most likely to occur. For instance, a user who is 18 years old should be accepted, while a 17-year-old user should not be.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class BoundaryValueAnalysisTest(TestCase):
    def test_age_just_below_boundary(self):
        response = self.client.post('/signup/', {'age': 17})
        self.assertContains(response, 'Age is too low.')

    def test_age_at_lower_boundary(self):
        response = self.client.post('/signup/', {'age': 18})
        self.assertContains(response, 'Congratulations')

    def test_age_at_upper_boundary(self):
        response = self.client.post('/signup/', {'age': 65})
        self.assertContains(response, 'Congratulations!')

    def test_age_just_above_boundary(self):
        response = self.client.post('/signup/', {'age': 66})
        self.assertContains(response, 'Age is too high.')

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cause-effect graphing
&lt;/h3&gt;

&lt;p&gt;This technique involves creating a chart that maps out the relationships between different causes (inputs) and their effects (outcomes). The graph is used to identify and devise test cases that cover all possible combinations of inputs and their resulting effects.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;in practice&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's say you have a form that accepts entries for an event. Causes could include: type of ticket selected, seat availability, event date. The effects could be: purchase confirmation, unavailable seat error, invalid date error, etc. You would create a chart to map all these inputs and their possible outcomes, ensuring that all scenarios are tested. In summary:&lt;/p&gt;

&lt;p&gt;Let's consider a simple event booking system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Causes (Inputs):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ticket type: Regular, VIP, Student Discount&lt;/li&gt;
&lt;li&gt;Seat availability: Available, Unavailable&lt;/li&gt;
&lt;li&gt;Event date: Valid Date, Past Date&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Effects (Outcomes):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Purchase Confirmation&lt;/li&gt;
&lt;li&gt;Unavailable Seat Error&lt;/li&gt;
&lt;li&gt;Invalid Date Error&lt;/li&gt;
&lt;li&gt;Discount Verification (for student discount)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The graph can be described as:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If Regular or VIP ticket is selected with &lt;em&gt;Available&lt;/em&gt; seat and &lt;em&gt;Valid Date&lt;/em&gt;, then the outcome is &lt;em&gt;Purchase Confirmation&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If &lt;em&gt;Student Discount&lt;/em&gt; ticket is selected with &lt;em&gt;Available&lt;/em&gt; seat and &lt;em&gt;Valid Date&lt;/em&gt;, then the outcomes are both &lt;em&gt;Purchase Confirmation&lt;/em&gt; and &lt;em&gt;Discount Verification&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If any ticket type is chosen with &lt;em&gt;Unavailable&lt;/em&gt; seat, regardless of the event date, then the outcome is &lt;em&gt;Unavailable Seat Error&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If any ticket type is selected with either &lt;em&gt;Available&lt;/em&gt; or &lt;em&gt;Unavailable&lt;/em&gt; seat but with &lt;em&gt;Past Date&lt;/em&gt;, then the outcome is &lt;em&gt;Invalid Date Error&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this cause-effect perception, you can build several tests to try to "break your code".&lt;/p&gt;

&lt;h3&gt;
  
  
  Error guessing
&lt;/h3&gt;

&lt;p&gt;As the name suggests, this technique is less systematic and more based on the tester's intuition and experience. Testers try to "guess" where errors might be present and craft test cases based on those assumptions. For instance, if a tester knows that a particular kind of input caused issues in previous software, they might decide to test that specific input again.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;in practice&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 ErrorGuessingTest(TestCase):
    def test_special_characters_in_name(self):
        response = self.client.post('/signup/', {'name': '@John!'})
        self.assertContains(response, 'Invalid chars')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  White box Testing
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;also known as logic-driven&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The other technique, white box testing, aims to test the flow control of our code. This approach is commonly used to know the test coverage of our application. However, as the author himself illustrates, in many cases, it's impossible to test all the flows of a particular system. Consider the example below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Q0v6wE39--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/twzh7rdkfc5w9uxbbhyj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Q0v6wE39--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/twzh7rdkfc5w9uxbbhyj.png" alt="Image description" width="769" height="706"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this image, there are 10¹³ possible flows between a and b. It would be impractical to create this number of test cases for our system.&lt;/p&gt;

&lt;p&gt;That's one of the reasons why the models limitation mentioned above of the &lt;strong&gt;Lack of Context&lt;/strong&gt; is crucial: only humans can have knowledge of full context of an application and decide which flow control is more critical to test.&lt;/p&gt;

&lt;p&gt;In this regard, some techniques are employed in the use of white box testing to guide our process to "break our code" (test):&lt;/p&gt;

&lt;h3&gt;
  
  
  Statement Coverage
&lt;/h3&gt;

&lt;p&gt;Aims to ensure that each statement or line of code is executed at least once; essential for detecting parts of the code that are not executed under any test scenario - this is what &lt;code&gt;coverage&lt;/code&gt; files typically show.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;in practice&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's suppose we have the following authentication function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def authenticate(username, password):
    user = CustomUser.objects.filter(username=username)

    if not user:
        return 'User not found'

    if not user.is_active:
        return 'User is not active'

    if user.failed_attempts &amp;gt;= 3:
        return 'Account locked due to too many failed attempts'

    if user.password != password:
        user.failed_attempts += 1
        user.save()
        return 'Invalid password'

    user.failed_attempts = 0
    user.save()
    return 'Authentication successful'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This technique ensures that each statement in the code is executed at least once during testing. In the authenticate function, we use statement coverage to ensure that each of the if statements is executed at least once during testing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def test_authenticate_statement_coverage(self):
    # Test case where user is not found
    result = authenticate('nonexistent_user', 'password')
    self.assertEqual(result, 'User not found')

    # Test case where user is not active
    inactive_user = CustomUser.objects.create(username='inactive_user', is_active=False, password='password')
    result = authenticate('inactive_user', 'password')
    self.assertEqual(result, 'User is not active')

    # Test case where account is locked due to too many failed attempts
    locked_user = CustomUser.objects.create(username='locked_user', failed_attempts=3, password='password')
    result = authenticate('locked_user', 'password')
    self.assertEqual(result, 'Account locked due to too many failed attempts')

    # Test case where password is invalid
    user = CustomUser.objects.create(username='user', password='password')
    result = authenticate('user', 'wrong_password')
    self.assertEqual(result, 'Invalid password')

    # Test case where authentication is successful
    result = authenticate('user', 'password')
    self.assertEqual(result, 'Authentication successful')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Decision Coverage
&lt;/h3&gt;

&lt;p&gt;Ensures that each decision or branch (like an &lt;code&gt;if-else&lt;/code&gt; statement) is tested for both options: &lt;code&gt;True&lt;/code&gt; or &lt;code&gt;False&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;in practice&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This technique ensures that each decision point in the code is executed both when the condition is &lt;code&gt;True&lt;/code&gt; and when it is &lt;code&gt;False&lt;/code&gt;. In the &lt;code&gt;authenticate&lt;/code&gt; function, we use decision coverage to ensure that the &lt;code&gt;if user.password != password&lt;/code&gt; decision point is executed both when the password is correct and when it is incorrect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def test_authenticate_decision_coverage(self):
    # Test case where password is invalid
    user = CustomUser.objects.create(username='user', password='password')
    result = authenticate('user', 'wrong_password')
    self.assertEqual(result, 'Invalid password')
    self.assertEqual(user.failed_attempts, 1)

    # Test case when password is valid
    result = authenticate('user', 'password')
    self.assertEqual(result, 'Authentication successful')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Multiple-Condition Coverage
&lt;/h3&gt;

&lt;p&gt;Similar to condition coverage, but goes further. It ensures that all possible combinations of conditions in a decision are tested.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;in practice&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This technique ensures that each possible combination of conditions in a decision point is executed at least once. In the &lt;code&gt;authentication&lt;/code&gt; function, we use multiple-condition coverage to ensure that each possible combination of conditions in the decision point is executed at least once during testing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def test_authenticate_multiple_condition_coverage(self):
    # Test case where user is not found
    result = authenticate('nonexistent_user', 'password')
    self.assertEqual(result, 'User not found')

    # Test case where user is not active
    inactive_user = CustomUser.objects.create(username='inactive_user', is_active=False, password='password')
    result = authenticate('inactive_user', 'password')
    self.assertEqual(result, 'User is not active')

    # Test case where account is locked due to too many failed attempts
    locked_user = CustomUser.objects.create(username='locked_user', failed_attempts=3, password='password')
    result = authenticate('locked_user', 'password')
    self.assertEqual(result, 'Account locked due to too many failed attempts')

    # Test case where password is invalid
    user = CustomUser.objects.create(username='user', password='password')
    result = authenticate('user', 'wrong_password')
    self.assertEqual(result, 'Invalid password')
    self.assertEqual(user.failed_attempts, 1)

    # Test case where authentication is successful
    result = authenticate('user', 'password')
    self.assertEqual(result, 'Authentication successful')
    self.assertEqual(user.failed_attempts, 0)

    # Test case where password is correct but user is not active
    inactive_user = CustomUser.objects.create(username='inactive_user2', is_active=False, password='password')
    result = authenticate('inactive_user2', 'password')
    self.assertEqual(result, 'User is not active')

    # Test case where password is correct but account is locked
    locked_user = CustomUser.objects.create(username='locked_user2', failed_attempts=3, password='password')
    result = authenticate('locked_user2', 'password')
    self.assertEqual(result, 'Account locked due to too many failed attempts')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Big Apps
&lt;/h2&gt;

&lt;p&gt;To provide some real world examples of these techniques been applied into our industry, I've brought some open source projects code snippets:&lt;/p&gt;

&lt;h3&gt;
  
  
  React
&lt;/h3&gt;

&lt;p&gt;This code block provides an example of how &lt;a href="https://github.com/facebook/react/blob/main/packages/react-dom/src/__tests__/ReactTestUtils-test.js"&gt;React codebase&lt;/a&gt; uses black box testing to check if given a &lt;code&gt;className&lt;/code&gt; (input) that contains &lt;code&gt;\n&lt;/code&gt; char, the actual &lt;code&gt;className&lt;/code&gt; rendered (output) will still be available.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;react/packages/react-dom/src/__tests__
/ReactTestUtils-test.js, Ln 100

it('can scryRenderedDOMComponentsWithClass with className contains \\n', () =&amp;gt; {
    class Wrapper extends React.Component {
      render() {
        return (
          &amp;lt;div&amp;gt;
            Hello &amp;lt;span className={'x\ny'}&amp;gt;Jim&amp;lt;/span&amp;gt;
          &amp;lt;/div&amp;gt;
        );
      }
    }

    const renderedComponent = ReactTestUtils.renderIntoDocument(&amp;lt;Wrapper /&amp;gt;);
    const scryResults = ReactTestUtils.scryRenderedDOMComponentsWithClass(
      renderedComponent,
      'x',
    );
    expect(scryResults.length).toBe(1);
  });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Python
&lt;/h3&gt;

&lt;p&gt;This code block provides an example of how &lt;a href="https://github.com/python/cpython/blob/main/Lib/test/test_tuple.py"&gt;Python codebase&lt;/a&gt; uses black box testing to check if a Tuple responds to correctly (output) to different arguments (input)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lib/test/test_tuple.py, Ln 27

def test_constructors(self):
        super().test_constructors()
        # calling built-in types without argument must return empty
        self.assertEqual(tuple(), ())
        t0_3 = (0, 1, 2, 3)
        t0_3_bis = tuple(t0_3)
        self.assertTrue(t0_3 is t0_3_bis)
        self.assertEqual(tuple([]), ())
        self.assertEqual(tuple([0, 1, 2, 3]), (0, 1, 2, 3))
        self.assertEqual(tuple(''), ())
        self.assertEqual(tuple('spam'), ('s', 'p', 'a', 'm'))
        self.assertEqual(tuple(x for x in range(10) if x % 2),
                         (1, 3, 5, 7, 9))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Gitlab
&lt;/h3&gt;

&lt;p&gt;This code block provides an example of how &lt;a href="https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/models/service_desk/custom_email_verification_spec.rb"&gt;Gitlab codebase&lt;/a&gt; uses white box testing to cover multiple status of a custom_email_verification service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spec/models/service_desk/custom_email_verification_spec.rb, Ln 40

context 'when status is :finished' do
      before do
        subject.mark_as_started!(user)
        subject.mark_as_finished!
      end

      it { is_expected.to validate_absence_of(:token) }
      it { is_expected.to validate_absence_of(:error) }
    end

    context 'when status is :failed' do
      before do
        subject.mark_as_started!(user)
        subject.mark_as_failed!(:smtp_host_issue)
      end

      it { is_expected.to validate_presence_of(:error) }
      it { is_expected.to validate_absence_of(:token) }
    end
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The Glendford' book covers much more about testing and i highly recommend you to read the book. The topics I've covered are just some of them that really helps me to criticize tests generated by AI's and have a deeper knowledge of what software test is really about.&lt;/p&gt;

&lt;p&gt;If there are specific topics from the book that have been particularly enlightening for you, I'd love to hear about them in the comments. Additionally, if you're aware of any strategies that can improve AI-generated tests, please share them with the community in the comments section.&lt;/p&gt;

</description>
      <category>test</category>
      <category>webdev</category>
      <category>books</category>
    </item>
    <item>
      <title>Why Django uses regex on routes?</title>
      <dc:creator>José Sobral</dc:creator>
      <pubDate>Thu, 31 Aug 2023 20:19:59 +0000</pubDate>
      <link>https://dev.to/jsobralgitpush/django-best-practices-routing-hik</link>
      <guid>https://dev.to/jsobralgitpush/django-best-practices-routing-hik</guid>
      <description>&lt;h2&gt;
  
  
  Configuration and Why
&lt;/h2&gt;

&lt;p&gt;Django is a web framework which allows you to build your whole application using the concept of a &lt;code&gt;project&lt;/code&gt; with one or several &lt;code&gt;apps&lt;/code&gt;. There are tons of &lt;a href="https://docs.djangoproject.com/en/4.2/misc/design-philosophies/"&gt;philosophies&lt;/a&gt; originated by this approach (like Modular Development, Separation of Concerns, Plug-n-play Features and etc).&lt;/p&gt;

&lt;p&gt;All of these apps are configured by your project. One of these configurations is the &lt;code&gt;ROOT_URLCONF&lt;/code&gt; in which allows you to pointer the 'master' url file config. This is fundamental to put in practice the django philosophies mentioned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Importance of regular expressions
&lt;/h2&gt;

&lt;p&gt;When i've first encountered regular expressions in Django apps, i think that was an over engineering process to build routes (as any junior can think). After some experiences i've understand the reason why and i think these two resources compiles well them&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.amazon.com.br/Definitive-Guide-Django-Development-Second/dp/143021936X"&gt;Django Book, The definitive guide to Django: Web Development Done Right&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A route example from the book:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.conf.urls.defaults import *
from mysite.views import current_datetime
urlpatterns = patterns('',
 (r'^time/$', current_datetime),
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The author explanation&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This concept is best explained by example. If we had instead used the pattern '^time/' (without a dollar sign at the end), then any URL that starts with time/ would match, such as /time/foo and /time/bar, not just /time/. Similarly, if we had left off the initial caret character ('time/$'), Django would match any URL that ends with time/, such as /foo/bar/time/. Thus, we use both the caret and dollar sign to ensure that only the URL /time/ matches. Nothing more, nothing less.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.reddit.com/r/django/comments/4crxug/why_does_django_use_regexs_for_the_url_routing/"&gt;One of the Django founders, on Reddit&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oC3mJVWZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8cinb5zdmpl50cvlqvwl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oC3mJVWZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8cinb5zdmpl50cvlqvwl.png" alt="Image description" width="744" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In summary, django uses &lt;code&gt;pattern matching&lt;/code&gt; to register &lt;code&gt;routes&lt;/code&gt; and using &lt;code&gt;regex&lt;/code&gt; you can build it in a easy and performatic way.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Open Source Examples
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Flagsmith, 2.7k stars&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://github.com/Flagsmith/flagsmith/blob/main/api/api/urls/v1.py"&gt;https://github.com/Flagsmith/flagsmith/blob/main/api/api/urls/v1.py&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Healthchecks, 6.5k stars&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://github.com/healthchecks/healthchecks"&gt;https://github.com/healthchecks/healthchecks&lt;/a&gt;&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <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>[Dashboard] Máscara de Campo</title>
      <dc:creator>José Sobral</dc:creator>
      <pubDate>Tue, 20 Dec 2022 19:17:30 +0000</pubDate>
      <link>https://dev.to/jsobralgitpush/dashboard-mascara-de-campo-3ine</link>
      <guid>https://dev.to/jsobralgitpush/dashboard-mascara-de-campo-3ine</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--55roYutK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wnqrg8ttpwf93a0k0zbv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--55roYutK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wnqrg8ttpwf93a0k0zbv.png" alt="Alt Text" width="788" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Neste artigo vamos explicar como foi realizada a implementação do projeto de Máscara de Campo no nosso Dashboard
&lt;/h3&gt;

&lt;p&gt;Como tudo que fazemos aqui na INK, o nosso Dashboard 2.0 lançado no mês passado (Jun/ 2021) foi feito na base do MVP...Let's make nossa V0. &lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;imagem da seção 'Financeiro' do novo dashboard&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Como toda v0, muitas partes da aplicação não foram implementadas; e, uma muita importante que implementamos recentemente, foi a máscara de campo e neste artigo vamos documentar como pensamos este projeto.&lt;/p&gt;




&lt;h3&gt;
  
  
  Escopo do Artigo
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Estratégia do projeto&lt;/li&gt;
&lt;li&gt;Biblioteca de Máscara de Campo&lt;/li&gt;
&lt;li&gt;Uso da Biblioteca&lt;/li&gt;
&lt;li&gt;Máscara de Campo criadas&lt;/li&gt;
&lt;li&gt;Implementação no Projeto&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Estratégia do projeto
&lt;/h3&gt;

&lt;p&gt;Recentemente no nosso time de Tech, estamos enfrentando um momento muito especial que acomete todo o time de tecnologia: &lt;strong&gt;como os Devs podem organizar o pensamento e criar uma gestão de conhecimento dentro do time?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Se algum Dev que estiver lendo este artigo e achar que a resposta é óbvia, estamos abertos a ouvi-la 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;Contudo, aqui na INK a resposta à esta pergunta ainda está em construção e nesta seção do artigo, vamos explicitar como estamos construindo esta resposta&lt;/p&gt;

&lt;p&gt;A melhor forma, até então, que estamos pensando esta gestão de conhecimentos é através de Mapas Mentais. Com eles, podemos pensar e compartilhar o pensamento visualmente através de conceitos com relacionamentos, cores e identidades próprias. &lt;/p&gt;

&lt;p&gt;No projeto de Máscara de Campo, construimos um mapa onde no centro temos todas seções do Dashboard (Painel INK, Financeiro, Produtos e etc) e uma ramificação para cada uma destas seções contendo os formulários presentes nesta seção. &lt;/p&gt;

&lt;p&gt;Então, por exemplo, temos um fórmulário dentro de seção de Cupom para criação de cupom. Nele, mapeamos todos os inputs presentes nesta seção e quais as máscaras de cada campo levaria&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;No pensamos na cor salmão, temos o nome do campo, o datatype do campo e o nome do component_id que levaria as propriedades de máscara de campo através de um jQuery&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;Dessa forma, qualquer Dev que quiser ver a estruturação do projeto pode acessar a publicação do mapa:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.figma.com/community/file/1004055413396117089"&gt;Mapa Mental - Mask Field&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;E, até então, essa é a nossa primeira resposta (a tal da V0) para a gestão de conhecimentos no time de tecnologia da INK :) &lt;/p&gt;




&lt;h3&gt;
  
  
  Biblioteca de Máscara de Campo
&lt;/h3&gt;

&lt;p&gt;Para este projeto usamos a &lt;a href="https://igorescobar.github.io/jQuery-Mask-Plugin/docs.html"&gt;biblioteca de máscara de campo criada pelo Igor Escobar&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Além de uma documentação fácil, a biblioteca é construída usando jQuery que é uma ferramenta bem difundida na arquitetura da INK.&lt;/p&gt;




&lt;h3&gt;
  
  
  Uso da Biblioteca
&lt;/h3&gt;

&lt;p&gt;Para implementarmos a máscara de campo com o uso desta biblioteca, alguns &lt;em&gt;tricks&lt;/em&gt; foram necessários.&lt;/p&gt;

&lt;p&gt;1) &lt;strong&gt;Como trazer um regex para um caracter?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Para trazermos um &lt;a href="https://regexr.com/"&gt;regex&lt;/a&gt; para um caracter, podemos usar o conceito de &lt;code&gt;pattern&lt;/code&gt; da &lt;a href="https://igorescobar.github.io/jQuery-Mask-Plugin/docs.html#translation"&gt;documentação&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Com ele, conseguimos desenhar um regex para cada um dos caracteres do campo que estamos aplicando a máscara!&lt;/p&gt;

&lt;p&gt;Olha o exemplo de máscara que aplicamos para um campo que deve aceitar um hexadecimal color (#000000)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  $('.hexadecimal-mask').mask(`BAAAAAAA`, {'translation': {
    B: {pattern: /[#]/},
    A: {pattern: /[A-Za-Z0-9]/}
  }});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Com ele, garantimos que o primeiro caracter chegue com o regex /[#]/, ou seja, só aceite # e os demais caracteres sejam letras do alfabeto, maiúsculas ou minúsculas ([A-Za-Z]) ou números [0-9].&lt;/p&gt;

&lt;p&gt;2) &lt;strong&gt;O que fazer quando queremos aplicar a máscara de campo em um campo que não sabemos o número de caracteres necessários?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No caso da máscara do CPF, sabemos que precisaremos de 11 dígitos; o mesmo é válido para o telefone, data de nascimento, etc. &lt;/p&gt;

&lt;p&gt;Agora, é campos como "primeiro nome"? Ou "valor do desconto"?&lt;/p&gt;

&lt;p&gt;Aqui entrou o segundo &lt;em&gt;trick&lt;/em&gt; no uso da biblioteca!&lt;/p&gt;

&lt;p&gt;Quando não sabemos o número de caracteres necessários, podemos ativar a flag de &lt;code&gt;optional: true&lt;/code&gt; e colocar tantos caracteres quanto for o limite permitido pelo campo.&lt;/p&gt;

&lt;p&gt;Por exemplo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  $(".alphanumeric-single-word-mask").mask(`${'A'.repeat(200)}`, {'translation': {
    A: {pattern: /^[a-zA-Z0-9çÇ]/, optional: true},
  }});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aqui criamos uma máscara que permite até 200 caracteres - &lt;em&gt;fazemos isso usando o &lt;code&gt;${'A'.repeat(200)}&lt;/code&gt;&lt;/em&gt; e, como eles são opcionais, podemos colocar quantos caracteres desejarmos num range de 1 há 200 caracteres.&lt;/p&gt;




&lt;h3&gt;
  
  
  Máscara de Campo criadas
&lt;/h3&gt;

&lt;p&gt;Como já exposto anteriormente no artigo, criamos a Máscara de Campo através do jQuery, fazendo com que uma classe levasse a um input as propriedades da máscara daquele campo.&lt;/p&gt;

&lt;p&gt;Por exemplo, na seção de Cadastro de Loja da nossa plataforma, o usuário precisa colocar o seu CPF, no formato XXX.XXX.XXX-XX. &lt;/p&gt;

&lt;p&gt;Neste exemplo, precisamos de um campo que aceite apenas números e que haja uma pontuação no lugar desejado, conforme o formato do CPF. Por isso, para este campo, será atribuído a máscara:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cpf-mask&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Campo só aceitará números e terá o formato XXX.XXX.XXX-XX&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As classes que criamos para este projeto foram:&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; assets &amp;gt; javascipts &amp;gt; dashboard &amp;gt; utils &amp;gt; mask.js

$(".alphabetic-multiple-words-with-accent-mask").mask(`${'A'.repeat(500)}`, {'translation': {
    A: {pattern: /^[a-zA-ZàèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸçÇ_ ]/, optional: true},
  }});

  $(".alphabetic-single-word-with-accent-mask").mask(`${'A'.repeat(200)}`, {'translation': {
    A: {pattern: /^[a-zA-ZàèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸçÇ]/, optional: true},
  }});

  $(".numeric-mask").mask(`${'0'.repeat(20)}`);

  $('.phone-mask').mask('(00) 00000-0000');

  $('.cpf-mask').mask('000.000.000-00', {reverse: true});

  $('.cnpj-mask').mask('00.000.000/0000-00', {reverse: true});

  $('.cep-mask').mask('00000-000');

  $(".alphanumeric-single-word-mask").mask(`${'A'.repeat(200)}`, {'translation': {
    A: {pattern: /^[a-zA-Z0-9çÇ]/, optional: true},
  }});

  $(".alphanumeric-single-word-with-accent-mask").mask(`${'A'.repeat(200)}`, {'translation': {
    A: {pattern: /^[a-zA-Z0-9àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸçÇ]/, optional: true},
  }});

  $(".alphanumeric-single-word-uppercase-mask").mask(`${'A'.repeat(200)}`, {'translation': {
    A: {pattern: /^[A-Z0-9Ç]/, optional: true},
  }});

  $(".alphanumeric-single-word-lowercase-mask").mask(`${'A'.repeat(200)}`, {'translation': {
    A: {pattern: /[a-z0-9]/, optional: true},
  }});

  $(".alphanumeric-multiple-word-with-accent-mask").mask(`${'A'.repeat(500)}`, {'translation': {
    A: {pattern: /^[a-zA-Z0-9àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸçÇ_ ]/, optional: true},
  }});

  $(".alphanumeric-multiple-word-with-accent-up-to-15-char-mask").mask(`${'A'.repeat(15)}`, {'translation': {
    A: {pattern: /^[a-zA-Z0-9àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝâêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿÄËÏÖÜŸçÇ_ ]/, optional: true},
  }});

  $('.float-mask').mask(`1${'1'.repeat(50)}.11`, {'translation': {
    1: {pattern: /[0-9]/, optional: true}
  }});

  $('.art-price-mask').mask(`211.11`, {'translation': {
    1: {pattern: /[0-9]/, optional: true},
    2: {pattern: /[1]/, optional: true}
  }});

  $(".art-range-price").attr('type', 'number');

  $(".art-range-price").attr({
    "max" : 159,        
    "min" : 59          
  });


  $('.hexadecimal-mask').mask(`BAAAAAAA`, {'translation': {
    B: {pattern: /[#]/},
    A: {pattern: /[A-Z0-9]/}
  }});

  $('.social-media-mask').mask(`${'A'.repeat(200)}`, {'translation': {
    A: {pattern: /[A-Za-z0-9(),-_.,]/, optional: true}
  }});

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

&lt;/div&gt;






&lt;h3&gt;
  
  
  Implementação no Projeto
&lt;/h3&gt;

&lt;p&gt;Por fim, para implementarmos a máscara no projeto, aplicamos em cada &lt;em&gt;component&lt;/em&gt; de input da aplicação o devido &lt;em&gt;component_class&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Dessa forma:&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;div class="col s12 m12 l6"&amp;gt;
  &amp;lt;%= render partial: 'user/dashboard/components/input/text_field_with_helper', locals: {
    title: 'Valor do desconto',
    helper_text: 'Valor em R$',
    name:'coupon[value]',
    component_class: "float-mask"
  } %&amp;gt; 
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Com amor e carinho,
&lt;/h3&gt;

&lt;p&gt;Sobral, INK&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>mask</category>
      <category>javascript</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>
    <item>
      <title>Galeria de Fotos com o Shrine</title>
      <dc:creator>José Sobral</dc:creator>
      <pubDate>Fri, 25 Sep 2020 14:18:38 +0000</pubDate>
      <link>https://dev.to/jsobralgitpush/galeria-de-fotos-com-o-shrine-1987</link>
      <guid>https://dev.to/jsobralgitpush/galeria-de-fotos-com-o-shrine-1987</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;: Galeria de fotos&lt;/p&gt;

&lt;h6&gt;
  
  
  &lt;em&gt;1-Usuários fazem upload de suas fotos&lt;/em&gt;
&lt;/h6&gt;

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

&lt;h6&gt;
  
  
  &lt;em&gt;2-Relacionam a suas camisetas&lt;/em&gt;
&lt;/h6&gt;

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

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

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GFePQcFZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/1ifw89yhqye3589e06lw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GFePQcFZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/1ifw89yhqye3589e06lw.png" alt="Alt Text" width="800" height="377"&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;Mãos ao código!&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Como instalar o shrine do zero&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Como configuramos em nossa aplicação&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;Modelagem de dados&lt;/li&gt;
&lt;li&gt;Criando os models no rails&lt;/li&gt;
&lt;li&gt;Utilizando o shrine para guardar as imagens&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Como criar o layout do form que guardará suas imagens&lt;/li&gt;
&lt;li&gt;Relacionar fotos e camisetas&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Criando a galeria de cada loja&lt;/li&gt;
&lt;li&gt;CRUD de fotos&lt;/li&gt;
&lt;li&gt;Relacionamento de fotos-camisetas&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;1-&lt;strong&gt;Como instalar o shrine do zero&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;a) &lt;code&gt;Gemfile&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;gem "shrine", "~&amp;gt; 3.0"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;b) &lt;code&gt;config &amp;gt; initializers &amp;gt; shrine.rb*&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;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;p&gt;2-&lt;strong&gt;Como configuramos em nossa aplicação&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;a) MODEL&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modelagem de dados&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para início de conversa, falemos sobre como a modelagem de dados foi feita. No ínicio, sabíamos que teriam três 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-camiseta&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Por isso, quando começamos o projeto pensamos em criar três Models: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Gallery&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Pictures&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GalleryRelatedProducts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Gallery
&lt;/h1&gt;

&lt;h6&gt;
  
  
  Estruturando o problema:
&lt;/h6&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;h6&gt;
  
  
  Decisão
&lt;/h6&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 (&lt;code&gt;Store_id&lt;/code&gt;) 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 &lt;code&gt;Store_id&lt;/code&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Picture
&lt;/h1&gt;

&lt;h6&gt;
  
  
  Estruturando o problema:
&lt;/h6&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;h6&gt;
  
  
  Decisão:
&lt;/h6&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 &lt;code&gt;ID&lt;/code&gt; e saberíamos qual o &lt;em&gt;owner&lt;/em&gt; da foto através do &lt;code&gt;gallery_id&lt;/code&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  GalleryRelatedProducts
&lt;/h1&gt;

&lt;h6&gt;
  
  
  Estruturando o problema:
&lt;/h6&gt;

&lt;ul&gt;
&lt;li&gt;Cada loja poderia relacionar N fotos a mesma camiseta&lt;/li&gt;
&lt;li&gt;Cada foto poderia ser associada a N camisetas&lt;/li&gt;
&lt;/ul&gt;

&lt;h6&gt;
  
  
  Decisão
&lt;/h6&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 (&lt;code&gt;picture_id&lt;/code&gt; único) se relacione com tantas camisetas quanto necessário (&lt;code&gt;Art_id&lt;/code&gt;) e vice e versa.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Criando os models no rails&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ rails generate model Gallery store:references
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ rails generate model Picture gallery:references image_data:text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ rails generate model PictureRelatedProduct picture:references art:references
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;PS1: O comando &lt;code&gt;references&lt;/code&gt; 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 &lt;code&gt;image_data&lt;/code&gt; 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. &lt;br&gt;
Genericamente: &lt;code&gt;&amp;lt;name&amp;gt;_data&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Utilizando o shrine para guardar as imagens&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neste passo você começará a sentir a potência do Shrine. &lt;/p&gt;

&lt;p&gt;Descreveremos em 3 passos como fazer esta configuração&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;app &amp;gt; uploaders &amp;gt; store_picture_uploader.rb&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 StorePictureUploader &amp;lt; Shrine
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Segundo passo:&lt;/em&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;code&gt;app &amp;gt; models &amp;gt; picture.rb&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 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 chegarem neste atributo (&lt;code&gt;image_data&lt;/code&gt;) venham através do Shrine. &lt;/p&gt;

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

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




&lt;p&gt;b) VIEW&lt;/p&gt;

&lt;p&gt;Arquivos utilizados:&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;app &amp;gt; views &amp;gt; user &amp;gt; dashboard &amp;gt; galleries &amp;gt; edit_image.html.erb&lt;/code&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 aos customers da 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;Vamos explorar as duas primeiras interfaces&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Como criar o layout do form que guardará suas imagens&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h6&gt;
  
  
  Upload de fotos
&lt;/h6&gt;

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

&lt;p&gt;&amp;lt;%= form_for :picture, url: gallery_upload_path do |f| %&amp;gt;&lt;br&gt;
    &amp;lt;%= f.hidden_field :image %&amp;gt;&lt;br&gt;
    &amp;lt;%= f.file_field :image %&amp;gt;&lt;br&gt;
    &amp;lt;%= f.submit “Enviar foto” %&amp;gt;&lt;br&gt;
&amp;lt;% end %&amp;gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  Debugando:
&lt;/h6&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;:picture&lt;/code&gt; → Model que estaremos enviando as informações do form&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;:image&lt;/code&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;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;gallery_upload_path&lt;/code&gt; → Rota de post para onde enviaremos as informações de upload&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
post '/user/dashboard/gallery/', to: 'user/dashboard/galleries#upload', as: :gallery_upload&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&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---fjnmxYi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/ddlwsqyarafn6bc3t3nz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---fjnmxYi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/ddlwsqyarafn6bc3t3nz.png" alt="Alt Text" width="469" height="80"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h6&gt;
  
  
  Deletar fotos
&lt;/h6&gt;

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

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
&amp;lt;%= link_to gallery_delete_picture_path(@image.id) do%&amp;gt;&lt;br&gt;
    Deletar Foto da Galeria &lt;br&gt;
&amp;lt;% end %&amp;gt;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  Debugando:
&lt;/h6&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;allery_delete_picture_path&lt;/code&gt; → Rota para deletar uma imagem no banco. Ela pede um parâmetro que é a &lt;code&gt;ID&lt;/code&gt; da foto&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
get '/user/dashboard/gallery/delete/:id', to: 'user/dashboard/galleries#delete', as: :gallery_delete_picture&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt; &lt;/p&gt;




&lt;h6&gt;
  
  
  Alterar fotos
&lt;/h6&gt;

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

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
&amp;lt;%= form_for :picture, url: gallery_update_picture_path(@image.id) do |f| %&amp;gt;&lt;br&gt;
    &amp;lt;%= f.hidden_field :image %&amp;gt;&lt;br&gt;
    &amp;lt;%= f.file_field :image, onchange: ‘this.form.submit();’ %&amp;gt;&lt;br&gt;
&amp;lt;% end %&amp;gt;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  Debugando:
&lt;/h6&gt;

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

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
post '/user/dashboard/gallery/update/:pic_id', to: 'user/dashboard/galleries#update', as: :gallery_update_picture&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

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




&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Relacionamento de fotos-camisetas&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h6&gt;
  
  
  Adicionar relação
&lt;/h6&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
&amp;lt;%= link_to gallery_insert_related_path(art.id, @image.id) %&amp;gt;&lt;br&gt;
    Adicionar &lt;br&gt;
&amp;lt;% end %&amp;gt;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  Debugando:
&lt;/h6&gt;

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

&lt;p&gt;&amp;lt;% arts.each do |art| %&amp;gt;&lt;/p&gt;

&lt;p&gt;&amp;lt;%= link_to gallery_insert_related_path(param1, param2) %&amp;gt;&lt;/p&gt;

&lt;p&gt;Adicionar &lt;/p&gt;

&lt;p&gt;&amp;lt;% end %&amp;gt;&lt;/p&gt;

&lt;p&gt;&amp;lt;% end %&amp;gt;&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;PS: param1 = &lt;code&gt;art.id&lt;/code&gt; , param2 = &lt;code&gt;@image.id&lt;/code&gt; *&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;PS2: Por isso que acessamos a variável &lt;code&gt;art&lt;/code&gt;. Além disso, &lt;code&gt;@arts = @store.arts&lt;/code&gt;, sendo &lt;code&gt;@store&lt;/code&gt; a variável que instanciamos através do &lt;code&gt;before_action&lt;/code&gt; *&lt;/em&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  Debugando
&lt;/h6&gt;

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

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
get '/user/dashboard/gallery/insert_related/:art_id/:pic_id', to: 'user/dashboard/galleries#insert_related', as: :gallery_insert_related&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;




&lt;h6&gt;
  
  
  Remover relação
&lt;/h6&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
&amp;lt;%= link_to gallery_remove_related_path(art.id, @image.id) %&amp;gt; Remove&lt;br&gt;
&amp;lt;% end %&amp;gt;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  Debugando:
&lt;/h6&gt;

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

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
get '/user/dashboard/remove_related/:art_id/:pic_id', to: 'user/dashboard/galleries#remove_related', as: :gallery_remove_related&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;




&lt;p&gt;C) CONTROLLER&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Criando a galeria de cada loja&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para criarmos uma galeria para o usuário, utilizamos o método &lt;code&gt;before_action&lt;/code&gt; 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;code&gt;galleries_controller.rb&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;class User::Dashboard::GalleriesController &amp;gt; ApplicationController&lt;br&gt;
    before_action :load_store_and_galley&lt;/p&gt;

&lt;p&gt;private&lt;br&gt;
def load_store_and_gallery&lt;br&gt;
    load_gallery&lt;br&gt;
    load_store&lt;br&gt;
end &lt;/p&gt;

&lt;p&gt;def  load_gallery&lt;br&gt;
    @gallery = current_user&amp;amp;.store&amp;amp;.gallery || Gallery.new(store: current_user&amp;amp;.store)&lt;br&gt;
end&lt;/p&gt;

&lt;p&gt;def load_store&lt;br&gt;
    @store = current_user&amp;amp;.store&lt;br&gt;
end&lt;/p&gt;

&lt;h6&gt;
  
  
  Debugando:
&lt;/h6&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;current_user&lt;/code&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;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;operador &lt;code&gt;&amp;amp;&lt;/code&gt; = Com ele, a nossa aplicação não irá quebrar caso uma das chamadas seja &lt;code&gt;nil&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CRUD de fotos&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h6&gt;
  
  
  Insert
&lt;/h6&gt;

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

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

&lt;p&gt;&lt;code&gt;&amp;lt;%= form_for :picture, url: gallery_upload_path do |f| %&amp;gt;&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
post '/user/dashboard/gallery/', to: 'user/dashboard/galleries#upload', as: :gallery_upload&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A action &lt;code&gt;upload&lt;/code&gt;, mencionada na rota acima ficou assim:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;galleries_controller.rb&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;def upload&lt;br&gt;
    @picture = Picture.new(post_params)&lt;br&gt;
    @picture.gallery = @gallery&lt;br&gt;
    if !@picture.save&lt;br&gt;
        flash[:error] = @picture.errors.messages[:image] &lt;br&gt;
    end&lt;br&gt;
end&lt;/p&gt;

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

&lt;h6&gt;
  
  
  Debugando:
&lt;/h6&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@gallery&lt;/code&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;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;flash[:error]&lt;/code&gt; = @picture.errors.message[:image] → em @picture, estamos instanciando um novo objeto do model Picture, logo ele retornará um ActiveRecord. 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;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;validate :validate_image&lt;/p&gt;

&lt;p&gt;def validate_image&lt;/p&gt;

&lt;p&gt;if image.blank?&lt;/p&gt;

&lt;p&gt;Errors.add(:image, ‘Por favor, adicione uma imagem’)&lt;br&gt;
return&lt;br&gt;
end&lt;/p&gt;

&lt;p&gt;end&lt;/p&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;code&gt;if !@picture.save&lt;/code&gt; → caso a imagem não seja salva no banco,  &lt;code&gt;@picture.errors.message[:image]&lt;/code&gt; → chamemos o log de erro que causou o não-salvamento do atributo &lt;code&gt;:image&lt;/code&gt;. &lt;/p&gt;




&lt;h6&gt;
  
  
  Update
&lt;/h6&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;form_for &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
&amp;lt;%= form_for :picture, url: gallery_update_picture_path(@image.id) do |f| %&amp;gt;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
post '/user/dashboard/gallery/update/:pic_id', to: 'user/dashboard/galleries#update', as: :gallery_update_picture&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;galleries_controller.rb&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;def update&lt;br&gt;
    @picture = Picture.find_by(id: params[:pic_id])&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if @picture.update(post_params)
    flash[:error] = @picture.errors.full_messages.first
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;end&lt;/p&gt;

&lt;h6&gt;
  
  
  Debugando:
&lt;/h6&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@picture.update&lt;/code&gt; → Como &lt;code&gt;@picture&lt;/code&gt; é um objeto ActiveRecord, conseguimos utiizar o método udpate&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@picture.errors.full_messages.first&lt;/code&gt; → Como não estamos instanciando um objeto novo no model Picture, a validação &lt;code&gt;validate_image&lt;/code&gt; 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;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h6&gt;
  
  
  Delete
&lt;/h6&gt;

&lt;p&gt;Esse foi o mais simples. Tivemos apenas que criar um &lt;code&gt;link_to&lt;/code&gt; 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;link_to &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&amp;lt;%= link_to gallery_delete_picture_path(@image.id) do%&amp;gt;&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
get '/user/dashboard/gallery/delete/:id', to: 'user/dashboard/galleries#delete', as: :gallery_delete_picture&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;galleries_controller.rb&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;def delete&lt;br&gt;
    @gallery_picture = Picture.find_by(id: params[:id])&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@gallery_picture . destroy

redirect_to user_dashboard_gallery_path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;end&lt;/p&gt;

&lt;h6&gt;
  
  
  Debugando:
&lt;/h6&gt;

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

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
get '/user/dashboard/gallery(/:page)', to: 'user/dashboard/galleries#index', as: :user_dashboard_gallery&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Relacionamento de fotos-camisetas&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h6&gt;
  
  
  # Adicionar relação foto – arte
&lt;/h6&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;code&gt;galleries_controller.rb&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;def insert_related&lt;br&gt;
    @picture = Picture.find_by(id: params[:id])&lt;br&gt;
    @related = GalleryRelatedPicture.new(art_id: params[:art_id], picture: @picture, gallery: @gallery)&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@related.save

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

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

&lt;/div&gt;

&lt;p&gt;end&lt;/p&gt;

&lt;h6&gt;
  
  
  Debugando:
&lt;/h6&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;request.referrer&lt;/code&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;h6&gt;
  
  
  Removendo relação foto-camiseta
&lt;/h6&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 &lt;code&gt;art.id&lt;/code&gt; 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;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
get '/user/dashboard/remove_related/:art_id/:pic_id', to: 'user/dashboard/galleries#remove_related', as: :gallery_remove_related&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;galleries_controller.rb&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;def remove_related&lt;br&gt;
    @related = GalleryRelatedPicture.find_by(art_id: params[:art_id], picture_id: params[:pic_id], gallery_id: @gallery.id)&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@related.destroy

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

&lt;/div&gt;

&lt;p&gt;end&lt;/p&gt;

&lt;h6&gt;
  
  
  Debugando:
&lt;/h6&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;redirect_back(fallback_location: root_path)&lt;/code&gt; → método semalhante ao &lt;code&gt;request.referrer&lt;/code&gt;. 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 &lt;code&gt;Picture&lt;/code&gt;. A percepção dos usuário foi excelente e temos muito orgulho do impacto dela na loja de cada usuário. Alguns exemplos:&lt;/p&gt;

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

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

&lt;p&gt;&lt;a href="https://www.reserva.ink/inspiringgirls/gallery"&gt;Inspiring Girls&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>
    </item>
  </channel>
</rss>
