diff --git a/app/controllers/api/cart_controller.rb b/app/controllers/api/cart_controller.rb index 5b150a70e..829bd29bb 100644 --- a/app/controllers/api/cart_controller.rb +++ b/app/controllers/api/cart_controller.rb @@ -52,6 +52,6 @@ class API::CartController < API::ApiController private def orderable - Product.find(cart_params[:orderable_id]) + params[:orderable_type].classify.constantize.find(cart_params[:orderable_id]) end end diff --git a/app/controllers/api/payments_controller.rb b/app/controllers/api/payments_controller.rb index 30cb5aa6c..7a878ee86 100644 --- a/app/controllers/api/payments_controller.rb +++ b/app/controllers/api/payments_controller.rb @@ -41,6 +41,7 @@ class API::PaymentsController < API::ApiController { json: res[:errors].drop_while(&:empty?), status: :unprocessable_entity } end rescue StandardError => e + Rails.logger.debug e.backtrace { json: e, status: :unprocessable_entity } end end diff --git a/app/exceptions/cart/unknown_item_error.rb b/app/exceptions/cart/unknown_item_error.rb new file mode 100644 index 000000000..b47b4c18f --- /dev/null +++ b/app/exceptions/cart/unknown_item_error.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# Raised when the added item is not a recognized class +class Cart::UnknownItemError < StandardError +end diff --git a/app/frontend/src/javascript/api/cart.ts b/app/frontend/src/javascript/api/cart.ts index 1f5627cd9..eeb89976b 100644 --- a/app/frontend/src/javascript/api/cart.ts +++ b/app/frontend/src/javascript/api/cart.ts @@ -1,6 +1,6 @@ import apiClient from './clients/api-client'; import { AxiosResponse } from 'axios'; -import { Order, OrderErrors } from '../models/order'; +import { Order, OrderableType, OrderErrors } from '../models/order'; export default class CartAPI { static async create (token?: string): Promise { @@ -8,28 +8,28 @@ export default class CartAPI { return res?.data; } - static async addItem (order: Order, orderableId: number, quantity: number): Promise { - const res: AxiosResponse = await apiClient.put('/api/cart/add_item', { order_token: order.token, orderable_id: orderableId, quantity }); + static async addItem (order: Order, orderableId: number, orderableType: OrderableType, quantity: number): Promise { + const res: AxiosResponse = await apiClient.put('/api/cart/add_item', { order_token: order.token, orderable_id: orderableId, orderable_type: orderableType, quantity }); return res?.data; } - static async removeItem (order: Order, orderableId: number): Promise { - const res: AxiosResponse = await apiClient.put('/api/cart/remove_item', { order_token: order.token, orderable_id: orderableId }); + static async removeItem (order: Order, orderableId: number, orderableType: OrderableType): Promise { + const res: AxiosResponse = await apiClient.put('/api/cart/remove_item', { order_token: order.token, orderable_id: orderableId, orderable_type: orderableType }); return res?.data; } - static async setQuantity (order: Order, orderableId: number, quantity: number): Promise { - const res: AxiosResponse = await apiClient.put('/api/cart/set_quantity', { order_token: order.token, orderable_id: orderableId, quantity }); + static async setQuantity (order: Order, orderableId: number, orderableType: OrderableType, quantity: number): Promise { + const res: AxiosResponse = await apiClient.put('/api/cart/set_quantity', { order_token: order.token, orderable_id: orderableId, orderable_type: orderableType, quantity }); return res?.data; } - static async setOffer (order: Order, orderableId: number, isOffered: boolean): Promise { - const res: AxiosResponse = await apiClient.put('/api/cart/set_offer', { order_token: order.token, orderable_id: orderableId, is_offered: isOffered, customer_id: order.user?.id }); + static async setOffer (order: Order, orderableId: number, orderableType: OrderableType, isOffered: boolean): Promise { + const res: AxiosResponse = await apiClient.put('/api/cart/set_offer', { order_token: order.token, orderable_id: orderableId, orderable_type: orderableType, is_offered: isOffered, customer_id: order.user?.id }); return res?.data; } - static async refreshItem (order: Order, orderableId: number): Promise { - const res: AxiosResponse = await apiClient.put('/api/cart/refresh_item', { order_token: order.token, orderable_id: orderableId }); + static async refreshItem (order: Order, orderableId: number, orderableType: OrderableType): Promise { + const res: AxiosResponse = await apiClient.put('/api/cart/refresh_item', { order_token: order.token, orderable_id: orderableId, orderable_type: orderableType }); return res?.data; } diff --git a/app/frontend/src/javascript/components/cart/store-cart.tsx b/app/frontend/src/javascript/components/cart/store-cart.tsx index 869e2cf80..6ff701989 100644 --- a/app/frontend/src/javascript/components/cart/store-cart.tsx +++ b/app/frontend/src/javascript/components/cart/store-cart.tsx @@ -65,7 +65,7 @@ const StoreCart: React.FC = ({ onSuccess, onError, currentUser, if (errors.length === 1 && errors[0].error === 'not_found') { reloadCart().catch(onError); } else { - CartAPI.removeItem(cart, item.orderable_id).then(data => { + CartAPI.removeItem(cart, item.orderable_id, item.orderable_type).then(data => { setCart(data); }).catch(onError); } @@ -76,7 +76,7 @@ const StoreCart: React.FC = ({ onSuccess, onError, currentUser, * Change product quantity */ const changeProductQuantity = (e: React.BaseSyntheticEvent, item) => { - CartAPI.setQuantity(cart, item.orderable_id, e.target.value) + CartAPI.setQuantity(cart, item.orderable_id, item.orderable_type, e.target.value) .then(data => { setCart(data); }) @@ -87,7 +87,7 @@ const StoreCart: React.FC = ({ onSuccess, onError, currentUser, * Increment/decrement product quantity */ const increaseOrDecreaseProductQuantity = (item, direction: 'up' | 'down') => { - CartAPI.setQuantity(cart, item.orderable_id, direction === 'up' ? item.quantity + 1 : item.quantity - 1) + CartAPI.setQuantity(cart, item.orderable_id, item.orderable_type, direction === 'up' ? item.quantity + 1 : item.quantity - 1) .then(data => { setCart(data); }) @@ -101,7 +101,7 @@ const StoreCart: React.FC = ({ onSuccess, onError, currentUser, return (e: React.BaseSyntheticEvent) => { e.preventDefault(); e.stopPropagation(); - CartAPI.refreshItem(cart, item.orderable_id).then(data => { + CartAPI.refreshItem(cart, item.orderable_id, item.orderable_type).then(data => { setCart(data); }).catch(onError); }; @@ -185,7 +185,7 @@ const StoreCart: React.FC = ({ onSuccess, onError, currentUser, // if the selected user is the operator, he cannot offer products to himself if (user.id === currentUser.id && cart.order_items_attributes.filter(item => item.is_offered).length > 0) { Promise.all(cart.order_items_attributes.filter(item => item.is_offered).map(item => { - return CartAPI.setOffer(cart, item.orderable_id, false); + return CartAPI.setOffer(cart, item.orderable_id, item.orderable_type, false); })).then((data) => setCart({ ...data[data.length - 1], user: { id: user.id, role: user.role } })); } else { setCart({ ...cart, user: { id: user.id, role: user.role } }); @@ -211,7 +211,7 @@ const StoreCart: React.FC = ({ onSuccess, onError, currentUser, */ const toggleProductOffer = (item) => { return (checked: boolean) => { - CartAPI.setOffer(cart, item.orderable_id, checked).then(data => { + CartAPI.setOffer(cart, item.orderable_id, item.orderable_type, checked).then(data => { setCart(data); }).catch(e => { if (e.match(/code 403/)) { diff --git a/app/frontend/src/javascript/components/reservations/reservations-summary.tsx b/app/frontend/src/javascript/components/reservations/reservations-summary.tsx new file mode 100644 index 000000000..ed60c4776 --- /dev/null +++ b/app/frontend/src/javascript/components/reservations/reservations-summary.tsx @@ -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 = ({ slot, customer, onError }) => { + const { cart, setCart } = useCart(customer); + const [pendingSlots, setPendingSlots] = useImmer>([]); + + 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 ( +
    {pendingSlots.map(slot => ( +
  • + {FormatLib.date(slot.start)} {FormatLib.time(slot.start)} - {FormatLib.time(slot.end)} + add to cart +
  • + ))}
