mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-12-01 12:24:28 +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])
|
authorize LocalPaymentContext.new(cart, price[:amount])
|
||||||
|
|
||||||
if cart.reservation
|
if cart.reservation
|
||||||
res = on_reservation_success(nil, nil, price[:details], cart)
|
res = on_reservation_success(nil, nil, cart)
|
||||||
elsif cart.subscription
|
elsif cart.subscription
|
||||||
res = on_subscription_success(nil, nil, price[:details], cart)
|
res = on_subscription_success(nil, nil, cart)
|
||||||
end
|
end
|
||||||
|
|
||||||
render res
|
render res
|
||||||
|
@ -12,6 +12,8 @@ class API::PaymentsController < API::ApiController
|
|||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
|
def post_save(_gateway_item_id, _gateway_item_type); end
|
||||||
|
|
||||||
def post_reservation_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
|
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
|
raise InvalidGroupError if plan.group_id != current_user.group_id
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_reservation_success(gateway_item_id, gateway_item_type, details, cart)
|
def on_success(gateway_item_id, gateway_item_type, cart)
|
||||||
@reservation = cart.reservation.to_reservation
|
cart.pay_and_save(gateway_item_id, gateway_item_type)
|
||||||
@reservation.plan_id = cart.subscription.plan.id if cart.subscription
|
|
||||||
|
|
||||||
payment_method = cart.payment_method || 'card'
|
|
||||||
user_id = if current_user.admin? || current_user.manager?
|
|
||||||
cart.customer.id
|
|
||||||
else
|
|
||||||
current_user.id
|
|
||||||
end
|
end
|
||||||
is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id)
|
|
||||||
.pay_and_save(@reservation,
|
def on_reservation_success(gateway_item_id, gateway_item_type, cart)
|
||||||
payment_details: details,
|
is_reserve = on_success(gateway_item_id, gateway_item_type, cart)
|
||||||
payment_id: gateway_item_id,
|
|
||||||
payment_type: gateway_item_type,
|
|
||||||
schedule: cart.payment_schedule.requested,
|
|
||||||
payment_method: payment_method)
|
|
||||||
post_reservation_save(gateway_item_id, gateway_item_type)
|
post_reservation_save(gateway_item_id, gateway_item_type)
|
||||||
|
|
||||||
if is_reserve
|
if is_reserve
|
||||||
SubscriptionExtensionAfterReservation.new(@reservation).extend_subscription_if_eligible
|
|
||||||
|
|
||||||
{ template: 'api/reservations/show', status: :created, location: @reservation }
|
{ template: 'api/reservations/show', status: :created, location: @reservation }
|
||||||
else
|
else
|
||||||
{ json: @reservation.errors, status: :unprocessable_entity }
|
{ json: @reservation.errors, status: :unprocessable_entity }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_subscription_success(gateway_item_id, gateway_item_type, details, cart)
|
def on_subscription_success(gateway_item_id, gateway_item_type, cart)
|
||||||
@subscription = cart.subscription.to_subscription
|
is_subscribe = on_success(gateway_item_id, gateway_item_type, cart)
|
||||||
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')
|
|
||||||
|
|
||||||
post_subscription_save(gateway_item_id, gateway_item_type)
|
post_subscription_save(gateway_item_id, gateway_item_type)
|
||||||
|
|
||||||
if is_subscribe
|
if is_subscribe
|
||||||
|
@ -48,13 +48,12 @@ class API::PayzenController < API::PaymentsController
|
|||||||
order = client.get(params[:order_id], operation_type: 'DEBIT')
|
order = client.get(params[:order_id], operation_type: 'DEBIT')
|
||||||
|
|
||||||
cart = shopping_cart
|
cart = shopping_cart
|
||||||
amount = debit_amount(cart)
|
|
||||||
|
|
||||||
if order['answer']['transactions'].first['status'] == 'PAID'
|
if order['answer']['transactions'].first['status'] == 'PAID'
|
||||||
if cart.reservation
|
if cart.reservation
|
||||||
res = on_reservation_success(params[:order_id], amount[:details], cart)
|
res = on_reservation_success(params[:order_id], cart)
|
||||||
elsif cart.subscription
|
elsif cart.subscription
|
||||||
res = on_subscription_success(params[:order_id], amount[:details], cart)
|
res = on_subscription_success(params[:order_id], cart)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -65,12 +64,12 @@ class API::PayzenController < API::PaymentsController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def on_reservation_success(order_id, details, cart)
|
def on_reservation_success(order_id, cart)
|
||||||
super(order_id, 'PayZen::Order', details, cart)
|
super(order_id, 'PayZen::Order', cart)
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_subscription_success(order_id, details, cart)
|
def on_subscription_success(order_id, cart)
|
||||||
super(order_id, 'PayZen::Order', details, cart)
|
super(order_id, 'PayZen::Order', cart)
|
||||||
end
|
end
|
||||||
|
|
||||||
def error_handling
|
def error_handling
|
||||||
|
@ -12,13 +12,12 @@ class API::StripeController < API::PaymentsController
|
|||||||
def confirm_payment
|
def confirm_payment
|
||||||
render(json: { error: 'Bad gateway or online payment is disabled' }, status: :bad_gateway) and return unless Stripe::Helper.enabled?
|
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
|
intent = nil # stripe's payment intent
|
||||||
res = nil # json of the API answer
|
res = nil # json of the API answer
|
||||||
|
|
||||||
cart = shopping_cart
|
cart = shopping_cart
|
||||||
begin
|
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?
|
if params[:payment_method_id].present?
|
||||||
check_coupon(cart)
|
check_coupon(cart)
|
||||||
check_plan(cart)
|
check_plan(cart)
|
||||||
@ -48,9 +47,9 @@ class API::StripeController < API::PaymentsController
|
|||||||
|
|
||||||
if intent&.status == 'succeeded'
|
if intent&.status == 'succeeded'
|
||||||
if cart.reservation
|
if cart.reservation
|
||||||
res = on_reservation_success(intent, amount[:details], cart)
|
res = on_reservation_success(intent, cart)
|
||||||
elsif cart.subscription
|
elsif cart.subscription
|
||||||
res = on_subscription_success(intent, amount[:details], cart)
|
res = on_subscription_success(intent, cart)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -81,12 +80,11 @@ class API::StripeController < API::PaymentsController
|
|||||||
intent = Stripe::SetupIntent.retrieve(params[:setup_intent_id], api_key: key)
|
intent = Stripe::SetupIntent.retrieve(params[:setup_intent_id], api_key: key)
|
||||||
|
|
||||||
cart = shopping_cart
|
cart = shopping_cart
|
||||||
amount = debit_amount(cart)
|
|
||||||
if intent&.status == 'succeeded'
|
if intent&.status == 'succeeded'
|
||||||
if cart.reservation
|
if cart.reservation
|
||||||
res = on_reservation_success(intent, amount[:details], cart)
|
res = on_reservation_success(intent, cart)
|
||||||
elsif cart.subscription
|
elsif cart.subscription
|
||||||
res = on_subscription_success(intent, amount[:details], cart)
|
res = on_subscription_success(intent, cart)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -128,12 +126,12 @@ class API::StripeController < API::PaymentsController
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_reservation_success(intent, details, cart)
|
def on_reservation_success(intent, cart)
|
||||||
super(intent.id, intent.class.name, details, cart)
|
super(intent.id, intent.class.name, cart)
|
||||||
end
|
end
|
||||||
|
|
||||||
def on_subscription_success(intent, details, cart)
|
def on_subscription_success(intent, cart)
|
||||||
super(intent.id, intent.class.name, details, cart)
|
super(intent.id, intent.class.name, cart)
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_payment_response(intent, res = nil)
|
def generate_payment_response(intent, res = nil)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import apiClient from './clients/api-client';
|
import apiClient from './clients/api-client';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { CartItems } from '../models/payment';
|
import { ShoppingCart } from '../models/payment';
|
||||||
import { User } from '../models/user';
|
import { User } from '../models/user';
|
||||||
import {
|
import {
|
||||||
CheckHashResponse,
|
CheckHashResponse,
|
||||||
@ -17,13 +17,13 @@ export default class PayzenAPI {
|
|||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async chargeCreatePayment(cartItems: CartItems, customer: User): Promise<CreatePaymentResponse> {
|
static async chargeCreatePayment(cart: ShoppingCart, customer: User): Promise<CreatePaymentResponse> {
|
||||||
const res: AxiosResponse<CreatePaymentResponse> = await apiClient.post('/api/payzen/create_payment', { cart_items: cartItems, customer_id: customer.id });
|
const res: AxiosResponse<CreatePaymentResponse> = await apiClient.post('/api/payzen/create_payment', { cart_items: cart, customer_id: customer.id });
|
||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async chargeCreateToken(cartItems: CartItems, customer: User): Promise<CreateTokenResponse> {
|
static async chargeCreateToken(cart: ShoppingCart, customer: User): Promise<CreateTokenResponse> {
|
||||||
const res: AxiosResponse = await apiClient.post('/api/payzen/create_token', { cart_items: cartItems, customer_id: customer.id });
|
const res: AxiosResponse = await apiClient.post('/api/payzen/create_token', { cart_items: cart, customer_id: customer.id });
|
||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,8 +32,8 @@ export default class PayzenAPI {
|
|||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async confirm(orderId: string, cartItems: CartItems): Promise<ConfirmPaymentResponse> {
|
static async confirm(orderId: string, cart: ShoppingCart): Promise<ConfirmPaymentResponse> {
|
||||||
const res: AxiosResponse<ConfirmPaymentResponse> = await apiClient.post('/api/payzen/confirm_payment', { cart_items: cartItems, order_id: orderId });
|
const res: AxiosResponse<ConfirmPaymentResponse> = await apiClient.post('/api/payzen/confirm_payment', { cart_items: cart, order_id: orderId });
|
||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import apiClient from './clients/api-client';
|
import apiClient from './clients/api-client';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { CartItems } from '../models/payment';
|
import { ShoppingCart } from '../models/payment';
|
||||||
import { ComputePriceResult } from '../models/price';
|
import { ComputePriceResult } from '../models/price';
|
||||||
|
|
||||||
export default class PriceAPI {
|
export default class PriceAPI {
|
||||||
static async compute (cartItems: CartItems): Promise<ComputePriceResult> {
|
static async compute (cart: ShoppingCart): Promise<ComputePriceResult> {
|
||||||
const res: AxiosResponse = await apiClient.post(`/api/prices/compute`, cartItems);
|
const res: AxiosResponse = await apiClient.post(`/api/prices/compute`, cart);
|
||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import apiClient from './clients/api-client';
|
import apiClient from './clients/api-client';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { CartItems, IntentConfirmation, PaymentConfirmation, UpdateCardResponse } from '../models/payment';
|
import { ShoppingCart, IntentConfirmation, PaymentConfirmation, UpdateCardResponse } from '../models/payment';
|
||||||
|
|
||||||
export default class StripeAPI {
|
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`, {
|
const res: AxiosResponse = await apiClient.post(`/api/stripe/confirm_payment`, {
|
||||||
payment_method_id: stp_payment_method_id,
|
payment_method_id: stp_payment_method_id,
|
||||||
cart_items
|
cart_items
|
||||||
@ -17,7 +17,7 @@ export default class StripeAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO, type the response
|
// 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`, {
|
const res: AxiosResponse = await apiClient.post(`/api/stripe/confirm_payment_schedule`, {
|
||||||
setup_intent_id,
|
setup_intent_id,
|
||||||
cart_items
|
cart_items
|
||||||
|
@ -6,7 +6,7 @@ import { FabModal, ModalSize } from '../base/fab-modal';
|
|||||||
import { HtmlTranslate } from '../base/html-translate';
|
import { HtmlTranslate } from '../base/html-translate';
|
||||||
import { CustomAssetName } from '../../models/custom-asset';
|
import { CustomAssetName } from '../../models/custom-asset';
|
||||||
import { IFablab } from '../../models/fablab';
|
import { IFablab } from '../../models/fablab';
|
||||||
import { CartItems } from '../../models/payment';
|
import { ShoppingCart } from '../../models/payment';
|
||||||
import { PaymentSchedule } from '../../models/payment-schedule';
|
import { PaymentSchedule } from '../../models/payment-schedule';
|
||||||
import { User } from '../../models/user';
|
import { User } from '../../models/user';
|
||||||
import CustomAssetAPI from '../../api/custom-asset';
|
import CustomAssetAPI from '../../api/custom-asset';
|
||||||
@ -24,7 +24,7 @@ export interface GatewayFormProps {
|
|||||||
operator: User,
|
operator: User,
|
||||||
className?: string,
|
className?: string,
|
||||||
paymentSchedule?: boolean,
|
paymentSchedule?: boolean,
|
||||||
cartItems?: CartItems,
|
cart?: ShoppingCart,
|
||||||
formId: string,
|
formId: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ interface AbstractPaymentModalProps {
|
|||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
toggleModal: () => void,
|
toggleModal: () => void,
|
||||||
afterSuccess: (result: any) => void,
|
afterSuccess: (result: any) => void,
|
||||||
cartItems: CartItems,
|
cart: ShoppingCart,
|
||||||
currentUser: User,
|
currentUser: User,
|
||||||
schedule: PaymentSchedule,
|
schedule: PaymentSchedule,
|
||||||
customer: User,
|
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
|
* 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
|
* @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
|
// customer's wallet
|
||||||
const [wallet, setWallet] = useState(null);
|
const [wallet, setWallet] = useState(null);
|
||||||
// server-computed price with all details
|
// server-computed price with all details
|
||||||
@ -80,17 +80,17 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
|
|||||||
* - Refresh the remaining price
|
* - Refresh the remaining price
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!cartItems) return;
|
if (!cart) return;
|
||||||
WalletAPI.getByUser(cartItems.customer_id).then((wallet) => {
|
WalletAPI.getByUser(cart.customer_id).then((wallet) => {
|
||||||
setWallet(wallet);
|
setWallet(wallet);
|
||||||
PriceAPI.compute(cartItems).then((res) => {
|
PriceAPI.compute(cart).then((res) => {
|
||||||
setPrice(res);
|
setPrice(res);
|
||||||
const wLib = new WalletLib(wallet);
|
const wLib = new WalletLib(wallet);
|
||||||
setRemainingPrice(wLib.computeRemainingPrice(res.price));
|
setRemainingPrice(wLib.computeRemainingPrice(res.price));
|
||||||
setReady(true);
|
setReady(true);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, [cartItems]);
|
}, [cart]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if there is currently an error to display
|
* Check if there is currently an error to display
|
||||||
@ -170,14 +170,14 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
|
|||||||
customFooter={logoFooter}
|
customFooter={logoFooter}
|
||||||
className={`payment-modal ${className ? className : ''}`}>
|
className={`payment-modal ${className ? className : ''}`}>
|
||||||
{ready && <div>
|
{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}
|
<GatewayForm onSubmit={handleSubmit}
|
||||||
onSuccess={handleFormSuccess}
|
onSuccess={handleFormSuccess}
|
||||||
onError={handleFormError}
|
onError={handleFormError}
|
||||||
operator={currentUser}
|
operator={currentUser}
|
||||||
className={`gateway-form ${formClassName ? formClassName : ''}`}
|
className={`gateway-form ${formClassName ? formClassName : ''}`}
|
||||||
formId={formId}
|
formId={formId}
|
||||||
cartItems={cartItems}
|
cart={cart}
|
||||||
customer={customer}
|
customer={customer}
|
||||||
paymentSchedule={isPaymentSchedule()}>
|
paymentSchedule={isPaymentSchedule()}>
|
||||||
{hasErrors() && <div className="payment-errors">
|
{hasErrors() && <div className="payment-errors">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { ReactElement, ReactNode } from 'react';
|
import React, { ReactElement, ReactNode } from 'react';
|
||||||
import { IApplication } from '../../models/application';
|
import { IApplication } from '../../models/application';
|
||||||
import { CartItems } from '../../models/payment';
|
import { ShoppingCart } from '../../models/payment';
|
||||||
import { User } from '../../models/user';
|
import { User } from '../../models/user';
|
||||||
import { PaymentSchedule } from '../../models/payment-schedule';
|
import { PaymentSchedule } from '../../models/payment-schedule';
|
||||||
import { Loader } from '../base/loader';
|
import { Loader } from '../base/loader';
|
||||||
@ -16,7 +16,7 @@ interface PaymentModalProps {
|
|||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
toggleModal: () => void,
|
toggleModal: () => void,
|
||||||
afterSuccess: (result: any) => void,
|
afterSuccess: (result: any) => void,
|
||||||
cartItems: CartItems,
|
cart: ShoppingCart,
|
||||||
currentUser: User,
|
currentUser: User,
|
||||||
schedule: PaymentSchedule,
|
schedule: PaymentSchedule,
|
||||||
customer: User
|
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
|
* This component open a modal dialog for the configured payment gateway, allowing the user to input his card data
|
||||||
* to process an online payment.
|
* 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();
|
const gateway = paymentGateway.read();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,7 +39,7 @@ const PaymentModal: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterS
|
|||||||
return <StripeModal isOpen={isOpen}
|
return <StripeModal isOpen={isOpen}
|
||||||
toggleModal={toggleModal}
|
toggleModal={toggleModal}
|
||||||
afterSuccess={afterSuccess}
|
afterSuccess={afterSuccess}
|
||||||
cartItems={cartItems}
|
cart={cart}
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
schedule={schedule}
|
schedule={schedule}
|
||||||
customer={customer} />
|
customer={customer} />
|
||||||
@ -52,7 +52,7 @@ const PaymentModal: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterS
|
|||||||
return <PayZenModal isOpen={isOpen}
|
return <PayZenModal isOpen={isOpen}
|
||||||
toggleModal={toggleModal}
|
toggleModal={toggleModal}
|
||||||
afterSuccess={afterSuccess}
|
afterSuccess={afterSuccess}
|
||||||
cartItems={cartItems}
|
cart={cart}
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
schedule={schedule}
|
schedule={schedule}
|
||||||
customer={customer} />
|
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 (
|
return (
|
||||||
<Loader>
|
<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>
|
</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.
|
* 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}.
|
* 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 PayZenKR = useRef<KryptonClient>(null);
|
||||||
const [loadingClass, setLoadingClass] = useState<'hidden' | 'loader' | 'loader-overlay'>('loader');
|
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);
|
.then(({ KR }) => PayZenKR.current = KR);
|
||||||
}).catch(error => onError(error));
|
}).catch(error => onError(error));
|
||||||
});
|
});
|
||||||
}, [cartItems, paymentSchedule, customer]);
|
}, [cart, paymentSchedule, customer]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ask the API to create the form token.
|
* 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> => {
|
const createToken = async (): Promise<CreateTokenResponse> => {
|
||||||
if (paymentSchedule) {
|
if (paymentSchedule) {
|
||||||
return await PayzenAPI.chargeCreateToken(cartItems, customer);
|
return await PayzenAPI.chargeCreateToken(cart, customer);
|
||||||
} else {
|
} 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];
|
const transaction = event.clientAnswer.transactions[0];
|
||||||
|
|
||||||
if (event.clientAnswer.orderStatus === 'PAID') {
|
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(() => {
|
PayZenKR.current.removeForms().then(() => {
|
||||||
onSuccess(confirmation);
|
onSuccess(confirmation);
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { FunctionComponent, ReactNode } from 'react';
|
import React, { FunctionComponent, ReactNode } from 'react';
|
||||||
import { GatewayFormProps, AbstractPaymentModal } from '../abstract-payment-modal';
|
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 { PaymentSchedule } from '../../../models/payment-schedule';
|
||||||
import { User } from '../../../models/user';
|
import { User } from '../../../models/user';
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ interface PayZenModalProps {
|
|||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
toggleModal: () => void,
|
toggleModal: () => void,
|
||||||
afterSuccess: (result: PaymentConfirmation) => void,
|
afterSuccess: (result: PaymentConfirmation) => void,
|
||||||
cartItems: CartItems,
|
cart: ShoppingCart,
|
||||||
currentUser: User,
|
currentUser: User,
|
||||||
schedule: PaymentSchedule,
|
schedule: PaymentSchedule,
|
||||||
customer: User
|
customer: User
|
||||||
@ -27,7 +27,7 @@ interface PayZenModalProps {
|
|||||||
* This component should not be called directly. Prefer using <PaymentModal> which can handle the configuration
|
* This component should not be called directly. Prefer using <PaymentModal> which can handle the configuration
|
||||||
* of a different payment gateway.
|
* 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.
|
* 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
|
* 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 (
|
return (
|
||||||
<PayzenForm onSubmit={onSubmit}
|
<PayzenForm onSubmit={onSubmit}
|
||||||
onSuccess={onSuccess}
|
onSuccess={onSuccess}
|
||||||
@ -52,7 +52,7 @@ export const PayZenModal: React.FC<PayZenModalProps> = ({ isOpen, toggleModal, a
|
|||||||
customer={customer}
|
customer={customer}
|
||||||
operator={operator}
|
operator={operator}
|
||||||
formId={formId}
|
formId={formId}
|
||||||
cartItems={cartItems}
|
cart={cart}
|
||||||
className={className}
|
className={className}
|
||||||
paymentSchedule={paymentSchedule}>
|
paymentSchedule={paymentSchedule}>
|
||||||
{children}
|
{children}
|
||||||
@ -68,7 +68,7 @@ export const PayZenModal: React.FC<PayZenModalProps> = ({ isOpen, toggleModal, a
|
|||||||
formClassName="payzen-form"
|
formClassName="payzen-form"
|
||||||
className="payzen-modal"
|
className="payzen-modal"
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
cartItems={cartItems}
|
cart={cart}
|
||||||
customer={customer}
|
customer={customer}
|
||||||
afterSuccess={afterSuccess}
|
afterSuccess={afterSuccess}
|
||||||
schedule={schedule}
|
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.
|
* 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}.
|
* 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');
|
const { t } = useTranslation('shared');
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ export const StripeForm: React.FC<StripeFormProps> = ({ onSubmit, onSuccess, onE
|
|||||||
try {
|
try {
|
||||||
if (!paymentSchedule) {
|
if (!paymentSchedule) {
|
||||||
// process the normal payment pipeline, including SCA validation
|
// 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);
|
await handleServerConfirmation(res);
|
||||||
} else {
|
} else {
|
||||||
// we start by associating the payment method with the user
|
// 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);
|
onError(error.message);
|
||||||
} else {
|
} else {
|
||||||
// then we confirm the payment schedule
|
// then we confirm the payment schedule
|
||||||
const res = await StripeAPI.confirmPaymentSchedule(setupIntent.id, cartItems);
|
const res = await StripeAPI.confirmPaymentSchedule(setupIntent.id, cart);
|
||||||
onSuccess(res);
|
onSuccess(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -100,7 +100,7 @@ export const StripeForm: React.FC<StripeFormProps> = ({ onSubmit, onSuccess, onE
|
|||||||
// The card action has been handled
|
// The card action has been handled
|
||||||
// The PaymentIntent can be confirmed again on the server
|
// The PaymentIntent can be confirmed again on the server
|
||||||
try {
|
try {
|
||||||
const confirmation = await StripeAPI.confirm(result.paymentIntent.id, cartItems);
|
const confirmation = await StripeAPI.confirm(result.paymentIntent.id, cart);
|
||||||
await handleServerConfirmation(confirmation);
|
await handleServerConfirmation(confirmation);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onError(e);
|
onError(e);
|
||||||
|
@ -3,7 +3,7 @@ import { SetupIntent } from '@stripe/stripe-js';
|
|||||||
import { StripeElements } from './stripe-elements';
|
import { StripeElements } from './stripe-elements';
|
||||||
import { StripeForm } from './stripe-form';
|
import { StripeForm } from './stripe-form';
|
||||||
import { GatewayFormProps, AbstractPaymentModal } from '../abstract-payment-modal';
|
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 { PaymentSchedule } from '../../../models/payment-schedule';
|
||||||
import { User } from '../../../models/user';
|
import { User } from '../../../models/user';
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ interface StripeModalProps {
|
|||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
toggleModal: () => void,
|
toggleModal: () => void,
|
||||||
afterSuccess: (result: SetupIntent|PaymentConfirmation) => void,
|
afterSuccess: (result: SetupIntent|PaymentConfirmation) => void,
|
||||||
cartItems: CartItems,
|
cart: ShoppingCart,
|
||||||
currentUser: User,
|
currentUser: User,
|
||||||
schedule: PaymentSchedule,
|
schedule: PaymentSchedule,
|
||||||
customer: User
|
customer: User
|
||||||
@ -29,7 +29,7 @@ interface StripeModalProps {
|
|||||||
* This component should not be called directly. Prefer using <PaymentModal> which can handle the configuration
|
* This component should not be called directly. Prefer using <PaymentModal> which can handle the configuration
|
||||||
* of a different payment gateway.
|
* 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.
|
* 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
|
* 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 (
|
return (
|
||||||
<StripeElements>
|
<StripeElements>
|
||||||
<StripeForm onSubmit={onSubmit}
|
<StripeForm onSubmit={onSubmit}
|
||||||
@ -56,7 +56,7 @@ export const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, a
|
|||||||
operator={operator}
|
operator={operator}
|
||||||
className={className}
|
className={className}
|
||||||
formId={formId}
|
formId={formId}
|
||||||
cartItems={cartItems}
|
cart={cart}
|
||||||
customer={customer}
|
customer={customer}
|
||||||
paymentSchedule={paymentSchedule}>
|
paymentSchedule={paymentSchedule}>
|
||||||
{children}
|
{children}
|
||||||
@ -73,7 +73,7 @@ export const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, a
|
|||||||
formId="stripe-form"
|
formId="stripe-form"
|
||||||
formClassName="stripe-form"
|
formClassName="stripe-form"
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
cartItems={cartItems}
|
cart={cart}
|
||||||
customer={customer}
|
customer={customer}
|
||||||
afterSuccess={afterSuccess}
|
afterSuccess={afterSuccess}
|
||||||
schedule={schedule}
|
schedule={schedule}
|
||||||
|
@ -8,7 +8,7 @@ import { User } from '../models/user';
|
|||||||
import { Wallet } from '../models/wallet';
|
import { Wallet } from '../models/wallet';
|
||||||
import { IFablab } from '../models/fablab';
|
import { IFablab } from '../models/fablab';
|
||||||
import WalletLib from '../lib/wallet';
|
import WalletLib from '../lib/wallet';
|
||||||
import { CartItems } from '../models/payment';
|
import { ShoppingCart } from '../models/payment';
|
||||||
import { Reservation } from '../models/reservation';
|
import { Reservation } from '../models/reservation';
|
||||||
import { SubscriptionRequest } from '../models/subscription';
|
import { SubscriptionRequest } from '../models/subscription';
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ declare var Application: IApplication;
|
|||||||
declare var Fablab: IFablab;
|
declare var Fablab: IFablab;
|
||||||
|
|
||||||
interface WalletInfoProps {
|
interface WalletInfoProps {
|
||||||
cartItems: CartItems,
|
cart: ShoppingCart,
|
||||||
currentUser: User,
|
currentUser: User,
|
||||||
wallet: Wallet,
|
wallet: Wallet,
|
||||||
price: number,
|
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
|
* 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 { t } = useTranslation('shared');
|
||||||
const [remainingPrice, setRemainingPrice] = useState(0);
|
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.
|
* 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 => {
|
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.
|
* 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?
|
* Does the current cart contains a payment schedule?
|
||||||
*/
|
*/
|
||||||
const isPaymentSchedule = (): boolean => {
|
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
|
* Return the human-readable name of the item currently bought with the wallet
|
||||||
*/
|
*/
|
||||||
const getPriceItem = (): string => {
|
const getPriceItem = (): string => {
|
||||||
let item = 'other';
|
let item = 'other';
|
||||||
if (cartItems.reservation) {
|
if (cart.items.find(i => 'reservation' in i)) {
|
||||||
item = 'reservation';
|
item = 'reservation';
|
||||||
} else if (cartItems.subscription) {
|
} else if (cart.items.find(i => 'subscription' in i)) {
|
||||||
if (cartItems.payment_schedule) {
|
if (cart.payment_schedule) {
|
||||||
item = 'first_deadline';
|
item = 'first_deadline';
|
||||||
} else item = 'subscription';
|
} 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 (
|
return (
|
||||||
<Loader>
|
<Loader>
|
||||||
<WalletInfo currentUser={currentUser} cartItems={cartItems} price={price} wallet={wallet}/>
|
<WalletInfo currentUser={currentUser} cart={cart} price={price} wallet={wallet}/>
|
||||||
</Loader>
|
</Loader>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Application.Components.component('walletInfo', react2angular(WalletInfoWrapper, ['currentUser', 'price', 'cartItems', 'wallet']));
|
Application.Components.component('walletInfo', react2angular(WalletInfoWrapper, ['currentUser', 'price', 'cart', 'wallet']));
|
||||||
|
@ -335,6 +335,8 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
|||||||
$scope.validReserveEvent = function () {
|
$scope.validReserveEvent = function () {
|
||||||
const cartItems = {
|
const cartItems = {
|
||||||
customer_id: $scope.ctrl.member.id,
|
customer_id: $scope.ctrl.member.id,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
reservation: {
|
reservation: {
|
||||||
reservable_id: $scope.event.id,
|
reservable_id: $scope.event.id,
|
||||||
reservable_type: 'Event',
|
reservable_type: 'Event',
|
||||||
@ -343,8 +345,10 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
|||||||
tickets_attributes: []
|
tickets_attributes: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
// a single slot is used for events
|
// 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,
|
start_at: $scope.event.start_date,
|
||||||
end_at: $scope.event.end_date,
|
end_at: $scope.event.end_date,
|
||||||
availability_id: $scope.event.availability.id
|
availability_id: $scope.event.availability.id
|
||||||
@ -353,7 +357,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
|||||||
for (let price_id in $scope.reserve.tickets) {
|
for (let price_id in $scope.reserve.tickets) {
|
||||||
if (Object.prototype.hasOwnProperty.call($scope.reserve.tickets, price_id)) {
|
if (Object.prototype.hasOwnProperty.call($scope.reserve.tickets, price_id)) {
|
||||||
const seats = $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,
|
event_price_category_id: price_id,
|
||||||
booked: seats
|
booked: seats
|
||||||
});
|
});
|
||||||
@ -363,7 +367,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
|||||||
$scope.attempting = true;
|
$scope.attempting = true;
|
||||||
// save the reservation to the API
|
// save the reservation to the API
|
||||||
return Reservation.save(cartItems, function (reservation) {
|
return Reservation.save(cartItems, function (reservation) {
|
||||||
// reservation successfull
|
// reservation successful
|
||||||
afterPayment(reservation);
|
afterPayment(reservation);
|
||||||
return $scope.attempting = false;
|
return $scope.attempting = false;
|
||||||
}
|
}
|
||||||
@ -657,7 +661,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
|||||||
* @param reservation {Object} as returned by mkReservation()
|
* @param reservation {Object} as returned by mkReservation()
|
||||||
* @param coupon {Object} Coupon as returned from the API
|
* @param coupon {Object} Coupon as returned from the API
|
||||||
* @param paymentMethod {string} 'card' | ''
|
* @param paymentMethod {string} 'card' | ''
|
||||||
* @return {CartItems}
|
* @return {ShoppingCart}
|
||||||
*/
|
*/
|
||||||
const mkCartItems = function (reservation, coupon, paymentMethod = '') {
|
const mkCartItems = function (reservation, coupon, paymentMethod = '') {
|
||||||
return {
|
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 items {Array<{reservation:{reservable_type: string, reservable_id: string, slots_attributes: []}}|{subscription: {plan_id: number}}>}
|
||||||
* @param paymentMethod {string}
|
* @param paymentMethod {string}
|
||||||
* @return {CartItems}
|
* @return {ShoppingCart}
|
||||||
*/
|
*/
|
||||||
const mkCartItems = function (items, paymentMethod = '') {
|
const mkCartItems = function (items, paymentMethod = '') {
|
||||||
return {
|
return {
|
||||||
|
@ -19,9 +19,11 @@ export enum PaymentMethod {
|
|||||||
Other = ''
|
Other = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CartItems {
|
export type CartItem = { reservation: Reservation }|{ subscription: SubscriptionRequest };
|
||||||
|
|
||||||
|
export interface ShoppingCart {
|
||||||
customer_id: number,
|
customer_id: number,
|
||||||
items: Array<Reservation|SubscriptionRequest>,
|
items: Array<CartItem>,
|
||||||
coupon_code?: string,
|
coupon_code?: string,
|
||||||
payment_schedule?: boolean,
|
payment_schedule?: boolean,
|
||||||
payment_method: PaymentMethod
|
payment_method: PaymentMethod
|
||||||
|
@ -209,7 +209,7 @@
|
|||||||
<payment-modal is-open="onlinePayment.showModal"
|
<payment-modal is-open="onlinePayment.showModal"
|
||||||
toggle-modal="toggleOnlinePaymentModal"
|
toggle-modal="toggleOnlinePaymentModal"
|
||||||
after-success="afterOnlinePaymentSuccess"
|
after-success="afterOnlinePaymentSuccess"
|
||||||
cart-items="onlinePayment.cartItems"
|
cart="onlinePayment.cartItems"
|
||||||
current-user="currentUser"
|
current-user="currentUser"
|
||||||
customer="ctrl.member"/>
|
customer="ctrl.member"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -203,7 +203,7 @@
|
|||||||
<payment-modal is-open="onlinePayment.showModal"
|
<payment-modal is-open="onlinePayment.showModal"
|
||||||
toggle-modal="toggleOnlinePaymentModal"
|
toggle-modal="toggleOnlinePaymentModal"
|
||||||
after-success="afterOnlinePaymentSuccess"
|
after-success="afterOnlinePaymentSuccess"
|
||||||
cart-items="onlinePayment.cartItems"
|
cart="onlinePayment.cartItems"
|
||||||
current-user="currentUser"
|
current-user="currentUser"
|
||||||
customer="user"
|
customer="user"
|
||||||
schedule="schedule.payment_schedule"/>
|
schedule="schedule.payment_schedule"/>
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<wallet-info current-user="currentUser"
|
<wallet-info current-user="currentUser"
|
||||||
cart-items="cartItems"
|
cart="cartItems"
|
||||||
price="price"
|
price="price"
|
||||||
wallet="wallet"/>
|
wallet="wallet"/>
|
||||||
</div>
|
</div>
|
||||||
@ -52,7 +52,7 @@
|
|||||||
<payment-modal is-open="isOpenOnlinePaymentModal"
|
<payment-modal is-open="isOpenOnlinePaymentModal"
|
||||||
toggle-modal="toggleOnlinePaymentModal"
|
toggle-modal="toggleOnlinePaymentModal"
|
||||||
after-success="afterCreatePaymentSchedule"
|
after-success="afterCreatePaymentSchedule"
|
||||||
cart-items="cartItems"
|
cart="cartItems"
|
||||||
current-user="currentUser"
|
current-user="currentUser"
|
||||||
schedule="schedule"
|
schedule="schedule"
|
||||||
customer="user"
|
customer="user"
|
||||||
|
@ -12,4 +12,6 @@ class CartItem::BaseItem
|
|||||||
def name
|
def name
|
||||||
''
|
''
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_object; end
|
||||||
end
|
end
|
||||||
|
@ -34,13 +34,14 @@ class CartItem::EventReservation < CartItem::Reservation
|
|||||||
{ elements: elements, amount: total }
|
{ elements: elements, amount: total }
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_reservation
|
def to_object
|
||||||
::Reservation.new(
|
::Reservation.new(
|
||||||
reservable_id: @reservable.id,
|
reservable_id: @reservable.id,
|
||||||
reservable_type: Event.name,
|
reservable_type: Event.name,
|
||||||
slots_attributes: slots_params,
|
slots_attributes: slots_params,
|
||||||
tickets_attributes: tickets_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
|
end
|
||||||
|
|
||||||
|
@ -12,11 +12,12 @@ class CartItem::MachineReservation < CartItem::Reservation
|
|||||||
@new_subscription = new_subscription
|
@new_subscription = new_subscription
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_reservation
|
def to_object
|
||||||
::Reservation.new(
|
::Reservation.new(
|
||||||
reservable_id: @reservable.id,
|
reservable_id: @reservable.id,
|
||||||
reservable_type: Machine.name,
|
reservable_type: Machine.name,
|
||||||
slots_attributes: slots_params
|
slots_attributes: slots_params,
|
||||||
|
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -33,10 +33,6 @@ class CartItem::Reservation < CartItem::BaseItem
|
|||||||
@reservable.name
|
@reservable.name
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_reservation
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def credits
|
def credits
|
||||||
|
@ -12,11 +12,12 @@ class CartItem::SpaceReservation < CartItem::Reservation
|
|||||||
@new_subscription = new_subscription
|
@new_subscription = new_subscription
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_reservation
|
def to_object
|
||||||
::Reservation.new(
|
::Reservation.new(
|
||||||
reservable_id: @reservable.id,
|
reservable_id: @reservable.id,
|
||||||
reservable_type: Space.name,
|
reservable_type: Space.name,
|
||||||
slots_attributes: slots_params
|
slots_attributes: slots_params,
|
||||||
|
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -4,10 +4,11 @@
|
|||||||
class CartItem::Subscription < CartItem::BaseItem
|
class CartItem::Subscription < CartItem::BaseItem
|
||||||
attr_reader :plan
|
attr_reader :plan
|
||||||
|
|
||||||
def initialize(plan)
|
def initialize(plan, customer)
|
||||||
raise TypeError unless plan.is_a? Plan
|
raise TypeError unless plan.is_a? Plan
|
||||||
|
|
||||||
@plan = plan
|
@plan = plan
|
||||||
|
@customer = customer
|
||||||
end
|
end
|
||||||
|
|
||||||
def price
|
def price
|
||||||
@ -21,9 +22,10 @@ class CartItem::Subscription < CartItem::BaseItem
|
|||||||
@plan.name
|
@plan.name
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_subscription
|
def to_object
|
||||||
Subscription.new(
|
Subscription.new(
|
||||||
plan_id: @plan.id
|
plan_id: @plan.id,
|
||||||
|
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -32,11 +32,12 @@ class CartItem::TrainingReservation < CartItem::Reservation
|
|||||||
{ elements: elements, amount: amount }
|
{ elements: elements, amount: amount }
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_reservation
|
def to_object
|
||||||
::Reservation.new(
|
::Reservation.new(
|
||||||
reservable_id: @reservable.id,
|
reservable_id: @reservable.id,
|
||||||
reservable_type: Training.name,
|
reservable_type: Training.name,
|
||||||
slots_attributes: slots_params
|
slots_attributes: slots_params,
|
||||||
|
statistic_profile_id: StatisticProfile.find_by(user: @customer).id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -23,24 +23,17 @@ class Reservation < ApplicationRecord
|
|||||||
validates_presence_of :reservable_id, :reservable_type
|
validates_presence_of :reservable_id, :reservable_type
|
||||||
validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) }
|
validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) }
|
||||||
validate :training_not_fully_reserved, if: -> { reservable.is_a?(Training) }
|
validate :training_not_fully_reserved, if: -> { reservable.is_a?(Training) }
|
||||||
|
validate :slots_not_locked
|
||||||
validates_with ReservationSlotSubscriptionValidator
|
validates_with ReservationSlotSubscriptionValidator
|
||||||
|
|
||||||
attr_accessor :plan_id, :subscription
|
attr_accessor :plan_id, :subscription
|
||||||
|
|
||||||
after_commit :notify_member_create_reservation, on: :create
|
after_commit :notify_member_create_reservation, on: :create
|
||||||
after_commit :notify_admin_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' }
|
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
|
## Generate the subscription associated with for the current reservation
|
||||||
def generate_subscription
|
def generate_subscription
|
||||||
return unless plan_id
|
return unless plan_id
|
||||||
@ -50,13 +43,6 @@ class Reservation < ApplicationRecord
|
|||||||
subscription
|
subscription
|
||||||
end
|
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
|
# @param canceled if true, count the number of seats for this reservation, including canceled seats
|
||||||
def total_booked_seats(canceled: false)
|
def total_booked_seats(canceled: false)
|
||||||
# cases:
|
# cases:
|
||||||
@ -106,6 +92,21 @@ class Reservation < ApplicationRecord
|
|||||||
errors.add(:training, 'already fully reserved') if Availability.find(slot.availability_id).completed?
|
errors.add(:training, 'already fully reserved') if Availability.find(slot.availability_id).completed?
|
||||||
end
|
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
|
def notify_member_create_reservation
|
||||||
NotificationCenter.call type: 'notify_member_create_reservation',
|
NotificationCenter.call type: 'notify_member_create_reservation',
|
||||||
receiver: user,
|
receiver: user,
|
||||||
|
@ -2,16 +2,18 @@
|
|||||||
|
|
||||||
# Stores data about a shopping data
|
# Stores data about a shopping data
|
||||||
class ShoppingCart
|
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 items {Array<CartItem::BaseItem>}
|
||||||
# @param coupon {CartItem::Coupon}
|
# @param coupon {CartItem::Coupon}
|
||||||
# @param payment_schedule {CartItem::PaymentSchedule}
|
# @param payment_schedule {CartItem::PaymentSchedule}
|
||||||
# @param customer {User}
|
# @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
|
raise TypeError unless customer.is_a? User
|
||||||
|
|
||||||
@customer = customer
|
@customer = customer
|
||||||
|
@operator = operator
|
||||||
@payment_method = payment_method
|
@payment_method = payment_method
|
||||||
@items = items
|
@items = items
|
||||||
@coupon = coupon
|
@coupon = coupon
|
||||||
@ -50,4 +52,44 @@ class ShoppingCart
|
|||||||
schedule: schedule_info[:schedule]
|
schedule: schedule_info[:schedule]
|
||||||
}
|
}
|
||||||
end
|
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
|
end
|
||||||
|
@ -16,23 +16,11 @@ class Subscription < ApplicationRecord
|
|||||||
validates_with SubscriptionGroupValidator
|
validates_with SubscriptionGroupValidator
|
||||||
|
|
||||||
# creation
|
# creation
|
||||||
|
before_create :set_expiration_date
|
||||||
after_save :notify_member_subscribed_plan
|
after_save :notify_member_subscribed_plan
|
||||||
after_save :notify_admin_subscribed_plan
|
after_save :notify_admin_subscribed_plan
|
||||||
after_save :notify_partner_subscribed_plan, if: :of_partner_plan?
|
after_save :notify_partner_subscribed_plan, if: :of_partner_plan?
|
||||||
|
after_commit :update_credits, on: :create
|
||||||
##
|
|
||||||
# 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
|
|
||||||
|
|
||||||
def generate_and_save_invoice(operator_profile_id)
|
def generate_and_save_invoice(operator_profile_id)
|
||||||
generate_invoice(operator_profile_id).save
|
generate_invoice(operator_profile_id).save
|
||||||
@ -148,4 +136,9 @@ class Subscription < ApplicationRecord
|
|||||||
def of_partner_plan?
|
def of_partner_plan?
|
||||||
plan.is_a?(PartnerPlan)
|
plan.is_a?(PartnerPlan)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# init the user's credits
|
||||||
|
def update_credits
|
||||||
|
UsersCredits::Manager.new(user: user).reset_credits
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -8,7 +8,7 @@ class CartService
|
|||||||
|
|
||||||
##
|
##
|
||||||
# For details about the expected hash format
|
# 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)
|
def from_hash(cart_items)
|
||||||
@customer = customer(cart_items)
|
@customer = customer(cart_items)
|
||||||
@ -17,7 +17,7 @@ class CartService
|
|||||||
items = []
|
items = []
|
||||||
cart_items[:items].each do |item|
|
cart_items[:items].each do |item|
|
||||||
if item.keys.first == 'subscription'
|
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'
|
elsif item.keys.first == 'reservation'
|
||||||
items.push(reservable_from_hash(item[:reservation], plan_info))
|
items.push(reservable_from_hash(item[:reservation], plan_info))
|
||||||
end
|
end
|
||||||
@ -28,6 +28,7 @@ class CartService
|
|||||||
|
|
||||||
ShoppingCart.new(
|
ShoppingCart.new(
|
||||||
@customer,
|
@customer,
|
||||||
|
@operator,
|
||||||
coupon,
|
coupon,
|
||||||
schedule,
|
schedule,
|
||||||
cart_items[:payment_method],
|
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
|
@operator_profile_id = operator_profile_id
|
||||||
end
|
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)
|
def extend_subscription(subscription, new_expiration_date, free_days)
|
||||||
return subscription.free_extend(new_expiration_date, @operator_profile_id) if 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
|
details = cart.total
|
||||||
|
|
||||||
payment = if schedule
|
payment = if schedule
|
||||||
generate_schedule(subscription: new_sub,
|
operator = InvoicingProfile.find(operator_profile_id)&.user
|
||||||
total: details[:before_coupon],
|
|
||||||
operator_profile_id: operator_profile_id,
|
PaymentScheduleService.new.create(
|
||||||
user: new_sub.user,
|
new_sub,
|
||||||
|
details[:before_coupon],
|
||||||
|
operator: operator,
|
||||||
payment_method: schedule.payment_method,
|
payment_method: schedule.payment_method,
|
||||||
|
user: new_sub.user,
|
||||||
payment_id: schedule.gateway_payment_mean&.id,
|
payment_id: schedule.gateway_payment_mean&.id,
|
||||||
payment_type: schedule.gateway_payment_mean&.class)
|
payment_type: schedule.gateway_payment_mean&.class
|
||||||
|
)
|
||||||
else
|
else
|
||||||
generate_invoice(subscription,
|
InvoicesService.create(
|
||||||
|
details,
|
||||||
operator_profile_id,
|
operator_profile_id,
|
||||||
details)
|
subscription: new_sub
|
||||||
|
)
|
||||||
end
|
end
|
||||||
payment.save
|
payment.save
|
||||||
payment.post_save(schedule&.gateway_payment_mean&.id)
|
payment.post_save(schedule&.gateway_payment_mean&.id)
|
||||||
@ -95,40 +59,4 @@ class Subscriptions::Subscribe
|
|||||||
end
|
end
|
||||||
false
|
false
|
||||||
end
|
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
|
end
|
||||||
|
@ -7,7 +7,8 @@ class ReservationSlotSubscriptionValidator < ActiveModel::Validator
|
|||||||
if record.user.subscribed_plan && s.availability.plan_ids.include?(record.user.subscribed_plan.id)
|
if record.user.subscribed_plan && s.availability.plan_ids.include?(record.user.subscribed_plan.id)
|
||||||
elsif s.availability.plan_ids.include?(record.plan_id)
|
elsif s.availability.plan_ids.include?(record.plan_id)
|
||||||
else
|
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
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
class SubscriptionGroupValidator < ActiveModel::Validator
|
class SubscriptionGroupValidator < ActiveModel::Validator
|
||||||
def validate(record)
|
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"
|
record.errors[:plan_id] << "This plan is not compatible with the current user's group"
|
||||||
end
|
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);
|
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: -
|
-- Name: projects projects_search_content_trigger; Type: TRIGGER; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
require 'test_helper'
|
require 'test_helper'
|
||||||
|
|
||||||
|
module Reservations; end
|
||||||
|
|
||||||
class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest
|
class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest
|
||||||
setup do
|
setup do
|
||||||
@user_without_subscription = User.members.without_subscription.first
|
@user_without_subscription = User.members.without_subscription.first
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
require 'test_helper'
|
require 'test_helper'
|
||||||
|
|
||||||
|
module Reservations; end
|
||||||
|
|
||||||
class Reservations::CreateTest < ActionDispatch::IntegrationTest
|
class Reservations::CreateTest < ActionDispatch::IntegrationTest
|
||||||
setup do
|
setup do
|
||||||
@user_without_subscription = User.members.without_subscription.first
|
@user_without_subscription = User.members.without_subscription.first
|
||||||
|
Loading…
Reference in New Issue
Block a user