1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-29 10:24:20 +01:00

Merge branch 'product_store-payment' into product_store-store

This commit is contained in:
Du Peng 2022-08-29 08:57:21 +02:00
commit 6d39e4a6ee
37 changed files with 284 additions and 134 deletions

View File

@ -9,22 +9,7 @@ class API::CartController < API::ApiController
def create
authorize :cart, :create?
@order = Order.find_by(token: order_token, state: 'cart')
if @order.nil?
if current_user&.member?
@order = Order.where(statistic_profile_id: current_user.statistic_profile.id,
state: 'cart').last
end
if current_user&.privileged?
@order = Order.where(operator_profile_id: current_user.invoicing_profile.id,
state: 'cart').last
end
end
if @order
@order.update(statistic_profile_id: current_user.statistic_profile.id) if @order.statistic_profile_id.nil? && current_user&.member?
@order.update(operator_profile_id: current_user.invoicing_profile.id) if @order.operator_profile_id.nil? && current_user&.privileged?
end
@order ||= Cart::CreateService.new.call(current_user)
@order ||= Cart::FindOrCreateService.new.call(order_token, current_user)
render 'api/orders/show'
end
@ -46,12 +31,6 @@ class API::CartController < API::ApiController
render 'api/orders/show'
end
def set_customer
authorize @current_order, policy_class: CartPolicy
@order = Cart::SetCustomerService.new.call(@current_order, cart_params[:user_id])
render 'api/orders/show'
end
private
def orderable

View File

@ -8,14 +8,21 @@ class API::CheckoutController < API::ApiController
before_action :ensure_order
def payment
res = Checkout::PaymentService.new.payment(@current_order, current_user, params[:payment_id])
authorize @current_order, policy_class: CheckoutPolicy
if @current_order.statistic_profile_id.nil? && current_user.privileged?
user = User.find(params[:customer_id])
@current_order.statistic_profile = user.statistic_profile
end
res = Checkout::PaymentService.new.payment(@current_order, current_user, params[:coupon_code],
params[:payment_id])
render json: res
rescue StandardError => e
render json: e, status: :unprocessable_entity
end
def confirm_payment
res = Checkout::PaymentService.new.confirm_payment(@current_order, current_user, params[:payment_id])
authorize @current_order, policy_class: CheckoutPolicy
res = Checkout::PaymentService.new.confirm_payment(@current_order, current_user, params[:coupon_code], params[:payment_id])
render json: res
rescue StandardError => e
render json: e, status: :unprocessable_entity

View File

@ -22,9 +22,4 @@ export default class CartAPI {
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/set_quantity', { order_token: order.token, orderable_id: orderableId, quantity });
return res?.data;
}
static async setCustomer (order: Order, userId: number): Promise<Order> {
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/set_customer', { order_token: order.token, user_id: userId });
return res?.data;
}
}

View File

