mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-03-01 23:29:23 +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
|
class API::PaymentsController < API::ApiController
|
||||||
before_action :authenticate_user!
|
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
|
protected
|
||||||
|
|
||||||
def get_wallet_debit(user, total_amount)
|
def get_wallet_debit(user, total_amount)
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
# API Controller for accessing PayZen API endpoints through the front-end app
|
# API Controller for accessing PayZen API endpoints through the front-end app
|
||||||
class API::PayzenController < API::PaymentsController
|
class API::PayzenController < API::PaymentsController
|
||||||
require 'pay_zen/charge'
|
require 'pay_zen/charge'
|
||||||
|
require 'pay_zen/order'
|
||||||
require 'pay_zen/helper'
|
require 'pay_zen/helper'
|
||||||
|
|
||||||
def sdk_test
|
def sdk_test
|
||||||
@ -23,7 +24,89 @@ class API::PayzenController < API::PaymentsController
|
|||||||
client = PayZen::Charge.new
|
client = PayZen::Charge.new
|
||||||
@result = client.create_payment(amount: amount[:amount],
|
@result = client.create_payment(amount: amount[:amount],
|
||||||
order_id: @id,
|
order_id: @id,
|
||||||
customer: { reference: params[:customer][:id], email: params[:customer][:email] })
|
customer: PayZen::Helper.generate_customer(params[:customer_id]))
|
||||||
@result
|
@result
|
||||||
end
|
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
|
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 { AxiosResponse } from 'axios';
|
||||||
import { CartItems } from '../models/payment';
|
import { CartItems } from '../models/payment';
|
||||||
import { User } from '../models/user';
|
import { User } from '../models/user';
|
||||||
import { CreatePaymentResponse, SdkTestResponse } from '../models/payzen';
|
import { ConfirmPaymentResponse, CreatePaymentResponse, SdkTestResponse } from '../models/payzen';
|
||||||
|
|
||||||
export default class PayzenAPI {
|
export default class PayzenAPI {
|
||||||
|
|
||||||
@ -11,8 +11,13 @@ export default class PayzenAPI {
|
|||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async chargeCreatePayment(cart_items: CartItems, customer: User): Promise<CreatePaymentResponse> {
|
static async chargeCreatePayment(cartItems: CartItems, customer: User): Promise<CreatePaymentResponse> {
|
||||||
const res: AxiosResponse = await apiClient.post('/api/payzen/create_payment', { cart_items, customer: { id: customer.id, email: customer.email } });
|
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;
|
return res?.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,10 +30,12 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
|
|||||||
const { t } = useTranslation('shared');
|
const { t } = useTranslation('shared');
|
||||||
const PayZenKR = useRef<KryptonClient>(null);
|
const PayZenKR = useRef<KryptonClient>(null);
|
||||||
const [loadingClass, setLoadingClass] = useState<'hidden' | 'loader' | 'loader-overlay'>('loader');
|
const [loadingClass, setLoadingClass] = useState<'hidden' | 'loader' | 'loader-overlay'>('loader');
|
||||||
|
const [hmacKey, setHmacKey] = useState<string>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const api = new SettingAPI();
|
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 => {
|
PayzenAPI.chargeCreatePayment(cartItems, customer).then(formToken => {
|
||||||
KRGlue.loadLibrary(settings.get(SettingName.PayZenEndpoint), settings.get(SettingName.PayZenPublicKey)) /* Load the remote library */
|
KRGlue.loadLibrary(settings.get(SettingName.PayZenEndpoint), settings.get(SettingName.PayZenPublicKey)) /* Load the remote library */
|
||||||
.then(({ KR }) =>
|
.then(({ KR }) =>
|
||||||
@ -54,11 +56,17 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
|
|||||||
* Callback triggered on PayZen successful payments
|
* Callback triggered on PayZen successful payments
|
||||||
*/
|
*/
|
||||||
const onPaid = (event: ProcessPaymentAnswer): boolean => {
|
const onPaid = (event: ProcessPaymentAnswer): boolean => {
|
||||||
|
// TODO check hash
|
||||||
|
|
||||||
|
const transaction = event.clientAnswer.transactions[0];
|
||||||
|
|
||||||
if (event.clientAnswer.orderStatus === 'PAID') {
|
if (event.clientAnswer.orderStatus === 'PAID') {
|
||||||
PayZenKR.current.removeForms();
|
PayzenAPI.confirm(event.clientAnswer.orderDetails.orderId, cartItems).then(() => {
|
||||||
onSuccess(event.clientAnswer);
|
PayZenKR.current.removeForms().then(() => {
|
||||||
|
onSuccess(event.clientAnswer);
|
||||||
|
});
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
const transaction = event.clientAnswer.transactions[0];
|
|
||||||
const error = `${transaction?.errorMessage}. ${transaction?.detailedErrorMessage || ''}`;
|
const error = `${transaction?.errorMessage}. ${transaction?.detailedErrorMessage || ''}`;
|
||||||
onError(error || event.clientAnswer.orderStatus);
|
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 = () => {
|
const Loader: FunctionComponent = () => {
|
||||||
return (
|
return (
|
||||||
<div className={`fa-3x ${loadingClass}`}>
|
<div className={`fa-3x ${loadingClass}`}>
|
||||||
|
@ -7,6 +7,10 @@ export interface CreatePaymentResponse {
|
|||||||
orderId: string
|
orderId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConfirmPaymentResponse {
|
||||||
|
todo?: any
|
||||||
|
}
|
||||||
|
|
||||||
export interface OrderDetails {
|
export interface OrderDetails {
|
||||||
mode?: 'TEST' | 'PRODUCTION',
|
mode?: 'TEST' | 'PRODUCTION',
|
||||||
orderCurrency?: string,
|
orderCurrency?: string,
|
||||||
|
@ -330,6 +330,10 @@ class User < ApplicationRecord
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def organization?
|
||||||
|
!invoicing_profile.organization.nil?
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
# remove projects drafts that are not linked to another user
|
# remove projects drafts that are not linked to another user
|
||||||
|
@ -182,6 +182,7 @@ Rails.application.routes.draw do
|
|||||||
## PayZen gateway
|
## PayZen gateway
|
||||||
post 'payzen/sdk_test' => 'payzen#sdk_test'
|
post 'payzen/sdk_test' => 'payzen#sdk_test'
|
||||||
post 'payzen/create_payment' => 'payzen#create_payment'
|
post 'payzen/create_payment' => 'payzen#create_payment'
|
||||||
|
post 'payzen/confirm_payment' => 'payzen#confirm_payment'
|
||||||
|
|
||||||
# FabAnalytics
|
# FabAnalytics
|
||||||
get 'analytics/data' => 'analytics#data'
|
get 'analytics/data' => 'analytics#data'
|
||||||
|
@ -6,6 +6,7 @@ module PayZen; end
|
|||||||
## Provides various methods around the PayZen payment gateway
|
## Provides various methods around the PayZen payment gateway
|
||||||
class PayZen::Helper
|
class PayZen::Helper
|
||||||
class << self
|
class << self
|
||||||
|
## Is the PayZen gateway enabled?
|
||||||
def enabled?
|
def enabled?
|
||||||
return false unless Setting.get('online_payment_module')
|
return false unless Setting.get('online_payment_module')
|
||||||
return false unless Setting.get('payment_gateway') == 'payzen'
|
return false unless Setting.get('payment_gateway') == 'payzen'
|
||||||
@ -17,11 +18,37 @@ class PayZen::Helper
|
|||||||
res
|
res
|
||||||
end
|
end
|
||||||
|
|
||||||
|
## generate an unique string reference for the content of a cart
|
||||||
def generate_ref(cart_items, customer)
|
def generate_ref(cart_items, customer)
|
||||||
require 'sha3'
|
require 'sha3'
|
||||||
|
|
||||||
content = { cart_items: cart_items, customer: customer }.to_json + DateTime.current.to_s
|
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
|
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…
x
Reference in New Issue
Block a user