mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-18 07:52:23 +01:00
(feat) add reservations in cart/order
This commit is contained in:
parent
05a6f517cd
commit
473aedbdcb
@ -4,16 +4,21 @@ import FormatLib from '../../lib/format';
|
||||
import OrderLib from '../../lib/order';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import Switch from 'react-switch';
|
||||
import type { OrderItem } from '../../models/order';
|
||||
import type { ItemError, OrderItem } from '../../models/order';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactNode } from 'react';
|
||||
import { Order } from '../../models/order';
|
||||
import CartAPI from '../../api/cart';
|
||||
|
||||
interface AbstractItemProps {
|
||||
item: OrderItem,
|
||||
hasError: boolean,
|
||||
errors: Array<ItemError>,
|
||||
cart: Order,
|
||||
setCart: (cart: Order) => void,
|
||||
reloadCart: () => Promise<void>,
|
||||
onError: (message: string) => void,
|
||||
className?: string,
|
||||
removeItemFromCart: (item: OrderItem) => void,
|
||||
toggleItemOffer: (item: OrderItem, checked: boolean) => void,
|
||||
offerItemLabel?: string,
|
||||
privilegedOperator: boolean,
|
||||
actions?: ReactNode
|
||||
}
|
||||
@ -21,7 +26,7 @@ interface AbstractItemProps {
|
||||
/**
|
||||
* This component shares the common code for items in the cart (product, cart-item, etc)
|
||||
*/
|
||||
export const AbstractItem: React.FC<AbstractItemProps> = ({ item, hasError, className, removeItemFromCart, toggleItemOffer, privilegedOperator, actions, children }) => {
|
||||
export const AbstractItem: React.FC<AbstractItemProps> = ({ item, errors, cart, setCart, reloadCart, onError, className, offerItemLabel, privilegedOperator, actions, children }) => {
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
/**
|
||||
@ -32,7 +37,13 @@ export const AbstractItem: React.FC<AbstractItemProps> = ({ item, hasError, clas
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
removeItemFromCart(item);
|
||||
if (errors.length === 1 && errors[0].error === 'not_found') {
|
||||
reloadCart().catch(onError);
|
||||
} else {
|
||||
CartAPI.removeItem(cart, item.orderable_id, item.orderable_type).then(data => {
|
||||
setCart(data);
|
||||
}).catch(onError);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@ -40,11 +51,21 @@ export const AbstractItem: React.FC<AbstractItemProps> = ({ item, hasError, clas
|
||||
* Return the callback triggered when the privileged user enable/disable the offered attribute for the given item
|
||||
*/
|
||||
const handleToggleOffer = (item: OrderItem) => {
|
||||
return (checked: boolean) => toggleItemOffer(item, checked);
|
||||
return (checked: boolean) => {
|
||||
CartAPI.setOffer(cart, item.orderable_id, item.orderable_type, checked).then(data => {
|
||||
setCart(data);
|
||||
}).catch(e => {
|
||||
if (e.match(/code 403/)) {
|
||||
onError(t('app.public.abstract_item.errors.unauthorized_offering_product'));
|
||||
} else {
|
||||
onError(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<article className={`item ${className || ''} ${hasError ? 'error' : ''}`}>
|
||||
<article className={`item ${className || ''} ${errors.length > 0 ? 'error' : ''}`}>
|
||||
<div className='picture'>
|
||||
<img alt='' src={item.orderable_main_image_url || noImage} />
|
||||
</div>
|
||||
@ -62,7 +83,7 @@ export const AbstractItem: React.FC<AbstractItemProps> = ({ item, hasError, clas
|
||||
{privilegedOperator &&
|
||||
<div className='offer'>
|
||||
<label>
|
||||
<span>{t('app.public.abstract_item.offer_product')}</span>
|
||||
<span>{offerItemLabel || t('app.public.abstract_item.offer_product')}</span>
|
||||
<Switch
|
||||
checked={item.is_offered || false}
|
||||
onChange={handleToggleOffer(item)}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import FormatLib from '../../lib/format';
|
||||
import { CaretDown, CaretUp } from 'phosphor-react';
|
||||
import type { OrderProduct, OrderErrors, Order } from '../../models/order';
|
||||
import type { OrderProduct, OrderErrors, Order, ItemError } from '../../models/order';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import _ from 'lodash';
|
||||
import CartAPI from '../../api/cart';
|
||||
@ -14,22 +14,21 @@ interface CartOrderProductProps {
|
||||
className?: string,
|
||||
cart: Order,
|
||||
setCart: (cart: Order) => void,
|
||||
reloadCart: () => Promise<void>,
|
||||
onError: (message: string) => void,
|
||||
removeProductFromCart: (item: OrderProduct) => void,
|
||||
toggleProductOffer: (item: OrderProduct, checked: boolean) => void,
|
||||
privilegedOperator: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a product in the cart
|
||||
*/
|
||||
export const CartOrderProduct: React.FC<CartOrderProductProps> = ({ item, cartErrors, className, cart, setCart, onError, removeProductFromCart, toggleProductOffer, privilegedOperator }) => {
|
||||
export const CartOrderProduct: React.FC<CartOrderProductProps> = ({ item, cartErrors, className, cart, setCart, reloadCart, onError, privilegedOperator }) => {
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
/**
|
||||
* Get the given item's errors
|
||||
*/
|
||||
const getItemErrors = (item: OrderProduct) => {
|
||||
const getItemErrors = (item: OrderProduct): Array<ItemError> => {
|
||||
if (!cartErrors) return [];
|
||||
const errors = _.find(cartErrors.details, (e) => e.item_id === item.id);
|
||||
return errors?.errors || [{ error: 'not_found' }];
|
||||
@ -120,11 +119,13 @@ export const CartOrderProduct: React.FC<CartOrderProductProps> = ({ item, cartEr
|
||||
|
||||
return (
|
||||
<AbstractItem className={`cart-order-product ${className || ''}`}
|
||||
hasError={getItemErrors(item).length > 0}
|
||||
errors={getItemErrors(item)}
|
||||
setCart={setCart}
|
||||
cart={cart}
|
||||
onError={onError}
|
||||
reloadCart={reloadCart}
|
||||
item={item}
|
||||
removeItemFromCart={removeProductFromCart}
|
||||
privilegedOperator={privilegedOperator}
|
||||
toggleItemOffer={toggleProductOffer}
|
||||
actions={buildActions()}>
|
||||
<div className="ref">
|
||||
<span>{t('app.public.cart_order_product.reference_short')} {item.orderable_ref || ''}</span>
|
||||
|
@ -3,30 +3,30 @@ import type { OrderErrors, Order } from '../../models/order';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import _ from 'lodash';
|
||||
import { AbstractItem } from './abstract-item';
|
||||
import { OrderCartItem } from '../../models/order';
|
||||
import { OrderCartItemReservation } from '../../models/order';
|
||||
import FormatLib from '../../lib/format';
|
||||
|
||||
interface CartOrderReservationProps {
|
||||
item: OrderCartItem,
|
||||
item: OrderCartItemReservation,
|
||||
cartErrors: OrderErrors,
|
||||
className?: string,
|
||||
cart: Order,
|
||||
setCart: (cart: Order) => void,
|
||||
reloadCart: () => Promise<void>,
|
||||
onError: (message: string) => void,
|
||||
removeProductFromCart: (item: OrderCartItem) => void,
|
||||
toggleProductOffer: (item: OrderCartItem, checked: boolean) => void,
|
||||
privilegedOperator: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a product in the cart
|
||||
*/
|
||||
export const CartOrderReservation: React.FC<CartOrderReservationProps> = ({ item, cartErrors, className, cart, setCart, onError, removeProductFromCart, toggleProductOffer, privilegedOperator }) => {
|
||||
export const CartOrderReservation: React.FC<CartOrderReservationProps> = ({ item, cartErrors, className, cart, setCart, reloadCart, onError, privilegedOperator }) => {
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
/**
|
||||
* Get the given item's errors
|
||||
*/
|
||||
const getItemErrors = (item: OrderCartItem) => {
|
||||
const getItemErrors = (item: OrderCartItemReservation) => {
|
||||
if (!cartErrors) return [];
|
||||
const errors = _.find(cartErrors.details, (e) => e.item_id === item.id);
|
||||
return errors?.errors || [{ error: 'not_found' }];
|
||||
@ -34,13 +34,26 @@ export const CartOrderReservation: React.FC<CartOrderReservationProps> = ({ item
|
||||
|
||||
return (
|
||||
<AbstractItem className={`cart-order-reservation ${className || ''}`}
|
||||
hasError={getItemErrors(item).length > 0}
|
||||
errors={getItemErrors(item)}
|
||||
item={item}
|
||||
removeItemFromCart={removeProductFromCart}
|
||||
privilegedOperator={privilegedOperator}
|
||||
toggleItemOffer={toggleProductOffer}>
|
||||
cart={cart}
|
||||
setCart={setCart}
|
||||
onError={onError}
|
||||
reloadCart={reloadCart}
|
||||
actions={<div/>}
|
||||
offerItemLabel={t('app.public.cart_order_reservation.offer_reservation')}
|
||||
privilegedOperator={privilegedOperator}>
|
||||
<div className="ref">
|
||||
<p>Réservation {item.orderable_name}</p>
|
||||
<p>{t('app.public.cart_order_reservation.reservation')} {item.orderable_name}</p>
|
||||
<ul>{item.slots_reservations.map(sr => (
|
||||
<li key={sr.id}>
|
||||
{
|
||||
t('app.public.cart_order_reservation.slot',
|
||||
{ DATE: FormatLib.date(sr.slot.start_at), START: FormatLib.time(sr.slot.start_at), END: FormatLib.time(sr.slot.end_at) })
|
||||
}
|
||||
<span>{sr.offered ? t('app.public.cart_order_reservation.offered') : ''}</span>
|
||||
</li>
|
||||
))}</ul>
|
||||
{getItemErrors(item)}
|
||||
</div>
|
||||
</AbstractItem>
|
||||
|
@ -11,7 +11,7 @@ import CartAPI from '../../api/cart';
|
||||
import type { User } from '../../models/user';
|
||||
import { PaymentModal } from '../payment/stripe/payment-modal';
|
||||
import { PaymentMethod } from '../../models/payment';
|
||||
import type { Order, OrderCartItem, OrderErrors, OrderItem, OrderProduct } from '../../models/order';
|
||||
import type { Order, OrderCartItemReservation, OrderErrors, OrderProduct } from '../../models/order';
|
||||
import { MemberSelect } from '../user/member-select';
|
||||
import { CouponInput } from '../coupon/coupon-input';
|
||||
import type { Coupon } from '../../models/coupon';
|
||||
@ -53,20 +53,6 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
||||
}
|
||||
}, [cart]);
|
||||
|
||||
/**
|
||||
* Remove the product from cart
|
||||
*/
|
||||
const removeProductFromCart = (item) => {
|
||||
const errors = getItemErrors(item);
|
||||
if (errors.length === 1 && errors[0].error === 'not_found') {
|
||||
reloadCart().catch(onError);
|
||||
} else {
|
||||
CartAPI.removeItem(cart, item.orderable_id, item.orderable_type).then(data => {
|
||||
setCart(data);
|
||||
}).catch(onError);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check the current cart's items (available, price, stock, quantity_min)
|
||||
*/
|
||||
@ -109,15 +95,6 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* get givean item's error
|
||||
*/
|
||||
const getItemErrors = (item) => {
|
||||
if (!cartErrors) return [];
|
||||
const errors = _.find(cartErrors.details, (e) => e.item_id === item.id);
|
||||
return errors?.errors || [{ error: 'not_found' }];
|
||||
};
|
||||
|
||||
/**
|
||||
* Open/closes the payment modal
|
||||
*/
|
||||
@ -159,21 +136,6 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
||||
return cart && cart.order_items_attributes.length === 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle product offer
|
||||
*/
|
||||
const toggleProductOffer = (item: OrderItem, checked: boolean) => {
|
||||
CartAPI.setOffer(cart, item.orderable_id, item.orderable_type, checked).then(data => {
|
||||
setCart(data);
|
||||
}).catch(e => {
|
||||
if (e.match(/code 403/)) {
|
||||
onError(t('app.public.store_cart.errors.unauthorized_offering_product'));
|
||||
} else {
|
||||
onError(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply coupon to current cart
|
||||
*/
|
||||
@ -196,22 +158,20 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
||||
cartErrors={cartErrors}
|
||||
cart={cart}
|
||||
setCart={setCart}
|
||||
reloadCart={reloadCart}
|
||||
onError={onError}
|
||||
removeProductFromCart={removeProductFromCart}
|
||||
toggleProductOffer={toggleProductOffer}
|
||||
privilegedOperator={isPrivileged()} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<CartOrderReservation item={item as OrderCartItem}
|
||||
<CartOrderReservation item={item as OrderCartItemReservation}
|
||||
key={item.id}
|
||||
className="store-cart-list-item"
|
||||
cartErrors={cartErrors}
|
||||
cart={cart}
|
||||
reloadCart={reloadCart}
|
||||
setCart={setCart}
|
||||
onError={onError}
|
||||
removeProductFromCart={removeProductFromCart}
|
||||
toggleProductOffer={toggleProductOffer}
|
||||
privilegedOperator={isPrivileged()} />
|
||||
);
|
||||
})}
|
||||
|
@ -39,8 +39,9 @@ export interface CartItemPrepaidPack extends CartItem {
|
||||
export interface CartItemFreeExtension extends CartItem {
|
||||
free_extension: { end_at: Date }
|
||||
}
|
||||
export type CartItemReservationType = 'CartItem::MachineReservation' | 'CartItem::SpaceReservation' | 'CartItem::TrainingReservation';
|
||||
|
||||
export type CartItemType = 'CartItem::EventReservation' | 'CartItem::MachineReservation' | 'CartItem::PrepaidPack' | 'CartItem::SpaceReservation' | 'CartItem::Subscription' | 'CartItem::TrainingReservation';
|
||||
export type CartItemType = 'CartItem::EventReservation' | 'CartItem::PrepaidPack' | 'CartItem::Subscription' | CartItemReservationType;
|
||||
|
||||
export interface CartItemResponse {
|
||||
id: number,
|
||||
|
@ -4,7 +4,7 @@ import { CreateTokenResponse } from './payzen';
|
||||
import { UserRole } from './user';
|
||||
import { Coupon } from './coupon';
|
||||
import { ApiFilter, PaginatedIndex } from './api';
|
||||
import { CartItemType } from './cart_item';
|
||||
import type { CartItemReservationType, CartItemType } from './cart_item';
|
||||
|
||||
export type OrderableType = 'Product' | CartItemType;
|
||||
|
||||
@ -13,6 +13,7 @@ export interface OrderItem {
|
||||
orderable_type: OrderableType,
|
||||
orderable_id: number,
|
||||
orderable_name: string,
|
||||
orderable_slug: string,
|
||||
orderable_main_image_url?: string;
|
||||
quantity: number,
|
||||
amount: number,
|
||||
@ -21,14 +22,22 @@ export interface OrderItem {
|
||||
|
||||
export interface OrderProduct extends OrderItem {
|
||||
orderable_type: 'Product',
|
||||
orderable_slug: string,
|
||||
orderable_ref?: string,
|
||||
orderable_external_stock: number,
|
||||
quantity_min: number
|
||||
}
|
||||
|
||||
export interface OrderCartItem extends OrderItem {
|
||||
orderable_type: CartItemType
|
||||
export interface OrderCartItemReservation extends OrderItem {
|
||||
orderable_type: CartItemReservationType
|
||||
slots_reservations: Array<{
|
||||
id: number,
|
||||
offered: boolean,
|
||||
slot: {
|
||||
id: number,
|
||||
start_at: TDateISO,
|
||||
end_at: TDateISO
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
@ -78,13 +87,14 @@ export interface OrderIndexFilter extends ApiFilter {
|
||||
period_to?: string
|
||||
}
|
||||
|
||||
export interface ItemError {
|
||||
error: string,
|
||||
value?: string|number
|
||||
}
|
||||
export interface OrderErrors {
|
||||
order_id: number,
|
||||
details: Array<{
|
||||
item_id: number,
|
||||
errors: Array<{
|
||||
error: string,
|
||||
value: string|number
|
||||
}>
|
||||
errors: Array<ItemError>
|
||||
}>
|
||||
}
|
||||
|
@ -40,6 +40,7 @@
|
||||
@import "modules/base/labelled-input";
|
||||
@import "modules/calendar/calendar";
|
||||
@import "modules/cart/cart-button";
|
||||
@import "modules/cart/cart-order-reservation";
|
||||
@import "modules/cart/store-cart";
|
||||
@import "modules/dashboard/reservations/credits-panel";
|
||||
@import "modules/dashboard/reservations/prepaid-packs-panel";
|
||||
|
@ -0,0 +1,9 @@
|
||||
.cart-order-reservation {
|
||||
.actions:before {
|
||||
content: ' '
|
||||
}
|
||||
|
||||
.ref > ul > li > span {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
@ -4,4 +4,18 @@
|
||||
# This is a single spot to configure app-wide model behavior.
|
||||
class ApplicationRecord < ActiveRecord::Base
|
||||
self.abstract_class = true
|
||||
end
|
||||
|
||||
# Update attributes with validation context.
|
||||
# In Rails you can provide a context while you save, for example: `.save(:step1)`, but no way to
|
||||
# provide a context while you update. This method just adds the way to update with validation
|
||||
# context.
|
||||
#
|
||||
# @param attributes [Hash] attributes to assign
|
||||
# @param context [*] validation context
|
||||
def update_with_context(attributes, context)
|
||||
with_transaction_returning_status do
|
||||
assign_attributes(attributes)
|
||||
save(context: context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
# An event reservation added to the shopping cart
|
||||
class CartItem::EventReservation < CartItem::Reservation
|
||||
self.table_name = 'cart_item_event_reservations'
|
||||
|
||||
has_many :cart_item_event_reservation_tickets, class_name: 'CartItem::EventReservationTicket', dependent: :destroy,
|
||||
inverse_of: :cart_item_event_reservation,
|
||||
foreign_key: 'cart_item_event_reservation_id'
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
# A machine reservation added to the shopping cart
|
||||
class CartItem::MachineReservation < CartItem::Reservation
|
||||
self.table_name = 'cart_item_reservations'
|
||||
|
||||
has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy, inverse_of: :cart_item,
|
||||
foreign_key: 'cart_item_id', foreign_type: 'cart_item_type'
|
||||
accepts_nested_attributes_for :cart_item_reservation_slots
|
||||
|
@ -1,13 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
MINUTES_PER_HOUR = 60.0
|
||||
SECONDS_PER_MINUTE = 60.0
|
||||
|
||||
GET_SLOT_PRICE_DEFAULT_OPTS = { has_credits: false, elements: nil, is_division: true, prepaid: { minutes: 0 }, custom_duration: nil }.freeze
|
||||
|
||||
# A generic reservation added to the shopping cart
|
||||
class CartItem::Reservation < CartItem::BaseItem
|
||||
self.abstract_class = true
|
||||
MINUTES_PER_HOUR = 60.0
|
||||
SECONDS_PER_MINUTE = 60.0
|
||||
|
||||
GET_SLOT_PRICE_DEFAULT_OPTS = { has_credits: false, elements: nil, is_division: true, prepaid: { minutes: 0 }, custom_duration: nil }.freeze
|
||||
|
||||
def reservable
|
||||
nil
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
# A space reservation added to the shopping cart
|
||||
class CartItem::SpaceReservation < CartItem::Reservation
|
||||
self.table_name = 'cart_item_reservations'
|
||||
|
||||
has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy, inverse_of: :cart_item,
|
||||
foreign_key: 'cart_item_id', foreign_type: 'cart_item_type'
|
||||
accepts_nested_attributes_for :cart_item_reservation_slots
|
||||
|
@ -29,7 +29,7 @@ class Cart::AddItemService
|
||||
def add_product(order, orderable, quantity)
|
||||
raise Cart::InactiveProductError unless orderable.is_active
|
||||
|
||||
item = order.order_items.find_by(orderable: orderable)
|
||||
item = order.order_items.find_by(orderable_type: orderable.class.name, orderable_id: orderable.id)
|
||||
quantity = orderable.quantity_min > quantity.to_i && item.nil? ? orderable.quantity_min : quantity.to_i
|
||||
|
||||
if item.nil?
|
||||
@ -43,6 +43,6 @@ class Cart::AddItemService
|
||||
end
|
||||
|
||||
def add_cart_item(order, orderable)
|
||||
order.order_items.new(quantity: 1, orderable: orderable, amount: orderable.price[:amount] || 0)
|
||||
order.order_items.new(quantity: 1, orderable_type: orderable.class.name, orderable_id: orderable.id, amount: orderable.price[:amount] || 0)
|
||||
end
|
||||
end
|
||||
|
@ -3,11 +3,11 @@
|
||||
# Provides methods to create new cart items, based on an existing Order
|
||||
class Cart::CreateCartItemService
|
||||
def initialize(order)
|
||||
raise Cart::AnonymousError if order.statistic_profile.nil? || order.operator_profile.nil?
|
||||
raise Cart::AnonymousError, I18n.t('cart_validation.select_user') if order.statistic_profile.nil? || order.operator_profile.nil?
|
||||
|
||||
@order = order
|
||||
@customer = order.user
|
||||
@operator = order.user.privileged? ? order.operator_profile.user : order.user
|
||||
@operator = order.operator_profile.user
|
||||
end
|
||||
|
||||
def create(item)
|
||||
|
@ -5,7 +5,7 @@ class Cart::RefreshItemService
|
||||
def call(order, orderable)
|
||||
raise Cart::InactiveProductError unless orderable.is_active
|
||||
|
||||
item = order.order_items.find_by(orderable: orderable)
|
||||
item = order.order_items.find_by(orderable_type: orderable.class.name, orderable_id: orderable.id)
|
||||
|
||||
raise ActiveRecord::RecordNotFound if item.nil?
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
# Provides methods for remove order item to cart
|
||||
class Cart::RemoveItemService
|
||||
def call(order, orderable)
|
||||
item = order.order_items.find_by(orderable: orderable)
|
||||
item = order.order_items.find_by(orderable_type: orderable.class.name, orderable_id: orderable.id)
|
||||
|
||||
raise ActiveRecord::RecordNotFound if item.nil?
|
||||
|
||||
|
@ -5,10 +5,13 @@ module Cart; end
|
||||
|
||||
# Provides methods to update the customer of the given cart
|
||||
class Cart::SetCustomerService
|
||||
# @param operator [User]
|
||||
def initialize(operator)
|
||||
@operator = operator
|
||||
end
|
||||
|
||||
# @param order[Order]
|
||||
# @param customer [User]
|
||||
def call(order, customer)
|
||||
return order unless @operator.privileged?
|
||||
|
||||
@ -19,6 +22,9 @@ class Cart::SetCustomerService
|
||||
ActiveRecord::Base.transaction do
|
||||
order.statistic_profile_id = customer.statistic_profile.id
|
||||
order.operator_profile_id = @operator.invoicing_profile.id
|
||||
order.order_items.each do |item|
|
||||
update_item_user(item, customer)
|
||||
end
|
||||
unset_offer(order, customer)
|
||||
Cart::UpdateTotalService.new.call(order)
|
||||
order.save
|
||||
@ -26,7 +32,11 @@ class Cart::SetCustomerService
|
||||
order.reload
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# If the operator is also the customer, he cannot offer items to himself, so we unset all the offers
|
||||
# @param order[Order]
|
||||
# @param customer [User]
|
||||
def unset_offer(order, customer)
|
||||
return unless @operator == customer
|
||||
|
||||
@ -35,4 +45,15 @@ class Cart::SetCustomerService
|
||||
item.save
|
||||
end
|
||||
end
|
||||
|
||||
# @param item[OrderItem]
|
||||
# @param customer [User]
|
||||
def update_item_user(item, customer)
|
||||
return unless item.orderable_type.match(/^CartItem::/)
|
||||
|
||||
item.orderable.update_with_context({
|
||||
customer_profile_id: customer.invoicing_profile.id,
|
||||
operator_profile_id: @operator.invoicing_profile.id
|
||||
}, item.order.order_items)
|
||||
end
|
||||
end
|
||||
|
@ -6,7 +6,7 @@ module Cart; end
|
||||
# Provides methods for set offer to item in cart
|
||||
class Cart::SetOfferService
|
||||
def call(order, orderable, is_offered)
|
||||
item = order.order_items.find_by(orderable: orderable)
|
||||
item = order.order_items.find_by(orderable_type: orderable.class.name, orderable_id: orderable.id)
|
||||
|
||||
raise ActiveRecord::RecordNotFound if item.nil?
|
||||
|
||||
|
@ -9,7 +9,7 @@ class Cart::SetQuantityService
|
||||
|
||||
raise Cart::OutStockError if quantity.to_i > orderable.stock['external']
|
||||
|
||||
item = order.order_items.find_by(orderable: orderable)
|
||||
item = order.order_items.find_by(orderable_type: orderable.class.name, orderable_id: orderable.id)
|
||||
|
||||
raise ActiveRecord::RecordNotFound if item.nil?
|
||||
|
||||
|
@ -2,13 +2,24 @@
|
||||
|
||||
# Provides methods for update total of cart
|
||||
class Cart::UpdateTotalService
|
||||
# @param order[Order]
|
||||
def call(order)
|
||||
total = 0
|
||||
order.order_items.each do |item|
|
||||
update_item_price(item)
|
||||
total += (item.amount * item.quantity) unless item.is_offered
|
||||
end
|
||||
order.total = total
|
||||
order.save
|
||||
order
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @param item[OrderItem]
|
||||
def update_item_price(item)
|
||||
return unless item.orderable_type.match(/^CartItem::/)
|
||||
|
||||
item.update(amount: item.orderable.price[:amount] || 0)
|
||||
end
|
||||
end
|
||||
|
11
app/views/api/orders/_cart_item_reservation.json.jbuilder
Normal file
11
app/views/api/orders/_cart_item_reservation.json.jbuilder
Normal file
@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.orderable_name item.orderable.reservable.name
|
||||
json.orderable_slug item.orderable.reservable.slug
|
||||
json.orderable_main_image_url item.orderable.reservable&.try("#{item.orderable.reservable_type.downcase}_image")&.attachment&.medium&.url
|
||||
json.slots_reservations item.orderable.cart_item_reservation_slots do |sr|
|
||||
json.extract! sr, :id, :offered
|
||||
json.slot do
|
||||
json.extract! sr.slot, :id, :start_at, :end_at
|
||||
end
|
||||
end
|
@ -28,4 +28,7 @@ json.order_items_attributes order.order_items.order(created_at: :asc) do |item|
|
||||
json.amount item.amount / 100.0
|
||||
json.is_offered item.is_offered
|
||||
json.partial! 'api/orders/product', item: item if item.orderable_type == 'Product'
|
||||
if %w[CartItem::MachineReservation CartItem::SpaceReservation CartItem::TrainingReservation].include?(item.orderable_type)
|
||||
json.partial! 'api/orders/cart_item_reservation', item: item
|
||||
end
|
||||
end
|
||||
|
@ -446,6 +446,8 @@ en:
|
||||
abstract_item:
|
||||
offer_product: "Offer the product"
|
||||
total: "Total"
|
||||
errors:
|
||||
unauthorized_offering_product: "You can't offer anything to yourself"
|
||||
cart_order_product:
|
||||
reference_short: "ref:"
|
||||
minimum_purchase: "Minimum purchase: "
|
||||
@ -458,7 +460,11 @@ en:
|
||||
stock_limit_QUANTITY: "Only {QUANTITY} {QUANTITY, plural, =1{unit} other{units}} left in stock, please adjust the quantity of items."
|
||||
quantity_min_QUANTITY: "Minimum number of product was changed to {QUANTITY}, please adjust the quantity of items."
|
||||
price_changed_PRICE: "The product price was modified to {PRICE}"
|
||||
unauthorized_offering_product: "You can't offer anything to yourself"
|
||||
cart_order_reservation:
|
||||
reservation: "Reservation"
|
||||
offer_reservation: "Offer the reservation"
|
||||
slot: "{DATE}: {START} - {END}"
|
||||
offered: "offered"
|
||||
orders_dashboard:
|
||||
heading: "My orders"
|
||||
sort:
|
||||
|
@ -524,6 +524,8 @@ en:
|
||||
space: "This space is disabled"
|
||||
machine: "This machine is disabled"
|
||||
reservable: "This machine is not reservable"
|
||||
cart_validation:
|
||||
select_user: "Please select a user before continuing"
|
||||
settings:
|
||||
locked_setting: "the setting is locked."
|
||||
about_title: "\"About\" page title"
|
||||
|
@ -518,6 +518,8 @@ fr:
|
||||
space: "Cet espace est désactivé"
|
||||
machine: "Cette machine est désactivée"
|
||||
reservable: "Cette machine n'est pas réservable"
|
||||
cart_validation:
|
||||
select_user: "Veuillez sélectionner un utilisateur avant de continuer"
|
||||
settings:
|
||||
locked_setting: "le paramètre est verrouillé."
|
||||
about_title: "Le titre de la page \"À propos\""
|
||||
|
Loading…
x
Reference in New Issue
Block a user