diff --git a/app/controllers/api/products_controller.rb b/app/controllers/api/products_controller.rb index 97f648fa3..a77d06e67 100644 --- a/app/controllers/api/products_controller.rb +++ b/app/controllers/api/products_controller.rb @@ -40,7 +40,7 @@ class API::ProductsController < API::ApiController def destroy authorize @product - @product.destroy + ProductService.destroy(@product) head :no_content end diff --git a/app/exceptions/cannot_delete_product_error.rb b/app/exceptions/cannot_delete_product_error.rb new file mode 100644 index 000000000..f4e2eec80 --- /dev/null +++ b/app/exceptions/cannot_delete_product_error.rb @@ -0,0 +1,3 @@ +# Raised when delete a product if this product has used in order +class CannotDeleteProductError < StandardError +end diff --git a/app/exceptions/cart/item_amount_error.rb b/app/exceptions/cart/item_amount_error.rb new file mode 100644 index 000000000..9085d68ce --- /dev/null +++ b/app/exceptions/cart/item_amount_error.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# Raised when the item's amount != product's amount +class Cart::ItemAmountError < StandardError +end diff --git a/app/exceptions/cart/quantity_min_error.rb b/app/exceptions/cart/quantity_min_error.rb new file mode 100644 index 000000000..f34e4c1b2 --- /dev/null +++ b/app/exceptions/cart/quantity_min_error.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# Raised when the item's quantity < product's quantity min +class Cart::QuantityMinError < StandardError +end diff --git a/app/frontend/src/javascript/components/form/form-input.tsx b/app/frontend/src/javascript/components/form/form-input.tsx index 31b04bec1..78f15d801 100644 --- a/app/frontend/src/javascript/components/form/form-input.tsx +++ b/app/frontend/src/javascript/components/form/form-input.tsx @@ -59,7 +59,7 @@ export const FormInput = ({ id, re {...register(id as FieldPath, { ...rules, valueAsDate: type === 'date', - setValueAs: v => (v === null && nullable) ? null : (type === 'number' ? parseInt(v, 10) : v), + setValueAs: v => (v === null && nullable) ? null : (type === 'number' ? parseFloat(v) : v), value: defaultValue as FieldPathValue>, onChange: (e) => { handleChange(e); } })} diff --git a/app/frontend/src/javascript/components/store/product-form.tsx b/app/frontend/src/javascript/components/store/product-form.tsx index 6bb1a36d6..07296e1be 100644 --- a/app/frontend/src/javascript/components/store/product-form.tsx +++ b/app/frontend/src/javascript/components/store/product-form.tsx @@ -280,7 +280,7 @@ export const ProductForm: React.FC = ({ product, title, onSucc diff --git a/app/frontend/src/javascript/components/store/product-item.tsx b/app/frontend/src/javascript/components/store/product-item.tsx index 177f460e7..c37f97dc1 100644 --- a/app/frontend/src/javascript/components/store/product-item.tsx +++ b/app/frontend/src/javascript/components/store/product-item.tsx @@ -79,12 +79,10 @@ export const ProductItem: React.FC = ({ product, onEdit, onDel {t('app.admin.store.product_item.stock.external')}

{product.stock.external}

- {product.amount && -
-

{FormatLib.price(product.amount)}

- / {t('app.admin.store.product_item.unit')} -
- } +
+

{FormatLib.price(product.amount || 0)}

