From ca8b1627d774e9ffc725c377e7dfeacd08f23c4a Mon Sep 17 00:00:00 2001 From: Shivam Chahar Date: Mon, 25 Apr 2022 17:48:45 +0530 Subject: [PATCH] Add stripe checkout for invoices --- .env.example | 4 ++ .../invoices/payments_controller.rb | 37 ++++++++++++ app/models/client.rb | 17 ++++++ app/models/invoice.rb | 4 ++ app/services/application_service.rb | 7 +++ app/services/invoice_payment/checkout.rb | 59 +++++++++++++++++++ app/views/invoice_mailer/invoice.html.erb | 2 + app/views/invoice_mailer/invoice.text.erb | 2 + app/views/invoices/payments/cancel.html.erb | 21 +++++++ app/views/invoices/payments/success.html.erb | 17 ++++++ config/initializers/stripe.rb | 3 + config/routes.rb | 9 +++ ...20220425074402_add_stripe_id_to_clients.rb | 7 +++ db/schema.rb | 5 +- spec/requests/invoices/payments_spec.rb | 9 +++ 15 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 app/controllers/invoices/payments_controller.rb create mode 100644 app/services/application_service.rb create mode 100644 app/services/invoice_payment/checkout.rb create mode 100644 app/views/invoices/payments/cancel.html.erb create mode 100644 app/views/invoices/payments/success.html.erb create mode 100644 config/initializers/stripe.rb create mode 100644 db/migrate/20220425074402_add_stripe_id_to_clients.rb create mode 100644 spec/requests/invoices/payments_spec.rb diff --git a/.env.example b/.env.example index d18b1dabc5..a19f5d3d00 100644 --- a/.env.example +++ b/.env.example @@ -29,3 +29,7 @@ NEW_RELIC_LICENSE_KEY='replace with newrelic licence key' # Redis url REDIS_URL='redis://127.0.0.1:6379/12' + +# Stripe +STRIPE_PUBLISHABLE_KEY="pk_test_NOgckL4BT40aggiIRMiU8O2g00yhQxp1yS" +STRIPE_SECRET_KEY="sk_test_0upT8snNIjPXvOteHwRKhOHK00dGraUDu5" diff --git a/app/controllers/invoices/payments_controller.rb b/app/controllers/invoices/payments_controller.rb new file mode 100644 index 0000000000..306adb50dc --- /dev/null +++ b/app/controllers/invoices/payments_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class Invoices::PaymentsController < ApplicationController + skip_before_action :authenticate_user! + skip_after_action :verify_authorized + before_action :load_invoice + before_action :ensure_invoice_unpaid, only: [:new] + + def new + session = @invoice.create_checkout_session!( + success_url: success_invoice_payments_url(@invoice), + cancel_url: cancel_invoice_payments_url(@invoice) + ) + + redirect_to session.url, allow_other_host: true + end + + def success + @invoice.paid! + end + + def cancel + render + end + + private + + def load_invoice + @invoice = Invoice.includes(client: :company).find(params[:invoice_id]) + end + + def ensure_invoice_unpaid + if @invoice.paid? + redirect_to success_invoice_payments_url(@invoice.id) + end + end +end diff --git a/app/models/client.rb b/app/models/client.rb index 4110173de4..ec21b4922b 100644 --- a/app/models/client.rb +++ b/app/models/client.rb @@ -12,6 +12,7 @@ # created_at :datetime not null # updated_at :datetime not null # discarded_at :datetime +# stripe_id :string # # Indexes # @@ -89,6 +90,22 @@ def client_detail(time_frame = "week") } end + def register_on_stripe! + self.transaction do + customer = Stripe::Customer.create( + { + email:, + name:, + phone:, + metadata: { + platform_id: id + } + }) + + update!(stripe_id: customer.id) + end + end + private def discard_projects diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 78a282bc64..0d1c3ad13c 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -68,4 +68,8 @@ def update_timesheet_entry_status! timesheet_entry_ids = invoice_line_items.pluck(:timesheet_entry_id) TimesheetEntry.where(id: timesheet_entry_ids).update!(bill_status: :billed) end + + def create_checkout_session!(success_url:, cancel_url:) + InvoicePayment::Checkout.process(invoice: self, success_url:, cancel_url:) + end end diff --git a/app/services/application_service.rb b/app/services/application_service.rb new file mode 100644 index 0000000000..b9a1299cfd --- /dev/null +++ b/app/services/application_service.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ApplicationService + def self.process(*args, &block) + new(*args, &block).process + end +end diff --git a/app/services/invoice_payment/checkout.rb b/app/services/invoice_payment/checkout.rb new file mode 100644 index 0000000000..8fd90b87cf --- /dev/null +++ b/app/services/invoice_payment/checkout.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module InvoicePayment + class Checkout < ApplicationService + def initialize(params) + @invoice = params[:invoice] + @company = invoice.client.company + @client = invoice.client + @success_url = params[:success_url] + @cancel_url = params[:cancel_url] + end + + def process + Invoice.transaction do + ensure_client_registered! + checkout! + end + end + + private + + attr_reader :invoice, :company, :client, :success_url, :cancel_url + + def ensure_client_registered! + return if client.stripe_id? + + client.register_on_stripe! + end + + def description + "Invoice from #{company.name} for #{currency} #{invoice.amount} due on #{invoice.due_date}" + end + + def currency + company.base_currency + end + + def checkout! + Stripe::Checkout::Session.create( + { + line_items: [{ + price_data: { + currency: company.base_currency.downcase, + product_data: { + name: invoice.invoice_number, + description: + }, + unit_amount: invoice.amount.to_i + }, + quantity: 1 + }], + mode: "payment", + customer: client.reload.stripe_id, + success_url:, + cancel_url: + }) + end + end +end diff --git a/app/views/invoice_mailer/invoice.html.erb b/app/views/invoice_mailer/invoice.html.erb index 78b2eaf0b8..75c2caffb0 100644 --- a/app/views/invoice_mailer/invoice.html.erb +++ b/app/views/invoice_mailer/invoice.html.erb @@ -14,6 +14,8 @@ The due date is <%= @invoice.due_date %>.

