mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
pay cart with coupon code
This commit is contained in:
parent
3a669109b5
commit
981cffa27d
@ -8,14 +8,14 @@ class API::CheckoutController < API::ApiController
|
||||
before_action :ensure_order
|
||||
|
||||
def payment
|
||||
res = Checkout::PaymentService.new.payment(@current_order, current_user, params[:payment_id])
|
||||
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])
|
||||
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
|
||||
|
@ -1,19 +1,21 @@
|
||||
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,
|
||||
order_token: order.token,
|
||||
coupon_code: order.coupon?.code,
|
||||
payment_id: paymentId
|
||||
});
|
||||
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,
|
||||
order_token: order.token,
|
||||
coupon_code: order.coupon?.code,
|
||||
payment_id: paymentId
|
||||
});
|
||||
return res?.data;
|
||||
|
@ -13,6 +13,8 @@ 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;
|
||||
|
||||
@ -103,6 +105,16 @@ 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) {
|
||||
cart.coupon = coupon;
|
||||
setCart({ ...cart, coupon });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="store-cart">
|
||||
{cart && cartIsEmpty() && <p>{t('app.public.store_cart.cart_is_empty')}</p>}
|
||||
@ -122,8 +134,10 @@ 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() && cart.user && <CouponInput user={cart.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 defaultUser={cart.user} onSelected={handleChangeMember} />}
|
||||
{cart && !cartIsEmpty() &&
|
||||
<FabButton className="checkout-btn" onClick={checkout} disabled={!cart.user || cart.order_items_attributes.length === 0}>
|
||||
|
@ -9,7 +9,7 @@ import { User } from '../../models/user';
|
||||
interface CouponInputProps {
|
||||
amount: number,
|
||||
user?: User,
|
||||
onChange?: (coupon: Coupon) => void
|
||||
onChange?: (coupon?: Coupon) => void
|
||||
}
|
||||
|
||||
interface Message {
|
||||
@ -58,7 +58,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,7 @@ 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);
|
||||
|
@ -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);
|
||||
|
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;
|
||||
};
|
@ -2,6 +2,7 @@ import { TDateISO } from '../typings/date-iso';
|
||||
import { PaymentConfirmation } from './payment';
|
||||
import { CreateTokenResponse } from './payzen';
|
||||
import { User } from './user';
|
||||
import { Coupon } from './coupon';
|
||||
|
||||
export interface Order {
|
||||
id: number,
|
||||
@ -13,6 +14,7 @@ export interface Order {
|
||||
state?: string,
|
||||
payment_state?: string,
|
||||
total?: number,
|
||||
coupon?: Coupon,
|
||||
created_at?: TDateISO,
|
||||
order_items_attributes: Array<{
|
||||
id: number,
|
||||
|
@ -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
|
||||
|
@ -4,6 +4,7 @@
|
||||
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
|
||||
|
||||
@ -17,6 +18,8 @@ class Order < PaymentDocument
|
||||
|
||||
before_create :add_environment
|
||||
|
||||
delegate :user, to: :statistic_profile
|
||||
|
||||
def footprint_children
|
||||
order_items
|
||||
end
|
||||
|
@ -6,31 +6,33 @@ class Checkout::PaymentService
|
||||
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)
|
||||
|
||||
CouponService.new.validate(coupon_code, order.statistic_profile.user)
|
||||
|
||||
amount = debit_amount(order)
|
||||
if operator.privileged? || amount.zero?
|
||||
Payments::LocalService.new.payment(order)
|
||||
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
|
||||
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
|
||||
|
@ -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,14 +9,17 @@ 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 = '', payment_id = nil, payment_type = nil)
|
||||
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.operator_profile_id = order.statistic_profile.user.invoicing_profile.id if order.operator_profile.nil?
|
||||
order.payment_method = if order.total == order.wallet_amount
|
||||
|
@ -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,12 +23,12 @@ 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', payment_id, 'PayZen::Order')
|
||||
o = payment_success(order, coupon_code, 'card', payment_id, 'PayZen::Order')
|
||||
{ order: o }
|
||||
else
|
||||
order.update(payment_state: 'failed')
|
||||
|
@ -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', intent.id, intent.class.name)
|
||||
o = payment_success(order, coupon_code, 'card', intent.id, intent.class.name)
|
||||
return { order: o }
|
||||
end
|
||||
|
||||
@ -33,10 +33,10 @@ 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', intent.id, intent.class.name)
|
||||
o = payment_success(order, coupon_code, 'card', intent.id, intent.class.name)
|
||||
{ order: o }
|
||||
else
|
||||
order.update(payment_state: 'failed')
|
||||
|
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_133518) 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"
|
||||
@ -473,6 +473,9 @@ ActiveRecord::Schema.define(version: 2022_08_26_133518) do
|
||||
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
|
||||
@ -1167,6 +1170,7 @@ ActiveRecord::Schema.define(version: 2022_08_26_133518) 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…
x
Reference in New Issue
Block a user