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:
commit
6d39e4a6ee
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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']));
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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];
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
*/
|
||||
|
13
app/frontend/src/javascript/lib/coupon.ts
Normal file
13
app/frontend/src/javascript/lib/coupon.ts
Normal 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;
|
||||
};
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
10
app/policies/checkout_policy.rb
Normal file
10
app/policies/checkout_policy.rb
Normal 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
|
@ -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
|
60
app/services/cart/find_or_create_service.rb
Normal file
60
app/services/cart/find_or_create_service.rb
Normal 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
|
@ -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
|
@ -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
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -0,0 +1,6 @@
|
||||
class AddFootprintAndEnvironmentToOrder < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :orders, :footprint, :string
|
||||
add_column :orders, :environment, :string
|
||||
end
|
||||
end
|
5
db/migrate/20220826140921_add_coupon_id_to_order.rb
Normal file
5
db/migrate/20220826140921_add_coupon_id_to_order.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class AddCouponIdToOrder < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_reference :orders, :coupon, index: true, foreign_key: true
|
||||
end
|
||||
end
|
5
db/migrate/20220826175129_add_paid_total_to_order.rb
Normal file
5
db/migrate/20220826175129_add_paid_total_to_order.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class AddPaidTotalToOrder < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :orders, :paid_total, :integer
|
||||
end
|
||||
end
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user