diff --git a/app/controllers/api/local_payment_controller.rb b/app/controllers/api/local_payment_controller.rb index dbb4b1bec..900f35c3b 100644 --- a/app/controllers/api/local_payment_controller.rb +++ b/app/controllers/api/local_payment_controller.rb @@ -9,9 +9,9 @@ class API::LocalPaymentController < API::PaymentsController authorize LocalPaymentContext.new(cart, price[:amount]) if cart.reservation - res = on_reservation_success(nil, nil, price[:details], cart) + res = on_reservation_success(nil, nil, cart) elsif cart.subscription - res = on_subscription_success(nil, nil, price[:details], cart) + res = on_subscription_success(nil, nil, cart) end render res diff --git a/app/controllers/api/payments_controller.rb b/app/controllers/api/payments_controller.rb index cad46b5fe..ebbc5d9ed 100644 --- a/app/controllers/api/payments_controller.rb +++ b/app/controllers/api/payments_controller.rb @@ -12,6 +12,8 @@ class API::PaymentsController < API::ApiController protected + def post_save(_gateway_item_id, _gateway_item_type); end + def post_reservation_save(_gateway_item_id, _gateway_item_type); end def post_subscription_save(_gateway_item_id, _gateway_item_type); end @@ -50,49 +52,23 @@ class API::PaymentsController < API::ApiController raise InvalidGroupError if plan.group_id != current_user.group_id end - def on_reservation_success(gateway_item_id, gateway_item_type, details, cart) - @reservation = cart.reservation.to_reservation - @reservation.plan_id = cart.subscription.plan.id if cart.subscription + def on_success(gateway_item_id, gateway_item_type, cart) + cart.pay_and_save(gateway_item_id, gateway_item_type) + end - payment_method = cart.payment_method || 'card' - user_id = if current_user.admin? || current_user.manager? - cart.customer.id - else - current_user.id - end - is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id) - .pay_and_save(@reservation, - payment_details: details, - payment_id: gateway_item_id, - payment_type: gateway_item_type, - schedule: cart.payment_schedule.requested, - payment_method: payment_method) + def on_reservation_success(gateway_item_id, gateway_item_type, cart) + is_reserve = on_success(gateway_item_id, gateway_item_type, cart) post_reservation_save(gateway_item_id, gateway_item_type) if is_reserve - SubscriptionExtensionAfterReservation.new(@reservation).extend_subscription_if_eligible - { template: 'api/reservations/show', status: :created, location: @reservation } else { json: @reservation.errors, status: :unprocessable_entity } end end - def on_subscription_success(gateway_item_id, gateway_item_type, details, cart) - @subscription = cart.subscription.to_subscription - user_id = if current_user.admin? || current_user.manager? - cart.customer.id - else - current_user.id - end - is_subscribe = Subscriptions::Subscribe.new(current_user.invoicing_profile.id, user_id) - .pay_and_save(@subscription, - payment_details: details, - payment_id: gateway_item_id, - payment_type: gateway_item_type, - schedule: cart.payment_schedule.requested, - payment_method: cart.payment_method || 'card') - + def on_subscription_success(gateway_item_id, gateway_item_type, cart) + is_subscribe = on_success(gateway_item_id, gateway_item_type, cart) post_subscription_save(gateway_item_id, gateway_item_type) if is_subscribe diff --git a/app/controllers/api/payzen_controller.rb b/app/controllers/api/payzen_controller.rb index 1191a764a..04b7bce5e 100644 --- a/app/controllers/api/payzen_controller.rb +++ b/app/controllers/api/payzen_controller.rb @@ -48,13 +48,12 @@ class API::PayzenController < API::PaymentsController order = client.get(params[:order_id], operation_type: 'DEBIT') cart = shopping_cart - amount = debit_amount(cart) if order['answer']['transactions'].first['status'] == 'PAID' if cart.reservation - res = on_reservation_success(params[:order_id], amount[:details], cart) + res = on_reservation_success(params[:order_id], cart) elsif cart.subscription - res = on_subscription_success(params[:order_id], amount[:details], cart) + res = on_subscription_success(params[:order_id], cart) end end @@ -65,12 +64,12 @@ class API::PayzenController < API::PaymentsController private - def on_reservation_success(order_id, details, cart) - super(order_id, 'PayZen::Order', details, cart) + def on_reservation_success(order_id, cart) + super(order_id, 'PayZen::Order', cart) end - def on_subscription_success(order_id, details, cart) - super(order_id, 'PayZen::Order', details, cart) + def on_subscription_success(order_id, cart) + super(order_id, 'PayZen::Order', cart) end def error_handling diff --git a/app/controllers/api/stripe_controller.rb b/app/controllers/api/stripe_controller.rb index 453d7cd70..4f23a7374 100644 --- a/app/controllers/api/stripe_controller.rb +++ b/app/controllers/api/stripe_controller.rb @@ -12,13 +12,12 @@ class API::StripeController < API::PaymentsController def confirm_payment render(json: { error: 'Bad gateway or online payment is disabled' }, status: :bad_gateway) and return unless Stripe::Helper.enabled? - amount = nil # will contains the amount and the details of each invoice lines intent = nil # stripe's payment intent res = nil # json of the API answer cart = shopping_cart begin - amount = debit_amount(cart) + amount = debit_amount(cart) # will contains the amount and the details of each invoice lines if params[:payment_method_id].present? check_coupon(cart) check_plan(cart) @@ -48,9 +47,9 @@ class API::StripeController < API::PaymentsController if intent&.status == 'succeeded' if cart.reservation - res = on_reservation_success(intent, amount[:details], cart) + res = on_reservation_success(intent, cart) elsif cart.subscription - res = on_subscription_success(intent, amount[:details], cart) + res = on_subscription_success(intent, cart) end end @@ -81,12 +80,11 @@ class API::StripeController < API::PaymentsController intent = Stripe::SetupIntent.retrieve(params[:setup_intent_id], api_key: key) cart = shopping_cart - amount = debit_amount(cart) if intent&.status == 'succeeded' if cart.reservation - res = on_reservation_success(intent, amount[:details], cart) + res = on_reservation_success(intent, cart) elsif cart.subscription - res = on_subscription_success(intent, amount[:details], cart) + res = on_subscription_success(intent, cart) end end @@ -128,12 +126,12 @@ class API::StripeController < API::PaymentsController ) end - def on_reservation_success(intent, details, cart) - super(intent.id, intent.class.name, details, cart) + def on_reservation_success(intent, cart) + super(intent.id, intent.class.name, cart) end - def on_subscription_success(intent, details, cart) - super(intent.id, intent.class.name, details, cart) + def on_subscription_success(intent, cart) + super(intent.id, intent.class.name, cart) end def generate_payment_response(intent, res = nil) diff --git a/app/frontend/src/javascript/api/payzen.ts b/app/frontend/src/javascript/api/payzen.ts index 5d404c180..4d1c6966b 100644 --- a/app/frontend/src/javascript/api/payzen.ts +++ b/app/frontend/src/javascript/api/payzen.ts @@ -1,6 +1,6 @@ import apiClient from './clients/api-client'; import { AxiosResponse } from 'axios'; -import { CartItems } from '../models/payment'; +import { ShoppingCart } from '../models/payment'; import { User } from '../models/user'; import { CheckHashResponse, @@ -17,13 +17,13 @@ export default class PayzenAPI { return res?.data; } - static async chargeCreatePayment(cartItems: CartItems, customer: User): Promise { - const res: AxiosResponse = await apiClient.post('/api/payzen/create_payment', { cart_items: cartItems, customer_id: customer.id }); + static async chargeCreatePayment(cart: ShoppingCart, customer: User): Promise { + const res: AxiosResponse = await apiClient.post('/api/payzen/create_payment', { cart_items: cart, customer_id: customer.id }); return res?.data; } - static async chargeCreateToken(cartItems: CartItems, customer: User): Promise { - const res: AxiosResponse = await apiClient.post('/api/payzen/create_token', { cart_items: cartItems, customer_id: customer.id }); + static async chargeCreateToken(cart: ShoppingCart, customer: User): Promise { + const res: AxiosResponse = await apiClient.post('/api/payzen/create_token', { cart_items: cart, customer_id: customer.id }); return res?.data; } @@ -32,8 +32,8 @@ export default class PayzenAPI { return res?.data; } - static async confirm(orderId: string, cartItems: CartItems): Promise { - const res: AxiosResponse = await apiClient.post('/api/payzen/confirm_payment', { cart_items: cartItems, order_id: orderId }); + static async confirm(orderId: string, cart: ShoppingCart): Promise { + const res: AxiosResponse = await apiClient.post('/api/payzen/confirm_payment', { cart_items: cart, order_id: orderId }); return res?.data; } } diff --git a/app/frontend/src/javascript/api/price.ts b/app/frontend/src/javascript/api/price.ts index 3acfa72e8..dfd9ea901 100644 --- a/app/frontend/src/javascript/api/price.ts +++ b/app/frontend/src/javascript/api/price.ts @@ -1,11 +1,11 @@ import apiClient from './clients/api-client'; import { AxiosResponse } from 'axios'; -import { CartItems } from '../models/payment'; +import { ShoppingCart } from '../models/payment'; import { ComputePriceResult } from '../models/price'; export default class PriceAPI { - static async compute (cartItems: CartItems): Promise { - const res: AxiosResponse = await apiClient.post(`/api/prices/compute`, cartItems); + static async compute (cart: ShoppingCart): Promise { + const res: AxiosResponse = await apiClient.post(`/api/prices/compute`, cart); return res?.data; } } diff --git a/app/frontend/src/javascript/api/stripe.ts b/app/frontend/src/javascript/api/stripe.ts index ffca1f3af..4e7a000d0 100644 --- a/app/frontend/src/javascript/api/stripe.ts +++ b/app/frontend/src/javascript/api/stripe.ts @@ -1,9 +1,9 @@ import apiClient from './clients/api-client'; import { AxiosResponse } from 'axios'; -import { CartItems, IntentConfirmation, PaymentConfirmation, UpdateCardResponse } from '../models/payment'; +import { ShoppingCart, IntentConfirmation, PaymentConfirmation, UpdateCardResponse } from '../models/payment'; export default class StripeAPI { - static async confirm (stp_payment_method_id: string, cart_items: CartItems): Promise { + static async confirm (stp_payment_method_id: string, cart_items: ShoppingCart): Promise { const res: AxiosResponse = await apiClient.post(`/api/stripe/confirm_payment`, { payment_method_id: stp_payment_method_id, cart_items @@ -17,7 +17,7 @@ export default class StripeAPI { } // TODO, type the response - static async confirmPaymentSchedule (setup_intent_id: string, cart_items: CartItems): Promise { + static async confirmPaymentSchedule (setup_intent_id: string, cart_items: ShoppingCart): Promise { const res: AxiosResponse = await apiClient.post(`/api/stripe/confirm_payment_schedule`, { setup_intent_id, cart_items diff --git a/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx b/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx index 54ce16b0d..a4fa4fc28 100644 --- a/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx +++ b/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx @@ -6,7 +6,7 @@ import { FabModal, ModalSize } from '../base/fab-modal'; import { HtmlTranslate } from '../base/html-translate'; import { CustomAssetName } from '../../models/custom-asset'; import { IFablab } from '../../models/fablab'; -import { CartItems } from '../../models/payment'; +import { ShoppingCart } from '../../models/payment'; import { PaymentSchedule } from '../../models/payment-schedule'; import { User } from '../../models/user'; import CustomAssetAPI from '../../api/custom-asset'; @@ -24,7 +24,7 @@ export interface GatewayFormProps { operator: User, className?: string, paymentSchedule?: boolean, - cartItems?: CartItems, + cart?: ShoppingCart, formId: string, } @@ -32,7 +32,7 @@ interface AbstractPaymentModalProps { isOpen: boolean, toggleModal: () => void, afterSuccess: (result: any) => void, - cartItems: CartItems, + cart: ShoppingCart, currentUser: User, schedule: PaymentSchedule, customer: User, @@ -53,7 +53,7 @@ const cgvFile = CustomAssetAPI.get(CustomAssetName.CgvFile); * This component must not be called directly but must be extended for each implemented payment gateway * @see https://reactjs.org/docs/composition-vs-inheritance.html */ -export const AbstractPaymentModal: React.FC = ({ isOpen, toggleModal, afterSuccess, cartItems, currentUser, schedule, customer, logoFooter, GatewayForm, formId, className, formClassName }) => { +export const AbstractPaymentModal: React.FC = ({ isOpen, toggleModal, afterSuccess, cart, currentUser, schedule, customer, logoFooter, GatewayForm, formId, className, formClassName }) => { // customer's wallet const [wallet, setWallet] = useState(null); // server-computed price with all details @@ -80,17 +80,17 @@ export const AbstractPaymentModal: React.FC = ({ isOp * - Refresh the remaining price */ useEffect(() => { - if (!cartItems) return; - WalletAPI.getByUser(cartItems.customer_id).then((wallet) => { + if (!cart) return; + WalletAPI.getByUser(cart.customer_id).then((wallet) => { setWallet(wallet); - PriceAPI.compute(cartItems).then((res) => { + PriceAPI.compute(cart).then((res) => { setPrice(res); const wLib = new WalletLib(wallet); setRemainingPrice(wLib.computeRemainingPrice(res.price)); setReady(true); }) }) - }, [cartItems]); + }, [cart]); /** * Check if there is currently an error to display @@ -170,14 +170,14 @@ export const AbstractPaymentModal: React.FC = ({ isOp customFooter={logoFooter} className={`payment-modal ${className ? className : ''}`}> {ready &&
- + {hasErrors() &&
diff --git a/app/frontend/src/javascript/components/payment/payment-modal.tsx b/app/frontend/src/javascript/components/payment/payment-modal.tsx index 360f23203..ccdb17dfe 100644 --- a/app/frontend/src/javascript/components/payment/payment-modal.tsx +++ b/app/frontend/src/javascript/components/payment/payment-modal.tsx @@ -1,6 +1,6 @@ import React, { ReactElement, ReactNode } from 'react'; import { IApplication } from '../../models/application'; -import { CartItems } from '../../models/payment'; +import { ShoppingCart } from '../../models/payment'; import { User } from '../../models/user'; import { PaymentSchedule } from '../../models/payment-schedule'; import { Loader } from '../base/loader'; @@ -16,7 +16,7 @@ interface PaymentModalProps { isOpen: boolean, toggleModal: () => void, afterSuccess: (result: any) => void, - cartItems: CartItems, + cart: ShoppingCart, currentUser: User, schedule: PaymentSchedule, customer: User @@ -29,7 +29,7 @@ const paymentGateway = SettingAPI.get(SettingName.PaymentGateway); * This component open a modal dialog for the configured payment gateway, allowing the user to input his card data * to process an online payment. */ -const PaymentModal: React.FC = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule , cartItems, customer }) => { +const PaymentModal: React.FC = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule , cart, customer }) => { const gateway = paymentGateway.read(); /** @@ -39,7 +39,7 @@ const PaymentModal: React.FC = ({ isOpen, toggleModal, afterS return @@ -52,7 +52,7 @@ const PaymentModal: React.FC = ({ isOpen, toggleModal, afterS return @@ -73,12 +73,12 @@ const PaymentModal: React.FC = ({ isOpen, toggleModal, afterS } -const PaymentModalWrapper: React.FC = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule , cartItems, customer }) => { +const PaymentModalWrapper: React.FC = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule , cart, customer }) => { return ( - + ); } -Application.Components.component('paymentModal', react2angular(PaymentModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess','currentUser', 'schedule', 'cartItems', 'customer'])); +Application.Components.component('paymentModal', react2angular(PaymentModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess','currentUser', 'schedule', 'cart', 'customer'])); diff --git a/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx b/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx index acd7ddbd8..b9b5adab8 100644 --- a/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx +++ b/app/frontend/src/javascript/components/payment/payzen/payzen-form.tsx @@ -16,7 +16,7 @@ import { * A form component to collect the credit card details and to create the payment method on Stripe. * The form validation button must be created elsewhere, using the attribute form={formId}. */ -export const PayzenForm: React.FC = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cartItems, customer, operator, formId }) => { +export const PayzenForm: React.FC = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cart, customer, operator, formId }) => { const PayZenKR = useRef(null); const [loadingClass, setLoadingClass] = useState<'hidden' | 'loader' | 'loader-overlay'>('loader'); @@ -39,7 +39,7 @@ export const PayzenForm: React.FC = ({ onSubmit, onSuccess, on .then(({ KR }) => PayZenKR.current = KR); }).catch(error => onError(error)); }); - }, [cartItems, paymentSchedule, customer]); + }, [cart, paymentSchedule, customer]); /** * Ask the API to create the form token. @@ -47,9 +47,9 @@ export const PayzenForm: React.FC = ({ onSubmit, onSuccess, on */ const createToken = async (): Promise => { if (paymentSchedule) { - return await PayzenAPI.chargeCreateToken(cartItems, customer); + return await PayzenAPI.chargeCreateToken(cart, customer); } else { - return await PayzenAPI.chargeCreatePayment(cartItems, customer); + return await PayzenAPI.chargeCreatePayment(cart, customer); } } @@ -63,7 +63,7 @@ export const PayzenForm: React.FC = ({ onSubmit, onSuccess, on const transaction = event.clientAnswer.transactions[0]; if (event.clientAnswer.orderStatus === 'PAID') { - PayzenAPI.confirm(event.clientAnswer.orderDetails.orderId, cartItems).then((confirmation) => { + PayzenAPI.confirm(event.clientAnswer.orderDetails.orderId, cart).then((confirmation) => { PayZenKR.current.removeForms().then(() => { onSuccess(confirmation); }); diff --git a/app/frontend/src/javascript/components/payment/payzen/payzen-modal.tsx b/app/frontend/src/javascript/components/payment/payzen/payzen-modal.tsx index 46440f809..f848f3428 100644 --- a/app/frontend/src/javascript/components/payment/payzen/payzen-modal.tsx +++ b/app/frontend/src/javascript/components/payment/payzen/payzen-modal.tsx @@ -1,6 +1,6 @@ import React, { FunctionComponent, ReactNode } from 'react'; import { GatewayFormProps, AbstractPaymentModal } from '../abstract-payment-modal'; -import { CartItems, PaymentConfirmation } from '../../../models/payment'; +import { ShoppingCart, PaymentConfirmation } from '../../../models/payment'; import { PaymentSchedule } from '../../../models/payment-schedule'; import { User } from '../../../models/user'; @@ -14,7 +14,7 @@ interface PayZenModalProps { isOpen: boolean, toggleModal: () => void, afterSuccess: (result: PaymentConfirmation) => void, - cartItems: CartItems, + cart: ShoppingCart, currentUser: User, schedule: PaymentSchedule, customer: User @@ -27,7 +27,7 @@ interface PayZenModalProps { * This component should not be called directly. Prefer using which can handle the configuration * of a different payment gateway. */ -export const PayZenModal: React.FC = ({ isOpen, toggleModal, afterSuccess, cartItems, currentUser, schedule, customer }) => { +export const PayZenModal: React.FC = ({ isOpen, toggleModal, afterSuccess, cart, currentUser, schedule, customer }) => { /** * Return the logos, shown in the modal footer. */ @@ -44,7 +44,7 @@ export const PayZenModal: React.FC = ({ isOpen, toggleModal, a /** * Integrates the PayzenForm into the parent PaymentModal */ - const renderForm: FunctionComponent = ({ onSubmit, onSuccess, onError, operator, className, formId, cartItems, customer, paymentSchedule, children}) => { + const renderForm: FunctionComponent = ({ onSubmit, onSuccess, onError, operator, className, formId, cart, customer, paymentSchedule, children}) => { return ( = ({ isOpen, toggleModal, a customer={customer} operator={operator} formId={formId} - cartItems={cartItems} + cart={cart} className={className} paymentSchedule={paymentSchedule}> {children} @@ -68,7 +68,7 @@ export const PayZenModal: React.FC = ({ isOpen, toggleModal, a formClassName="payzen-form" className="payzen-modal" currentUser={currentUser} - cartItems={cartItems} + cart={cart} customer={customer} afterSuccess={afterSuccess} schedule={schedule} diff --git a/app/frontend/src/javascript/components/payment/stripe/stripe-form.tsx b/app/frontend/src/javascript/components/payment/stripe/stripe-form.tsx index 70a7ef4d9..890895b86 100644 --- a/app/frontend/src/javascript/components/payment/stripe/stripe-form.tsx +++ b/app/frontend/src/javascript/components/payment/stripe/stripe-form.tsx @@ -14,7 +14,7 @@ interface StripeFormProps extends GatewayFormProps { * A form component to collect the credit card details and to create the payment method on Stripe. * The form validation button must be created elsewhere, using the attribute form={formId}. */ -export const StripeForm: React.FC = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cartItems, customer, operator, formId }) => { +export const StripeForm: React.FC = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cart, customer, operator, formId }) => { const { t } = useTranslation('shared'); @@ -45,7 +45,7 @@ export const StripeForm: React.FC = ({ onSubmit, onSuccess, onE try { if (!paymentSchedule) { // process the normal payment pipeline, including SCA validation - const res = await StripeAPI.confirm(paymentMethod.id, cartItems); + const res = await StripeAPI.confirm(paymentMethod.id, cart); await handleServerConfirmation(res); } else { // we start by associating the payment method with the user @@ -66,7 +66,7 @@ export const StripeForm: React.FC = ({ onSubmit, onSuccess, onE onError(error.message); } else { // then we confirm the payment schedule - const res = await StripeAPI.confirmPaymentSchedule(setupIntent.id, cartItems); + const res = await StripeAPI.confirmPaymentSchedule(setupIntent.id, cart); onSuccess(res); } } @@ -100,7 +100,7 @@ export const StripeForm: React.FC = ({ onSubmit, onSuccess, onE // The card action has been handled // The PaymentIntent can be confirmed again on the server try { - const confirmation = await StripeAPI.confirm(result.paymentIntent.id, cartItems); + const confirmation = await StripeAPI.confirm(result.paymentIntent.id, cart); await handleServerConfirmation(confirmation); } catch (e) { onError(e); diff --git a/app/frontend/src/javascript/components/payment/stripe/stripe-modal.tsx b/app/frontend/src/javascript/components/payment/stripe/stripe-modal.tsx index 01a2990e1..7161abc78 100644 --- a/app/frontend/src/javascript/components/payment/stripe/stripe-modal.tsx +++ b/app/frontend/src/javascript/components/payment/stripe/stripe-modal.tsx @@ -3,7 +3,7 @@ import { SetupIntent } from '@stripe/stripe-js'; import { StripeElements } from './stripe-elements'; import { StripeForm } from './stripe-form'; import { GatewayFormProps, AbstractPaymentModal } from '../abstract-payment-modal'; -import { CartItems, PaymentConfirmation } from '../../../models/payment'; +import { ShoppingCart, PaymentConfirmation } from '../../../models/payment'; import { PaymentSchedule } from '../../../models/payment-schedule'; import { User } from '../../../models/user'; @@ -16,7 +16,7 @@ interface StripeModalProps { isOpen: boolean, toggleModal: () => void, afterSuccess: (result: SetupIntent|PaymentConfirmation) => void, - cartItems: CartItems, + cart: ShoppingCart, currentUser: User, schedule: PaymentSchedule, customer: User @@ -29,7 +29,7 @@ interface StripeModalProps { * This component should not be called directly. Prefer using which can handle the configuration * of a different payment gateway. */ -export const StripeModal: React.FC = ({ isOpen, toggleModal, afterSuccess, cartItems, currentUser, schedule, customer }) => { +export const StripeModal: React.FC = ({ isOpen, toggleModal, afterSuccess, cart, currentUser, schedule, customer }) => { /** * Return the logos, shown in the modal footer. */ @@ -47,7 +47,7 @@ export const StripeModal: React.FC = ({ isOpen, toggleModal, a /** * Integrates the StripeForm into the parent PaymentModal */ - const renderForm: FunctionComponent = ({ onSubmit, onSuccess, onError, operator, className, formId, cartItems, customer, paymentSchedule, children}) => { + const renderForm: FunctionComponent = ({ onSubmit, onSuccess, onError, operator, className, formId, cart, customer, paymentSchedule, children}) => { return ( = ({ isOpen, toggleModal, a operator={operator} className={className} formId={formId} - cartItems={cartItems} + cart={cart} customer={customer} paymentSchedule={paymentSchedule}> {children} @@ -73,7 +73,7 @@ export const StripeModal: React.FC = ({ isOpen, toggleModal, a formId="stripe-form" formClassName="stripe-form" currentUser={currentUser} - cartItems={cartItems} + cart={cart} customer={customer} afterSuccess={afterSuccess} schedule={schedule} diff --git a/app/frontend/src/javascript/components/wallet-info.tsx b/app/frontend/src/javascript/components/wallet-info.tsx index ffc25f6fc..ed22fe87e 100644 --- a/app/frontend/src/javascript/components/wallet-info.tsx +++ b/app/frontend/src/javascript/components/wallet-info.tsx @@ -8,7 +8,7 @@ import { User } from '../models/user'; import { Wallet } from '../models/wallet'; import { IFablab } from '../models/fablab'; import WalletLib from '../lib/wallet'; -import { CartItems } from '../models/payment'; +import { ShoppingCart } from '../models/payment'; import { Reservation } from '../models/reservation'; import { SubscriptionRequest } from '../models/subscription'; @@ -16,7 +16,7 @@ declare var Application: IApplication; declare var Fablab: IFablab; interface WalletInfoProps { - cartItems: CartItems, + cart: ShoppingCart, currentUser: User, wallet: Wallet, price: number, @@ -25,7 +25,7 @@ interface WalletInfoProps { /** * This component displays a summary of the amount paid with the virtual wallet, for the current transaction */ -export const WalletInfo: React.FC = ({ cartItems, currentUser, wallet, price }) => { +export const WalletInfo: React.FC = ({ cart, currentUser, wallet, price }) => { const { t } = useTranslation('shared'); const [remainingPrice, setRemainingPrice] = useState(0); @@ -48,7 +48,7 @@ export const WalletInfo: React.FC = ({ cartItems, currentUser, * If the currently connected user (i.e. the operator), is an admin or a manager, he may book the reservation for someone else. */ const isOperatorAndClient = (): boolean => { - return currentUser.id == cartItems.customer_id; + return currentUser.id == cart.customer_id; } /** * If the client has some money in his wallet & the price is not zero, then we should display this component. @@ -67,17 +67,17 @@ export const WalletInfo: React.FC = ({ cartItems, currentUser, * Does the current cart contains a payment schedule? */ const isPaymentSchedule = (): boolean => { - return cartItems.subscription && cartItems.payment_schedule; + return cart.items.find(i => 'subscription' in i) && cart.payment_schedule; } /** * Return the human-readable name of the item currently bought with the wallet */ const getPriceItem = (): string => { let item = 'other'; - if (cartItems.reservation) { + if (cart.items.find(i => 'reservation' in i)) { item = 'reservation'; - } else if (cartItems.subscription) { - if (cartItems.payment_schedule) { + } else if (cart.items.find(i => 'subscription' in i)) { + if (cart.payment_schedule) { item = 'first_deadline'; } else item = 'subscription'; } @@ -121,12 +121,12 @@ export const WalletInfo: React.FC = ({ cartItems, currentUser, ); } -const WalletInfoWrapper: React.FC = ({ currentUser, cartItems, price, wallet }) => { +const WalletInfoWrapper: React.FC = ({ currentUser, cart, price, wallet }) => { return ( - + ); } -Application.Components.component('walletInfo', react2angular(WalletInfoWrapper, ['currentUser', 'price', 'cartItems', 'wallet'])); +Application.Components.component('walletInfo', react2angular(WalletInfoWrapper, ['currentUser', 'price', 'cart', 'wallet'])); diff --git a/app/frontend/src/javascript/controllers/events.js.erb b/app/frontend/src/javascript/controllers/events.js.erb index 48696c6e0..52d4c08c9 100644 --- a/app/frontend/src/javascript/controllers/events.js.erb +++ b/app/frontend/src/javascript/controllers/events.js.erb @@ -335,16 +335,20 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' $scope.validReserveEvent = function () { const cartItems = { customer_id: $scope.ctrl.member.id, - reservation: { - reservable_id: $scope.event.id, - reservable_type: 'Event', - slots_attributes: [], - nb_reserve_places: $scope.reserve.nbReservePlaces, - tickets_attributes: [] - } + items: [ + { + reservation: { + reservable_id: $scope.event.id, + reservable_type: 'Event', + slots_attributes: [], + nb_reserve_places: $scope.reserve.nbReservePlaces, + tickets_attributes: [] + } + } + ] } // a single slot is used for events - cartItems.reservation.slots_attributes.push({ + cartItems.items[0].reservation.slots_attributes.push({ start_at: $scope.event.start_date, end_at: $scope.event.end_date, availability_id: $scope.event.availability.id @@ -353,7 +357,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' for (let price_id in $scope.reserve.tickets) { if (Object.prototype.hasOwnProperty.call($scope.reserve.tickets, price_id)) { const seats = $scope.reserve.tickets[price_id]; - cartItems.reservation.tickets_attributes.push({ + cartItems.items[0].reservation.tickets_attributes.push({ event_price_category_id: price_id, booked: seats }); @@ -363,7 +367,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' $scope.attempting = true; // save the reservation to the API return Reservation.save(cartItems, function (reservation) { - // reservation successfull + // reservation successful afterPayment(reservation); return $scope.attempting = false; } @@ -657,7 +661,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' * @param reservation {Object} as returned by mkReservation() * @param coupon {Object} Coupon as returned from the API * @param paymentMethod {string} 'card' | '' - * @return {CartItems} + * @return {ShoppingCart} */ const mkCartItems = function (reservation, coupon, paymentMethod = '') { return { diff --git a/app/frontend/src/javascript/directives/cart.js b/app/frontend/src/javascript/directives/cart.js index 27179e5de..1a0572ed5 100644 --- a/app/frontend/src/javascript/directives/cart.js +++ b/app/frontend/src/javascript/directives/cart.js @@ -681,10 +681,10 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', }; /** - * Build the CartItems object, from the current reservation + * Build the ShoppingCart object, from the current reservation * @param items {Array<{reservation:{reservable_type: string, reservable_id: string, slots_attributes: []}}|{subscription: {plan_id: number}}>} * @param paymentMethod {string} - * @return {CartItems} + * @return {ShoppingCart} */ const mkCartItems = function (items, paymentMethod = '') { return { diff --git a/app/frontend/src/javascript/models/payment.ts b/app/frontend/src/javascript/models/payment.ts index 16777e46c..6352c9ab6 100644 --- a/app/frontend/src/javascript/models/payment.ts +++ b/app/frontend/src/javascript/models/payment.ts @@ -19,9 +19,11 @@ export enum PaymentMethod { Other = '' } -export interface CartItems { +export type CartItem = { reservation: Reservation }|{ subscription: SubscriptionRequest }; + +export interface ShoppingCart { customer_id: number, - items: Array, + items: Array, coupon_code?: string, payment_schedule?: boolean, payment_method: PaymentMethod diff --git a/app/frontend/templates/events/show.html b/app/frontend/templates/events/show.html index f05f06ea6..c8ccf9234 100644 --- a/app/frontend/templates/events/show.html +++ b/app/frontend/templates/events/show.html @@ -209,7 +209,7 @@
diff --git a/app/frontend/templates/shared/_cart.html b/app/frontend/templates/shared/_cart.html index 1f37dfa24..2e56a2dfa 100644 --- a/app/frontend/templates/shared/_cart.html +++ b/app/frontend/templates/shared/_cart.html @@ -203,7 +203,7 @@ diff --git a/app/frontend/templates/shared/valid_reservation_modal.html b/app/frontend/templates/shared/valid_reservation_modal.html index 76a6e5fb5..779bb15a5 100644 --- a/app/frontend/templates/shared/valid_reservation_modal.html +++ b/app/frontend/templates/shared/valid_reservation_modal.html @@ -41,7 +41,7 @@
@@ -52,7 +52,7 @@ { reservable.is_a?(Machine) } validate :training_not_fully_reserved, if: -> { reservable.is_a?(Training) } + validate :slots_not_locked validates_with ReservationSlotSubscriptionValidator attr_accessor :plan_id, :subscription after_commit :notify_member_create_reservation, on: :create after_commit :notify_admin_member_create_reservation, on: :create + after_commit :update_credits, on: :create + after_commit :extend_subscription, on: :create after_save :update_event_nb_free_places, if: proc { |reservation| reservation.reservable_type == 'Event' } - ## - # These checks will run before the invoice/payment-schedule is generated - ## - def pre_check - # check that none of the reserved availabilities was locked - slots.each do |slot| - raise LockedError if slot.availability.lock - end - end - ## Generate the subscription associated with for the current reservation def generate_subscription return unless plan_id @@ -50,13 +43,6 @@ class Reservation < ApplicationRecord subscription end - ## - # These actions will be realized after the reservation is initially saved (on creation) - ## - def post_save - UsersCredits::Manager.new(reservation: self).update_credits - end - # @param canceled if true, count the number of seats for this reservation, including canceled seats def total_booked_seats(canceled: false) # cases: @@ -106,6 +92,21 @@ class Reservation < ApplicationRecord errors.add(:training, 'already fully reserved') if Availability.find(slot.availability_id).completed? end + def slots_not_locked + # check that none of the reserved availabilities was locked + slots.each do |slot| + errors.add(:slots, 'locked') if slot.availability.lock + end + end + + def update_credits + UsersCredits::Manager.new(reservation: self).update_credits + end + + def extend_subscription + SubscriptionExtensionAfterReservation.new(self).extend_subscription_if_eligible + end + def notify_member_create_reservation NotificationCenter.call type: 'notify_member_create_reservation', receiver: user, diff --git a/app/models/shopping_cart.rb b/app/models/shopping_cart.rb index f67d9b118..fe0b84046 100644 --- a/app/models/shopping_cart.rb +++ b/app/models/shopping_cart.rb @@ -2,16 +2,18 @@ # Stores data about a shopping data class ShoppingCart - attr_accessor :customer, :payment_method, :items, :coupon, :payment_schedule + attr_accessor :customer, :operator, :payment_method, :items, :coupon, :payment_schedule # @param items {Array} # @param coupon {CartItem::Coupon} # @param payment_schedule {CartItem::PaymentSchedule} # @param customer {User} - def initialize(customer, coupon, payment_schedule, payment_method = '', items: []) + # @param operator {User} + def initialize(customer, operator, coupon, payment_schedule, payment_method = '', items: []) raise TypeError unless customer.is_a? User @customer = customer + @operator = operator @payment_method = payment_method @items = items @coupon = coupon @@ -50,4 +52,44 @@ class ShoppingCart schedule: schedule_info[:schedule] } end + + def pay_and_save(payment_id, payment_type) + price = total + objects = [] + ActiveRecord::Base.transaction do + items.each do |item| + object = item.to_object + object.save + objects.push(object) + raise ActiveRecord::Rollback unless object.errors.count.zero? + end + + payment = if price[:schedule] + PaymentScheduleService.new.create( + subscription&.to_object, + price[:before_coupon], + coupon: @coupon, + operator: @operator, + payment_method: @payment_method, + user: @customer, + reservation: reservation&.to_object, + payment_id: payment_id, + payment_type: payment_type + ) + else + InvoicesService.create( + price, + @operator.invoicing_profile.id, + reservation: reservation&.to_object, + payment_id: payment_id, + payment_type: payment_type, + payment_method: @payment_method + ) + end + payment.save + payment.post_save(payment_id) + end + + objects.map(&:errors).flatten.count.zero? + end end diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 377060347..dfaecd674 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -16,23 +16,11 @@ class Subscription < ApplicationRecord validates_with SubscriptionGroupValidator # creation + before_create :set_expiration_date after_save :notify_member_subscribed_plan after_save :notify_admin_subscribed_plan after_save :notify_partner_subscribed_plan, if: :of_partner_plan? - - ## - # Set the inner properties of the subscription, init the user's credits and save the subscription into the DB - # @return {boolean} true, if the operation succeeded - ## - def init_save - return false unless valid? - - set_expiration_date - return false unless save - - UsersCredits::Manager.new(user: user).reset_credits - true - end + after_commit :update_credits, on: :create def generate_and_save_invoice(operator_profile_id) generate_invoice(operator_profile_id).save @@ -148,4 +136,9 @@ class Subscription < ApplicationRecord def of_partner_plan? plan.is_a?(PartnerPlan) end + + # init the user's credits + def update_credits + UsersCredits::Manager.new(user: user).reset_credits + end end diff --git a/app/services/cart_service.rb b/app/services/cart_service.rb index 391394463..be8e43f2a 100644 --- a/app/services/cart_service.rb +++ b/app/services/cart_service.rb @@ -8,7 +8,7 @@ class CartService ## # For details about the expected hash format - # @see app/frontend/src/javascript/models/payment.ts > interface CartItems + # @see app/frontend/src/javascript/models/payment.ts > interface ShoppingCart ## def from_hash(cart_items) @customer = customer(cart_items) @@ -17,7 +17,7 @@ class CartService items = [] cart_items[:items].each do |item| if item.keys.first == 'subscription' - items.push(CartItem::Subscription.new(plan_info[:plan])) if plan_info[:new_subscription] + items.push(CartItem::Subscription.new(plan_info[:plan], @customer)) if plan_info[:new_subscription] elsif item.keys.first == 'reservation' items.push(reservable_from_hash(item[:reservation], plan_info)) end @@ -28,6 +28,7 @@ class CartService ShoppingCart.new( @customer, + @operator, coupon, schedule, cart_items[:payment_method], diff --git a/app/services/reservations/reserve.rb b/app/services/reservations/reserve.rb deleted file mode 100644 index 86dfaea03..000000000 --- a/app/services/reservations/reserve.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -# Provides helper methods for Reservation actions -class Reservations::Reserve - attr_accessor :user_id, :operator_profile_id - - def initialize(user_id, operator_profile_id) - @user_id = user_id - @operator_profile_id = operator_profile_id - end - - ## - # Confirm the payment of the given reservation, generate the associated documents and save the record into - # the database. - ## - def pay_and_save(reservation, payment_details: nil, payment_id: nil, payment_type: nil, schedule: false, payment_method: nil) - user = User.find(user_id) - reservation.statistic_profile_id = StatisticProfile.find_by(user_id: user_id).id - - ActiveRecord::Base.transaction do - reservation.pre_check - payment = if schedule - generate_schedule(reservation: reservation, - total: payment_details[:before_coupon], - operator_profile_id: operator_profile_id, - user: user, - payment_method: payment_method, - coupon: payment_details[:coupon], - payment_id: payment_id, - payment_type: payment_type) - else - generate_invoice(reservation, - operator_profile_id, - payment_details, - payment_id: payment_id, - payment_type: payment_type, - payment_method: payment_method) - end - WalletService.debit_user_wallet(payment, user, reservation) - reservation.save - reservation.post_save - payment.save - payment.post_save(payment_id) - end - true - end - - private - - ## - # Generate the invoice for the given reservation+subscription - ## - def generate_schedule(reservation: nil, total: nil, operator_profile_id: nil, user: nil, payment_method: nil, coupon: nil, - payment_id: nil, payment_type: nil) - operator = InvoicingProfile.find(operator_profile_id)&.user - - PaymentScheduleService.new.create( - nil, - total, - coupon: coupon, - operator: operator, - payment_method: payment_method, - user: user, - reservation: reservation, - payment_id: payment_id, - payment_type: payment_type - ) - end - - ## - # Generate the invoice for the given reservation - ## - def generate_invoice(reservation, operator_profile_id, payment_details, payment_id: nil, payment_type: nil, payment_method: nil) - InvoicesService.create( - payment_details, - operator_profile_id, - reservation: reservation, - payment_id: payment_id, - payment_type: payment_type, - payment_method: payment_method - ) - end - -end diff --git a/app/services/subscriptions/subscribe.rb b/app/services/subscriptions/subscribe.rb index a5ccd6c99..0f55eec93 100644 --- a/app/services/subscriptions/subscribe.rb +++ b/app/services/subscriptions/subscribe.rb @@ -9,48 +9,6 @@ class Subscriptions::Subscribe @operator_profile_id = operator_profile_id end - ## - # @param subscription {Subscription} - # @param payment_details {Hash} as generated by ShoppingCart.total - # @param payment_id {String} from the payment gateway - # @param payment_type {String} the object type of payment_id - # @param schedule {Boolean} - # @param payment_method {String} - ## - def pay_and_save(subscription, payment_details: nil, payment_id: nil, payment_type: nil, schedule: false, payment_method: nil) - return false if user_id.nil? - - user = User.find(user_id) - subscription.statistic_profile_id = StatisticProfile.find_by(user_id: user_id).id - - ActiveRecord::Base.transaction do - subscription.init_save - raise InvalidSubscriptionError unless subscription&.persisted? - - payment = if schedule - generate_schedule(subscription: subscription, - total: payment_details[:before_coupon], - operator_profile_id: operator_profile_id, - user: user, - payment_method: payment_method, - coupon: payment_details[:coupon], - payment_id: payment_id, - payment_type: payment_type) - else - generate_invoice(subscription, - operator_profile_id, - payment_details, - payment_id: payment_id, - payment_type: payment_type, - payment_method: payment_method) - end - WalletService.debit_user_wallet(payment, user, subscription) - payment.save - payment.post_save(payment_id) - end - true - end - def extend_subscription(subscription, new_expiration_date, free_days) return subscription.free_extend(new_expiration_date, @operator_profile_id) if free_days @@ -76,17 +34,23 @@ class Subscriptions::Subscribe details = cart.total payment = if schedule - generate_schedule(subscription: new_sub, - total: details[:before_coupon], - operator_profile_id: operator_profile_id, - user: new_sub.user, - payment_method: schedule.payment_method, - payment_id: schedule.gateway_payment_mean&.id, - payment_type: schedule.gateway_payment_mean&.class) + operator = InvoicingProfile.find(operator_profile_id)&.user + + PaymentScheduleService.new.create( + new_sub, + details[:before_coupon], + operator: operator, + payment_method: schedule.payment_method, + user: new_sub.user, + payment_id: schedule.gateway_payment_mean&.id, + payment_type: schedule.gateway_payment_mean&.class + ) else - generate_invoice(subscription, - operator_profile_id, - details) + InvoicesService.create( + details, + operator_profile_id, + subscription: new_sub + ) end payment.save payment.post_save(schedule&.gateway_payment_mean&.id) @@ -95,40 +59,4 @@ class Subscriptions::Subscribe end false end - - private - - ## - # Generate the invoice for the given subscription - ## - def generate_schedule(subscription: nil, total: nil, operator_profile_id: nil, user: nil, payment_method: nil, coupon: nil, - payment_id: nil, payment_type: nil) - operator = InvoicingProfile.find(operator_profile_id)&.user - - PaymentScheduleService.new.create( - subscription, - total, - coupon: coupon, - operator: operator, - payment_method: payment_method, - user: user, - payment_id: payment_id, - payment_type: payment_type - ) - end - - ## - # Generate the invoice for the given subscription - ## - def generate_invoice(subscription, operator_profile_id, payment_details, payment_id: nil, payment_type: nil, payment_method: nil) - InvoicesService.create( - payment_details, - operator_profile_id, - subscription: subscription, - payment_id: payment_id, - payment_type: payment_type, - payment_method: payment_method - ) - end - end diff --git a/app/validators/reservation_slot_subscription_validator.rb b/app/validators/reservation_slot_subscription_validator.rb index 8815bae12..f88a695ef 100644 --- a/app/validators/reservation_slot_subscription_validator.rb +++ b/app/validators/reservation_slot_subscription_validator.rb @@ -7,7 +7,8 @@ class ReservationSlotSubscriptionValidator < ActiveModel::Validator if record.user.subscribed_plan && s.availability.plan_ids.include?(record.user.subscribed_plan.id) elsif s.availability.plan_ids.include?(record.plan_id) else - record.errors[:slots] << 'slot is restrict for subscriptions' + # FIXME, admin should be able to reserve anyway + # record.errors[:slots] << 'slot is restrict for subscriptions' end end end diff --git a/app/validators/subscription_group_validator.rb b/app/validators/subscription_group_validator.rb index 33d2ce3e9..72ec73680 100644 --- a/app/validators/subscription_group_validator.rb +++ b/app/validators/subscription_group_validator.rb @@ -1,6 +1,6 @@ class SubscriptionGroupValidator < ActiveModel::Validator def validate(record) - return if record.statistic_profile.group_id == record.plan.group_id + return if record.statistic_profile&.group_id == record.plan&.group_id record.errors[:plan_id] << "This plan is not compatible with the current user's group" end diff --git a/db/migrate/20210521073742_fix_invoices_without_invoiced_id.rb b/db/migrate/20210521073742_fix_invoices_without_invoiced_id.rb new file mode 100644 index 000000000..2552acf75 --- /dev/null +++ b/db/migrate/20210521073742_fix_invoices_without_invoiced_id.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'integrity/archive_helper' + +# This migration will ensure data integrity for invoices. +# A bug introduced with v4.7.0 has made invoices without invoiced_id for Reservations. +# This issue is concerning slots restricted to subscribers, when the restriction was manually overridden by an admin. +class FixInvoicesWithoutInvoicedId < ActiveRecord::Migration[5.2] + def up + return unless Invoice.where(invoiced_id: nil).count.positive? + + # check the footprints and save the archives + Integrity::ArchiveHelper.check_footprints + periods = Integrity::ArchiveHelper.backup_and_remove_periods + + # fix invoices data + Invoice.where(invoiced_id: nil).each do |invoice| + if invoice.invoiced_type != 'Reservation' + STDERR.puts "WARNING: Invoice #{invoice.id} is not about a reservation, ignoring..." + next + end + + ii = invoice.invoice_items.where(subscription_id: nil).first + reservable = find_reservable(ii) + if reservable + if reservable.is_a? Event + STDERR.puts "WARNING: invoice #{invoice.id} may be linked to the Event #{reservable.id}. This is unsupported, ignoring..." + next + end + ::Reservation.create!( + reservable_id: reservable.id, + reservable_type: reservable.class.name, + slots_attributes: slots_attributes(invoice, reservable), + statistic_profile_id: StatisticProfile.find_by(user: invoice.user).id + ) + invoice.update_attributes(invoiced: reservation) + else + STDERR.puts "WARNING: Unable to guess the reservable for invoice #{invoice.id}, ignoring..." + end + end + + # chain records + puts 'Chaining all record. This may take a while...' + InvoiceItem.order(:id).all.each(&:chain_record) + Invoice.order(:id).all.each(&:chain_record) + + # re-create all archives from the memory dump + Integrity::ArchiveHelper.restore_periods(periods) + end + + def down; end + + private + + def find_reservable(invoice_item) + descr = /^([a-zA-Z\u00C0-\u017F]+\s+)+/.match(invoice_item.description)[0].strip[/(.*)\s/, 1] + reservable = InvoiceItem.where('description LIKE ?', "#{descr}%") + .map(&:invoice) + .filter { |i| !i.invoiced_id.nil? } + .map(&:invoiced) + .map(&:reservable) + .first + reservable ||= [Machine, Training, Space].map { |c| c.where('name LIKE ?', "#{descr}%") } + .filter { |r| r.count.positive? } + .first + &.first + + reservable || Event.where('title LIKE ?', "#{descr}%").first + end + + def find_slots(invoice) + invoice.invoice_items.map do |ii| + start = DateTime.parse(ii.description) + end_time = DateTime.parse(/- (.+)$/.match(ii.description)[1]) + [start, DateTime.new(start.year, start.month, start.day, end_time.hour, end_time.min, end_time.sec, end_time.zone)] + end + end + + def find_availability(reservable, slot) + return if reservable.is_a? Event + + availability = reservable.availabilities.where('start_at <= ? AND end_at >= ?', slot[0], slot[1]).first + unless availability + STDERR.puts "WARNING: Unable to find an availability for #{reservable.class.name} #{reservable.id}, at #{slot[0]}..." + end + availability + end + + def slots_attributes(invoice, reservable) + find_slots(invoice).map do |slot| + { + start_at: slot[0], + end_at: slot[1], + availability_id: find_availability(reservable, slot).id, + offered: invoice.total.zero? + } + end + end +end diff --git a/db/migrate/20210521085710_add_object_to_invoice_item.rb b/db/migrate/20210521085710_add_object_to_invoice_item.rb new file mode 100644 index 000000000..4c84ff470 --- /dev/null +++ b/db/migrate/20210521085710_add_object_to_invoice_item.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'integrity/archive_helper' + +# Previously, the relation between an invoice and the bought objects where stored disparately: +# Invoice.invoiced_id & Invoice.invoiced_type saved the main object, and if a subscription was took at the same time of +# a reservation (the only case where two object were bought at the same time), the reservation was saved in +# Invoice.invoiced and the subscription was saved in InvoiceItem.subscription_id +# +# From this migration, everything will be saved in InvoiceItems.object_id & InvoiceItem.object_type. This will be more +# extensible and will allow to invoice more types of objects the future. +class AddObjectToInvoiceItem < ActiveRecord::Migration[5.2] + def up + # first, check the footprints + Integrity::ArchiveHelper.check_footprints + + # if everything is ok, proceed with migration: remove and save periods in memory + periods = Integrity::ArchiveHelper.backup_and_remove_periods + + add_reference :invoice_items, :object, polymorphic: true + add_column :invoice_items, :main, :boolean + # migrate data + Invoice.where.not(invoiced_type: 'Reservation').each do |invoice| + invoice.invoice_items.first.update_attributes( + object_id: invoice.invoiced_id, + object_type: invoice.invoiced_type, + main: true + ) + end + Invoice.where(invoiced_type: 'Reservation').each do |invoice| + invoice.invoice_items.first.update_attributes( + object_id: invoice.invoiced_id, + object_type: invoice.invoiced_type, + main: true + ) + end + remove_column :invoice_items, :subscription_id + remove_reference :invoices, :invoiced + + # chain records + puts 'Chaining all record. This may take a while...' + InvoiceItem.order(:id).all.each(&:chain_record) + Invoice.order(:id).all.each(&:chain_record) + + # re-create all archives from the memory dump + Integrity::ArchiveHelper.restore_periods(periods) + end + + def down + # first, check the footprints + Integrity::ArchiveHelper.check_footprints + + # if everything is ok, proceed with migration: remove and save periods in memory + periods = Integrity::ArchiveHelper.backup_and_remove_periods + + add_column :invoice_items, :subscription_id, :integer + add_reference :invoices, :invoiced, polymorphic: true + # migrate data + remove_column :invoice_items, :main + remove_reference :invoice_items, :object + + # chain records + puts 'Chaining all record. This may take a while...' + InvoiceItem.order(:id).all.each(&:chain_record) + Invoice.order(:id).all.each(&:chain_record) + + # re-create all archives from the memory dump + Integrity::ArchiveHelper.restore_periods(periods) + end +end diff --git a/db/structure.sql b/db/structure.sql index 9dc35298e..691e13904 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -5160,6 +5160,22 @@ CREATE INDEX profiles_lower_unaccent_last_name_trgm_idx ON public.profiles USING CREATE INDEX projects_search_vector_idx ON public.projects USING gin (search_vector); +-- +-- Name: accounting_periods accounting_periods_del_protect; Type: RULE; Schema: public; Owner: - +-- + +CREATE RULE accounting_periods_del_protect AS + ON DELETE TO public.accounting_periods DO INSTEAD NOTHING; + + +-- +-- Name: accounting_periods accounting_periods_upd_protect; Type: RULE; Schema: public; Owner: - +-- + +CREATE RULE accounting_periods_upd_protect AS + ON UPDATE TO public.accounting_periods DO INSTEAD NOTHING; + + -- -- Name: projects projects_search_content_trigger; Type: TRIGGER; Schema: public; Owner: - -- diff --git a/test/integration/reservations/create_as_admin_test.rb b/test/integration/reservations/create_as_admin_test.rb index b5cee982b..615e6fbb5 100644 --- a/test/integration/reservations/create_as_admin_test.rb +++ b/test/integration/reservations/create_as_admin_test.rb @@ -2,6 +2,8 @@ require 'test_helper' +module Reservations; end + class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest setup do @user_without_subscription = User.members.without_subscription.first diff --git a/test/integration/reservations/create_test.rb b/test/integration/reservations/create_test.rb index 9fd4ecda2..5375d4e66 100644 --- a/test/integration/reservations/create_test.rb +++ b/test/integration/reservations/create_test.rb @@ -2,6 +2,8 @@ require 'test_helper' +module Reservations; end + class Reservations::CreateTest < ActionDispatch::IntegrationTest setup do @user_without_subscription = User.members.without_subscription.first