1
0
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:
Du Peng 2022-08-26 20:10:21 +02:00
parent 3a669109b5
commit 981cffa27d
21 changed files with 99 additions and 41 deletions

View File

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

View File

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

View File

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

View File

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

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,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);

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

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

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

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

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

View File

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

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

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,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')

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', 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')

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_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"