1
0
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:
Sylvain 2023-01-02 16:59:41 +01:00
parent bfe0936b40
commit bbaac2c122
12 changed files with 168 additions and 38 deletions

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
# Raised when an anonymous cart it not allowed
class Cart::AnonymousError < StandardError
end

View File

@ -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;

View File

@ -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']));

View 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
}

View File

@ -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,

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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