diff --git a/app/controllers/api/cart_controller.rb b/app/controllers/api/cart_controller.rb index 95f30fe1e..14f3becb5 100644 --- a/app/controllers/api/cart_controller.rb +++ b/app/controllers/api/cart_controller.rb @@ -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 diff --git a/app/controllers/api/checkout_controller.rb b/app/controllers/api/checkout_controller.rb index 3aeb661d4..05e5faa84 100644 --- a/app/controllers/api/checkout_controller.rb +++ b/app/controllers/api/checkout_controller.rb @@ -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 diff --git a/app/frontend/src/javascript/api/cart.ts b/app/frontend/src/javascript/api/cart.ts index e3f0ebd16..4601de322 100644 --- a/app/frontend/src/javascript/api/cart.ts +++ b/app/frontend/src/javascript/api/cart.ts @@ -22,9 +22,4 @@ export default class CartAPI { const res: AxiosResponse = 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 { - const res: AxiosResponse = await apiClient.put('/api/cart/set_customer', { order_token: order.token, user_id: userId }); - return res?.data; - } } diff --git a/app/frontend/src/javascript/api/checkout.ts b/app/frontend/src/javascript/api/checkout.ts index ed9c41bb8..0a7e8f34a 100644 --- a/app/frontend/src/javascript/api/checkout.ts +++ b/app/frontend/src/javascript/api/checkout.ts @@ -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 { + static async payment (order: Order, paymentId?: string): Promise { const res: AxiosResponse = 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 { + static async confirmPayment (order: Order, paymentId: string): Promise { const res: AxiosResponse = 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; } diff --git a/app/frontend/src/javascript/components/cart/store-cart.tsx b/app/frontend/src/javascript/components/cart/store-cart.tsx index aaf540f22..8e621741e 100644 --- a/app/frontend/src/javascript/components/cart/store-cart.tsx +++ b/app/frontend/src/javascript/components/cart/store-cart.tsx @@ -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 = ({ onError, currentUser }) => { +const StoreCart: React.FC = ({ onError, currentUser, userLogin }) => { const { t } = useTranslation('public'); const { cart, setCart } = useCart(currentUser); @@ -58,7 +61,11 @@ const StoreCart: React.FC = ({ onError, currentUser }) => { * Checkout cart */ const checkout = () => { - setPaymentModal(true); + if (!currentUser) { + userLogin(); + } else { + setPaymentModal(true); + } }; /** @@ -84,9 +91,7 @@ const StoreCart: React.FC = ({ 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 = ({ 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 (
{cart && cartIsEmpty() &&

{t('app.public.store_cart.cart_is_empty')}

} @@ -122,11 +136,13 @@ const StoreCart: React.FC = ({ onError, currentUser }) => {
))} - {cart && !cartIsEmpty() && } - {cart && !cartIsEmpty() &&

Totale: {FormatLib.price(cart.total)}

} - {cart && !cartIsEmpty() && isPrivileged() && } + {cart && !cartIsEmpty() && cart.user && } + {cart && !cartIsEmpty() &&

Total produits: {FormatLib.price(cart.total)}

} + {cart && !cartIsEmpty() && cart.coupon && computePriceWithCoupon(cart.total, cart.coupon) !== cart.total &&

Coupon réduction: {FormatLib.price(-(cart.total - computePriceWithCoupon(cart.total, cart.coupon)))}

} + {cart && !cartIsEmpty() &&

Total panier: {FormatLib.price(computePriceWithCoupon(cart.total, cart.coupon))}

} + {cart && !cartIsEmpty() && isPrivileged() && } {cart && !cartIsEmpty() && - + {t('app.public.store_cart.checkout')} } @@ -138,7 +154,7 @@ const StoreCart: React.FC = ({ 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'} /> } @@ -153,4 +169,4 @@ const StoreCartWrapper: React.FC = (props) => { ); }; -Application.Components.component('storeCart', react2angular(StoreCartWrapper, ['onError', 'currentUser'])); +Application.Components.component('storeCart', react2angular(StoreCartWrapper, ['onError', 'currentUser', 'userLogin'])); diff --git a/app/frontend/src/javascript/components/coupon/coupon-input.tsx b/app/frontend/src/javascript/components/coupon/coupon-input.tsx index 1e147e0ad..d3fb917a2 100644 --- a/app/frontend/src/javascript/components/coupon/coupon-input.tsx +++ b/app/frontend/src/javascript/components/coupon/coupon-input.tsx @@ -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 = ({ 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 = ({ 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); } }; diff --git a/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx b/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx index 5eab0f329..d6e9c1f83 100644 --- a/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx +++ b/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx @@ -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 = ({ 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); diff --git a/app/frontend/src/javascript/components/payment/local-payment/local-payment-form.tsx b/app/frontend/src/javascript/components/payment/local-payment/local-payment-form.tsx index 9119a8b9d..61edf3180 100644 --- a/app/frontend/src/javascript/components/payment/local-payment/local-payment-form.tsx +++ b/app/frontend/src/javascript/components/payment/local-payment/local-payment-form.tsx @@ -89,7 +89,8 @@ export const LocalPaymentForm: React.FC = ({ 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 = ({ 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]; }; diff --git a/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx b/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx index fb53a57e7..09a0a9ef9 100644 --- a/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx +++ b/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx @@ -58,7 +58,7 @@ export const PayzenForm: React.FC = ({ 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 = ({ 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); diff --git a/app/frontend/src/javascript/components/payment/stripe/payment-modal.tsx b/app/frontend/src/javascript/components/payment/stripe/payment-modal.tsx index f21c7c9b7..17c625343 100644 --- a/app/frontend/src/javascript/components/payment/stripe/payment-modal.tsx +++ b/app/frontend/src/javascript/components/payment/stripe/payment-modal.tsx @@ -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 = ({ 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); diff --git a/app/frontend/src/javascript/components/payment/stripe/stripe-form.tsx b/app/frontend/src/javascript/components/payment/stripe/stripe-form.tsx index 3ea6fda3b..826246764 100644 --- a/app/frontend/src/javascript/components/payment/stripe/stripe-form.tsx +++ b/app/frontend/src/javascript/components/payment/stripe/stripe-form.tsx @@ -44,7 +44,7 @@ export const StripeForm: React.FC = ({ 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 = ({ 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); diff --git a/app/frontend/src/javascript/controllers/cart.js b/app/frontend/src/javascript/controllers/cart.js index e8e753d14..e69162c33 100644 --- a/app/frontend/src/javascript/controllers/cart.js +++ b/app/frontend/src/javascript/controllers/cart.js @@ -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 */ diff --git a/app/frontend/src/javascript/lib/coupon.ts b/app/frontend/src/javascript/lib/coupon.ts new file mode 100644 index 000000000..7dd8da184 --- /dev/null +++ b/app/frontend/src/javascript/lib/coupon.ts @@ -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; +}; diff --git a/app/frontend/src/javascript/lib/format.ts b/app/frontend/src/javascript/lib/format.ts index 16337d780..3b6b608e6 100644 --- a/app/frontend/src/javascript/lib/format.ts +++ b/app/frontend/src/javascript/lib/format.ts @@ -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; + }; } diff --git a/app/frontend/src/javascript/models/order.ts b/app/frontend/src/javascript/models/order.ts index 80d6d239e..cd2a9d991 100644 --- a/app/frontend/src/javascript/models/order.ts +++ b/app/frontend/src/javascript/models/order.ts @@ -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, diff --git a/app/frontend/templates/cart/index.html b/app/frontend/templates/cart/index.html index 75c73bbed..3fedbe771 100644 --- a/app/frontend/templates/cart/index.html +++ b/app/frontend/templates/cart/index.html @@ -15,5 +15,5 @@
- +
diff --git a/app/models/coupon.rb b/app/models/coupon.rb index 744ad35ba..623c8623e 100644 --- a/app/models/coupon.rb +++ b/app/models/coupon.rb @@ -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 diff --git a/app/models/order.rb b/app/models/order.rb index 8ea21a646..898195af6 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -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 diff --git a/app/models/payment_gateway_object.rb b/app/models/payment_gateway_object.rb index c5b976ac1..fa3ef7f58 100644 --- a/app/models/payment_gateway_object.rb +++ b/app/models/payment_gateway_object.rb @@ -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 diff --git a/app/models/payment_schedule.rb b/app/models/payment_schedule.rb index 11646e8ff..c6b0c7587 100644 --- a/app/models/payment_schedule.rb +++ b/app/models/payment_schedule.rb @@ -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 diff --git a/app/policies/cart_policy.rb b/app/policies/cart_policy.rb index 401622563..0aad87849 100644 --- a/app/policies/cart_policy.rb +++ b/app/policies/cart_policy.rb @@ -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 diff --git a/app/policies/checkout_policy.rb b/app/policies/checkout_policy.rb new file mode 100644 index 000000000..045361caf --- /dev/null +++ b/app/policies/checkout_policy.rb @@ -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 diff --git a/app/services/cart/create_service.rb b/app/services/cart/create_service.rb deleted file mode 100644 index 996baa73d..000000000 --- a/app/services/cart/create_service.rb +++ /dev/null @@ -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 diff --git a/app/services/cart/find_or_create_service.rb b/app/services/cart/find_or_create_service.rb new file mode 100644 index 000000000..cb48dc159 --- /dev/null +++ b/app/services/cart/find_or_create_service.rb @@ -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 diff --git a/app/services/cart/set_customer_service.rb b/app/services/cart/set_customer_service.rb deleted file mode 100644 index 2cdc21e15..000000000 --- a/app/services/cart/set_customer_service.rb +++ /dev/null @@ -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 diff --git a/app/services/checkout/payment_service.rb b/app/services/checkout/payment_service.rb index ccd021df0..6f6e94216 100644 --- a/app/services/checkout/payment_service.rb +++ b/app/services/checkout/payment_service.rb @@ -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 diff --git a/app/services/payment_document_service.rb b/app/services/payment_document_service.rb index 1590eff3c..99ecda9ea 100644 --- a/app/services/payment_document_service.rb +++ b/app/services/payment_document_service.rb @@ -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]) diff --git a/app/services/payments/local_service.rb b/app/services/payments/local_service.rb index d7912b0af..43bd07d08 100644 --- a/app/services/payments/local_service.rb +++ b/app/services/payments/local_service.rb @@ -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 diff --git a/app/services/payments/payment_concern.rb b/app/services/payments/payment_concern.rb index 9b2a18554..9148cf7ae 100644 --- a/app/services/payments/payment_concern.rb +++ b/app/services/payments/payment_concern.rb @@ -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 diff --git a/app/services/payments/payzen_service.rb b/app/services/payments/payzen_service.rb index 174d328d1..30e1adeac 100644 --- a/app/services/payments/payzen_service.rb +++ b/app/services/payments/payzen_service.rb @@ -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 diff --git a/app/services/payments/stripe_service.rb b/app/services/payments/stripe_service.rb index de1e8773b..c4c8a09b5 100644 --- a/app/services/payments/stripe_service.rb +++ b/app/services/payments/stripe_service.rb @@ -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 diff --git a/app/services/product_service.rb b/app/services/product_service.rb index d930be4b9..811a48322 100644 --- a/app/services/product_service.rb +++ b/app/services/product_service.rb @@ -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, diff --git a/config/routes.rb b/config/routes.rb index 6a190075f..141f84856 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20220826133518_add_footprint_and_environment_to_order.rb b/db/migrate/20220826133518_add_footprint_and_environment_to_order.rb new file mode 100644 index 000000000..966fdc47c --- /dev/null +++ b/db/migrate/20220826133518_add_footprint_and_environment_to_order.rb @@ -0,0 +1,6 @@ +class AddFootprintAndEnvironmentToOrder < ActiveRecord::Migration[5.2] + def change + add_column :orders, :footprint, :string + add_column :orders, :environment, :string + end +end diff --git a/db/migrate/20220826140921_add_coupon_id_to_order.rb b/db/migrate/20220826140921_add_coupon_id_to_order.rb new file mode 100644 index 000000000..cca23c0bd --- /dev/null +++ b/db/migrate/20220826140921_add_coupon_id_to_order.rb @@ -0,0 +1,5 @@ +class AddCouponIdToOrder < ActiveRecord::Migration[5.2] + def change + add_reference :orders, :coupon, index: true, foreign_key: true + end +end diff --git a/db/migrate/20220826175129_add_paid_total_to_order.rb b/db/migrate/20220826175129_add_paid_total_to_order.rb new file mode 100644 index 000000000..5681a3ed5 --- /dev/null +++ b/db/migrate/20220826175129_add_paid_total_to_order.rb @@ -0,0 +1,5 @@ +class AddPaidTotalToOrder < ActiveRecord::Migration[5.2] + def change + add_column :orders, :paid_total, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 756cdbdb1..e9373c7c0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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"