mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-29 18:52:22 +01:00
validate reservation/subscription after payment schedule creation
This commit is contained in:
parent
1d64c517c9
commit
8e8fc9b682
@ -75,17 +75,37 @@ class API::PaymentsController < API::ApiController
|
||||
render json: { client_secret: @intent.client_secret }
|
||||
end
|
||||
|
||||
def confirm_payment_schedule
|
||||
key = Setting.get('stripe_secret_key')
|
||||
intent = Stripe::SetupIntent.retrieve(params[:setup_intent_id], api_key: key)
|
||||
|
||||
amount = card_amount
|
||||
if intent&.status == 'succeeded'
|
||||
if params[:cart_items][:reservation]
|
||||
res = on_reservation_success(intent, amount[:details])
|
||||
elsif params[:cart_items][:subscription]
|
||||
res = on_subscription_success(intent)
|
||||
end
|
||||
end
|
||||
|
||||
render generate_payment_response(intent, res)
|
||||
rescue Stripe::InvalidRequestError
|
||||
render json: { error: 'no such setup intent' }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def on_reservation_success(intent, details)
|
||||
@reservation = Reservation.new(reservation_params)
|
||||
is_reserve = Reservations::Reserve.new(current_user.id, current_user.invoicing_profile.id)
|
||||
.pay_and_save(@reservation, payment_details: details, payment_intent_id: intent.id)
|
||||
Stripe::PaymentIntent.update(
|
||||
intent.id,
|
||||
{ description: "Invoice reference: #{@reservation.invoice.reference}" },
|
||||
{ api_key: Setting.get('stripe_secret_key') }
|
||||
)
|
||||
if intent.class == Stripe::PaymentIntent
|
||||
Stripe::PaymentIntent.update(
|
||||
intent.id,
|
||||
{ description: "Invoice reference: #{@reservation.invoice.reference}" },
|
||||
{ api_key: Setting.get('stripe_secret_key') }
|
||||
)
|
||||
end
|
||||
|
||||
if is_reserve
|
||||
SubscriptionExtensionAfterReservation.new(@reservation).extend_subscription_if_eligible
|
||||
@ -104,12 +124,13 @@ class API::PaymentsController < API::ApiController
|
||||
invoice: true,
|
||||
payment_intent_id: intent.id,
|
||||
payment_method: 'stripe')
|
||||
|
||||
Stripe::PaymentIntent.update(
|
||||
intent.id,
|
||||
{ description: "Invoice reference: #{@subscription.invoices.first.reference}" },
|
||||
{ api_key: Setting.get('stripe_secret_key') }
|
||||
)
|
||||
if intent.class == Stripe::PaymentIntent
|
||||
Stripe::PaymentIntent.update(
|
||||
intent.id,
|
||||
{ description: "Invoice reference: #{@subscription.invoices.first.reference}" },
|
||||
{ api_key: Setting.get('stripe_secret_key') }
|
||||
)
|
||||
end
|
||||
|
||||
if is_subscribe
|
||||
{ template: 'api/subscriptions/show', status: :created, location: @subscription }
|
||||
|
@ -15,5 +15,13 @@ export default class PaymentAPI {
|
||||
const res: AxiosResponse = await apiClient.get(`/api/payments/setup_intent/${user_id}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async confirmPaymentSchedule (setup_intent_id: string, cart_items: CartItems): Promise<any> {
|
||||
const res: AxiosResponse = await apiClient.post(`/api/payments/confirm_payment_schedule`, {
|
||||
setup_intent_id,
|
||||
cart_items
|
||||
});
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,21 +2,29 @@
|
||||
* This component initializes the stripe's Elements tag with the API key
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import SettingAPI from '../api/setting';
|
||||
import { SettingName } from '../models/setting';
|
||||
import { Loader } from './loader';
|
||||
|
||||
const stripePublicKey = SettingAPI.get(SettingName.StripePublicKey);
|
||||
|
||||
export const StripeElements: React.FC = memo(({ children }) => {
|
||||
const publicKey = stripePublicKey.read();
|
||||
const stripePromise = loadStripe(publicKey.value);
|
||||
const [stripe, setStripe] = useState(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const key = stripePublicKey.read();
|
||||
const promise = loadStripe(key.value);
|
||||
setStripe(promise);
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Elements stripe={stripePromise}>
|
||||
{children}
|
||||
</Elements>
|
||||
<div>
|
||||
{stripe && <Elements stripe={stripe}>
|
||||
{children}
|
||||
</Elements>}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { FormEvent } from 'react';
|
||||
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
||||
import { PaymentIntent } from "@stripe/stripe-js";
|
||||
import { SetupIntent } from "@stripe/stripe-js";
|
||||
import PaymentAPI from '../api/payment';
|
||||
import { CartItems, PaymentConfirmation } from '../models/payment';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -8,11 +8,11 @@ import { User } from '../models/user';
|
||||
|
||||
interface StripeFormProps {
|
||||
onSubmit: () => void,
|
||||
onSuccess: (result: PaymentIntent|PaymentConfirmation|any) => void,
|
||||
onSuccess: (result: SetupIntent|PaymentConfirmation|any) => void,
|
||||
onError: (message: string) => void,
|
||||
customer: User,
|
||||
className?: string,
|
||||
processPayment?: boolean,
|
||||
paymentSchedule?: boolean,
|
||||
cartItems?: CartItems
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ interface StripeFormProps {
|
||||
* A form component to collect the credit card details and to create the payment method on Stripe.
|
||||
* The form validation button must be created elsewhere, using the attribute form="stripe-form".
|
||||
*/
|
||||
export const StripeForm: React.FC<StripeFormProps> = ({ onSubmit, onSuccess, onError, children, className, processPayment = true, cartItems, customer }) => {
|
||||
export const StripeForm: React.FC<StripeFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cartItems, customer }) => {
|
||||
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
@ -47,12 +47,12 @@ export const StripeForm: React.FC<StripeFormProps> = ({ onSubmit, onSuccess, onE
|
||||
if (error) {
|
||||
onError(error.message);
|
||||
} else {
|
||||
if (processPayment) {
|
||||
// process the full payment pipeline, including SCA validation
|
||||
if (!paymentSchedule) {
|
||||
// process the normal payment pipeline, including SCA validation
|
||||
const res = await PaymentAPI.confirm(paymentMethod.id, cartItems);
|
||||
await handleServerConfirmation(res);
|
||||
} else {
|
||||
// we don't want to process the payment, only associate the payment method with the user
|
||||
// we start by associating the payment method with the user
|
||||
const { client_secret } = await PaymentAPI.setupIntent(customer.id);
|
||||
const { setupIntent, error } = await stripe.confirmCardSetup(client_secret, {
|
||||
payment_method: paymentMethod.id
|
||||
@ -60,9 +60,9 @@ export const StripeForm: React.FC<StripeFormProps> = ({ onSubmit, onSuccess, onE
|
||||
if (error) {
|
||||
onError(error.message);
|
||||
} else {
|
||||
if (setupIntent.status === 'succeeded') {
|
||||
onSuccess(setupIntent);
|
||||
}
|
||||
// then we confirm the payment schedule
|
||||
const res = await PaymentAPI.confirmPaymentSchedule(setupIntent.id, cartItems);
|
||||
onSuccess(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import { IApplication } from '../models/application';
|
||||
import { StripeElements } from './stripe-elements';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabModal, ModalSize } from './fab-modal';
|
||||
import { PaymentIntent } from '@stripe/stripe-js';
|
||||
import { SetupIntent } from '@stripe/stripe-js';
|
||||
import { WalletInfo } from './wallet-info';
|
||||
import { User } from '../models/user';
|
||||
import CustomAssetAPI from '../api/custom-asset';
|
||||
@ -33,7 +33,7 @@ declare var Fablab: IFablab;
|
||||
interface StripeModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
afterSuccess: (result: PaymentIntent|PaymentConfirmation) => void,
|
||||
afterSuccess: (result: SetupIntent|PaymentConfirmation) => void,
|
||||
cartItems: CartItems,
|
||||
currentUser: User,
|
||||
schedule: PaymentSchedule,
|
||||
@ -126,7 +126,6 @@ const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuc
|
||||
<img src={stripeLogo} alt="powered by stripe" />
|
||||
<img src={mastercardLogo} alt="mastercard" />
|
||||
<img src={visaLogo} alt="visa" />
|
||||
{/* compile */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -141,7 +140,7 @@ const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuc
|
||||
/**
|
||||
* After sending the form with success, process the resulting payment method
|
||||
*/
|
||||
const handleFormSuccess = async (result: PaymentIntent|PaymentConfirmation|any): Promise<void> => {
|
||||
const handleFormSuccess = async (result: SetupIntent|PaymentConfirmation|any): Promise<void> => {
|
||||
setSubmitState(false);
|
||||
afterSuccess(result);
|
||||
}
|
||||
@ -181,7 +180,7 @@ const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuc
|
||||
className="stripe-form"
|
||||
cartItems={cartItems}
|
||||
customer={customer}
|
||||
processPayment={!isPaymentSchedule()}>
|
||||
paymentSchedule={isPaymentSchedule()}>
|
||||
{hasErrors() && <div className="stripe-errors">
|
||||
{errors}
|
||||
</div>}
|
||||
|
@ -10,8 +10,8 @@
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', 'growl', 'Auth', 'Price', 'Wallet', 'CustomAsset', 'Slot', 'AuthService', 'helpers', '_t',
|
||||
function ($rootScope, $uibModal, dialogs, growl, Auth, Price, Wallet, CustomAsset, Slot, AuthService, helpers, _t) {
|
||||
Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', 'growl', 'Auth', 'Price', 'Wallet', 'CustomAsset', 'Slot', 'AuthService', 'Payment', 'helpers', '_t',
|
||||
function ($rootScope, $uibModal, dialogs, growl, Auth, Price, Wallet, CustomAsset, Slot, AuthService, Payment, helpers, _t) {
|
||||
return ({
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
@ -331,11 +331,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
*/
|
||||
$scope.afterStripeSuccess = (result) => {
|
||||
$scope.toggleStripeModal();
|
||||
if ($scope.schedule.requested_schedule) {
|
||||
afterPaymentIntentCreation(result);
|
||||
} else {
|
||||
afterPayment(result);
|
||||
}
|
||||
afterPayment(result);
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
@ -700,18 +696,28 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the CartItems object, from the current reservation
|
||||
* @param reservation {*}
|
||||
* @param paymentMethod {string}
|
||||
* @return {CartItems}
|
||||
*/
|
||||
const mkCartItems = function (reservation, paymentMethod) {
|
||||
let request = { reservation };
|
||||
if (reservation.slots_attributes.length === 0 && reservation.plan_id) {
|
||||
request = mkSubscription($scope.selectedPlan.id, reservation.user_id, $scope.schedule.requested_schedule, paymentMethod);
|
||||
} else {
|
||||
request.reservation.payment_method = paymentMethod;
|
||||
}
|
||||
return mkRequestParams(request, $scope.coupon.applied);
|
||||
};
|
||||
|
||||
/**
|
||||
* Open a modal window that allows the user to process a credit card payment for his current shopping cart.
|
||||
*/
|
||||
const payByStripe = function (reservation) {
|
||||
$scope.toggleStripeModal(() => {
|
||||
let request = { reservation };
|
||||
if (reservation.slots_attributes.length === 0 && reservation.plan_id) {
|
||||
request = mkSubscription($scope.selectedPlan.id, reservation.user_id, $scope.schedule.requested_schedule, 'stripe');
|
||||
} else {
|
||||
request.reservation.payment_method = 'stripe';
|
||||
}
|
||||
$scope.stripe.cartItems = mkRequestParams(request, $scope.coupon.applied);
|
||||
$scope.stripe.cartItems = mkCartItems(reservation, 'stripe');
|
||||
});
|
||||
};
|
||||
/**
|
||||
@ -729,7 +735,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
return Price.compute(mkRequestParams({ reservation }, $scope.coupon.applied)).$promise;
|
||||
},
|
||||
cartItems () {
|
||||
return mkRequestParams({ reservation }, $scope.coupon.applied);
|
||||
return mkCartItems(reservation, 'stripe');
|
||||
},
|
||||
wallet () {
|
||||
return Wallet.getWalletByUser({ user_id: reservation.user_id }).$promise;
|
||||
@ -834,8 +840,12 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
}, 50);
|
||||
};
|
||||
|
||||
$scope.afterCreatePaymentMethod = function (a) {
|
||||
console.log('TODO', a);
|
||||
/**
|
||||
* After creating a payment schedule by card, from an administrator.
|
||||
* @param result {*} Reservation or Subscription
|
||||
*/
|
||||
$scope.afterCreatePaymentSchedule = function (result) {
|
||||
console.log('TODO', result);
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
@ -844,8 +854,9 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
* Kind of constructor: these actions will be realized first when the directive is loaded
|
||||
*/
|
||||
const initialize = function () {
|
||||
$scope.$watch('method.payment_method', function () {
|
||||
$scope.$watch('method.payment_method', function (newValue) {
|
||||
$scope.validButtonName = computeValidButtonName();
|
||||
$scope.cartItems = mkCartItems($scope.reservation, newValue);
|
||||
});
|
||||
};
|
||||
|
||||
@ -905,17 +916,6 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
$scope.schedule.payment_schedule = undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Actions to run after the payment intent was created on Stripe.
|
||||
* A payment intent associates a payment method with a stripe customer.
|
||||
* This is used for payment schedules.
|
||||
* @param intent {PaymentIntent}
|
||||
*/
|
||||
const afterPaymentIntentCreation = function (intent) {
|
||||
// TODO, create an API endpoint for payment_schedule validation
|
||||
// or: POST reservation || POST subscription (if admin/manager)
|
||||
};
|
||||
|
||||
/**
|
||||
* Actions to pay slots
|
||||
*/
|
||||
|
@ -51,7 +51,7 @@
|
||||
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
|
||||
<stripe-modal is-open="isOpenStripeModal"
|
||||
toggle-modal="toggleStripeModal"
|
||||
after-success="afterCreatePaymentMethod"
|
||||
after-success="afterCreatePaymentSchedule"
|
||||
cart-items="cartItems"
|
||||
current-user="currentUser"
|
||||
schedule="schedule"
|
||||
|
@ -166,6 +166,7 @@ Rails.application.routes.draw do
|
||||
post 'payments/confirm_payment' => 'payments/confirm_payment'
|
||||
get 'payments/online_payment_status' => 'payments/online_payment_status'
|
||||
get 'payments/setup_intent/:user_id' => 'payments#setup_intent'
|
||||
post 'payments/confirm_payment_schedule' => 'payments#confirm_payment_schedule'
|
||||
|
||||
# FabAnalytics
|
||||
get 'analytics/data' => 'analytics#data'
|
||||
|
Loading…
x
Reference in New Issue
Block a user