mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-30 19:52:20 +01:00
(feat) save cart items in db
This commit is contained in:
parent
7a1809940c
commit
42d830b4f8
@ -52,6 +52,6 @@ class API::CartController < API::ApiController
|
||||
private
|
||||
|
||||
def orderable
|
||||
Product.find(cart_params[:orderable_id])
|
||||
params[:orderable_type].classify.constantize.find(cart_params[:orderable_id])
|
||||
end
|
||||
end
|
||||
|
@ -41,6 +41,7 @@ class API::PaymentsController < API::ApiController
|
||||
{ json: res[:errors].drop_while(&:empty?), status: :unprocessable_entity }
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.debug e.backtrace
|
||||
{ json: e, status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
|
5
app/exceptions/cart/unknown_item_error.rb
Normal file
5
app/exceptions/cart/unknown_item_error.rb
Normal file
@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Raised when the added item is not a recognized class
|
||||
class Cart::UnknownItemError < StandardError
|
||||
end
|
@ -1,6 +1,6 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Order, OrderErrors } from '../models/order';
|
||||
import { Order, OrderableType, OrderErrors } from '../models/order';
|
||||
|
||||
export default class CartAPI {
|
||||
static async create (token?: string): Promise<Order> {
|
||||
@ -8,28 +8,28 @@ export default class CartAPI {
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async addItem (order: Order, orderableId: number, quantity: number): Promise<Order> {
|
||||
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/add_item', { order_token: order.token, orderable_id: orderableId, quantity });
|
||||
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;
|
||||
}
|
||||
|
||||
static async removeItem (order: Order, orderableId: number): Promise<Order> {
|
||||
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/remove_item', { order_token: order.token, orderable_id: orderableId });
|
||||
static async removeItem (order: Order, orderableId: number, orderableType: OrderableType): Promise<Order> {
|
||||
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/remove_item', { order_token: order.token, orderable_id: orderableId, orderable_type: orderableType });
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async setQuantity (order: Order, orderableId: number, quantity: number): Promise<Order> {
|
||||
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/set_quantity', { order_token: order.token, orderable_id: orderableId, quantity });
|
||||
static async setQuantity (order: Order, orderableId: number, orderableType: OrderableType, quantity: number): Promise<Order> {
|
||||
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/set_quantity', { order_token: order.token, orderable_id: orderableId, orderable_type: orderableType, quantity });
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async setOffer (order: Order, orderableId: number, isOffered: boolean): Promise<Order> {
|
||||
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/set_offer', { order_token: order.token, orderable_id: orderableId, is_offered: isOffered, customer_id: order.user?.id });
|
||||
static async setOffer (order: Order, orderableId: number, orderableType: OrderableType, isOffered: boolean): Promise<Order> {
|
||||
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/set_offer', { order_token: order.token, orderable_id: orderableId, orderable_type: orderableType, is_offered: isOffered, customer_id: order.user?.id });
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async refreshItem (order: Order, orderableId: number): Promise<Order> {
|
||||
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/refresh_item', { order_token: order.token, orderable_id: orderableId });
|
||||
static async refreshItem (order: Order, orderableId: number, orderableType: OrderableType): Promise<Order> {
|
||||
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/refresh_item', { order_token: order.token, orderable_id: orderableId, orderable_type: orderableType });
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
||||
if (errors.length === 1 && errors[0].error === 'not_found') {
|
||||
reloadCart().catch(onError);
|
||||
} else {
|
||||
CartAPI.removeItem(cart, item.orderable_id).then(data => {
|
||||
CartAPI.removeItem(cart, item.orderable_id, item.orderable_type).then(data => {
|
||||
setCart(data);
|
||||
}).catch(onError);
|
||||
}
|
||||
@ -76,7 +76,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
||||
* Change product quantity
|
||||
*/
|
||||
const changeProductQuantity = (e: React.BaseSyntheticEvent, item) => {
|
||||
CartAPI.setQuantity(cart, item.orderable_id, e.target.value)
|
||||
CartAPI.setQuantity(cart, item.orderable_id, item.orderable_type, e.target.value)
|
||||
.then(data => {
|
||||
setCart(data);
|
||||
})
|
||||
@ -87,7 +87,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
||||
* Increment/decrement product quantity
|
||||
*/
|
||||
const increaseOrDecreaseProductQuantity = (item, direction: 'up' | 'down') => {
|
||||
CartAPI.setQuantity(cart, item.orderable_id, direction === 'up' ? item.quantity + 1 : item.quantity - 1)
|
||||
CartAPI.setQuantity(cart, item.orderable_id, item.orderable_type, direction === 'up' ? item.quantity + 1 : item.quantity - 1)
|
||||
.then(data => {
|
||||
setCart(data);
|
||||
})
|
||||
@ -101,7 +101,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
||||
return (e: React.BaseSyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
CartAPI.refreshItem(cart, item.orderable_id).then(data => {
|
||||
CartAPI.refreshItem(cart, item.orderable_id, item.orderable_type).then(data => {
|
||||
setCart(data);
|
||||
}).catch(onError);
|
||||
};
|
||||
@ -185,7 +185,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
||||
// if the selected user is the operator, he cannot offer products to himself
|
||||
if (user.id === currentUser.id && cart.order_items_attributes.filter(item => item.is_offered).length > 0) {
|
||||
Promise.all(cart.order_items_attributes.filter(item => item.is_offered).map(item => {
|
||||
return CartAPI.setOffer(cart, item.orderable_id, false);
|
||||
return CartAPI.setOffer(cart, item.orderable_id, item.orderable_type, false);
|
||||
})).then((data) => setCart({ ...data[data.length - 1], user: { id: user.id, role: user.role } }));
|
||||
} else {
|
||||
setCart({ ...cart, user: { id: user.id, role: user.role } });
|
||||
@ -211,7 +211,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
||||
*/
|
||||
const toggleProductOffer = (item) => {
|
||||
return (checked: boolean) => {
|
||||
CartAPI.setOffer(cart, item.orderable_id, checked).then(data => {
|
||||
CartAPI.setOffer(cart, item.orderable_id, item.orderable_type, checked).then(data => {
|
||||
setCart(data);
|
||||
}).catch(e => {
|
||||
if (e.match(/code 403/)) {
|
||||
|
@ -0,0 +1,64 @@
|
||||
import { IApplication } from '../../models/application';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Loader } from '../base/loader';
|
||||
import { react2angular } from 'react2angular';
|
||||
import type { Slot } from '../../models/slot';
|
||||
import { useImmer } from 'use-immer';
|
||||
import FormatLib from '../../lib/format';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { ShoppingCart } from 'phosphor-react';
|
||||
import CartAPI from '../../api/cart';
|
||||
import useCart from '../../hooks/use-cart';
|
||||
import type { User } from '../../models/user';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface ReservationsSummaryProps {
|
||||
slot: Slot,
|
||||
customer: User,
|
||||
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 { cart, setCart } = useCart(customer);
|
||||
const [pendingSlots, setPendingSlots] = useImmer<Array<Slot>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (slot) {
|
||||
if (pendingSlots.find(s => s.slot_id === slot.slot_id)) {
|
||||
setPendingSlots(draft => draft.filter(s => s.slot_id !== slot.slot_id));
|
||||
} else {
|
||||
setPendingSlots(draft => { draft.push(slot); });
|
||||
}
|
||||
}
|
||||
}, [slot]);
|
||||
|
||||
/**
|
||||
* Add the product to cart
|
||||
*/
|
||||
const addSlotToCart = (slot: Slot) => {
|
||||
return () => {
|
||||
CartAPI.addItem(cart, slot.slot_id, 'Slot', 1).then(setCart).catch(onError);
|
||||
};
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
const ReservationsSummaryWrapper: React.FC<ReservationsSummaryProps> = (props) => (
|
||||
<Loader>
|
||||
<ReservationsSummary {...props} />
|
||||
</Loader>
|
||||
);
|
||||
|
||||
Application.Components.component('reservationsSummary', react2angular(ReservationsSummaryWrapper, ['slot', 'customer', 'onError']));
|
@ -40,7 +40,7 @@ export const StoreProductItem: React.FC<StoreProductItemProps> = ({ product, car
|
||||
const addProductToCart = (e: React.BaseSyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
CartAPI.addItem(cart, product.id, 1).then(onSuccessAddProductToCart).catch(() => {
|
||||
CartAPI.addItem(cart, product.id, 'Product', 1).then(onSuccessAddProductToCart).catch(() => {
|
||||
onError(t('app.public.store_product_item.stock_limit'));
|
||||
});
|
||||
};
|
||||
|
@ -108,7 +108,7 @@ export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, current
|
||||
*/
|
||||
const addToCart = () => {
|
||||
if (toCartCount <= product.stock.external) {
|
||||
CartAPI.addItem(cart, product.id, toCartCount).then(data => {
|
||||
CartAPI.addItem(cart, product.id, 'Product', toCartCount).then(data => {
|
||||
setCart(data);
|
||||
onSuccess(t('app.public.store_product.add_to_cart_success'));
|
||||
}).catch(() => {
|
||||
|
@ -393,12 +393,6 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
||||
// Slot free to be booked
|
||||
const FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_COLOR %>';
|
||||
|
||||
// Slot already booked by another user
|
||||
const UNAVAILABLE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_IS_RESERVED_BY_USER %>';
|
||||
|
||||
// Slot already booked by the current user
|
||||
const BOOKED_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::IS_RESERVED_BY_CURRENT_USER %>';
|
||||
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// bind the machine availabilities with full-Calendar events
|
||||
@ -642,7 +636,6 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
||||
* Callback triggered after a successful prepaid-pack purchase
|
||||
*/
|
||||
$scope.onSuccess = function (message) {
|
||||
|
||||
growl.success(message);
|
||||
};
|
||||
|
||||
|
@ -5,6 +5,8 @@ import { UserRole } from './user';
|
||||
import { Coupon } from './coupon';
|
||||
import { ApiFilter, PaginatedIndex } from './api';
|
||||
|
||||
export type OrderableType = 'Product' | 'Slot';
|
||||
|
||||
export interface Order {
|
||||
id: number,
|
||||
token: string,
|
||||
@ -28,7 +30,7 @@ export interface Order {
|
||||
paid_total?: number,
|
||||
order_items_attributes: Array<{
|
||||
id: number,
|
||||
orderable_type: string,
|
||||
orderable_type: OrderableType,
|
||||
orderable_id: number,
|
||||
orderable_name: string,
|
||||
orderable_slug: string,
|
||||
|
50
app/frontend/src/javascript/models/slot.ts
Normal file
50
app/frontend/src/javascript/models/slot.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { TDateISO } from '../typings/date-iso';
|
||||
|
||||
export interface Slot {
|
||||
slot_id: number,
|
||||
can_modify: boolean,
|
||||
title: string,
|
||||
start: TDateISO,
|
||||
end: TDateISO,
|
||||
is_reserved: boolean,
|
||||
is_completed: boolean,
|
||||
backgroundColor: 'white',
|
||||
|
||||
availability_id: number,
|
||||
slots_reservations_ids: Array<number>,
|
||||
tag_ids: Array<number>,
|
||||
tags: Array<{
|
||||
id: number,
|
||||
name: string,
|
||||
}>
|
||||
plan_ids: Array<number>,
|
||||
|
||||
// the users who booked on this slot, if any
|
||||
users: Array<{
|
||||
id: number,
|
||||
name: string
|
||||
}>,
|
||||
|
||||
borderColor?: '#eeeeee' | '#b2e774' | '#e4cd78' | '#bd7ae9' | '#dd7e6b' | '#3fc7ff' | '#000',
|
||||
// machine
|
||||
machine?: {
|
||||
id: number,
|
||||
name: string
|
||||
},
|
||||
// training
|
||||
nb_total_places?: number,
|
||||
training?: {
|
||||
id: number,
|
||||
name: string,
|
||||
description: string,
|
||||
machines: Array<{
|
||||
id: number,
|
||||
name: string
|
||||
}>
|
||||
},
|
||||
// space
|
||||
space?: {
|
||||
id: number,
|
||||
name: string
|
||||
}
|
||||
}
|
@ -2,16 +2,22 @@
|
||||
|
||||
<section class="heading b-b">
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<div class="col-xs-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
|
||||
<div class="col-xs-8 col-md-10 b-l">
|
||||
<section class="heading-title">
|
||||
<h1>{{ 'app.logged.machines_reserve.machine_planning' | translate }} : {{machine.name}}</h1>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-2 col-md-1 b-t hide-b-md p-none">
|
||||
<section class="heading-actions">
|
||||
<cart-button />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -39,6 +45,8 @@
|
||||
refresh="afterPaymentPromise">
|
||||
</packs-summary>
|
||||
|
||||
<reservations-summary slot="selectedEvent" customer="ctrl.member" on-error="onError"></reservations-summary>
|
||||
|
||||
<cart slot="selectedEvent"
|
||||
slot-selection-time="selectionTime"
|
||||
events="events"
|
||||
|
@ -7,7 +7,6 @@ module AvailabilityHelper
|
||||
SPACE_COLOR = '#3fc7ff'
|
||||
EVENT_COLOR = '#dd7e6b'
|
||||
IS_RESERVED_BY_CURRENT_USER = '#b2e774'
|
||||
MACHINE_IS_RESERVED_BY_USER = '#1d98ec'
|
||||
IS_FULL = '#eeeeee'
|
||||
|
||||
def availability_border_color(availability)
|
||||
|
@ -1,15 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Items that can be added to the shopping cart
|
||||
module CartItem; end
|
||||
module CartItem
|
||||
def self.table_name_prefix
|
||||
'cart_item_'
|
||||
end
|
||||
end
|
||||
|
||||
# This is an abstract class implemented by classes that can be added to the shopping cart
|
||||
class CartItem::BaseItem
|
||||
attr_reader :errors
|
||||
|
||||
def initialize(*)
|
||||
@errors = {}
|
||||
end
|
||||
class CartItem::BaseItem < ApplicationRecord
|
||||
self.abstract_class = true
|
||||
|
||||
def price
|
||||
{ elements: {}, amount: 0 }
|
||||
|
@ -1,20 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# A discount coupon applied to the whole shopping cart
|
||||
class CartItem::Coupon
|
||||
attr_reader :errors
|
||||
class CartItem::Coupon < ApplicationRecord
|
||||
belongs_to :operator_profile, class_name: 'InvoicingProfile'
|
||||
belongs_to :customer_profile, class_name: 'InvoicingProfile'
|
||||
belongs_to :coupon
|
||||
|
||||
# @param coupon {String|Coupon} may be nil or empty string if no coupons are applied
|
||||
def initialize(customer, operator, coupon)
|
||||
@customer = customer
|
||||
@operator = operator
|
||||
@coupon = coupon
|
||||
@errors = {}
|
||||
def operator
|
||||
operator_profile.user
|
||||
end
|
||||
|
||||
def coupon
|
||||
cs = CouponService.new
|
||||
cs.validate(@coupon, @customer.id)
|
||||
def customer
|
||||
customer_profile.user
|
||||
end
|
||||
|
||||
def price(cart_total = 0)
|
||||
@ -31,11 +28,10 @@ class CartItem::Coupon
|
||||
end
|
||||
|
||||
def valid?(_all_items)
|
||||
return true if @coupon.nil?
|
||||
return true if coupon.nil?
|
||||
|
||||
c = ::Coupon.find_by(code: @coupon)
|
||||
if c.nil? || c.status(@customer.id) != 'active'
|
||||
@errors[:item] = 'coupon is invalid'
|
||||
if coupon.status(customer.id) != 'active'
|
||||
errors.add(:coupon, 'invalid coupon')
|
||||
return false
|
||||
end
|
||||
true
|
||||
|
@ -2,30 +2,38 @@
|
||||
|
||||
# An event reservation added to the shopping cart
|
||||
class CartItem::EventReservation < CartItem::Reservation
|
||||
# @param normal_tickets {Number} number of tickets at the normal price
|
||||
# @param other_tickets {Array<{booked: Number, event_price_category_id: Number}>}
|
||||
def initialize(customer, operator, event, slots, normal_tickets: 0, other_tickets: [])
|
||||
raise TypeError unless event.is_a? Event
|
||||
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'
|
||||
accepts_nested_attributes_for :cart_item_event_reservation_tickets
|
||||
|
||||
super(customer, operator, event, slots)
|
||||
@normal_tickets = normal_tickets || 0
|
||||
@other_tickets = other_tickets || []
|
||||
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
|
||||
|
||||
belongs_to :operator_profile, class_name: 'InvoicingProfile'
|
||||
belongs_to :customer_profile, class_name: 'InvoicingProfile'
|
||||
|
||||
belongs_to :event
|
||||
|
||||
def reservable
|
||||
event
|
||||
end
|
||||
|
||||
def price
|
||||
amount = @reservable.amount * @normal_tickets
|
||||
is_privileged = @operator.privileged? && @operator.id != @customer.id
|
||||
amount = reservable.amount * normal_tickets
|
||||
is_privileged = operator.privileged? && operator.id != customer.id
|
||||
|
||||
@other_tickets.each do |ticket|
|
||||
amount += ticket[:booked] * EventPriceCategory.find(ticket[:event_price_category_id]).amount
|
||||
cart_item_event_reservation_tickets.each do |ticket|
|
||||
amount += ticket.booked * ticket.event_price_category.amount
|
||||
end
|
||||
|
||||
elements = { slots: [] }
|
||||
total = 0
|
||||
|
||||
@slots.each do |slot|
|
||||
cart_item_reservation_slots.each do |sr|
|
||||
total += get_slot_price(amount,
|
||||
slot,
|
||||
sr,
|
||||
is_privileged,
|
||||
elements: elements,
|
||||
is_division: false)
|
||||
@ -36,26 +44,25 @@ class CartItem::EventReservation < CartItem::Reservation
|
||||
|
||||
def to_object
|
||||
::Reservation.new(
|
||||
reservable_id: @reservable.id,
|
||||
reservable_id: reservable.id,
|
||||
reservable_type: Event.name,
|
||||
slots_reservations_attributes: slots_params,
|
||||
tickets_attributes: tickets_params,
|
||||
nb_reserve_places: @normal_tickets,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
||||
tickets_attributes: cart_item_event_reservation_tickets.map do |t|
|
||||
{
|
||||
event_price_category_id: t.event_price_category_id,
|
||||
booked: t.booked
|
||||
}
|
||||
end,
|
||||
nb_reserve_places: normal_tickets,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: customer).id
|
||||
)
|
||||
end
|
||||
|
||||
def name
|
||||
@reservable.title
|
||||
reservable.title
|
||||
end
|
||||
|
||||
def type
|
||||
'event'
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def tickets_params
|
||||
@other_tickets.map { |ticket| ticket.permit(:event_price_category_id, :booked) }
|
||||
end
|
||||
end
|
||||
|
7
app/models/cart_item/event_reservation_ticket.rb
Normal file
7
app/models/cart_item/event_reservation_ticket.rb
Normal file
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# A relation table between a pending event reservation and a special price for this event
|
||||
class CartItem::EventReservationTicket < ApplicationRecord
|
||||
belongs_to :cart_item_event_reservation, class_name: 'CartItem::EventReservation', inverse_of: :cart_item_event_reservation_tickets
|
||||
belongs_to :event_price_category, inverse_of: :cart_item_event_reservation_tickets
|
||||
end
|
@ -2,22 +2,20 @@
|
||||
|
||||
# A subscription extended for free, added to the shopping cart
|
||||
class CartItem::FreeExtension < CartItem::BaseItem
|
||||
def initialize(customer, subscription, new_expiration_date)
|
||||
raise TypeError unless subscription.is_a? Subscription
|
||||
belongs_to :customer_profile, class_name: 'InvoicingProfile'
|
||||
belongs_to :subscription
|
||||
|
||||
@customer = customer
|
||||
@new_expiration_date = new_expiration_date
|
||||
@subscription = subscription
|
||||
super
|
||||
def customer
|
||||
statistic_profile.user
|
||||
end
|
||||
|
||||
def start_at
|
||||
raise InvalidSubscriptionError if @subscription.nil?
|
||||
if @new_expiration_date.nil? || @new_expiration_date <= @subscription.expired_at
|
||||
raise InvalidSubscriptionError if subscription.nil?
|
||||
if new_expiration_date.nil? || new_expiration_date <= subscription.expired_at
|
||||
raise InvalidSubscriptionError, I18n.t('cart_items.must_be_after_expiration')
|
||||
end
|
||||
|
||||
@subscription.expired_at
|
||||
subscription.expired_at
|
||||
end
|
||||
|
||||
def price
|
||||
@ -27,14 +25,14 @@ class CartItem::FreeExtension < CartItem::BaseItem
|
||||
end
|
||||
|
||||
def name
|
||||
I18n.t('cart_items.free_extension', DATE: I18n.l(@new_expiration_date))
|
||||
I18n.t('cart_items.free_extension', DATE: I18n.l(new_expiration_date))
|
||||
end
|
||||
|
||||
def to_object
|
||||
::OfferDay.new(
|
||||
subscription_id: @subscription.id,
|
||||
subscription_id: subscription.id,
|
||||
start_at: start_at,
|
||||
end_at: @new_expiration_date
|
||||
end_at: new_expiration_date
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -2,46 +2,40 @@
|
||||
|
||||
# A machine reservation added to the shopping cart
|
||||
class CartItem::MachineReservation < CartItem::Reservation
|
||||
# @param plan {Plan} a subscription bought at the same time of the reservation OR an already running subscription
|
||||
# @param new_subscription {Boolean} true is new subscription is being bought at the same time of the reservation
|
||||
def initialize(customer, operator, machine, slots, plan: nil, new_subscription: false)
|
||||
raise TypeError unless machine.is_a? Machine
|
||||
self.table_name = 'cart_item_reservations'
|
||||
|
||||
super(customer, operator, machine, slots)
|
||||
@plan = plan
|
||||
@new_subscription = new_subscription
|
||||
end
|
||||
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
|
||||
|
||||
def to_object
|
||||
::Reservation.new(
|
||||
reservable_id: @reservable.id,
|
||||
reservable_type: Machine.name,
|
||||
slots_reservations_attributes: slots_params,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
||||
)
|
||||
end
|
||||
belongs_to :operator_profile, class_name: 'InvoicingProfile'
|
||||
belongs_to :customer_profile, class_name: 'InvoicingProfile'
|
||||
|
||||
belongs_to :reservable, polymorphic: true
|
||||
|
||||
belongs_to :plan
|
||||
|
||||
def type
|
||||
'machine'
|
||||
end
|
||||
|
||||
def valid?(all_items)
|
||||
@slots.each do |slot|
|
||||
cart_item_reservation_slots.each do |slot|
|
||||
same_hour_slots = SlotsReservation.joins(:reservation).where(
|
||||
reservations: { reservable: @reservable },
|
||||
reservations: { reservable: reservable },
|
||||
slot_id: slot[:slot_id],
|
||||
canceled_at: nil
|
||||
).count
|
||||
if same_hour_slots.positive?
|
||||
@errors[:slot] = I18n.t('cart_item_validation.reserved')
|
||||
errors.add(:slot, I18n.t('cart_item_validation.reserved'))
|
||||
return false
|
||||
end
|
||||
if @reservable.disabled
|
||||
@errors[:reservable] = I18n.t('cart_item_validation.machine')
|
||||
if reservable.disabled
|
||||
errors.add(:reservable, I18n.t('cart_item_validation.machine'))
|
||||
return false
|
||||
end
|
||||
unless @reservable.reservable
|
||||
@errors[:reservable] = I18n.t('cart_item_validation.reservable')
|
||||
unless reservable.reservable
|
||||
errors.add(:reservable, I18n.t('cart_item_validation.reservable'))
|
||||
return false
|
||||
end
|
||||
end
|
||||
@ -52,9 +46,9 @@ class CartItem::MachineReservation < CartItem::Reservation
|
||||
protected
|
||||
|
||||
def credits
|
||||
return 0 if @plan.nil?
|
||||
return 0 if plan.nil?
|
||||
|
||||
machine_credit = @plan.machine_credits.find { |credit| credit.creditable_id == @reservable.id }
|
||||
credits_hours(machine_credit, new_plan_being_bought: @new_subscription)
|
||||
machine_credit = plan.machine_credits.find { |credit| credit.creditable_id == reservable.id }
|
||||
credits_hours(machine_credit, new_plan_being_bought: new_subscription)
|
||||
end
|
||||
end
|
||||
|
@ -1,23 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# A payment schedule applied to plan in the shopping cart
|
||||
class CartItem::PaymentSchedule
|
||||
attr_reader :requested, :errors
|
||||
class CartItem::PaymentSchedule < ApplicationRecord
|
||||
belongs_to :customer_profile, class_name: 'InvoicingProfile'
|
||||
belongs_to :coupon
|
||||
belongs_to :plan
|
||||
|
||||
def initialize(plan, coupon, requested, customer, start_at = nil)
|
||||
raise TypeError unless coupon.is_a? CartItem::Coupon
|
||||
|
||||
@plan = plan
|
||||
@coupon = coupon
|
||||
@requested = requested
|
||||
@customer = customer
|
||||
@start_at = start_at
|
||||
@errors = {}
|
||||
def customer
|
||||
customer_profile.user
|
||||
end
|
||||
|
||||
def schedule(total, total_without_coupon)
|
||||
schedule = if @requested && @plan&.monthly_payment
|
||||
PaymentScheduleService.new.compute(@plan, total_without_coupon, @customer, coupon: @coupon.coupon, start_at: @start_at)
|
||||
schedule = if requested && plan&.monthly_payment
|
||||
PaymentScheduleService.new.compute(plan, total_without_coupon, customer, coupon: coupon.coupon, start_at: start_at)
|
||||
else
|
||||
nil
|
||||
end
|
||||
@ -36,10 +31,10 @@ class CartItem::PaymentSchedule
|
||||
end
|
||||
|
||||
def valid?(_all_items)
|
||||
return true unless @requested && @plan&.monthly_payment
|
||||
return true unless requested && plan&.monthly_payment
|
||||
|
||||
if @plan&.disabled
|
||||
@errors[:item] = I18n.t('cart_item_validation.plan')
|
||||
if plan&.disabled
|
||||
errors.add(:plan, I18n.t('cart_item_validation.plan'))
|
||||
return false
|
||||
end
|
||||
true
|
||||
|
@ -2,18 +2,14 @@
|
||||
|
||||
# A prepaid-pack added to the shopping cart
|
||||
class CartItem::PrepaidPack < CartItem::BaseItem
|
||||
def initialize(pack, customer)
|
||||
raise TypeError unless pack.is_a? PrepaidPack
|
||||
belongs_to :prepaid_pack
|
||||
|
||||
@pack = pack
|
||||
@customer = customer
|
||||
super
|
||||
def customer
|
||||
customer_profile.user
|
||||
end
|
||||
|
||||
def pack
|
||||
raise InvalidGroupError if @pack.group_id != @customer.group_id
|
||||
|
||||
@pack
|
||||
prepaid_pack
|
||||
end
|
||||
|
||||
def price
|
||||
@ -24,13 +20,13 @@ class CartItem::PrepaidPack < CartItem::BaseItem
|
||||
end
|
||||
|
||||
def name
|
||||
"#{@pack.minutes / 60} h"
|
||||
"#{pack.minutes / 60} h"
|
||||
end
|
||||
|
||||
def to_object
|
||||
::StatisticProfilePrepaidPack.new(
|
||||
prepaid_pack_id: @pack.id,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
||||
prepaid_pack_id: pack.id,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: customer).id
|
||||
)
|
||||
end
|
||||
|
||||
@ -39,10 +35,14 @@ class CartItem::PrepaidPack < CartItem::BaseItem
|
||||
end
|
||||
|
||||
def valid?(_all_items)
|
||||
if @pack.disabled
|
||||
if pack.disabled
|
||||
@errors[:item] = I18n.t('cart_item_validation.pack')
|
||||
return false
|
||||
end
|
||||
if pack.group_id != customer.group_id
|
||||
@errors[:group] = "pack is reserved for members of group #{pack.group.name}"
|
||||
return false
|
||||
end
|
||||
true
|
||||
end
|
||||
end
|
||||
|
@ -7,17 +7,27 @@ GET_SLOT_PRICE_DEFAULT_OPTS = { has_credits: false, elements: nil, is_division:
|
||||
|
||||
# A generic reservation added to the shopping cart
|
||||
class CartItem::Reservation < CartItem::BaseItem
|
||||
def initialize(customer, operator, reservable, slots)
|
||||
@customer = customer
|
||||
@operator = operator
|
||||
@reservable = reservable
|
||||
@slots = slots.map { |s| expand_slot(s) }
|
||||
super
|
||||
self.abstract_class = true
|
||||
|
||||
def reservable
|
||||
nil
|
||||
end
|
||||
|
||||
def plan
|
||||
nil
|
||||
end
|
||||
|
||||
def operator
|
||||
operator_profile.user
|
||||
end
|
||||
|
||||
def customer
|
||||
customer_profile.user
|
||||
end
|
||||
|
||||
def price
|
||||
is_privileged = @operator.privileged? && @operator.id != @customer.id
|
||||
prepaid = { minutes: PrepaidPackService.minutes_available(@customer, @reservable) }
|
||||
is_privileged = operator.privileged? && operator.id != customer.id
|
||||
prepaid = { minutes: PrepaidPackService.minutes_available(customer, reservable) }
|
||||
|
||||
raise InvalidGroupError, I18n.t('cart_items.group_subscription_mismatch') if !@plan.nil? && @customer.group_id != @plan.group_id
|
||||
|
||||
@ -39,7 +49,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
end
|
||||
|
||||
def name
|
||||
@reservable.name
|
||||
reservable&.name
|
||||
end
|
||||
|
||||
def valid?(all_items)
|
||||
@ -48,39 +58,48 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
reservation_deadline_minutes = Setting.get('reservation_deadline').to_i
|
||||
reservation_deadline = reservation_deadline_minutes.minutes.since
|
||||
|
||||
@slots.each do |slot|
|
||||
slot_db = Slot.find(slot[:slot_id])
|
||||
if slot_db.nil?
|
||||
@errors[:slot] = I18n.t('cart_item_validation.slot')
|
||||
cart_item_reservation_slots.each do |sr|
|
||||
slot = sr.slot
|
||||
if slot.nil?
|
||||
errors.add(:slot, I18n.t('cart_item_validation.slot'))
|
||||
return false
|
||||
end
|
||||
|
||||
availability = Availability.find_by(id: slot[:slot_attributes][:availability_id])
|
||||
availability = slot.availability
|
||||
if availability.nil?
|
||||
@errors[:availability] = I18n.t('cart_item_validation.availability')
|
||||
errors.add(:availability, I18n.t('cart_item_validation.availability'))
|
||||
return false
|
||||
end
|
||||
|
||||
if slot_db.full?
|
||||
@errors[:slot] = I18n.t('cart_item_validation.full')
|
||||
if slot.full?
|
||||
errors.add(:slot, I18n.t('cart_item_validation.full')
|
||||
return false
|
||||
end
|
||||
|
||||
if slot_db.start_at < reservation_deadline && !@operator.privileged?
|
||||
@errors[:slot] = I18n.t('cart_item_validation.deadline', { MINUTES: reservation_deadline_minutes })
|
||||
if slot.start_at < reservation_deadline && !operator.privileged?
|
||||
errors.add(:slot, I18n.t('cart_item_validation.deadline', { MINUTES: reservation_deadline_minutes }))
|
||||
return false
|
||||
end
|
||||
|
||||
next if availability.plan_ids.empty?
|
||||
next if required_subscription?(availability, pending_subscription)
|
||||
|
||||
@errors[:availability] = I18n.t('cart_item_validation.restricted')
|
||||
errors.add(:availability, I18n.t('cart_item_validation.restricted'))
|
||||
return false
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def to_object
|
||||
::Reservation.new(
|
||||
reservable_id: reservable_id,
|
||||
reservable_type: reservable_type,
|
||||
slots_reservations_attributes: slots_params,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: customer).id
|
||||
)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def credits
|
||||
@ -91,13 +110,9 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
# Group the slots by date, if the extended_prices_in_same_day option is set to true
|
||||
##
|
||||
def grouped_slots
|
||||
return { all: @slots } unless Setting.get('extended_prices_in_same_day')
|
||||
return { all: cart_item_reservation_slots } unless Setting.get('extended_prices_in_same_day')
|
||||
|
||||
@slots.group_by { |slot| slot[:slot_attributes][:start_at].to_date }
|
||||
end
|
||||
|
||||
def expand_slot(slot)
|
||||
slot.merge({ slot_attributes: Slot.find(slot[:slot_id]) })
|
||||
cart_item_reservation_slots.group_by { |slot| slot.slot[:start_at].to_date }
|
||||
end
|
||||
|
||||
##
|
||||
@ -105,16 +120,16 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
# @param prices {{ prices: Array<{price: Price, duration: number}> }} list of prices to use with the current reservation
|
||||
# @see get_slot_price
|
||||
##
|
||||
def get_slot_price_from_prices(prices, slot, is_privileged, options = {})
|
||||
def get_slot_price_from_prices(prices, slot_reservation, is_privileged, options = {})
|
||||
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
||||
|
||||
slot_minutes = (slot[:slot_attributes][:end_at].to_time - slot[:slot_attributes][:start_at].to_time) / SECONDS_PER_MINUTE
|
||||
slot_minutes = (slot_reservation.slot[:end_at].to_time - slot_reservation.slot[:start_at].to_time) / SECONDS_PER_MINUTE
|
||||
price = prices[:prices].find { |p| p[:duration] <= slot_minutes && p[:duration].positive? }
|
||||
price = prices[:prices].first if price.nil?
|
||||
hourly_rate = ((Rational(price[:price].amount.to_f) / Rational(price[:price].duration)) * Rational(MINUTES_PER_HOUR)).to_f
|
||||
|
||||
# apply the base price to the real slot duration
|
||||
real_price = get_slot_price(hourly_rate, slot, is_privileged, options)
|
||||
real_price = get_slot_price(hourly_rate, slot_reservation, is_privileged, options)
|
||||
|
||||
price[:duration] -= slot_minutes
|
||||
|
||||
@ -125,7 +140,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
# Compute the price of a single slot, according to the base price and the ability for an admin
|
||||
# to offer the slot.
|
||||
# @param hourly_rate {Number} base price of a slot
|
||||
# @param slot {Hash} Slot object
|
||||
# @param slot_reservation {CartItem::ReservationSlot}
|
||||
# @param is_privileged {Boolean} true if the current user has a privileged role (admin or manager)
|
||||
# @param [options] {Hash} optional parameters, allowing the following options:
|
||||
# - elements {Array} if provided the resulting price will be append into elements.slots
|
||||
@ -134,11 +149,11 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
# - prepaid_minutes {Number} number of remaining prepaid minutes for the customer
|
||||
# @return {Number} price of the slot
|
||||
##
|
||||
def get_slot_price(hourly_rate, slot, is_privileged, options = {})
|
||||
def get_slot_price(hourly_rate, slot_reservation, is_privileged, options = {})
|
||||
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
||||
|
||||
slot_rate = options[:has_credits] || (slot[:offered] && is_privileged) ? 0 : hourly_rate
|
||||
slot_minutes = (slot[:slot_attributes][:end_at].to_time - slot[:slot_attributes][:start_at].to_time) / SECONDS_PER_MINUTE
|
||||
slot_rate = options[:has_credits] || (slot_reservation[:offered] && is_privileged) ? 0 : hourly_rate
|
||||
slot_minutes = (slot_reservation.slot[:end_at].to_time - slot_reservation.slot[:start_at].to_time) / SECONDS_PER_MINUTE
|
||||
# apply the base price to the real slot duration
|
||||
real_price = if options[:is_division]
|
||||
((Rational(slot_rate) / Rational(MINUTES_PER_HOUR)) * Rational(slot_minutes)).to_f
|
||||
@ -155,7 +170,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
|
||||
unless options[:elements].nil?
|
||||
options[:elements][:slots].push(
|
||||
start_at: slot[:slot_attributes][:start_at],
|
||||
start_at: slot_reservation.slot[:start_at],
|
||||
price: real_price,
|
||||
promo: (slot_rate != hourly_rate)
|
||||
)
|
||||
@ -168,19 +183,19 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
# Eg. If the reservation is for 12 hours, and there are prices for 3 hours, 7 hours,
|
||||
# and the base price (1 hours), we use the 7 hours price, then 3 hours price, and finally the base price twice (7+3+1+1 = 12).
|
||||
# All these prices are returned to be applied to the reservation.
|
||||
def applicable_prices(slots)
|
||||
total_duration = slots.map do |slot|
|
||||
(slot[:slot_attributes][:end_at].to_time - slot[:slot_attributes][:start_at].to_time) / SECONDS_PER_MINUTE
|
||||
def applicable_prices(slots_reservations)
|
||||
total_duration = slots_reservations.map do |slot|
|
||||
(slot.slot[:end_at].to_time - slot.slot[:start_at].to_time) / SECONDS_PER_MINUTE
|
||||
end.reduce(:+)
|
||||
rates = { prices: [] }
|
||||
|
||||
remaining_duration = total_duration
|
||||
while remaining_duration.positive?
|
||||
max_duration = @reservable.prices.where(group_id: @customer.group_id, plan_id: @plan.try(:id))
|
||||
.where(Price.arel_table[:duration].lteq(remaining_duration))
|
||||
.maximum(:duration)
|
||||
max_duration = reservable&.prices&.where(group_id: customer.group_id, plan_id: plan.try(:id))
|
||||
&.where(Price.arel_table[:duration].lteq(remaining_duration))
|
||||
&.maximum(:duration)
|
||||
max_duration = 60 if max_duration.nil?
|
||||
max_duration_price = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id), duration: max_duration)
|
||||
max_duration_price = reservable&.prices&.find_by(group_id: customer.group_id, plan_id: plan.try(:id), duration: max_duration)
|
||||
|
||||
current_duration = [remaining_duration, max_duration].min
|
||||
rates[:prices].push(price: max_duration_price, duration: current_duration)
|
||||
@ -200,14 +215,14 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
|
||||
hours_available = credits.hours
|
||||
unless new_plan_being_bought
|
||||
user_credit = @customer.users_credits.find_by(credit_id: credits.id)
|
||||
user_credit = customer.users_credits.find_by(credit_id: credits.id)
|
||||
hours_available = credits.hours - user_credit.hours_used if user_credit
|
||||
end
|
||||
hours_available
|
||||
end
|
||||
|
||||
def slots_params
|
||||
@slots.map { |slot| slot.permit(:id, :slot_id, :offered) }
|
||||
cart_item_reservation_slots.map { |sr| { id: sr.slots_reservation_id, slot_id: sr.slot_id, offered: sr.offered } }
|
||||
end
|
||||
|
||||
##
|
||||
@ -215,9 +230,9 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
# has the required susbcription, otherwise, check if the operator is privileged
|
||||
##
|
||||
def required_subscription?(availability, pending_subscription)
|
||||
(@customer.subscribed_plan && availability.plan_ids.include?(@customer.subscribed_plan.id)) ||
|
||||
(customer.subscribed_plan && availability.plan_ids.include?(customer.subscribed_plan.id)) ||
|
||||
(pending_subscription && availability.plan_ids.include?(pending_subscription.plan.id)) ||
|
||||
(@operator.manager? && @customer.id != @operator.id) ||
|
||||
@operator.admin?
|
||||
(operator.manager? && customer.id != operator.id) ||
|
||||
operator.admin?
|
||||
end
|
||||
end
|
||||
|
9
app/models/cart_item/reservation_slot.rb
Normal file
9
app/models/cart_item/reservation_slot.rb
Normal file
@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# A relation table between a pending reservation and a slot
|
||||
class CartItem::ReservationSlot < ApplicationRecord
|
||||
belongs_to :cart_item, polymorphic: true
|
||||
|
||||
belongs_to :slot
|
||||
belongs_to :slots_reservation
|
||||
end
|
@ -2,33 +2,26 @@
|
||||
|
||||
# A space reservation added to the shopping cart
|
||||
class CartItem::SpaceReservation < CartItem::Reservation
|
||||
# @param plan {Plan} a subscription bought at the same time of the reservation OR an already running subscription
|
||||
# @param new_subscription {Boolean} true is new subscription is being bought at the same time of the reservation
|
||||
def initialize(customer, operator, space, slots, plan: nil, new_subscription: false)
|
||||
raise TypeError unless space.is_a? Space
|
||||
self.table_name = 'cart_item_reservations'
|
||||
|
||||
super(customer, operator, space, slots)
|
||||
@plan = plan
|
||||
@space = space
|
||||
@new_subscription = new_subscription
|
||||
end
|
||||
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
|
||||
|
||||
def to_object
|
||||
::Reservation.new(
|
||||
reservable_id: @reservable.id,
|
||||
reservable_type: Space.name,
|
||||
slots_reservations_attributes: slots_params,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
||||
)
|
||||
end
|
||||
belongs_to :operator_profile, class_name: 'InvoicingProfile'
|
||||
belongs_to :customer_profile, class_name: 'InvoicingProfile'
|
||||
|
||||
belongs_to :reservable, polymorphic: true
|
||||
|
||||
belongs_to :plan
|
||||
|
||||
def type
|
||||
'space'
|
||||
end
|
||||
|
||||
def valid?(all_items)
|
||||
if @space.disabled
|
||||
@errors[:reservable] = I18n.t('cart_item_validation.space')
|
||||
if reservable.disabled
|
||||
errors.add(:reservable, I18n.t('cart_item_validation.space'))
|
||||
return false
|
||||
end
|
||||
|
||||
@ -38,9 +31,9 @@ class CartItem::SpaceReservation < CartItem::Reservation
|
||||
protected
|
||||
|
||||
def credits
|
||||
return 0 if @plan.nil?
|
||||
return 0 if plan.nil?
|
||||
|
||||
space_credit = @plan.space_credits.find { |credit| credit.creditable_id == @reservable.id }
|
||||
credits_hours(space_credit, new_plan_being_bought: @new_subscription)
|
||||
space_credit = plan.space_credits.find { |credit| credit.creditable_id == reservable.id }
|
||||
credits_hours(space_credit, new_plan_being_bought: new_subscription)
|
||||
end
|
||||
end
|
||||
|
@ -2,21 +2,11 @@
|
||||
|
||||
# A subscription added to the shopping cart
|
||||
class CartItem::Subscription < CartItem::BaseItem
|
||||
attr_reader :start_at
|
||||
belongs_to :plan
|
||||
belongs_to :customer_profile, class_name: 'InvoicingProfile'
|
||||
|
||||
def initialize(plan, customer, start_at = nil)
|
||||
raise TypeError unless plan.is_a? Plan
|
||||
|
||||
@plan = plan
|
||||
@customer = customer
|
||||
@start_at = start_at
|
||||
super
|
||||
end
|
||||
|
||||
def plan
|
||||
raise InvalidGroupError if @plan.group_id != @customer.group_id
|
||||
|
||||
@plan
|
||||
def customer
|
||||
customer_profile.user
|
||||
end
|
||||
|
||||
def price
|
||||
@ -27,14 +17,14 @@ class CartItem::Subscription < CartItem::BaseItem
|
||||
end
|
||||
|
||||
def name
|
||||
@plan.base_name
|
||||
plan.base_name
|
||||
end
|
||||
|
||||
def to_object
|
||||
::Subscription.new(
|
||||
plan_id: @plan.id,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: @customer).id,
|
||||
start_at: @start_at
|
||||
plan_id: plan.id,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: customer).id,
|
||||
start_at: start_at
|
||||
)
|
||||
end
|
||||
|
||||
@ -43,8 +33,12 @@ class CartItem::Subscription < CartItem::BaseItem
|
||||
end
|
||||
|
||||
def valid?(_all_items)
|
||||
if @plan.disabled
|
||||
@errors[:item] = I18n.t('cart_item_validation.plan')
|
||||
if plan.disabled
|
||||
errors.add(:plan, I18n.t('cart_item_validation.plan'))
|
||||
return false
|
||||
end
|
||||
if plan.group_id != customer.group_id
|
||||
errors.add(:group, "plan is reserved for members of group #{plan.group.name}")
|
||||
return false
|
||||
end
|
||||
true
|
||||
|
@ -2,45 +2,39 @@
|
||||
|
||||
# A training reservation added to the shopping cart
|
||||
class CartItem::TrainingReservation < CartItem::Reservation
|
||||
# @param plan {Plan} a subscription bought at the same time of the reservation OR an already running subscription
|
||||
# @param new_subscription {Boolean} true is new subscription is being bought at the same time of the reservation
|
||||
def initialize(customer, operator, training, slots, plan: nil, new_subscription: false)
|
||||
raise TypeError unless training.is_a? Training
|
||||
self.table_name = 'cart_item_reservations'
|
||||
|
||||
super(customer, operator, training, slots)
|
||||
@plan = plan
|
||||
@new_subscription = new_subscription
|
||||
end
|
||||
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
|
||||
|
||||
belongs_to :operator_profile, class_name: 'InvoicingProfile'
|
||||
belongs_to :customer_profile, class_name: 'InvoicingProfile'
|
||||
|
||||
belongs_to :reservable, polymorphic: true
|
||||
|
||||
belongs_to :plan
|
||||
|
||||
def price
|
||||
base_amount = @reservable.amount_by_group(@customer.group_id).amount
|
||||
is_privileged = @operator.admin? || (@operator.manager? && @operator.id != @customer.id)
|
||||
base_amount = reservable&.amount_by_group(customer.group_id)&.amount
|
||||
is_privileged = operator.admin? || (operator.manager? && operator.id != customer.id)
|
||||
|
||||
elements = { slots: [] }
|
||||
amount = 0
|
||||
|
||||
hours_available = credits
|
||||
@slots.each do |slot|
|
||||
cart_item_reservation_slots.each do |sr|
|
||||
amount += get_slot_price(base_amount,
|
||||
slot,
|
||||
sr,
|
||||
is_privileged,
|
||||
elements: elements,
|
||||
has_credits: (@customer.training_credits.size < hours_available),
|
||||
has_credits: (customer.training_credits.size < hours_available),
|
||||
is_division: false)
|
||||
end
|
||||
|
||||
{ elements: elements, amount: amount }
|
||||
end
|
||||
|
||||
def to_object
|
||||
::Reservation.new(
|
||||
reservable_id: @reservable.id,
|
||||
reservable_type: Training.name,
|
||||
slots_reservations_attributes: slots_params,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
||||
)
|
||||
end
|
||||
|
||||
def type
|
||||
'training'
|
||||
end
|
||||
@ -48,9 +42,9 @@ class CartItem::TrainingReservation < CartItem::Reservation
|
||||
protected
|
||||
|
||||
def credits
|
||||
return 0 if @plan.nil?
|
||||
return 0 if plan.nil?
|
||||
|
||||
is_creditable = @plan.training_credits.select { |credit| credit.creditable_id == @reservable.id }.any?
|
||||
is_creditable ? @plan.training_credit_nb : 0
|
||||
is_creditable = plan&.training_credits&.select { |credit| credit.creditable_id == reservable&.id }&.any?
|
||||
is_creditable ? plan&.training_credit_nb : 0
|
||||
end
|
||||
end
|
||||
|
@ -2,16 +2,18 @@
|
||||
|
||||
# Coupon is a textual code associated with a discount rate or an amount of discount
|
||||
class Coupon < ApplicationRecord
|
||||
has_many :invoices, dependent: :nullify
|
||||
has_many :payment_schedule, dependent: :nullify
|
||||
has_many :orders, dependent: :nullify
|
||||
has_many :invoices, dependent: :restrict_with_error
|
||||
has_many :payment_schedule, dependent: :restrict_with_error
|
||||
has_many :orders, dependent: :restrict_with_error
|
||||
|
||||
has_many :cart_item_coupons, class_name: 'CartItem::Coupon', dependent: :destroy
|
||||
|
||||
after_create :create_gateway_coupon
|
||||
before_destroy :delete_gateway_coupon
|
||||
|
||||
validates :name, presence: true
|
||||
validates :code, presence: true
|
||||
validates :code, format: { with: /\A[A-Z0-9\-]+\z/, message: I18n.t('coupon.invalid_format') }
|
||||
validates :code, format: { with: /\A[A-Z0-9\-]+\z/, message: I18n.t('coupon.code_format_error') }
|
||||
validates :code, uniqueness: true
|
||||
validates :validity_per_user, presence: true
|
||||
validates :validity_per_user, inclusion: { in: %w[once forever] }
|
||||
|
@ -31,6 +31,8 @@ class Event < ApplicationRecord
|
||||
has_one :advanced_accounting, as: :accountable, dependent: :destroy
|
||||
accepts_nested_attributes_for :advanced_accounting, allow_destroy: true
|
||||
|
||||
has_many :cart_item_event_reservations, class_name: 'CartItem::EventReservation', dependent: :destroy
|
||||
|
||||
attr_accessor :recurrence, :recurrence_end_at
|
||||
|
||||
before_save :update_nb_free_places
|
||||
|
@ -5,7 +5,8 @@ class EventPriceCategory < ApplicationRecord
|
||||
belongs_to :event
|
||||
belongs_to :price_category
|
||||
|
||||
has_many :tickets
|
||||
has_many :tickets, dependent: :restrict_with_error
|
||||
has_many :cart_item_event_reservation_tickets, class_name: 'CartItem::EventReservationTicket', dependent: :restrict_with_error
|
||||
|
||||
validates :price_category_id, presence: true
|
||||
validates :amount, presence: true
|
||||
@ -17,5 +18,4 @@ class EventPriceCategory < ApplicationRecord
|
||||
def verify_no_associated_tickets
|
||||
throw(:abort) unless tickets.count.zero?
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -27,6 +27,25 @@ class InvoicingProfile < ApplicationRecord
|
||||
|
||||
has_many :accounting_lines, dependent: :destroy
|
||||
|
||||
# as operator
|
||||
has_many :operated_cart_item_event_reservations, class_name: 'CartItem::EventReservation', dependent: :nullify, inverse_of: :operator_profile
|
||||
has_many :operated_cart_item_machine_reservations, class_name: 'CartItem::MachineReservation', dependent: :nullify,
|
||||
inverse_of: :operator_profile
|
||||
has_many :operated_cart_item_space_reservations, class_name: 'CartItem::SpaceReservation', dependent: :nullify, inverse_of: :operator_profile
|
||||
has_many :operated_cart_item_training_reservations, class_name: 'CartItem::TrainingReservation', dependent: :nullify,
|
||||
inverse_of: :operator_profile
|
||||
has_many :operated_cart_item_coupon, class_name: 'CartItem::Coupon', dependent: :nullify, inverse_of: :operator_profile
|
||||
# as customer
|
||||
has_many :cart_item_event_reservations, class_name: 'CartItem::EventReservation', dependent: :destroy, inverse_of: :customer_profile
|
||||
has_many :cart_item_machine_reservations, class_name: 'CartItem::MachineReservation', dependent: :destroy, inverse_of: :customer_profile
|
||||
has_many :cart_item_space_reservations, class_name: 'CartItem::SpaceReservation', dependent: :destroy, inverse_of: :customer_profile
|
||||
has_many :cart_item_training_reservations, class_name: 'CartItem::TrainingReservation', dependent: :destroy, inverse_of: :customer_profile
|
||||
has_many :cart_item_free_extensions, class_name: 'CartItem::FreeExtension', dependent: :destroy, inverse_of: :customer_profile
|
||||
has_many :cart_item_subscriptions, class_name: 'CartItem::Subscription', dependent: :destroy, inverse_of: :customer_profile
|
||||
has_many :cart_item_prepaid_packs, class_name: 'CartItem::PrepaidPack', dependent: :destroy, inverse_of: :customer_profile
|
||||
has_many :cart_item_coupons, class_name: 'CartItem::Coupon', dependent: :destroy, inverse_of: :customer_profile
|
||||
has_many :cart_item_payment_schedules, class_name: 'CartItem::PaymentSchedule', dependent: :destroy, inverse_of: :customer_profile
|
||||
|
||||
before_validation :set_external_id_nil
|
||||
validates :external_id, uniqueness: true, allow_blank: true
|
||||
validates :address, presence: true, if: -> { Setting.get('address_required') }
|
||||
|
@ -37,6 +37,9 @@ class Machine < ApplicationRecord
|
||||
has_one :advanced_accounting, as: :accountable, dependent: :destroy
|
||||
accepts_nested_attributes_for :advanced_accounting, allow_destroy: true
|
||||
|
||||
has_many :cart_item_machine_reservations, class_name: 'CartItem::MachineReservation', dependent: :destroy, inverse_of: :reservable,
|
||||
foreign_type: 'reservable_type', foreign_key: 'reservable_id'
|
||||
|
||||
belongs_to :category
|
||||
|
||||
after_create :create_statistic_subtype
|
||||
|
@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Order is a model for the user hold information of order
|
||||
# Order is a model used to hold orders data
|
||||
class Order < PaymentDocument
|
||||
belongs_to :statistic_profile
|
||||
belongs_to :operator_profile, class_name: 'InvoicingProfile'
|
||||
|
@ -15,6 +15,12 @@ class Plan < ApplicationRecord
|
||||
has_many :prices, dependent: :destroy
|
||||
has_one :payment_gateway_object, -> { order id: :desc }, inverse_of: :plan, as: :item, dependent: :destroy
|
||||
|
||||
has_many :cart_item_machine_reservations, class_name: 'CartItem::MachineReservation', dependent: :destroy
|
||||
has_many :cart_item_space_reservations, class_name: 'CartItem::SpaceReservation', dependent: :destroy
|
||||
has_many :cart_item_training_reservations, class_name: 'CartItem::TrainingReservation', dependent: :destroy
|
||||
has_many :cart_item_subscriptions, class_name: 'CartItem::Subscription', dependent: :destroy
|
||||
has_many :cart_item_payment_schedules, class_name: 'CartItem::PaymentSchedule', dependent: :destroy
|
||||
|
||||
extend FriendlyId
|
||||
friendly_id :base_name, use: :slugged
|
||||
|
||||
|
@ -8,12 +8,14 @@
|
||||
# The number of hours in a pack is stored in minutes.
|
||||
class PrepaidPack < ApplicationRecord
|
||||
belongs_to :priceable, polymorphic: true
|
||||
belongs_to :machine, foreign_type: 'Machine', foreign_key: 'priceable_id'
|
||||
belongs_to :space, foreign_type: 'Space', foreign_key: 'priceable_id'
|
||||
belongs_to :machine, foreign_type: 'Machine', foreign_key: 'priceable_id', inverse_of: :prepaid_packs
|
||||
belongs_to :space, foreign_type: 'Space', foreign_key: 'priceable_id', inverse_of: :prepaid_packs
|
||||
|
||||
belongs_to :group
|
||||
|
||||
has_many :statistic_profile_prepaid_packs
|
||||
has_many :statistic_profile_prepaid_packs, dependent: :destroy
|
||||
|
||||
has_many :cart_item_prepaid_packs, class_name: 'CartItem::PrepaidPack', dependent: :destroy
|
||||
|
||||
validates :amount, :group_id, :priceable_id, :priceable_type, :minutes, presence: true
|
||||
|
||||
|
@ -59,7 +59,7 @@ class Project < ApplicationRecord
|
||||
scope :published_or_drafts, lambda { |author_profile|
|
||||
where("state = 'published' OR (state = 'draft' AND author_statistic_profile_id = ?)", author_profile)
|
||||
}
|
||||
scope :user_projects, ->(author_profile) { where(author_statistic_profile: author_profile) }
|
||||
scope :user_projects, ->(author_profile) { where(author_statistic_profile_id: author_profile) }
|
||||
scope :collaborations, ->(collaborators_ids) { joins(:project_users).where(project_users: { user_id: collaborators_ids }) }
|
||||
scope :with_machine, ->(machines_ids) { joins(:projects_machines).where(projects_machines: { machine_id: machines_ids }) }
|
||||
scope :with_theme, ->(themes_ids) { joins(:projects_themes).where(projects_themes: { theme_id: themes_ids }) }
|
||||
|
@ -70,7 +70,7 @@ class ShoppingCart
|
||||
payment.post_save(payment_id, payment_type)
|
||||
end
|
||||
|
||||
success = !payment.nil? && objects.map(&:errors).flatten.map(&:empty?).all? && items.map(&:errors).map(&:empty?).all?
|
||||
success = !payment.nil? && objects.map(&:errors).flatten.map(&:empty?).all? && items.map(&:errors).map(&:blank?).all?
|
||||
errors = objects.map(&:errors).flatten.concat(items.map(&:errors))
|
||||
errors.push('Unable to create the PaymentDocument') if payment.nil?
|
||||
{ success: success, payment: payment, errors: errors }
|
||||
|
@ -10,6 +10,8 @@ class Slot < ApplicationRecord
|
||||
has_many :reservations, through: :slots_reservations
|
||||
belongs_to :availability
|
||||
|
||||
has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy
|
||||
|
||||
attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :current_user_slots_reservations_ids
|
||||
|
||||
def full?(reservable = nil)
|
||||
|
@ -5,6 +5,7 @@
|
||||
class SlotsReservation < ApplicationRecord
|
||||
belongs_to :slot
|
||||
belongs_to :reservation
|
||||
has_one :cart_item_reservation_slot, class_name: 'CartItem::ReservationSlot', dependent: :nullify
|
||||
|
||||
after_update :set_ex_start_end_dates_attrs, if: :slot_changed?
|
||||
after_update :notify_member_and_admin_slot_is_modified, if: :slot_changed?
|
||||
@ -13,7 +14,7 @@ class SlotsReservation < ApplicationRecord
|
||||
after_update :update_event_nb_free_places, if: :canceled?
|
||||
|
||||
def set_ex_start_end_dates_attrs
|
||||
update_columns(ex_start_at: previous_slot.start_at, ex_end_at: previous_slot.end_at)
|
||||
update_columns(ex_start_at: previous_slot.start_at, ex_end_at: previous_slot.end_at) # rubocop:disable Rails/SkipsModelValidations
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -31,6 +31,9 @@ class Space < ApplicationRecord
|
||||
has_one :advanced_accounting, as: :accountable, dependent: :destroy
|
||||
accepts_nested_attributes_for :advanced_accounting, allow_destroy: true
|
||||
|
||||
has_many :cart_item_space_reservations, class_name: 'CartItem::SpaceReservation', dependent: :destroy, inverse_of: :reservable,
|
||||
foreign_type: 'reservable_type', foreign_key: 'reservable_id'
|
||||
|
||||
after_create :create_statistic_subtype
|
||||
after_create :create_space_prices
|
||||
after_create :update_gateway_product
|
||||
|
@ -12,6 +12,8 @@ class Subscription < ApplicationRecord
|
||||
has_many :invoice_items, as: :object, dependent: :destroy
|
||||
has_many :offer_days, dependent: :destroy
|
||||
|
||||
has_many :cart_item_free_extensions, class_name: 'CartItem::FreeExtension', dependent: :destroy
|
||||
|
||||
validates :plan_id, presence: true
|
||||
validates_with SubscriptionGroupValidator
|
||||
|
||||
|
@ -32,6 +32,9 @@ class Training < ApplicationRecord
|
||||
has_one :advanced_accounting, as: :accountable, dependent: :destroy
|
||||
accepts_nested_attributes_for :advanced_accounting, allow_destroy: true
|
||||
|
||||
has_many :cart_item_training_reservations, class_name: 'CartItem::TrainingReservation', dependent: :destroy, inverse_of: :reservable,
|
||||
foreign_type: 'reservable_type', foreign_key: 'reservable_id'
|
||||
|
||||
after_create :create_statistic_subtype
|
||||
after_create :create_trainings_pricings
|
||||
after_create :update_gateway_product
|
||||
|
@ -5,10 +5,30 @@ class Cart::AddItemService
|
||||
def call(order, orderable, quantity = 1)
|
||||
return order if quantity.to_i.zero?
|
||||
|
||||
raise Cart::InactiveProductError unless orderable.is_active
|
||||
item = case orderable
|
||||
when Product
|
||||
add_product(order, orderable, quantity)
|
||||
when Slot
|
||||
add_slot(order, orderable)
|
||||
else
|
||||
raise Cart::UnknownItemError
|
||||
end
|
||||
|
||||
order.created_at = Time.current if order.order_items.length.zero?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
item.save
|
||||
Cart::UpdateTotalService.new.call(order)
|
||||
order.save
|
||||
end
|
||||
order.reload
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_product(order, orderable, quantity)
|
||||
raise Cart::InactiveProductError unless orderable.is_active
|
||||
|
||||
item = order.order_items.find_by(orderable: orderable)
|
||||
quantity = orderable.quantity_min > quantity.to_i && item.nil? ? orderable.quantity_min : quantity.to_i
|
||||
|
||||
@ -19,11 +39,14 @@ class Cart::AddItemService
|
||||
end
|
||||
raise Cart::OutStockError if item.quantity > orderable.stock['external']
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
item.save
|
||||
Cart::UpdateTotalService.new.call(order)
|
||||
order.save
|
||||
end
|
||||
order.reload
|
||||
item
|
||||
end
|
||||
|
||||
def add_slot(order, orderable)
|
||||
item = order.order_items.find_by(orderable: orderable)
|
||||
|
||||
item = order.order_items.new(quantity: 1, orderable: orderable, amount: orderable.amount || 0) if item.nil?
|
||||
|
||||
item
|
||||
end
|
||||
end
|
||||
|
@ -11,25 +11,46 @@ class CartService
|
||||
# @see app/frontend/src/javascript/models/payment.ts > interface ShoppingCart
|
||||
##
|
||||
def from_hash(cart_items)
|
||||
cart_items.permit! if cart_items.is_a? ActionController::Parameters
|
||||
|
||||
@customer = customer(cart_items)
|
||||
plan_info = plan(cart_items)
|
||||
|
||||
items = []
|
||||
cart_items[:items].each do |item|
|
||||
if ['subscription', :subscription].include?(item.keys.first)
|
||||
items.push(CartItem::Subscription.new(plan_info[:plan], @customer, item[:subscription][:start_at])) if plan_info[:new_subscription]
|
||||
if ['subscription', :subscription].include?(item.keys.first) && plan_info[:new_subscription]
|
||||
items.push(CartItem::Subscription.new(
|
||||
plan: plan_info[:plan],
|
||||
customer_profile: @customer.invoicing_profile,
|
||||
start_at: item[:subscription][:start_at]
|
||||
))
|
||||
elsif ['reservation', :reservation].include?(item.keys.first)
|
||||
items.push(reservable_from_hash(item[:reservation], plan_info))
|
||||
elsif ['prepaid_pack', :prepaid_pack].include?(item.keys.first)
|
||||
items.push(CartItem::PrepaidPack.new(PrepaidPack.find(item[:prepaid_pack][:id]), @customer))
|
||||
items.push(CartItem::PrepaidPack.new(
|
||||
prepaid_pack: PrepaidPack.find(item[:prepaid_pack][:id]),
|
||||
customer_profile: @customer.invoicing_profile
|
||||
))
|
||||
elsif ['free_extension', :free_extension].include?(item.keys.first)
|
||||
items.push(CartItem::FreeExtension.new(@customer, plan_info[:subscription], item[:free_extension][:end_at]))
|
||||
items.push(CartItem::FreeExtension.new(
|
||||
customer_profile: @customer.invoicing_profile,
|
||||
subscription: plan_info[:subscription],
|
||||
new_expiration_date: item[:free_extension][:end_at]
|
||||
))
|
||||
end
|
||||
end
|
||||
|
||||
coupon = CartItem::Coupon.new(@customer, @operator, cart_items[:coupon_code])
|
||||
coupon = CartItem::Coupon.new(
|
||||
customer_profile: @customer.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
coupon: Coupon.find_by(code: cart_items[:coupon_code])
|
||||
)
|
||||
schedule = CartItem::PaymentSchedule.new(
|
||||
plan_info[:plan], coupon, cart_items[:payment_schedule], @customer, plan_info[:subscription]&.start_at
|
||||
plan: plan_info[:plan],
|
||||
coupon: coupon,
|
||||
requested: cart_items[:payment_schedule],
|
||||
customer_profile: @customer.invoicing_profile,
|
||||
start_at: plan_info[:subscription]&.start_at
|
||||
)
|
||||
|
||||
ShoppingCart.new(
|
||||
@ -47,19 +68,40 @@ class CartService
|
||||
subscription = payment_schedule.payment_schedule_objects.find { |pso| pso.object_type == Subscription.name }&.subscription
|
||||
plan = subscription&.plan
|
||||
|
||||
coupon = CartItem::Coupon.new(@customer, @operator, payment_schedule.coupon&.code)
|
||||
schedule = CartItem::PaymentSchedule.new(plan, coupon, true, @customer, subscription.start_at)
|
||||
coupon = CartItem::Coupon.new(
|
||||
customer_profile: @customer.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
coupon: payment_schedule.coupon
|
||||
)
|
||||
schedule = CartItem::PaymentSchedule.new(
|
||||
plan: plan,
|
||||
coupon: coupon,
|
||||
requested: true,
|
||||
customer_profile: @customer.invoicing_profile,
|
||||
start_at: subscription.start_at
|
||||
)
|
||||
|
||||
items = []
|
||||
payment_schedule.payment_schedule_objects.each do |object|
|
||||
if object.object_type == Subscription.name
|
||||
items.push(CartItem::Subscription.new(object.subscription.plan, @customer, object.subscription.start_at))
|
||||
items.push(CartItem::Subscription.new(
|
||||
plan: object.subscription.plan,
|
||||
customer_profile: @customer.invoicing_profile,
|
||||
start_at: object.subscription.start_at
|
||||
))
|
||||
elsif object.object_type == Reservation.name
|
||||
items.push(reservable_from_payment_schedule_object(object, plan))
|
||||
elsif object.object_type == PrepaidPack.name
|
||||
items.push(CartItem::PrepaidPack.new(object.statistic_profile_prepaid_pack.prepaid_pack_id, @customer))
|
||||
items.push(CartItem::PrepaidPack.new(
|
||||
prepaid_pack_id: object.statistic_profile_prepaid_pack.prepaid_pack_id,
|
||||
customer_profile: @customer.invoicing_profile
|
||||
))
|
||||
elsif object.object_type == OfferDay.name
|
||||
items.push(CartItem::FreeExtension.new(@customer, object.offer_day.subscription, object.offer_day.end_date))
|
||||
items.push(CartItem::FreeExtension.new(
|
||||
customer_profile: @customer.invoicing_profile,
|
||||
subscription: object.offer_day.subscription,
|
||||
new_expiration_date: object.offer_day.end_date
|
||||
))
|
||||
end
|
||||
end
|
||||
|
||||
@ -83,7 +125,11 @@ class CartService
|
||||
if cart_items[:items][index][:subscription][:plan_id]
|
||||
new_plan_being_bought = true
|
||||
plan = Plan.find(cart_items[:items][index][:subscription][:plan_id])
|
||||
subscription = CartItem::Subscription.new(plan, @customer, cart_items[:items][index][:subscription][:start_at]).to_object
|
||||
subscription = CartItem::Subscription.new(
|
||||
plan: plan,
|
||||
customer_profile: @customer.invoicing_profile,
|
||||
start_at: cart_items[:items][index][:subscription][:start_at]
|
||||
).to_object
|
||||
plan
|
||||
end
|
||||
elsif @customer.subscribed_plan
|
||||
@ -107,31 +153,31 @@ class CartService
|
||||
reservable = cart_item[:reservable_type]&.constantize&.find(cart_item[:reservable_id])
|
||||
case reservable
|
||||
when Machine
|
||||
CartItem::MachineReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
cart_item[:slots_reservations_attributes],
|
||||
CartItem::MachineReservation.new(customer_profile: @customer.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
reservable: reservable,
|
||||
cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes],
|
||||
plan: plan_info[:plan],
|
||||
new_subscription: plan_info[:new_subscription])
|
||||
when Training
|
||||
CartItem::TrainingReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
cart_item[:slots_reservations_attributes],
|
||||
CartItem::TrainingReservation.new(customer_profile: @customer.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
reservable: reservable,
|
||||
cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes],
|
||||
plan: plan_info[:plan],
|
||||
new_subscription: plan_info[:new_subscription])
|
||||
when Event
|
||||
CartItem::EventReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
cart_item[:slots_reservations_attributes],
|
||||
CartItem::EventReservation.new(customer_profile: @customer.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
event: reservable,
|
||||
cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes],
|
||||
normal_tickets: cart_item[:nb_reserve_places],
|
||||
other_tickets: cart_item[:tickets_attributes])
|
||||
cart_item_event_reservation_tickets_attributes: cart_item[:tickets_attributes] || {})
|
||||
when Space
|
||||
CartItem::SpaceReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
cart_item[:slots_reservations_attributes],
|
||||
CartItem::SpaceReservation.new(customer_profile: @customer.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
reservable: reservable,
|
||||
cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes],
|
||||
plan: plan_info[:plan],
|
||||
new_subscription: plan_info[:new_subscription])
|
||||
else
|
||||
@ -144,31 +190,31 @@ class CartService
|
||||
reservable = object.reservation.reservable
|
||||
case reservable
|
||||
when Machine
|
||||
CartItem::MachineReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
object.reservation.slots_reservations,
|
||||
CartItem::MachineReservation.new(customer_profile: @customer.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
reservable: reservable,
|
||||
cart_item_reservation_slots_attributes: object.reservation.slots_reservations,
|
||||
plan: plan,
|
||||
new_subscription: true)
|
||||
when Training
|
||||
CartItem::TrainingReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
object.reservation.slots_reservations,
|
||||
CartItem::TrainingReservation.new(customer_profile: @customer.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
reservable: reservable,
|
||||
cart_item_reservation_slots_attributes: object.reservation.slots_reservations,
|
||||
plan: plan,
|
||||
new_subscription: true)
|
||||
when Event
|
||||
CartItem::EventReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
object.reservation.slots_reservations,
|
||||
CartItem::EventReservation.new(customer_profile: @customer.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
event: reservable,
|
||||
cart_item_reservation_slots_attributes: object.reservation.slots_reservation,
|
||||
normal_tickets: object.reservation.nb_reserve_places,
|
||||
other_tickets: object.reservation.tickets)
|
||||
cart_item_event_reservation_tickets_attributes: object.reservation.tickets)
|
||||
when Space
|
||||
CartItem::SpaceReservation.new(@customer,
|
||||
@operator,
|
||||
reservable,
|
||||
object.reservation.slots_reservations,
|
||||
CartItem::SpaceReservation.new(customer_profile: @customer.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
reservable: reservable,
|
||||
cart_item_reservation_slots_attributes: object.reservation.slots_reservations,
|
||||
plan: plan,
|
||||
new_subscription: true)
|
||||
else
|
||||
|
@ -7,9 +7,9 @@ class StripeCardTokenValidator
|
||||
|
||||
res = Stripe::Token.retrieve(options[:token], api_key: Setting.get('stripe_secret_key'))
|
||||
if res[:id] != options[:token]
|
||||
record.errors[:card_token] << "A problem occurred while retrieving the card with the specified token: #{res.id}"
|
||||
record.errors.add(:card_token, "A problem occurred while retrieving the card with the specified token: #{res.id}")
|
||||
end
|
||||
rescue Stripe::InvalidRequestError => e
|
||||
record.errors[:card_token] << e
|
||||
record.errors.add(:card_token, e)
|
||||
end
|
||||
end
|
||||
|
@ -67,6 +67,8 @@ en:
|
||||
length_must_be_slot_multiple: "must be at least %{MIN} minutes after the start date"
|
||||
must_be_associated_with_at_least_1_machine: "must be associated with at least 1 machine"
|
||||
deleted_user: "Deleted user"
|
||||
coupon:
|
||||
code_format_error: "only caps letters, numbers, and dashes are allowed"
|
||||
#members management
|
||||
members:
|
||||
unable_to_change_the_group_while_a_subscription_is_running: "Unable to change the group while a subscription is running"
|
||||
|
@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# From this migration, we save the pending event reservations in database, instead of just creating them on the fly
|
||||
class CreateCartItemEventReservation < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :cart_item_event_reservations do |t|
|
||||
t.integer :normal_tickets
|
||||
t.references :event, foreign_key: true
|
||||
t.references :operator_profile, foreign_key: { to_table: 'invoicing_profiles' }
|
||||
t.references :customer_profile, foreign_key: { to_table: 'invoicing_profiles' }
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# A relation table between a pending event reservation and a special price for this event
|
||||
class CreateCartItemEventReservationTicket < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :cart_item_event_reservation_tickets do |t|
|
||||
t.integer :booked
|
||||
t.references :event_price_category, foreign_key: true, index: { name: 'index_cart_item_tickets_on_event_price_category' }
|
||||
t.references :cart_item_event_reservation, foreign_key: true, index: { name: 'index_cart_item_tickets_on_cart_item_event_reservation' }
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# A relation table between a pending reservation and a slot
|
||||
class CreateCartItemReservationSlot < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :cart_item_reservation_slots do |t|
|
||||
t.references :cart_item, polymorphic: true, index: { name: 'index_cart_item_slots_on_cart_item' }
|
||||
t.references :slot, foreign_key: true
|
||||
t.references :slots_reservation, foreign_key: true
|
||||
t.boolean :offered, default: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
17
db/migrate/20221229085430_create_cart_item_reservation.rb
Normal file
17
db/migrate/20221229085430_create_cart_item_reservation.rb
Normal file
@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# From this migration, we save the pending machine/space/training reservations in database, instead of just creating them on the fly
|
||||
class CreateCartItemReservation < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :cart_item_reservations do |t|
|
||||
t.references :reservable, polymorphic: true, index: { name: 'index_cart_item_reservations_on_reservable' }
|
||||
t.references :plan, foreign_key: true
|
||||
t.boolean :new_subscription
|
||||
t.references :customer_profile, foreign_key: { to_table: 'invoicing_profiles' }
|
||||
t.references :operator_profile, foreign_key: { to_table: 'invoicing_profiles' }
|
||||
t.string :type
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
14
db/migrate/20221229094334_create_cart_item_free_extension.rb
Normal file
14
db/migrate/20221229094334_create_cart_item_free_extension.rb
Normal file
@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# From this migration, we save the pending free-extensions of subscriptions in database, instead of just creating them on the fly
|
||||
class CreateCartItemFreeExtension < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :cart_item_free_extensions do |t|
|
||||
t.references :subscription, foreign_key: true
|
||||
t.datetime :new_expiration_date
|
||||
t.references :customer_profile, foreign_key: { to_table: 'invoicing_profiles' }
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
14
db/migrate/20221229100157_create_cart_item_subscription.rb
Normal file
14
db/migrate/20221229100157_create_cart_item_subscription.rb
Normal file
@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# From this migration, we save the pending subscriptions in database, instead of just creating them on the fly
|
||||
class CreateCartItemSubscription < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :cart_item_subscriptions do |t|
|
||||
t.references :plan, foreign_key: true
|
||||
t.datetime :start_at
|
||||
t.references :customer_profile, foreign_key: { to_table: 'invoicing_profiles' }
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
13
db/migrate/20221229103407_create_cart_item_prepaid_pack.rb
Normal file
13
db/migrate/20221229103407_create_cart_item_prepaid_pack.rb
Normal file
@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# From this migration, we save the pending prepaid-packs in database, instead of just creating them on the fly
|
||||
class CreateCartItemPrepaidPack < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :cart_item_prepaid_packs do |t|
|
||||
t.references :prepaid_pack, foreign_key: true
|
||||
t.references :customer_profile, foreign_key: { to_table: 'invoicing_profiles' }
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Coupon's codes should validate uniqness in database
|
||||
class AddUniqueIndexOnCouponCode < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_index :coupons, :code, unique: true
|
||||
end
|
||||
end
|
14
db/migrate/20221229115757_create_cart_item_coupon.rb
Normal file
14
db/migrate/20221229115757_create_cart_item_coupon.rb
Normal file
@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# From this migration, we save the pending coupons in database, instead of just creating them on the fly
|
||||
class CreateCartItemCoupon < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :cart_item_coupons do |t|
|
||||
t.references :coupon, foreign_key: true
|
||||
t.references :customer_profile, foreign_key: { to_table: 'invoicing_profiles' }
|
||||
t.references :operator_profile, foreign_key: { to_table: 'invoicing_profiles' }
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# From this migration, we save the pending payment schedules in database, instead of just creating them on the fly
|
||||
class CreateCartItemPaymentSchedule < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :cart_item_payment_schedules do |t|
|
||||
t.references :plan, foreign_key: true
|
||||
t.references :coupon, foreign_key: true
|
||||
t.boolean :requested
|
||||
t.datetime :start_at
|
||||
t.references :customer_profile, foreign_key: { to_table: 'invoicing_profiles' }
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
18
db/schema.rb
18
db/schema.rb
@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
||||
enable_extension "unaccent"
|
||||
|
||||
create_table "abuses", id: :serial, force: :cascade do |t|
|
||||
t.integer "signaled_id"
|
||||
t.string "signaled_type"
|
||||
t.integer "signaled_id"
|
||||
t.string "first_name"
|
||||
t.string "last_name"
|
||||
t.string "email"
|
||||
@ -68,8 +68,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
||||
t.string "locality"
|
||||
t.string "country"
|
||||
t.string "postal_code"
|
||||
t.integer "placeable_id"
|
||||
t.string "placeable_type"
|
||||
t.integer "placeable_id"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
@ -93,8 +93,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
||||
end
|
||||
|
||||
create_table "assets", id: :serial, force: :cascade do |t|
|
||||
t.integer "viewable_id"
|
||||
t.string "viewable_type"
|
||||
t.integer "viewable_id"
|
||||
t.string "attachment"
|
||||
t.string "type"
|
||||
t.datetime "created_at"
|
||||
@ -281,8 +281,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
||||
end
|
||||
|
||||
create_table "credits", id: :serial, force: :cascade do |t|
|
||||
t.integer "creditable_id"
|
||||
t.string "creditable_type"
|
||||
t.integer "creditable_id"
|
||||
t.integer "plan_id"
|
||||
t.integer "hours"
|
||||
t.datetime "created_at"
|
||||
@ -524,15 +524,15 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
||||
|
||||
create_table "notifications", id: :serial, force: :cascade do |t|
|
||||
t.integer "receiver_id"
|
||||
t.integer "attached_object_id"
|
||||
t.string "attached_object_type"
|
||||
t.integer "attached_object_id"
|
||||
t.integer "notification_type_id"
|
||||
t.boolean "is_read", default: false
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.string "receiver_type"
|
||||
t.boolean "is_send", default: false
|
||||
t.jsonb "meta_data", default: {}
|
||||
t.jsonb "meta_data", default: "{}"
|
||||
t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id"
|
||||
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
|
||||
end
|
||||
@ -772,8 +772,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
||||
create_table "prices", id: :serial, force: :cascade do |t|
|
||||
t.integer "group_id"
|
||||
t.integer "plan_id"
|
||||
t.integer "priceable_id"
|
||||
t.string "priceable_type"
|
||||
t.integer "priceable_id"
|
||||
t.integer "amount"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
@ -976,8 +976,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
||||
t.text "message"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.integer "reservable_id"
|
||||
t.string "reservable_type"
|
||||
t.integer "reservable_id"
|
||||
t.integer "nb_reserve_places"
|
||||
t.integer "statistic_profile_id"
|
||||
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id"
|
||||
@ -986,8 +986,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
||||
|
||||
create_table "roles", id: :serial, force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.integer "resource_id"
|
||||
t.string "resource_type"
|
||||
t.integer "resource_id"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
module Subscriptions; end
|
||||
|
||||
class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@user = User.find_by(username: 'jdupond')
|
||||
@ -57,7 +59,7 @@ class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest
|
||||
|
||||
# Check notifications were sent for every admins
|
||||
notifications = Notification.where(
|
||||
notification_type_id: NotificationType.find_by_name('notify_admin_subscribed_plan'),
|
||||
notification_type_id: NotificationType.find_by_name('notify_admin_subscribed_plan'), # rubocop:disable Rails/DynamicFindBy
|
||||
attached_object_type: 'Subscription',
|
||||
attached_object_id: subscription[:id]
|
||||
)
|
||||
@ -100,7 +102,7 @@ class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest
|
||||
assert_equal Mime[:json], response.content_type
|
||||
|
||||
# Check the error was handled
|
||||
assert_match(/plan is not compatible/, response.body)
|
||||
assert_match(/plan is reserved for members of group/, response.body)
|
||||
|
||||
# Check that the user has no subscription
|
||||
assert_nil @user.subscription, "user's subscription was found"
|
||||
@ -162,7 +164,7 @@ class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest
|
||||
|
||||
# Check notifications were sent for every admins
|
||||
notifications = Notification.where(
|
||||
notification_type_id: NotificationType.find_by_name('notify_admin_subscribed_plan'),
|
||||
notification_type_id: NotificationType.find_by_name('notify_admin_subscribed_plan'), # rubocop:disable Rails/DynamicFindBy
|
||||
attached_object_type: 'Subscription',
|
||||
attached_object_id: subscription[:id]
|
||||
)
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
module Subscriptions; end
|
||||
|
||||
class Subscriptions::RenewAsUserTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@user = User.find_by(username: 'atiermoulin')
|
||||
@ -60,7 +62,7 @@ class Subscriptions::RenewAsUserTest < ActionDispatch::IntegrationTest
|
||||
|
||||
# Check notifications were sent for every admins
|
||||
notifications = Notification.where(
|
||||
notification_type_id: NotificationType.find_by_name('notify_admin_subscribed_plan'),
|
||||
notification_type_id: NotificationType.find_by_name('notify_admin_subscribed_plan'), # rubocop:disable Rails/DynamicFindBy
|
||||
attached_object_type: 'Subscription',
|
||||
attached_object_id: subscription[:id]
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user