1
0
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:
Sylvain 2020-12-08 17:30:33 +01:00
parent 1d64c517c9
commit 8e8fc9b682
8 changed files with 99 additions and 62 deletions

View File

@ -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 }

View File

@ -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;
}
}

View File

@ -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>
);
})

View File

@ -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);
}
}
}

View File

@ -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>}

View File

@ -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
*/

View File

@ -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"

View File

@ -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'