By Sails.js Tech Brasil

Passo a passo da Integração com Stripe©

Em termos gerais, Sails.js é um gerador de códigos JavaScript que entrega, em poucos minutos, um protótipo executável de uma Aplicação WEB do tipo backend robusta, segura e escalável. O resultado é a capacidade de entregar páginas HTML5 + CSS + JavaScript dinâmicas ao Cliente Web.

O Stripe é um processador de pagamentos online que permite que empresas aceitem pagamentos seguros e escaláveis.

O Sails.js já vem com uma pre integração habilitada, mas é preciso expandir suas funcionalidades.


Pré-requisitos

Se não está familiarizado com os pré-requisitos acima, recomendamos a leitura dos conteúdos abaixo antes de continuar:


Compreendendo a Arquitetura do Stripe no Sails.js
  1. Por padrão de instalação, a integração do Sails.js com o Stripe está desativada (ou não configurada). Para ativá-la é preciso configurar o arquivo /config/custom.js com as chaves de acesso ao Stripe.
      /**************************************************************************
      *                                                                         *
      * Billing & payments configuration                                        *
      *                                                                         *
      * (https://dashboard.stripe.com/account/apikeys)                          *
      *                                                                         *
      **************************************************************************/
      // stripePublishableKey: `stripePublishabelKey`,
      // stripeSecret: `stripeSecret`,
      //--------------------------------------------------------------------------
      // /\  Configure these to enable support for billing features.
      // ||  (Or if you don't need billing, feel free to remove them.)
      //--------------------------------------------------------------------------
                

    Ou o /env/production.js, se as configurações forem diferentes para cada ambiente.

  2. Acesse o sítio da Stripe© para criar uma conta e obter os publishable key e secret key.
      /**************************************************************************
      *                                                                         *
      * Billing & payments configuration                                        *
      *                                                                         *
      * (https://dashboard.stripe.com/account/apikeys)                          *
      *                                                                         *
      **************************************************************************/
      stripePublishableKey: `${process.env.STRIPE_PUBLISHABLE_KEY}`,
      stripeSecret: `${process.env.STRIPE_SECRET}`,
      //--------------------------------------------------------------------------
      // /\  Configure these to enable support for billing features.
      // ||  (Or if you don't need billing, feel free to remove them.)
      //--------------------------------------------------------------------------
                

    Não informe os valores obtidos diretamente no código-fonte! Isso será uma falha de segurança! Coloque-os como variáveis de ambiente!

  3. A partir deste momento o Sails.js considera que há uma configuração de cobrança (billing). Reinicie sua aplicação e acesse o [Profile] do usuário autenticado. Você verá que as opções para informação do número de cartão de crédito estão disponíveis.
  4. Também a partir deste momento, toda vez que um novo usuário for criado na base de dados, ele também receberá um stripeCustomerId
        /* signup.js, linha105 */
    
        // If billing features are enabled, save a new customer entry in the Stripe API.
        // Then persist the Stripe customer id in the database.
        if (sails.config.custom.enableBillingFeatures) {
          let stripeCustomerId = await sails.helpers.stripe.saveBillingInfo.with({
            emailAddress: newEmailAddress
          }).timeout(5000).retry();
          await User.updateOne({id: newUserRecord.id})
          .set({
            stripeCustomerId
          });
        }
            
Certo! E agora, como você vai usar o Stripe© para cobrar os usuários?
Como o Stripe funciona:
  1. Primeiro, você precisa criar uma conta no Stripe©.
  2. Depois, você precisa criar um produto.
  3. Por fim, você precisa criar um preço para o produto.
  4. Depois, você precisa criar uma sessão de pagamento.
  5. Você precisa processar o pagamento.
  6. Você precisa confirmar o pagamento.
  7. Você precisa capturar o pagamento.
  8. Você precisa atualizar o status do pagamento.

As etapas de 1 a 3 são realizadas no sítio do stripe, então não vamos falar sobre isso aqui.

