1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-28 09:24:24 +01:00

WIP: refactoring to singularize the booking process

We need to achieve only one process for all booking, not one for subscription, one for reservations, etc.
Moreover we must store one object per invoice_item/payment_schedule_object and stop using Invoice.invoiced or PaymentSchedule.scheduled
This commit is contained in:
Sylvain 2021-05-21 18:25:18 +02:00
parent 2fc0ad1ba0
commit c7a59c8cb7
40 changed files with 413 additions and 359 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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<CreatePaymentResponse> {
const res: AxiosResponse<CreatePaymentResponse> = await apiClient.post('/api/payzen/create_payment', { cart_items: cartItems, customer_id: customer.id });
static async chargeCreatePayment(cart: ShoppingCart, customer: User): Promise<CreatePaymentResponse> {
const res: AxiosResponse<CreatePaymentResponse> = 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<CreateTokenResponse> {
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<CreateTokenResponse> {
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<ConfirmPaymentResponse> {
const res: AxiosResponse<ConfirmPaymentResponse> = await apiClient.post('/api/payzen/confirm_payment', { cart_items: cartItems, order_id: orderId });
static async confirm(orderId: string, cart: ShoppingCart): Promise<ConfirmPaymentResponse> {
const res: AxiosResponse<ConfirmPaymentResponse> = await apiClient.post('/api/payzen/confirm_payment', { cart_items: cart, order_id: orderId });
return res?.data;
}
}

View File

@ -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<ComputePriceResult> {
const res: AxiosResponse = await apiClient.post(`/api/prices/compute`, cartItems);
static async compute (cart: ShoppingCart): Promise<ComputePriceResult> {
const res: AxiosResponse = await apiClient.post(`/api/prices/compute`, cart);
return res?.data;
}
}

View File

@ -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<PaymentConfirmation> {
static async confirm (stp_payment_method_id: string, cart_items: ShoppingCart): Promise<PaymentConfirmation> {
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<any> {
static async confirmPaymentSchedule (setup_intent_id: string, cart_items: ShoppingCart): Promise<any> {
const res: AxiosResponse = await apiClient.post(`/api/stripe/confirm_payment_schedule`, {
setup_intent_id,
cart_items

View File

@ -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<AbstractPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, cartItems, currentUser, schedule, customer, logoFooter, GatewayForm, formId, className, formClassName }) => {
export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ 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<AbstractPaymentModalProps> = ({ 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<AbstractPaymentModalProps> = ({ isOp
customFooter={logoFooter}
className={`payment-modal ${className ? className : ''}`}>
{ready && <div>
<WalletInfo cartItems={cartItems} currentUser={currentUser} wallet={wallet} price={price?.price} />
<WalletInfo cart={cart} currentUser={currentUser} wallet={wallet} price={price?.price} />
<GatewayForm onSubmit={handleSubmit}
onSuccess={handleFormSuccess}
onError={handleFormError}
operator={currentUser}
className={`gateway-form ${formClassName ? formClassName : ''}`}
formId={formId}
cartItems={cartItems}
cart={cart}
customer={customer}
paymentSchedule={isPaymentSchedule()}>
{hasErrors() && <div className="payment-errors">

View File

@ -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<PaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule , cartItems, customer }) => {
const PaymentModal: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule , cart, customer }) => {
const gateway = paymentGateway.read();
/**
@ -39,7 +39,7 @@ const PaymentModal: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterS
return <StripeModal isOpen={isOpen}
toggleModal={toggleModal}
afterSuccess={afterSuccess}
cartItems={cartItems}
cart={cart}
currentUser={currentUser}
schedule={schedule}
customer={customer} />
@ -52,7 +52,7 @@ const PaymentModal: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterS
return <PayZenModal isOpen={isOpen}
toggleModal={toggleModal}
afterSuccess={afterSuccess}
cartItems={cartItems}
cart={cart}
currentUser={currentUser}
schedule={schedule}
customer={customer} />
@ -73,12 +73,12 @@ const PaymentModal: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterS
}
const PaymentModalWrapper: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule , cartItems, customer }) => {
const PaymentModalWrapper: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule , cart, customer }) => {
return (
<Loader>
<PaymentModal isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} currentUser={currentUser} schedule={schedule} cartItems={cartItems} customer={customer} />
<PaymentModal isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} currentUser={currentUser} schedule={schedule} cart={cart} customer={customer} />
</Loader>
);
}
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']));

View File

@ -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<GatewayFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cartItems, customer, operator, formId }) => {
export const PayzenForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cart, customer, operator, formId }) => {
const PayZenKR = useRef<KryptonClient>(null);
const [loadingClass, setLoadingClass] = useState<'hidden' | 'loader' | 'loader-overlay'>('loader');
@ -39,7 +39,7 @@ export const PayzenForm: React.FC<GatewayFormProps> = ({ 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<GatewayFormProps> = ({ onSubmit, onSuccess, on
*/
const createToken = async (): Promise<CreateTokenResponse> => {
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<GatewayFormProps> = ({ 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);
});

View File

@ -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 <PaymentModal> which can handle the configuration
* of a different payment gateway.
*/
export const PayZenModal: React.FC<PayZenModalProps> = ({ isOpen, toggleModal, afterSuccess, cartItems, currentUser, schedule, customer }) => {
export const PayZenModal: React.FC<PayZenModalProps> = ({ isOpen, toggleModal, afterSuccess, cart, currentUser, schedule, customer }) => {
/**
* Return the logos, shown in the modal footer.
*/
@ -44,7 +44,7 @@ export const PayZenModal: React.FC<PayZenModalProps> = ({ isOpen, toggleModal, a
/**
* Integrates the PayzenForm into the parent PaymentModal
*/
const renderForm: FunctionComponent<GatewayFormProps> = ({ onSubmit, onSuccess, onError, operator, className, formId, cartItems, customer, paymentSchedule, children}) => {
const renderForm: FunctionComponent<GatewayFormProps> = ({ onSubmit, onSuccess, onError, operator, className, formId, cart, customer, paymentSchedule, children}) => {
return (
<PayzenForm onSubmit={onSubmit}
onSuccess={onSuccess}
@ -52,7 +52,7 @@ export const PayZenModal: React.FC<PayZenModalProps> = ({ 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<PayZenModalProps> = ({ isOpen, toggleModal, a
formClassName="payzen-form"
className="payzen-modal"
currentUser={currentUser}
cartItems={cartItems}
cart={cart}
customer={customer}
afterSuccess={afterSuccess}
schedule={schedule}

View File

@ -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<StripeFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cartItems, customer, operator, formId }) => {
export const StripeForm: React.FC<StripeFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cart, customer, operator, formId }) => {
const { t } = useTranslation('shared');
@ -45,7 +45,7 @@ export const StripeForm: React.FC<StripeFormProps> = ({ 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<StripeFormProps> = ({ 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<StripeFormProps> = ({ 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);

View File

@ -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 <PaymentModal> which can handle the configuration
* of a different payment gateway.
*/
export const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, cartItems, currentUser, schedule, customer }) => {
export const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, cart, currentUser, schedule, customer }) => {
/**
* Return the logos, shown in the modal footer.
*/
@ -47,7 +47,7 @@ export const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, a
/**
* Integrates the StripeForm into the parent PaymentModal
*/
const renderForm: FunctionComponent<GatewayFormProps> = ({ onSubmit, onSuccess, onError, operator, className, formId, cartItems, customer, paymentSchedule, children}) => {
const renderForm: FunctionComponent<GatewayFormProps> = ({ onSubmit, onSuccess, onError, operator, className, formId, cart, customer, paymentSchedule, children}) => {
return (
<StripeElements>
<StripeForm onSubmit={onSubmit}
@ -56,7 +56,7 @@ export const StripeModal: React.FC<StripeModalProps> = ({ 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<StripeModalProps> = ({ isOpen, toggleModal, a
formId="stripe-form"
formClassName="stripe-form"
currentUser={currentUser}
cartItems={cartItems}
cart={cart}
customer={customer}
afterSuccess={afterSuccess}
schedule={schedule}

View File

@ -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<WalletInfoProps> = ({ cartItems, currentUser, wallet, price }) => {
export const WalletInfo: React.FC<WalletInfoProps> = ({ cart, currentUser, wallet, price }) => {
const { t } = useTranslation('shared');
const [remainingPrice, setRemainingPrice] = useState(0);
@ -48,7 +48,7 @@ export const WalletInfo: React.FC<WalletInfoProps> = ({ 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<WalletInfoProps> = ({ 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<WalletInfoProps> = ({ cartItems, currentUser,
);
}
const WalletInfoWrapper: React.FC<WalletInfoProps> = ({ currentUser, cartItems, price, wallet }) => {
const WalletInfoWrapper: React.FC<WalletInfoProps> = ({ currentUser, cart, price, wallet }) => {
return (
<Loader>
<WalletInfo currentUser={currentUser} cartItems={cartItems} price={price} wallet={wallet}/>
<WalletInfo currentUser={currentUser} cart={cart} price={price} wallet={wallet}/>
</Loader>
);
}
Application.Components.component('walletInfo', react2angular(WalletInfoWrapper, ['currentUser', 'price', 'cartItems', 'wallet']));
Application.Components.component('walletInfo', react2angular(WalletInfoWrapper, ['currentUser', 'price', 'cart', 'wallet']));

View File

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

View File

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

View File

@ -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<Reservation|SubscriptionRequest>,
items: Array<CartItem>,
coupon_code?: string,
payment_schedule?: boolean,
payment_method: PaymentMethod

View File

@ -209,7 +209,7 @@
<payment-modal is-open="onlinePayment.showModal"
toggle-modal="toggleOnlinePaymentModal"
after-success="afterOnlinePaymentSuccess"
cart-items="onlinePayment.cartItems"
cart="onlinePayment.cartItems"
current-user="currentUser"
customer="ctrl.member"/>
</div>

View File

@ -203,7 +203,7 @@
<payment-modal is-open="onlinePayment.showModal"
toggle-modal="toggleOnlinePaymentModal"
after-success="afterOnlinePaymentSuccess"
cart-items="onlinePayment.cartItems"
cart="onlinePayment.cartItems"
current-user="currentUser"
customer="user"
schedule="schedule.payment_schedule"/>

View File

@ -41,7 +41,7 @@
</div>
<div class="row">
<wallet-info current-user="currentUser"
cart-items="cartItems"
cart="cartItems"
price="price"
wallet="wallet"/>
</div>
@ -52,7 +52,7 @@
<payment-modal is-open="isOpenOnlinePaymentModal"
toggle-modal="toggleOnlinePaymentModal"
after-success="afterCreatePaymentSchedule"
cart-items="cartItems"
cart="cartItems"
current-user="currentUser"
schedule="schedule"
customer="user"

View File

@ -12,4 +12,6 @@ class CartItem::BaseItem
def name
''
end
def to_object; end
end

View File

@ -34,13 +34,14 @@ class CartItem::EventReservation < CartItem::Reservation
{ elements: elements, amount: total }
end
def to_reservation
def to_object
::Reservation.new(
reservable_id: @reservable.id,
reservable_type: Event.name,
slots_attributes: slots_params,
tickets_attributes: tickets_params,
nb_reserve_places: @normal_tickets
nb_reserve_places: @normal_tickets,
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
)
end

View File

@ -12,11 +12,12 @@ class CartItem::MachineReservation < CartItem::Reservation
@new_subscription = new_subscription
end
def to_reservation
def to_object
::Reservation.new(
reservable_id: @reservable.id,
reservable_type: Machine.name,
slots_attributes: slots_params
slots_attributes: slots_params,
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
)
end

View File

@ -33,10 +33,6 @@ class CartItem::Reservation < CartItem::BaseItem
@reservable.name
end
def to_reservation
nil
end
protected
def credits

View File

@ -12,11 +12,12 @@ class CartItem::SpaceReservation < CartItem::Reservation
@new_subscription = new_subscription
end
def to_reservation
def to_object
::Reservation.new(
reservable_id: @reservable.id,
reservable_type: Space.name,
slots_attributes: slots_params
slots_attributes: slots_params,
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
)
end

View File

@ -4,10 +4,11 @@
class CartItem::Subscription < CartItem::BaseItem
attr_reader :plan
def initialize(plan)
def initialize(plan, customer)
raise TypeError unless plan.is_a? Plan
@plan = plan
@customer = customer
end
def price
@ -21,9 +22,10 @@ class CartItem::Subscription < CartItem::BaseItem
@plan.name
end
def to_subscription
def to_object
Subscription.new(
plan_id: @plan.id
plan_id: @plan.id,
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
)
end
end

View File

@ -32,11 +32,12 @@ class CartItem::TrainingReservation < CartItem::Reservation
{ elements: elements, amount: amount }
end
def to_reservation
def to_object
::Reservation.new(
reservable_id: @reservable.id,
reservable_type: Training.name,
slots_attributes: slots_params
slots_attributes: slots_params,
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
)
end

View File

@ -23,24 +23,17 @@ class Reservation < ApplicationRecord
validates_presence_of :reservable_id, :reservable_type
validate :machine_not_already_reserved, if: -> { 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,

View File

@ -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<CartItem::BaseItem>}
# @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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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