+ ); +}; + +const ReservationsSummaryWrapper: React.FC = (props) => ( + + + +); + +Application.Components.component('reservationsSummary', react2angular(ReservationsSummaryWrapper, ['slot', 'customer', 'onError'])); diff --git a/app/frontend/src/javascript/components/store/store-product-item.tsx b/app/frontend/src/javascript/components/store/store-product-item.tsx index eb1c42145..2afaaf7e1 100644 --- a/app/frontend/src/javascript/components/store/store-product-item.tsx +++ b/app/frontend/src/javascript/components/store/store-product-item.tsx @@ -40,7 +40,7 @@ export const StoreProductItem: React.FC = ({ product, car const addProductToCart = (e: React.BaseSyntheticEvent) => { e.preventDefault(); e.stopPropagation(); - CartAPI.addItem(cart, product.id, 1).then(onSuccessAddProductToCart).catch(() => { + CartAPI.addItem(cart, product.id, 'Product', 1).then(onSuccessAddProductToCart).catch(() => { onError(t('app.public.store_product_item.stock_limit')); }); }; diff --git a/app/frontend/src/javascript/components/store/store-product.tsx b/app/frontend/src/javascript/components/store/store-product.tsx index 8175e7acc..ba3268479 100644 --- a/app/frontend/src/javascript/components/store/store-product.tsx +++ b/app/frontend/src/javascript/components/store/store-product.tsx @@ -108,7 +108,7 @@ export const StoreProduct: React.FC = ({ productSlug, current */ const addToCart = () => { if (toCartCount <= product.stock.external) { - CartAPI.addItem(cart, product.id, toCartCount).then(data => { + CartAPI.addItem(cart, product.id, 'Product', toCartCount).then(data => { setCart(data); onSuccess(t('app.public.store_product.add_to_cart_success')); }).catch(() => { diff --git a/app/frontend/src/javascript/controllers/machines.js.erb b/app/frontend/src/javascript/controllers/machines.js.erb index 16e19d276..de25fed88 100644 --- a/app/frontend/src/javascript/controllers/machines.js.erb +++ b/app/frontend/src/javascript/controllers/machines.js.erb @@ -393,12 +393,6 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran // Slot free to be booked const FREE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_COLOR %>'; - // Slot already booked by another user - const UNAVAILABLE_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::MACHINE_IS_RESERVED_BY_USER %>'; - - // Slot already booked by the current user - const BOOKED_SLOT_BORDER_COLOR = '<%= AvailabilityHelper::IS_RESERVED_BY_CURRENT_USER %>'; - /* PUBLIC SCOPE */ // bind the machine availabilities with full-Calendar events @@ -642,7 +636,6 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran * Callback triggered after a successful prepaid-pack purchase */ $scope.onSuccess = function (message) { - growl.success(message); }; diff --git a/app/frontend/src/javascript/models/order.ts b/app/frontend/src/javascript/models/order.ts index 08d9d53bf..c82e33df6 100644 --- a/app/frontend/src/javascript/models/order.ts +++ b/app/frontend/src/javascript/models/order.ts @@ -5,6 +5,8 @@ import { UserRole } from './user'; import { Coupon } from './coupon'; import { ApiFilter, PaginatedIndex } from './api'; +export type OrderableType = 'Product' | 'Slot'; + export interface Order { id: number, token: string, @@ -28,7 +30,7 @@ export interface Order { paid_total?: number, order_items_attributes: Array<{ id: number, - orderable_type: string, + orderable_type: OrderableType, orderable_id: number, orderable_name: string, orderable_slug: string, diff --git a/app/frontend/src/javascript/models/slot.ts b/app/frontend/src/javascript/models/slot.ts new file mode 100644 index 000000000..e62129898 --- /dev/null +++ b/app/frontend/src/javascript/models/slot.ts @@ -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, + tag_ids: Array, + tags: Array<{ + id: number, + name: string, + }> + plan_ids: Array, + + // 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 + } +} diff --git a/app/frontend/templates/machines/reserve.html b/app/frontend/templates/machines/reserve.html index 785ba2cee..b0b9d6aed 100644 --- a/app/frontend/templates/machines/reserve.html +++ b/app/frontend/templates/machines/reserve.html @@ -2,16 +2,22 @@
-
+
-
+

{{ 'app.logged.machines_reserve.machine_planning' | translate }} : {{machine.name}}

+ +
+
+ +
+
@@ -39,6 +45,8 @@ refresh="afterPaymentPromise"> + + } - def initialize(customer, operator, event, slots, normal_tickets: 0, other_tickets: []) - raise TypeError unless event.is_a? Event + has_many :cart_item_event_reservation_tickets, class_name: 'CartItem::EventReservationTicket', dependent: :destroy, + inverse_of: :cart_item_event_reservation, + foreign_key: 'cart_item_event_reservation_id' + accepts_nested_attributes_for :cart_item_event_reservation_tickets - super(customer, operator, event, slots) - @normal_tickets = normal_tickets || 0 - @other_tickets = other_tickets || [] + has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy, inverse_of: :cart_item, + foreign_key: 'cart_item_id', foreign_type: 'cart_item_type' + accepts_nested_attributes_for :cart_item_reservation_slots + + belongs_to :operator_profile, class_name: 'InvoicingProfile' + belongs_to :customer_profile, class_name: 'InvoicingProfile' + + belongs_to :event + + def reservable + event end def price - amount = @reservable.amount * @normal_tickets - is_privileged = @operator.privileged? && @operator.id != @customer.id + amount = reservable.amount * normal_tickets + is_privileged = operator.privileged? && operator.id != customer.id - @other_tickets.each do |ticket| - amount += ticket[:booked] * EventPriceCategory.find(ticket[:event_price_category_id]).amount + cart_item_event_reservation_tickets.each do |ticket| + amount += ticket.booked * ticket.event_price_category.amount end elements = { slots: [] } total = 0 - @slots.each do |slot| + cart_item_reservation_slots.each do |sr| total += get_slot_price(amount, - slot, + sr, is_privileged, elements: elements, is_division: false) @@ -36,26 +44,25 @@ class CartItem::EventReservation < CartItem::Reservation def to_object ::Reservation.new( - reservable_id: @reservable.id, + reservable_id: reservable.id, reservable_type: Event.name, slots_reservations_attributes: slots_params, - tickets_attributes: tickets_params, - nb_reserve_places: @normal_tickets, - statistic_profile_id: StatisticProfile.find_by(user: @customer).id + tickets_attributes: cart_item_event_reservation_tickets.map do |t| + { + event_price_category_id: t.event_price_category_id, + booked: t.booked + } + end, + nb_reserve_places: normal_tickets, + statistic_profile_id: StatisticProfile.find_by(user: customer).id ) end def name - @reservable.title + reservable.title end def type 'event' end - - protected - - def tickets_params - @other_tickets.map { |ticket| ticket.permit(:event_price_category_id, :booked) } - end end diff --git a/app/models/cart_item/event_reservation_ticket.rb b/app/models/cart_item/event_reservation_ticket.rb new file mode 100644 index 000000000..341e9b16f --- /dev/null +++ b/app/models/cart_item/event_reservation_ticket.rb @@ -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 diff --git a/app/models/cart_item/free_extension.rb b/app/models/cart_item/free_extension.rb index ce4a87d7d..ab2905b4b 100644 --- a/app/models/cart_item/free_extension.rb +++ b/app/models/cart_item/free_extension.rb @@ -2,22 +2,20 @@ # A subscription extended for free, added to the shopping cart class CartItem::FreeExtension < CartItem::BaseItem - def initialize(customer, subscription, new_expiration_date) - raise TypeError unless subscription.is_a? Subscription + belongs_to :customer_profile, class_name: 'InvoicingProfile' + belongs_to :subscription - @customer = customer - @new_expiration_date = new_expiration_date - @subscription = subscription - super + def customer + statistic_profile.user end def start_at - raise InvalidSubscriptionError if @subscription.nil? - if @new_expiration_date.nil? || @new_expiration_date <= @subscription.expired_at + raise InvalidSubscriptionError if subscription.nil? + if new_expiration_date.nil? || new_expiration_date <= subscription.expired_at raise InvalidSubscriptionError, I18n.t('cart_items.must_be_after_expiration') end - @subscription.expired_at + subscription.expired_at end def price @@ -27,14 +25,14 @@ class CartItem::FreeExtension < CartItem::BaseItem end def name - I18n.t('cart_items.free_extension', DATE: I18n.l(@new_expiration_date)) + I18n.t('cart_items.free_extension', DATE: I18n.l(new_expiration_date)) end def to_object ::OfferDay.new( - subscription_id: @subscription.id, + subscription_id: subscription.id, start_at: start_at, - end_at: @new_expiration_date + end_at: new_expiration_date ) end diff --git a/app/models/cart_item/machine_reservation.rb b/app/models/cart_item/machine_reservation.rb index 4c6105ccb..b85d71e15 100644 --- a/app/models/cart_item/machine_reservation.rb +++ b/app/models/cart_item/machine_reservation.rb @@ -2,46 +2,40 @@ # A machine reservation added to the shopping cart class CartItem::MachineReservation < CartItem::Reservation - # @param plan {Plan} a subscription bought at the same time of the reservation OR an already running subscription - # @param new_subscription {Boolean} true is new subscription is being bought at the same time of the reservation - def initialize(customer, operator, machine, slots, plan: nil, new_subscription: false) - raise TypeError unless machine.is_a? Machine + self.table_name = 'cart_item_reservations' - super(customer, operator, machine, slots) - @plan = plan - @new_subscription = new_subscription - end + has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy, inverse_of: :cart_item, + foreign_key: 'cart_item_id', foreign_type: 'cart_item_type' + accepts_nested_attributes_for :cart_item_reservation_slots - def to_object - ::Reservation.new( - reservable_id: @reservable.id, - reservable_type: Machine.name, - slots_reservations_attributes: slots_params, - statistic_profile_id: StatisticProfile.find_by(user: @customer).id - ) - end + belongs_to :operator_profile, class_name: 'InvoicingProfile' + belongs_to :customer_profile, class_name: 'InvoicingProfile' + + belongs_to :reservable, polymorphic: true + + belongs_to :plan def type 'machine' end def valid?(all_items) - @slots.each do |slot| + cart_item_reservation_slots.each do |slot| same_hour_slots = SlotsReservation.joins(:reservation).where( - reservations: { reservable: @reservable }, + reservations: { reservable: reservable }, slot_id: slot[:slot_id], canceled_at: nil ).count if same_hour_slots.positive? - @errors[:slot] = I18n.t('cart_item_validation.reserved') + errors.add(:slot, I18n.t('cart_item_validation.reserved')) return false end - if @reservable.disabled - @errors[:reservable] = I18n.t('cart_item_validation.machine') + if reservable.disabled + errors.add(:reservable, I18n.t('cart_item_validation.machine')) return false end - unless @reservable.reservable - @errors[:reservable] = I18n.t('cart_item_validation.reservable') + unless reservable.reservable + errors.add(:reservable, I18n.t('cart_item_validation.reservable')) return false end end @@ -52,9 +46,9 @@ class CartItem::MachineReservation < CartItem::Reservation protected def credits - return 0 if @plan.nil? + return 0 if plan.nil? - machine_credit = @plan.machine_credits.find { |credit| credit.creditable_id == @reservable.id } - credits_hours(machine_credit, new_plan_being_bought: @new_subscription) + machine_credit = plan.machine_credits.find { |credit| credit.creditable_id == reservable.id } + credits_hours(machine_credit, new_plan_being_bought: new_subscription) end end diff --git a/app/models/cart_item/payment_schedule.rb b/app/models/cart_item/payment_schedule.rb index f26984f14..eb59849d5 100644 --- a/app/models/cart_item/payment_schedule.rb +++ b/app/models/cart_item/payment_schedule.rb @@ -1,23 +1,18 @@ # frozen_string_literal: true # A payment schedule applied to plan in the shopping cart -class CartItem::PaymentSchedule - attr_reader :requested, :errors +class CartItem::PaymentSchedule < ApplicationRecord + belongs_to :customer_profile, class_name: 'InvoicingProfile' + belongs_to :coupon + belongs_to :plan - def initialize(plan, coupon, requested, customer, start_at = nil) - raise TypeError unless coupon.is_a? CartItem::Coupon - - @plan = plan - @coupon = coupon - @requested = requested - @customer = customer - @start_at = start_at - @errors = {} + def customer + customer_profile.user end def schedule(total, total_without_coupon) - schedule = if @requested && @plan&.monthly_payment - PaymentScheduleService.new.compute(@plan, total_without_coupon, @customer, coupon: @coupon.coupon, start_at: @start_at) + schedule = if requested && plan&.monthly_payment + PaymentScheduleService.new.compute(plan, total_without_coupon, customer, coupon: coupon.coupon, start_at: start_at) else nil end @@ -36,10 +31,10 @@ class CartItem::PaymentSchedule end def valid?(_all_items) - return true unless @requested && @plan&.monthly_payment + return true unless requested && plan&.monthly_payment - if @plan&.disabled - @errors[:item] = I18n.t('cart_item_validation.plan') + if plan&.disabled + errors.add(:plan, I18n.t('cart_item_validation.plan')) return false end true diff --git a/app/models/cart_item/prepaid_pack.rb b/app/models/cart_item/prepaid_pack.rb index aa2e89b98..8e1053435 100644 --- a/app/models/cart_item/prepaid_pack.rb +++ b/app/models/cart_item/prepaid_pack.rb @@ -2,18 +2,14 @@ # A prepaid-pack added to the shopping cart class CartItem::PrepaidPack < CartItem::BaseItem - def initialize(pack, customer) - raise TypeError unless pack.is_a? PrepaidPack + belongs_to :prepaid_pack - @pack = pack - @customer = customer - super + def customer + customer_profile.user end def pack - raise InvalidGroupError if @pack.group_id != @customer.group_id - - @pack + prepaid_pack end def price @@ -24,13 +20,13 @@ class CartItem::PrepaidPack < CartItem::BaseItem end def name - "#{@pack.minutes / 60} h" + "#{pack.minutes / 60} h" end def to_object ::StatisticProfilePrepaidPack.new( - prepaid_pack_id: @pack.id, - statistic_profile_id: StatisticProfile.find_by(user: @customer).id + prepaid_pack_id: pack.id, + statistic_profile_id: StatisticProfile.find_by(user: customer).id ) end @@ -39,10 +35,14 @@ class CartItem::PrepaidPack < CartItem::BaseItem end def valid?(_all_items) - if @pack.disabled + if pack.disabled @errors[:item] = I18n.t('cart_item_validation.pack') return false end + if pack.group_id != customer.group_id + @errors[:group] = "pack is reserved for members of group #{pack.group.name}" + return false + end true end end diff --git a/app/models/cart_item/reservation.rb b/app/models/cart_item/reservation.rb index 364bcf59a..7e792a1ee 100644 --- a/app/models/cart_item/reservation.rb +++ b/app/models/cart_item/reservation.rb @@ -7,17 +7,27 @@ GET_SLOT_PRICE_DEFAULT_OPTS = { has_credits: false, elements: nil, is_division: # A generic reservation added to the shopping cart class CartItem::Reservation < CartItem::BaseItem - def initialize(customer, operator, reservable, slots) - @customer = customer - @operator = operator - @reservable = reservable - @slots = slots.map { |s| expand_slot(s) } - super + self.abstract_class = true + + def reservable + nil + end + + def plan + nil + end + + def operator + operator_profile.user + end + + def customer + customer_profile.user end def price - is_privileged = @operator.privileged? && @operator.id != @customer.id - prepaid = { minutes: PrepaidPackService.minutes_available(@customer, @reservable) } + is_privileged = operator.privileged? && operator.id != customer.id + prepaid = { minutes: PrepaidPackService.minutes_available(customer, reservable) } raise InvalidGroupError, I18n.t('cart_items.group_subscription_mismatch') if !@plan.nil? && @customer.group_id != @plan.group_id @@ -39,7 +49,7 @@ class CartItem::Reservation < CartItem::BaseItem end def name - @reservable.name + reservable&.name end def valid?(all_items) @@ -48,39 +58,48 @@ class CartItem::Reservation < CartItem::BaseItem reservation_deadline_minutes = Setting.get('reservation_deadline').to_i reservation_deadline = reservation_deadline_minutes.minutes.since - @slots.each do |slot| - slot_db = Slot.find(slot[:slot_id]) - if slot_db.nil? - @errors[:slot] = I18n.t('cart_item_validation.slot') + cart_item_reservation_slots.each do |sr| + slot = sr.slot + if slot.nil? + errors.add(:slot, I18n.t('cart_item_validation.slot')) return false end - availability = Availability.find_by(id: slot[:slot_attributes][:availability_id]) + availability = slot.availability if availability.nil? - @errors[:availability] = I18n.t('cart_item_validation.availability') + errors.add(:availability, I18n.t('cart_item_validation.availability')) return false end - if slot_db.full? - @errors[:slot] = I18n.t('cart_item_validation.full') + if slot.full? + errors.add(:slot, I18n.t('cart_item_validation.full') return false end - if slot_db.start_at < reservation_deadline && !@operator.privileged? - @errors[:slot] = I18n.t('cart_item_validation.deadline', { MINUTES: reservation_deadline_minutes }) + if slot.start_at < reservation_deadline && !operator.privileged? + errors.add(:slot, I18n.t('cart_item_validation.deadline', { MINUTES: reservation_deadline_minutes })) return false end next if availability.plan_ids.empty? next if required_subscription?(availability, pending_subscription) - @errors[:availability] = I18n.t('cart_item_validation.restricted') + errors.add(:availability, I18n.t('cart_item_validation.restricted')) return false end true end + def to_object + ::Reservation.new( + reservable_id: reservable_id, + reservable_type: reservable_type, + slots_reservations_attributes: slots_params, + statistic_profile_id: StatisticProfile.find_by(user: customer).id + ) + end + protected def credits @@ -91,13 +110,9 @@ class CartItem::Reservation < CartItem::BaseItem # Group the slots by date, if the extended_prices_in_same_day option is set to true ## def grouped_slots - return { all: @slots } unless Setting.get('extended_prices_in_same_day') + return { all: cart_item_reservation_slots } unless Setting.get('extended_prices_in_same_day') - @slots.group_by { |slot| slot[:slot_attributes][:start_at].to_date } - end - - def expand_slot(slot) - slot.merge({ slot_attributes: Slot.find(slot[:slot_id]) }) + cart_item_reservation_slots.group_by { |slot| slot.slot[:start_at].to_date } end ## @@ -105,16 +120,16 @@ class CartItem::Reservation < CartItem::BaseItem # @param prices {{ prices: Array<{price: Price, duration: number}> }} list of prices to use with the current reservation # @see get_slot_price ## - def get_slot_price_from_prices(prices, slot, is_privileged, options = {}) + def get_slot_price_from_prices(prices, slot_reservation, is_privileged, options = {}) options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options) - slot_minutes = (slot[:slot_attributes][:end_at].to_time - slot[:slot_attributes][:start_at].to_time) / SECONDS_PER_MINUTE + slot_minutes = (slot_reservation.slot[:end_at].to_time - slot_reservation.slot[:start_at].to_time) / SECONDS_PER_MINUTE price = prices[:prices].find { |p| p[:duration] <= slot_minutes && p[:duration].positive? } price = prices[:prices].first if price.nil? hourly_rate = ((Rational(price[:price].amount.to_f) / Rational(price[:price].duration)) * Rational(MINUTES_PER_HOUR)).to_f # apply the base price to the real slot duration - real_price = get_slot_price(hourly_rate, slot, is_privileged, options) + real_price = get_slot_price(hourly_rate, slot_reservation, is_privileged, options) price[:duration] -= slot_minutes @@ -125,7 +140,7 @@ class CartItem::Reservation < CartItem::BaseItem # Compute the price of a single slot, according to the base price and the ability for an admin # to offer the slot. # @param hourly_rate {Number} base price of a slot - # @param slot {Hash} Slot object + # @param slot_reservation {CartItem::ReservationSlot} # @param is_privileged {Boolean} true if the current user has a privileged role (admin or manager) # @param [options] {Hash} optional parameters, allowing the following options: # - elements {Array} if provided the resulting price will be append into elements.slots @@ -134,11 +149,11 @@ class CartItem::Reservation < CartItem::BaseItem # - prepaid_minutes {Number} number of remaining prepaid minutes for the customer # @return {Number} price of the slot ## - def get_slot_price(hourly_rate, slot, is_privileged, options = {}) + def get_slot_price(hourly_rate, slot_reservation, is_privileged, options = {}) options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options) - slot_rate = options[:has_credits] || (slot[:offered] && is_privileged) ? 0 : hourly_rate - slot_minutes = (slot[:slot_attributes][:end_at].to_time - slot[:slot_attributes][:start_at].to_time) / SECONDS_PER_MINUTE + slot_rate = options[:has_credits] || (slot_reservation[:offered] && is_privileged) ? 0 : hourly_rate + slot_minutes = (slot_reservation.slot[:end_at].to_time - slot_reservation.slot[:start_at].to_time) / SECONDS_PER_MINUTE # apply the base price to the real slot duration real_price = if options[:is_division] ((Rational(slot_rate) / Rational(MINUTES_PER_HOUR)) * Rational(slot_minutes)).to_f @@ -155,7 +170,7 @@ class CartItem::Reservation < CartItem::BaseItem unless options[:elements].nil? options[:elements][:slots].push( - start_at: slot[:slot_attributes][:start_at], + start_at: slot_reservation.slot[:start_at], price: real_price, promo: (slot_rate != hourly_rate) ) @@ -168,19 +183,19 @@ class CartItem::Reservation < CartItem::BaseItem # Eg. If the reservation is for 12 hours, and there are prices for 3 hours, 7 hours, # and the base price (1 hours), we use the 7 hours price, then 3 hours price, and finally the base price twice (7+3+1+1 = 12). # All these prices are returned to be applied to the reservation. - def applicable_prices(slots) - total_duration = slots.map do |slot| - (slot[:slot_attributes][:end_at].to_time - slot[:slot_attributes][:start_at].to_time) / SECONDS_PER_MINUTE + def applicable_prices(slots_reservations) + total_duration = slots_reservations.map do |slot| + (slot.slot[:end_at].to_time - slot.slot[:start_at].to_time) / SECONDS_PER_MINUTE end.reduce(:+) rates = { prices: [] } remaining_duration = total_duration while remaining_duration.positive? - max_duration = @reservable.prices.where(group_id: @customer.group_id, plan_id: @plan.try(:id)) - .where(Price.arel_table[:duration].lteq(remaining_duration)) - .maximum(:duration) + max_duration = reservable&.prices&.where(group_id: customer.group_id, plan_id: plan.try(:id)) + &.where(Price.arel_table[:duration].lteq(remaining_duration)) + &.maximum(:duration) max_duration = 60 if max_duration.nil? - max_duration_price = @reservable.prices.find_by(group_id: @customer.group_id, plan_id: @plan.try(:id), duration: max_duration) + max_duration_price = reservable&.prices&.find_by(group_id: customer.group_id, plan_id: plan.try(:id), duration: max_duration) current_duration = [remaining_duration, max_duration].min rates[:prices].push(price: max_duration_price, duration: current_duration) @@ -200,14 +215,14 @@ class CartItem::Reservation < CartItem::BaseItem hours_available = credits.hours unless new_plan_being_bought - user_credit = @customer.users_credits.find_by(credit_id: credits.id) + user_credit = customer.users_credits.find_by(credit_id: credits.id) hours_available = credits.hours - user_credit.hours_used if user_credit end hours_available end def slots_params - @slots.map { |slot| slot.permit(:id, :slot_id, :offered) } + cart_item_reservation_slots.map { |sr| { id: sr.slots_reservation_id, slot_id: sr.slot_id, offered: sr.offered } } end ## @@ -215,9 +230,9 @@ class CartItem::Reservation < CartItem::BaseItem # has the required susbcription, otherwise, check if the operator is privileged ## def required_subscription?(availability, pending_subscription) - (@customer.subscribed_plan && availability.plan_ids.include?(@customer.subscribed_plan.id)) || + (customer.subscribed_plan && availability.plan_ids.include?(customer.subscribed_plan.id)) || (pending_subscription && availability.plan_ids.include?(pending_subscription.plan.id)) || - (@operator.manager? && @customer.id != @operator.id) || - @operator.admin? + (operator.manager? && customer.id != operator.id) || + operator.admin? end end diff --git a/app/models/cart_item/reservation_slot.rb b/app/models/cart_item/reservation_slot.rb new file mode 100644 index 000000000..9f2db25a4 --- /dev/null +++ b/app/models/cart_item/reservation_slot.rb @@ -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 diff --git a/app/models/cart_item/space_reservation.rb b/app/models/cart_item/space_reservation.rb index 7a70dc08b..145c84289 100644 --- a/app/models/cart_item/space_reservation.rb +++ b/app/models/cart_item/space_reservation.rb @@ -2,33 +2,26 @@ # A space reservation added to the shopping cart class CartItem::SpaceReservation < CartItem::Reservation - # @param plan {Plan} a subscription bought at the same time of the reservation OR an already running subscription - # @param new_subscription {Boolean} true is new subscription is being bought at the same time of the reservation - def initialize(customer, operator, space, slots, plan: nil, new_subscription: false) - raise TypeError unless space.is_a? Space + self.table_name = 'cart_item_reservations' - super(customer, operator, space, slots) - @plan = plan - @space = space - @new_subscription = new_subscription - end + has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy, inverse_of: :cart_item, + foreign_key: 'cart_item_id', foreign_type: 'cart_item_type' + accepts_nested_attributes_for :cart_item_reservation_slots - def to_object - ::Reservation.new( - reservable_id: @reservable.id, - reservable_type: Space.name, - slots_reservations_attributes: slots_params, - statistic_profile_id: StatisticProfile.find_by(user: @customer).id - ) - end + belongs_to :operator_profile, class_name: 'InvoicingProfile' + belongs_to :customer_profile, class_name: 'InvoicingProfile' + + belongs_to :reservable, polymorphic: true + + belongs_to :plan def type 'space' end def valid?(all_items) - if @space.disabled - @errors[:reservable] = I18n.t('cart_item_validation.space') + if reservable.disabled + errors.add(:reservable, I18n.t('cart_item_validation.space')) return false end @@ -38,9 +31,9 @@ class CartItem::SpaceReservation < CartItem::Reservation protected def credits - return 0 if @plan.nil? + return 0 if plan.nil? - space_credit = @plan.space_credits.find { |credit| credit.creditable_id == @reservable.id } - credits_hours(space_credit, new_plan_being_bought: @new_subscription) + space_credit = plan.space_credits.find { |credit| credit.creditable_id == reservable.id } + credits_hours(space_credit, new_plan_being_bought: new_subscription) end end diff --git a/app/models/cart_item/subscription.rb b/app/models/cart_item/subscription.rb index f4cd52cb1..e816452b7 100644 --- a/app/models/cart_item/subscription.rb +++ b/app/models/cart_item/subscription.rb @@ -2,21 +2,11 @@ # A subscription added to the shopping cart class CartItem::Subscription < CartItem::BaseItem - attr_reader :start_at + belongs_to :plan + belongs_to :customer_profile, class_name: 'InvoicingProfile' - def initialize(plan, customer, start_at = nil) - raise TypeError unless plan.is_a? Plan - - @plan = plan - @customer = customer - @start_at = start_at - super - end - - def plan - raise InvalidGroupError if @plan.group_id != @customer.group_id - - @plan + def customer + customer_profile.user end def price @@ -27,14 +17,14 @@ class CartItem::Subscription < CartItem::BaseItem end def name - @plan.base_name + plan.base_name end def to_object ::Subscription.new( - plan_id: @plan.id, - statistic_profile_id: StatisticProfile.find_by(user: @customer).id, - start_at: @start_at + plan_id: plan.id, + statistic_profile_id: StatisticProfile.find_by(user: customer).id, + start_at: start_at ) end @@ -43,8 +33,12 @@ class CartItem::Subscription < CartItem::BaseItem end def valid?(_all_items) - if @plan.disabled - @errors[:item] = I18n.t('cart_item_validation.plan') + if plan.disabled + errors.add(:plan, I18n.t('cart_item_validation.plan')) + return false + end + if plan.group_id != customer.group_id + errors.add(:group, "plan is reserved for members of group #{plan.group.name}") return false end true diff --git a/app/models/cart_item/training_reservation.rb b/app/models/cart_item/training_reservation.rb index bfdc3b528..3edfe6bdf 100644 --- a/app/models/cart_item/training_reservation.rb +++ b/app/models/cart_item/training_reservation.rb @@ -2,45 +2,39 @@ # A training reservation added to the shopping cart class CartItem::TrainingReservation < CartItem::Reservation - # @param plan {Plan} a subscription bought at the same time of the reservation OR an already running subscription - # @param new_subscription {Boolean} true is new subscription is being bought at the same time of the reservation - def initialize(customer, operator, training, slots, plan: nil, new_subscription: false) - raise TypeError unless training.is_a? Training + self.table_name = 'cart_item_reservations' - super(customer, operator, training, slots) - @plan = plan - @new_subscription = new_subscription - end + has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy, inverse_of: :cart_item, + foreign_key: 'cart_item_id', foreign_type: 'cart_item_type' + accepts_nested_attributes_for :cart_item_reservation_slots + + belongs_to :operator_profile, class_name: 'InvoicingProfile' + belongs_to :customer_profile, class_name: 'InvoicingProfile' + + belongs_to :reservable, polymorphic: true + + belongs_to :plan def price - base_amount = @reservable.amount_by_group(@customer.group_id).amount - is_privileged = @operator.admin? || (@operator.manager? && @operator.id != @customer.id) + base_amount = reservable&.amount_by_group(customer.group_id)&.amount + is_privileged = operator.admin? || (operator.manager? && operator.id != customer.id) elements = { slots: [] } amount = 0 hours_available = credits - @slots.each do |slot| + cart_item_reservation_slots.each do |sr| amount += get_slot_price(base_amount, - slot, + sr, is_privileged, elements: elements, - has_credits: (@customer.training_credits.size < hours_available), + has_credits: (customer.training_credits.size < hours_available), is_division: false) end { elements: elements, amount: amount } end - def to_object - ::Reservation.new( - reservable_id: @reservable.id, - reservable_type: Training.name, - slots_reservations_attributes: slots_params, - statistic_profile_id: StatisticProfile.find_by(user: @customer).id - ) - end - def type 'training' end @@ -48,9 +42,9 @@ class CartItem::TrainingReservation < CartItem::Reservation protected def credits - return 0 if @plan.nil? + return 0 if plan.nil? - is_creditable = @plan.training_credits.select { |credit| credit.creditable_id == @reservable.id }.any? - is_creditable ? @plan.training_credit_nb : 0 + is_creditable = plan&.training_credits&.select { |credit| credit.creditable_id == reservable&.id }&.any? + is_creditable ? plan&.training_credit_nb : 0 end end diff --git a/app/models/coupon.rb b/app/models/coupon.rb index 20051d990..9195d1a9c 100644 --- a/app/models/coupon.rb +++ b/app/models/coupon.rb @@ -2,16 +2,18 @@ # Coupon is a textual code associated with a discount rate or an amount of discount class Coupon < ApplicationRecord - has_many :invoices, dependent: :nullify - has_many :payment_schedule, dependent: :nullify - has_many :orders, dependent: :nullify + has_many :invoices, dependent: :restrict_with_error + has_many :payment_schedule, dependent: :restrict_with_error + has_many :orders, dependent: :restrict_with_error + + has_many :cart_item_coupons, class_name: 'CartItem::Coupon', dependent: :destroy after_create :create_gateway_coupon before_destroy :delete_gateway_coupon validates :name, presence: true validates :code, presence: true - validates :code, format: { with: /\A[A-Z0-9\-]+\z/, message: I18n.t('coupon.invalid_format') } + validates :code, format: { with: /\A[A-Z0-9\-]+\z/, message: I18n.t('coupon.code_format_error') } validates :code, uniqueness: true validates :validity_per_user, presence: true validates :validity_per_user, inclusion: { in: %w[once forever] } diff --git a/app/models/event.rb b/app/models/event.rb index cfaa0fdce..330dc8bac 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -31,6 +31,8 @@ class Event < ApplicationRecord has_one :advanced_accounting, as: :accountable, dependent: :destroy accepts_nested_attributes_for :advanced_accounting, allow_destroy: true + has_many :cart_item_event_reservations, class_name: 'CartItem::EventReservation', dependent: :destroy + attr_accessor :recurrence, :recurrence_end_at before_save :update_nb_free_places diff --git a/app/models/event_price_category.rb b/app/models/event_price_category.rb index 47b6e8558..8fe8e8858 100644 --- a/app/models/event_price_category.rb +++ b/app/models/event_price_category.rb @@ -5,7 +5,8 @@ class EventPriceCategory < ApplicationRecord belongs_to :event belongs_to :price_category - has_many :tickets + has_many :tickets, dependent: :restrict_with_error + has_many :cart_item_event_reservation_tickets, class_name: 'CartItem::EventReservationTicket', dependent: :restrict_with_error validates :price_category_id, presence: true validates :amount, presence: true @@ -17,5 +18,4 @@ class EventPriceCategory < ApplicationRecord def verify_no_associated_tickets throw(:abort) unless tickets.count.zero? end - end diff --git a/app/models/invoicing_profile.rb b/app/models/invoicing_profile.rb index 6e9cd012f..78678ca8b 100644 --- a/app/models/invoicing_profile.rb +++ b/app/models/invoicing_profile.rb @@ -27,6 +27,25 @@ class InvoicingProfile < ApplicationRecord has_many :accounting_lines, dependent: :destroy + # as operator + has_many :operated_cart_item_event_reservations, class_name: 'CartItem::EventReservation', dependent: :nullify, inverse_of: :operator_profile + has_many :operated_cart_item_machine_reservations, class_name: 'CartItem::MachineReservation', dependent: :nullify, + inverse_of: :operator_profile + has_many :operated_cart_item_space_reservations, class_name: 'CartItem::SpaceReservation', dependent: :nullify, inverse_of: :operator_profile + has_many :operated_cart_item_training_reservations, class_name: 'CartItem::TrainingReservation', dependent: :nullify, + inverse_of: :operator_profile + has_many :operated_cart_item_coupon, class_name: 'CartItem::Coupon', dependent: :nullify, inverse_of: :operator_profile + # as customer + has_many :cart_item_event_reservations, class_name: 'CartItem::EventReservation', dependent: :destroy, inverse_of: :customer_profile + has_many :cart_item_machine_reservations, class_name: 'CartItem::MachineReservation', dependent: :destroy, inverse_of: :customer_profile + has_many :cart_item_space_reservations, class_name: 'CartItem::SpaceReservation', dependent: :destroy, inverse_of: :customer_profile + has_many :cart_item_training_reservations, class_name: 'CartItem::TrainingReservation', dependent: :destroy, inverse_of: :customer_profile + has_many :cart_item_free_extensions, class_name: 'CartItem::FreeExtension', dependent: :destroy, inverse_of: :customer_profile + has_many :cart_item_subscriptions, class_name: 'CartItem::Subscription', dependent: :destroy, inverse_of: :customer_profile + has_many :cart_item_prepaid_packs, class_name: 'CartItem::PrepaidPack', dependent: :destroy, inverse_of: :customer_profile + has_many :cart_item_coupons, class_name: 'CartItem::Coupon', dependent: :destroy, inverse_of: :customer_profile + has_many :cart_item_payment_schedules, class_name: 'CartItem::PaymentSchedule', dependent: :destroy, inverse_of: :customer_profile + before_validation :set_external_id_nil validates :external_id, uniqueness: true, allow_blank: true validates :address, presence: true, if: -> { Setting.get('address_required') } diff --git a/app/models/machine.rb b/app/models/machine.rb index 57a5c929e..72d100e10 100644 --- a/app/models/machine.rb +++ b/app/models/machine.rb @@ -37,6 +37,9 @@ class Machine < ApplicationRecord has_one :advanced_accounting, as: :accountable, dependent: :destroy accepts_nested_attributes_for :advanced_accounting, allow_destroy: true + has_many :cart_item_machine_reservations, class_name: 'CartItem::MachineReservation', dependent: :destroy, inverse_of: :reservable, + foreign_type: 'reservable_type', foreign_key: 'reservable_id' + belongs_to :category after_create :create_statistic_subtype diff --git a/app/models/order.rb b/app/models/order.rb index 311aa096c..037e69a70 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Order is a model for the user hold information of order +# Order is a model used to hold orders data class Order < PaymentDocument belongs_to :statistic_profile belongs_to :operator_profile, class_name: 'InvoicingProfile' diff --git a/app/models/plan.rb b/app/models/plan.rb index 6c1e393a0..8370551e9 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -15,6 +15,12 @@ class Plan < ApplicationRecord has_many :prices, dependent: :destroy has_one :payment_gateway_object, -> { order id: :desc }, inverse_of: :plan, as: :item, dependent: :destroy + has_many :cart_item_machine_reservations, class_name: 'CartItem::MachineReservation', dependent: :destroy + has_many :cart_item_space_reservations, class_name: 'CartItem::SpaceReservation', dependent: :destroy + has_many :cart_item_training_reservations, class_name: 'CartItem::TrainingReservation', dependent: :destroy + has_many :cart_item_subscriptions, class_name: 'CartItem::Subscription', dependent: :destroy + has_many :cart_item_payment_schedules, class_name: 'CartItem::PaymentSchedule', dependent: :destroy + extend FriendlyId friendly_id :base_name, use: :slugged diff --git a/app/models/prepaid_pack.rb b/app/models/prepaid_pack.rb index 76f08aaa8..16d752b36 100644 --- a/app/models/prepaid_pack.rb +++ b/app/models/prepaid_pack.rb @@ -8,12 +8,14 @@ # The number of hours in a pack is stored in minutes. class PrepaidPack < ApplicationRecord belongs_to :priceable, polymorphic: true - belongs_to :machine, foreign_type: 'Machine', foreign_key: 'priceable_id' - belongs_to :space, foreign_type: 'Space', foreign_key: 'priceable_id' + belongs_to :machine, foreign_type: 'Machine', foreign_key: 'priceable_id', inverse_of: :prepaid_packs + belongs_to :space, foreign_type: 'Space', foreign_key: 'priceable_id', inverse_of: :prepaid_packs belongs_to :group - has_many :statistic_profile_prepaid_packs + has_many :statistic_profile_prepaid_packs, dependent: :destroy + + has_many :cart_item_prepaid_packs, class_name: 'CartItem::PrepaidPack', dependent: :destroy validates :amount, :group_id, :priceable_id, :priceable_type, :minutes, presence: true diff --git a/app/models/project.rb b/app/models/project.rb index c0d1b418b..6e0bb3497 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -59,7 +59,7 @@ class Project < ApplicationRecord scope :published_or_drafts, lambda { |author_profile| where("state = 'published' OR (state = 'draft' AND author_statistic_profile_id = ?)", author_profile) } - scope :user_projects, ->(author_profile) { where(author_statistic_profile: author_profile) } + scope :user_projects, ->(author_profile) { where(author_statistic_profile_id: author_profile) } scope :collaborations, ->(collaborators_ids) { joins(:project_users).where(project_users: { user_id: collaborators_ids }) } scope :with_machine, ->(machines_ids) { joins(:projects_machines).where(projects_machines: { machine_id: machines_ids }) } scope :with_theme, ->(themes_ids) { joins(:projects_themes).where(projects_themes: { theme_id: themes_ids }) } diff --git a/app/models/shopping_cart.rb b/app/models/shopping_cart.rb index 032477afa..ba3f6378d 100644 --- a/app/models/shopping_cart.rb +++ b/app/models/shopping_cart.rb @@ -70,7 +70,7 @@ class ShoppingCart payment.post_save(payment_id, payment_type) end - success = !payment.nil? && objects.map(&:errors).flatten.map(&:empty?).all? && items.map(&:errors).map(&:empty?).all? + success = !payment.nil? && objects.map(&:errors).flatten.map(&:empty?).all? && items.map(&:errors).map(&:blank?).all? errors = objects.map(&:errors).flatten.concat(items.map(&:errors)) errors.push('Unable to create the PaymentDocument') if payment.nil? { success: success, payment: payment, errors: errors } diff --git a/app/models/slot.rb b/app/models/slot.rb index f6fc13178..354fb0963 100644 --- a/app/models/slot.rb +++ b/app/models/slot.rb @@ -10,6 +10,8 @@ class Slot < ApplicationRecord has_many :reservations, through: :slots_reservations belongs_to :availability + has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy + attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :current_user_slots_reservations_ids def full?(reservable = nil) diff --git a/app/models/slots_reservation.rb b/app/models/slots_reservation.rb index 3594dd14d..ba31cff2e 100644 --- a/app/models/slots_reservation.rb +++ b/app/models/slots_reservation.rb @@ -5,6 +5,7 @@ class SlotsReservation < ApplicationRecord belongs_to :slot belongs_to :reservation + has_one :cart_item_reservation_slot, class_name: 'CartItem::ReservationSlot', dependent: :nullify after_update :set_ex_start_end_dates_attrs, if: :slot_changed? after_update :notify_member_and_admin_slot_is_modified, if: :slot_changed? @@ -13,7 +14,7 @@ class SlotsReservation < ApplicationRecord after_update :update_event_nb_free_places, if: :canceled? def set_ex_start_end_dates_attrs - update_columns(ex_start_at: previous_slot.start_at, ex_end_at: previous_slot.end_at) + update_columns(ex_start_at: previous_slot.start_at, ex_end_at: previous_slot.end_at) # rubocop:disable Rails/SkipsModelValidations end private diff --git a/app/models/space.rb b/app/models/space.rb index 3921e58c1..399b99d30 100644 --- a/app/models/space.rb +++ b/app/models/space.rb @@ -31,6 +31,9 @@ class Space < ApplicationRecord has_one :advanced_accounting, as: :accountable, dependent: :destroy accepts_nested_attributes_for :advanced_accounting, allow_destroy: true + has_many :cart_item_space_reservations, class_name: 'CartItem::SpaceReservation', dependent: :destroy, inverse_of: :reservable, + foreign_type: 'reservable_type', foreign_key: 'reservable_id' + after_create :create_statistic_subtype after_create :create_space_prices after_create :update_gateway_product diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 227110028..3e9ffa370 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -12,6 +12,8 @@ class Subscription < ApplicationRecord has_many :invoice_items, as: :object, dependent: :destroy has_many :offer_days, dependent: :destroy + has_many :cart_item_free_extensions, class_name: 'CartItem::FreeExtension', dependent: :destroy + validates :plan_id, presence: true validates_with SubscriptionGroupValidator diff --git a/app/models/training.rb b/app/models/training.rb index 8e853e006..f81999872 100644 --- a/app/models/training.rb +++ b/app/models/training.rb @@ -32,6 +32,9 @@ class Training < ApplicationRecord has_one :advanced_accounting, as: :accountable, dependent: :destroy accepts_nested_attributes_for :advanced_accounting, allow_destroy: true + has_many :cart_item_training_reservations, class_name: 'CartItem::TrainingReservation', dependent: :destroy, inverse_of: :reservable, + foreign_type: 'reservable_type', foreign_key: 'reservable_id' + after_create :create_statistic_subtype after_create :create_trainings_pricings after_create :update_gateway_product diff --git a/app/services/cart/add_item_service.rb b/app/services/cart/add_item_service.rb index c0e8bb9fd..6f868d0fd 100644 --- a/app/services/cart/add_item_service.rb +++ b/app/services/cart/add_item_service.rb @@ -5,10 +5,30 @@ class Cart::AddItemService def call(order, orderable, quantity = 1) return order if quantity.to_i.zero? - raise Cart::InactiveProductError unless orderable.is_active + item = case orderable + when Product + add_product(order, orderable, quantity) + when Slot + add_slot(order, orderable) + else + raise Cart::UnknownItemError + end order.created_at = Time.current if order.order_items.length.zero? + ActiveRecord::Base.transaction do + item.save + Cart::UpdateTotalService.new.call(order) + order.save + end + order.reload + end + + private + + def add_product(order, orderable, quantity) + raise Cart::InactiveProductError unless orderable.is_active + item = order.order_items.find_by(orderable: orderable) quantity = orderable.quantity_min > quantity.to_i && item.nil? ? orderable.quantity_min : quantity.to_i @@ -19,11 +39,14 @@ class Cart::AddItemService end raise Cart::OutStockError if item.quantity > orderable.stock['external'] - ActiveRecord::Base.transaction do - item.save - Cart::UpdateTotalService.new.call(order) - order.save - end - order.reload + item + end + + def add_slot(order, orderable) + item = order.order_items.find_by(orderable: orderable) + + item = order.order_items.new(quantity: 1, orderable: orderable, amount: orderable.amount || 0) if item.nil? + + item end end diff --git a/app/services/cart_service.rb b/app/services/cart_service.rb index 5b15457f6..aea1c834d 100644 --- a/app/services/cart_service.rb +++ b/app/services/cart_service.rb @@ -11,25 +11,46 @@ class CartService # @see app/frontend/src/javascript/models/payment.ts > interface ShoppingCart ## def from_hash(cart_items) + cart_items.permit! if cart_items.is_a? ActionController::Parameters + @customer = customer(cart_items) plan_info = plan(cart_items) items = [] cart_items[:items].each do |item| - if ['subscription', :subscription].include?(item.keys.first) - items.push(CartItem::Subscription.new(plan_info[:plan], @customer, item[:subscription][:start_at])) if plan_info[:new_subscription] + if ['subscription', :subscription].include?(item.keys.first) && plan_info[:new_subscription] + items.push(CartItem::Subscription.new( + plan: plan_info[:plan], + customer_profile: @customer.invoicing_profile, + start_at: item[:subscription][:start_at] + )) elsif ['reservation', :reservation].include?(item.keys.first) items.push(reservable_from_hash(item[:reservation], plan_info)) elsif ['prepaid_pack', :prepaid_pack].include?(item.keys.first) - items.push(CartItem::PrepaidPack.new(PrepaidPack.find(item[:prepaid_pack][:id]), @customer)) + items.push(CartItem::PrepaidPack.new( + prepaid_pack: PrepaidPack.find(item[:prepaid_pack][:id]), + customer_profile: @customer.invoicing_profile + )) elsif ['free_extension', :free_extension].include?(item.keys.first) - items.push(CartItem::FreeExtension.new(@customer, plan_info[:subscription], item[:free_extension][:end_at])) + items.push(CartItem::FreeExtension.new( + customer_profile: @customer.invoicing_profile, + subscription: plan_info[:subscription], + new_expiration_date: item[:free_extension][:end_at] + )) end end - coupon = CartItem::Coupon.new(@customer, @operator, cart_items[:coupon_code]) + coupon = CartItem::Coupon.new( + customer_profile: @customer.invoicing_profile, + operator_profile: @operator.invoicing_profile, + coupon: Coupon.find_by(code: cart_items[:coupon_code]) + ) schedule = CartItem::PaymentSchedule.new( - plan_info[:plan], coupon, cart_items[:payment_schedule], @customer, plan_info[:subscription]&.start_at + plan: plan_info[:plan], + coupon: coupon, + requested: cart_items[:payment_schedule], + customer_profile: @customer.invoicing_profile, + start_at: plan_info[:subscription]&.start_at ) ShoppingCart.new( @@ -47,19 +68,40 @@ class CartService subscription = payment_schedule.payment_schedule_objects.find { |pso| pso.object_type == Subscription.name }&.subscription plan = subscription&.plan - coupon = CartItem::Coupon.new(@customer, @operator, payment_schedule.coupon&.code) - schedule = CartItem::PaymentSchedule.new(plan, coupon, true, @customer, subscription.start_at) + coupon = CartItem::Coupon.new( + customer_profile: @customer.invoicing_profile, + operator_profile: @operator.invoicing_profile, + coupon: payment_schedule.coupon + ) + schedule = CartItem::PaymentSchedule.new( + plan: plan, + coupon: coupon, + requested: true, + customer_profile: @customer.invoicing_profile, + start_at: subscription.start_at + ) items = [] payment_schedule.payment_schedule_objects.each do |object| if object.object_type == Subscription.name - items.push(CartItem::Subscription.new(object.subscription.plan, @customer, object.subscription.start_at)) + items.push(CartItem::Subscription.new( + plan: object.subscription.plan, + customer_profile: @customer.invoicing_profile, + start_at: object.subscription.start_at + )) elsif object.object_type == Reservation.name items.push(reservable_from_payment_schedule_object(object, plan)) elsif object.object_type == PrepaidPack.name - items.push(CartItem::PrepaidPack.new(object.statistic_profile_prepaid_pack.prepaid_pack_id, @customer)) + items.push(CartItem::PrepaidPack.new( + prepaid_pack_id: object.statistic_profile_prepaid_pack.prepaid_pack_id, + customer_profile: @customer.invoicing_profile + )) elsif object.object_type == OfferDay.name - items.push(CartItem::FreeExtension.new(@customer, object.offer_day.subscription, object.offer_day.end_date)) + items.push(CartItem::FreeExtension.new( + customer_profile: @customer.invoicing_profile, + subscription: object.offer_day.subscription, + new_expiration_date: object.offer_day.end_date + )) end end @@ -83,7 +125,11 @@ class CartService if cart_items[:items][index][:subscription][:plan_id] new_plan_being_bought = true plan = Plan.find(cart_items[:items][index][:subscription][:plan_id]) - subscription = CartItem::Subscription.new(plan, @customer, cart_items[:items][index][:subscription][:start_at]).to_object + subscription = CartItem::Subscription.new( + plan: plan, + customer_profile: @customer.invoicing_profile, + start_at: cart_items[:items][index][:subscription][:start_at] + ).to_object plan end elsif @customer.subscribed_plan @@ -107,31 +153,31 @@ class CartService reservable = cart_item[:reservable_type]&.constantize&.find(cart_item[:reservable_id]) case reservable when Machine - CartItem::MachineReservation.new(@customer, - @operator, - reservable, - cart_item[:slots_reservations_attributes], + CartItem::MachineReservation.new(customer_profile: @customer.invoicing_profile, + operator_profile: @operator.invoicing_profile, + reservable: reservable, + cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes], plan: plan_info[:plan], new_subscription: plan_info[:new_subscription]) when Training - CartItem::TrainingReservation.new(@customer, - @operator, - reservable, - cart_item[:slots_reservations_attributes], + CartItem::TrainingReservation.new(customer_profile: @customer.invoicing_profile, + operator_profile: @operator.invoicing_profile, + reservable: reservable, + cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes], plan: plan_info[:plan], new_subscription: plan_info[:new_subscription]) when Event - CartItem::EventReservation.new(@customer, - @operator, - reservable, - cart_item[:slots_reservations_attributes], + CartItem::EventReservation.new(customer_profile: @customer.invoicing_profile, + operator_profile: @operator.invoicing_profile, + event: reservable, + cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes], normal_tickets: cart_item[:nb_reserve_places], - other_tickets: cart_item[:tickets_attributes]) + cart_item_event_reservation_tickets_attributes: cart_item[:tickets_attributes] || {}) when Space - CartItem::SpaceReservation.new(@customer, - @operator, - reservable, - cart_item[:slots_reservations_attributes], + CartItem::SpaceReservation.new(customer_profile: @customer.invoicing_profile, + operator_profile: @operator.invoicing_profile, + reservable: reservable, + cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes], plan: plan_info[:plan], new_subscription: plan_info[:new_subscription]) else @@ -144,31 +190,31 @@ class CartService reservable = object.reservation.reservable case reservable when Machine - CartItem::MachineReservation.new(@customer, - @operator, - reservable, - object.reservation.slots_reservations, + CartItem::MachineReservation.new(customer_profile: @customer.invoicing_profile, + operator_profile: @operator.invoicing_profile, + reservable: reservable, + cart_item_reservation_slots_attributes: object.reservation.slots_reservations, plan: plan, new_subscription: true) when Training - CartItem::TrainingReservation.new(@customer, - @operator, - reservable, - object.reservation.slots_reservations, + CartItem::TrainingReservation.new(customer_profile: @customer.invoicing_profile, + operator_profile: @operator.invoicing_profile, + reservable: reservable, + cart_item_reservation_slots_attributes: object.reservation.slots_reservations, plan: plan, new_subscription: true) when Event - CartItem::EventReservation.new(@customer, - @operator, - reservable, - object.reservation.slots_reservations, + CartItem::EventReservation.new(customer_profile: @customer.invoicing_profile, + operator_profile: @operator.invoicing_profile, + event: reservable, + cart_item_reservation_slots_attributes: object.reservation.slots_reservation, normal_tickets: object.reservation.nb_reserve_places, - other_tickets: object.reservation.tickets) + cart_item_event_reservation_tickets_attributes: object.reservation.tickets) when Space - CartItem::SpaceReservation.new(@customer, - @operator, - reservable, - object.reservation.slots_reservations, + CartItem::SpaceReservation.new(customer_profile: @customer.invoicing_profile, + operator_profile: @operator.invoicing_profile, + reservable: reservable, + cart_item_reservation_slots_attributes: object.reservation.slots_reservations, plan: plan, new_subscription: true) else diff --git a/app/validators/stripe_card_token_validator.rb b/app/validators/stripe_card_token_validator.rb index 89f160e89..a813a6763 100644 --- a/app/validators/stripe_card_token_validator.rb +++ b/app/validators/stripe_card_token_validator.rb @@ -7,9 +7,9 @@ class StripeCardTokenValidator res = Stripe::Token.retrieve(options[:token], api_key: Setting.get('stripe_secret_key')) if res[:id] != options[:token] - record.errors[:card_token] << "A problem occurred while retrieving the card with the specified token: #{res.id}" + record.errors.add(:card_token, "A problem occurred while retrieving the card with the specified token: #{res.id}") end rescue Stripe::InvalidRequestError => e - record.errors[:card_token] << e + record.errors.add(:card_token, e) end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 23944812b..83c57c0bc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -67,6 +67,8 @@ en: length_must_be_slot_multiple: "must be at least %{MIN} minutes after the start date" must_be_associated_with_at_least_1_machine: "must be associated with at least 1 machine" deleted_user: "Deleted user" + coupon: + code_format_error: "only caps letters, numbers, and dashes are allowed" #members management members: unable_to_change_the_group_while_a_subscription_is_running: "Unable to change the group while a subscription is running" diff --git a/db/migrate/20221228152719_create_cart_item_event_reservation.rb b/db/migrate/20221228152719_create_cart_item_event_reservation.rb new file mode 100644 index 000000000..41f72060f --- /dev/null +++ b/db/migrate/20221228152719_create_cart_item_event_reservation.rb @@ -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 diff --git a/db/migrate/20221228152747_create_cart_item_event_reservation_ticket.rb b/db/migrate/20221228152747_create_cart_item_event_reservation_ticket.rb new file mode 100644 index 000000000..177ddb475 --- /dev/null +++ b/db/migrate/20221228152747_create_cart_item_event_reservation_ticket.rb @@ -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 diff --git a/db/migrate/20221228160449_create_cart_item_reservation_slot.rb b/db/migrate/20221228160449_create_cart_item_reservation_slot.rb new file mode 100644 index 000000000..3266e9750 --- /dev/null +++ b/db/migrate/20221228160449_create_cart_item_reservation_slot.rb @@ -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 diff --git a/db/migrate/20221229085430_create_cart_item_reservation.rb b/db/migrate/20221229085430_create_cart_item_reservation.rb new file mode 100644 index 000000000..7500eb26c --- /dev/null +++ b/db/migrate/20221229085430_create_cart_item_reservation.rb @@ -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 diff --git a/db/migrate/20221229094334_create_cart_item_free_extension.rb b/db/migrate/20221229094334_create_cart_item_free_extension.rb new file mode 100644 index 000000000..e21e18979 --- /dev/null +++ b/db/migrate/20221229094334_create_cart_item_free_extension.rb @@ -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 diff --git a/db/migrate/20221229100157_create_cart_item_subscription.rb b/db/migrate/20221229100157_create_cart_item_subscription.rb new file mode 100644 index 000000000..81b4ca343 --- /dev/null +++ b/db/migrate/20221229100157_create_cart_item_subscription.rb @@ -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 diff --git a/db/migrate/20221229103407_create_cart_item_prepaid_pack.rb b/db/migrate/20221229103407_create_cart_item_prepaid_pack.rb new file mode 100644 index 000000000..1df8f95c1 --- /dev/null +++ b/db/migrate/20221229103407_create_cart_item_prepaid_pack.rb @@ -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 diff --git a/db/migrate/20221229105954_add_unique_index_on_coupon_code.rb b/db/migrate/20221229105954_add_unique_index_on_coupon_code.rb new file mode 100644 index 000000000..8238ea244 --- /dev/null +++ b/db/migrate/20221229105954_add_unique_index_on_coupon_code.rb @@ -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 diff --git a/db/migrate/20221229115757_create_cart_item_coupon.rb b/db/migrate/20221229115757_create_cart_item_coupon.rb new file mode 100644 index 000000000..28c3f338f --- /dev/null +++ b/db/migrate/20221229115757_create_cart_item_coupon.rb @@ -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 diff --git a/db/migrate/20221229120932_create_cart_item_payment_schedule.rb b/db/migrate/20221229120932_create_cart_item_payment_schedule.rb new file mode 100644 index 000000000..b75b37756 --- /dev/null +++ b/db/migrate/20221229120932_create_cart_item_payment_schedule.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 4ba30590d..4ed4819f2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do enable_extension "unaccent" create_table "abuses", id: :serial, force: :cascade do |t| - t.integer "signaled_id" t.string "signaled_type" + t.integer "signaled_id" t.string "first_name" t.string "last_name" t.string "email" @@ -68,8 +68,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do t.string "locality" t.string "country" t.string "postal_code" - t.integer "placeable_id" t.string "placeable_type" + t.integer "placeable_id" t.datetime "created_at" t.datetime "updated_at" end @@ -93,8 +93,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do end create_table "assets", id: :serial, force: :cascade do |t| - t.integer "viewable_id" t.string "viewable_type" + t.integer "viewable_id" t.string "attachment" t.string "type" t.datetime "created_at" @@ -281,8 +281,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do end create_table "credits", id: :serial, force: :cascade do |t| - t.integer "creditable_id" t.string "creditable_type" + t.integer "creditable_id" t.integer "plan_id" t.integer "hours" t.datetime "created_at" @@ -524,15 +524,15 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do create_table "notifications", id: :serial, force: :cascade do |t| t.integer "receiver_id" - t.integer "attached_object_id" t.string "attached_object_type" + t.integer "attached_object_id" t.integer "notification_type_id" t.boolean "is_read", default: false t.datetime "created_at" t.datetime "updated_at" t.string "receiver_type" t.boolean "is_send", default: false - t.jsonb "meta_data", default: {} + t.jsonb "meta_data", default: "{}" t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id" t.index ["receiver_id"], name: "index_notifications_on_receiver_id" end @@ -772,8 +772,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do create_table "prices", id: :serial, force: :cascade do |t| t.integer "group_id" t.integer "plan_id" - t.integer "priceable_id" t.string "priceable_type" + t.integer "priceable_id" t.integer "amount" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -976,8 +976,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do t.text "message" t.datetime "created_at" t.datetime "updated_at" - t.integer "reservable_id" t.string "reservable_type" + t.integer "reservable_id" t.integer "nb_reserve_places" t.integer "statistic_profile_id" t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id" @@ -986,8 +986,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do create_table "roles", id: :serial, force: :cascade do |t| t.string "name" - t.integer "resource_id" t.string "resource_type" + t.integer "resource_id" t.datetime "created_at" t.datetime "updated_at" t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" diff --git a/test/integration/subscriptions/create_as_user_test.rb b/test/integration/subscriptions/create_as_user_test.rb index b947be596..cb449eeb3 100644 --- a/test/integration/subscriptions/create_as_user_test.rb +++ b/test/integration/subscriptions/create_as_user_test.rb @@ -2,6 +2,8 @@ require 'test_helper' +module Subscriptions; end + class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest setup do @user = User.find_by(username: 'jdupond') @@ -57,7 +59,7 @@ class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest # Check notifications were sent for every admins notifications = Notification.where( - notification_type_id: NotificationType.find_by_name('notify_admin_subscribed_plan'), + notification_type_id: NotificationType.find_by_name('notify_admin_subscribed_plan'), # rubocop:disable Rails/DynamicFindBy attached_object_type: 'Subscription', attached_object_id: subscription[:id] ) @@ -100,7 +102,7 @@ class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest assert_equal Mime[:json], response.content_type # Check the error was handled - assert_match(/plan is not compatible/, response.body) + assert_match(/plan is reserved for members of group/, response.body) # Check that the user has no subscription assert_nil @user.subscription, "user's subscription was found" @@ -162,7 +164,7 @@ class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest # Check notifications were sent for every admins notifications = Notification.where( - notification_type_id: NotificationType.find_by_name('notify_admin_subscribed_plan'), + notification_type_id: NotificationType.find_by_name('notify_admin_subscribed_plan'), # rubocop:disable Rails/DynamicFindBy attached_object_type: 'Subscription', attached_object_id: subscription[:id] ) diff --git a/test/integration/subscriptions/renew_as_user_test.rb b/test/integration/subscriptions/renew_as_user_test.rb index 40b13059f..3b505ec00 100644 --- a/test/integration/subscriptions/renew_as_user_test.rb +++ b/test/integration/subscriptions/renew_as_user_test.rb @@ -2,6 +2,8 @@ require 'test_helper' +module Subscriptions; end + class Subscriptions::RenewAsUserTest < ActionDispatch::IntegrationTest setup do @user = User.find_by(username: 'atiermoulin') @@ -60,7 +62,7 @@ class Subscriptions::RenewAsUserTest < ActionDispatch::IntegrationTest # Check notifications were sent for every admins notifications = Notification.where( - notification_type_id: NotificationType.find_by_name('notify_admin_subscribed_plan'), + notification_type_id: NotificationType.find_by_name('notify_admin_subscribed_plan'), # rubocop:disable Rails/DynamicFindBy attached_object_type: 'Subscription', attached_object_id: subscription[:id] )