From 5dde75ccfc28794d50e8fa0e048b79e227b368f3 Mon Sep 17 00:00:00 2001 From: swatijadhav Date: Sat, 6 Feb 2021 23:10:48 +0530 Subject: [PATCH 1/4] stripe integration setup --- .env.sample | 3 + Gemfile | 1 + Gemfile.lock | 2 + app/controllers/api/v1/ability.rb | 1 + app/controllers/api/v1/stripe_controller.rb | 26 ++++++ app/services/stripe_service.rb | 92 +++++++++++++++++++++ config/routes.rb | 8 +- config/secrets.yml | 3 + db/schema.rb | 59 +++++++------ 9 files changed, 169 insertions(+), 26 deletions(-) create mode 100644 app/controllers/api/v1/stripe_controller.rb create mode 100644 app/services/stripe_service.rb diff --git a/.env.sample b/.env.sample index bf66b2e28..5b3d2a4e4 100644 --- a/.env.sample +++ b/.env.sample @@ -59,3 +59,6 @@ STOCKIT_API_TOKEN= SLACK_API_TOKEN= SLACK_PIN_CHANNEL= + +STRIPE_PUBLISHABLE_KEY= +STRIPE_SECRET_KEY= diff --git a/Gemfile b/Gemfile index 4b0ae7d68..0286664b8 100755 --- a/Gemfile +++ b/Gemfile @@ -52,6 +52,7 @@ gem 'sidekiq-statistic' gem 'sinatra', require: nil # for sidekiq reporting console gem 'slack-ruby-client' gem 'state_machine' +gem 'stripe', '~> 5.29.0' gem 'traco' gem 'twilio-ruby', '~> 5.11.0' gem 'whenever', '~> 0.9.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 1816bf2b1..6beabc7d1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -486,6 +486,7 @@ GEM net-scp (>= 1.1.2) net-ssh (>= 2.8.0) state_machine (1.2.0) + stripe (5.29.0) sys-uname (1.2.1) ffi (>= 1.0.0) thor (1.0.1) @@ -598,6 +599,7 @@ DEPENDENCIES spring spring-commands-rspec state_machine + stripe (~> 5.29.0) timecop traco twilio-ruby (~> 5.11.0) diff --git a/app/controllers/api/v1/ability.rb b/app/controllers/api/v1/ability.rb index 479579f10..4e82f2824 100644 --- a/app/controllers/api/v1/ability.rb +++ b/app/controllers/api/v1/ability.rb @@ -385,6 +385,7 @@ def taxonomies can [:index, :show], UserRole can [:index, :show], CancellationReason can [:names], Organisation + can [:fetch_public_key, :create_setupintent, :save_payment_method], :stripe if can_add_or_remove_inventory_number? || @api_user can [:create, :remove_number], InventoryNumber diff --git a/app/controllers/api/v1/stripe_controller.rb b/app/controllers/api/v1/stripe_controller.rb new file mode 100644 index 000000000..d29f9cde8 --- /dev/null +++ b/app/controllers/api/v1/stripe_controller.rb @@ -0,0 +1,26 @@ +module Api + module V1 + class StripeController < Api::V1::ApiController + + # skip_before_action :validate_token, only: [:index, :show] + load_and_authorize_resource class: false + + api :GET, "/v1/fetch_public_key", "Get stripe public key" + def fetch_public_key + render json: StripeService.new.public_key + end + + api :POST, "/v1/create_setupintent", "Create setup-intent for current-user" + def create_setupintent + render json: StripeService.new.create_setup_intent + end + + api :POST, "/v1/save_payment_method", "Save payment-method" + def save_payment_method + puts params + render json: params + end + + end + end +end diff --git a/app/services/stripe_service.rb b/app/services/stripe_service.rb new file mode 100644 index 000000000..9641a158f --- /dev/null +++ b/app/services/stripe_service.rb @@ -0,0 +1,92 @@ +## Steps for saving cards for future use: +## 1. Create or retrieve custome from associated user. +## 2. Create setup-intent for customer. (Save setup-intent-ID and customer-ID for future use) +## 3. On client side: setup-intent-ID is passed, which is used to store card details and +## returns payment-method details. +## 4. Update payment-method value in record. (created in step 2) + +class StripeService + + def initialize + Stripe.api_key = stripe_secret_key + end + + def create_customer(user) + customer = Stripe::Customer.create({ + email: user&.email, + name: user&.full_name, + phone: user&.mobile, + }) + + ## TODO: + ## Save customer-id as stripe_customer_id for User + + customer + end + + def fetch_customer(user_id=nil) + user = user_id ? User.find_by(id: user_id) : User.current_user + customer_response = nil + + ## TODO: + ## Add column stripe_customer_id in users. + + # if user&.stripe_customer_id + # begin + # customer_response = Stripe::Customer.retrieve(user.stripe_customer_id) + # rescue Stripe::InvalidRequestError => e + # create_customer(user) + # end + # else + # create_customer(user) + # end + + customer_response = Stripe::Customer.retrieve("cus_IqFB5lMIYw6i8a") + end + + def create_setup_intent(user_id=nil) + customer = fetch_customer(user_id) + + setup_intent_response = Stripe::SetupIntent.create({ + customer: customer['id'] + }) + + ## TODO: + ## Add new record having setup-intent-id and customer-id in stripe-payments table + + setup_intent_response + end + + ## Reference: https :/ / stripe.com / docs / payments / capture - later + # def create_payment_intent(amount, currency, offer_id) + # intent = Stripe::PaymentIntent.create({ + # amount: amount, + # currency: currency, + # payment_method_types: ['card'], + # statement_descriptor: "GOGOX Booking Charge for offer ##{offer_id}", + # setup_future_usage: 'off_session', + # capture_method: 'manual', + # metadata: { + # integration_check: "accept_a_payment", + # offer_id: offer_id, + # }, + # }) + # end + + def public_key + { + 'publicKey': stripe_publishable_key + } + end + + private + + def stripe_publishable_key + Rails.application.secrets.stripe[:publishable_key] + end + + def stripe_secret_key + Rails.application.secrets.stripe[:secret_key] + end + +end diff --git a/config/routes.rb b/config/routes.rb index 800965672..853079e10 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,7 +16,7 @@ resources :users do get :me, on: :collection end - + resources :shareables, only: [:show, :index, :create, :destroy, :update] do collection do delete :unshare @@ -225,6 +225,12 @@ get "stockit_items/:id", to: "packages#stockit_item_details" put "orders_packages/:id/actions/:action_name", to: "orders_packages#exec_action" put "packages/:id/actions/:action_name", to: "packages#register_quantity_change" + + get "stripe/fetch_public_key", to: "stripe#fetch_public_key" + post "stripe/create_setupintent", to: "stripe#create_setupintent" + post "stripe/save_payment_method", to: "stripe#save_payment_method" + + end end end diff --git a/config/secrets.yml b/config/secrets.yml index 477844e1b..91ba1f7b3 100755 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -55,6 +55,9 @@ base: &BASE printer_host: <%=ENV['BARCODE_PRINTER_HOST']%> printer_user: <%=ENV['BARCODE_PRINTER_USER']%> printer_pwd: <%=ENV['BARCODE_PRINTER_PWD']%> + stripe: + publishable_key: <%= ENV['STRIPE_PUBLISHABLE_KEY'] %> + secret_key: <%= ENV['STRIPE_SECRET_KEY'] %> development: <<: *BASE diff --git a/db/schema.rb b/db/schema.rb index 9c2a6decd..d2c67fbad 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_01_12_024003) do +ActiveRecord::Schema.define(version: 2021_01_18_111336) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gin" @@ -442,7 +442,7 @@ t.index ["contact_id"], name: "index_order_transports_on_contact_id" t.index ["gogovan_order_id"], name: "index_order_transports_on_gogovan_order_id" t.index ["gogovan_transport_id"], name: "index_order_transports_on_gogovan_transport_id" - t.index ["order_id"], name: "index_order_transports_on_order_id" + t.index ["order_id"], name: "index_order_transports_on_order_id", unique: true t.index ["scheduled_at"], name: "index_order_transports_on_scheduled_at" end @@ -450,12 +450,9 @@ t.string "code" t.string "detail_type" t.integer "detail_id" - t.integer "stockit_contact_id" - t.integer "stockit_organisation_id" t.datetime "created_at" t.datetime "updated_at", null: false t.text "description" - t.integer "stockit_activity_id" t.integer "country_id" t.integer "created_by_id" t.integer "processed_by_id" @@ -483,6 +480,9 @@ t.integer "cancellation_reason_id" t.boolean "continuous", default: false t.date "shipment_date" + t.integer "stockit_activity_id" + t.integer "stockit_organisation_id" + t.integer "stockit_contact_id" t.index ["address_id"], name: "index_orders_on_address_id" t.index ["beneficiary_id"], name: "index_orders_on_beneficiary_id" t.index ["cancelled_by_id"], name: "index_orders_on_cancelled_by_id" @@ -498,9 +498,6 @@ t.index ["processed_by_id"], name: "index_orders_on_processed_by_id" t.index ["shipment_date"], name: "index_orders_on_shipment_date" t.index ["state"], name: "index_orders_on_state" - t.index ["stockit_activity_id"], name: "index_orders_on_stockit_activity_id" - t.index ["stockit_contact_id"], name: "index_orders_on_stockit_contact_id" - t.index ["stockit_organisation_id"], name: "index_orders_on_stockit_organisation_id" t.index ["submitted_by_id"], name: "index_orders_on_submitted_by_id" end @@ -656,13 +653,9 @@ t.integer "box_id" t.integer "pallet_id" t.integer "order_id" - t.date "stockit_sent_on" - t.date "stockit_designated_on" - t.integer "stockit_designated_by_id" - t.integer "stockit_sent_by_id" + t.integer "on_hand_boxed_quantity", default: 0 + t.integer "on_hand_palletized_quantity", default: 0 t.integer "favourite_image_id" - t.date "stockit_moved_on" - t.integer "stockit_moved_by_id" t.boolean "saleable" t.string "case_number" t.boolean "allow_web_publish" @@ -681,8 +674,12 @@ t.integer "package_set_id" t.integer "restriction_id" t.text "comment" - t.integer "on_hand_boxed_quantity", default: 0 - t.integer "on_hand_palletized_quantity", default: 0 + t.integer "stockit_moved_by_id" + t.datetime "stockit_moved_on" + t.integer "stockit_sent_by_id" + t.integer "stockit_designated_by_id" + t.datetime "stockit_designated_on" + t.datetime "stockit_sent_on" t.text "notes_zh_tw" t.index ["allow_web_publish"], name: "index_packages_on_allow_web_publish" t.index ["available_quantity"], name: "index_packages_on_available_quantity" @@ -705,9 +702,6 @@ t.index ["package_type_id"], name: "index_packages_on_package_type_id" t.index ["pallet_id"], name: "index_packages_on_pallet_id" t.index ["state"], name: "index_packages_on_state", using: :gin - t.index ["stockit_designated_by_id"], name: "index_packages_on_stockit_designated_by_id" - t.index ["stockit_moved_by_id"], name: "index_packages_on_stockit_moved_by_id" - t.index ["stockit_sent_by_id"], name: "index_packages_on_stockit_sent_by_id" t.index ["storage_type_id"], name: "index_packages_on_storage_type_id" end @@ -867,6 +861,7 @@ t.text "notes_zh_tw" t.index ["created_by_id"], name: "index_shareables_on_created_by_id" t.index ["expires_at"], name: "index_shareables_on_expires_at" + t.index ["public_uid"], name: "index_shareables_on_public_uid" t.index ["resource_id", "resource_type"], name: "index_shareables_on_resource_id_and_resource_type", unique: true t.index ["resource_type", "resource_id"], name: "index_shareables_on_resource_type_and_resource_id", unique: true t.index ["resource_type"], name: "index_shareables_on_resource_type" @@ -988,6 +983,26 @@ t.datetime "updated_at" end + create_table "transport_orders", force: :cascade do |t| + t.integer "transport_provider_id" + t.string "order_uuid" + t.string "status" + t.datetime "scheduled_at" + t.jsonb "metadata" + t.integer "offer_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "transport_providers", force: :cascade do |t| + t.string "name" + t.string "logo" + t.text "description" + t.jsonb "metadata", default: "{}" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "user_favourites", force: :cascade do |t| t.string "favourite_type" t.integer "favourite_id" @@ -1110,10 +1125,7 @@ add_foreign_key "orders", "countries", name: "orders_country_id_fk" add_foreign_key "orders", "districts", name: "orders_district_id_fk" add_foreign_key "orders", "organisations", name: "orders_organisation_id_fk" - add_foreign_key "orders", "stockit_activities", name: "orders_stockit_activity_id_fk" - add_foreign_key "orders", "stockit_contacts", name: "orders_stockit_contact_id_fk" add_foreign_key "orders", "stockit_local_orders", column: "detail_id", name: "orders_detail_id_fk" - add_foreign_key "orders", "stockit_organisations", name: "orders_stockit_organisation_id_fk" add_foreign_key "orders", "users", column: "cancelled_by_id", name: "orders_cancelled_by_id_fk" add_foreign_key "orders", "users", column: "closed_by_id", name: "orders_closed_by_id_fk" add_foreign_key "orders", "users", column: "created_by_id", name: "orders_created_by_id_fk" @@ -1147,9 +1159,6 @@ add_foreign_key "packages", "pallets", name: "packages_pallet_id_fk" add_foreign_key "packages", "restrictions", name: "packages_restriction_id_fk" add_foreign_key "packages", "storage_types", name: "packages_storage_type_id_fk" - add_foreign_key "packages", "users", column: "stockit_designated_by_id", name: "packages_stockit_designated_by_id_fk" - add_foreign_key "packages", "users", column: "stockit_moved_by_id", name: "packages_stockit_moved_by_id_fk" - add_foreign_key "packages", "users", column: "stockit_sent_by_id", name: "packages_stockit_sent_by_id_fk" add_foreign_key "packages_inventories", "locations" add_foreign_key "packages_inventories", "packages" add_foreign_key "packages_inventories", "users" From 53b21dd4f32f4efb8c2a61ed831caf4ecf9396c7 Mon Sep 17 00:00:00 2001 From: swatijadhav Date: Tue, 23 Feb 2021 21:52:29 +0530 Subject: [PATCH 2/4] updated stripe service --- app/controllers/api/v1/stripe_controller.rb | 29 ++- app/models/stripe_payment.rb | 3 + app/services/stripe_service.rb | 166 +++++++++++------- ...7160745_add_stripe_customer_id_to_users.rb | 5 + .../20210217163846_create_stripe_payments.rb | 17 ++ db/schema.rb | 20 ++- spec/factories/stripe_payments.rb | 10 ++ spec/models/stripe_payment_spec.rb | 4 + 8 files changed, 190 insertions(+), 64 deletions(-) create mode 100644 app/models/stripe_payment.rb create mode 100644 db/migrate/20210217160745_add_stripe_customer_id_to_users.rb create mode 100644 db/migrate/20210217163846_create_stripe_payments.rb create mode 100644 spec/factories/stripe_payments.rb create mode 100644 spec/models/stripe_payment_spec.rb diff --git a/app/controllers/api/v1/stripe_controller.rb b/app/controllers/api/v1/stripe_controller.rb index d29f9cde8..2d7c2c80a 100644 --- a/app/controllers/api/v1/stripe_controller.rb +++ b/app/controllers/api/v1/stripe_controller.rb @@ -2,7 +2,6 @@ module Api module V1 class StripeController < Api::V1::ApiController - # skip_before_action :validate_token, only: [:index, :show] load_and_authorize_resource class: false api :GET, "/v1/fetch_public_key", "Get stripe public key" @@ -16,8 +15,34 @@ def create_setupintent end api :POST, "/v1/save_payment_method", "Save payment-method" + param :stripe_response, Hash, required: true + param :source_id, [Integer, String], required: true, desc: "Id of the source (transport_order)" + param :source_type, String, required: true, desc: "Type of the source (transport_order)" + param :authorize_amount, [true, false, 'true', 'false'], allow_nil: true, default: false, desc: 'Amount should be authorized from card for given source' def save_payment_method - puts params + # stripe_response = { + # "id"=>"seti_1ILLMvJG1rVU4bz1HIM6tijG", + # "object"=>"setup_intent", + # "cancellation_reason"=>"", + # "client_secret"=> + # "seti_1ILLMvJG1rVU4bz1HIM6tijG_secret_IxFs70kJjXmCk2UaIm8DqvB5C3iHod0", + # "created"=>"1613450461", + # "description"=>"", + # "last_setup_error"=>"", + # "livemode"=>"false", + # "next_action"=>"", + # "payment_method"=>"pm_1ILLjYJG1rVU4bz1ElxLd5Mj", # <---- + # "payment_method_types"=>["card"], + # "status"=>"succeeded", + # "usage"=>"off_session" + # } + + StripeService.new( + source_type: params[:source_type], + source_id: params[:source_id], + authorize_amount: params[:authorize_amount] + ).save_payment_method(params[:stripe_response]) + render json: params end diff --git a/app/models/stripe_payment.rb b/app/models/stripe_payment.rb new file mode 100644 index 000000000..1ecd8c02e --- /dev/null +++ b/app/models/stripe_payment.rb @@ -0,0 +1,3 @@ +class StripePayment < ApplicationRecord + belongs_to :source, polymorphic: true +end diff --git a/app/services/stripe_service.rb b/app/services/stripe_service.rb index 9641a158f..1895a2845 100644 --- a/app/services/stripe_service.rb +++ b/app/services/stripe_service.rb @@ -1,82 +1,89 @@ ## Steps for saving cards for future use: -## 1. Create or retrieve custome from associated user. -## 2. Create setup-intent for customer. (Save setup-intent-ID and customer-ID for future use) +## 1. Create or retrieve customer from associated user. +## 2. Create setup-intent for customer. ## 3. On client side: setup-intent-ID is passed, which is used to store card details and ## returns payment-method details. ## 4. Update payment-method value in record. (created in step 2) +## 5. Authorize amount to above saved-card i.e. payment_method. Set capture_method as 'manual' +## 6. Capture payment class StripeService - def initialize + attr_accessor :user_id, :source_type, :source_id, :customer_id, :amount, :payment, + :authorize_amount + + def initialize(args={}) + @user_id = User.current_user.try(:id) + @source_type = args[:source_type] + @source_id = args[:source_id] + @authorize_amount = args[:authorize_amount] + Stripe.api_key = stripe_secret_key + @customer_id = fetch_customer_id # STEP 1 end - def create_customer(user) - customer = Stripe::Customer.create({ - email: user&.email, - name: user&.full_name, - phone: user&.mobile, + # STEP 2 + def create_setup_intent + Stripe::SetupIntent.create({ + customer: @customer_id }) - - ## TODO: - ## Save customer-id as stripe_customer_id for User - - customer end - def fetch_customer(user_id=nil) - user = user_id ? User.find_by(id: user_id) : User.current_user - customer_response = nil - - ## TODO: - ## Add column stripe_customer_id in users. - - # if user&.stripe_customer_id - # begin - # customer_response = Stripe::Customer.retrieve(user.stripe_customer_id) - # rescue Stripe::InvalidRequestError => e - # create_customer(user) - # end - # else - # create_customer(user) - # end - - customer_response = Stripe::Customer.retrieve("cus_IqFB5lMIYw6i8a") + def public_key + { + 'publicKey': stripe_publishable_key + } end - def create_setup_intent(user_id=nil) - customer = fetch_customer(user_id) - - setup_intent_response = Stripe::SetupIntent.create({ - customer: customer['id'] - }) + # STEP 4 + def save_payment_method(details) + setup_stripe_payment( + setup_intent_id: details[:id], + status: details[:status], + payment_method_id: details[:payment_method] + ) + + if @authorize_amount.downcase.to_s == 'true' + charge_saved_card(amount_for_source, @customer_id, details[:payment_method]) + end + end - ## TODO: - ## Add new record having setup-intent-id and customer-id in stripe-payments table + # STEP 5 + def charge_saved_card(amount, customer_id, payment_method_id) + begin + intent = Stripe::PaymentIntent.create({ + amount: amount, + currency: "inr", + customer: customer_id, + payment_method: payment_method_id, + off_session: true, + confirm: true, + capture_method: 'manual', # https://stripe.com/docs/payments/capture-later + }) + + # Update payment-intent details for customer's service. + @payment.update({payment_intent_id: intent["id"], status: intent["status"]}) + + rescue Stripe::CardError => e + # TODO + # Error code will be authentication_required if authentication is needed + puts "Error is: #{e.error.code}" + payment_intent_id = e.error.payment_intent.id + payment_intent = Stripe::PaymentIntent.retrieve(payment_intent_id) + puts payment_intent.id + end - setup_intent_response end - ## Reference: https :/ / stripe.com / docs / payments / capture - later - # def create_payment_intent(amount, currency, offer_id) - # intent = Stripe::PaymentIntent.create({ - # amount: amount, - # currency: currency, - # payment_method_types: ['card'], - # statement_descriptor: "GOGOX Booking Charge for offer ##{offer_id}", - # setup_future_usage: 'off_session', - # capture_method: 'manual', - # metadata: { - # integration_check: "accept_a_payment", - # offer_id: offer_id, - # }, - # }) - # end - - def public_key - { - 'publicKey': stripe_publishable_key - } + # STEP 6 + # Capture payment when specific service(transport-order) is completed. + def capture_payment(payment_intent_id, amount) + intent = Stripe::PaymentIntent.capture( + payment_intent_id, + { + amount_to_capture: amount, + } + ) end private @@ -89,4 +96,43 @@ def stripe_secret_key Rails.application.secrets.stripe[:secret_key] end + def fetch_customer_id + user = User.find_by(id: @user_id) + user&.stripe_customer_id || create_customer(user) + end + + def create_customer(user) + customer = Stripe::Customer.create({ + email: user&.email, + name: user&.full_name, + phone: user&.mobile, + }) + + user.update_column(:stripe_customer_id, customer.id) + customer.id + end + + def setup_stripe_payment(details) + @payment = StripePayment.create( + setup_intent_id: details[:setup_intent_id], + payment_method_id: details[:payment_method_id], + status: details[:status], + user_id: @user_id, + amount: @amount, + source_id: @source_id, + source_type: @source_type + ) + end + + # TODO: + # Fetch amount from the source for which amount has to be deducted. + def amount_for_source + if @source_id && @source_type + # @amount = @payment&.source&.amount + @amount = 30000 + end + + @amount + end + end diff --git a/db/migrate/20210217160745_add_stripe_customer_id_to_users.rb b/db/migrate/20210217160745_add_stripe_customer_id_to_users.rb new file mode 100644 index 000000000..f490a27fb --- /dev/null +++ b/db/migrate/20210217160745_add_stripe_customer_id_to_users.rb @@ -0,0 +1,5 @@ +class AddStripeCustomerIdToUsers < ActiveRecord::Migration[5.2] + def change + add_column :users, :stripe_customer_id, :string, default: nil + end +end diff --git a/db/migrate/20210217163846_create_stripe_payments.rb b/db/migrate/20210217163846_create_stripe_payments.rb new file mode 100644 index 000000000..156d8d171 --- /dev/null +++ b/db/migrate/20210217163846_create_stripe_payments.rb @@ -0,0 +1,17 @@ +class CreateStripePayments < ActiveRecord::Migration[5.2] + def change + create_table :stripe_payments do |t| + t.integer :user_id + t.string :setup_intent_id + t.string :payment_method_id + t.string :payment_intent_id + t.float :amount + t.string :status + t.string :receipt_url + t.string :source_type + t.integer :source_id + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index d2c67fbad..e899f838c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_01_18_111336) do +ActiveRecord::Schema.define(version: 2021_02_17_163846) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gin" @@ -947,6 +947,20 @@ t.integer "max_unit_quantity" end + create_table "stripe_payments", force: :cascade do |t| + t.integer "user_id" + t.string "setup_intent_id" + t.string "payment_method_id" + t.string "payment_intent_id" + t.float "amount" + t.string "status" + t.string "receipt_url" + t.string "source_type" + t.integer "source_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "subpackage_types", id: :serial, force: :cascade do |t| t.integer "package_type_id" t.integer "subpackage_type_id" @@ -989,7 +1003,8 @@ t.string "status" t.datetime "scheduled_at" t.jsonb "metadata" - t.integer "offer_id" + t.integer "source_id" + t.string "source_type" t.datetime "created_at", null: false t.datetime "updated_at", null: false end @@ -1043,6 +1058,7 @@ t.boolean "receive_email", default: false t.string "other_phone" t.string "preferred_language" + t.string "stripe_customer_id" t.index ["image_id"], name: "index_users_on_image_id" t.index ["mobile"], name: "index_users_on_mobile" t.index ["sms_reminder_sent_at"], name: "index_users_on_sms_reminder_sent_at" diff --git a/spec/factories/stripe_payments.rb b/spec/factories/stripe_payments.rb new file mode 100644 index 000000000..77f6683aa --- /dev/null +++ b/spec/factories/stripe_payments.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :stripe_payment do + user_id { 1 } + setup_intent_id { "MyString" } + payment_intent_id { "MyString" } + amount { 1.5 } + status { "MyString" } + receipt_url { "MyString" } + end +end diff --git a/spec/models/stripe_payment_spec.rb b/spec/models/stripe_payment_spec.rb new file mode 100644 index 000000000..238b93da7 --- /dev/null +++ b/spec/models/stripe_payment_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe StripePayment, type: :model do +end From b8fa2a10ab515cd8536430099ade0539454d8f2d Mon Sep 17 00:00:00 2001 From: swatijadhav Date: Thu, 25 Feb 2021 21:34:28 +0530 Subject: [PATCH 3/4] updated stripe-service specs --- app/services/stripe_service.rb | 9 +- spec/factories/users.rb | 4 + spec/services/stripe_service_spec.rb | 147 +++++++++++++++++++++++++++ spec/support/env.rb | 3 + 4 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 spec/services/stripe_service_spec.rb diff --git a/app/services/stripe_service.rb b/app/services/stripe_service.rb index 1895a2845..b56b8d3c8 100644 --- a/app/services/stripe_service.rb +++ b/app/services/stripe_service.rb @@ -43,13 +43,13 @@ def save_payment_method(details) payment_method_id: details[:payment_method] ) - if @authorize_amount.downcase.to_s == 'true' - charge_saved_card(amount_for_source, @customer_id, details[:payment_method]) + if @authorize_amount.to_s.downcase == 'true' + authorize_amount_on_saved_card(amount_for_source, @customer_id, details[:payment_method]) end end # STEP 5 - def charge_saved_card(amount, customer_id, payment_method_id) + def authorize_amount_on_saved_card(amount, customer_id, payment_method_id) begin intent = Stripe::PaymentIntent.create({ amount: amount, @@ -58,7 +58,7 @@ def charge_saved_card(amount, customer_id, payment_method_id) payment_method: payment_method_id, off_session: true, confirm: true, - capture_method: 'manual', # https://stripe.com/docs/payments/capture-later + capture_method: 'manual', # Ref: https://stripe.com/docs/payments/capture-later }) # Update payment-intent details for customer's service. @@ -72,7 +72,6 @@ def charge_saved_card(amount, customer_id, payment_method_id) payment_intent = Stripe::PaymentIntent.retrieve(payment_intent_id) puts payment_intent.id end - end # STEP 6 diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 63c73e8a3..dc39ffeb6 100755 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -86,6 +86,10 @@ is_mobile_verified { false } end + trait :stripe_user do + stripe_customer_id { 'cus_IzVEJhwLTIZg1F' } + end + trait :stockit_user do first_name { 'Stockit' } last_name { 'User' } diff --git a/spec/services/stripe_service_spec.rb b/spec/services/stripe_service_spec.rb new file mode 100644 index 000000000..994805bd6 --- /dev/null +++ b/spec/services/stripe_service_spec.rb @@ -0,0 +1,147 @@ +require "rails_helper" + +describe StripeService do + + let(:stripe_object) { StripeService.new(attributes) } + + let(:user) { create :user, :stripe_user } + let(:new_user) { create :user } + + let(:stripe_setupintent_object) { + { + id: "seti_1IO3z18WYtC2zB", + object: "setup_intent", + customer: user.stripe_customer_id + } + } + + let(:attributes) { + { + source_type: 'transport_order', + source_id: 1, + authorize_amount: false, + } + } + + let(:stripe_payment_method_object) { + { + id: "seti_1IO3abJG1rVU42zB", + payment_method: "pm_1IO3bbz1shC6ytgC", + status: "requires_payment_method" + } + } + + let(:authorize_charge_response) { + { id: "pi_1IO3bLJG1rVU4", status: "requires_capture" } + } + + before { User.current_user = user } + + context "initialization" do + it "user_id" do + expect(stripe_object.user_id).to eql(user.id) + end + + it "customer_id" do + expect(stripe_object.customer_id).to eql(user.stripe_customer_id) + end + + it "source_type" do + expect(stripe_object.source_type).to eql(attributes[:source_type]) + end + + it "source_id" do + expect(stripe_object.source_id).to eql(attributes[:source_id]) + end + + it "authorize_amount" do + expect(stripe_object.authorize_amount).to eql(attributes[:authorize_amount]) + end + end + + context "public_key" do + it do + expect(stripe_object.public_key).to eql( + { publicKey: Rails.application.secrets.stripe[:publishable_key] } + ) + end + end + + context "stripe customer creation" do + + before { User.current_user = new_user } + let(:stripe_customer_id) { "cus_J0KAOGkksjsbBT" } + + it "should hit stript endpoint to create new customer" do + + stub_request(:post, "https://api.stripe.com/v1/customers"). + with( + body: {"email" => new_user.email, "name" => new_user.full_name, "phone" => new_user.mobile}, + ).to_return(status: 200, body: {id: stripe_customer_id}.to_json, headers: {}) + + stripe_object = StripeService.new() + + expect(stripe_object.customer_id).to eq(stripe_customer_id) + expect(new_user.reload.stripe_customer_id).to eq(stripe_customer_id) + end + end + + context "create_setup_intent" do + it "should hit stripe to initiate stripe-payment process for customer" do + + stub_request(:post, "https://api.stripe.com/v1/setup_intents") + .with(body: {"customer"=>"cus_IzVEJhwLTIZg1F"}) + .to_return(status: 200, body: stripe_setupintent_object.to_json, headers: {}) + + expect(stripe_object.create_setup_intent.to_json).to eql(stripe_setupintent_object.to_json) + end + end + + context "save_payment_method" do + it "add StripePayment record for customer payment-method details" do + expect{ + stripe_object.save_payment_method(stripe_payment_method_object) + }.to change(StripePayment, :count).by(1) + + payment = StripePayment.last + expect(payment.setup_intent_id).to eq(stripe_payment_method_object[:id]) + expect(payment.payment_method_id).to eq(stripe_payment_method_object[:payment_method]) + expect(payment.status).to eq(stripe_payment_method_object[:status]) + end + end + + context "authorize_amount_on_saved_card" do + before { stripe_object.save_payment_method(stripe_payment_method_object) } + + it "should hit stripe to authorize charge manually" do + + stub_request(:post, "https://api.stripe.com/v1/payment_intents") + .with( + body: { + amount: "20000", + capture_method: "manual", + confirm: "true", + currency: "inr", + customer: user.stripe_customer_id, + off_session: "true", + payment_method: stripe_payment_method_object[:payment_method] + }) + .to_return(status: 200, body: authorize_charge_response.to_json, headers: {}) + + stripe_object.authorize_amount_on_saved_card(20000, user.stripe_customer_id, stripe_payment_method_object[:payment_method]) + + expect(StripePayment.last.payment_intent_id).to eq(authorize_charge_response[:id]) + expect(StripePayment.last.status).to eq(authorize_charge_response[:status]) + end + end + + context "capture_payment" do + it "should capture amount from authorized amount" do + stub_request(:post, "https://api.stripe.com/v1/payment_intents/#{authorize_charge_response[:id]}/capture") + .with( body: { amount_to_capture: "20000" }).to_return(status: 200, body: {}.to_json, headers: {}) + + stripe_object.capture_payment(authorize_charge_response[:id], 20000) + end + end + +end diff --git a/spec/support/env.rb b/spec/support/env.rb index 706c4ecf3..8240f1158 100644 --- a/spec/support/env.rb +++ b/spec/support/env.rb @@ -27,3 +27,6 @@ ENV['JWT_VALIDITY_FOR_API'] = "31536000" ENV['OTP_CODE_VALIDITY']="30" ENV['SOCKETIO_SERVICE_URL']="http://localhost:1337/send?site=goodcity&apiKey=132323" + +ENV['STRIPE_PUBLISHABLE_KEY'] = "pk_test_51IBXaDJG1rVU4bz1Z8GkITD4hci63k9cPs5Jq60" +ENV['STRIPE_SECRET_KEY'] = "sk_test_51IBXaDJG1rVU4bz1ZjTt0cI6daI5HuVZi0hOQ5P" From de2b219ced690196a3c2d7c70a132ae5a0069b88 Mon Sep 17 00:00:00 2001 From: swatijadhav Date: Fri, 26 Feb 2021 10:40:49 +0530 Subject: [PATCH 4/4] stripe response for reference --- app/controllers/api/v1/stripe_controller.rb | 38 ++++--- app/services/stripe_service.rb | 113 ++++++++++++++++++++ 2 files changed, 136 insertions(+), 15 deletions(-) diff --git a/app/controllers/api/v1/stripe_controller.rb b/app/controllers/api/v1/stripe_controller.rb index 2d7c2c80a..cd75c7dbf 100644 --- a/app/controllers/api/v1/stripe_controller.rb +++ b/app/controllers/api/v1/stripe_controller.rb @@ -20,21 +20,29 @@ def create_setupintent param :source_type, String, required: true, desc: "Type of the source (transport_order)" param :authorize_amount, [true, false, 'true', 'false'], allow_nil: true, default: false, desc: 'Amount should be authorized from card for given source' def save_payment_method - # stripe_response = { - # "id"=>"seti_1ILLMvJG1rVU4bz1HIM6tijG", - # "object"=>"setup_intent", - # "cancellation_reason"=>"", - # "client_secret"=> - # "seti_1ILLMvJG1rVU4bz1HIM6tijG_secret_IxFs70kJjXmCk2UaIm8DqvB5C3iHod0", - # "created"=>"1613450461", - # "description"=>"", - # "last_setup_error"=>"", - # "livemode"=>"false", - # "next_action"=>"", - # "payment_method"=>"pm_1ILLjYJG1rVU4bz1ElxLd5Mj", # <---- - # "payment_method_types"=>["card"], - # "status"=>"succeeded", - # "usage"=>"off_session" + + # PARAMS: + # { + # "stripe_response": { + # "id": "seti_1IOyT3JG1rVU4bz1mBbHH6Yu", + # "object": "setup_intent", + # "cancellation_reason": "", + # "client_secret": "seti_1IOyT3JG1rVU4bz1mBbHH6Yu_secret_J10Uje2R9PSUp74wN3S3TfwaDyCMMG8", + # "created": "1614315741", + # "description": "", + # "last_setup_error": "", + # "livemode": "false", + # "next_action": "", + # "payment_method": "pm_1IOyTrJG1rVU4bz1wYJv9s5N", "payment_method_types": ["card"], + # "status": "succeeded", + # "usage": "off_session" + # }, + # "source_id": "1", + # "source_type": "transport_order", + # "authorize_amount": "true", + # "format": "json", + # "controller": "api/v1/stripe", + # "action": "save_payment_method" # } StripeService.new( diff --git a/app/services/stripe_service.rb b/app/services/stripe_service.rb index b56b8d3c8..203b4bf64 100644 --- a/app/services/stripe_service.rb +++ b/app/services/stripe_service.rb @@ -27,6 +27,33 @@ def create_setup_intent Stripe::SetupIntent.create({ customer: @customer_id }) + + ## RESPONCE + # # JSON: { + # "id": "seti_1IOyT3JG1rVU4bz1mBbHH6Yu", + # "object": "setup_intent", + # "application": null, + # "cancellation_reason": null, + # "client_secret": "seti_1IOyT3JG1rVU4bz1mBbHH6Yu_secret_J10Uje2R9PSUp74wN3S3TfwaDyCMMG8", + # "created": 1614314537, + # "customer": "cus_IzVEJkwLRIZg1F", + # "description": null, + # "last_setup_error": null, + # "latest_attempt": null, + # "livemode": false, + # "mandate": null, + # "metadata": {}, + # "next_action": null, + # "on_behalf_of": null, + # "payment_method": null, + # "payment_method_options": {"card":{"request_three_d_secure":"automatic"}}, + # "payment_method_types": [ + # "card" + # ], + # "single_use_mandate": null, + # "status": "requires_payment_method", + # "usage": "off_session" + # } end def public_key @@ -51,6 +78,7 @@ def save_payment_method(details) # STEP 5 def authorize_amount_on_saved_card(amount, customer_id, payment_method_id) begin + intent = Stripe::PaymentIntent.create({ amount: amount, currency: "inr", @@ -61,6 +89,48 @@ def authorize_amount_on_saved_card(amount, customer_id, payment_method_id) capture_method: 'manual', # Ref: https://stripe.com/docs/payments/capture-later }) + ## RESPONSE: + # # JSON: { + # "id": "pi_1IOyTuJG1rVU4bz1qiHYyCsO", + # "object": "payment_intent", + # "amount": 30000, + # "amount_capturable": 30000, + # "amount_received": 0, + # "application": null, + # "application_fee_amount": null, + # "canceled_at": null, + # "cancellation_reason": null, + # "capture_method": "manual", + # "charges": {"object":"list","data":[{"id":"ch_1IOyTuJG1rVU4bz1t6dznTFj","object":"charge","amount":30000,"amount_captured":0,"amount_refunded":0,"application":null,"application_fee":null,"application_fee_amount":null,"balance_transaction":null,"billing_details":{"address":{"city":null,"country":null,"line1":null,"line2":null,"postal_code":"42424","state":null},"email":"swati@kiprosh.com","name":null,"phone":null},"calculated_statement_descriptor":"Stripe","captured":false,"created":1614315794,"currency":"inr","customer":"cus_IzVEJkwLRIZg1F","description":null,"destination":null,"dispute":null,"disputed":false,"failure_code":null,"failure_message":null,"fraud_details":{},"invoice":null,"livemode":false,"metadata":{},"on_behalf_of":null,"order":null,"outcome":{"network_status":"approved_by_network","reason":null,"risk_level":"normal","risk_score":43,"seller_message":"Payment complete.","type":"authorized"},"paid":true,"payment_intent":"pi_1IOyTuJG1rVU4bz1qiHYyCsO","payment_method":"pm_1IOyTrJG1rVU4bz1wYJv9s5N","payment_method_details":{"card":{"brand":"visa","checks":{"address_line1_check":null,"address_postal_code_check":"pass","cvc_check":"pass"},"country":"US","exp_month":4,"exp_year":2024,"fingerprint":"VTCnHArgdpMTv44P","funding":"credit","installments":null,"last4":"4242","network":"visa","three_d_secure":null,"wallet":null},"type":"card"},"receipt_email":null,"receipt_number":null,"receipt_url":"https://pay.stripe.com/receipts/acct_1IBXaDJG1rVU4bz1/ch_1IOyTuJG1rVU4bz1t6dznTFj/rcpt_J10Uwnh11pAfJsEc8fnTHmGeBHcq3Iv","refunded":false,"refunds":{"object":"list","data":[],"has_more":false,"total_count":0,"url":"/v1/charges/ch_1IOyTuJG1rVU4bz1t6dznTFj/refunds"},"review":null,"shipping":null,"source":null,"source_transfer":null,"statement_descriptor":null,"statement_descriptor_suffix":null,"status":"succeeded","transfer_data":null,"transfer_group":null}],"has_more":false,"total_count":1,"url":"/v1/charges?payment_intent=pi_1IOyTuJG1rVU4bz1qiHYyCsO"}, + # "client_secret": "pi_1IOyTuJG1rVU4bz1qiHYyCsO_secret_UVumzun2tSD79h3haVp7wuUEh", + # "confirmation_method": "automatic", + # "created": 1614315794, + # "currency": "inr", + # "customer": "cus_IzVEJkwLRIZg1F", + # "description": null, + # "invoice": null, + # "last_payment_error": null, + # "livemode": false, + # "metadata": {}, + # "next_action": null, + # "on_behalf_of": null, + # "payment_method": "pm_1IOyTrJG1rVU4bz1wYJv9s5N", + # "payment_method_options": {"card":{"installments":null,"network":null,"request_three_d_secure":"automatic"}}, + # "payment_method_types": [ + # "card" + # ], + # "receipt_email": null, + # "review": null, + # "setup_future_usage": null, + # "shipping": null, + # "source": null, + # "statement_descriptor": null, + # "statement_descriptor_suffix": null, + # "status": "requires_capture", + # "transfer_data": null, + # "transfer_group": null + # } + # Update payment-intent details for customer's service. @payment.update({payment_intent_id: intent["id"], status: intent["status"]}) @@ -83,6 +153,49 @@ def capture_payment(payment_intent_id, amount) amount_to_capture: amount, } ) + + ## RESPONSE + ## JSON: { + # "id": "pi_1IOyTuJG1rVU4bz1qiHYyCsO", + # "object": "payment_intent", + # "amount": 30000, + # "amount_capturable": 0, + # "amount_received": 20000, + # "application": null, + # "application_fee_amount": null, + # "canceled_at": null, + # "cancellation_reason": null, + # "capture_method": "manual", + # "charges": {"object":"list","data":[{"id":"ch_1IOyTuJG1rVU4bz1t6dznTFj","object":"charge","amount":30000,"amount_captured":20000,"amount_refunded":10000,"application":null,"application_fee":null,"application_fee_amount":null,"balance_transaction":"txn_1IOyZMJG1rVU4bz10NuRnEhQ","billing_details":{"address":{"city":null,"country":null,"line1":null,"line2":null,"postal_code":"42424","state":null},"email":"swati@kiprosh.com","name":null,"phone":null},"calculated_statement_descriptor":"Stripe","captured":true,"created":1614315794,"currency":"inr","customer":"cus_IzVEJkwLRIZg1F","description":null,"destination":null,"dispute":null,"disputed":false,"failure_code":null,"failure_message":null,"fraud_details":{},"invoice":null,"livemode":false,"metadata":{},"on_behalf_of":null,"order":null,"outcome":{"network_status":"approved_by_network","reason":null,"risk_level":"normal","risk_score":43,"seller_message":"Payment complete.","type":"authorized"},"paid":true,"payment_intent":"pi_1IOyTuJG1rVU4bz1qiHYyCsO","payment_method":"pm_1IOyTrJG1rVU4bz1wYJv9s5N","payment_method_details":{"card":{"brand":"visa","checks":{"address_line1_check":null,"address_postal_code_check":"pass","cvc_check":"pass"},"country":"US","exp_month":4,"exp_year":2024,"fingerprint":"VTCnHArgdpMTv44P","funding":"credit","installments":null,"last4":"4242","network":"visa","three_d_secure":null,"wallet":null},"type":"card"},"receipt_email":null,"receipt_number":null,"receipt_url":"https://pay.stripe.com/receipts/acct_1IBXaDJG1rVU4bz1/ch_1IOyTuJG1rVU4bz1t6dznTFj/rcpt_J10Uwnh11pAfJsEc8fnTHmGeBHcq3Iv","refunded":false,"refunds":{"object":"list","data":[{"id":"re_1IOyZMJG1rVU4bz1xhvil8nK","object":"refund","amount":10000,"balance_transaction":"txn_1IOyZMJG1rVU4bz1oRiPReWp","charge":"ch_1IOyTuJG1rVU4bz1t6dznTFj","created":1614316132,"currency":"inr","metadata":{},"payment_intent":"pi_1IOyTuJG1rVU4bz1qiHYyCsO","reason":null,"receipt_number":null,"source_transfer_reversal":null,"status":"succeeded","transfer_reversal":null}],"has_more":false,"total_count":1,"url":"/v1/charges/ch_1IOyTuJG1rVU4bz1t6dznTFj/refunds"},"review":null,"shipping":null,"source":null,"source_transfer":null,"statement_descriptor":null,"statement_descriptor_suffix":null,"status":"succeeded","transfer_data":null,"transfer_group":null}],"has_more":false,"total_count":1,"url":"/v1/charges?payment_intent=pi_1IOyTuJG1rVU4bz1qiHYyCsO"}, + # "client_secret": "pi_1IOyTuJG1rVU4bz1qiHYyCsO_secret_UVumzun2tSD79h3haVp7wuUEh", + # "confirmation_method": "automatic", + # "created": 1614315794, + # "currency": "inr", + # "customer": "cus_IzVEJkwLRIZg1F", + # "description": null, + # "invoice": null, + # "last_payment_error": null, + # "livemode": false, + # "metadata": {}, + # "next_action": null, + # "on_behalf_of": null, + # "payment_method": "pm_1IOyTrJG1rVU4bz1wYJv9s5N", + # "payment_method_options": {"card":{"installments":null,"network":null,"request_three_d_secure":"automatic"}}, + # "payment_method_types": [ + # "card" + # ], + # "receipt_email": null, + # "review": null, + # "setup_future_usage": null, + # "shipping": null, + # "source": null, + # "statement_descriptor": null, + # "statement_descriptor_suffix": null, + # "status": "succeeded", + # "transfer_data": null, + # "transfer_group": null + # } + end private