mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
(wip) use cartitem from order api
This commit is contained in:
parent
bfe0936b40
commit
bbaac2c122
5
app/exceptions/cart/anonymous_error.rb
Normal file
5
app/exceptions/cart/anonymous_error.rb
Normal file
@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Raised when an anonymous cart it not allowed
|
||||
class Cart::AnonymousError < StandardError
|
||||
end
|
@ -1,6 +1,7 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Order, OrderableType, OrderErrors } from '../models/order';
|
||||
import { CartItem, CartItemResponse } from '../models/cart_item';
|
||||
|
||||
export default class CartAPI {
|
||||
static async create (token?: string): Promise<Order> {
|
||||
@ -8,6 +9,11 @@ export default class CartAPI {
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async createItem (order: Order, item: CartItem): Promise<CartItemResponse> {
|
||||
const res: AxiosResponse<CartItemResponse> = await apiClient.post('/api/cart/create_item', { order_token: order.token, ...item });
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async addItem (order: Order, orderableId: number, orderableType: OrderableType, quantity: number): Promise<Order> {
|
||||
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/add_item', { order_token: order.token, orderable_id: orderableId, orderable_type: orderableType, quantity });
|
||||
return res?.data;
|
||||
|
@ -10,21 +10,34 @@ import { ShoppingCart } from 'phosphor-react';
|
||||
import CartAPI from '../../api/cart';
|
||||
import useCart from '../../hooks/use-cart';
|
||||
import type { User } from '../../models/user';
|
||||
import Switch from 'react-switch';
|
||||
import { CartItemReservation } from '../../models/cart_item';
|
||||
import { ReservableType } from '../../models/reservation';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface ReservationsSummaryProps {
|
||||
slot: Slot,
|
||||
customer: User,
|
||||
reservableId: number,
|
||||
reservableType: ReservableType,
|
||||
onError: (error: string) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a summary of the selected slots, and ask for confirmation before adding them to the cart
|
||||
*/
|
||||
const ReservationsSummary: React.FC<ReservationsSummaryProps> = ({ slot, customer, onError }) => {
|
||||
const ReservationsSummary: React.FC<ReservationsSummaryProps> = ({ slot, customer, reservableId, reservableType, onError }) => {
|
||||
const { cart, setCart } = useCart(customer);
|
||||
const [pendingSlots, setPendingSlots] = useImmer<Array<Slot>>([]);
|
||||
const [offeredSlots, setOfferedSlots] = useImmer<Map<number, boolean>>(new Map());
|
||||
const [reservation, setReservation] = useImmer<CartItemReservation>({
|
||||
reservation: {
|
||||
reservable_id: reservableId,
|
||||
reservable_type: reservableType,
|
||||
slots_reservations_attributes: []
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (slot) {
|
||||
@ -36,22 +49,63 @@ const ReservationsSummary: React.FC<ReservationsSummaryProps> = ({ slot, custome
|
||||
}
|
||||
}, [slot]);
|
||||
|
||||
useEffect(() => {
|
||||
if (customer) {
|
||||
CartAPI.setCustomer(cart, customer.id).then(setCart).catch(onError);
|
||||
}
|
||||
}, [customer]);
|
||||
|
||||
/**
|
||||
* Add the product to cart
|
||||
*/
|
||||
const addSlotToCart = (slot: Slot) => {
|
||||
const addSlotToReservation = (slot: Slot) => {
|
||||
return () => {
|
||||
CartAPI.addItem(cart, slot.slot_id, 'Slot', 1).then(setCart).catch(onError);
|
||||
setReservation(draft => {
|
||||
draft.reservation.slots_reservations_attributes.push({
|
||||
slot_id: slot.slot_id,
|
||||
offered: !!offeredSlots.get(slot.slot_id)
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Build / validate the reservation at server-side, then add it to the cart.
|
||||
*/
|
||||
const addReservationToCart = async () => {
|
||||
try {
|
||||
const item = await CartAPI.createItem(cart, reservation);
|
||||
const newCart = await CartAPI.addItem(cart, item.id, item.type, 1);
|
||||
setCart(newCart);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the "offered" status of the given slot
|
||||
*/
|
||||
const offerSlot = (slot: Slot) => {
|
||||
return () => {
|
||||
setOfferedSlots(draft => {
|
||||
draft.set(slot.slot_id, !draft.get(slot.slot_id));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<ul>{pendingSlots.map(slot => (
|
||||
<li key={slot.slot_id}>
|
||||
<span>{FormatLib.date(slot.start)} {FormatLib.time(slot.start)} - {FormatLib.time(slot.end)}</span>
|
||||
<FabButton onClick={addSlotToCart(slot)}><ShoppingCart size={24}/> add to cart </FabButton>
|
||||
</li>
|
||||
))}</ul>
|
||||
<div>
|
||||
<ul>{pendingSlots.map(slot => (
|
||||
<li key={slot.slot_id}>
|
||||
<span>{FormatLib.date(slot.start)} {FormatLib.time(slot.start)} - {FormatLib.time(slot.end)}</span>
|
||||
<label>offered? <Switch checked={offeredSlots.get(slot.slot_id)} onChange={offerSlot(slot)} /></label>
|
||||
<FabButton onClick={addSlotToReservation(slot)}>validate this slot</FabButton>
|
||||
</li>
|
||||
))}</ul>
|
||||
{reservation.reservation.slots_reservations_attributes.length > 0 && <div>
|
||||
<FabButton onClick={addReservationToCart}><ShoppingCart size={24}/>Ajouter au panier</FabButton>
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -61,4 +115,4 @@ const ReservationsSummaryWrapper: React.FC<ReservationsSummaryProps> = (props) =
|
||||
</Loader>
|
||||
);
|
||||
|
||||
Application.Components.component('reservationsSummary', react2angular(ReservationsSummaryWrapper, ['slot', 'customer', 'onError']));
|
||||
Application.Components.component('reservationsSummary', react2angular(ReservationsSummaryWrapper, ['slot', 'customer', 'reservableId', 'reservableType', 'onError']));
|
||||
|
48
app/frontend/src/javascript/models/cart_item.ts
Normal file
48
app/frontend/src/javascript/models/cart_item.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { ReservableType } from './reservation';
|
||||
import { SubscriptionRequest } from './subscription';
|
||||
|
||||
export type CartItem = Record<string, unknown>;
|
||||
|
||||
export interface CartItemReservationSlot {
|
||||
offered: boolean,
|
||||
slot_id: number,
|
||||
}
|
||||
|
||||
export interface CartItemReservation extends CartItem {
|
||||
reservation: {
|
||||
reservable_id: number,
|
||||
reservable_type: ReservableType,
|
||||
slots_reservations_attributes: Array<CartItemReservationSlot>
|
||||
}
|
||||
}
|
||||
|
||||
export interface CartItemEventReservation extends CartItem {
|
||||
reservation: {
|
||||
reservable_id: number,
|
||||
reservable_type: 'Event',
|
||||
slots_reservations_attributes: Array<CartItemReservationSlot>
|
||||
nb_reserve_places: number,
|
||||
tickets_attributes?: {
|
||||
event_price_category_id: number,
|
||||
booked: number
|
||||
}
|
||||
}
|
||||
}
|
||||
export interface CartItemSubscription extends CartItem {
|
||||
subscription: SubscriptionRequest
|
||||
}
|
||||
|
||||
export interface CartItemPrepaidPack extends CartItem {
|
||||
prepaid_pack: { id: number }
|
||||
}
|
||||
|
||||
export interface CartItemFreeExtension extends CartItem {
|
||||
free_extension: { end_at: Date }
|
||||
}
|
||||
|
||||
export type CartItemType = 'CartItem::EventReservation' | 'CartItem::MachineReservation' | 'CartItem::PrepaidPack' | 'CartItem::SpaceReservation' | 'CartItem::Subscription' | 'CartItem::TrainingReservation';
|
||||
|
||||
export interface CartItemResponse {
|
||||
id: number,
|
||||
type: CartItemType
|
||||
}
|
@ -4,8 +4,9 @@ import { CreateTokenResponse } from './payzen';
|
||||
import { UserRole } from './user';
|
||||
import { Coupon } from './coupon';
|
||||
import { ApiFilter, PaginatedIndex } from './api';
|
||||
import { CartItemType } from './cart_item';
|
||||
|
||||
export type OrderableType = 'Product' | 'Slot';
|
||||
export type OrderableType = 'Product' | CartItemType;
|
||||
|
||||
export interface Order {
|
||||
id: number,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Reservation } from './reservation';
|
||||
import { SubscriptionRequest } from './subscription';
|
||||
import { CartItem } from './cart_item';
|
||||
|
||||
export interface PaymentConfirmation {
|
||||
requires_action?: boolean,
|
||||
@ -23,11 +22,6 @@ export enum PaymentMethod {
|
||||
Other = ''
|
||||
}
|
||||
|
||||
export type CartItem = { reservation: Reservation }|
|
||||
{ subscription: SubscriptionRequest }|
|
||||
{ prepaid_pack: { id: number } }|
|
||||
{ free_extension: { end_at: Date } };
|
||||
|
||||
export interface ShoppingCart {
|
||||
customer_id: number,
|
||||
// WARNING: items ordering matters! The first item in the array will be considered as the main item
|
||||
|
@ -45,7 +45,7 @@
|
||||
refresh="afterPaymentPromise">
|
||||
</packs-summary>
|
||||
|
||||
<reservations-summary slot="selectedEvent" customer="ctrl.member" on-error="onError"></reservations-summary>
|
||||
<reservations-summary slot="selectedEvent" customer="ctrl.member" on-error="onError" reservable-id="machine.id" reservable-type="'Machine'"></reservations-summary>
|
||||
|
||||
<cart slot="selectedEvent"
|
||||
slot-selection-time="selectionTime"
|
||||
|
@ -5,8 +5,8 @@ class Cart::AddItemService
|
||||
def call(order, orderable, quantity = 1)
|
||||
return order if quantity.to_i.zero?
|
||||
|
||||
item = case orderable
|
||||
when Product
|
||||
item = case orderable.class.name
|
||||
when 'Product'
|
||||
add_product(order, orderable, quantity)
|
||||
when /^CartItem::/
|
||||
add_cart_item(order, orderable)
|
||||
@ -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: orderable, amount: orderable.price[:amount] || 0)
|
||||
end
|
||||
end
|
||||
|
@ -1,21 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Provides methods for check cart's items (available, price, stock, quantity_min)
|
||||
# Provides methods to check cart's items (available, price, stock, quantity_min)
|
||||
class Cart::CheckCartService
|
||||
def call(order)
|
||||
res = { order_id: order.id, details: [] }
|
||||
order.order_items.each do |item|
|
||||
errors = []
|
||||
errors.push({ error: 'is_active', value: false }) unless item.orderable.is_active
|
||||
if item.quantity > item.orderable.stock['external'] || item.orderable.stock['external'] < item.orderable.quantity_min
|
||||
value = item.orderable.stock['external'] < item.orderable.quantity_min ? 0 : item.orderable.stock['external']
|
||||
errors.push({ error: 'stock', value: value })
|
||||
end
|
||||
orderable_amount = item.orderable.amount || 0
|
||||
errors.push({ error: 'amount', value: orderable_amount / 100.0 }) if item.amount != orderable_amount
|
||||
errors.push({ error: 'quantity_min', value: item.orderable.quantity_min }) if item.quantity < item.orderable.quantity_min
|
||||
errors = case item.orderable_type
|
||||
when 'Product'
|
||||
check_product(item)
|
||||
when /^CartItem::/
|
||||
check_cart_item(item, order)
|
||||
else
|
||||
[]
|
||||
end
|
||||
res[:details].push({ item_id: item.id, errors: errors })
|
||||
end
|
||||
res
|
||||
end
|
||||
|
||||
def check_product(item)
|
||||
errors = []
|
||||
errors.push({ error: 'is_active', value: false }) unless item.orderable.is_active
|
||||
if item.quantity > item.orderable.stock['external'] || item.orderable.stock['external'] < item.orderable.quantity_min
|
||||
value = item.orderable.stock['external'] < item.orderable.quantity_min ? 0 : item.orderable.stock['external']
|
||||
errors.push({ error: 'stock', value: value })
|
||||
end
|
||||
orderable_amount = item.orderable.amount || 0
|
||||
errors.push({ error: 'amount', value: orderable_amount / 100.0 }) if item.amount != orderable_amount
|
||||
errors.push({ error: 'quantity_min', value: item.orderable.quantity_min }) if item.quantity < item.orderable.quantity_min
|
||||
errors
|
||||
end
|
||||
|
||||
def check_cart_item(item, order)
|
||||
item.valid?(order.order_items)
|
||||
item.errors.to_a
|
||||
end
|
||||
end
|
||||
|
@ -3,6 +3,8 @@
|
||||
# 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?
|
||||
|
||||
@order = order
|
||||
@customer = order.user
|
||||
@operator = order.user.privileged? ? order.operator_profile.user : order.user
|
||||
|
@ -24,13 +24,8 @@ 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
|
||||
json.orderable_name item.orderable.name
|
||||
json.orderable_ref item.orderable.sku
|
||||
json.orderable_slug item.orderable.slug
|
||||
json.orderable_main_image_url item.orderable.main_image&.attachment_url
|
||||
json.orderable_external_stock item.orderable.stock['external']
|
||||
json.quantity item.quantity
|
||||
json.quantity_min item.orderable.quantity_min
|
||||
json.amount item.amount / 100.0
|
||||
json.is_offered item.is_offered
|
||||
json.partial! 'api/orders/product', item: item if item.orderable_type == 'Product'
|
||||
end
|
||||
|
8
app/views/api/orders/_product.json.jbuilder
Normal file
8
app/views/api/orders/_product.json.jbuilder
Normal file
@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.orderable_name item.orderable.name
|
||||
json.orderable_ref item.orderable.sku
|
||||
json.orderable_slug item.orderable.slug
|
||||
json.orderable_main_image_url item.orderable.main_image&.attachment_url
|
||||
json.orderable_external_stock item.orderable.stock['external']
|
||||
json.quantity_min item.orderable.quantity_min
|
Loading…
x
Reference in New Issue
Block a user