Passos para usar o Stripe© para cobrar os usuários:

Criar uma sessão de pagamento:

Crie uma "Action 2" para iniciar a sessão de pagamento.

/* stripe-sessions-create.js */

module.exports = {

  friendlyName: 'Do stripe session',

  description: '',

  inputs: {
    priceId: {         // <-- conforme definido no site do Stripe
      type: 'string',
      required: true
    },
    quantity: {       // <-- se for realizada a compra multipla
      type: 'number',
      defaultsTo: 1
    },
    recurring: {      // <-- se for compra recorrente
      type: 'boolean',
      defaultsTo: false
    }
  },


  exits: {},


  fn: async function (inputs) {

    const User = sails.models.user;
    const user = await User.findOne({id: this.req.me.id});
    const stripeCustomerId = user.stripeCustomerId;
    if (!stripeCustomerId) {
      return {}
    }
    // Nao esquecer do npm install stripe --save // Não esquecer de editar o arquivo config/custom.js //
    const stripe = require('stripe')(sails.config.custom.stripeSecret);

    try {

      const stripeSession = await stripe.checkout.sessions.create({
        customer: stripeCustomerId,
        mode: inputs.recurring ? 'subscription' : 'payment',
        success_url: sails.config.custom.baseUrl + '/account/stripe-checkout-session-successfully',
        line_items: [
          {
            price: inputs.priceId,
            quantity: inputs.quantity
          }
        ]
      }

      )

      return {stripeSession: stripeSession};

    } catch (err) {
      sails.log.error(err)
      return {}
    }

  }


};

    

Criar uma "Page" para visualizar o resultado da criação da sessão


/* view-stripe-checkout-session-successfull.js */

