1
0
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:
Du Peng 2021-09-09 10:30:12 +02:00
commit 4e02122c30
12 changed files with 126 additions and 56 deletions

View File

@ -69,14 +69,30 @@ class API::StripeController < API::PaymentsController
render json: { id: @intent.id, client_secret: @intent.client_secret }
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
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
if intent&.status == 'succeeded'
res = on_payment_success(intent, cart)
render generate_payment_response(intent, res)
if subscription&.status == 'active'
res = on_payment_success(subscription, cart)
render generate_payment_response(subscription, res)
end
rescue Stripe::InvalidRequestError => e
render json: e, status: :unprocessable_entity

View File

@ -14,21 +14,29 @@ export default class StripeAPI {
}
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,
cart_items: cartItems
});
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> {
const res: AxiosResponse<IntentConfirmation> = await apiClient.get(`/api/stripe/setup_intent/${userId}`);
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', {
setup_intent_id: setupIntentId,
subscription_id: subscriptionId,
cart_items: cartItems
});
return res?.data;

View File

@ -43,27 +43,36 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
const res = await StripeAPI.confirmMethod(paymentMethod.id, cart);
await handleServerConfirmation(res);
} else {
// we start by associating the payment method with the user
const intent = await StripeAPI.setupIntent(customer.id);
const { setupIntent, error } = await stripe.confirmCardSetup(intent.client_secret, {
payment_method: paymentMethod.id,
mandate_data: {
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);
const paymentMethodId = paymentMethod.id;
const subscription: any = await StripeAPI.paymentSchedule(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('Your card was declined.');
}
}
} catch (err) {
// catch api errors

View File

@ -84,7 +84,7 @@ const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPl
* Check if the plan is allowing a monthly payment schedule
*/
const canBeScheduled = (): boolean => {
return plan.monthly_payment;
return plan.monthly_payment && plan.interval_count !== 1;
};
/**
* Callback triggered when the user select the plan

View File

@ -630,6 +630,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
if (Auth.isAuthenticated()) {
if ($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;
} else {
$scope.selectedPlan = null;

View File

@ -132,7 +132,7 @@
<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>
<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>
<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>

View File

@ -81,6 +81,12 @@ class PaymentSchedule < PaymentDocument
PaymentGatewayService.new.create_subscription(self, gateway_method_id)
end
def pay(gateway_method_id)
return unless payment_method == 'card'
PaymentGatewayService.new.pay_subscription(self, gateway_method_id)
end
def render_resource
{ partial: 'api/payment_schedules/payment_schedule', locals: { payment_schedule: self } }
end

View File

@ -69,6 +69,20 @@ class ShoppingCart
{ success: success, payment: payment, errors: errors }
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
# Save the object associated with the provided item or raise and Rollback if something wrong append.

View File

@ -23,6 +23,10 @@ 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

View File

@ -95,16 +95,20 @@ class WalletService
##
# 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)
return unless wallet_amount.present? && wallet_amount != 0
amount = wallet_amount / 100.0
wallet_transaction = WalletService.new(user: user, wallet: user.wallet).debit(amount)
# wallet debit success
raise DebitWalletError unless wallet_transaction
if transaction
wallet_transaction = WalletService.new(user: user, wallet: user.wallet).debit(amount)
# 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

View File

@ -180,6 +180,7 @@ Rails.application.routes.draw do
# card payments handling
## Stripe gateway
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/setup_intent/:user_id' => 'stripe#setup_intent'
post 'stripe/confirm_payment_schedule' => 'stripe#confirm_payment_schedule'

View File

@ -8,16 +8,28 @@ 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, setup_intent_id)
def create_subscription(payment_schedule, subscription_id)
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
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
subscription = payment_schedule.main_object.object
subscription = main_object.object
reservable_stp_id = nil
else
raise InvalidSubscriptionError
@ -25,28 +37,23 @@ class Stripe::Service < Payment::Service
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'],
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)
stp_subscription = Stripe::Subscription.create({
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,
add_invoice_items: items,
coupon: payment_schedule.coupon&.code,
items: [
{ price: price[:id] }
],
default_payment_method: intent[:payment_method]
}, { api_key: stripe_key })
pgo = PaymentGatewayObject.new(item: payment_schedule)
pgo.gateway_object = stp_subscription
pgo.save!
Stripe::Subscription.create({
customer: payment_schedule.invoicing_profile.user.payment_gateway_object.gateway_object_id,
cancel_at: (payment_schedule.payment_schedule_items.max_by(&:due_date).due_date + 3.day).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 })
end
def create_user(user_id)
@ -141,10 +148,10 @@ class Stripe::Service < Payment::Service
private
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 = []
if first_item.amount != second_item.amount
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