mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-18 07:52:23 +01:00
Merge branch 'stripe_payment_schedule' into dev
This commit is contained in:
commit
4e02122c30
@ -69,14 +69,30 @@ class API::StripeController < API::PaymentsController
|
|||||||
render json: { id: @intent.id, client_secret: @intent.client_secret }
|
render json: { id: @intent.id, client_secret: @intent.client_secret }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def payment_schedule
|
||||||
|
cart = shopping_cart
|
||||||
|
Stripe.api_key = Setting.get('stripe_secret_key')
|
||||||
|
@intent = Stripe::PaymentMethod.attach(
|
||||||
|
params[:payment_method_id],
|
||||||
|
customer: cart.customer.payment_gateway_object.gateway_object_id
|
||||||
|
)
|
||||||
|
# Set the default payment method on the customer
|
||||||
|
Stripe::Customer.update(
|
||||||
|
cart.customer.payment_gateway_object.gateway_object_id,
|
||||||
|
invoice_settings: { default_payment_method: params[:payment_method_id] }
|
||||||
|
)
|
||||||
|
@res = cart.pay_schedule(@intent.id, @intent.class.name)
|
||||||
|
render json: @res.to_json
|
||||||
|
end
|
||||||
|
|
||||||
def confirm_payment_schedule
|
def confirm_payment_schedule
|
||||||
key = Setting.get('stripe_secret_key')
|
key = Setting.get('stripe_secret_key')
|
||||||
intent = Stripe::SetupIntent.retrieve(params[:setup_intent_id], api_key: key)
|
subscription = Stripe::Subscription.retrieve(params[:subscription_id], api_key: key)
|
||||||
|
|
||||||
cart = shopping_cart
|
cart = shopping_cart
|
||||||
if intent&.status == 'succeeded'
|
if subscription&.status == 'active'
|
||||||
res = on_payment_success(intent, cart)
|
res = on_payment_success(subscription, cart)
|
||||||
render generate_payment_response(intent, res)
|
render generate_payment_response(subscription, res)
|
||||||
end
|
end
|
||||||
rescue Stripe::InvalidRequestError => e
|
rescue Stripe::InvalidRequestError => e
|
||||||
render json: e, status: :unprocessable_entity
|
render json: e, status: :unprocessable_entity
|
||||||
|
@ -14,21 +14,29 @@ export default class StripeAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async confirmIntent (paymentMethodId: string, cartItems: ShoppingCart): Promise<PaymentConfirmation|Invoice> {
|
static async confirmIntent (paymentMethodId: string, cartItems: ShoppingCart): Promise<PaymentConfirmation|Invoice> {
|
||||||
const res: AxiosResponse = await apiClient.post(`/api/stripe/confirm_payment`, {
|
const res: AxiosResponse = await apiClient.post('/api/stripe/confirm_payment', {
|
||||||
payment_intent_id: paymentMethodId,
|
payment_intent_id: paymentMethodId,
|
||||||
cart_items: cartItems
|
cart_items: cartItems
|
||||||
});
|
});
|
||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async paymentSchedule (paymentMethodId: string, cartItems: ShoppingCart): Promise<any> {
|
||||||
|
const res: AxiosResponse = await apiClient.post('/api/stripe/payment_schedule', {
|
||||||
|
payment_method_id: paymentMethodId,
|
||||||
|
cart_items: cartItems
|
||||||
|
});
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
|
||||||
static async setupIntent (userId: number): Promise<IntentConfirmation> {
|
static async setupIntent (userId: number): Promise<IntentConfirmation> {
|
||||||
const res: AxiosResponse<IntentConfirmation> = await apiClient.get(`/api/stripe/setup_intent/${userId}`);
|
const res: AxiosResponse<IntentConfirmation> = await apiClient.get(`/api/stripe/setup_intent/${userId}`);
|
||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async confirmPaymentSchedule (setupIntentId: string, cartItems: ShoppingCart): Promise<PaymentSchedule> {
|
static async confirmPaymentSchedule (subscriptionId: string, cartItems: ShoppingCart): Promise<PaymentSchedule> {
|
||||||
const res: AxiosResponse<PaymentSchedule> = await apiClient.post('/api/stripe/confirm_payment_schedule', {
|
const res: AxiosResponse<PaymentSchedule> = await apiClient.post('/api/stripe/confirm_payment_schedule', {
|
||||||
setup_intent_id: setupIntentId,
|
subscription_id: subscriptionId,
|
||||||
cart_items: cartItems
|
cart_items: cartItems
|
||||||
});
|
});
|
||||||
return res?.data;
|
return res?.data;
|
||||||
|
@ -43,27 +43,36 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
|
|||||||
const res = await StripeAPI.confirmMethod(paymentMethod.id, cart);
|
const res = await StripeAPI.confirmMethod(paymentMethod.id, cart);
|
||||||
await handleServerConfirmation(res);
|
await handleServerConfirmation(res);
|
||||||
} else {
|
} else {
|
||||||
// we start by associating the payment method with the user
|
const paymentMethodId = paymentMethod.id;
|
||||||
const intent = await StripeAPI.setupIntent(customer.id);
|
const subscription: any = await StripeAPI.paymentSchedule(paymentMethod.id, cart);
|
||||||
const { setupIntent, error } = await stripe.confirmCardSetup(intent.client_secret, {
|
if (subscription && subscription.status === 'active') {
|
||||||
payment_method: paymentMethod.id,
|
// Subscription is active, no customer actions required.
|
||||||
mandate_data: {
|
const res = await StripeAPI.confirmPaymentSchedule(subscription.id, cart);
|
||||||
customer_acceptance: {
|
|
||||||
type: 'online',
|
|
||||||
online: {
|
|
||||||
ip_address: operator.ip_address,
|
|
||||||
user_agent: navigator.userAgent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (error) {
|
|
||||||
onError(error.message);
|
|
||||||
} else {
|
|
||||||
// then we confirm the payment schedule
|
|
||||||
const res = await StripeAPI.confirmPaymentSchedule(setupIntent.id, cart);
|
|
||||||
onSuccess(res);
|
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('Your card was declined.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// catch api errors
|
// catch api errors
|
||||||
|
@ -84,7 +84,7 @@ const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPl
|
|||||||
* Check if the plan is allowing a monthly payment schedule
|
* Check if the plan is allowing a monthly payment schedule
|
||||||
*/
|
*/
|
||||||
const canBeScheduled = (): boolean => {
|
const canBeScheduled = (): boolean => {
|
||||||
return plan.monthly_payment;
|
return plan.monthly_payment && plan.interval_count !== 1;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Callback triggered when the user select the plan
|
* Callback triggered when the user select the plan
|
||||||
|
@ -630,6 +630,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
|||||||
if (Auth.isAuthenticated()) {
|
if (Auth.isAuthenticated()) {
|
||||||
if ($scope.selectedPlan !== $scope.plan) {
|
if ($scope.selectedPlan !== $scope.plan) {
|
||||||
$scope.selectedPlan = $scope.plan;
|
$scope.selectedPlan = $scope.plan;
|
||||||
|
if ($scope.selectedPlan.monthly_payment && $scope.selectedPlan.interval_count === 1) { $scope.selectedPlan.monthly_payment = false; }
|
||||||
$scope.schedule.requested_schedule = $scope.plan.monthly_payment;
|
$scope.schedule.requested_schedule = $scope.plan.monthly_payment;
|
||||||
} else {
|
} else {
|
||||||
$scope.selectedPlan = null;
|
$scope.selectedPlan = null;
|
||||||
|
@ -132,7 +132,7 @@
|
|||||||
|
|
||||||
<div class="input-group m-t-md">
|
<div class="input-group m-t-md">
|
||||||
<label for="plan[monthly_payment]" class="control-label m-r-md">{{ 'app.shared.plan.monthly_payment' | translate }} *</label>
|
<label for="plan[monthly_payment]" class="control-label m-r-md">{{ 'app.shared.plan.monthly_payment' | translate }} *</label>
|
||||||
<switch id="plan[monthly_payment]" disabled="plan.interval === 'week'" checked="plan.monthly_payment" on-change="toggleMonthlyPayment" class-name="'v-middle'" ng-if="plan && method != 'PATCH'"></switch>
|
<switch id="plan[monthly_payment]" disabled="plan.interval === 'week' || plan.interval_count === 1" checked="plan.monthly_payment" on-change="toggleMonthlyPayment" class-name="'v-middle'" ng-if="plan && method != 'PATCH'"></switch>
|
||||||
<span ng-if="method == 'PATCH'">{{ (plan.monthly_payment ? 'app.shared.buttons.yes' : 'app.shared.buttons.no') | translate }}</span>
|
<span ng-if="method == 'PATCH'">{{ (plan.monthly_payment ? 'app.shared.buttons.yes' : 'app.shared.buttons.no') | translate }}</span>
|
||||||
<input type="hidden" id="plan_monthly_input" name="plan[monthly_payment]" value="{{plan.monthly_payment}}" />
|
<input type="hidden" id="plan_monthly_input" name="plan[monthly_payment]" value="{{plan.monthly_payment}}" />
|
||||||
<span class="help-block" translate>{{ 'app.shared.plan.monthly_payment_info' }}</span>
|
<span class="help-block" translate>{{ 'app.shared.plan.monthly_payment_info' }}</span>
|
||||||
|
@ -81,6 +81,12 @@ class PaymentSchedule < PaymentDocument
|
|||||||
PaymentGatewayService.new.create_subscription(self, gateway_method_id)
|
PaymentGatewayService.new.create_subscription(self, gateway_method_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pay(gateway_method_id)
|
||||||
|
return unless payment_method == 'card'
|
||||||
|
|
||||||
|
PaymentGatewayService.new.pay_subscription(self, gateway_method_id)
|
||||||
|
end
|
||||||
|
|
||||||
def render_resource
|
def render_resource
|
||||||
{ partial: 'api/payment_schedules/payment_schedule', locals: { payment_schedule: self } }
|
{ partial: 'api/payment_schedules/payment_schedule', locals: { payment_schedule: self } }
|
||||||
end
|
end
|
||||||
|
@ -69,6 +69,20 @@ class ShoppingCart
|
|||||||
{ success: success, payment: payment, errors: errors }
|
{ success: success, payment: payment, errors: errors }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pay_schedule(payment_id, payment_type)
|
||||||
|
price = total
|
||||||
|
objects = []
|
||||||
|
items.each do |item|
|
||||||
|
rails 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
|
private
|
||||||
|
|
||||||
# Save the object associated with the provided item or raise and Rollback if something wrong append.
|
# Save the object associated with the provided item or raise and Rollback if something wrong append.
|
||||||
|
@ -23,6 +23,10 @@ class PaymentGatewayService
|
|||||||
@gateway.create_subscription(payment_schedule, gateway_object_id)
|
@gateway.create_subscription(payment_schedule, gateway_object_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pay_subscription(payment_schedule, gateway_object_id)
|
||||||
|
@gateway.pay_subscription(payment_schedule, gateway_object_id)
|
||||||
|
end
|
||||||
|
|
||||||
def create_user(user_id)
|
def create_user(user_id)
|
||||||
@gateway.create_user(user_id)
|
@gateway.create_user(user_id)
|
||||||
end
|
end
|
||||||
|
@ -95,16 +95,20 @@ class WalletService
|
|||||||
##
|
##
|
||||||
# Subtract the amount of the payment document (Invoice|PaymentSchedule) from the customer's wallet
|
# Subtract the amount of the payment document (Invoice|PaymentSchedule) from the customer's wallet
|
||||||
##
|
##
|
||||||
def self.debit_user_wallet(payment, user)
|
def self.debit_user_wallet(payment, user, transaction: true)
|
||||||
wallet_amount = WalletService.wallet_amount_debit(payment, user)
|
wallet_amount = WalletService.wallet_amount_debit(payment, user)
|
||||||
return unless wallet_amount.present? && wallet_amount != 0
|
return unless wallet_amount.present? && wallet_amount != 0
|
||||||
|
|
||||||
amount = wallet_amount / 100.0
|
amount = wallet_amount / 100.0
|
||||||
wallet_transaction = WalletService.new(user: user, wallet: user.wallet).debit(amount)
|
if transaction
|
||||||
# wallet debit success
|
wallet_transaction = WalletService.new(user: user, wallet: user.wallet).debit(amount)
|
||||||
raise DebitWalletError unless wallet_transaction
|
# wallet debit success
|
||||||
|
raise DebitWalletError unless wallet_transaction
|
||||||
|
|
||||||
payment.set_wallet_transaction(wallet_amount, wallet_transaction.id)
|
payment.set_wallet_transaction(wallet_amount, wallet_transaction.id)
|
||||||
|
else
|
||||||
|
payment.set_wallet_transaction(wallet_amount, nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -180,6 +180,7 @@ Rails.application.routes.draw do
|
|||||||
# card payments handling
|
# card payments handling
|
||||||
## Stripe gateway
|
## Stripe gateway
|
||||||
post 'stripe/confirm_payment' => 'stripe/confirm_payment'
|
post 'stripe/confirm_payment' => 'stripe/confirm_payment'
|
||||||
|
post 'stripe/payment_schedule' => 'stripe/payment_schedule'
|
||||||
get 'stripe/online_payment_status' => 'stripe/online_payment_status'
|
get 'stripe/online_payment_status' => 'stripe/online_payment_status'
|
||||||
get 'stripe/setup_intent/:user_id' => 'stripe#setup_intent'
|
get 'stripe/setup_intent/:user_id' => 'stripe#setup_intent'
|
||||||
post 'stripe/confirm_payment_schedule' => 'stripe#confirm_payment_schedule'
|
post 'stripe/confirm_payment_schedule' => 'stripe#confirm_payment_schedule'
|
||||||
|
@ -8,16 +8,28 @@ module Stripe; end
|
|||||||
## create remote objects on stripe
|
## create remote objects on stripe
|
||||||
class Stripe::Service < Payment::Service
|
class Stripe::Service < Payment::Service
|
||||||
# Create the provided PaymentSchedule on Stripe, using the Subscription API
|
# Create the provided PaymentSchedule on Stripe, using the Subscription API
|
||||||
def create_subscription(payment_schedule, setup_intent_id)
|
def create_subscription(payment_schedule, subscription_id)
|
||||||
stripe_key = Setting.get('stripe_secret_key')
|
stripe_key = Setting.get('stripe_secret_key')
|
||||||
first_item = payment_schedule.ordered_items.first
|
|
||||||
|
|
||||||
case payment_schedule.main_object.object_type
|
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
|
when Reservation.name
|
||||||
subscription = payment_schedule.payment_schedule_objects.find { |pso| pso.object_type == Subscription.name }.object
|
subscription = payment_schedule.payment_schedule_objects.find { |pso| pso.object_type == Subscription.name }.object
|
||||||
reservable_stp_id = payment_schedule.main_object.object.reservable&.payment_gateway_object&.gateway_object_id
|
reservable_stp_id = main_object.object.reservable&.payment_gateway_object&.gateway_object_id
|
||||||
when Subscription.name
|
when Subscription.name
|
||||||
subscription = payment_schedule.main_object.object
|
subscription = main_object.object
|
||||||
reservable_stp_id = nil
|
reservable_stp_id = nil
|
||||||
else
|
else
|
||||||
raise InvalidSubscriptionError
|
raise InvalidSubscriptionError
|
||||||
@ -25,28 +37,23 @@ class Stripe::Service < Payment::Service
|
|||||||
|
|
||||||
handle_wallet_transaction(payment_schedule)
|
handle_wallet_transaction(payment_schedule)
|
||||||
|
|
||||||
# setup intent (associates the customer and the payment method)
|
|
||||||
intent = Stripe::SetupIntent.retrieve(setup_intent_id, api_key: stripe_key)
|
|
||||||
# subscription (recurring price)
|
|
||||||
price = create_price(first_item.details['recurring'],
|
price = create_price(first_item.details['recurring'],
|
||||||
subscription.plan.payment_gateway_object.gateway_object_id,
|
subscription.plan.payment_gateway_object.gateway_object_id,
|
||||||
nil, monthly: true)
|
nil, monthly: true)
|
||||||
# other items (not recurring)
|
# other items (not recurring)
|
||||||
items = subscription_invoice_items(payment_schedule, subscription, first_item, reservable_stp_id)
|
items = subscription_invoice_items(payment_schedule, subscription, first_item, reservable_stp_id)
|
||||||
|
|
||||||
stp_subscription = Stripe::Subscription.create({
|
Stripe::Subscription.create({
|
||||||
customer: payment_schedule.invoicing_profile.user.payment_gateway_object.gateway_object_id,
|
customer: payment_schedule.invoicing_profile.user.payment_gateway_object.gateway_object_id,
|
||||||
cancel_at: (payment_schedule.ordered_items.last.due_date + 3.day).to_i,
|
cancel_at: (payment_schedule.payment_schedule_items.max_by(&:due_date).due_date + 3.day).to_i,
|
||||||
add_invoice_items: items,
|
add_invoice_items: items,
|
||||||
coupon: payment_schedule.coupon&.code,
|
coupon: payment_schedule.coupon&.code,
|
||||||
items: [
|
items: [
|
||||||
{ price: price[:id] }
|
{ price: price[:id] }
|
||||||
],
|
],
|
||||||
default_payment_method: intent[:payment_method]
|
default_payment_method: payment_method_id,
|
||||||
}, { api_key: stripe_key })
|
expand: %w[latest_invoice.payment_intent]
|
||||||
pgo = PaymentGatewayObject.new(item: payment_schedule)
|
}, { api_key: stripe_key })
|
||||||
pgo.gateway_object = stp_subscription
|
|
||||||
pgo.save!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_user(user_id)
|
def create_user(user_id)
|
||||||
@ -141,10 +148,10 @@ class Stripe::Service < Payment::Service
|
|||||||
private
|
private
|
||||||
|
|
||||||
def subscription_invoice_items(payment_schedule, subscription, first_item, reservable_stp_id)
|
def subscription_invoice_items(payment_schedule, subscription, first_item, reservable_stp_id)
|
||||||
second_item = payment_schedule.ordered_items[1]
|
second_item = payment_schedule.payment_schedule_items.sort_by(&:due_date)[1]
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
if first_item.amount != second_item.amount
|
if second_item && first_item.amount != second_item.amount
|
||||||
unless first_item.details['adjustment']&.zero?
|
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.
|
# adjustment: when dividing the price of the plan / months, sometimes it forces us to round the amount per month.
|
||||||
# The difference is invoiced here
|
# The difference is invoiced here
|
||||||
|
Loading…
x
Reference in New Issue
Block a user