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
|
private
|
||||||
|
|
||||||
def orderable
|
def orderable
|
||||||
Product.find(cart_params[:orderable_id])
|
params[:orderable_type].classify.constantize.find(cart_params[:orderable_id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -41,6 +41,7 @@ class API::PaymentsController < API::ApiController
|
|||||||
{ json: res[:errors].drop_while(&:empty?), status: :unprocessable_entity }
|
{ json: res[:errors].drop_while(&:empty?), status: :unprocessable_entity }
|
||||||
end
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
|
Rails.logger.debug e.backtrace
|
||||||
{ json: e, status: :unprocessable_entity }
|
{ json: e, status: :unprocessable_entity }
|
||||||
end
|
end
|
||||||
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 apiClient from './clients/api-client';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { Order, OrderErrors } from '../models/order';
|
import { Order, OrderableType, OrderErrors } from '../models/order';
|
||||||
|
|
||||||
export default class CartAPI {
|
export default class CartAPI {
|
||||||
static async create (token?: string): Promise<Order> {
|
static async create (token?: string): Promise<Order> {
|
||||||
@ -8,28 +8,28 @@ export default class CartAPI {
|
|||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async addItem (order: Order, orderableId: number, quantity: number): Promise<Order> {
|
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, quantity });
|
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;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async removeItem (order: Order, orderableId: number): Promise<Order> {
|
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 });
|
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/remove_item', { order_token: order.token, orderable_id: orderableId, orderable_type: orderableType });
|
||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async setQuantity (order: Order, orderableId: number, quantity: number): Promise<Order> {
|
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, quantity });
|
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;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async setOffer (order: Order, orderableId: number, isOffered: boolean): Promise<Order> {
|
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, is_offered: isOffered, customer_id: order.user?.id });
|
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;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async refreshItem (order: Order, orderableId: number): Promise<Order> {
|
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 });
|
const res: AxiosResponse<Order> = await apiClient.put('/api/cart/refresh_item', { order_token: order.token, orderable_id: orderableId, orderable_type: orderableType });
|
||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
if (errors.length === 1 && errors[0].error === 'not_found') {
|
if (errors.length === 1 && errors[0].error === 'not_found') {
|
||||||
reloadCart().catch(onError);
|
reloadCart().catch(onError);
|
||||||
} else {
|
} else {
|
||||||
CartAPI.removeItem(cart, item.orderable_id).then(data => {
|
CartAPI.removeItem(cart, item.orderable_id, item.orderable_type).then(data => {
|
||||||
setCart(data);
|
setCart(data);
|
||||||
}).catch(onError);
|
}).catch(onError);
|
||||||
}
|
}
|
||||||
@ -76,7 +76,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
* Change product quantity
|
* Change product quantity
|
||||||
*/
|
*/
|
||||||
const changeProductQuantity = (e: React.BaseSyntheticEvent, item) => {
|
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 => {
|
.then(data => {
|
||||||
setCart(data);
|
setCart(data);
|
||||||
})
|
})
|
||||||
@ -87,7 +87,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
* Increment/decrement product quantity
|
* Increment/decrement product quantity
|
||||||
*/
|
*/
|
||||||
const increaseOrDecreaseProductQuantity = (item, direction: 'up' | 'down') => {
|
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 => {
|
.then(data => {
|
||||||
setCart(data);
|
setCart(data);
|
||||||
})
|
})
|
||||||
@ -101,7 +101,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
return (e: React.BaseSyntheticEvent) => {
|
return (e: React.BaseSyntheticEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
CartAPI.refreshItem(cart, item.orderable_id).then(data => {
|
CartAPI.refreshItem(cart, item.orderable_id, item.orderable_type).then(data => {
|
||||||
setCart(data);
|
setCart(data);
|
||||||
}).catch(onError);
|
}).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 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) {
|
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 => {
|
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 } }));
|
})).then((data) => setCart({ ...data[data.length - 1], user: { id: user.id, role: user.role } }));
|
||||||
} else {
|
} else {
|
||||||
setCart({ ...cart, user: { id: user.id, role: user.role } });
|
setCart({ ...cart, user: { id: user.id, role: user.role } });
|
||||||
@ -211,7 +211,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
|
|||||||
*/
|
*/
|
||||||
const toggleProductOffer = (item) => {
|
const toggleProductOffer = (item) => {
|
||||||
return (checked: boolean) => {
|
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);
|
setCart(data);
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
if (e.match(/code 403/)) {
|
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) => {
|
const addProductToCart = (e: React.BaseSyntheticEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
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'));
|
onError(t('app.public.store_product_item.stock_limit'));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -108,7 +108,7 @@ export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, current
|
|||||||
*/
|
*/
|
||||||
const addToCart = () => {
|
const addToCart = () => {
|
||||||
if (toCartCount <= product.stock.external) {
|
if (toCartCount <= product.stock.external) {
|
||||||
CartAPI.addItem(cart, product.id, toCartCount).then(data => {
|
CartAPI.addItem(cart, product.id, 'Product', toCartCount).then(data => {
|
||||||
setCart(data);
|
setCart(data);
|
||||||
onSuccess(t('app.public.store_product.add_to_cart_success'));
|
onSuccess(t('app.public.store_product.add_to_cart_success'));
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
@ -393,12 +393,6 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
|||||||
// Slot free to be booked
|
// Slot free to be booked
|
||||||
const FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_COLOR %>';
|
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 */
|
/* PUBLIC SCOPE */
|
||||||
|
|
||||||
// bind the machine availabilities with full-Calendar events
|
// 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
|
* Callback triggered after a successful prepaid-pack purchase
|
||||||
*/
|
*/
|
||||||
$scope.onSuccess = function (message) {
|
$scope.onSuccess = function (message) {
|
||||||
|
|
||||||
growl.success(message);
|
growl.success(message);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ import { UserRole } from './user';
|
|||||||
import { Coupon } from './coupon';
|
import { Coupon } from './coupon';
|
||||||
import { ApiFilter, PaginatedIndex } from './api';
|
import { ApiFilter, PaginatedIndex } from './api';
|
||||||
|
|
||||||
|
export type OrderableType = 'Product' | 'Slot';
|
||||||
|
|
||||||
export interface Order {
|
export interface Order {
|
||||||
id: number,
|
id: number,
|
||||||
token: string,
|
token: string,
|
||||||
@ -28,7 +30,7 @@ export interface Order {
|
|||||||
paid_total?: number,
|
paid_total?: number,
|
||||||
order_items_attributes: Array<{
|
order_items_attributes: Array<{
|
||||||
id: number,
|
id: number,
|
||||||
orderable_type: string,
|
orderable_type: OrderableType,
|
||||||
orderable_id: number,
|
orderable_id: number,
|
||||||
orderable_name: string,
|
orderable_name: string,
|
||||||
orderable_slug: 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">
|
<section class="heading b-b">
|
||||||
<div class="row no-gutter">
|
<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">
|
<section class="heading-btn">
|
||||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</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">
|
<section class="heading-title">
|
||||||
<h1>{{ 'app.logged.machines_reserve.machine_planning' | translate }} : {{machine.name}}</h1>
|
<h1>{{ 'app.logged.machines_reserve.machine_planning' | translate }} : {{machine.name}}</h1>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -39,6 +45,8 @@
|
|||||||
refresh="afterPaymentPromise">
|
refresh="afterPaymentPromise">
|
||||||
</packs-summary>
|
</packs-summary>
|
||||||
|
|
||||||
|
<reservations-summary slot="selectedEvent" customer="ctrl.member" on-error="onError"></reservations-summary>
|
||||||
|
|
||||||
<cart slot="selectedEvent"
|
<cart slot="selectedEvent"
|
||||||
slot-selection-time="selectionTime"
|
slot-selection-time="selectionTime"
|
||||||
events="events"
|
events="events"
|
||||||
|
@ -7,7 +7,6 @@ module AvailabilityHelper
|
|||||||
SPACE_COLOR = '#3fc7ff'
|
SPACE_COLOR = '#3fc7ff'
|
||||||
EVENT_COLOR = '#dd7e6b'
|
EVENT_COLOR = '#dd7e6b'
|
||||||
IS_RESERVED_BY_CURRENT_USER = '#b2e774'
|
IS_RESERVED_BY_CURRENT_USER = '#b2e774'
|
||||||
MACHINE_IS_RESERVED_BY_USER = '#1d98ec'
|
|
||||||
IS_FULL = '#eeeeee'
|
IS_FULL = '#eeeeee'
|
||||||
|
|
||||||
def availability_border_color(availability)
|
def availability_border_color(availability)
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Items that can be added to the shopping cart
|
# 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
|
# This is an abstract class implemented by classes that can be added to the shopping cart
|
||||||
class CartItem::BaseItem
|
class CartItem::BaseItem < ApplicationRecord
|
||||||
attr_reader :errors
|
self.abstract_class = true
|
||||||
|
|
||||||
def initialize(*)
|
|
||||||
@errors = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
def price
|
def price
|
||||||
{ elements: {}, amount: 0 }
|
{ elements: {}, amount: 0 }
|
||||||
|
@ -1,20 +1,17 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# A discount coupon applied to the whole shopping cart
|
# A discount coupon applied to the whole shopping cart
|
||||||
class CartItem::Coupon
|
class CartItem::Coupon < ApplicationRecord
|
||||||
attr_reader :errors
|
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 operator
|
||||||
def initialize(customer, operator, coupon)
|
operator_profile.user
|
||||||
@customer = customer
|
|
||||||
@operator = operator
|
|
||||||
@coupon = coupon
|
|
||||||
@errors = {}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def coupon
|
def customer
|
||||||
cs = CouponService.new
|
customer_profile.user
|
||||||
cs.validate(@coupon, @customer.id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def price(cart_total = 0)
|
def price(cart_total = 0)
|
||||||
@ -31,11 +28,10 @@ class CartItem::Coupon
|
|||||||
end
|
end
|
||||||
|
|
||||||
def valid?(_all_items)
|
def valid?(_all_items)
|
||||||
return true if @coupon.nil?
|
return true if coupon.nil?
|
||||||
|
|
||||||
c = ::Coupon.find_by(code: @coupon)
|
if coupon.status(customer.id) != 'active'
|
||||||
if c.nil? || c.status(@customer.id) != 'active'
|
errors.add(:coupon, 'invalid coupon')
|
||||||
@errors[:item] = 'coupon is invalid'
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
true
|
true
|
||||||
|
@ -2,30 +2,38 @@
|
|||||||
|
|
||||||
# An event reservation added to the shopping cart
|
# An event reservation added to the shopping cart
|
||||||
class CartItem::EventReservation < CartItem::Reservation
|
class CartItem::EventReservation < CartItem::Reservation
|
||||||
# @param normal_tickets {Number} number of tickets at the normal price
|
has_many :cart_item_event_reservation_tickets, class_name: 'CartItem::EventReservationTicket', dependent: :destroy,
|
||||||
# @param other_tickets {Array<{booked: Number, event_price_category_id: Number}>}
|
inverse_of: :cart_item_event_reservation,
|
||||||
def initialize(customer, operator, event, slots, normal_tickets: 0, other_tickets: [])
|
foreign_key: 'cart_item_event_reservation_id'
|
||||||
raise TypeError unless event.is_a? Event
|
accepts_nested_attributes_for :cart_item_event_reservation_tickets
|
||||||
|
|
||||||
super(customer, operator, event, slots)
|
has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy, inverse_of: :cart_item,
|
||||||
@normal_tickets = normal_tickets || 0
|
foreign_key: 'cart_item_id', foreign_type: 'cart_item_type'
|
||||||
@other_tickets = other_tickets || []
|
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
|
end
|
||||||
|
|
||||||
def price
|
def price
|
||||||
amount = @reservable.amount * @normal_tickets
|
amount = reservable.amount * normal_tickets
|
||||||
is_privileged = @operator.privileged? && @operator.id != @customer.id
|
is_privileged = operator.privileged? && operator.id != customer.id
|
||||||
|
|
||||||
@other_tickets.each do |ticket|
|
cart_item_event_reservation_tickets.each do |ticket|
|
||||||
amount += ticket[:booked] * EventPriceCategory.find(ticket[:event_price_category_id]).amount
|
amount += ticket.booked * ticket.event_price_category.amount
|
||||||
end
|
end
|
||||||
|
|
||||||
elements = { slots: [] }
|
elements = { slots: [] }
|
||||||
total = 0
|
total = 0
|
||||||
|
|
||||||
@slots.each do |slot|
|
cart_item_reservation_slots.each do |sr|
|
||||||
total += get_slot_price(amount,
|
total += get_slot_price(amount,
|
||||||
slot,
|
sr,
|
||||||
is_privileged,
|
is_privileged,
|
||||||
elements: elements,
|
elements: elements,
|
||||||
is_division: false)
|
is_division: false)
|
||||||
@ -36,26 +44,25 @@ class CartItem::EventReservation < CartItem::Reservation
|
|||||||
|
|
||||||
def to_object
|
def to_object
|
||||||
::Reservation.new(
|
::Reservation.new(
|
||||||
reservable_id: @reservable.id,
|
reservable_id: reservable.id,
|
||||||
reservable_type: Event.name,
|
reservable_type: Event.name,
|
||||||
slots_reservations_attributes: slots_params,
|
slots_reservations_attributes: slots_params,
|
||||||
tickets_attributes: tickets_params,
|
tickets_attributes: cart_item_event_reservation_tickets.map do |t|
|
||||||
nb_reserve_places: @normal_tickets,
|
{
|
||||||
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
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
|
end
|
||||||
|
|
||||||
def name
|
def name
|
||||||
@reservable.title
|
reservable.title
|
||||||
end
|
end
|
||||||
|
|
||||||
def type
|
def type
|
||||||
'event'
|
'event'
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
def tickets_params
|
|
||||||
@other_tickets.map { |ticket| ticket.permit(:event_price_category_id, :booked) }
|
|
||||||
end
|
|
||||||
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
|
# A subscription extended for free, added to the shopping cart
|
||||||
class CartItem::FreeExtension < CartItem::BaseItem
|
class CartItem::FreeExtension < CartItem::BaseItem
|
||||||
def initialize(customer, subscription, new_expiration_date)
|
belongs_to :customer_profile, class_name: 'InvoicingProfile'
|
||||||
raise TypeError unless subscription.is_a? Subscription
|
belongs_to :subscription
|
||||||
|
|
||||||
@customer = customer
|
def customer
|
||||||
@new_expiration_date = new_expiration_date
|
statistic_profile.user
|
||||||
@subscription = subscription
|
|
||||||
super
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def start_at
|
def start_at
|
||||||
raise InvalidSubscriptionError if @subscription.nil?
|
raise InvalidSubscriptionError if subscription.nil?
|
||||||
if @new_expiration_date.nil? || @new_expiration_date <= @subscription.expired_at
|
if new_expiration_date.nil? || new_expiration_date <= subscription.expired_at
|
||||||
raise InvalidSubscriptionError, I18n.t('cart_items.must_be_after_expiration')
|
raise InvalidSubscriptionError, I18n.t('cart_items.must_be_after_expiration')
|
||||||
end
|
end
|
||||||
|
|
||||||
@subscription.expired_at
|
subscription.expired_at
|
||||||
end
|
end
|
||||||
|
|
||||||
def price
|
def price
|
||||||
@ -27,14 +25,14 @@ class CartItem::FreeExtension < CartItem::BaseItem
|
|||||||
end
|
end
|
||||||
|
|
||||||
def name
|
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
|
end
|
||||||
|
|
||||||
def to_object
|
def to_object
|
||||||
::OfferDay.new(
|
::OfferDay.new(
|
||||||
subscription_id: @subscription.id,
|
subscription_id: subscription.id,
|
||||||
start_at: start_at,
|
start_at: start_at,
|
||||||
end_at: @new_expiration_date
|
end_at: new_expiration_date
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2,46 +2,40 @@
|
|||||||
|
|
||||||
# A machine reservation added to the shopping cart
|
# A machine reservation added to the shopping cart
|
||||||
class CartItem::MachineReservation < CartItem::Reservation
|
class CartItem::MachineReservation < CartItem::Reservation
|
||||||
# @param plan {Plan} a subscription bought at the same time of the reservation OR an already running subscription
|
self.table_name = 'cart_item_reservations'
|
||||||
# @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
|
|
||||||
|
|
||||||
super(customer, operator, machine, slots)
|
has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy, inverse_of: :cart_item,
|
||||||
@plan = plan
|
foreign_key: 'cart_item_id', foreign_type: 'cart_item_type'
|
||||||
@new_subscription = new_subscription
|
accepts_nested_attributes_for :cart_item_reservation_slots
|
||||||
end
|
|
||||||
|
|
||||||
def to_object
|
belongs_to :operator_profile, class_name: 'InvoicingProfile'
|
||||||
::Reservation.new(
|
belongs_to :customer_profile, class_name: 'InvoicingProfile'
|
||||||
reservable_id: @reservable.id,
|
|
||||||
reservable_type: Machine.name,
|
belongs_to :reservable, polymorphic: true
|
||||||
slots_reservations_attributes: slots_params,
|
|
||||||
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
belongs_to :plan
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def type
|
def type
|
||||||
'machine'
|
'machine'
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid?(all_items)
|
def valid?(all_items)
|
||||||
@slots.each do |slot|
|
cart_item_reservation_slots.each do |slot|
|
||||||
same_hour_slots = SlotsReservation.joins(:reservation).where(
|
same_hour_slots = SlotsReservation.joins(:reservation).where(
|
||||||
reservations: { reservable: @reservable },
|
reservations: { reservable: reservable },
|
||||||
slot_id: slot[:slot_id],
|
slot_id: slot[:slot_id],
|
||||||
canceled_at: nil
|
canceled_at: nil
|
||||||
).count
|
).count
|
||||||
if same_hour_slots.positive?
|
if same_hour_slots.positive?
|
||||||
@errors[:slot] = I18n.t('cart_item_validation.reserved')
|
errors.add(:slot, I18n.t('cart_item_validation.reserved'))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
if @reservable.disabled
|
if reservable.disabled
|
||||||
@errors[:reservable] = I18n.t('cart_item_validation.machine')
|
errors.add(:reservable, I18n.t('cart_item_validation.machine'))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
unless @reservable.reservable
|
unless reservable.reservable
|
||||||
@errors[:reservable] = I18n.t('cart_item_validation.reservable')
|
errors.add(:reservable, I18n.t('cart_item_validation.reservable'))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -52,9 +46,9 @@ class CartItem::MachineReservation < CartItem::Reservation
|
|||||||
protected
|
protected
|
||||||
|
|
||||||
def credits
|
def credits
|
||||||
return 0 if @plan.nil?
|
return 0 if plan.nil?
|
||||||
|
|
||||||
machine_credit = @plan.machine_credits.find { |credit| credit.creditable_id == @reservable.id }
|
machine_credit = plan.machine_credits.find { |credit| credit.creditable_id == reservable.id }
|
||||||
credits_hours(machine_credit, new_plan_being_bought: @new_subscription)
|
credits_hours(machine_credit, new_plan_being_bought: new_subscription)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,23 +1,18 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# A payment schedule applied to plan in the shopping cart
|
# A payment schedule applied to plan in the shopping cart
|
||||||
class CartItem::PaymentSchedule
|
class CartItem::PaymentSchedule < ApplicationRecord
|
||||||
attr_reader :requested, :errors
|
belongs_to :customer_profile, class_name: 'InvoicingProfile'
|
||||||
|
belongs_to :coupon
|
||||||
|
belongs_to :plan
|
||||||
|
|
||||||
def initialize(plan, coupon, requested, customer, start_at = nil)
|
def customer
|
||||||
raise TypeError unless coupon.is_a? CartItem::Coupon
|
customer_profile.user
|
||||||
|
|
||||||
@plan = plan
|
|
||||||
@coupon = coupon
|
|
||||||
@requested = requested
|
|
||||||
@customer = customer
|
|
||||||
@start_at = start_at
|
|
||||||
@errors = {}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def schedule(total, total_without_coupon)
|
def schedule(total, total_without_coupon)
|
||||||
schedule = if @requested && @plan&.monthly_payment
|
schedule = if requested && plan&.monthly_payment
|
||||||
PaymentScheduleService.new.compute(@plan, total_without_coupon, @customer, coupon: @coupon.coupon, start_at: @start_at)
|
PaymentScheduleService.new.compute(plan, total_without_coupon, customer, coupon: coupon.coupon, start_at: start_at)
|
||||||
else
|
else
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
@ -36,10 +31,10 @@ class CartItem::PaymentSchedule
|
|||||||
end
|
end
|
||||||
|
|
||||||
def valid?(_all_items)
|
def valid?(_all_items)
|
||||||
return true unless @requested && @plan&.monthly_payment
|
return true unless requested && plan&.monthly_payment
|
||||||
|
|
||||||
if @plan&.disabled
|
if plan&.disabled
|
||||||
@errors[:item] = I18n.t('cart_item_validation.plan')
|
errors.add(:plan, I18n.t('cart_item_validation.plan'))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
true
|
true
|
||||||
|
@ -2,18 +2,14 @@
|
|||||||
|
|
||||||
# A prepaid-pack added to the shopping cart
|
# A prepaid-pack added to the shopping cart
|
||||||
class CartItem::PrepaidPack < CartItem::BaseItem
|
class CartItem::PrepaidPack < CartItem::BaseItem
|
||||||
def initialize(pack, customer)
|
belongs_to :prepaid_pack
|
||||||
raise TypeError unless pack.is_a? PrepaidPack
|
|
||||||
|
|
||||||
@pack = pack
|
def customer
|
||||||
@customer = customer
|
customer_profile.user
|
||||||
super
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def pack
|
def pack
|
||||||
raise InvalidGroupError if @pack.group_id != @customer.group_id
|
prepaid_pack
|
||||||
|
|
||||||
@pack
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def price
|
def price
|
||||||
@ -24,13 +20,13 @@ class CartItem::PrepaidPack < CartItem::BaseItem
|
|||||||
end
|
end
|
||||||
|
|
||||||
def name
|
def name
|
||||||
"#{@pack.minutes / 60} h"
|
"#{pack.minutes / 60} h"
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_object
|
def to_object
|
||||||
::StatisticProfilePrepaidPack.new(
|
::StatisticProfilePrepaidPack.new(
|
||||||
prepaid_pack_id: @pack.id,
|
prepaid_pack_id: pack.id,
|
||||||
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
statistic_profile_id: StatisticProfile.find_by(user: customer).id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -39,10 +35,14 @@ class CartItem::PrepaidPack < CartItem::BaseItem
|
|||||||
end
|
end
|
||||||
|
|
||||||
def valid?(_all_items)
|
def valid?(_all_items)
|
||||||
if @pack.disabled
|
if pack.disabled
|
||||||
@errors[:item] = I18n.t('cart_item_validation.pack')
|
@errors[:item] = I18n.t('cart_item_validation.pack')
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
if pack.group_id != customer.group_id
|
||||||
|
@errors[:group] = "pack is reserved for members of group #{pack.group.name}"
|
||||||
|
return false
|
||||||
|
end
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
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
|
# A generic reservation added to the shopping cart
|
||||||
class CartItem::Reservation < CartItem::BaseItem
|
class CartItem::Reservation < CartItem::BaseItem
|
||||||
def initialize(customer, operator, reservable, slots)
|
self.abstract_class = true
|
||||||
@customer = customer
|
|
||||||
@operator = operator
|
def reservable
|
||||||
@reservable = reservable
|
nil
|
||||||
@slots = slots.map { |s| expand_slot(s) }
|
end
|
||||||
super
|
|
||||||
|
def plan
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def operator
|
||||||
|
operator_profile.user
|
||||||
|
end
|
||||||
|
|
||||||
|
def customer
|
||||||
|
customer_profile.user
|
||||||
end
|
end
|
||||||
|
|
||||||
def price
|
def price
|
||||||
is_privileged = @operator.privileged? && @operator.id != @customer.id
|
is_privileged = operator.privileged? && operator.id != customer.id
|
||||||
prepaid = { minutes: PrepaidPackService.minutes_available(@customer, @reservable) }
|
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
|
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
|
end
|
||||||
|
|
||||||
def name
|
def name
|
||||||
@reservable.name
|
reservable&.name
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid?(all_items)
|
def valid?(all_items)
|
||||||
@ -48,39 +58,48 @@ class CartItem::Reservation < CartItem::BaseItem
|
|||||||
reservation_deadline_minutes = Setting.get('reservation_deadline').to_i
|
reservation_deadline_minutes = Setting.get('reservation_deadline').to_i
|
||||||
reservation_deadline = reservation_deadline_minutes.minutes.since
|
reservation_deadline = reservation_deadline_minutes.minutes.since
|
||||||
|
|
||||||
@slots.each do |slot|
|
cart_item_reservation_slots.each do |sr|
|
||||||
slot_db = Slot.find(slot[:slot_id])
|
slot = sr.slot
|
||||||
if slot_db.nil?
|
if slot.nil?
|
||||||
@errors[:slot] = I18n.t('cart_item_validation.slot')
|
errors.add(:slot, I18n.t('cart_item_validation.slot'))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
availability = Availability.find_by(id: slot[:slot_attributes][:availability_id])
|
availability = slot.availability
|
||||||
if availability.nil?
|
if availability.nil?
|
||||||
@errors[:availability] = I18n.t('cart_item_validation.availability')
|
errors.add(:availability, I18n.t('cart_item_validation.availability'))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
if slot_db.full?
|
if slot.full?
|
||||||
@errors[:slot] = I18n.t('cart_item_validation.full')
|
errors.add(:slot, I18n.t('cart_item_validation.full')
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
if slot_db.start_at < reservation_deadline && !@operator.privileged?
|
if slot.start_at < reservation_deadline && !operator.privileged?
|
||||||
@errors[:slot] = I18n.t('cart_item_validation.deadline', { MINUTES: reservation_deadline_minutes })
|
errors.add(:slot, I18n.t('cart_item_validation.deadline', { MINUTES: reservation_deadline_minutes }))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
next if availability.plan_ids.empty?
|
next if availability.plan_ids.empty?
|
||||||
next if required_subscription?(availability, pending_subscription)
|
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
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
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
|
protected
|
||||||
|
|
||||||
def credits
|
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
|
# Group the slots by date, if the extended_prices_in_same_day option is set to true
|
||||||
##
|
##
|
||||||
def grouped_slots
|
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 }
|
cart_item_reservation_slots.group_by { |slot| slot.slot[:start_at].to_date }
|
||||||
end
|
|
||||||
|
|
||||||
def expand_slot(slot)
|
|
||||||
slot.merge({ slot_attributes: Slot.find(slot[:slot_id]) })
|
|
||||||
end
|
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
|
# @param prices {{ prices: Array<{price: Price, duration: number}> }} list of prices to use with the current reservation
|
||||||
# @see get_slot_price
|
# @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)
|
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].find { |p| p[:duration] <= slot_minutes && p[:duration].positive? }
|
||||||
price = prices[:prices].first if price.nil?
|
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
|
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
|
# 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
|
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
|
# Compute the price of a single slot, according to the base price and the ability for an admin
|
||||||
# to offer the slot.
|
# to offer the slot.
|
||||||
# @param hourly_rate {Number} base price of a 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 is_privileged {Boolean} true if the current user has a privileged role (admin or manager)
|
||||||
# @param [options] {Hash} optional parameters, allowing the following options:
|
# @param [options] {Hash} optional parameters, allowing the following options:
|
||||||
# - elements {Array} if provided the resulting price will be append into elements.slots
|
# - 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
|
# - prepaid_minutes {Number} number of remaining prepaid minutes for the customer
|
||||||
# @return {Number} price of the slot
|
# @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)
|
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
|
||||||
|
|
||||||
slot_rate = options[:has_credits] || (slot[:offered] && is_privileged) ? 0 : hourly_rate
|
slot_rate = options[:has_credits] || (slot_reservation[: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_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
|
# apply the base price to the real slot duration
|
||||||
real_price = if options[:is_division]
|
real_price = if options[:is_division]
|
||||||
((Rational(slot_rate) / Rational(MINUTES_PER_HOUR)) * Rational(slot_minutes)).to_f
|
((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?
|
unless options[:elements].nil?
|
||||||
options[:elements][:slots].push(
|
options[:elements][:slots].push(
|
||||||
start_at: slot[:slot_attributes][:start_at],
|
start_at: slot_reservation.slot[:start_at],
|
||||||
price: real_price,
|
price: real_price,
|
||||||
promo: (slot_rate != hourly_rate)
|
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,
|
# 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).
|
# 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.
|
# All these prices are returned to be applied to the reservation.
|
||||||
def applicable_prices(slots)
|
def applicable_prices(slots_reservations)
|
||||||
total_duration = slots.map do |slot|
|
total_duration = slots_reservations.map do |slot|
|
||||||
(slot[:slot_attributes][:end_at].to_time - slot[:slot_attributes][:start_at].to_time) / SECONDS_PER_MINUTE
|
(slot.slot[:end_at].to_time - slot.slot[:start_at].to_time) / SECONDS_PER_MINUTE
|
||||||
end.reduce(:+)
|
end.reduce(:+)
|
||||||
rates = { prices: [] }
|
rates = { prices: [] }
|
||||||
|
|
||||||
remaining_duration = total_duration
|
remaining_duration = total_duration
|
||||||
while remaining_duration.positive?
|
while remaining_duration.positive?
|
||||||
max_duration = @reservable.prices.where(group_id: @customer.group_id, plan_id: @plan.try(:id))
|
max_duration = reservable&.prices&.where(group_id: customer.group_id, plan_id: plan.try(:id))
|
||||||
.where(Price.arel_table[:duration].lteq(remaining_duration))
|
&.where(Price.arel_table[:duration].lteq(remaining_duration))
|
||||||
.maximum(:duration)
|
&.maximum(:duration)
|
||||||
max_duration = 60 if max_duration.nil?
|
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
|
current_duration = [remaining_duration, max_duration].min
|
||||||
rates[:prices].push(price: max_duration_price, duration: current_duration)
|
rates[:prices].push(price: max_duration_price, duration: current_duration)
|
||||||
@ -200,14 +215,14 @@ class CartItem::Reservation < CartItem::BaseItem
|
|||||||
|
|
||||||
hours_available = credits.hours
|
hours_available = credits.hours
|
||||||
unless new_plan_being_bought
|
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
|
hours_available = credits.hours - user_credit.hours_used if user_credit
|
||||||
end
|
end
|
||||||
hours_available
|
hours_available
|
||||||
end
|
end
|
||||||
|
|
||||||
def slots_params
|
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
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
@ -215,9 +230,9 @@ class CartItem::Reservation < CartItem::BaseItem
|
|||||||
# has the required susbcription, otherwise, check if the operator is privileged
|
# has the required susbcription, otherwise, check if the operator is privileged
|
||||||
##
|
##
|
||||||
def required_subscription?(availability, pending_subscription)
|
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)) ||
|
(pending_subscription && availability.plan_ids.include?(pending_subscription.plan.id)) ||
|
||||||
(@operator.manager? && @customer.id != @operator.id) ||
|
(operator.manager? && customer.id != operator.id) ||
|
||||||
@operator.admin?
|
operator.admin?
|
||||||
end
|
end
|
||||||
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
|
# A space reservation added to the shopping cart
|
||||||
class CartItem::SpaceReservation < CartItem::Reservation
|
class CartItem::SpaceReservation < CartItem::Reservation
|
||||||
# @param plan {Plan} a subscription bought at the same time of the reservation OR an already running subscription
|
self.table_name = 'cart_item_reservations'
|
||||||
# @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
|
|
||||||
|
|
||||||
super(customer, operator, space, slots)
|
has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy, inverse_of: :cart_item,
|
||||||
@plan = plan
|
foreign_key: 'cart_item_id', foreign_type: 'cart_item_type'
|
||||||
@space = space
|
accepts_nested_attributes_for :cart_item_reservation_slots
|
||||||
@new_subscription = new_subscription
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_object
|
belongs_to :operator_profile, class_name: 'InvoicingProfile'
|
||||||
::Reservation.new(
|
belongs_to :customer_profile, class_name: 'InvoicingProfile'
|
||||||
reservable_id: @reservable.id,
|
|
||||||
reservable_type: Space.name,
|
belongs_to :reservable, polymorphic: true
|
||||||
slots_reservations_attributes: slots_params,
|
|
||||||
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
belongs_to :plan
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def type
|
def type
|
||||||
'space'
|
'space'
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid?(all_items)
|
def valid?(all_items)
|
||||||
if @space.disabled
|
if reservable.disabled
|
||||||
@errors[:reservable] = I18n.t('cart_item_validation.space')
|
errors.add(:reservable, I18n.t('cart_item_validation.space'))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -38,9 +31,9 @@ class CartItem::SpaceReservation < CartItem::Reservation
|
|||||||
protected
|
protected
|
||||||
|
|
||||||
def credits
|
def credits
|
||||||
return 0 if @plan.nil?
|
return 0 if plan.nil?
|
||||||
|
|
||||||
space_credit = @plan.space_credits.find { |credit| credit.creditable_id == @reservable.id }
|
space_credit = plan.space_credits.find { |credit| credit.creditable_id == reservable.id }
|
||||||
credits_hours(space_credit, new_plan_being_bought: @new_subscription)
|
credits_hours(space_credit, new_plan_being_bought: new_subscription)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,21 +2,11 @@
|
|||||||
|
|
||||||
# A subscription added to the shopping cart
|
# A subscription added to the shopping cart
|
||||||
class CartItem::Subscription < CartItem::BaseItem
|
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)
|
def customer
|
||||||
raise TypeError unless plan.is_a? Plan
|
customer_profile.user
|
||||||
|
|
||||||
@plan = plan
|
|
||||||
@customer = customer
|
|
||||||
@start_at = start_at
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def plan
|
|
||||||
raise InvalidGroupError if @plan.group_id != @customer.group_id
|
|
||||||
|
|
||||||
@plan
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def price
|
def price
|
||||||
@ -27,14 +17,14 @@ class CartItem::Subscription < CartItem::BaseItem
|
|||||||
end
|
end
|
||||||
|
|
||||||
def name
|
def name
|
||||||
@plan.base_name
|
plan.base_name
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_object
|
def to_object
|
||||||
::Subscription.new(
|
::Subscription.new(
|
||||||
plan_id: @plan.id,
|
plan_id: plan.id,
|
||||||
statistic_profile_id: StatisticProfile.find_by(user: @customer).id,
|
statistic_profile_id: StatisticProfile.find_by(user: customer).id,
|
||||||
start_at: @start_at
|
start_at: start_at
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -43,8 +33,12 @@ class CartItem::Subscription < CartItem::BaseItem
|
|||||||
end
|
end
|
||||||
|
|
||||||
def valid?(_all_items)
|
def valid?(_all_items)
|
||||||
if @plan.disabled
|
if plan.disabled
|
||||||
@errors[:item] = I18n.t('cart_item_validation.plan')
|
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
|
return false
|
||||||
end
|
end
|
||||||
true
|
true
|
||||||
|
@ -2,45 +2,39 @@
|
|||||||
|
|
||||||
# A training reservation added to the shopping cart
|
# A training reservation added to the shopping cart
|
||||||
class CartItem::TrainingReservation < CartItem::Reservation
|
class CartItem::TrainingReservation < CartItem::Reservation
|
||||||
# @param plan {Plan} a subscription bought at the same time of the reservation OR an already running subscription
|
self.table_name = 'cart_item_reservations'
|
||||||
# @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
|
|
||||||
|
|
||||||
super(customer, operator, training, slots)
|
has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy, inverse_of: :cart_item,
|
||||||
@plan = plan
|
foreign_key: 'cart_item_id', foreign_type: 'cart_item_type'
|
||||||
@new_subscription = new_subscription
|
accepts_nested_attributes_for :cart_item_reservation_slots
|
||||||
end
|
|
||||||
|
belongs_to :operator_profile, class_name: 'InvoicingProfile'
|
||||||
|
belongs_to :customer_profile, class_name: 'InvoicingProfile'
|
||||||
|
|
||||||
|
belongs_to :reservable, polymorphic: true
|
||||||
|
|
||||||
|
belongs_to :plan
|
||||||
|
|
||||||
def price
|
def price
|
||||||
base_amount = @reservable.amount_by_group(@customer.group_id).amount
|
base_amount = reservable&.amount_by_group(customer.group_id)&.amount
|
||||||
is_privileged = @operator.admin? || (@operator.manager? && @operator.id != @customer.id)
|
is_privileged = operator.admin? || (operator.manager? && operator.id != customer.id)
|
||||||
|
|
||||||
elements = { slots: [] }
|
elements = { slots: [] }
|
||||||
amount = 0
|
amount = 0
|
||||||
|
|
||||||
hours_available = credits
|
hours_available = credits
|
||||||
@slots.each do |slot|
|
cart_item_reservation_slots.each do |sr|
|
||||||
amount += get_slot_price(base_amount,
|
amount += get_slot_price(base_amount,
|
||||||
slot,
|
sr,
|
||||||
is_privileged,
|
is_privileged,
|
||||||
elements: elements,
|
elements: elements,
|
||||||
has_credits: (@customer.training_credits.size < hours_available),
|
has_credits: (customer.training_credits.size < hours_available),
|
||||||
is_division: false)
|
is_division: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
{ elements: elements, amount: amount }
|
{ elements: elements, amount: amount }
|
||||||
end
|
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
|
def type
|
||||||
'training'
|
'training'
|
||||||
end
|
end
|
||||||
@ -48,9 +42,9 @@ class CartItem::TrainingReservation < CartItem::Reservation
|
|||||||
protected
|
protected
|
||||||
|
|
||||||
def credits
|
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_credits&.select { |credit| credit.creditable_id == reservable&.id }&.any?
|
||||||
is_creditable ? @plan.training_credit_nb : 0
|
is_creditable ? plan&.training_credit_nb : 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,16 +2,18 @@
|
|||||||
|
|
||||||
# Coupon is a textual code associated with a discount rate or an amount of discount
|
# Coupon is a textual code associated with a discount rate or an amount of discount
|
||||||
class Coupon < ApplicationRecord
|
class Coupon < ApplicationRecord
|
||||||
has_many :invoices, dependent: :nullify
|
has_many :invoices, dependent: :restrict_with_error
|
||||||
has_many :payment_schedule, dependent: :nullify
|
has_many :payment_schedule, dependent: :restrict_with_error
|
||||||
has_many :orders, dependent: :nullify
|
has_many :orders, dependent: :restrict_with_error
|
||||||
|
|
||||||
|
has_many :cart_item_coupons, class_name: 'CartItem::Coupon', dependent: :destroy
|
||||||
|
|
||||||
after_create :create_gateway_coupon
|
after_create :create_gateway_coupon
|
||||||
before_destroy :delete_gateway_coupon
|
before_destroy :delete_gateway_coupon
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
validates :code, 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 :code, uniqueness: true
|
||||||
validates :validity_per_user, presence: true
|
validates :validity_per_user, presence: true
|
||||||
validates :validity_per_user, inclusion: { in: %w[once forever] }
|
validates :validity_per_user, inclusion: { in: %w[once forever] }
|
||||||
|
@ -31,6 +31,8 @@ class Event < ApplicationRecord
|
|||||||
has_one :advanced_accounting, as: :accountable, dependent: :destroy
|
has_one :advanced_accounting, as: :accountable, dependent: :destroy
|
||||||
accepts_nested_attributes_for :advanced_accounting, allow_destroy: true
|
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
|
attr_accessor :recurrence, :recurrence_end_at
|
||||||
|
|
||||||
before_save :update_nb_free_places
|
before_save :update_nb_free_places
|
||||||
|
@ -5,7 +5,8 @@ class EventPriceCategory < ApplicationRecord
|
|||||||
belongs_to :event
|
belongs_to :event
|
||||||
belongs_to :price_category
|
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 :price_category_id, presence: true
|
||||||
validates :amount, presence: true
|
validates :amount, presence: true
|
||||||
@ -17,5 +18,4 @@ class EventPriceCategory < ApplicationRecord
|
|||||||
def verify_no_associated_tickets
|
def verify_no_associated_tickets
|
||||||
throw(:abort) unless tickets.count.zero?
|
throw(:abort) unless tickets.count.zero?
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -27,6 +27,25 @@ class InvoicingProfile < ApplicationRecord
|
|||||||
|
|
||||||
has_many :accounting_lines, dependent: :destroy
|
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
|
before_validation :set_external_id_nil
|
||||||
validates :external_id, uniqueness: true, allow_blank: true
|
validates :external_id, uniqueness: true, allow_blank: true
|
||||||
validates :address, presence: true, if: -> { Setting.get('address_required') }
|
validates :address, presence: true, if: -> { Setting.get('address_required') }
|
||||||
|
@ -37,6 +37,9 @@ class Machine < ApplicationRecord
|
|||||||
has_one :advanced_accounting, as: :accountable, dependent: :destroy
|
has_one :advanced_accounting, as: :accountable, dependent: :destroy
|
||||||
accepts_nested_attributes_for :advanced_accounting, allow_destroy: true
|
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
|
belongs_to :category
|
||||||
|
|
||||||
after_create :create_statistic_subtype
|
after_create :create_statistic_subtype
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# 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
|
class Order < PaymentDocument
|
||||||
belongs_to :statistic_profile
|
belongs_to :statistic_profile
|
||||||
belongs_to :operator_profile, class_name: 'InvoicingProfile'
|
belongs_to :operator_profile, class_name: 'InvoicingProfile'
|
||||||
|
@ -15,6 +15,12 @@ class Plan < ApplicationRecord
|
|||||||
has_many :prices, dependent: :destroy
|
has_many :prices, dependent: :destroy
|
||||||
has_one :payment_gateway_object, -> { order id: :desc }, inverse_of: :plan, as: :item, 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
|
extend FriendlyId
|
||||||
friendly_id :base_name, use: :slugged
|
friendly_id :base_name, use: :slugged
|
||||||
|
|
||||||
|
@ -8,12 +8,14 @@
|
|||||||
# The number of hours in a pack is stored in minutes.
|
# The number of hours in a pack is stored in minutes.
|
||||||
class PrepaidPack < ApplicationRecord
|
class PrepaidPack < ApplicationRecord
|
||||||
belongs_to :priceable, polymorphic: true
|
belongs_to :priceable, polymorphic: true
|
||||||
belongs_to :machine, foreign_type: 'Machine', 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'
|
belongs_to :space, foreign_type: 'Space', foreign_key: 'priceable_id', inverse_of: :prepaid_packs
|
||||||
|
|
||||||
belongs_to :group
|
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
|
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|
|
scope :published_or_drafts, lambda { |author_profile|
|
||||||
where("state = 'published' OR (state = 'draft' AND author_statistic_profile_id = ?)", 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 :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_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 }) }
|
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)
|
payment.post_save(payment_id, payment_type)
|
||||||
end
|
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 = objects.map(&:errors).flatten.concat(items.map(&:errors))
|
||||||
errors.push('Unable to create the PaymentDocument') if payment.nil?
|
errors.push('Unable to create the PaymentDocument') if payment.nil?
|
||||||
{ success: success, payment: payment, errors: errors }
|
{ success: success, payment: payment, errors: errors }
|
||||||
|
@ -10,6 +10,8 @@ class Slot < ApplicationRecord
|
|||||||
has_many :reservations, through: :slots_reservations
|
has_many :reservations, through: :slots_reservations
|
||||||
belongs_to :availability
|
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
|
attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :current_user_slots_reservations_ids
|
||||||
|
|
||||||
def full?(reservable = nil)
|
def full?(reservable = nil)
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
class SlotsReservation < ApplicationRecord
|
class SlotsReservation < ApplicationRecord
|
||||||
belongs_to :slot
|
belongs_to :slot
|
||||||
belongs_to :reservation
|
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 :set_ex_start_end_dates_attrs, if: :slot_changed?
|
||||||
after_update :notify_member_and_admin_slot_is_modified, 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?
|
after_update :update_event_nb_free_places, if: :canceled?
|
||||||
|
|
||||||
def set_ex_start_end_dates_attrs
|
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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -31,6 +31,9 @@ class Space < ApplicationRecord
|
|||||||
has_one :advanced_accounting, as: :accountable, dependent: :destroy
|
has_one :advanced_accounting, as: :accountable, dependent: :destroy
|
||||||
accepts_nested_attributes_for :advanced_accounting, allow_destroy: true
|
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_statistic_subtype
|
||||||
after_create :create_space_prices
|
after_create :create_space_prices
|
||||||
after_create :update_gateway_product
|
after_create :update_gateway_product
|
||||||
|
@ -12,6 +12,8 @@ class Subscription < ApplicationRecord
|
|||||||
has_many :invoice_items, as: :object, dependent: :destroy
|
has_many :invoice_items, as: :object, dependent: :destroy
|
||||||
has_many :offer_days, 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 :plan_id, presence: true
|
||||||
validates_with SubscriptionGroupValidator
|
validates_with SubscriptionGroupValidator
|
||||||
|
|
||||||
|
@ -32,6 +32,9 @@ class Training < ApplicationRecord
|
|||||||
has_one :advanced_accounting, as: :accountable, dependent: :destroy
|
has_one :advanced_accounting, as: :accountable, dependent: :destroy
|
||||||
accepts_nested_attributes_for :advanced_accounting, allow_destroy: true
|
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_statistic_subtype
|
||||||
after_create :create_trainings_pricings
|
after_create :create_trainings_pricings
|
||||||
after_create :update_gateway_product
|
after_create :update_gateway_product
|
||||||
|
@ -5,10 +5,30 @@ class Cart::AddItemService
|
|||||||
def call(order, orderable, quantity = 1)
|
def call(order, orderable, quantity = 1)
|
||||||
return order if quantity.to_i.zero?
|
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?
|
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)
|
item = order.order_items.find_by(orderable: orderable)
|
||||||
quantity = orderable.quantity_min > quantity.to_i && item.nil? ? orderable.quantity_min : quantity.to_i
|
quantity = orderable.quantity_min > quantity.to_i && item.nil? ? orderable.quantity_min : quantity.to_i
|
||||||
|
|
||||||
@ -19,11 +39,14 @@ class Cart::AddItemService
|
|||||||
end
|
end
|
||||||
raise Cart::OutStockError if item.quantity > orderable.stock['external']
|
raise Cart::OutStockError if item.quantity > orderable.stock['external']
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
item
|
||||||
item.save
|
|
||||||
Cart::UpdateTotalService.new.call(order)
|
|
||||||
order.save
|
|
||||||
end
|
end
|
||||||
order.reload
|
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
@ -11,25 +11,46 @@ class CartService
|
|||||||
# @see app/frontend/src/javascript/models/payment.ts > interface ShoppingCart
|
# @see app/frontend/src/javascript/models/payment.ts > interface ShoppingCart
|
||||||
##
|
##
|
||||||
def from_hash(cart_items)
|
def from_hash(cart_items)
|
||||||
|
cart_items.permit! if cart_items.is_a? ActionController::Parameters
|
||||||
|
|
||||||
@customer = customer(cart_items)
|
@customer = customer(cart_items)
|
||||||
plan_info = plan(cart_items)
|
plan_info = plan(cart_items)
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
cart_items[:items].each do |item|
|
cart_items[:items].each do |item|
|
||||||
if ['subscription', :subscription].include?(item.keys.first)
|
if ['subscription', :subscription].include?(item.keys.first) && plan_info[:new_subscription]
|
||||||
items.push(CartItem::Subscription.new(plan_info[:plan], @customer, item[:subscription][:start_at])) if 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)
|
elsif ['reservation', :reservation].include?(item.keys.first)
|
||||||
items.push(reservable_from_hash(item[:reservation], plan_info))
|
items.push(reservable_from_hash(item[:reservation], plan_info))
|
||||||
elsif ['prepaid_pack', :prepaid_pack].include?(item.keys.first)
|
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)
|
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
|
||||||
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(
|
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(
|
ShoppingCart.new(
|
||||||
@ -47,19 +68,40 @@ class CartService
|
|||||||
subscription = payment_schedule.payment_schedule_objects.find { |pso| pso.object_type == Subscription.name }&.subscription
|
subscription = payment_schedule.payment_schedule_objects.find { |pso| pso.object_type == Subscription.name }&.subscription
|
||||||
plan = subscription&.plan
|
plan = subscription&.plan
|
||||||
|
|
||||||
coupon = CartItem::Coupon.new(@customer, @operator, payment_schedule.coupon&.code)
|
coupon = CartItem::Coupon.new(
|
||||||
schedule = CartItem::PaymentSchedule.new(plan, coupon, true, @customer, subscription.start_at)
|
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 = []
|
items = []
|
||||||
payment_schedule.payment_schedule_objects.each do |object|
|
payment_schedule.payment_schedule_objects.each do |object|
|
||||||
if object.object_type == Subscription.name
|
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
|
elsif object.object_type == Reservation.name
|
||||||
items.push(reservable_from_payment_schedule_object(object, plan))
|
items.push(reservable_from_payment_schedule_object(object, plan))
|
||||||
elsif object.object_type == PrepaidPack.name
|
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
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -83,7 +125,11 @@ class CartService
|
|||||||
if cart_items[:items][index][:subscription][:plan_id]
|
if cart_items[:items][index][:subscription][:plan_id]
|
||||||
new_plan_being_bought = true
|
new_plan_being_bought = true
|
||||||
plan = Plan.find(cart_items[:items][index][:subscription][:plan_id])
|
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
|
plan
|
||||||
end
|
end
|
||||||
elsif @customer.subscribed_plan
|
elsif @customer.subscribed_plan
|
||||||
@ -107,31 +153,31 @@ class CartService
|
|||||||
reservable = cart_item[:reservable_type]&.constantize&.find(cart_item[:reservable_id])
|
reservable = cart_item[:reservable_type]&.constantize&.find(cart_item[:reservable_id])
|
||||||
case reservable
|
case reservable
|
||||||
when Machine
|
when Machine
|
||||||
CartItem::MachineReservation.new(@customer,
|
CartItem::MachineReservation.new(customer_profile: @customer.invoicing_profile,
|
||||||
@operator,
|
operator_profile: @operator.invoicing_profile,
|
||||||
reservable,
|
reservable: reservable,
|
||||||
cart_item[:slots_reservations_attributes],
|
cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes],
|
||||||
plan: plan_info[:plan],
|
plan: plan_info[:plan],
|
||||||
new_subscription: plan_info[:new_subscription])
|
new_subscription: plan_info[:new_subscription])
|
||||||
when Training
|
when Training
|
||||||
CartItem::TrainingReservation.new(@customer,
|
CartItem::TrainingReservation.new(customer_profile: @customer.invoicing_profile,
|
||||||
@operator,
|
operator_profile: @operator.invoicing_profile,
|
||||||
reservable,
|
reservable: reservable,
|
||||||
cart_item[:slots_reservations_attributes],
|
cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes],
|
||||||
plan: plan_info[:plan],
|
plan: plan_info[:plan],
|
||||||
new_subscription: plan_info[:new_subscription])
|
new_subscription: plan_info[:new_subscription])
|
||||||
when Event
|
when Event
|
||||||
CartItem::EventReservation.new(@customer,
|
CartItem::EventReservation.new(customer_profile: @customer.invoicing_profile,
|
||||||
@operator,
|
operator_profile: @operator.invoicing_profile,
|
||||||
reservable,
|
event: reservable,
|
||||||
cart_item[:slots_reservations_attributes],
|
cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes],
|
||||||
normal_tickets: cart_item[:nb_reserve_places],
|
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
|
when Space
|
||||||
CartItem::SpaceReservation.new(@customer,
|
CartItem::SpaceReservation.new(customer_profile: @customer.invoicing_profile,
|
||||||
@operator,
|
operator_profile: @operator.invoicing_profile,
|
||||||
reservable,
|
reservable: reservable,
|
||||||
cart_item[:slots_reservations_attributes],
|
cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes],
|
||||||
plan: plan_info[:plan],
|
plan: plan_info[:plan],
|
||||||
new_subscription: plan_info[:new_subscription])
|
new_subscription: plan_info[:new_subscription])
|
||||||
else
|
else
|
||||||
@ -144,31 +190,31 @@ class CartService
|
|||||||
reservable = object.reservation.reservable
|
reservable = object.reservation.reservable
|
||||||
case reservable
|
case reservable
|
||||||
when Machine
|
when Machine
|
||||||
CartItem::MachineReservation.new(@customer,
|
CartItem::MachineReservation.new(customer_profile: @customer.invoicing_profile,
|
||||||
@operator,
|
operator_profile: @operator.invoicing_profile,
|
||||||
reservable,
|
reservable: reservable,
|
||||||
object.reservation.slots_reservations,
|
cart_item_reservation_slots_attributes: object.reservation.slots_reservations,
|
||||||
plan: plan,
|
plan: plan,
|
||||||
new_subscription: true)
|
new_subscription: true)
|
||||||
when Training
|
when Training
|
||||||
CartItem::TrainingReservation.new(@customer,
|
CartItem::TrainingReservation.new(customer_profile: @customer.invoicing_profile,
|
||||||
@operator,
|
operator_profile: @operator.invoicing_profile,
|
||||||
reservable,
|
reservable: reservable,
|
||||||
object.reservation.slots_reservations,
|
cart_item_reservation_slots_attributes: object.reservation.slots_reservations,
|
||||||
plan: plan,
|
plan: plan,
|
||||||
new_subscription: true)
|
new_subscription: true)
|
||||||
when Event
|
when Event
|
||||||
CartItem::EventReservation.new(@customer,
|
CartItem::EventReservation.new(customer_profile: @customer.invoicing_profile,
|
||||||
@operator,
|
operator_profile: @operator.invoicing_profile,
|
||||||
reservable,
|
event: reservable,
|
||||||
object.reservation.slots_reservations,
|
cart_item_reservation_slots_attributes: object.reservation.slots_reservation,
|
||||||
normal_tickets: object.reservation.nb_reserve_places,
|
normal_tickets: object.reservation.nb_reserve_places,
|
||||||
other_tickets: object.reservation.tickets)
|
cart_item_event_reservation_tickets_attributes: object.reservation.tickets)
|
||||||
when Space
|
when Space
|
||||||
CartItem::SpaceReservation.new(@customer,
|
CartItem::SpaceReservation.new(customer_profile: @customer.invoicing_profile,
|
||||||
@operator,
|
operator_profile: @operator.invoicing_profile,
|
||||||
reservable,
|
reservable: reservable,
|
||||||
object.reservation.slots_reservations,
|
cart_item_reservation_slots_attributes: object.reservation.slots_reservations,
|
||||||
plan: plan,
|
plan: plan,
|
||||||
new_subscription: true)
|
new_subscription: true)
|
||||||
else
|
else
|
||||||
|
@ -7,9 +7,9 @@ class StripeCardTokenValidator
|
|||||||
|
|
||||||
res = Stripe::Token.retrieve(options[:token], api_key: Setting.get('stripe_secret_key'))
|
res = Stripe::Token.retrieve(options[:token], api_key: Setting.get('stripe_secret_key'))
|
||||||
if res[:id] != options[:token]
|
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
|
end
|
||||||
rescue Stripe::InvalidRequestError => e
|
rescue Stripe::InvalidRequestError => e
|
||||||
record.errors[:card_token] << e
|
record.errors.add(:card_token, e)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -67,6 +67,8 @@ en:
|
|||||||
length_must_be_slot_multiple: "must be at least %{MIN} minutes after the start date"
|
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"
|
must_be_associated_with_at_least_1_machine: "must be associated with at least 1 machine"
|
||||||
deleted_user: "Deleted user"
|
deleted_user: "Deleted user"
|
||||||
|
coupon:
|
||||||
|
code_format_error: "only caps letters, numbers, and dashes are allowed"
|
||||||
#members management
|
#members management
|
||||||
members:
|
members:
|
||||||
unable_to_change_the_group_while_a_subscription_is_running: "Unable to change the group while a subscription is running"
|
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"
|
enable_extension "unaccent"
|
||||||
|
|
||||||
create_table "abuses", id: :serial, force: :cascade do |t|
|
create_table "abuses", id: :serial, force: :cascade do |t|
|
||||||
t.integer "signaled_id"
|
|
||||||
t.string "signaled_type"
|
t.string "signaled_type"
|
||||||
|
t.integer "signaled_id"
|
||||||
t.string "first_name"
|
t.string "first_name"
|
||||||
t.string "last_name"
|
t.string "last_name"
|
||||||
t.string "email"
|
t.string "email"
|
||||||
@ -68,8 +68,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
t.string "locality"
|
t.string "locality"
|
||||||
t.string "country"
|
t.string "country"
|
||||||
t.string "postal_code"
|
t.string "postal_code"
|
||||||
t.integer "placeable_id"
|
|
||||||
t.string "placeable_type"
|
t.string "placeable_type"
|
||||||
|
t.integer "placeable_id"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
end
|
end
|
||||||
@ -93,8 +93,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "assets", id: :serial, force: :cascade do |t|
|
create_table "assets", id: :serial, force: :cascade do |t|
|
||||||
t.integer "viewable_id"
|
|
||||||
t.string "viewable_type"
|
t.string "viewable_type"
|
||||||
|
t.integer "viewable_id"
|
||||||
t.string "attachment"
|
t.string "attachment"
|
||||||
t.string "type"
|
t.string "type"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
@ -281,8 +281,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "credits", id: :serial, force: :cascade do |t|
|
create_table "credits", id: :serial, force: :cascade do |t|
|
||||||
t.integer "creditable_id"
|
|
||||||
t.string "creditable_type"
|
t.string "creditable_type"
|
||||||
|
t.integer "creditable_id"
|
||||||
t.integer "plan_id"
|
t.integer "plan_id"
|
||||||
t.integer "hours"
|
t.integer "hours"
|
||||||
t.datetime "created_at"
|
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|
|
create_table "notifications", id: :serial, force: :cascade do |t|
|
||||||
t.integer "receiver_id"
|
t.integer "receiver_id"
|
||||||
t.integer "attached_object_id"
|
|
||||||
t.string "attached_object_type"
|
t.string "attached_object_type"
|
||||||
|
t.integer "attached_object_id"
|
||||||
t.integer "notification_type_id"
|
t.integer "notification_type_id"
|
||||||
t.boolean "is_read", default: false
|
t.boolean "is_read", default: false
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.string "receiver_type"
|
t.string "receiver_type"
|
||||||
t.boolean "is_send", default: false
|
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 ["notification_type_id"], name: "index_notifications_on_notification_type_id"
|
||||||
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
|
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
|
||||||
end
|
end
|
||||||
@ -772,8 +772,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
|
|||||||
create_table "prices", id: :serial, force: :cascade do |t|
|
create_table "prices", id: :serial, force: :cascade do |t|
|
||||||
t.integer "group_id"
|
t.integer "group_id"
|
||||||
t.integer "plan_id"
|
t.integer "plan_id"
|
||||||
t.integer "priceable_id"
|
|
||||||
t.string "priceable_type"
|
t.string "priceable_type"
|
||||||
|
t.integer "priceable_id"
|
||||||
t.integer "amount"
|
t.integer "amount"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_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.text "message"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.integer "reservable_id"
|
|
||||||
t.string "reservable_type"
|
t.string "reservable_type"
|
||||||
|
t.integer "reservable_id"
|
||||||
t.integer "nb_reserve_places"
|
t.integer "nb_reserve_places"
|
||||||
t.integer "statistic_profile_id"
|
t.integer "statistic_profile_id"
|
||||||
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_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|
|
create_table "roles", id: :serial, force: :cascade do |t|
|
||||||
t.string "name"
|
t.string "name"
|
||||||
t.integer "resource_id"
|
|
||||||
t.string "resource_type"
|
t.string "resource_type"
|
||||||
|
t.integer "resource_id"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.datetime "updated_at"
|
t.datetime "updated_at"
|
||||||
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
|
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'
|
require 'test_helper'
|
||||||
|
|
||||||
|
module Subscriptions; end
|
||||||
|
|
||||||
class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest
|
class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest
|
||||||
setup do
|
setup do
|
||||||
@user = User.find_by(username: 'jdupond')
|
@user = User.find_by(username: 'jdupond')
|
||||||
@ -57,7 +59,7 @@ class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
# Check notifications were sent for every admins
|
# Check notifications were sent for every admins
|
||||||
notifications = Notification.where(
|
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_type: 'Subscription',
|
||||||
attached_object_id: subscription[:id]
|
attached_object_id: subscription[:id]
|
||||||
)
|
)
|
||||||
@ -100,7 +102,7 @@ class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest
|
|||||||
assert_equal Mime[:json], response.content_type
|
assert_equal Mime[:json], response.content_type
|
||||||
|
|
||||||
# Check the error was handled
|
# 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
|
# Check that the user has no subscription
|
||||||
assert_nil @user.subscription, "user's subscription was found"
|
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
|
# Check notifications were sent for every admins
|
||||||
notifications = Notification.where(
|
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_type: 'Subscription',
|
||||||
attached_object_id: subscription[:id]
|
attached_object_id: subscription[:id]
|
||||||
)
|
)
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
require 'test_helper'
|
require 'test_helper'
|
||||||
|
|
||||||
|
module Subscriptions; end
|
||||||
|
|
||||||
class Subscriptions::RenewAsUserTest < ActionDispatch::IntegrationTest
|
class Subscriptions::RenewAsUserTest < ActionDispatch::IntegrationTest
|
||||||
setup do
|
setup do
|
||||||
@user = User.find_by(username: 'atiermoulin')
|
@user = User.find_by(username: 'atiermoulin')
|
||||||
@ -60,7 +62,7 @@ class Subscriptions::RenewAsUserTest < ActionDispatch::IntegrationTest
|
|||||||
|
|
||||||
# Check notifications were sent for every admins
|
# Check notifications were sent for every admins
|
||||||
notifications = Notification.where(
|
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_type: 'Subscription',
|
||||||
attached_object_id: subscription[:id]
|
attached_object_id: subscription[:id]
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user