mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-12-01 12:24:28 +01:00
253 lines
11 KiB
Ruby
253 lines
11 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'payment/service'
|
|
|
|
# Stripe payement gateway
|
|
module Stripe; end
|
|
|
|
## create remote objects on stripe
|
|
class Stripe::Service < Payment::Service
|
|
|
|
# Build the subscription base on the given shopping cart and create it on the remote stripe API
|
|
def subscribe(payment_method_id, shopping_cart)
|
|
price_details = shopping_cart.total
|
|
|
|
payment_schedule = price_details[:schedule][:payment_schedule]
|
|
payment_schedule.payment_schedule_items = price_details[:schedule][:items]
|
|
first_item = price_details[:schedule][:items].min_by(&:due_date)
|
|
subscription = shopping_cart.items.find { |item| item.class == CartItem::Subscription }.to_object
|
|
reservable_stp_id = shopping_cart.items.find { |item| item.is_a?(CartItem::Reservation) }&.to_object
|
|
&.reservable&.payment_gateway_object&.gateway_object_id
|
|
|
|
WalletService.debit_user_wallet(payment_schedule, shopping_cart.customer, transaction: false)
|
|
handle_wallet_transaction(payment_schedule)
|
|
|
|
price = create_price(first_item.details['recurring'],
|
|
subscription.plan.payment_gateway_object.gateway_object_id,
|
|
nil, monthly: true)
|
|
# other items (not recurring)
|
|
items = subscription_invoice_items(payment_schedule, subscription, first_item, reservable_stp_id)
|
|
|
|
create_remote_subscription(shopping_cart, payment_schedule, items, price, payment_method_id)
|
|
end
|
|
|
|
def create_subscription(payment_schedule, stp_object_id, stp_object_type)
|
|
stripe_key = Setting.get('stripe_secret_key')
|
|
|
|
stp_subscription = Stripe::Item.new(stp_object_type, stp_object_id).retrieve
|
|
|
|
payment_method_id = Stripe::Customer.retrieve(stp_subscription.customer, api_key: stripe_key).invoice_settings.default_payment_method
|
|
payment_method = Stripe::PaymentMethod.retrieve(payment_method_id, api_key: stripe_key)
|
|
pgo = PaymentGatewayObject.new(item: payment_schedule)
|
|
pgo.gateway_object = payment_method
|
|
pgo.save!
|
|
end
|
|
|
|
def cancel_subscription(payment_schedule)
|
|
stripe_key = Setting.get('stripe_secret_key')
|
|
|
|
stp_subscription = payment_schedule.gateway_subscription.retrieve
|
|
|
|
res = Stripe::Subscription.delete(stp_subscription.id, {}, api_key: stripe_key)
|
|
res.status == 'canceled'
|
|
end
|
|
|
|
def create_user(user_id)
|
|
StripeWorker.perform_async(:create_stripe_customer, user_id)
|
|
end
|
|
|
|
def create_coupon(coupon_id)
|
|
coupon = Coupon.find(coupon_id)
|
|
stp_coupon = { id: coupon.code }
|
|
if coupon.type == 'percent_off'
|
|
stp_coupon[:percent_off] = coupon.percent_off
|
|
elsif coupon.type == stripe_amount('amount_off')
|
|
stp_coupon[:amount_off] = coupon.amount_off
|
|
stp_coupon[:currency] = Setting.get('stripe_currency')
|
|
end
|
|
|
|
stp_coupon[:duration] = coupon.validity_per_user == 'always' ? 'forever' : 'once'
|
|
stp_coupon[:redeem_by] = coupon.valid_until.to_i unless coupon.valid_until.nil?
|
|
stp_coupon[:max_redemptions] = coupon.max_usages unless coupon.max_usages.nil?
|
|
|
|
Stripe::Coupon.create(stp_coupon, api_key: Setting.get('stripe_secret_key'))
|
|
end
|
|
|
|
def delete_coupon(coupon_id)
|
|
coupon = Coupon.find(coupon_id)
|
|
StripeWorker.perform_async(:delete_stripe_coupon, coupon.code)
|
|
end
|
|
|
|
def create_or_update_product(klass, id)
|
|
StripeWorker.perform_async(:create_or_update_stp_product, klass, id)
|
|
rescue Stripe::InvalidRequestError => e
|
|
raise ::PaymentGatewayError, e
|
|
end
|
|
|
|
def stripe_amount(amount)
|
|
currency = Setting.get('stripe_currency')
|
|
return amount / 100 if zero_decimal_currencies.any? { |s| s.casecmp(currency).zero? }
|
|
|
|
amount
|
|
end
|
|
|
|
def process_payment_schedule_item(payment_schedule_item)
|
|
stripe_key = Setting.get('stripe_secret_key')
|
|
stp_subscription = payment_schedule_item.payment_schedule.gateway_subscription.retrieve
|
|
if stp_subscription.status == 'canceled'
|
|
# the subscription was canceled by the gateway => notify & update the status
|
|
notify_payment_schedule_gateway_canceled(payment_schedule_item)
|
|
payment_schedule_item.update_attributes(state: 'gateway_canceled')
|
|
return
|
|
end
|
|
stp_invoice = Stripe::Invoice.retrieve(stp_subscription.latest_invoice, api_key: stripe_key)
|
|
if stp_invoice.status == 'paid'
|
|
##### Successfully paid
|
|
PaymentScheduleService.new.generate_invoice(payment_schedule_item,
|
|
payment_method: 'card',
|
|
payment_id: stp_invoice.payment_intent,
|
|
payment_type: 'Stripe::PaymentIntent')
|
|
payment_schedule_item.update_attributes(state: 'paid', payment_method: 'card')
|
|
pgo = PaymentGatewayObject.find_or_initialize_by(item: payment_schedule_item)
|
|
pgo.gateway_object = stp_invoice
|
|
pgo.save!
|
|
elsif stp_subscription.status == 'past_due' || stp_invoice.status == 'open'
|
|
##### Payment error
|
|
notify_payment_schedule_item_failed(payment_schedule_item)
|
|
stp_payment_intent = Stripe::PaymentIntent.retrieve(stp_invoice.payment_intent, api_key: stripe_key)
|
|
payment_schedule_item.update_attributes(state: stp_payment_intent.status,
|
|
client_secret: stp_payment_intent.client_secret)
|
|
pgo = PaymentGatewayObject.find_or_initialize_by(item: payment_schedule_item)
|
|
pgo.gateway_object = stp_invoice
|
|
pgo.save!
|
|
elsif stp_invoice.status == 'draft'
|
|
return # Could be that the stripe invoice does not yet reflect the payment made by the member, because we called that service just after payment is made. We call return here and PaymentScheduleItemWorker will anyway call that method every hour
|
|
else
|
|
notify_payment_schedule_item_error(payment_schedule_item)
|
|
payment_schedule_item.update_attributes(state: 'error')
|
|
end
|
|
end
|
|
|
|
def pay_payment_schedule_item(payment_schedule_item)
|
|
stripe_key = Setting.get('stripe_secret_key')
|
|
stp_invoice = Stripe::Invoice.pay(@payment_schedule_item.payment_gateway_object.gateway_object_id, {}, { api_key: stripe_key })
|
|
PaymentScheduleItemWorker.new.perform(@payment_schedule_item.id)
|
|
|
|
{ status: stp_invoice.status }
|
|
rescue Stripe::StripeError => e
|
|
stripe_key = Setting.get('stripe_secret_key')
|
|
stp_invoice = Stripe::Invoice.retrieve(@payment_schedule_item.payment_gateway_object.gateway_object_id, api_key: stripe_key)
|
|
PaymentScheduleItemWorker.new.perform(@payment_schedule_item.id)
|
|
|
|
{ status: stp_invoice.status, error: e }
|
|
end
|
|
|
|
def attach_method_as_default(payment_method_id, customer_id)
|
|
stripe_key = Setting.get('stripe_secret_key')
|
|
|
|
# attach the payment method to the given customer
|
|
method = Stripe::PaymentMethod.attach(
|
|
payment_method_id,
|
|
{ customer: customer_id },
|
|
{ api_key: stripe_key }
|
|
)
|
|
# then set it as the default payment method for this customer
|
|
Stripe::Customer.update(
|
|
customer_id,
|
|
{ invoice_settings: { default_payment_method: payment_method_id } },
|
|
{ api_key: stripe_key }
|
|
)
|
|
method
|
|
end
|
|
|
|
private
|
|
|
|
|
|
# Create the provided PaymentSchedule on Stripe, using the Subscription API
|
|
def create_remote_subscription(shopping_cart, payment_schedule, items, price, payment_method_id)
|
|
stripe_key = Setting.get('stripe_secret_key')
|
|
if payment_schedule.start_at.nil?
|
|
Stripe::Subscription.create({
|
|
customer: shopping_cart.customer.payment_gateway_object.gateway_object_id,
|
|
cancel_at: (payment_schedule.payment_schedule_items.max_by(&:due_date).due_date + 1.month).to_i,
|
|
add_invoice_items: items,
|
|
coupon: payment_schedule.coupon&.code,
|
|
items: [
|
|
{ price: price[:id] }
|
|
],
|
|
default_payment_method: payment_method_id,
|
|
expand: %w[latest_invoice.payment_intent]
|
|
}, { api_key: stripe_key })
|
|
else
|
|
Stripe::SubscriptionSchedule.create({
|
|
customer: shopping_cart.customer.payment_gateway_object.gateway_object_id,
|
|
start_date: payment_schedule.start_at.to_i,
|
|
end_behavior: 'cancel',
|
|
phases: [
|
|
{
|
|
items: [
|
|
{ price: price[:id] }
|
|
],
|
|
add_invoice_items: items,
|
|
coupon: payment_schedule.coupon&.code,
|
|
default_payment_method: payment_method_id,
|
|
end_date: (
|
|
payment_schedule.payment_schedule_items.max_by(&:due_date).due_date + 1.month
|
|
).to_i
|
|
}
|
|
]
|
|
}, { api_key: stripe_key })
|
|
end
|
|
end
|
|
|
|
def subscription_invoice_items(payment_schedule, subscription, first_item, reservable_stp_id)
|
|
second_item = payment_schedule.payment_schedule_items.sort_by(&:due_date)[1]
|
|
|
|
items = []
|
|
if second_item && first_item.amount != second_item.amount
|
|
unless first_item.details['adjustment']&.zero?
|
|
# adjustment: when dividing the price of the plan / months, sometimes it forces us to round the amount per month.
|
|
# The difference is invoiced here
|
|
p1 = create_price(first_item.details['adjustment'],
|
|
subscription.plan.payment_gateway_object.gateway_object_id,
|
|
"Price adjustment for payment schedule #{payment_schedule.id}")
|
|
items.push(price: p1[:id])
|
|
end
|
|
unless first_item.details['other_items']&.zero?
|
|
# when taking a subscription at the same time of a reservation (space, machine or training), the amount of the
|
|
# reservation is invoiced here.
|
|
p2 = create_price(first_item.details['other_items'],
|
|
reservable_stp_id,
|
|
"Reservations for payment schedule #{payment_schedule.id}")
|
|
items.push(price: p2[:id])
|
|
end
|
|
end
|
|
|
|
items
|
|
end
|
|
|
|
def create_price(amount, stp_product_id, name, monthly: false)
|
|
params = {
|
|
unit_amount: stripe_amount(amount),
|
|
currency: Setting.get('stripe_currency'),
|
|
product: stp_product_id,
|
|
nickname: name
|
|
}
|
|
params[:recurring] = { interval: 'month', interval_count: 1 } if monthly
|
|
|
|
Stripe::Price.create(params, api_key: Setting.get('stripe_secret_key'))
|
|
end
|
|
|
|
def handle_wallet_transaction(payment_schedule)
|
|
return unless payment_schedule.wallet_amount
|
|
|
|
customer_id = payment_schedule.invoicing_profile.user.payment_gateway_object.gateway_object_id
|
|
Stripe::Customer.update(customer_id, { balance: -payment_schedule.wallet_amount }, { api_key: Setting.get('stripe_secret_key') })
|
|
end
|
|
|
|
# @see https://stripe.com/docs/currencies#zero-decimal
|
|
def zero_decimal_currencies
|
|
%w[BIF CLP DJF GNF JPY KMF KRW MGA PYG RWF UGX VND VUV XAF XOF XPF]
|
|
end
|
|
end
|