+ / {t('app.admin.store.product_item.unit')} +
diff --git a/app/frontend/src/javascript/components/store/store-product-item.tsx b/app/frontend/src/javascript/components/store/store-product-item.tsx index fef8d883a..321dc4b1e 100644 --- a/app/frontend/src/javascript/components/store/store-product-item.tsx +++ b/app/frontend/src/javascript/components/store/store-product-item.tsx @@ -81,12 +81,10 @@ export const StoreProductItem: React.FC = ({ product, car

{product.name}

- {product.amount && -
-

{FormatLib.price(product.amount)}

- / {t('app.public.store_product_item.unit')} -
- } +
+

{FormatLib.price(product.amount || 0)}

+ / {t('app.public.store_product_item.unit')} +
{productStockStatus(product)} diff --git a/app/frontend/src/javascript/components/store/store-product.tsx b/app/frontend/src/javascript/components/store/store-product.tsx index ac02b0931..4ebae268a 100644 --- a/app/frontend/src/javascript/components/store/store-product.tsx +++ b/app/frontend/src/javascript/components/store/store-product.tsx @@ -180,7 +180,7 @@ export const StoreProduct: React.FC = ({ productSlug, current {productStockStatus(product)}
-

{FormatLib.price(product.amount)} TTC

+

{FormatLib.price(product.amount || 0)} TTC

/ {t('app.public.store_product_item.unit')}
{product.stock.external > (product.quantity_min || 1) && diff --git a/app/models/product.rb b/app/models/product.rb index 682a3f8fe..37f5d1a04 100644 --- a/app/models/product.rb +++ b/app/models/product.rb @@ -20,7 +20,8 @@ class Product < ApplicationRecord accepts_nested_attributes_for :product_stock_movements, allow_destroy: true, reject_if: :all_blank validates :name, :slug, presence: true - validates :amount, numericality: { greater_than: 0, allow_nil: true } + validates :slug, uniqueness: true + validates :amount, numericality: { greater_than_or_equal_to: 0, allow_nil: true } scope :active, -> { where(is_active: true) } diff --git a/app/models/product_category.rb b/app/models/product_category.rb index 40af30a1a..afe4595ae 100644 --- a/app/models/product_category.rb +++ b/app/models/product_category.rb @@ -7,6 +7,7 @@ class ProductCategory < ApplicationRecord friendly_id :name, use: :slugged validates :name, :slug, presence: true + validates :slug, uniqueness: true belongs_to :parent, class_name: 'ProductCategory' has_many :children, class_name: 'ProductCategory', foreign_key: :parent_id diff --git a/app/services/cart/add_item_service.rb b/app/services/cart/add_item_service.rb index 31477a180..4fbc4ea3a 100644 --- a/app/services/cart/add_item_service.rb +++ b/app/services/cart/add_item_service.rb @@ -11,7 +11,7 @@ class Cart::AddItemService quantity = orderable.quantity_min > quantity.to_i && item.nil? ? orderable.quantity_min : quantity.to_i if item.nil? - item = order.order_items.new(quantity: quantity, orderable: orderable, amount: orderable.amount) + item = order.order_items.new(quantity: quantity, orderable: orderable, amount: orderable.amount || 0) else item.quantity += quantity.to_i end diff --git a/app/services/checkout/payment_service.rb b/app/services/checkout/payment_service.rb index 470086f9b..429870e5f 100644 --- a/app/services/checkout/payment_service.rb +++ b/app/services/checkout/payment_service.rb @@ -7,9 +7,13 @@ class Checkout::PaymentService include Payments::PaymentConcern def payment(order, operator, coupon_code, payment_id = '') + raise Cart::InactiveProductError unless Orders::OrderService.new.all_products_is_active?(order) + raise Cart::OutStockError unless Orders::OrderService.new.in_stock?(order, 'external') - raise Cart::InactiveProductError unless Orders::OrderService.new.all_products_is_active?(order) + raise Cart::QuantityMinError unless Orders::OrderService.new.greater_than_quantity_min?(order) + + raise Cart::ItemAmountError unless Orders::OrderService.new.item_amount_not_equal?(order) CouponService.new.validate(coupon_code, order.statistic_profile.user.id) diff --git a/app/services/orders/order_service.rb b/app/services/orders/order_service.rb index 622acade9..4b1209131 100644 --- a/app/services/orders/order_service.rb +++ b/app/services/orders/order_service.rb @@ -58,6 +58,20 @@ class Orders::OrderService true end + def greater_than_quantity_min?(order) + order.order_items.each do |item| + return false if item.quantity < item.orderable.quantity_min + end + true + end + + def item_amount_not_equal?(order) + order.order_items.each do |item| + return false if item.amount != item.orderable.amount + end + true + end + def all_products_is_active?(order) order.order_items.each do |item| return false unless item.orderable.is_active diff --git a/app/services/product_service.rb b/app/services/product_service.rb index 38db4eefb..ff7f8432e 100644 --- a/app/services/product_service.rb +++ b/app/services/product_service.rb @@ -55,7 +55,8 @@ class ProductService def create(product_params, stock_movement_params = []) product = Product.new(product_params) product.amount = amount_multiplied_by_hundred(product_params[:amount]) - update(product, product_params, stock_movement_params) + update_stock(product, stock_movement_params) + product end def update(product, product_params, stock_movement_params = []) @@ -64,5 +65,20 @@ class ProductService update_stock(product, stock_movement_params) product end + + def destroy(product) + used_in_order = OrderItem.joins(:order).where.not('orders.state' => 'cart') + .exists?(orderable: product) + raise CannotDeleteProductError if used_in_order + + ActiveRecord::Base.transaction do + orders_with_product = Order.joins(:order_items).where(state: 'cart').where('order_items.orderable': product) + orders_with_product.each do |order| + ::Cart::RemoveItemService.new.call(order, product) + end + + product.destroy + end + end end end