Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,6 @@ STOCKIT_API_TOKEN=

SLACK_API_TOKEN=
SLACK_PIN_CHANNEL=

STRIPE_PUBLISHABLE_KEY=
STRIPE_SECRET_KEY=
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -598,6 +599,7 @@ DEPENDENCIES
spring
spring-commands-rspec
state_machine
stripe (~> 5.29.0)
timecop
traco
twilio-ruby (~> 5.11.0)
Expand Down
1 change: 1 addition & 0 deletions app/controllers/api/v1/ability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions app/controllers/api/v1/stripe_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module Api
module V1
class StripeController < Api::V1::ApiController

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"
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

# 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(
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

end
end
end
3 changes: 3 additions & 0 deletions app/models/stripe_payment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class StripePayment < ApplicationRecord
belongs_to :source, polymorphic: true
end
250 changes: 250 additions & 0 deletions app/services/stripe_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
## Steps for saving cards 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

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

# STEP 2
def create_setup_intent
Stripe::SetupIntent.create({
customer: @customer_id
})

## RESPONCE
# #<Stripe::SetupIntent:0x3fd1c38065cc id=seti_1IOy9dJG1rVU4bz10kMAVLry> 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
{
'publicKey': stripe_publishable_key
}
end

# 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.to_s.downcase == 'true'
authorize_amount_on_saved_card(amount_for_source, @customer_id, details[:payment_method])
end
end

# STEP 5
def authorize_amount_on_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', # Ref: https://stripe.com/docs/payments/capture-later
})

## RESPONSE:
# #<Stripe::PaymentIntent:0x3fd680173608 id=pi_1IOyTuJG1rVU4bz1qiHYyCsO> 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":"[email protected]","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"]})

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
end

# 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,
}
)

## RESPONSE
## <Stripe::PaymentIntent:0x3fd1c63fc1a4 id=pi_1IOyTuJG1rVU4bz1qiHYyCsO> 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":"[email protected]","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

def stripe_publishable_key
Rails.application.secrets.stripe[:publishable_key]
end

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
8 changes: 7 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions config/secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20210217160745_add_stripe_customer_id_to_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddStripeCustomerIdToUsers < ActiveRecord::Migration[5.2]
def change
add_column :users, :stripe_customer_id, :string, default: nil
end
end
Loading