+

You can pay for the invoice here: <%= link_to nil, new_invoice_payment_url(@invoice), target: "_blank", rel: "nofollow" %>

+

Thanks!

diff --git a/app/views/invoice_mailer/invoice.text.erb b/app/views/invoice_mailer/invoice.text.erb index 760fc8de7d..2b0b92f74b 100644 --- a/app/views/invoice_mailer/invoice.text.erb +++ b/app/views/invoice_mailer/invoice.text.erb @@ -4,5 +4,7 @@ Hi, <%= @invoice.client_name %> You have an invoice - <%= @invoice.invoice_number %> with amount <%= @invoice.amount %> due date is: <%= @invoice.due_date %>. +You can pay for the invoice here: <%= new_invoice_payment_url(@invoice) %> + Thanks! Miru diff --git a/app/views/invoices/payments/cancel.html.erb b/app/views/invoices/payments/cancel.html.erb new file mode 100644 index 0000000000..84a7f535c3 --- /dev/null +++ b/app/views/invoices/payments/cancel.html.erb @@ -0,0 +1,21 @@ +
+
+
+ +
+ +
+
+

Invoice #<%= @invoice.invoice_number %>

+

Payment was cancelled.

+

Didn't cancel the payment?

+
+ <%= link_to "Try again", new_invoice_payment_url(@invoice), class: "text-base font-medium text-miru-han-purple-600 group-hover:text-miru-han-purple-400", target: "_blank", rel: "nofollow" %> + +
+
+
+
+
diff --git a/app/views/invoices/payments/success.html.erb b/app/views/invoices/payments/success.html.erb new file mode 100644 index 0000000000..47c612b919 --- /dev/null +++ b/app/views/invoices/payments/success.html.erb @@ -0,0 +1,17 @@ +
+
+
+ +
+ +
+
+

Invoice #<%= @invoice.invoice_number %>

+

Payment was successful. 🎉

+

We have received your payment.

+
+
+
+
diff --git a/config/initializers/stripe.rb b/config/initializers/stripe.rb new file mode 100644 index 0000000000..388e5264fe --- /dev/null +++ b/config/initializers/stripe.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +Stripe.api_key = ENV["STRIPE_SECRET_KEY"] diff --git a/config/routes.rb b/config/routes.rb index 92d4dc1c38..c8d835ceb1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -40,6 +40,15 @@ def draw(routes_name) resources :reports, only: [:index] resources :workspaces, only: [:update] + resources :invoices, only: [], module: :invoices do + resources :payments, only: [:new] do + collection do + get :success + get :cancel + end + end + end + get "clients/*path", to: "clients#index", via: :all get "clients", to: "clients#index" diff --git a/db/migrate/20220425074402_add_stripe_id_to_clients.rb b/db/migrate/20220425074402_add_stripe_id_to_clients.rb new file mode 100644 index 0000000000..e2f2755fab --- /dev/null +++ b/db/migrate/20220425074402_add_stripe_id_to_clients.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddStripeIdToClients < ActiveRecord::Migration[7.0] + def change + add_column :clients, :stripe_id, :string, default: nil + end +end diff --git a/db/schema.rb b/db/schema.rb index 659a7c39b1..15537bea6d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,7 +12,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_04_14_122335) do +ActiveRecord::Schema.define(version: 2022_04_25_074402) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -54,6 +54,7 @@ t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.datetime "discarded_at", precision: 6 + t.string "stripe_id" t.index ["company_id"], name: "index_clients_on_company_id" t.index ["discarded_at"], name: "index_clients_on_discarded_at" t.index ["email", "company_id"], name: "index_clients_on_email_and_company_id", unique: true @@ -164,7 +165,7 @@ t.bigint "user_id", null: false t.bigint "project_id", null: false t.float "duration", null: false - t.text "note", default: "" + t.text "note" t.date "work_date", null: false t.integer "bill_status", null: false t.datetime "created_at", precision: 6, null: false diff --git a/spec/requests/invoices/payments_spec.rb b/spec/requests/invoices/payments_spec.rb new file mode 100644 index 0000000000..c8d78d2228 --- /dev/null +++ b/spec/requests/invoices/payments_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "Invoices::Payments", type: :request do + describe "GET /index" do + pending "add some examples (or delete) #{__FILE__}" + end +end