module.exports = {


  friendlyName: 'View stripe checkout session successfull',


  description: 'Display "Stripe checkout session successfull" page.',


  exits: {

    success: {
      viewTemplatePath: 'pages/account/stripe-checkout-session-successfull'
    }

  },


  fn: async function () {

    if (this.req?.me?.id) {

      // Nao esquecer do npm install stripe --save // Não esquecer de editar o arquivo config/custom.js //
      const stripe = require('stripe')(key);
      const key = sails.config.custom.stripeSecret

      const Checkout = sails.models.checkout; <-- Esta é uma tabela de banco de dados que armazena informações sobre pagamentos realizados através do Stripe.
      const checkouts = await Checkout.find({user: this.req.me.id});

      const items = []
      const intents = []
      for (let checkout of checkouts) {
        const lineItems = await stripe.checkout.sessions.listLineItems(
          checkout.stripeSessionId
        );
        items.push(...lineItems.data);

        const paymentIntents = await stripe.paymentIntents.search({
          query: `customer:"${checkout.customer}"`,
        });
        intents.push(...paymentIntents.data);
      }


      for (let i of items) {
        const c = await Checkout.findOne({priceId: i.price.id})
        if (c) {
          i.stripeSessionId = c.stripeSessionId
          i.payment_status = c.payment_status
          i.paymentUrl = JSON.parse(c.content).url
          i.claimedAt = c.claimedAt
          i.customer = c.customer
        }
      }


      return {items: items, intents: intents};

    } else {
      return {items: [], intents: []};
    }
  }
};

    

 <div id="stripe-checkout-session-successfull" v-cloak>
 <div class="container">
   <div class="display-1">
    Pagamento realizado com sucesso!
   </div>

   <div class="display-3">
     {{stripeSession.currency}} {{stripeSession.amount_total}}
   </div>

   <label class="mt-2" for="compras">Suas compras...</label>
   <ul id="compras" class="ul list-group">
     <li class="list-group-item" v-for="(item, index) in items" :key="index">
       {{index+1}}. {{item.description}} - {{item.quantity}}X {{item.price.currency.toUpperCase()}} {{getCurrency(item.price.unit_amount)}} ({{item.payment_status}})
       <span class="border-1" v-if="item.payment_status==='unpaid'">
         <a :href="item.paymentUrl"> [ Clique para pagar ]</a>
       </span>
       <span class="border-1" v-if="item.payment_status==='paid'">
         <button class="btn btn-sm btn-primary" v-if="!!item.claimedAt">[Já ativado]... em {{item.claimedAt}}</button>
         <button class="btn btn-sm btn-primary" v-if="!item.claimedAt" @click="activateIt(item)">[Ativar]</button>
       </span>
       <em class="ml-1 mr-1 fa fa-question-circle" :title="item.stripeSessionId"></em>
     </li>
   </ul>
   <label class="mt-2" for="intents">Suas transações...</label>
   <table class="table table-striped" id="intents">
     <caption></caption>
     <thead>
      <tr>
        <th id="1">Data</th>
        <th id="2">Moeda</th>
        <th id="3">Orçado</th>
        <th id="4">Pago</th>
        <th id="5">Ativo/Inativo</th>
        <th id="6">Situação</th>
        <th id="7"></th>
      </tr>
     </thead>
     <tbody>
      <tr v-for="item of intents">
        <td headers="1">
          <js-timestamp :at="item.created"></js-timestamp>
        </td>
        <td headers="2">{{item.currency.toUpperCase()}}</td>
        <td headers="3">{{(item.amount)}}</td>
        <td headers="4">{{(item.amount_received)}}</td>
        <td headers="5">{{item.livemode?'Ativo':'Inativo'}}</td>
        <td headers="6">{{item.status}}</td>
        <td headers="7">
          <em class="ml-1 mr-1 fa fa-question-circle" :title="item.payment_details.order_reference"></em>
        </td>
      </tr>
     </tbody>
     <tfoot>
      <tr>
        <td colspan="7"></td>
      </tr>
     </tfoot>
   </table>
   <modal v-if="loading" >
     <div class="display-3">Carregando...</div>
   </modal>
   <modal v-if="confirm" @close="confirm=false">
     <div class="display-3">Está certo de?</div>
     <p class="text-center">Ativar o pacote {{instance.description}}</p>
     <p>Ao pressionar [Sim] você estará ativando este pacote definitivamente.</p>
     <button class="btn btn-lg btn-primary mb-1" @click="activate()">Sim</button>
     <button class="btn btn-lg btn-primary" @click="confirm=false">Não</button>
   </modal>
 </div>
</div>
<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %>


    

/* stripe-checkout-session-successfull.page.js */

methods: {
        ...
activate: function () {
      this.confirm = false
      this.loading = true
      Cloud
        .processBonus
        .with({stripeSessionId: this.instance.stripeSessionId, claimer: this.me.id})
        .then((res) => {
          if(res.processed){
          this.loading = false
          alert('Bonus claimed successfully! Check your account or Bunker`s inventory')
          window.location.reload()
          } else {
            console.log(res.error)
            this.loading = false
            alert('Sorry! We have an error on processing data')
          }
        })
        .catch(err => {
          console.log(err)
          this.loading = false
          alert('Sorry! We have an error on processing data')
        })
    }
        ...
  }
    

/* process-bonus.js  */

