diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 8951cd0..4e2ab76 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,7 +1,6 @@ class ProjectsController < ApplicationController before_action :authenticate_user! before_action :authenticate_subscriber, only: :create_invitation_request - def index @invitation_request = current_user.invitation_requests.build @invitation_requests = current_user.invitation_requests.pluck(:project_id).to_json diff --git a/app/jobs/notify_billing_job.rb b/app/jobs/notify_billing_job.rb new file mode 100644 index 0000000..f71bcb7 --- /dev/null +++ b/app/jobs/notify_billing_job.rb @@ -0,0 +1,15 @@ +class NotifyBillingJob < ApplicationJob + queue_as :default + + before_perform do |job| + current_billing = job.arguments.first + next_billing = current_billing.subscription.billings.create!( + billing_date: current_billing.billing_date + 1.month, amount: 19.90 + ) + self.class.set(wait_until: next_billing.billing_date.to_datetime).perform_later(next_billing) + end + + def perform(billing) + BillingsMailer.with(billing:).notify_billing.deliver + end +end diff --git a/app/mailers/billings_mailer.rb b/app/mailers/billings_mailer.rb new file mode 100644 index 0000000..d5be09c --- /dev/null +++ b/app/mailers/billings_mailer.rb @@ -0,0 +1,10 @@ +class BillingsMailer < ApplicationMailer + default from: 'billing@portfoliorrr.com' + + def notify_billing + @billing = params[:billing] + @user = @billing.subscription.user + + mail(subject: default_i18n_subject, to: @user.email) + end +end diff --git a/app/models/billing.rb b/app/models/billing.rb new file mode 100644 index 0000000..704a633 --- /dev/null +++ b/app/models/billing.rb @@ -0,0 +1,3 @@ +class Billing < ApplicationRecord + belongs_to :subscription +end diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 72ab124..5d1c2bb 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -1,7 +1,13 @@ class Subscription < ApplicationRecord + SUBSCRIPTION_AMOUNT = 19.90 + belongs_to :user + has_many :billings, dependent: :destroy + enum status: { inactive: 0, active: 10 } + after_update :create_billing, if: :active? && :saved_change_to_status? + def active! self.start_date = Time.zone.now.to_date super @@ -11,4 +17,22 @@ def inactive! self.start_date = nil super end + + private + + def create_billing + return if start_date.nil? + + billing_date = set_billing_date + billing = billings.create(billing_date:, amount: SUBSCRIPTION_AMOUNT) + NotifyBillingJob.set(wait_until: billing_date.to_datetime).perform_later(billing) + end + + def set_billing_date + if start_date.day < 29 + start_date + 1.month + else + start_date.next_month.beginning_of_month + 1.month + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index 96a106d..b3e2455 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -15,6 +15,7 @@ class User < ApplicationRecord has_many :professional_infos, through: :profile has_many :education_infos, through: :profile has_many :invitation_requests, through: :profile + has_one :subscription, dependent: :destroy enum role: { user: 0, admin: 10 } diff --git a/app/views/billings_mailer/notify_billing.html.erb b/app/views/billings_mailer/notify_billing.html.erb new file mode 100644 index 0000000..09cefb6 --- /dev/null +++ b/app/views/billings_mailer/notify_billing.html.erb @@ -0,0 +1,9 @@ +
<%= t('.reminder')%>
+ +<%= t('.call_to_action')%>
+ +<%= t('.due_date', due_date: I18n.l(@billing.billing_date)) %>
+ +<%= t('.amount', amount: number_to_currency(@billing.amount)) %>
diff --git a/config/locales/models/billings.pt-BR.yml b/config/locales/models/billings.pt-BR.yml new file mode 100644 index 0000000..190aef1 --- /dev/null +++ b/config/locales/models/billings.pt-BR.yml @@ -0,0 +1,18 @@ +pt-BR: + activerecord: + models: + billings: + one: Cobrança + other: Cobranças + attributes: + billings: + billing_date: Data de Cobrança + amount: Valor + billings_mailer: + notify_billing: + subject: Porfoliorrr - Assinatura mensal + greeting: Olá, %{recipient}! + reminder: Sua assinatura mensal do Porfoliorrr Premium vence hoje + call_to_action: Efetue o pagamento para continuar usando os benefícios da sua assinatura + due_date: "Vencimento: %{due_date}" + amount: "Valor: %{amount}" \ No newline at end of file diff --git a/db/migrate/20240215115912_create_billings.rb b/db/migrate/20240215115912_create_billings.rb new file mode 100644 index 0000000..bf6c039 --- /dev/null +++ b/db/migrate/20240215115912_create_billings.rb @@ -0,0 +1,11 @@ +class CreateBillings < ActiveRecord::Migration[7.1] + def change + create_table :billings do |t| + t.references :subscription, null: false, foreign_key: true + t.date :billing_date, null: false + t.decimal :amount, precision: 8, scale: 2, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 5aa8144..4463e2e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -49,6 +49,15 @@ t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end + create_table "billings", force: :cascade do |t| + t.integer "subscription_id", null: false + t.date "billing_date", null: false + t.decimal "amount", precision: 8, scale: 2, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["subscription_id"], name: "index_billings_on_subscription_id" + end + create_table "comments", force: :cascade do |t| t.text "message" t.integer "post_id", null: false @@ -402,6 +411,7 @@ add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "billings", "subscriptions" add_foreign_key "comments", "posts" add_foreign_key "comments", "users" add_foreign_key "connections", "profiles", column: "followed_profile_id" diff --git a/spec/factories/subscriptions.rb b/spec/factories/subscriptions.rb index 3ef588e..4c25c48 100644 --- a/spec/factories/subscriptions.rb +++ b/spec/factories/subscriptions.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :subscription do user - start_date { nil } + start_date { Time.zone.now.to_date } status { 0 } end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 0b1e332..932f3a5 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -5,10 +5,6 @@ email { Faker::Internet.email } password { '123456' } - after(:create) do |user| - user.subscription.active! - end - trait :seed do after(:build) do |user| user.class.skip_callback(:create, :after, :create_profile!, raise: false) diff --git a/spec/jobs/notify_billing_job_spec.rb b/spec/jobs/notify_billing_job_spec.rb new file mode 100644 index 0000000..400f1b4 --- /dev/null +++ b/spec/jobs/notify_billing_job_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +RSpec.describe NotifyBillingJob, type: :job do + context 'envia email de cobrança ' do + it 'para usuário com assinatura premium' do + user = create(:user, :paid, full_name: 'Marcos Madeira', email: 'marcos@madeira.com') + billing = user.subscription.billings.first + + mail = double('mail', deliver: true) + mailer = double('BillingsMailer', notifify_billing: mail) + + allow(BillingsMailer).to receive(:with).and_return(mailer) + allow(mailer).to receive(:notify_billing).and_return(mail) + + NotifyBillingJob.perform_now(billing) + + expect(mail).to have_received(:deliver).once + end + + it 'A cobrança é recriada para ser enviada via email no próximo mês' do + user = create(:user, :paid, full_name: 'Marcos Madeira', email: 'marcos@madeira.com') + billing = user.subscription.billings.first + + expect { NotifyBillingJob.perform_now(billing) }.to have_enqueued_job(NotifyBillingJob).on_queue('default') + end + end +end diff --git a/spec/mailer/billings_mailer_spec.rb b/spec/mailer/billings_mailer_spec.rb new file mode 100644 index 0000000..f127553 --- /dev/null +++ b/spec/mailer/billings_mailer_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +RSpec.describe BillingsMailer, type: :mailer do + context '#notify_billing' do + it 'envia e-mail de acordo com data de cobrança da assinatura' do + user = create(:user, :paid, full_name: 'Marcos Madeira', email: 'marcos@madeira.com') + + billing = user.subscription.billings.first + + mail = BillingsMailer.with(billing:).notify_billing + + expect(mail.subject).to eq 'Porfoliorrr - Assinatura mensal' + expect(mail.to).to eq ['marcos@madeira.com'] + expect(mail.from).to eq ['billing@portfoliorrr.com'] + expect(mail.body).to include 'Olá, Marcos Madeira!' + expect(mail.body).to include 'Sua assinatura mensal do Porfoliorrr Premium vence hoje' + expect(mail.body).to include 'Efetue o pagamento para continuar usando os benefícios da sua assinatura' + expect(mail.body).to include "Vencimento: #{I18n.l(billing.billing_date)}" + expect(mail.body).to include 'Valor: R$ 19,90' + end + end +end diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index 5c2c04c..2af7f06 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -253,7 +253,7 @@ it 'retorna perfis premium primeiro e depois os perfis free' do create(:user, :free, full_name: 'André Porteira') create(:user, :free, full_name: 'Eliseu Ramos') - create(:user, full_name: 'Moisés Campus') + create(:user, :paid, full_name: 'Moisés Campus') user_premium_inactive = create(:user, full_name: 'Joao Almeida') user_premium_inactive.subscription.inactive! @@ -268,8 +268,8 @@ it 'ordena por nome em caso de mesmo status de assinatura' do create(:user, :free, full_name: 'André Almeida') create(:user, :free, full_name: 'André Barbosa') - create(:user, full_name: 'André Campus') - create(:user, full_name: 'André Dias') + create(:user, :paid, full_name: 'André Campus') + create(:user, :paid, full_name: 'André Dias') result = Profile.order_by_premium diff --git a/spec/models/subscription_spec.rb b/spec/models/subscription_spec.rb index d50e872..9fda617 100644 --- a/spec/models/subscription_spec.rb +++ b/spec/models/subscription_spec.rb @@ -1,25 +1,74 @@ require 'rails_helper' RSpec.describe Subscription, type: :model do - describe '#active!' do - it 'atualiza data de início automaticamente' do - subscription = create(:subscription, status: :inactive, start_date: nil) + context 'assinatura de usuário premium' do + it 'cria cobrança antes do dia 29' do + user = create(:user, :free) - subscription.active! + travel_to Date.parse('2024-02-10') do + user.subscription.active! + end - expect(subscription.start_date).to eq Time.zone.now.to_date - expect(subscription).to be_active + billing = Billing.first + expect(Billing.count).to eq 1 + expect(billing.billing_date).to eq Date.parse '2024-03-10' + expect(billing.amount).to eq 19.90 + end + + context 'recebe cobrança no primeiro dia do mês seguinte' do + it 'quando assinatura for no dia 29' do + user = create(:user, :free) + + travel_to Date.parse('2024-03-29') do + user.subscription.active! + end + + expect(Billing.count).to eq 1 + expect(Billing.first.billing_date).to eq Date.parse '2024-05-01' + end + + it 'quando assinatura for no dia 30' do + user = create(:user, :free) + + travel_to Date.parse('2024-06-30') do + user.subscription.active! + end + + expect(Billing.count).to eq 1 + expect(Billing.first.billing_date).to eq Date.parse '2024-08-01' + end + + it 'quando assinatura for no dia 31' do + user = create(:user, :free) + + travel_to Date.parse('2024-10-31') do + user.subscription.active! + end + + expect(Billing.count).to eq 1 + expect(Billing.first.billing_date).to eq Date.parse '2024-12-01' + end + end + describe '#active!' do + it 'atualiza data de início automaticamente' do + subscription = create(:subscription, status: :inactive, start_date: nil) + + subscription.active! + + expect(subscription.start_date).to eq Time.zone.now.to_date + expect(subscription).to be_active + end end - end - describe '#active!' do - it 'atualiza data de início automaticamente' do - subscription = create(:subscription, status: :active, start_date: Time.zone.now) + describe '#active!' do + it 'atualiza data de início automaticamente' do + subscription = create(:subscription, status: :active, start_date: Time.zone.now) - subscription.inactive! + subscription.inactive! - expect(subscription.start_date).to be_nil - expect(subscription).to be_inactive + expect(subscription.start_date).to be_nil + expect(subscription).to be_inactive + end end end end diff --git a/spec/requests/apis/v1/search_user_by_job_categories_spec.rb b/spec/requests/apis/v1/search_user_by_job_categories_spec.rb index 4c821f1..b913dea 100644 --- a/spec/requests/apis/v1/search_user_by_job_categories_spec.rb +++ b/spec/requests/apis/v1/search_user_by_job_categories_spec.rb @@ -90,8 +90,8 @@ it 'retorna perfis premium primeiro e depois os perfis comuns' do create(:user, :free, full_name: 'Eliseu Ramos') create(:user, :free, full_name: 'André Porteira') - create(:user, full_name: 'Moisés Campus') - create(:user, full_name: 'Joao Almeida') + create(:user, :paid, full_name: 'Moisés Campus') + create(:user, :paid, full_name: 'Joao Almeida') get '/api/v1/profiles' @@ -107,7 +107,7 @@ it 'retorna perfis premium primeiro e depois os perfis free na busca com parâmetro' do ruby = create(:job_category, name: 'Ruby on Rails') - user_premium = create(:user, full_name: 'Moisés Campus') + user_premium = create(:user, :paid, full_name: 'Moisés Campus') user_premium.profile.profile_job_categories.create(job_category: ruby, description: 'Sou um especialista em Ruby') user_free = create(:user, :free, full_name: 'André Almeida') user_free.profile.profile_job_categories.create(job_category: ruby, description: 'Fiz um e-commerce em Ruby') diff --git a/spec/requests/invitation_requests/user_creates_invitation_request_spec.rb b/spec/requests/invitation_requests/user_creates_invitation_request_spec.rb index a1f42ff..148eba3 100644 --- a/spec/requests/invitation_requests/user_creates_invitation_request_spec.rb +++ b/spec/requests/invitation_requests/user_creates_invitation_request_spec.rb @@ -2,19 +2,22 @@ describe 'Usuário solicita convite para um projeto' do it 'com sucesso' do - user = create(:user) - + user = create(:user, :paid) login_as user - post invitation_request_path, params: { invitation_request: { message: 'Me convida', project_id: 1 } } - + post invitation_request_path, params: { + 'project_id' => 1, + 'invitation_request' => { + 'message' => 'Me convida' + } + } user_requests = user.profile.invitation_requests - expect(user_requests.count).to eq 1 + expect(user_requests.reload.count).to eq 1 expect(user_requests.last.message).to eq 'Me convida' end it 'e falha se já solicitou convite para o mesmo projeto' do - user = create(:user) + user = create(:user, :paid) create(:invitation_request, profile: user.profile) login_as user diff --git a/spec/system/invitation_requests/invitation_request_is_accepted_spec.rb b/spec/system/invitation_requests/invitation_request_is_accepted_spec.rb index 2951e7a..a69e29f 100644 --- a/spec/system/invitation_requests/invitation_request_is_accepted_spec.rb +++ b/spec/system/invitation_requests/invitation_request_is_accepted_spec.rb @@ -2,7 +2,7 @@ describe 'Solicitação de convite tem status atualizado para "aceita"' do it 'quando recebe convite para o mesmo projeto' do - user = create(:user) + user = create(:user, :paid) invitation_request_one = create(:invitation_request, profile: user.profile, project_id: 1, status: :pending) diff --git a/spec/system/invitation_requests/user_filters_invitation_requests_spec.rb b/spec/system/invitation_requests/user_filters_invitation_requests_spec.rb index d06ec17..6d9ad33 100644 --- a/spec/system/invitation_requests/user_filters_invitation_requests_spec.rb +++ b/spec/system/invitation_requests/user_filters_invitation_requests_spec.rb @@ -2,7 +2,7 @@ describe 'Usuário acessa página de pedidos de convite' do it 'e filtra por status "Processando"' do - user = create(:user) + user = create(:user, :paid) processing_request_one = create(:invitation_request, profile: user.profile, message: 'Me aceita', project_id: 1, status: :processing, created_at: 15.minutes.ago) processing_request_two = create(:invitation_request, profile: user.profile, message: 'Sou bom para este projeto', @@ -45,7 +45,7 @@ end it 'e filtra por status "Pendente"' do - user = create(:user) + user = create(:user, :paid) processing_request = create(:invitation_request, profile: user.profile, message: 'Me aceita', project_id: 1, status: :processing, created_at: 15.minutes.ago) pending_request = create(:invitation_request, profile: user.profile, message: 'Sou bom para este projeto', @@ -73,7 +73,7 @@ end it 'e filtra por status "Aceita"' do - user = create(:user) + user = create(:user, :paid) aborted_request = create(:invitation_request, profile: user.profile, message: 'Me aceita', project_id: 1, status: :aborted, created_at: 2.days.ago) accepted_request = create(:invitation_request, profile: user.profile, message: 'Sou bom para este projeto', @@ -101,7 +101,7 @@ end it 'e filtra por status "Recusada"' do - user = create(:user) + user = create(:user, :paid) refused_request = create(:invitation_request, profile: user.profile, message: 'Me aceita', project_id: 1, status: :refused, created_at: 2.days.ago) pending_request = create(:invitation_request, profile: user.profile, message: 'Sou bom para este projeto', @@ -129,7 +129,7 @@ end it 'e filtra por status "Erro"' do - user = create(:user) + user = create(:user, :paid) error_request = create(:invitation_request, profile: user.profile, message: 'Me aceita', project_id: 1, status: :error, created_at: 2.days.ago) accepted_request = create(:invitation_request, profile: user.profile, message: 'Sou bom para este projeto', @@ -157,7 +157,7 @@ end it 'e filtra por status "Cancelada"' do - user = create(:user) + user = create(:user, :paid) aborted_request = create(:invitation_request, profile: user.profile, message: 'Me aceita', project_id: 1, status: :aborted, created_at: 2.days.ago) accepted_request = create(:invitation_request, profile: user.profile, message: 'Sou bom para este projeto', @@ -185,7 +185,7 @@ end it 'e não existe solicitação com o status selecionado no filtro' do - user = create(:user) + user = create(:user, :paid) pending_request = create(:invitation_request, profile: user.profile, message: 'Me aceita', project_id: 1, status: :pending, created_at: 2.days.ago) accepted_request = create(:invitation_request, profile: user.profile, message: 'Sou bom para este projeto', diff --git a/spec/system/invitation_requests/user_views_invitation_requests_spec.rb b/spec/system/invitation_requests/user_views_invitation_requests_spec.rb index 244b139..203112e 100644 --- a/spec/system/invitation_requests/user_views_invitation_requests_spec.rb +++ b/spec/system/invitation_requests/user_views_invitation_requests_spec.rb @@ -2,7 +2,7 @@ describe 'Usuário acessa página de pedidos de convite' do it 'a partir da home' do - user = create(:user) + user = create(:user, :paid) create(:invitation_request, profile: user.profile, message: 'Me aceita', project_id: 1, status: :pending, created_at: 1.day.ago) create(:invitation_request, profile: user.profile, message: 'Sou bom para este projeto', @@ -33,7 +33,7 @@ end it 'e não existem pedidos' do - user = create(:user) + user = create(:user, :paid) login_as user visit invitation_requests_path @@ -42,7 +42,7 @@ end it 'e ocorre erro na conexão da API Cola?Bora!' do - user = create(:user) + user = create(:user, :paid) create(:invitation_request, profile: user.profile) fake_response = double('faraday_response', success?: false, diff --git a/spec/system/projects/user_request_project_invitation_spec.rb b/spec/system/projects/user_request_project_invitation_spec.rb index d7a23a0..c675c78 100644 --- a/spec/system/projects/user_request_project_invitation_spec.rb +++ b/spec/system/projects/user_request_project_invitation_spec.rb @@ -6,7 +6,7 @@ json_projects_data = File.read(Rails.root.join('./spec/support/json/projects.json')) fake_projects_response = double('faraday_response', status: 200, body: json_projects_data) allow(Faraday).to receive(:get).with('http://localhost:3000/api/v1/projects').and_return(fake_projects_response) - user = create(:user) + user = create(:user, :paid) request_invitation_job_spy = spy(RequestInvitationJob) stub_const('RequestInvitationJob', request_invitation_job_spy) @@ -30,7 +30,7 @@ allow(Faraday).to receive(:get).with('http://localhost:3000/api/v1/projects').and_return(fake_projects_response) - user = create(:user) + user = create(:user, :paid) login_as user diff --git a/spec/system/projects/user_views_projects_spec.rb b/spec/system/projects/user_views_projects_spec.rb index 4fc797a..08341c9 100644 --- a/spec/system/projects/user_views_projects_spec.rb +++ b/spec/system/projects/user_views_projects_spec.rb @@ -9,7 +9,7 @@ allow(Faraday).to receive(:get).with('http://localhost:3000/api/v1/projects').and_return(fake_response) - user = create(:user) + user = create(:user, :paid) login_as user @@ -41,7 +41,7 @@ allow(Faraday).to receive(:get).with('http://localhost:3000/api/v1/projects').and_return(fake_response) - user = create(:user) + user = create(:user, :paid) login_as user visit projects_path @@ -58,7 +58,7 @@ allow(Faraday).to receive(:get).with('http://localhost:3000/api/v1/projects').and_return(fake_response) - user = create(:user) + user = create(:user, :paid) login_as user visit projects_path @@ -86,7 +86,7 @@ allow(Faraday).to receive(:get).with('http://localhost:3000/api/v1/projects').and_return(fake_response) - user = create(:user) + user = create(:user, :paid) login_as user visit projects_path @@ -113,7 +113,7 @@ allow(Faraday).to receive(:get).with('http://localhost:3000/api/v1/projects').and_return(fake_response) - user = create(:user) + user = create(:user, :paid) login_as user visit projects_path @@ -140,7 +140,7 @@ allow(Faraday).to receive(:get).with('http://localhost:3000/api/v1/projects').and_return(fake_response) - user = create(:user) + user = create(:user, :paid) login_as user visit projects_path @@ -167,7 +167,7 @@ allow(Faraday).to receive(:get).with('http://localhost:3000/api/v1/projects').and_return(fake_response) - user = create(:user) + user = create(:user, :paid) login_as user visit projects_path @@ -194,7 +194,7 @@ allow(Faraday).to receive(:get).with('http://localhost:3000/api/v1/projects').and_return(fake_response) - user = create(:user) + user = create(:user, :paid) login_as user visit projects_path expect(page).to have_content('Não foi possível carregar os projetos. Tente mais tarde') @@ -206,7 +206,7 @@ allow(Faraday).to receive(:get).with('http://localhost:3000/api/v1/projects').and_return(fake_response) - user = create(:user) + user = create(:user, :paid) login_as user visit projects_path expect(page).to have_content('Não foi possível carregar os projetos. Tente mais tarde')