1
0
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:
Sylvain 2021-04-13 17:16:05 +02:00
parent dbbf6b5f63
commit 4e512dda45
10 changed files with 206 additions and 9 deletions

View File

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

View File

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

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
# Raised when an an error occurred with the PayZen payment gateway
class PayzenError < StandardError
end

View File

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

View File

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

View File

@ -7,6 +7,10 @@ export interface CreatePaymentResponse {
orderId: string
}
export interface ConfirmPaymentResponse {
todo?: any
}
export interface OrderDetails {
mode?: 'TEST' | 'PRODUCTION',
orderCurrency?: string,

View File

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

View File

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

View File

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