mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-12-01 12:24:28 +01:00
validate the payment server side
This commit is contained in:
parent
dbbf6b5f63
commit
4e512dda45
@ -4,6 +4,12 @@
|
||||
class API::PaymentsController < API::ApiController
|
||||
before_action :authenticate_user!
|
||||
|
||||
|
||||
# This method must be overridden by the the gateways controllers that inherits API::PaymentsControllers
|
||||
def confirm_payment
|
||||
raise NoMethodError
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def get_wallet_debit(user, total_amount)
|
||||
|
@ -3,6 +3,7 @@
|
||||
# API Controller for accessing PayZen API endpoints through the front-end app
|
||||
class API::PayzenController < API::PaymentsController
|
||||
require 'pay_zen/charge'
|
||||
require 'pay_zen/order'
|
||||
require 'pay_zen/helper'
|
||||
|
||||
def sdk_test
|
||||
@ -23,7 +24,89 @@ class API::PayzenController < API::PaymentsController
|
||||
client = PayZen::Charge.new
|
||||
@result = client.create_payment(amount: amount[:amount],
|
||||
order_id: @id,
|
||||
customer: { reference: params[:customer][:id], email: params[:customer][:email] })
|
||||
customer: PayZen::Helper.generate_customer(params[:customer_id]))
|
||||
@result
|
||||
end
|
||||
|
||||
def confirm_payment
|
||||
render(json: { error: 'Bad gateway or online payment is disabled' }, status: :bad_gateway) and return unless PayZen::Helper.enabled?
|
||||
|
||||
client = PayZen::Order.new
|
||||
order = client.get(params[:order_id])
|
||||
|
||||
amount = card_amount
|
||||
|
||||
if order[:transactions].first.status == 'PAID'
|
||||
if params[:cart_items][:reservation]
|
||||
res = on_reservation_success(intent, amount[:details])
|
||||
elsif params[:cart_items][:subscription]
|
||||
res = on_subscription_success(intent, amount[:details])
|
||||
end
|
||||
end
|
||||
|
||||
render generate_payment_response(intent, res)
|
||||
rescue StandardError => e
|
||||
render json: e, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def on_reservation_success(intent, details)
|
||||
@reservation = Reservation.new(reservation_params)
|
||||
payment_method = params[:cart_items][:reservation][:payment_method] || 'stripe'
|
||||
user_id = if current_user.admin? || current_user.manager?
|
||||
params[:cart_items][:reservation][:user_id]
|
||||
else
|
||||
current_user.id
|
||||
end
|
||||
is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id)
|
||||
.pay_and_save(@reservation,
|
||||
payment_details: details,
|
||||
intent_id: intent.id,
|
||||
schedule: params[:cart_items][:reservation][:payment_schedule],
|
||||
payment_method: payment_method)
|
||||
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
|
||||
|
||||
{ template: 'api/reservations/show', status: :created, location: @reservation }
|
||||
else
|
||||
{ json: @reservation.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
|
||||
def on_subscription_success(intent, details)
|
||||
@subscription = Subscription.new(subscription_params)
|
||||
user_id = if current_user.admin? || current_user.manager?
|
||||
params[:cart_items][:subscription][:user_id]
|
||||
else
|
||||
current_user.id
|
||||
end
|
||||
is_subscribe = Subscriptions::Subscribe.new(current_user.invoicing_profile.id, user_id)
|
||||
.pay_and_save(@subscription,
|
||||
payment_details: details,
|
||||
intent_id: intent.id,
|
||||
schedule: params[:cart_items][:subscription][:payment_schedule],
|
||||
payment_method: 'stripe')
|
||||
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 }
|
||||
else
|
||||
{ json: @subscription.errors, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
6
app/exceptions/payzen_error.rb
Normal file
6
app/exceptions/payzen_error.rb
Normal file
@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Raised when an an error occurred with the PayZen payment gateway
|
||||
class PayzenError < StandardError
|
||||
end
|
||||
|
@ -2,7 +2,7 @@ import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { CartItems } from '../models/payment';
|
||||
import { User } from '../models/user';
|
||||
import { CreatePaymentResponse, SdkTestResponse } from '../models/payzen';
|
||||
import { ConfirmPaymentResponse, CreatePaymentResponse, SdkTestResponse } from '../models/payzen';
|
||||
|
||||
export default class PayzenAPI {
|
||||
|
||||
@ -11,8 +11,13 @@ export default class PayzenAPI {
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async chargeCreatePayment(cart_items: CartItems, customer: User): Promise<CreatePaymentResponse> {
|
||||
const res: AxiosResponse = await apiClient.post('/api/payzen/create_payment', { cart_items, customer: { id: customer.id, email: customer.email } });
|
||||
static async chargeCreatePayment(cartItems: CartItems, customer: User): Promise<CreatePaymentResponse> {
|
||||
const res: AxiosResponse = await apiClient.post('/api/payzen/create_payment', { cart_items: cartItems, customer_id: customer.id });
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async confirm(orderId: string, cartItems: CartItems): Promise<ConfirmPaymentResponse> {
|
||||
const res: AxiosResponse = await apiClient.post('/api/payzen/confirm_payment', { cart_items: cartItems, order_id: orderId });
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
@ -30,10 +30,12 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
|
||||
const { t } = useTranslation('shared');
|
||||
const PayZenKR = useRef<KryptonClient>(null);
|
||||
const [loadingClass, setLoadingClass] = useState<'hidden' | 'loader' | 'loader-overlay'>('loader');
|
||||
const [hmacKey, setHmacKey] = useState<string>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const api = new SettingAPI();
|
||||
api.query([SettingName.PayZenEndpoint, SettingName.PayZenPublicKey]).then(settings => {
|
||||
api.query([SettingName.PayZenEndpoint, SettingName.PayZenPublicKey, SettingName.PayZenHmacKey]).then(settings => {
|
||||
setHmacKey(settings.get(SettingName.PayZenHmacKey));
|
||||
PayzenAPI.chargeCreatePayment(cartItems, customer).then(formToken => {
|
||||
KRGlue.loadLibrary(settings.get(SettingName.PayZenEndpoint), settings.get(SettingName.PayZenPublicKey)) /* Load the remote library */
|
||||
.then(({ KR }) =>
|
||||
@ -54,11 +56,17 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
|
||||
* Callback triggered on PayZen successful payments
|
||||
*/
|
||||
const onPaid = (event: ProcessPaymentAnswer): boolean => {
|
||||
// TODO check hash
|
||||
|
||||
const transaction = event.clientAnswer.transactions[0];
|
||||
|
||||
if (event.clientAnswer.orderStatus === 'PAID') {
|
||||
PayZenKR.current.removeForms();
|
||||
onSuccess(event.clientAnswer);
|
||||
PayzenAPI.confirm(event.clientAnswer.orderDetails.orderId, cartItems).then(() => {
|
||||
PayZenKR.current.removeForms().then(() => {
|
||||
onSuccess(event.clientAnswer);
|
||||
});
|
||||
})
|
||||
} else {
|
||||
const transaction = event.clientAnswer.transactions[0];
|
||||
const error = `${transaction?.errorMessage}. ${transaction?.detailedErrorMessage || ''}`;
|
||||
onError(error || event.clientAnswer.orderStatus);
|
||||
}
|
||||
@ -107,6 +115,40 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const checkHash = (answer: ProcessPaymentAnswer, key: string = hmacKey): boolean => {
|
||||
/*
|
||||
TODO: convert the following to JS
|
||||
|
||||
## Check Kr-Answser object signature
|
||||
def check_hash(answer, key = nil)
|
||||
supported_hash_algorithm = ['sha256_hmac']
|
||||
|
||||
# check if the hash algorithm is supported
|
||||
unless supported_hash_algorithm.include? answer[:hashAlgorithm]
|
||||
raise PayzenError("hash algorithm not supported: #{answer[:hashAlgorithm]}. Update your SDK")
|
||||
end
|
||||
|
||||
# if key is not defined, we use kr-hash-key parameter to choose it
|
||||
if key.nil?
|
||||
if answer[:hashKey] == 'sha256_hmac'
|
||||
key = Setting.get('payzen_hmac')
|
||||
elsif answer[:hashKey] == 'password'
|
||||
key = Setting.get('payzen_password')
|
||||
else
|
||||
raise PayzenError('invalid hash-key parameter')
|
||||
end
|
||||
end
|
||||
|
||||
hash = OpenSSL::HMAC.hexdigest('SHA256', key, answer[:rawClientAnswer])
|
||||
|
||||
# return true if calculated hash and sent hash are the same
|
||||
hash == answer[:hash]
|
||||
end
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
|
||||
const Loader: FunctionComponent = () => {
|
||||
return (
|
||||
<div className={`fa-3x ${loadingClass}`}>
|
||||
|
@ -7,6 +7,10 @@ export interface CreatePaymentResponse {
|
||||
orderId: string
|
||||
}
|
||||
|
||||
export interface ConfirmPaymentResponse {
|
||||
todo?: any
|
||||
}
|
||||
|
||||
export interface OrderDetails {
|
||||
mode?: 'TEST' | 'PRODUCTION',
|
||||
orderCurrency?: string,
|
||||
|
@ -330,6 +330,10 @@ class User < ApplicationRecord
|
||||
)
|
||||
end
|
||||
|
||||
def organization?
|
||||
!invoicing_profile.organization.nil?
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# remove projects drafts that are not linked to another user
|
||||
|
@ -182,6 +182,7 @@ Rails.application.routes.draw do
|
||||
## PayZen gateway
|
||||
post 'payzen/sdk_test' => 'payzen#sdk_test'
|
||||
post 'payzen/create_payment' => 'payzen#create_payment'
|
||||
post 'payzen/confirm_payment' => 'payzen#confirm_payment'
|
||||
|
||||
# FabAnalytics
|
||||
get 'analytics/data' => 'analytics#data'
|
||||
|
@ -6,6 +6,7 @@ module PayZen; end
|
||||
## Provides various methods around the PayZen payment gateway
|
||||
class PayZen::Helper
|
||||
class << self
|
||||
## Is the PayZen gateway enabled?
|
||||
def enabled?
|
||||
return false unless Setting.get('online_payment_module')
|
||||
return false unless Setting.get('payment_gateway') == 'payzen'
|
||||
@ -17,11 +18,37 @@ class PayZen::Helper
|
||||
res
|
||||
end
|
||||
|
||||
## generate an unique string reference for the content of a cart
|
||||
def generate_ref(cart_items, customer)
|
||||
require 'sha3'
|
||||
|
||||
content = { cart_items: cart_items, customer: customer }.to_json + DateTime.current.to_s
|
||||
SHA3::Digest.hexdigest(:sha256, content)[0...9]
|
||||
SHA3::Digest.hexdigest(:sha256, content)[0...12]
|
||||
end
|
||||
|
||||
## Generate a hash map compatible with PayZen 'V4/Customer/Customer'
|
||||
def generate_customer(customer_id)
|
||||
customer = User.find(customer_id)
|
||||
address = if customer.organization?
|
||||
customer.invoicing_profile.organization.address&.address
|
||||
else
|
||||
customer.invoicing_profile.address&.address
|
||||
end
|
||||
|
||||
{
|
||||
reference: customer.id,
|
||||
email: customer.invoicing_profile.email,
|
||||
billingDetails: {
|
||||
firstName: customer.invoicing_profile.first_name,
|
||||
lastName: customer.invoicing_profile.last_name,
|
||||
legalName: customer.organization? ? customer.invoicing_profile.organization.name : nil,
|
||||
address: address
|
||||
},
|
||||
shippingDetails: {
|
||||
category: customer.organization? ? 'COMPANY' : 'PRIVATE',
|
||||
shippingMethod: 'ETICKET'
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
19
lib/pay_zen/order.rb
Normal file
19
lib/pay_zen/order.rb
Normal file
@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'pay_zen/client'
|
||||
|
||||
# Order/* endpoints of the PayZen REST API
|
||||
class PayZen::Order < PayZen::Client
|
||||
def initialize(base_url: nil, username: nil, password: nil)
|
||||
super(base_url: base_url, username: username, password: password)
|
||||
end
|
||||
|
||||
##
|
||||
# @see https://payzen.io/en-EN/rest/V4.0/api/playground/Order/Get/
|
||||
##
|
||||
def get(order_id, operation_type: nil)
|
||||
post('/Transaction/Get/', orderId: order_id, operationType: operation_type)
|
||||
end
|
||||
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user