@ -1,20 +1,24 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { OrderPayment } from '../models/order';
import { OrderPayment, Order } from '../models/order';
export default class CheckoutAPI {
static async payment (token: string, paymentId?: string): Promise<OrderPayment> {
static async payment (order: Order, paymentId?: string): Promise<OrderPayment> {
const res: AxiosResponse<OrderPayment> = await apiClient.post('/api/checkout/payment', {
order_token: token,
payment_id: paymentId
order_token: order.token,
coupon_code: order.coupon?.code,
payment_id: paymentId,
customer_id: order.user.id
});
return res?.data;
}
static async confirmPayment (token: string, paymentId: string): Promise<OrderPayment> {
static async confirmPayment (order: Order, paymentId: string): Promise<OrderPayment> {
const res: AxiosResponse<OrderPayment> = await apiClient.post('/api/checkout/confirm_payment', {
order_token: token,
payment_id: paymentId
order_token: order.token,
coupon_code: order.coupon?.code,
payment_id: paymentId,
customer_id: order.user.id
});
return res?.data;
}

View File

@ -13,18 +13,21 @@ import { PaymentMethod } from '../../models/payment';
import { Order } from '../../models/order';
import { MemberSelect } from '../user/member-select';
import { CouponInput } from '../coupon/coupon-input';
import { Coupon } from '../../models/coupon';
import { computePriceWithCoupon } from '../../lib/coupon';
declare const Application: IApplication;
interface StoreCartProps {
onError: (message: string) => void,
currentUser?: User,
userLogin: () => void,
currentUser?: User
}
/**
* This component shows user's cart
*/
const StoreCart: React.FC<StoreCartProps> = ({ onError, currentUser }) => {
const StoreCart: React.FC<StoreCartProps> = ({ onError, currentUser, userLogin }) => {
const { t } = useTranslation('public');
const { cart, setCart } = useCart(currentUser);
@ -58,7 +61,11 @@ const StoreCart: React.FC<StoreCartProps> = ({ onError, currentUser }) => {
* Checkout cart
*/
const checkout = () => {
setPaymentModal(true);
if (!currentUser) {
userLogin();
} else {
setPaymentModal(true);
}
};
/**
@ -84,9 +91,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onError, currentUser }) => {
* Change cart's customer by admin/manger
*/
const handleChangeMember = (userId: number): void => {
CartAPI.setCustomer(cart, userId).then(data => {
setCart(data);
}).catch(onError);
setCart({ ...cart, user: { id: userId, role: 'member' } });
};
/**
@ -103,6 +108,15 @@ const StoreCart: React.FC<StoreCartProps> = ({ onError, currentUser }) => {
return cart && cart.order_items_attributes.length === 0;
};
/**
* Apply coupon to current cart
*/
const applyCoupon = (coupon?: Coupon): void => {
if (coupon !== cart.coupon) {
setCart({ ...cart, coupon });
}
};
return (
<div className="store-cart">
{cart && cartIsEmpty() && <p>{t('app.public.store_cart.cart_is_empty')}</p>}
@ -122,11 +136,13 @@ const StoreCart: React.FC<StoreCartProps> = ({ onError, currentUser }) => {
</FabButton>
</div>
))}
{cart && !cartIsEmpty() && <CouponInput user={cart.user} amount={cart.total} />}
{cart && !cartIsEmpty() && <p>Totale: {FormatLib.price(cart.total)}</p>}
{cart && !cartIsEmpty() && isPrivileged() && <MemberSelect defaultUser={cart.user} onSelected={handleChangeMember} />}
{cart && !cartIsEmpty() && cart.user && <CouponInput user={cart.user as User} amount={cart.total} onChange={applyCoupon} />}
{cart && !cartIsEmpty() && <p>Total produits: {FormatLib.price(cart.total)}</p>}
{cart && !cartIsEmpty() && cart.coupon && computePriceWithCoupon(cart.total, cart.coupon) !== cart.total && <p>Coupon réduction: {FormatLib.price(-(cart.total - computePriceWithCoupon(cart.total, cart.coupon)))}</p>}
{cart && !cartIsEmpty() && <p>Total panier: {FormatLib.price(computePriceWithCoupon(cart.total, cart.coupon))}</p>}
{cart && !cartIsEmpty() && isPrivileged() && <MemberSelect onSelected={handleChangeMember} />}
{cart && !cartIsEmpty() &&
<FabButton className="checkout-btn" onClick={checkout} disabled={!cart.user || cart.order_items_attributes.length === 0}>
<FabButton className="checkout-btn" onClick={checkout}>
{t('app.public.store_cart.checkout')}
</FabButton>
}
@ -138,7 +154,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onError, currentUser }) => {
cart={{ customer_id: cart.user.id, items: [], payment_method: PaymentMethod.Card }}
order={cart}
operator={currentUser}
customer={cart.user}
customer={cart.user as User}
updateCart={() => 'dont need update shopping cart'} />
</div>}
</div>
@ -153,4 +169,4 @@ const StoreCartWrapper: React.FC<StoreCartProps> = (props) => {
);
};
Application.Components.component('storeCart', react2angular(StoreCartWrapper, ['onError', 'currentUser']));
Application.Components.component('storeCart', react2angular(StoreCartWrapper, ['onError', 'currentUser', 'userLogin']));

View File

@ -5,11 +5,12 @@ import { FabAlert } from '../base/fab-alert';
import CouponAPI from '../../api/coupon';
import { Coupon } from '../../models/coupon';
import { User } from '../../models/user';
import FormatLib from '../../lib/format';
interface CouponInputProps {
amount: number,
user?: User,
onChange?: (coupon: Coupon) => void
onChange?: (coupon?: Coupon) => void
}
interface Message {
@ -42,7 +43,7 @@ export const CouponInput: React.FC<CouponInputProps> = ({ user, amount, onChange
if (res.type === 'percent_off') {
mgs.push({ type: 'info', message: t('app.shared.coupon_input.the_coupon_has_been_applied_you_get_PERCENT_discount', { PERCENT: res.percent_off }) });
} else {
mgs.push({ type: 'info', message: t('app.shared.coupon_input.the_coupon_has_been_applied_you_get_AMOUNT_CURRENCY', { AMOUNT: res.amount_off, CURRENCY: 'euro' }) });
mgs.push({ type: 'info', message: t('app.shared.coupon_input.the_coupon_has_been_applied_you_get_AMOUNT_CURRENCY', { AMOUNT: res.amount_off, CURRENCY: FormatLib.currencySymbol() }) });
}
if (res.validity_per_user === 'once') {
mgs.push({ type: 'warning', message: t('app.shared.coupon_input.coupon_validity_once') });
@ -58,7 +59,10 @@ export const CouponInput: React.FC<CouponInputProps> = ({ user, amount, onChange
setCoupon(null);
setLoading(false);
setMessages([{ type: 'danger', message: t(`app.shared.coupon_input.unable_to_apply_the_coupon_because_${state}`) }]);
onChange(null);
});
} else {
onChange(null);
}
};

View File

@ -19,6 +19,7 @@ import { ComputePriceResult } from '../../models/price';
import { Wallet } from '../../models/wallet';
import FormatLib from '../../lib/format';
import { Order } from '../../models/order';
import { computePriceWithCoupon } from '../../lib/coupon';
export interface GatewayFormProps {
onSubmit: () => void,
@ -114,7 +115,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
if (order && order?.user?.id) {
WalletAPI.getByUser(order.user.id).then((wallet) => {
setWallet(wallet);
const p = { price: order.total, price_without_coupon: order.total };
const p = { price: computePriceWithCoupon(order.total, order.coupon), price_without_coupon: order.total };
setPrice(p);
setRemainingPrice(new WalletLib(wallet).computeRemainingPrice(p.price));
setReady(true);

View File

@ -89,7 +89,8 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
try {
let res;
if (order) {
res = await CheckoutAPI.payment(order.token);
res = await CheckoutAPI.payment(order);
res = res.order;
} else {
res = await LocalPaymentAPI.confirmPayment(cart);
}
@ -120,6 +121,9 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
* Get the type of the main item in the cart compile
*/
const mainItemType = (): string => {
if (order) {
return '';
}
return Object.keys(cart.items[0])[0];
};

View File

@ -58,7 +58,7 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
} else if (paymentSchedule) {
return await PayzenAPI.chargeCreateToken(cart, customer);
} else if (order) {
const res = await CheckoutAPI.payment(order.token);
const res = await CheckoutAPI.payment(order);
return res.payment as CreateTokenResponse;
} else {
return await PayzenAPI.chargeCreatePayment(cart, customer);
@ -97,7 +97,7 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
if (paymentSchedule) {
return await PayzenAPI.confirmPaymentSchedule(event.clientAnswer.orderDetails.orderId, transaction.uuid, cart);
} else if (order) {
const res = await CheckoutAPI.confirmPayment(order.token, event.clientAnswer.orderDetails.orderId);
const res = await CheckoutAPI.confirmPayment(order, event.clientAnswer.orderDetails.orderId);
return res.order;
} else {
return await PayzenAPI.confirm(event.clientAnswer.orderDetails.orderId, cart);

View File

@ -12,6 +12,7 @@ import { CardPaymentModal } from '../card-payment-modal';
import PriceAPI from '../../../api/price';
import { ComputePriceResult } from '../../../models/price';
import { Order } from '../../../models/order';
import { computePriceWithCoupon } from '../../../lib/coupon';
interface PaymentModalProps {
isOpen: boolean,
@ -47,7 +48,7 @@ export const PaymentModal: React.FC<PaymentModalProps> = ({ isOpen, toggleModal,
// refresh the price when the cart changes
useEffect(() => {
if (order) {
setPrice({ price: order.total, price_without_coupon: order.total });
setPrice({ price: computePriceWithCoupon(order.total, order.coupon), price_without_coupon: order.total });
} else {
PriceAPI.compute(cart).then(price => {
setPrice(price);

View File

@ -44,7 +44,7 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
try {
if (!paymentSchedule) {
if (order) {
const res = await CheckoutAPI.payment(order.token, paymentMethod.id);
const res = await CheckoutAPI.payment(order, paymentMethod.id);
if (res.payment) {
await handleServerConfirmation(res.payment as PaymentConfirmation);
} else {
@ -90,7 +90,7 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
// The PaymentIntent can be confirmed again on the server
try {
if (order) {
const confirmation = await CheckoutAPI.confirmPayment(order.token, result.paymentIntent.id);
const confirmation = await CheckoutAPI.confirmPayment(order, result.paymentIntent.id);
await handleServerConfirmation(confirmation.order);
} else {
const confirmation = await StripeAPI.confirmIntent(result.paymentIntent.id, cart);

View File

@ -4,12 +4,24 @@
*/
'use strict';
Application.Controllers.controller('CartController', ['$scope', 'CSRF', 'growl', '$state',
function ($scope, CSRF, growl, $state) {
Application.Controllers.controller('CartController', ['$scope', 'CSRF', 'growl',
function ($scope, CSRF, growl) {
/* PRIVATE SCOPE */
/* PUBLIC SCOPE */
/**
* Open the modal dialog allowing the user to log into the system
*/
$scope.userLogin = function () {
setTimeout(() => {
if (!$scope.isAuthenticated()) {
$scope.login();
$scope.$apply();
}
}, 50);
};
/**
* Callback triggered in case of error
*/

View File

@ -0,0 +1,13 @@
import { Coupon } from '../models/coupon';
export const computePriceWithCoupon = (price: number, coupon?: Coupon): number => {
if (!coupon) {
return price;
}
if (coupon.type === 'percent_off') {
return price - (price * coupon.percent_off / 100.00);
} else if (coupon.type === 'amount_off' && price > coupon.amount_off) {
return price - coupon.amount_off;
}
return price;
};

View File

@ -32,4 +32,11 @@ export default class FormatLib {
static price = (price: number): string => {
return new Intl.NumberFormat(Fablab.intl_locale, { style: 'currency', currency: Fablab.intl_currency }).format(price);
};
/**
* Return currency symbol for currency setting
*/
static currencySymbol = (): string => {
return new Intl.NumberFormat('fr', { style: 'currency', currency: Fablab.intl_currency }).formatToParts()[2].value;
};
}

View File

@ -1,18 +1,24 @@
import { TDateISO } from '../typings/date-iso';
import { PaymentConfirmation } from './payment';
import { CreateTokenResponse } from './payzen';
import { User } from './user';
import { UserRole } from './user';
import { Coupon } from './coupon';
export interface Order {
id: number,
token: string,
statistic_profile_id?: number,
user?: User,
user?: {
id: number,
role: UserRole
name?: string,
},
operator_profile_id?: number,
reference?: string,
state?: string,
payment_state?: string,
total?: number,
coupon?: Coupon,
created_at?: TDateISO,
order_items_attributes: Array<{
id: number,

View File

@ -15,5 +15,5 @@
<section class="m-lg">
<store-cart current-user="currentUser" on-error="onError" on-success="onSuccess" />
<store-cart current-user="currentUser" user-login="userLogin" on-error="onError" on-success="onSuccess" />
</section>

View File

@ -4,6 +4,7 @@
class Coupon < ApplicationRecord
has_many :invoices
has_many :payment_schedule
has_many :orders
after_create :create_gateway_coupon
before_destroy :delete_gateway_coupon
@ -82,7 +83,7 @@ class Coupon < ApplicationRecord
end
def users
invoices.map(&:user)
invoices.map(&:user).concat(orders.map(&:user)).uniq(&:id)
end
def users_ids
@ -104,5 +105,4 @@ class Coupon < ApplicationRecord
def delete_gateway_coupon
PaymentGatewayService.new.delete_coupon(id)
end
end

View File

@ -1,10 +1,12 @@
# frozen_string_literal: true
# Order is a model for the user hold information of order
class Order < ApplicationRecord
class Order < PaymentDocument
belongs_to :statistic_profile
belongs_to :operator_profile, class_name: 'InvoicingProfile'
belongs_to :coupon
has_many :order_items, dependent: :destroy
has_one :payment_gateway_object, as: :item
ALL_STATES = %w[cart in_progress ready canceled return].freeze
enum state: ALL_STATES.zip(ALL_STATES).to_h
@ -14,8 +16,19 @@ class Order < ApplicationRecord
validates :token, :state, presence: true
def set_wallet_transaction(amount, transaction_id)
self.wallet_amount = amount
self.wallet_transaction_id = transaction_id
before_create :add_environment
delegate :user, to: :statistic_profile
def footprint_children
order_items
end
def paid_by_card?
!payment_gateway_object.nil? && payment_method == 'card'
end
def self.columns_out_of_footprint
%w[payment_method]
end
end

View File

@ -15,6 +15,7 @@ class PaymentGatewayObject < ApplicationRecord
belongs_to :machine, foreign_type: 'Machine', foreign_key: 'item_id'
belongs_to :space, foreign_type: 'Space', foreign_key: 'item_id'
belongs_to :training, foreign_type: 'Training', foreign_key: 'item_id'
belongs_to :order, foreign_type: 'Order', foreign_key: 'item_id'
belongs_to :payment_gateway_object # some objects may require a reference to another object for remote recovery

View File

@ -9,7 +9,7 @@ class PaymentSchedule < PaymentDocument
belongs_to :coupon
belongs_to :invoicing_profile
belongs_to :statistic_profile
belongs_to :operator_profile, foreign_key: :operator_profile_id, class_name: 'InvoicingProfile'
belongs_to :operator_profile, class_name: 'InvoicingProfile'
has_many :payment_schedule_items
has_many :payment_gateway_objects, as: :item
@ -61,9 +61,7 @@ class PaymentSchedule < PaymentDocument
payment_schedule_objects.find_by(main: true)
end
def user
invoicing_profile.user
end
delegate :user, to: :invoicing_profile
# for debug & used by rake task "fablab:maintenance:regenerate_schedules"
def regenerate_pdf

View File

@ -6,10 +6,6 @@ class CartPolicy < ApplicationPolicy
true
end
def set_customer?
user.privileged?
end
%w[add_item remove_item set_quantity].each do |action|
define_method "#{action}?" do
return user.privileged? || (record.statistic_profile_id == user.statistic_profile.id) if user

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
# Check the access policies for API::CheckoutController
class CheckoutPolicy < ApplicationPolicy
%w[payment confirm_payment].each do |action|
define_method "#{action}?" do
return user.privileged? || (record.statistic_profile_id == user.statistic_profile.id)
end
end
end

View File

@ -1,19 +0,0 @@
# frozen_string_literal: true
# Provides methods for create cart
class Cart::CreateService
def call(user)
token = GenerateTokenService.new.call(Order)
order_param = {
token: token,
state: 'cart',
total: 0
}
if user
order_param[:statistic_profile_id] = user.statistic_profile.id if user.member?
order_param[:operator_profile_id] = user.invoicing_profile.id if user.privileged?
end
Order.create!(order_param)
end
end

View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
# Provides methods for find or create a cart
class Cart::FindOrCreateService
def call(order_token, user)
order = Order.find_by(token: order_token, state: 'cart')
if order && user && ((user.member? && order.statistic_profile_id.present? && order.statistic_profile_id != user.statistic_profile.id) ||
(user.privileged? && order.operator_profile_id.present? && order.operator_profile_id != user.invoicing_profile.id))
order = nil
end
order = nil if order && !user && order.statistic_profile_id.present?
if order && order.statistic_profile_id.present? && Order.where(statistic_profile_id: order.statistic_profile_id,
payment_state: 'paid').where('created_at > ?', order.created_at).last.present?
order = nil
end
if order.nil?
if user&.member?
last_paid_order = Order.where(statistic_profile_id: user.statistic_profile.id,
payment_state: 'paid').last
order = if last_paid_order
Order.where(statistic_profile_id: user.statistic_profile.id,
state: 'cart').where('created_at > ?', last_paid_order.created_at).last
else
Order.where(statistic_profile_id: user.statistic_profile.id, state: 'cart').last
end
end
if user&.privileged?
last_paid_order = Order.where(operator_profile_id: user.invoicing_profile.id,
payment_state: 'paid').last
order = if last_paid_order
Order.where(operator_profile_id: user.invoicing_profile.id,
state: 'cart').where('created_at > ?', last_paid_order.created_at).last
else
Order.where(operator_profile_id: user.invoicing_profile.id, state: 'cart').last
end
end
end
if order
order.update(statistic_profile_id: user.statistic_profile.id) if order.statistic_profile_id.nil? && user&.member?
order.update(operator_profile_id: user.invoicing_profile.id) if order.operator_profile_id.nil? && user&.privileged?
return order
end
token = GenerateTokenService.new.call(Order)
order_param = {
token: token,
state: 'cart',
total: 0
}
if user
order_param[:statistic_profile_id] = user.statistic_profile.id if user.member?
order_param[:operator_profile_id] = user.invoicing_profile.id if user.privileged?
end
Order.create!(order_param)
end
end

View File

@ -1,10 +0,0 @@
# frozen_string_literal: true
# Provides methods for admin set customer to order
class Cart::SetCustomerService
def call(order, user_id)
user = User.find(user_id)
order.update(statistic_profile_id: user.statistic_profile.id)
order.reload
end
end

View File

@ -4,31 +4,35 @@
class Checkout::PaymentService
require 'pay_zen/helper'
require 'stripe/helper'
include Payments::PaymentConcern
def payment(order, operator, payment_id = '')
def payment(order, operator, coupon_code, payment_id = '')
raise Cart::OutStockError unless Orders::OrderService.new.in_stock?(order, 'external')
raise Cart::InactiveProductError unless Orders::OrderService.new.all_products_is_active?(order)
if operator.member?
CouponService.new.validate(coupon_code, order.statistic_profile.user)
amount = debit_amount(order)
if operator.privileged? || amount.zero?
Payments::LocalService.new.payment(order, coupon_code)
elsif operator.member?
if Stripe::Helper.enabled?
Payments::StripeService.new.payment(order, payment_id)
Payments::StripeService.new.payment(order, coupon_code, payment_id)
elsif PayZen::Helper.enabled?
Payments::PayzenService.new.payment(order)
Payments::PayzenService.new.payment(order, coupon_code)
else
raise Error('Bad gateway or online payment is disabled')
end
elsif operator.privileged?
Payments::LocalService.new.payment(order)
end
end
def confirm_payment(order, operator, payment_id = '')
def confirm_payment(order, operator, coupon_code, payment_id = '')
if operator.member?
if Stripe::Helper.enabled?
Payments::StripeService.new.confirm_payment(order, payment_id)
Payments::StripeService.new.confirm_payment(order, coupon_code, payment_id)
elsif PayZen::Helper.enabled?
Payments::PayzenService.new.confirm_payment(order, payment_id)
Payments::PayzenService.new.confirm_payment(order, coupon_code, payment_id)
else
raise Error('Bad gateway or online payment is disabled')
end

View File

@ -32,6 +32,18 @@ class PaymentDocumentService
reference.gsub!(/X\[([^\]]+)\]/, ''.to_s)
end
# remove information about refunds (R[text])
reference.gsub!(/R\[([^\]]+)\]/, ''.to_s)
# remove information about payment schedule (S[text])
reference.gsub!(/S\[([^\]]+)\]/, ''.to_s)
elsif document.is_a? Order
# information about online selling (X[text])
if document.paid_by_card?
reference.gsub!(/X\[([^\]]+)\]/, '\1')
else
reference.gsub!(/X\[([^\]]+)\]/, ''.to_s)
end
# remove information about refunds (R[text])
reference.gsub!(/R\[([^\]]+)\]/, ''.to_s)
# remove information about payment schedule (S[text])

View File

@ -4,8 +4,8 @@
class Payments::LocalService
include Payments::PaymentConcern
def payment(order)
o = payment_success(order, 'local')
def payment(order, coupon_code)
o = payment_success(order, coupon_code, 'local')
{ order: o }
end
end

View File

@ -9,19 +9,34 @@ module Payments::PaymentConcern
wallet_amount >= total_amount ? total_amount : wallet_amount
end
def debit_amount(order)
total = order.total
def debit_amount(order, coupon_code = nil)
total = CouponService.new.apply(order.total, coupon_code, order.statistic_profile.user)
wallet_debit = get_wallet_debit(order.statistic_profile.user, total)
total - wallet_debit
end
def payment_success(order, payment_method = '')
def payment_success(order, coupon_code, payment_method = '', payment_id = nil, payment_type = nil)
ActiveRecord::Base.transaction do
order.paid_total = debit_amount(order, coupon_code)
coupon = Coupon.find_by(code: coupon_code)
order.coupon_id = coupon.id if coupon
WalletService.debit_user_wallet(order, order.statistic_profile.user)
order.update(state: 'in_progress', payment_state: 'paid', payment_method: payment_method)
order.operator_profile_id = order.statistic_profile.user.invoicing_profile.id if order.operator_profile.nil?
order.payment_method = if order.total == order.wallet_amount
'wallet'
else
payment_method
end
order.state = 'in_progress'
order.payment_state = 'paid'
if payment_id && payment_type
order.payment_gateway_object = PaymentGatewayObject.new(gateway_object_id: payment_id, gateway_object_type: payment_type)
end
order.order_items.each do |item|
ProductService.update_stock(item.orderable, 'external', 'sold', -item.quantity, item.id)
end
order.reference = order.generate_reference
order.save
order.reload
end
end

View File

@ -8,8 +8,8 @@ class Payments::PayzenService
require 'pay_zen/service'
include Payments::PaymentConcern
def payment(order)
amount = debit_amount(order)
def payment(order, coupon_code)
amount = debit_amount(order, coupon_code)
raise Cart::ZeroPriceError if amount.zero?
@ -23,16 +23,16 @@ class Payments::PayzenService
{ order: order, payment: { formToken: result['answer']['formToken'], orderId: id } }
end
def confirm_payment(order, payment_id)
def confirm_payment(order, coupon_code, payment_id)
client = PayZen::Order.new
payzen_order = client.get(payment_id, operation_type: 'DEBIT')
if payzen_order['answer']['transactions'].any? { |transaction| transaction['status'] == 'PAID' }
o = payment_success(order, 'card')
o = payment_success(order, coupon_code, 'card', payment_id, 'PayZen::Order')
{ order: o }
else
order.update(payment_state: 'failed')
{ order: order, payment_error: payzen_order['answer'] }
{ order: order, payment: { error: { statusText: payzen_order['answer'] } } }
end
end
end

View File

@ -5,8 +5,8 @@ class Payments::StripeService
require 'stripe/service'
include Payments::PaymentConcern
def payment(order, payment_id)
amount = debit_amount(order)
def payment(order, coupon_code, payment_id)
amount = debit_amount(order, coupon_code)
raise Cart::ZeroPriceError if amount.zero?
@ -23,7 +23,7 @@ class Payments::StripeService
)
if intent&.status == 'succeeded'
o = payment_success(order, 'card')
o = payment_success(order, coupon_code, 'card', intent.id, intent.class.name)
return { order: o }
end
@ -33,14 +33,14 @@ class Payments::StripeService
end
end
def confirm_payment(order, payment_id)
def confirm_payment(order, coupon_code, payment_id)
intent = Stripe::PaymentIntent.confirm(payment_id, {}, { api_key: Setting.get('stripe_secret_key') })
if intent&.status == 'succeeded'
o = payment_success(order, 'card')
o = payment_success(order, coupon_code, 'card', intent.id, intent.class.name)
{ order: o }
else
order.update(payment_state: 'failed')
{ order: order, payment_error: 'payment failed' }
{ order: order, payment: { error: { statusText: 'payment failed' } } }
end
end
end

View File

@ -23,7 +23,7 @@ class ProductService
nil
end
def self.update_stock(product, stock_type, reason, quantity, order_item_id)
def self.update_stock(product, stock_type, reason, quantity, order_item_id = nil)
remaining_stock = product.stock[stock_type] + quantity
product.product_stock_movements.create(stock_type: stock_type, reason: reason, quantity: quantity, remaining_stock: remaining_stock,
date: DateTime.current,

View File

@ -159,7 +159,6 @@ Rails.application.routes.draw do
put 'add_item', on: :collection
put 'remove_item', on: :collection
put 'set_quantity', on: :collection
put 'set_customer', on: :collection
end
resources :checkout, only: %i[] do
post 'payment', on: :collection

View File

@ -0,0 +1,6 @@
class AddFootprintAndEnvironmentToOrder < ActiveRecord::Migration[5.2]
def change
add_column :orders, :footprint, :string
add_column :orders, :environment, :string
end
end

View File

@ -0,0 +1,5 @@
class AddCouponIdToOrder < ActiveRecord::Migration[5.2]
def change
add_reference :orders, :coupon, index: true, foreign_key: true
end
end

View File

@ -0,0 +1,5 @@
class AddPaidTotalToOrder < ActiveRecord::Migration[5.2]
def change
add_column :orders, :paid_total, :integer
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_08_26_093503) do
ActiveRecord::Schema.define(version: 2022_08_26_175129) do
# These are extensions that must be enabled in order to support this database
enable_extension "fuzzystrmatch"
@ -471,6 +471,11 @@ ActiveRecord::Schema.define(version: 2022_08_26_093503) do
t.integer "wallet_amount"
t.integer "wallet_transaction_id"
t.string "payment_method"
t.string "footprint"
t.string "environment"
t.bigint "coupon_id"
t.integer "paid_total"
t.index ["coupon_id"], name: "index_orders_on_coupon_id"
t.index ["operator_profile_id"], name: "index_orders_on_operator_profile_id"
t.index ["statistic_profile_id"], name: "index_orders_on_statistic_profile_id"
end
@ -1165,6 +1170,7 @@ ActiveRecord::Schema.define(version: 2022_08_26_093503) do
add_foreign_key "invoices", "wallet_transactions"
add_foreign_key "invoicing_profiles", "users"
add_foreign_key "order_items", "orders"
add_foreign_key "orders", "coupons"
add_foreign_key "orders", "invoicing_profiles", column: "operator_profile_id"
add_foreign_key "orders", "statistic_profiles"
add_foreign_key "organizations", "invoicing_profiles"