module.exports = {


  friendlyName: 'Process bonus',


  description: '',


  inputs: {
    stripeSessionId: {type: 'string', required: true, example: 'cs_test_123'},
    claimer: {type: 'string', description: 'The id of user who claim the bonus', required: true}
  },


  exits: {},


  fn: async function (inputs) {

    const Checkout = sails.models.checkout;
    const Bonus = sails.models.bonus;
    const User = sails.models.user;

    const checkout = await Checkout.findOne({stripeSessionId: inputs.stripeSessionId})
    if (!checkout || !!checkout.claimedAt) return {processed: false}
    const bonus = await Bonus.findOne({stripePriceId: checkout.priceId})
    if (!bonus) return {processed: false};
    const user = await User.findOne({id: inputs.claimer})
    if (!user) return {processed: false};

    const tipo = bonus.tipe;

    // Example: ITEM_UPLINK_1, ITEM_ANTENA_2 --> Add 1/2 antennas into Users` Bunker`s Inventory
    if (tipo.startsWith('ITEM_')) {
      const parts = tipo.split('_');
      const itemId = parts[1];
      const quantity = parts[2];
      const Item = sails.models.item;
      try {
        const user = await User.findOne({id: inputs.claimer}).populate('bunker')
        const Inventory = sails.models.inventory;
        const inventory = await Inventory.find({id: user.bunker.id}).limit(1)

        if (!inventory || inventory.length === 0) return {processed: false}

        for (let i = 0; i < quantity; i++) {
          await Item.create({
            name: `PURCHASED ${itemId}`,
            value: (++i)+'',
            tipe: itemId,
            creator: user.id,
            inventory: inventory[0].id
          });
        }

        await Checkout.updateOne({id: checkout.id}).set({claimedAt: new Date()});
        return {processed: true}
      } catch (err) {
        sails.log.error(err);
        return {processed: false, error: err};
      }
    }

    // Example: POINT_CRAFT_100 -> Plus 100 Craft Points to Claimer
    if (tipo.startsWith('POINT_')) {
      const parts = tipo.split('_');
      const itemId = parts[1];
      const quantity = parts[2];
      const userPoints = user.level;
      if (itemId === 'CRAFT') {
        try {
          await User.updateOne({id: user.id}).set({level: userPoints + parseInt(quantity)});
          await Checkout.updateOne({id: checkout.id}).set({claimedAt: new Date()});
          return {processed: true}
        } catch (err) {
          sails.log.error(err);
          return {processed: false, error: err};
        }
      }
    }

    // Example: SUBSCRIPTION_YEARS_1, SUBSCRIPTION_MONTHS_3, SUBSCRIPTION_DAYS_30
    if (tipo.startsWith('SUBSCRIPTION_')) {
      const parts = tipo.split('_');
      const itemId = parts[1];
      const quantity = parseInt(parts[2], 10);
      const today = new Date();
      let timestampe;

      if (itemId === 'YEARS') {
        today.setFullYear(today.getFullYear() + quantity);
        timestampe = today.getTime();
      }

      if (itemId === 'MONTHS') {
        today.setMonth(today.getMonth() + quantity);
        timestampe = today.getTime();
      }

      if (itemId === 'DAYS') {
        today.setDate(today.getDate() + quantity);
        timestampe = today.getTime();
      }

      if (!timestampe) return {processed: false, error: 'Invalid subscription type'};

      try {
        await User.updateOne({id: user.id}).set({subscriptionUntil: timestampe});
        await Checkout.updateOne({id: checkout.id}).set({claimedAt: new Date()});
        return {processed: true};
      } catch (err) {
        sails.log.error(err);
        return {processed: false, error: err};
      }
    }

    return {processed: false, error: 'Invalid bonus type'};

  }


};

    

