1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

(feat) save both operator/customer for orders

This commit is contained in:
Sylvain 2022-12-30 17:52:28 +01:00
parent 48a44ff4f3
commit bfe0936b40
18 changed files with 212 additions and 128 deletions

View File

@ -141,6 +141,7 @@
- Updated tiptap editor and its dependencies to 2.0.0-beta.204
- [TODO DEPLOY] `rails db:seed`
- [TODO DEPLOY] `rails fablab:setup:build_accounting_lines`
- [TODO DEPLOY] `rails fablab:fix:cart_operator`
## v5.5.8 2022 December 16

View File

@ -60,6 +60,13 @@ class API::CartController < API::ApiController
render json: @order_errors
end
def set_customer
authorize @current_order, policy_class: CartPolicy
customer = User.find(params[:user_id])
@order = Cart::SetCustomerService.new(current_user).call(@current_order, customer)
render 'api/orders/show'
end
private
def orderable

View File

@ -24,6 +24,8 @@ class API::CheckoutController < API::ApiController
rescue PayzenError => e
render json: PayZen::Helper.human_error(e), status: :unprocessable_entity
rescue StandardError => e
Rails.logger.error e
Rails.logger.debug e.backtrace
render json: e, status: :unprocessable_entity
end

View File

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

View File

@ -182,14 +182,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
* Change cart's customer by admin/manger
*/
const handleChangeMember = (user: User): void => {
// if the selected user is the operator, he cannot offer products to himself
if (user.id === currentUser.id && cart.order_items_attributes.filter(item => item.is_offered).length > 0) {
Promise.all(cart.order_items_attributes.filter(item => item.is_offered).map(item => {
return CartAPI.setOffer(cart, item.orderable_id, item.orderable_type, false);
})).then((data) => setCart({ ...data[data.length - 1], user: { id: user.id, role: user.role } }));
} else {
setCart({ ...cart, user: { id: user.id, role: user.role } });
}
CartAPI.setCustomer(cart, user.id).then(setCart).catch(onError);
};
/**

View File

@ -31,6 +31,9 @@ export const MemberSelect: React.FC<MemberSelectProps> = ({ defaultUser, value,
if (!defaultUser && option) {
onSelected({ id: option.value, name: option.label });
}
if (!option && defaultUser) {
setOption({ value: defaultUser.id, label: defaultUser.name });
}
}, [defaultUser]);
useEffect(() => {
@ -77,6 +80,7 @@ export const MemberSelect: React.FC<MemberSelectProps> = ({ defaultUser, value,
defaultOptions
onChange={onChange}
value={option}
defaultInputValue={defaultUser?.name}
/>
</div>
);

View File

@ -17,4 +17,8 @@ class CartPolicy < ApplicationPolicy
def set_offer?
!record.is_offered || (user.privileged? && record.customer_id != user.id)
end
def set_customer?
user.privileged?
end
end

View File

@ -2,140 +2,105 @@
# Provides methods for find or create a cart
class Cart::FindOrCreateService
def initialize(user)
@user = user
def initialize(operator = nil, customer = nil)
@operator = operator
@customer = customer
@customer = operator if @customer.nil? && operator&.member?
end
def call(order_token)
@order = Order.find_by(token: order_token, state: 'cart')
check_order_authorization
set_last_cart_if_user_login if @order.nil?
@order = last_cart if @order.nil?
check_operator_authorization
# find an existing order (and update it)
if @order
if @order.order_items.count.zero? && @user && ((@user.member? && @order.statistic_profile_id.nil?) || (@user.privileged? && @order.operator_profile_id.nil?))
set_last_order_if_anonymous_order_s_items_is_empty_after_user_login
end
clean_old_cart if @user
@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?
set_last_order_after_login
clean_old_cart
@order.update(statistic_profile_id: @customer.statistic_profile.id) if @order.statistic_profile_id.nil? && !@customer.nil?
@order.update(operator_profile_id: @operator.invoicing_profile.id) if @order.operator_profile_id.nil? && !@operator.nil?
Cart::UpdateTotalService.new.call(@order)
return @order
end
# OR create a new order
token = GenerateTokenService.new.call(Order)
order_param = {
token: token,
state: 'cart',
total: 0
total: 0,
statistic_profile_id: @customer&.statistic_profile&.id,
operator_profile_id: @operator&.invoicing_profile&.id
}
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
# This function check current order that
# 1. belongs current user
# 2. has belonged an user but this user dont login
# 3. created date > last paid order of user
# if not, set current order = nil
# This function check the access rights for the currently set order.
# If the rights are not validated, set the current order to nil
def check_order_authorization
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? || @order.operator_profile_id.present?)
if @order && @order.statistic_profile_id.present? && Order.where(statistic_profile_id: @order.statistic_profile_id,
state: 'paid').where('created_at > ?', @order.created_at).last.present?
@order = nil
end
if @order && @order.operator_profile_id.present? && Order.where(operator_profile_id: @order.operator_profile_id,
state: 'paid').where('created_at > ?', @order.created_at).last.present?
# order belongs to the current user
@order = nil if belongs_to_another?(@order, @customer) && !@operator&.privileged?
# order has belonged to an user, but this user is not logged-in
@order = nil if !@operator && @order&.statistic_profile_id&.present?
# order creation date is before than the last paid order of the user
if @order&.statistic_profile_id&.present? && Order.where(statistic_profile_id: @order&.statistic_profile_id, state: 'paid')
.where('created_at > ?', @order&.created_at).last.present?
@order = nil
end
end
# set user last cart of user when login
def set_last_cart_if_user_login
if @user&.member?
last_paid_order = Order.where(statistic_profile_id: @user.statistic_profile.id,
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,
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
# Check that the current operator is allowed to operate on the current order
def check_operator_authorization
return if @order&.operator_profile_id.nil?
@order = nil if @order&.operator_profile_id != @operator&.invoicing_profile&.id
end
def belongs_to_another?(order, user)
order&.statistic_profile_id.present? && order&.statistic_profile_id != user&.statistic_profile&.id
end
# retrieve the last cart of the current user
def last_cart
return if @customer.nil?
last_paid_order = Order.where(statistic_profile_id: @customer&.statistic_profile&.id, state: 'paid')
.last
if last_paid_order
Order.where(statistic_profile_id: @customer&.statistic_profile&.id, state: 'cart')
.where('created_at > ?', last_paid_order.created_at)
.last
else
Order.where(statistic_profile_id: @customer&.statistic_profile&.id, state: 'cart')
.last
end
end
# set last order if current cart is anoymous and user is login
def set_last_order_if_anonymous_order_s_items_is_empty_after_user_login
last_unpaid_order = nil
if @user&.member?
last_paid_order = Order.where(statistic_profile_id: @user.statistic_profile.id,
state: 'paid').last
last_unpaid_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,
state: 'paid').last
last_unpaid_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
if last_unpaid_order && last_unpaid_order.id != @order.id
@order.destroy
@order = last_unpaid_order
end
# Check if the provided order/cart is empty AND anonymous
def empty_and_anonymous?(order)
order&.order_items&.count&.zero? && (order&.operator_profile_id.nil? || order&.statistic_profile_id.nil?)
end
# If the current cart is empty and anonymous, set the current cart as the last unpaid order.
# This is relevant after the user has logged-in
def set_last_order_after_login
return unless empty_and_anonymous?(@order)
last_unpaid_order = last_cart
return if last_unpaid_order.nil? || last_unpaid_order.id == @order&.id
@order&.destroy
@order = last_unpaid_order
end
# delete all old cart if last cart of user isnt empty
# keep every user only one cart
def clean_old_cart
if @user&.member?
Order.where(statistic_profile_id: @user.statistic_profile.id, state: 'cart')
.where.not(id: @order.id)
.destroy_all
end
if @user&.privileged?
Order.where(operator_profile_id: @user.invoicing_profile.id, state: 'cart')
.where.not(id: @order.id)
.destroy_all
end
end
return if @customer.nil?
# delete all empty cart if last cart of user isnt empty
def clean_empty_cart
if @user&.member?
Order.where(statistic_profile_id: @user.statistic_profile.id, state: 'cart')
.where('(SELECT COUNT(*) FROM order_items WHERE order_items.order_id = orders.id) = 0')
.destroy_all
end
if @user&.privileged?
Order.where(operator_profile_id: @user.invoicing_profile.id, state: 'cart')
.where('(SELECT COUNT(*) FROM order_items WHERE order_items.order_id = orders.id) = 0')
.destroy_all
end
Order.where(statistic_profile_id: @customer&.statistic_profile&.id, state: 'cart')
.where.not(id: @order&.id)
.destroy_all
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
# module definition
module Cart; end
# Provides methods to update the customer of the given cart
class Cart::SetCustomerService
def initialize(operator)
@operator = operator
end
def call(order, customer)
return order unless @operator.privileged?
return order unless order.operator_profile_id.blank? || order.operator_profile_id == @operator.invoicing_profile.id
return order unless order.state == 'cart'
ActiveRecord::Base.transaction do
order.statistic_profile_id = customer.statistic_profile.id
order.operator_profile_id = @operator.invoicing_profile.id
unset_offer(order, customer)
Cart::UpdateTotalService.new.call(order)
order.save
end
order.reload
end
# If the operator is also the customer, he cannot offer items to himself, so we unset all the offers
def unset_offer(order, customer)
return unless @operator == customer
order.order_items.each do |item|
item.is_offered = false
item.save
end
end
end

View File

@ -25,7 +25,7 @@ class Checkout::PaymentService
elsif PayZen::Helper.enabled?
Payments::PayzenService.new.payment(order, coupon_code)
else
raise Error('Bad gateway or online payment is disabled')
raise PaymentGatewayError, 'Bad gateway or online payment is disabled'
end
end
@ -37,7 +37,7 @@ class Checkout::PaymentService
elsif PayZen::Helper.enabled?
Payments::PayzenService.new.confirm_payment(order, coupon_code, payment_id)
else
raise Error('Bad gateway or online payment is disabled')
raise PaymentGatewayError, 'Bad gateway or online payment is disabled'
end
end
end

View File

@ -42,7 +42,7 @@ class Members::ListService
end
def search(current_user, query, subscription)
members = User.includes(:profile, :statistic_profile)
members = User.includes(:profile, :statistic_profile, invoicing_profile: [:address])
.joins(:profile,
:statistic_profile,
:roles,
@ -60,7 +60,6 @@ class Members::ListService
search: word)
end
if current_user.member?
# non-admin can only retrieve users with "public profiles"
members = members.where("users.is_allow_contact = 'true'")

View File

@ -81,10 +81,14 @@ class ProductService
pi = new_product.product_images.build
pi.is_main = image.is_main
pi.attachment = File.open(image.attachment.file.file)
rescue Errno::ENOENT => e
Rails.logger.warn "Unable to clone product image: #{e}"
end
product.product_files.each do |file|
pf = new_product.product_files.build
pf.attachment = File.open(file.attachment.file.file)
rescue Errno::ENOENT => e
Rails.logger.warn "Unable to clone product file: #{e}"
end
new_product
end

View File

@ -169,6 +169,7 @@ Rails.application.routes.draw do
put 'refresh_item', on: :collection
post 'validate', on: :collection
post 'create_item', on: :collection
put 'set_customer', on: :collection
end
resources :checkout, only: %i[] do
post 'payment', on: :collection

View File

@ -295,4 +295,14 @@ namespace :fablab do
Rake::Task['fablab:chain:invoices_items'].invoke
end
end
desc '[release 5.7] fix operator of self-bought carts'
task cart_operator: :environment do |_task, _args|
Order.where.not(statistic_profile_id: nil).find_each do |order|
order.update(operator_profile_id: order.user&.invoicing_profile&.id)
end
Order.where.not(operator_profile_id: nil).find_each do |order|
order.update(statistic_profile_id: order.operator_profile&.user&.statistic_profile&.id)
end
end
end

View File

@ -163,7 +163,7 @@ order_15:
order_16:
id: 16
statistic_profile_id: 10
operator_profile_id:
operator_profile_id: 10
token: 9VWkmJDSx7QixRusL7ppWg1666628033284
reference: '005901-<%= DateTime.current.utc.strftime('%m') %>-<%= DateTime.current.utc.strftime('%d') %>'
state: cart
@ -199,7 +199,7 @@ order_17:
order_18:
id: 18
statistic_profile_id: 9
operator_profile_id:
operator_profile_id: 9
token: KbSmmD_gi9w_CrpwtK9OwA1666687433963
reference: '005902-<%= DateTime.current.utc.strftime('%m') %>-<%= DateTime.current.utc.strftime('%d') %>'
state: cart
@ -234,7 +234,7 @@ order_19:
invoice_id:
order_20:
id: 20
statistic_profile_id:
statistic_profile_id: 1
operator_profile_id: 1
token: 0DKxbAOzSXRx-amXyhmDdg1666691976019
reference: '005904-<%= DateTime.current.utc.strftime('%m') %>-<%= DateTime.current.utc.strftime('%d') %>'

View File

@ -11,6 +11,7 @@ class Store::AdminPayOrderTest < ActionDispatch::IntegrationTest
@caisse_en_bois = Product.find_by(slug: 'caisse-en-bois')
@panneaux = Product.find_by(slug: 'panneaux-de-mdf')
@cart1 = Order.find_by(token: '0DKxbAOzSXRx-amXyhmDdg1666691976019')
Cart::SetCustomerService.new(@admin).call(@cart1, @pjproudhon)
end
test 'admin pay user order by local with success' do

View File

@ -9,7 +9,7 @@ class Cart::FindOrCreateServiceTest < ActiveSupport::TestCase
@admin = User.find_by(username: 'admin')
end
test 'anoymous user create a new cart' do
test 'anonymous user create a new cart' do
cart = Cart::FindOrCreateService.new(nil).call(nil)
assert_equal cart.state, 'cart'
assert_equal cart.total, 0
@ -22,7 +22,7 @@ class Cart::FindOrCreateServiceTest < ActiveSupport::TestCase
assert_equal cart.state, 'cart'
assert_equal cart.statistic_profile_id, @jdupond.statistic_profile.id
assert_equal cart.total, 0
assert_nil cart.operator_profile_id
assert_equal cart.operator_profile_id, @jdupond.invoicing_profile.id
assert_equal Order.where(statistic_profile_id: @jdupond.statistic_profile.id, state: 'cart').count, 1
end
@ -39,20 +39,20 @@ class Cart::FindOrCreateServiceTest < ActiveSupport::TestCase
assert_equal cart.token, '9VWkmJDSx7QixRusL7ppWg1666628033284'
end
test 'cannot get cart of other user by token' do
test 'cannot get cart of other user by token but last user cart is returned instead' do
cart = Cart::FindOrCreateService.new(@jdupond).call('9VWkmJDSx7QixRusL7ppWg1666628033284')
assert_not_equal cart.token, '9VWkmJDSx7QixRusL7ppWg1666628033284'
assert_equal cart.state, 'cart'
assert_equal cart.total, 0
assert_nil cart.operator_profile_id
assert_not_equal cart.token, '9VWkmJDSx7QixRusL7ppWg1666628033284'
assert_equal cart.operator_profile_id, @jdupond.invoicing_profile.id
end
test 'migrate a cart to user' do
test 'migrate an anonymous cart to a newly logged user' do
cart = Cart::FindOrCreateService.new(@jdupond).call('MkI5z9qVxe_YdNYCR_WN6g1666628074732')
assert_equal cart.state, 'cart'
assert_equal cart.total, 0
assert_equal cart.statistic_profile_id, @jdupond.statistic_profile.id
assert_nil cart.operator_profile_id
assert_equal cart.operator_profile_id, @jdupond.invoicing_profile.id
assert_equal Order.where(statistic_profile_id: @jdupond.statistic_profile.id, state: 'cart').count, 1
end
@ -62,17 +62,33 @@ class Cart::FindOrCreateServiceTest < ActiveSupport::TestCase
assert_equal cart.state, 'cart'
assert_equal cart.total, 0
assert_equal cart.statistic_profile_id, @acamus.statistic_profile.id
assert_nil cart.operator_profile_id
assert_equal cart.operator_profile_id, @acamus.invoicing_profile.id
assert_equal Order.where(statistic_profile_id: @acamus.statistic_profile.id, state: 'cart').count, 1
assert_nil Order.find_by(token: 'MkI5z9qVxe_YdNYCR_WN6g1666628074732')
end
test 'admin get a cart' do
cart = Cart::FindOrCreateService.new(@admin).call(nil)
test 'admin get a cart for himself' do
cart = Cart::FindOrCreateService.new(@admin, @admin).call(nil)
assert_equal cart.state, 'cart'
assert_equal cart.total, 262_500
assert_equal cart.operator_profile_id, @admin.invoicing_profile.id
assert_nil cart.statistic_profile_id
assert_equal cart.statistic_profile_id, @admin.statistic_profile.id
assert_equal Order.where(operator_profile_id: @admin.invoicing_profile.id, state: 'cart').count, 1
end
test 'admin create new cart for a member' do
cart = Cart::FindOrCreateService.new(@admin, @acamus).call(nil)
assert_not_nil cart
assert_equal cart.statistic_profile_id, @acamus.statistic_profile.id
assert_equal cart.operator_profile_id, @admin.invoicing_profile.id
assert_equal 'cart', cart.state
assert_equal 0, cart.total
end
test 'admin create new cart for a member then get it' do
cart = Cart::FindOrCreateService.new(@admin, @acamus).call(nil)
cart2 = Cart::FindOrCreateService.new(@admin).call(cart.token)
assert_not_nil cart2
assert_equal cart.token, cart2.token
end
end

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
require 'test_helper'
class SetCustomerServiceTest < ActiveSupport::TestCase
setup do
@admin = User.find_by(username: 'admin')
@pjproudhon = User.find_by(username: 'pjproudhon')
end
test 'admin update the customer of an anonymous cart' do
order = Order.find_by(token: '4bB96D-MlqJGBr5T8eui-g1666690417460')
service = Cart::SetCustomerService.new(@admin)
service.call(order, @pjproudhon)
assert_equal @pjproudhon, order.user
assert_equal @admin, order.operator_profile.user
end
test 'admin cannot update the customer of a paid cart' do
order = Order.find_by(token: 'ttG9U892Bu0gbu8OnJkwTw1664892253183')
service = Cart::SetCustomerService.new(@admin)
service.call(order, @pjproudhon)
assert_not_equal @pjproudhon, order.user
assert_not_equal @admin, order.operator_profile.user
end
test 'member cannot update the customer himself' do
order = Order.find_by(token: '4bB96D-MlqJGBr5T8eui-g1666690417460')
service = Cart::SetCustomerService.new(@pjproudhon)
service.call(order, @pjproudhon)
assert_nil order.statistic_profile_id
assert_nil order.operator_profile_id
end
end