From d98082a2c48de91357f6329eba7b2dfa45328047 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Wed, 7 Sep 2022 16:57:07 +0200 Subject: [PATCH 1/9] (fix) cart's operator_id has rename to operator_profile_id --- app/frontend/src/javascript/hooks/use-cart.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/frontend/src/javascript/hooks/use-cart.ts b/app/frontend/src/javascript/hooks/use-cart.ts index a1617b667..88ab53397 100644 --- a/app/frontend/src/javascript/hooks/use-cart.ts +++ b/app/frontend/src/javascript/hooks/use-cart.ts @@ -35,7 +35,7 @@ export default function useCart (user?: User) { }; useEffect(() => { - if (user && cart && (!cart.statistic_profile_id || !cart.operator_id)) { + if (user && cart && (!cart.statistic_profile_id || !cart.operator_profile_id)) { reloadCart(); } }, [user]); From 6d2239bc156a5ac29723edf9a20bf9b36b9ee8e8 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Wed, 7 Sep 2022 17:24:14 +0200 Subject: [PATCH 2/9] (fix) cant pay order with coupon --- app/services/checkout/payment_service.rb | 2 +- app/services/payments/payment_concern.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/checkout/payment_service.rb b/app/services/checkout/payment_service.rb index 6f6e94216..9197de71f 100644 --- a/app/services/checkout/payment_service.rb +++ b/app/services/checkout/payment_service.rb @@ -11,7 +11,7 @@ class Checkout::PaymentService raise Cart::InactiveProductError unless Orders::OrderService.new.all_products_is_active?(order) - CouponService.new.validate(coupon_code, order.statistic_profile.user) + CouponService.new.validate(coupon_code, order.statistic_profile.user.id) amount = debit_amount(order) if operator.privileged? || amount.zero? diff --git a/app/services/payments/payment_concern.rb b/app/services/payments/payment_concern.rb index 5e2686ff9..90ea97334 100644 --- a/app/services/payments/payment_concern.rb +++ b/app/services/payments/payment_concern.rb @@ -10,7 +10,7 @@ module Payments::PaymentConcern end def debit_amount(order, coupon_code = nil) - total = CouponService.new.apply(order.total, coupon_code, order.statistic_profile.user) + total = CouponService.new.apply(order.total, coupon_code, order.statistic_profile.user.id) wallet_debit = get_wallet_debit(order.statistic_profile.user, total) total - wallet_debit end From 45bac88b26f562ca7ba6fd66f251c8af148ac446 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 7 Sep 2022 17:28:41 +0200 Subject: [PATCH 3/9] (quality) refactored categories sorting + fix ts issues --- .../javascript/components/base/fab-modal.tsx | 2 +- .../payment/stripe/stripe-card-update.tsx | 2 +- .../categories/manage-product-category.tsx | 1 - .../store/categories/product-categories.tsx | 14 ++-------- .../components/store/product-form.tsx | 26 +++++-------------- .../components/store/product-stock-form.tsx | 6 ++--- .../components/store/product-stock-modal.tsx | 3 ++- .../javascript/components/store/products.tsx | 14 ++-------- .../components/user/user-profile-form.tsx | 2 +- app/frontend/src/javascript/lib/product.ts | 21 +++++++++++++++ app/frontend/src/javascript/models/product.ts | 6 ++--- 11 files changed, 43 insertions(+), 54 deletions(-) create mode 100644 app/frontend/src/javascript/lib/product.ts diff --git a/app/frontend/src/javascript/components/base/fab-modal.tsx b/app/frontend/src/javascript/components/base/fab-modal.tsx index 3a091f1dc..84fa446d6 100644 --- a/app/frontend/src/javascript/components/base/fab-modal.tsx +++ b/app/frontend/src/javascript/components/base/fab-modal.tsx @@ -42,7 +42,7 @@ export const FabModal: React.FC = ({ title, isOpen, toggleModal, return ( {closeButton && {t('app.shared.fab_modal.close')}} diff --git a/app/frontend/src/javascript/components/payment/stripe/stripe-card-update.tsx b/app/frontend/src/javascript/components/payment/stripe/stripe-card-update.tsx index ca48eaa79..d6fccc677 100644 --- a/app/frontend/src/javascript/components/payment/stripe/stripe-card-update.tsx +++ b/app/frontend/src/javascript/components/payment/stripe/stripe-card-update.tsx @@ -95,7 +95,7 @@ export const StripeCardUpdate: React.FC = ({ onSubmit, on }; return ( -
+ {children} diff --git a/app/frontend/src/javascript/components/store/categories/manage-product-category.tsx b/app/frontend/src/javascript/components/store/categories/manage-product-category.tsx index 71df0ce11..42fdf6161 100644 --- a/app/frontend/src/javascript/components/store/categories/manage-product-category.tsx +++ b/app/frontend/src/javascript/components/store/categories/manage-product-category.tsx @@ -69,7 +69,6 @@ export const ManageProductCategory: React.FC = ({ pr
{ toggleBtn() } = ({ onSuccess, onErro */ const refreshCategories = () => { ProductCategoryAPI.index().then(data => { - // Map product categories by position - const sortedCategories = data - .filter(c => !c.parent_id) - .sort((a, b) => a.position - b.position); - const childrenCategories = data - .filter(c => typeof c.parent_id === 'number') - .sort((a, b) => b.position - a.position); - childrenCategories.forEach(c => { - const parentIndex = sortedCategories.findIndex(i => i.id === c.parent_id); - sortedCategories.splice(parentIndex + 1, 0, c); - }); - setProductCategories(sortedCategories); + setProductCategories(new ProductLib().sortCategories(data)); }).catch((error) => onError(error)); }; diff --git a/app/frontend/src/javascript/components/store/product-form.tsx b/app/frontend/src/javascript/components/store/product-form.tsx index 7839ed8b2..496e50c46 100644 --- a/app/frontend/src/javascript/components/store/product-form.tsx +++ b/app/frontend/src/javascript/components/store/product-form.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { useForm, useWatch } from 'react-hook-form'; +import { SubmitHandler, useForm, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import slugify from 'slugify'; import _ from 'lodash'; @@ -19,6 +19,7 @@ import MachineAPI from '../../api/machine'; import ProductAPI from '../../api/product'; import { Plus } from 'phosphor-react'; import { ProductStockForm } from './product-stock-form'; +import ProductLib from '../../lib/product'; interface ProductFormProps { product: Product, @@ -53,18 +54,7 @@ export const ProductForm: React.FC = ({ product, title, onSucc useEffect(() => { ProductCategoryAPI.index().then(data => { - // Map product categories by position - const sortedCategories = data - .filter(c => !c.parent_id) - .sort((a, b) => a.position - b.position); - const childrenCategories = data - .filter(c => typeof c.parent_id === 'number') - .sort((a, b) => b.position - a.position); - childrenCategories.forEach(c => { - const parentIndex = sortedCategories.findIndex(i => i.id === c.parent_id); - sortedCategories.splice(parentIndex + 1, 0, c); - }); - setProductCategories(buildSelectOptions(sortedCategories)); + setProductCategories(buildSelectOptions(new ProductLib().sortCategories(data))); }).catch(onError); MachineAPI.index({ disabled: false }).then(data => { setMachines(buildChecklistOptions(data)); @@ -111,10 +101,8 @@ export const ProductForm: React.FC = ({ product, title, onSucc /** * Callback triggered when the form is submitted: process with the product creation or update. */ - const onSubmit = (event: React.FormEvent) => { - return handleSubmit((data: Product) => { - saveProduct(data); - })(event); + const onSubmit: SubmitHandler = (data: Product) => { + saveProduct(data); }; /** @@ -236,13 +224,13 @@ export const ProductForm: React.FC = ({ product, title, onSucc {t('app.admin.store.product_form.save')}
-
+

setStockTab(false)}>{t('app.admin.store.product_form.product_parameters')}

setStockTab(true)}>{t('app.admin.store.product_form.stock_management')}

{stockTab - ? + ? :
{ @@ -26,7 +27,7 @@ interface ProductStockFormProps { /** * Form tab to manage a product's stock */ -export const ProductStockForm = ({ product, register, control, formState, onError, onSuccess }: ProductStockFormProps) => { +export const ProductStockForm = ({ product, register, control, formState, onError, onSuccess }: ProductStockFormProps) => { const { t } = useTranslation('admin'); const [activeThreshold, setActiveThreshold] = useState(false); @@ -197,12 +198,11 @@ export const ProductStockForm = ({ produ
- +
); diff --git a/app/frontend/src/javascript/components/store/product-stock-modal.tsx b/app/frontend/src/javascript/components/store/product-stock-modal.tsx index 132f52a87..ba4f465f9 100644 --- a/app/frontend/src/javascript/components/store/product-stock-modal.tsx +++ b/app/frontend/src/javascript/components/store/product-stock-modal.tsx @@ -6,6 +6,7 @@ import { Control, FormState } from 'react-hook-form/dist/types/form'; import { FormSelect } from '../form/form-select'; import { FormInput } from '../form/form-input'; import { FabButton } from '../base/fab-button'; +import { FieldValues } from 'react-hook-form/dist/types/fields'; type selectOption = { value: number, label: string }; @@ -23,7 +24,7 @@ interface ProductStockModalProps { */ // TODO: delete next eslint disable // eslint-disable-next-line @typescript-eslint/no-unused-vars -export const ProductStockModal = ({ product, register, control, formState, onError, onSuccess }: ProductStockModalProps) => { +export const ProductStockModal = ({ product, register, control, formState, onError, onSuccess }: ProductStockModalProps) => { const { t } = useTranslation('admin'); const [movement, setMovement] = useState<'in' | 'out'>('in'); diff --git a/app/frontend/src/javascript/components/store/products.tsx b/app/frontend/src/javascript/components/store/products.tsx index c1ab15dad..f1d9ff010 100644 --- a/app/frontend/src/javascript/components/store/products.tsx +++ b/app/frontend/src/javascript/components/store/products.tsx @@ -14,6 +14,7 @@ import MachineAPI from '../../api/machine'; import { AccordionItem } from './accordion-item'; import { X } from 'phosphor-react'; import { StoreListHeader } from './store-list-header'; +import ProductLib from '../../lib/product'; declare const Application: IApplication; @@ -52,18 +53,7 @@ const Products: React.FC = ({ onSuccess, onError }) => { }); ProductCategoryAPI.index().then(data => { - // Map product categories by position - const sortedCategories = data - .filter(c => !c.parent_id) - .sort((a, b) => a.position - b.position); - const childrenCategories = data - .filter(c => typeof c.parent_id === 'number') - .sort((a, b) => b.position - a.position); - childrenCategories.forEach(c => { - const parentIndex = sortedCategories.findIndex(i => i.id === c.parent_id); - sortedCategories.splice(parentIndex + 1, 0, c); - }); - setProductCategories(sortedCategories); + setProductCategories(new ProductLib().sortCategories(data)); }).catch(onError); MachineAPI.index({ disabled: false }).then(data => { diff --git a/app/frontend/src/javascript/components/user/user-profile-form.tsx b/app/frontend/src/javascript/components/user/user-profile-form.tsx index 1c449c0e5..eae3d01c2 100644 --- a/app/frontend/src/javascript/components/user/user-profile-form.tsx +++ b/app/frontend/src/javascript/components/user/user-profile-form.tsx @@ -179,7 +179,7 @@ export const UserProfileForm: React.FC = ({ action, size, const userNetworks = new UserLib(user).getUserSocialNetworks(); return ( - +
): Array => { + const sortedCategories = categories + .filter(c => !c.parent_id) + .sort((a, b) => a.position - b.position); + const childrenCategories = categories + .filter(c => typeof c.parent_id === 'number') + .sort((a, b) => b.position - a.position); + childrenCategories.forEach(c => { + const parentIndex = sortedCategories.findIndex(i => i.id === c.parent_id); + sortedCategories.splice(parentIndex + 1, 0, c); + }); + return sortedCategories; + }; +} diff --git a/app/frontend/src/javascript/models/product.ts b/app/frontend/src/javascript/models/product.ts index 27665b281..62129dc4e 100644 --- a/app/frontend/src/javascript/models/product.ts +++ b/app/frontend/src/javascript/models/product.ts @@ -16,11 +16,11 @@ export interface Stock { } export interface Product { - id: number, + id?: number, name: string, slug: string, - sku: string, - description: string, + sku?: string, + description?: string, is_active: boolean, product_category_id?: number, amount?: number, From 75b3295f65d1a8daaeac9bce3638543979948fe0 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Wed, 7 Sep 2022 17:52:23 +0200 Subject: [PATCH 4/9] (feat) allow to apply coupon in cart without login --- app/controllers/api/coupons_controller.rb | 14 +++++++------- .../src/javascript/components/cart/store-cart.tsx | 2 +- .../javascript/components/coupon/coupon-input.tsx | 8 +++++++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/controllers/api/coupons_controller.rb b/app/controllers/api/coupons_controller.rb index 3d7b37703..8081a7bf7 100644 --- a/app/controllers/api/coupons_controller.rb +++ b/app/controllers/api/coupons_controller.rb @@ -3,7 +3,7 @@ # API Controller for resources of type Coupon # Coupons are used in payments class API::CouponsController < API::ApiController - before_action :authenticate_user! + before_action :authenticate_user!, except: %i[validate] before_action :set_coupon, only: %i[show update destroy] # Number of notifications added to the page when the user clicks on 'load next notifications' @@ -31,18 +31,18 @@ class API::CouponsController < API::ApiController if @coupon.nil? render json: { status: 'rejected' }, status: :not_found else - _user_id = if !current_user.admin? - current_user.id - else + _user_id = if current_user&.admin? params[:user_id] + else + current_user&.id end amount = params[:amount].to_f * 100.0 status = @coupon.status(_user_id, amount) - if status != 'active' - render json: { status: status }, status: :unprocessable_entity - else + if status == 'active' render :validate, status: :ok, location: @coupon + else + render json: { status: status }, status: :unprocessable_entity end end end diff --git a/app/frontend/src/javascript/components/cart/store-cart.tsx b/app/frontend/src/javascript/components/cart/store-cart.tsx index a345a8b2d..8e06369fd 100644 --- a/app/frontend/src/javascript/components/cart/store-cart.tsx +++ b/app/frontend/src/javascript/components/cart/store-cart.tsx @@ -194,7 +194,7 @@ const StoreCart: React.FC = ({ onSuccess, onError, currentUser,

[TODO: texte venant des paramètres de la boutique…]

- {cart && !cartIsEmpty() && cart.user && + {cart && !cartIsEmpty() &&
diff --git a/app/frontend/src/javascript/components/coupon/coupon-input.tsx b/app/frontend/src/javascript/components/coupon/coupon-input.tsx index d3fb917a2..56cccba6c 100644 --- a/app/frontend/src/javascript/components/coupon/coupon-input.tsx +++ b/app/frontend/src/javascript/components/coupon/coupon-input.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { FabInput } from '../base/fab-input'; import { FabAlert } from '../base/fab-alert'; @@ -28,6 +28,12 @@ export const CouponInput: React.FC = ({ user, amount, onChange const [error, setError] = useState(false); const [coupon, setCoupon] = useState(); + useEffect(() => { + if (user && coupon) { + handleChange(coupon.code); + } + }, [user]); + /** * callback for validate the code */ From 85720c31fa73084460df6ee2b2034a03e838113f Mon Sep 17 00:00:00 2001 From: Du Peng Date: Thu, 8 Sep 2022 12:13:34 +0200 Subject: [PATCH 5/9] (feat) order items order by created date --- app/views/api/orders/_order.json.jbuilder | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/api/orders/_order.json.jbuilder b/app/views/api/orders/_order.json.jbuilder index a002b1483..57e1e7808 100644 --- a/app/views/api/orders/_order.json.jbuilder +++ b/app/views/api/orders/_order.json.jbuilder @@ -10,7 +10,7 @@ if order&.statistic_profile&.user end end -json.order_items_attributes order.order_items do |item| +json.order_items_attributes order.order_items.order(created_at: :asc) do |item| json.id item.id json.orderable_type item.orderable_type json.orderable_id item.orderable_id From 8d414a31729e32aec1167fa46eccc87d56c22530 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Thu, 8 Sep 2022 15:10:56 +0200 Subject: [PATCH 6/9] (feat) admin can set offer for product in cart --- app/controllers/api/cart_controller.rb | 6 ++ app/controllers/concerns/api/order_concern.rb | 2 +- app/frontend/src/javascript/api/cart.ts | 5 ++ .../javascript/components/cart/store-cart.tsx | 66 +++++++++++++++---- .../components/coupon/coupon-input.tsx | 12 +++- .../components/user/member-select.tsx | 6 ++ app/frontend/src/javascript/lib/coupon.ts | 4 +- app/policies/cart_policy.rb | 4 ++ app/services/cart/add_item_service.rb | 2 +- app/services/cart/remove_item_service.rb | 2 +- app/services/cart/set_offer_service.rb | 22 +++++++ app/services/cart/set_quantity_service.rb | 2 +- config/routes.rb | 1 + 13 files changed, 113 insertions(+), 21 deletions(-) create mode 100644 app/services/cart/set_offer_service.rb diff --git a/app/controllers/api/cart_controller.rb b/app/controllers/api/cart_controller.rb index c9de2170d..82cc48615 100644 --- a/app/controllers/api/cart_controller.rb +++ b/app/controllers/api/cart_controller.rb @@ -31,6 +31,12 @@ class API::CartController < API::ApiController render 'api/orders/show' end + def set_offer + authorize @current_order, policy_class: CartPolicy + @order = Cart::SetOfferService.new.call(@current_order, orderable, cart_params[:is_offered]) + render 'api/orders/show' + end + private def orderable diff --git a/app/controllers/concerns/api/order_concern.rb b/app/controllers/concerns/api/order_concern.rb index 9e14854dd..6c0b753c1 100644 --- a/app/controllers/concerns/api/order_concern.rb +++ b/app/controllers/concerns/api/order_concern.rb @@ -17,6 +17,6 @@ module API::OrderConcern end def cart_params - params.permit(:order_token, :orderable_id, :quantity, :user_id) + params.permit(:order_token, :orderable_id, :quantity, :user_id, :is_offered) end end diff --git a/app/frontend/src/javascript/api/cart.ts b/app/frontend/src/javascript/api/cart.ts index 4601de322..f0efea8db 100644 --- a/app/frontend/src/javascript/api/cart.ts +++ b/app/frontend/src/javascript/api/cart.ts @@ -22,4 +22,9 @@ 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 setOffer (order: Order, orderableId: number, isOffered: boolean): Promise { + const res: AxiosResponse = await apiClient.put('/api/cart/set_offer', { order_token: order.token, orderable_id: orderableId, is_offered: isOffered }); + 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 8e06369fd..f385d3821 100644 --- a/app/frontend/src/javascript/components/cart/store-cart.tsx +++ b/app/frontend/src/javascript/components/cart/store-cart.tsx @@ -115,8 +115,12 @@ const StoreCart: React.FC = ({ onSuccess, onError, currentUser, /** * Toggle product offer */ - const onSwitch = (product, checked: boolean) => { - console.log('Offer ', product.orderable_name, ': ', checked); + const toogleProductOffer = (item) => { + return (checked: boolean) => { + CartAPI.setOffer(cart, item.orderable_id, checked).then(data => { + setCart(data); + }).catch(onError); + }; }; /** @@ -128,14 +132,50 @@ const StoreCart: React.FC = ({ onSuccess, onError, currentUser, } }; + /** + * Get the item total + */ + const itemAmount = (item): number => { + return item.quantity * Math.trunc(item.amount * 100) / 100; + }; + + /** + * return true if cart has offered item + */ + const hasOfferedItem = (): boolean => { + return cart.order_items_attributes + .filter(i => i.is_offered).length > 0; + }; + /** * Get the offered item total */ const offeredAmount = (): number => { return cart.order_items_attributes .filter(i => i.is_offered) - .map(i => i.amount) - .reduce((acc, curr) => acc + curr, 0); + .map(i => Math.trunc(i.amount * 100) * i.quantity) + .reduce((acc, curr) => acc + curr, 0) / 100; + }; + + /** + * Get the total amount before offered amount + */ + const totalBeforeOfferedAmount = (): number => { + return (Math.trunc(cart.total * 100) + Math.trunc(offeredAmount() * 100)) / 100; + }; + + /** + * Get the coupon amount + */ + const couponAmount = (): number => { + return (Math.trunc(cart.total * 100) - Math.trunc(computePriceWithCoupon(cart.total, cart.coupon) * 100)) / 100.00; + }; + + /** + * Get the paid total amount + */ + const paidTotal = (): number => { + return computePriceWithCoupon(cart.total, cart.coupon); }; return ( @@ -163,7 +203,7 @@ const StoreCart: React.FC = ({ onSuccess, onError, currentUser,
{t('app.public.store_cart.total')} -

{FormatLib.price(item.quantity * item.amount)}

+

{FormatLib.price(itemAmount(item))}

@@ -175,7 +215,7 @@ const StoreCart: React.FC = ({ onSuccess, onError, currentUser, Offer the product onSwitch(item, checked)} + onChange={toogleProductOffer(item)} width={40} height={19} uncheckedIcon={false} @@ -203,7 +243,7 @@ const StoreCart: React.FC = ({ onSuccess, onError, currentUser,