mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-11-29 10:24:20 +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:
parent
2fc0ad1ba0
commit
c7a59c8cb7
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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']));
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
|
@ -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}
|
||||
|
@ -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']));
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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"/>
|
||||
|
@ -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"
|
||||
|
@ -12,4 +12,6 @@ class CartItem::BaseItem
|
||||
def name
|
||||
''
|
||||
end
|
||||
|
||||
def to_object; end
|
||||
end
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -33,10 +33,6 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
@reservable.name
|
||||
end
|
||||
|
||||
def to_reservation
|
||||
nil
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def credits
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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],
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
70
db/migrate/20210521085710_add_object_to_invoice_item.rb
Normal file
70
db/migrate/20210521085710_add_object_to_invoice_item.rb
Normal 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
|
@ -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: -
|
||||
--
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user