Criar uma forma de Confirmar o Pagamento

    /* stripe-checkout-session-successfull.page.js */

    ...
    methods: {
        ...
    retrieveStripeCheckout: function (stripeSessionId) {
      this.loading = true
      Cloud
        .retrieveStripeCheckout
        .with({sessionId: stripeSessionId})
        .then(() => {
          window.location.reload()
        })
        .catch(err => {
          console.log(err)
          this.loading = false
          alert('Sorry! We have an error on refreshing data')
        })
    },
        ...

    

Criar uma forma para Capturar o pagamento


     /* qualquer *.page.js */

     methods: {
        ...
     runStripeCheckout: function(priceId, tipe, userId) {
      Cloud.doStripeCheckout.with({priceId: priceId, quantity: 1, recurring: tipe==='recurring'}).then(function(response) {
        if(response.stripeSession){
          Cloud
            .saveStripeCheckout // <-- Cria o registro de Checkout realizado no banco de dados
            .with({
              checkout: response.stripeSession, // <-- stripeSession é retornado pelo Cloud.doStripeCheckout
              priceId: priceId,
              userId: userId
            })
            .then(()=>{
              window.location.href=response.stripeSession.url
            })
            .catch((err)=>{console.log(err); alert("Sorry! Error on saving checkout session.")})
        }else{
          alert("Sorry! Error on creating checkout session.")
        }

      })
    },
    
/* do-stripe-checkout.js */

module.exports = {


  friendlyName: 'Do stripe checkout',


  description: '',


  inputs: {
    priceId: {
      type: 'string',
      required: true
    },
    quantity: {
      type: 'number',
      defaultsTo: 1
    },
    recurring: {
      type: 'boolean',
      defaultsTo: false
    }
  },


  exits: {},


  fn: async function (inputs) {

    const User = sails.models.user;
    const user = await User.findOne({id: this.req.me.id});
    const stripeCustomerId = user.stripeCustomerId;
    if (!stripeCustomerId) {
      return {}
    }

    const stripe = require('stripe')(sails.config.custom.stripeSecret);

    try {

      const stripeSession = await stripe.checkout.sessions.create({
        customer: stripeCustomerId,
        mode: inputs.recurring ? 'subscription' : 'payment',
        success_url: sails.config.custom.baseUrl + '/account/stripe-checkout-successfully',
        line_items: [
          {
            price: inputs.priceId,
            quantity: inputs.quantity
          }
        ]
      }

      )

      return {stripeSession: stripeSession};

    } catch (err) {
      sails.log.error(err)
      return {}
    }

  }


};

    
    /* save-stripe-checkout.js */
    module.exports = {


  friendlyName: 'Save stripe checkout',


  description: '',


  inputs: {
    checkout: {
      type: 'json'
    },
    userId: {
      type: 'string'
    },
    priceId: {
      type: 'string'
    }
  },


  exits: {

  },


  fn: async function (inputs) {

    const Checkout = sails.models.checkout;

    inputs.checkout.stripeSessionId = inputs.checkout.id
    delete inputs.checkout.id

    const stringVersion = JSON.stringify(inputs.checkout);
    const converted = stringVersion.replaceAll('null',`""`)
    const parsed = JSON.parse(converted);

    return await Checkout.create({
      stripeSessionId: parsed.stripeSessionId,
      priceId: inputs.priceId,
      object: parsed.object,
      amount_subtotal: parsed.amount_subtotal,
      amount_total: parsed.amount_total,
      payment_status: parsed.payment_status,
      customer: parsed.customer,
      user: inputs.userId,
      content: parsed
    }).fetch();

  }


};


Criar uma forma de Atualizar os dados do pagamento

          /* retrieve-stripe-checkout.js */

module.exports = {


  friendlyName: 'Retrieve stripe checkout',


  description: '',


  inputs: {
    sessionId: { type: 'string', required: true }
  },


  exits: {

  },


  fn: async function (inputs) {

    const key = sails.config.custom.stripeSecret
    const stripe = require('stripe')(key);

    const Checkout = sails.models.checkout; <-- Esta é uma tabela de banco de dados que armazena informações sobre pagamentos realizados através do Stripe.
    const checkouts = await Checkout.find();

    for(let checkout of checkouts){
      const content = JSON.parse(checkout.content);
      const paymentIntents = await stripe.paymentIntents.search({
        query: `customer:"${content.customer}"`,
      });
      for(let paymentIntent of paymentIntents.data){
        const paid = paymentIntent.status === 'succeeded';
        if(paid){
          await Checkout.update({stripeSessionId: checkout.stripeSessionId, payment_status: 'unpaid'}, {payment_status: 'paid'})
        }
      }
    }

    const lineItems = await stripe.checkout.sessions.listLineItems(
      inputs.sessionId
    );

    return {lineItems: lineItems.data};

  }


};

    
Início