mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-17 06:52:27 +01:00
full stripe subscription code refacto
TODO: test
This commit is contained in:
parent
d494b012d4
commit
3663f8ab86
@ -47,7 +47,7 @@ class API::StripeController < API::PaymentsController
|
||||
|
||||
res = on_payment_success(intent, cart) if intent&.status == 'succeeded'
|
||||
|
||||
render generate_payment_response(intent, res)
|
||||
render generate_payment_response(intent, 'payment', res)
|
||||
end
|
||||
|
||||
def online_payment_status
|
||||
@ -71,15 +71,24 @@ class API::StripeController < API::PaymentsController
|
||||
|
||||
def create_subscription
|
||||
cart = shopping_cart
|
||||
intent = Stripe::Service.new.attach_method_as_default(
|
||||
cart.items.each do |item|
|
||||
raise InvalidSubscriptionError unless item.valid?(@items)
|
||||
raise InvalidSubscriptionError unless item.to_object.errors.empty?
|
||||
end
|
||||
|
||||
service = Stripe::Service.new
|
||||
method = service.attach_method_as_default(
|
||||
params[:payment_method_id],
|
||||
cart.customer.payment_gateway_object.gateway_object_id
|
||||
)
|
||||
@res = cart.pay_schedule(intent.id, intent.class.name)
|
||||
render json: @res.to_json
|
||||
|
||||
stp_subscription = service.subscribe(method.id, cart)
|
||||
|
||||
res = on_payment_success(stp_subscription, cart) if stp_subscription&.status == 'active'
|
||||
render generate_payment_response(stp_subscription.latest_invoice.payment_intent, 'subscription', res, stp_subscription.id)
|
||||
end
|
||||
|
||||
def confirm_payment_schedule
|
||||
def confirm_subscription
|
||||
key = Setting.get('stripe_secret_key')
|
||||
subscription = Stripe::Subscription.retrieve(params[:subscription_id], api_key: key)
|
||||
|
||||
@ -124,7 +133,7 @@ class API::StripeController < API::PaymentsController
|
||||
super(intent.id, intent.class.name, cart)
|
||||
end
|
||||
|
||||
def generate_payment_response(intent, res = nil)
|
||||
def generate_payment_response(intent, type, res = nil, stp_subscription_id = nil)
|
||||
return res unless res.nil?
|
||||
|
||||
if intent.status == 'requires_action' && intent.next_action.type == 'use_stripe_sdk'
|
||||
@ -133,7 +142,9 @@ class API::StripeController < API::PaymentsController
|
||||
status: 200,
|
||||
json: {
|
||||
requires_action: true,
|
||||
payment_intent_client_secret: intent.client_secret
|
||||
payment_intent_client_secret: intent.client_secret,
|
||||
type: type,
|
||||
subscription_id: stp_subscription_id
|
||||
}
|
||||
}
|
||||
elsif intent.status == 'succeeded'
|
||||
|
@ -1,6 +1,6 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { ShoppingCart, IntentConfirmation, PaymentConfirmation, UpdateCardResponse, StripeSubscription } from '../models/payment';
|
||||
import { ShoppingCart, IntentConfirmation, PaymentConfirmation, UpdateCardResponse } from '../models/payment';
|
||||
import { PaymentSchedule } from '../models/payment-schedule';
|
||||
import { Invoice } from '../models/invoice';
|
||||
|
||||
@ -21,7 +21,7 @@ export default class StripeAPI {
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async createSubscription (paymentMethodId: string, cartItems: ShoppingCart): Promise<StripeSubscription> {
|
||||
static async createSubscription (paymentMethodId: string, cartItems: ShoppingCart): Promise<PaymentConfirmation|PaymentSchedule> {
|
||||
const res: AxiosResponse = await apiClient.post('/api/stripe/create_subscription', {
|
||||
payment_method_id: paymentMethodId,
|
||||
cart_items: cartItems
|
||||
@ -34,8 +34,8 @@ export default class StripeAPI {
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async confirmPaymentSchedule (subscriptionId: string, cartItems: ShoppingCart): Promise<PaymentSchedule> {
|
||||
const res: AxiosResponse<PaymentSchedule> = await apiClient.post('/api/stripe/confirm_payment_schedule', {
|
||||
static async confirmSubscription (subscriptionId: string, cartItems: ShoppingCart): Promise<PaymentSchedule> {
|
||||
const res: AxiosResponse<PaymentSchedule> = await apiClient.post('/api/stripe/confirm_subscription', {
|
||||
subscription_id: subscriptionId,
|
||||
cart_items: cartItems
|
||||
});
|
||||
|
@ -2,9 +2,10 @@ import React, { FormEvent } from 'react';
|
||||
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GatewayFormProps } from '../abstract-payment-modal';
|
||||
import { PaymentConfirmation, StripeSubscription } from '../../../models/payment';
|
||||
import { PaymentConfirmation } from '../../../models/payment';
|
||||
import StripeAPI from '../../../api/stripe';
|
||||
import { Invoice } from '../../../models/invoice';
|
||||
import { PaymentSchedule } from '../../../models/payment-schedule';
|
||||
|
||||
/**
|
||||
* A form component to collect the credit card details and to create the payment method on Stripe.
|
||||
@ -43,36 +44,8 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
|
||||
const res = await StripeAPI.confirmMethod(paymentMethod.id, cart);
|
||||
await handleServerConfirmation(res);
|
||||
} else {
|
||||
const paymentMethodId = paymentMethod.id;
|
||||
const subscription: StripeSubscription = await StripeAPI.createSubscription(paymentMethod.id, cart);
|
||||
if (subscription && subscription.status === 'active') {
|
||||
// Subscription is active, no customer actions required.
|
||||
const res = await StripeAPI.confirmPaymentSchedule(subscription.id, cart);
|
||||
onSuccess(res);
|
||||
}
|
||||
const paymentIntent = subscription.latest_invoice.payment_intent;
|
||||
|
||||
if (paymentIntent.status === 'requires_action') {
|
||||
return stripe
|
||||
.confirmCardPayment(paymentIntent.client_secret, {
|
||||
payment_method: paymentMethodId
|
||||
})
|
||||
.then(async (result) => {
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
} else {
|
||||
if (result.paymentIntent.status === 'succeeded') {
|
||||
const res = await StripeAPI.confirmPaymentSchedule(subscription.id, cart);
|
||||
onSuccess(res);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
onError(error.message);
|
||||
});
|
||||
} else if (paymentIntent.status === 'requires_payment_method') {
|
||||
onError(t('app.shared.messages.payment_card_declined'));
|
||||
}
|
||||
const res = await StripeAPI.createSubscription(paymentMethod.id, cart);
|
||||
await handleServerConfirmation(res);
|
||||
}
|
||||
} catch (err) {
|
||||
// catch api errors
|
||||
@ -83,10 +56,10 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
|
||||
|
||||
/**
|
||||
* Process the server response about the Strong-customer authentication (SCA)
|
||||
* @param response can be a PaymentConfirmation, or an Invoice (if the payment succeeded)
|
||||
* @param response can be a PaymentConfirmation, or an Invoice/PaymentSchedule (if the payment succeeded)
|
||||
* @see app/controllers/api/stripe_controller.rb#confirm_payment
|
||||
*/
|
||||
const handleServerConfirmation = async (response: PaymentConfirmation|Invoice) => {
|
||||
const handleServerConfirmation = async (response: PaymentConfirmation|Invoice|PaymentSchedule) => {
|
||||
if ('error' in response) {
|
||||
if (response.error.statusText) {
|
||||
onError(response.error.statusText);
|
||||
@ -102,8 +75,14 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
|
||||
// The card action has been handled
|
||||
// The PaymentIntent can be confirmed again on the server
|
||||
try {
|
||||
if (response.type === 'payment') {
|
||||
const confirmation = await StripeAPI.confirmIntent(result.paymentIntent.id, cart);
|
||||
await handleServerConfirmation(confirmation);
|
||||
}
|
||||
if (response.type === 'subscription') {
|
||||
const confirmation = await StripeAPI.confirmSubscription(response.subscription_id, cart);
|
||||
await handleServerConfirmation(confirmation);
|
||||
}
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import { SubscriptionRequest } from './subscription';
|
||||
export interface PaymentConfirmation {
|
||||
requires_action?: boolean,
|
||||
payment_intent_client_secret?: string,
|
||||
type?: 'payment' | 'subscription',
|
||||
subscription_id?: string,
|
||||
success?: boolean,
|
||||
error?: {
|
||||
statusText: string
|
||||
@ -34,14 +36,3 @@ export interface UpdateCardResponse {
|
||||
updated: boolean,
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface StripeSubscription {
|
||||
id: string,
|
||||
status: string,
|
||||
latest_invoice: {
|
||||
payment_intent: {
|
||||
status: string,
|
||||
client_secret: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { PaymentIntent } from '@stripe/stripe-js';
|
||||
|
||||
// https://stripe.com/docs/api/tokens/object
|
||||
export interface PIIToken {
|
||||
id: string,
|
||||
object: 'token',
|
||||
@ -8,6 +11,7 @@ export interface PIIToken {
|
||||
used: boolean
|
||||
}
|
||||
|
||||
// https://stripe.com/docs/api/charges/object
|
||||
export interface Charge {
|
||||
id: string,
|
||||
object: 'charge',
|
||||
@ -50,3 +54,118 @@ export interface ListCharges {
|
||||
has_more: boolean,
|
||||
data: Array<Charge>
|
||||
}
|
||||
|
||||
// https://stripe.com/docs/api/prices/object
|
||||
export interface Price {
|
||||
id: string,
|
||||
object: 'price',
|
||||
active: boolean,
|
||||
billing_scheme: 'per_unit' | 'tiered',
|
||||
created: Date,
|
||||
currency: string,
|
||||
livemode: boolean,
|
||||
lookup_key: string,
|
||||
metadata: Record<string, unknown>,
|
||||
nickname: string,
|
||||
product: string,
|
||||
recurring: {
|
||||
aggregate_usage: 'sum' | 'last_during_period' | 'last_ever' | 'max',
|
||||
interval: 'day' | 'week' | 'month' | 'year',
|
||||
interval_count: number,
|
||||
usage_type: 'metered' | 'licensed'
|
||||
},
|
||||
tax_behavior: 'inclusive' | 'exclusive' | 'unspecified',
|
||||
tiers: [
|
||||
{
|
||||
flat_amount: number,
|
||||
flat_amount_decimal: string,
|
||||
unit_amount: number,
|
||||
unit_amount_decimal: string,
|
||||
up_to: number
|
||||
}
|
||||
],
|
||||
tiers_mode: 'graduated' | 'volume',
|
||||
transform_quantity: {
|
||||
divide_by: number,
|
||||
round: 'up' | 'down'
|
||||
},
|
||||
type: 'one_time' | 'recurring'
|
||||
unit_amount: number,
|
||||
unit_amount_decimal: string
|
||||
|
||||
}
|
||||
|
||||
// https://stripe.com/docs/api/tax_rates/object
|
||||
export interface TaxRate {
|
||||
id: string,
|
||||
object: 'tax_rate',
|
||||
active: boolean,
|
||||
country: string,
|
||||
description: string,
|
||||
display_name: string,
|
||||
inclusive: boolean,
|
||||
jurisdiction: string,
|
||||
metadata: Record<string, unknown>,
|
||||
percentage: number,
|
||||
state: string,
|
||||
created: Date,
|
||||
livemode: boolean,
|
||||
tax_type: 'vat' | 'sales_tax' | string
|
||||
}
|
||||
|
||||
// https://stripe.com/docs/api/subscription_items/object
|
||||
export interface SubscriptionItem {
|
||||
id: string,
|
||||
object: 'subscription_item',
|
||||
billing_thresholds: {
|
||||
usage_gte: number,
|
||||
},
|
||||
created: Date,
|
||||
metadata: Record<string, unknown>,
|
||||
price: Price,
|
||||
quantity: number,
|
||||
subscription: string;
|
||||
tax_rates: Array<TaxRate>
|
||||
}
|
||||
|
||||
// https://stripe.com/docs/api/invoices/object
|
||||
export interface Invoice {
|
||||
id: string,
|
||||
object: 'invoice',
|
||||
auto_advance: boolean,
|
||||
charge: string,
|
||||
collection_method: 'charge_automatically' | 'send_invoice',
|
||||
currency: string,
|
||||
customer: string,
|
||||
description: string,
|
||||
hosted_invoice_url: string,
|
||||
lines: [],
|
||||
metadata: Record<string, unknown>,
|
||||
payment_intent: PaymentIntent,
|
||||
period_end: Date,
|
||||
period_start: Date,
|
||||
status: 'draft' | 'open' | 'paid' | 'uncollectible' | 'void',
|
||||
subscription: string,
|
||||
total: number
|
||||
}
|
||||
|
||||
// https://stripe.com/docs/api/subscriptions/object
|
||||
export interface Subscription {
|
||||
id: string,
|
||||
object: 'subscription',
|
||||
cancel_at_period_end: boolean,
|
||||
current_period_end: Date,
|
||||
current_period_start: Date,
|
||||
customer: string,
|
||||
default_payment_method: string,
|
||||
items: [
|
||||
{
|
||||
object: 'list',
|
||||
data: Array<SubscriptionItem>,
|
||||
has_more: boolean,
|
||||
url: string
|
||||
}
|
||||
]
|
||||
status: 'incomplete' | 'incomplete_expired' | 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid',
|
||||
latest_invoice: Invoice
|
||||
}
|
||||
|
@ -81,10 +81,6 @@ class PaymentSchedule < PaymentDocument
|
||||
PaymentGatewayService.new.create_subscription(self, gateway_method_id)
|
||||
end
|
||||
|
||||
def pay(gateway_method_id)
|
||||
PaymentGatewayService.new.pay_subscription(self, gateway_method_id)
|
||||
end
|
||||
|
||||
def render_resource
|
||||
{ partial: 'api/payment_schedules/payment_schedule', locals: { payment_schedule: self } }
|
||||
end
|
||||
|
@ -69,21 +69,6 @@ class ShoppingCart
|
||||
{ success: success, payment: payment, errors: errors }
|
||||
end
|
||||
|
||||
def pay_schedule(payment_id, payment_type)
|
||||
price = total
|
||||
objects = []
|
||||
items.each do |item|
|
||||
raise InvalidSubscriptionError unless item.valid?(@items)
|
||||
|
||||
object = item.to_object
|
||||
objects.push(object)
|
||||
raise InvalidSubscriptionError unless object.errors.empty?
|
||||
end
|
||||
payment = create_payment_document(price, objects, payment_id, payment_type)
|
||||
WalletService.debit_user_wallet(payment, @customer, transaction: false)
|
||||
payment.pay(payment_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Save the object associated with the provided item or raise and Rollback if something wrong append.
|
||||
|
@ -23,10 +23,6 @@ class PaymentGatewayService
|
||||
@gateway.create_subscription(payment_schedule, gateway_object_id)
|
||||
end
|
||||
|
||||
def pay_subscription(payment_schedule, gateway_object_id)
|
||||
@gateway.pay_subscription(payment_schedule, gateway_object_id)
|
||||
end
|
||||
|
||||
def create_user(user_id)
|
||||
@gateway.create_user(user_id)
|
||||
end
|
||||
|
@ -94,6 +94,7 @@ class WalletService
|
||||
|
||||
##
|
||||
# Subtract the amount of the payment document (Invoice|PaymentSchedule) from the customer's wallet
|
||||
# @param transaction, if false: the wallet is not debited, the transaction is only simulated on the payment document
|
||||
##
|
||||
def self.debit_user_wallet(payment, user, transaction: true)
|
||||
wallet_amount = WalletService.wallet_amount_debit(payment, user)
|
||||
|
@ -180,10 +180,10 @@ Rails.application.routes.draw do
|
||||
# card payments handling
|
||||
## Stripe gateway
|
||||
post 'stripe/confirm_payment' => 'stripe/confirm_payment'
|
||||
post 'stripe/create_subscription' => 'stripe/create_subscription'
|
||||
get 'stripe/online_payment_status' => 'stripe/online_payment_status'
|
||||
get 'stripe/setup_intent/:user_id' => 'stripe#setup_intent'
|
||||
post 'stripe/confirm_payment_schedule' => 'stripe#confirm_payment_schedule'
|
||||
post 'stripe/create_subscription' => 'stripe/create_subscription'
|
||||
post 'stripe/confirm_subscription' => 'stripe#confirm_subscription'
|
||||
post 'stripe/update_card' => 'stripe#update_card'
|
||||
|
||||
## PayZen gateway
|
||||
|
@ -8,33 +8,16 @@ module Stripe; end
|
||||
## create remote objects on stripe
|
||||
class Stripe::Service < Payment::Service
|
||||
# Create the provided PaymentSchedule on Stripe, using the Subscription API
|
||||
def create_subscription(payment_schedule, subscription_id)
|
||||
stripe_key = Setting.get('stripe_secret_key')
|
||||
def subscribe(payment_method_id, shopping_cart)
|
||||
price_details = shopping_cart.total
|
||||
|
||||
handle_wallet_transaction(payment_schedule)
|
||||
|
||||
stp_subscription = Stripe::Subscription.retrieve(subscription_id, api_key: stripe_key)
|
||||
pgo = PaymentGatewayObject.new(item: payment_schedule)
|
||||
pgo.gateway_object = stp_subscription
|
||||
pgo.save!
|
||||
end
|
||||
|
||||
def pay_subscription(payment_schedule, payment_method_id)
|
||||
stripe_key = Setting.get('stripe_secret_key')
|
||||
first_item = payment_schedule.payment_schedule_items.min_by(&:due_date)
|
||||
|
||||
main_object = payment_schedule.payment_schedule_objects.find(&:main)
|
||||
case main_object.object_type
|
||||
when Reservation.name
|
||||
subscription = payment_schedule.payment_schedule_objects.find { |pso| pso.object_type == Subscription.name }.object
|
||||
reservable_stp_id = main_object.object.reservable&.payment_gateway_object&.gateway_object_id
|
||||
when Subscription.name
|
||||
subscription = main_object.object
|
||||
reservable_stp_id = nil
|
||||
else
|
||||
raise InvalidSubscriptionError
|
||||
end
|
||||
payment_schedule = price_details[:schedule][:payment_schedule]
|
||||
first_item = price_details[:schedule][:items].min_by(&:due_date)
|
||||
subscription = shopping_cart.items.find { |item| item.class == CartItem::Subscription }
|
||||
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, payment_schedule.invoicing_profile.user, transaction: false)
|
||||
handle_wallet_transaction(payment_schedule)
|
||||
|
||||
price = create_price(first_item.details['recurring'],
|
||||
@ -56,6 +39,17 @@ class Stripe::Service < Payment::Service
|
||||
}, { api_key: stripe_key })
|
||||
end
|
||||
|
||||
def create_subscription(payment_schedule, subscription_id)
|
||||
stripe_key = Setting.get('stripe_secret_key')
|
||||
|
||||
handle_wallet_transaction(payment_schedule)
|
||||
|
||||
stp_subscription = Stripe::Subscription.retrieve(subscription_id, api_key: stripe_key)
|
||||
pgo = PaymentGatewayObject.new(item: payment_schedule)
|
||||
pgo.gateway_object = stp_subscription
|
||||
pgo.save!
|
||||
end
|
||||
|
||||
def create_user(user_id)
|
||||
StripeWorker.perform_async(:create_stripe_customer, user_id)
|
||||
end
|
||||
@ -146,19 +140,21 @@ class Stripe::Service < Payment::Service
|
||||
end
|
||||
|
||||
def attach_method_as_default(payment_method_id, customer_id)
|
||||
Stripe.api_key = Setting.get('stripe_secret_key')
|
||||
stripe_key = Setting.get('stripe_secret_key')
|
||||
|
||||
# attach the payment method to the given customer
|
||||
intent = Stripe::PaymentMethod.attach(
|
||||
method = Stripe::PaymentMethod.attach(
|
||||
payment_method_id,
|
||||
customer: customer_id
|
||||
{ customer: customer_id },
|
||||
{ api_key: stripe_key }
|
||||
)
|
||||
# then set it as the default payment method for this customer
|
||||
Stripe::Customer.update(
|
||||
cart.customer.payment_gateway_object.gateway_object_id,
|
||||
invoice_settings: { default_payment_method: params[:payment_method_id] }
|
||||
customer_id,
|
||||
{ invoice_settings: { default_payment_method: payment_method_id } },
|
||||
{ api_key: stripe_key }
|
||||
)
|
||||
intent
|
||||
method
|
||||
end
|
||||
|
||||
private
|
||||
|
Loading…
x
Reference in New Issue
Block a user