mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-19 13:54:25 +01:00
front-end adaptation for interacting with new object[] API
Also: [bug] handle not onnected users on subscription page - fix showing an error message when no gateway is selected
This commit is contained in:
parent
e08157b5f6
commit
81bc22c494
@ -16,6 +16,7 @@
|
||||
- Fix a bug: unable to set date formats during installation
|
||||
- Fix a bug: unable to cancel the upgrade before it begins
|
||||
- Fix a bug: in the admin calendar, the trainings' info panel shows "duration: null minutes"
|
||||
- Fix a bug: on the subscriptions page, not logged-in users do not see the action button
|
||||
- `SUPERADMIN_EMAIL` renamed to `ADMINSYS_EMAIL`
|
||||
- `scripts/run-tests.sh` renamed to `scripts/tests.sh`
|
||||
- [BREAKING CHANGE] GET `open_api/v1/invoices` won't return `stp_invoice_id` OR `stp_payment_intent_id` anymore. The new field `payment_gateway_object` will contain some similar data if the invoice was paid online by card.
|
||||
|
@ -4,11 +4,12 @@ import { ShoppingCart } from '../models/payment';
|
||||
import { User } from '../models/user';
|
||||
import {
|
||||
CheckHashResponse,
|
||||
ConfirmPaymentResponse,
|
||||
CreatePaymentResponse,
|
||||
CreateTokenResponse,
|
||||
SdkTestResponse
|
||||
} from '../models/payzen';
|
||||
import { Invoice } from '../models/invoice';
|
||||
import { PaymentSchedule } from '../models/payment-schedule';
|
||||
|
||||
export default class PayzenAPI {
|
||||
|
||||
@ -32,8 +33,8 @@ export default class PayzenAPI {
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
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 });
|
||||
static async confirm(orderId: string, cart: ShoppingCart): Promise<Invoice|PaymentSchedule> {
|
||||
const res: AxiosResponse<Invoice|PaymentSchedule> = await apiClient.post('/api/payzen/confirm_payment', { cart_items: cart, order_id: orderId });
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { ShoppingCart, IntentConfirmation, PaymentConfirmation, UpdateCardResponse } from '../models/payment';
|
||||
import { PaymentSchedule } from '../models/payment-schedule';
|
||||
import { Invoice } from '../models/invoice';
|
||||
|
||||
export default class StripeAPI {
|
||||
static async confirm (stp_payment_method_id: string, cart_items: ShoppingCart): Promise<PaymentConfirmation> {
|
||||
const res: AxiosResponse = await apiClient.post(`/api/stripe/confirm_payment`, {
|
||||
static async confirm (stp_payment_method_id: string, cart_items: ShoppingCart): Promise<PaymentConfirmation|Invoice> {
|
||||
const res: AxiosResponse<PaymentConfirmation|Invoice> = await apiClient.post(`/api/stripe/confirm_payment`, {
|
||||
payment_method_id: stp_payment_method_id,
|
||||
cart_items
|
||||
});
|
||||
@ -12,13 +14,12 @@ export default class StripeAPI {
|
||||
}
|
||||
|
||||
static async setupIntent (user_id: number): Promise<IntentConfirmation> {
|
||||
const res: AxiosResponse = await apiClient.get(`/api/stripe/setup_intent/${user_id}`);
|
||||
const res: AxiosResponse<IntentConfirmation> = await apiClient.get(`/api/stripe/setup_intent/${user_id}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
// TODO, type the response
|
||||
static async confirmPaymentSchedule (setup_intent_id: string, cart_items: ShoppingCart): Promise<any> {
|
||||
const res: AxiosResponse = await apiClient.post(`/api/stripe/confirm_payment_schedule`, {
|
||||
static async confirmPaymentSchedule (setup_intent_id: string, cart_items: ShoppingCart): Promise<PaymentSchedule> {
|
||||
const res: AxiosResponse<PaymentSchedule> = await apiClient.post(`/api/stripe/confirm_payment_schedule`, {
|
||||
setup_intent_id,
|
||||
cart_items
|
||||
});
|
||||
@ -26,7 +27,7 @@ export default class StripeAPI {
|
||||
}
|
||||
|
||||
static async updateCard (user_id: number, stp_payment_method_id: string): Promise<UpdateCardResponse> {
|
||||
const res: AxiosResponse = await apiClient.post(`/api/stripe/update_card`, {
|
||||
const res: AxiosResponse<UpdateCardResponse> = await apiClient.post(`/api/stripe/update_card`, {
|
||||
user_id,
|
||||
payment_method_id: stp_payment_method_id,
|
||||
});
|
||||
|
@ -12,13 +12,14 @@ import { User } from '../../models/user';
|
||||
import CustomAssetAPI from '../../api/custom-asset';
|
||||
import PriceAPI from '../../api/price';
|
||||
import WalletAPI from '../../api/wallet';
|
||||
import { Invoice } from '../../models/invoice';
|
||||
|
||||
declare var Fablab: IFablab;
|
||||
|
||||
|
||||
export interface GatewayFormProps {
|
||||
onSubmit: () => void,
|
||||
onSuccess: (result: any) => void,
|
||||
onSuccess: (result: Invoice|PaymentSchedule) => void,
|
||||
onError: (message: string) => void,
|
||||
customer: User,
|
||||
operator: User,
|
||||
@ -31,7 +32,7 @@ export interface GatewayFormProps {
|
||||
interface AbstractPaymentModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
afterSuccess: (result: any) => void,
|
||||
afterSuccess: (result: Invoice|PaymentSchedule) => void,
|
||||
cart: ShoppingCart,
|
||||
currentUser: User,
|
||||
schedule: PaymentSchedule,
|
||||
@ -137,7 +138,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
|
||||
/**
|
||||
* After sending the form with success, process the resulting payment method
|
||||
*/
|
||||
const handleFormSuccess = async (result: any): Promise<void> => {
|
||||
const handleFormSuccess = async (result: Invoice|PaymentSchedule): Promise<void> => {
|
||||
setSubmitState(false);
|
||||
afterSuccess(result);
|
||||
}
|
||||
|
@ -1,21 +1,24 @@
|
||||
import React, { ReactElement, ReactNode } from 'react';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Loader } from '../base/loader';
|
||||
import { StripeModal } from './stripe/stripe-modal';
|
||||
import { PayZenModal } from './payzen/payzen-modal';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { ShoppingCart } from '../../models/payment';
|
||||
import { User } from '../../models/user';
|
||||
import { PaymentSchedule } from '../../models/payment-schedule';
|
||||
import { Loader } from '../base/loader';
|
||||
import { react2angular } from 'react2angular';
|
||||
import SettingAPI from '../../api/setting';
|
||||
import { SettingName } from '../../models/setting';
|
||||
import { StripeModal } from './stripe/stripe-modal';
|
||||
import { PayZenModal } from './payzen/payzen-modal';
|
||||
import { Invoice } from '../../models/invoice';
|
||||
import SettingAPI from '../../api/setting';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
declare var Application: IApplication;
|
||||
|
||||
interface PaymentModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
afterSuccess: (result: any) => void,
|
||||
afterSuccess: (result: Invoice|PaymentSchedule) => void,
|
||||
onError: (message: string) => void,
|
||||
cart: ShoppingCart,
|
||||
currentUser: User,
|
||||
schedule: PaymentSchedule,
|
||||
@ -29,7 +32,8 @@ 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 , cart, customer }) => {
|
||||
const PaymentModal: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule , cart, customer }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
const gateway = paymentGateway.read();
|
||||
|
||||
/**
|
||||
@ -66,19 +70,24 @@ const PaymentModal: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterS
|
||||
return renderStripeModal();
|
||||
case 'payzen':
|
||||
return renderPayZenModal();
|
||||
case null:
|
||||
case undefined:
|
||||
onError(t('app.shared.payment_modal.online_payment_disabled'));
|
||||
return <div />;
|
||||
default:
|
||||
onError(t('app.shared.payment_modal.unexpected_error'));
|
||||
console.error(`[PaymentModal] Unimplemented gateway: ${gateway.value}`);
|
||||
return <div />
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const PaymentModalWrapper: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule , cart, customer }) => {
|
||||
const PaymentModalWrapper: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule , cart, customer }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PaymentModal isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} currentUser={currentUser} schedule={schedule} cart={cart} customer={customer} />
|
||||
<PaymentModal isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} currentUser={currentUser} schedule={schedule} cart={cart} customer={customer} />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('paymentModal', react2angular(PaymentModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess','currentUser', 'schedule', 'cart', 'customer']));
|
||||
Application.Components.component('paymentModal', react2angular(PaymentModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess', 'onError', 'currentUser', 'schedule', 'cart', 'customer']));
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React, { FunctionComponent, ReactNode } from 'react';
|
||||
import { GatewayFormProps, AbstractPaymentModal } from '../abstract-payment-modal';
|
||||
import { ShoppingCart, PaymentConfirmation } from '../../../models/payment';
|
||||
import { ShoppingCart } from '../../../models/payment';
|
||||
import { PaymentSchedule } from '../../../models/payment-schedule';
|
||||
import { User } from '../../../models/user';
|
||||
import { Invoice } from '../../../models/invoice';
|
||||
|
||||
import payzenLogo from '../../../../../images/payzen-secure.png';
|
||||
import mastercardLogo from '../../../../../images/mastercard.png';
|
||||
@ -13,7 +14,7 @@ import { PayzenForm } from './payzen-form';
|
||||
interface PayZenModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
afterSuccess: (result: PaymentConfirmation) => void,
|
||||
afterSuccess: (result: Invoice|PaymentSchedule) => void,
|
||||
cart: ShoppingCart,
|
||||
currentUser: User,
|
||||
schedule: PaymentSchedule,
|
||||
|
@ -1,20 +1,16 @@
|
||||
import React, { FormEvent } from 'react';
|
||||
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
||||
import { SetupIntent } from "@stripe/stripe-js";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GatewayFormProps } from '../abstract-payment-modal';
|
||||
import { PaymentConfirmation } from '../../../models/payment';
|
||||
import StripeAPI from '../../../api/stripe';
|
||||
|
||||
interface StripeFormProps extends GatewayFormProps {
|
||||
onSuccess: (result: SetupIntent|PaymentConfirmation) => void,
|
||||
}
|
||||
import { Invoice } from '../../../models/invoice';
|
||||
|
||||
/**
|
||||
* 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, cart, customer, operator, formId }) => {
|
||||
export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cart, customer, operator, formId }) => {
|
||||
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
@ -79,19 +75,17 @@ export const StripeForm: React.FC<StripeFormProps> = ({ onSubmit, onSuccess, onE
|
||||
|
||||
/**
|
||||
* Process the server response about the Strong-customer authentication (SCA)
|
||||
* @param response can be a PaymentConfirmation, or a Reservation (if the reservation succeeded), or a Subscription (if the subscription succeeded)
|
||||
* @see app/controllers/api/payments_controller.rb#on_reservation_success
|
||||
* @see app/controllers/api/payments_controller.rb#on_subscription_success
|
||||
* @see app/controllers/api/payments_controller.rb#generate_payment_response
|
||||
* @param response can be a PaymentConfirmation, or an Invoice (if the payment succeeded)
|
||||
* @see app/controllers/api/stripe_controller.rb#confirm_payment
|
||||
*/
|
||||
const handleServerConfirmation = async (response: PaymentConfirmation|any) => {
|
||||
if (response.error) {
|
||||
const handleServerConfirmation = async (response: PaymentConfirmation|Invoice) => {
|
||||
if ('error' in response) {
|
||||
if (response.error.statusText) {
|
||||
onError(response.error.statusText);
|
||||
} else {
|
||||
onError(`${t('app.shared.messages.payment_card_error')} ${response.error}`);
|
||||
}
|
||||
} else if (response.requires_action) {
|
||||
} else if ('requires_action' in response) {
|
||||
// Use Stripe.js to handle required card action
|
||||
const result = await stripe.handleCardAction(response.payment_intent_client_secret);
|
||||
if (result.error) {
|
||||
@ -106,8 +100,10 @@ export const StripeForm: React.FC<StripeFormProps> = ({ onSubmit, onSuccess, onE
|
||||
onError(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if ('id' in response) {
|
||||
onSuccess(response);
|
||||
} else {
|
||||
console.error(`[StripeForm] unknown response received: ${response}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,21 +1,21 @@
|
||||
import React, { FunctionComponent, ReactNode } from 'react';
|
||||
import { SetupIntent } from '@stripe/stripe-js';
|
||||
import { StripeElements } from './stripe-elements';
|
||||
import { StripeForm } from './stripe-form';
|
||||
import { GatewayFormProps, AbstractPaymentModal } from '../abstract-payment-modal';
|
||||
import { ShoppingCart, PaymentConfirmation } from '../../../models/payment';
|
||||
import { ShoppingCart } from '../../../models/payment';
|
||||
import { PaymentSchedule } from '../../../models/payment-schedule';
|
||||
import { User } from '../../../models/user';
|
||||
|
||||
import stripeLogo from '../../../../../images/powered_by_stripe.png';
|
||||
import mastercardLogo from '../../../../../images/mastercard.png';
|
||||
import visaLogo from '../../../../../images/visa.png';
|
||||
import { Invoice } from '../../../models/invoice';
|
||||
|
||||
|
||||
interface StripeModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
afterSuccess: (result: SetupIntent|PaymentConfirmation) => void,
|
||||
afterSuccess: (result: Invoice|PaymentSchedule) => void,
|
||||
cart: ShoppingCart,
|
||||
currentUser: User,
|
||||
schedule: PaymentSchedule,
|
||||
|
@ -20,12 +20,13 @@ interface PlanCardProps {
|
||||
operator: User,
|
||||
isSelected: boolean,
|
||||
onSelectPlan: (plan: Plan) => void,
|
||||
onLoginRequested: () => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is a "card" (visually), publicly presenting the details of a plan and allowing a user to subscribe.
|
||||
*/
|
||||
const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected }) => {
|
||||
const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested }) => {
|
||||
const { t } = useTranslation('public');
|
||||
/**
|
||||
* Return the formatted localized amount of the given plan (eg. 20.5 => "20,50 €")
|
||||
@ -46,6 +47,12 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, ope
|
||||
const duration = (): string => {
|
||||
return moment.duration(plan.interval_count, plan.interval).humanize();
|
||||
}
|
||||
/**
|
||||
* Check if no users are currently logged-in
|
||||
*/
|
||||
const mustLogin = (): boolean => {
|
||||
return _.isNil(operator);
|
||||
}
|
||||
/**
|
||||
* Check if the user can subscribe to the current plan, for himself
|
||||
*/
|
||||
@ -88,6 +95,12 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, ope
|
||||
const handleSelectPlan = (): void => {
|
||||
onSelectPlan(plan);
|
||||
}
|
||||
/**
|
||||
* Callback triggered when a visitor (not logged-in user) select a plan
|
||||
*/
|
||||
const handleLoginRequest = (): void => {
|
||||
onLoginRequested();
|
||||
}
|
||||
return (
|
||||
<div className="plan-card">
|
||||
<h3 className="title">{plan.base_name}</h3>
|
||||
@ -108,12 +121,14 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, ope
|
||||
<div className="card-footer">
|
||||
{hasDescription() && <div className="plan-description" dangerouslySetInnerHTML={{__html: plan.description}}/>}
|
||||
{hasAttachment() && <a className="info-link" href={ plan.plan_file_url } target="_blank">{ t('app.public.plans.more_information') }</a>}
|
||||
{mustLogin() && <div className="cta-button">
|
||||
<button className="subscribe-button" onClick={handleLoginRequest}>{t('app.public.plans.i_subscribe_online')}</button>
|
||||
</div>}
|
||||
{canSubscribeForMe() && <div className="cta-button">
|
||||
{!hasSubscribedToThisPlan() && <button className={`subscribe-button ${isSelected ? 'selected-card' : ''}`}
|
||||
onClick={handleSelectPlan}
|
||||
disabled={!_.isNil(subscribedPlanId)}>
|
||||
{userId && <span>{t('app.public.plans.i_choose_that_plan')}</span>}
|
||||
{!userId && <span>{t('app.public.plans.i_subscribe_online')}</span>}
|
||||
{t('app.public.plans.i_choose_that_plan')}
|
||||
</button>}
|
||||
{hasSubscribedToThisPlan() && <button className="subscribe-button selected-card" disabled>
|
||||
{ t('app.public.plans.i_already_subscribed') }
|
||||
@ -131,12 +146,12 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, ope
|
||||
);
|
||||
}
|
||||
|
||||
const PlanCardWrapper: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected }) => {
|
||||
const PlanCardWrapper: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PlanCard plan={plan} userId={userId} subscribedPlanId={subscribedPlanId} operator={operator} isSelected={isSelected} onSelectPlan={onSelectPlan}/>
|
||||
<PlanCard plan={plan} userId={userId} subscribedPlanId={subscribedPlanId} operator={operator} isSelected={isSelected} onSelectPlan={onSelectPlan} onLoginRequested={onLoginRequested}/>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('planCard', react2angular(PlanCardWrapper, ['plan', 'userId', 'subscribedPlanId', 'operator', 'onSelectPlan', 'isSelected']));
|
||||
Application.Components.component('planCard', react2angular(PlanCardWrapper, ['plan', 'userId', 'subscribedPlanId', 'operator', 'onSelectPlan', 'isSelected', 'onLoginRequested']));
|
||||
|
@ -571,11 +571,19 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
|
||||
/**
|
||||
* Invoked atfer a successful card payment
|
||||
* @param reservation {*} reservation
|
||||
* @param invoice {*} the invoice
|
||||
*/
|
||||
$scope.afterOnlinePaymentSuccess = (reservation) => {
|
||||
$scope.afterOnlinePaymentSuccess = (invoice) => {
|
||||
$scope.toggleOnlinePaymentModal();
|
||||
afterPayment(reservation);
|
||||
afterPayment(invoice);
|
||||
};
|
||||
|
||||
/**
|
||||
* Invoked when something wrong occurred during the payment dialog initialization
|
||||
* @param message {string}
|
||||
*/
|
||||
$scope.onOnlinePaymentError = (message) => {
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
@ -798,14 +806,16 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
|
||||
/**
|
||||
* What to do after the payment was successful
|
||||
* @param reservation {Object} booked reservation
|
||||
* @param invoice {Object} the invoice for the booked reservation
|
||||
*/
|
||||
const afterPayment = function (reservation) {
|
||||
$scope.event.nb_free_places = $scope.event.nb_free_places - reservation.total_booked_seats;
|
||||
const afterPayment = function (invoice) {
|
||||
Reservation.get({ id: invoice.main_object.id }, function (reservation) {
|
||||
$scope.event.nb_free_places = $scope.event.nb_free_places - reservation.total_booked_seats;
|
||||
$scope.reservations.push(reservation);
|
||||
});
|
||||
resetEventReserve();
|
||||
$scope.reserveSuccess = true;
|
||||
$scope.coupon.applied = null;
|
||||
$scope.reservations.push(reservation);
|
||||
if ($scope.currentUser.role === 'admin') {
|
||||
return $scope.ctrl.member = null;
|
||||
}
|
||||
|
@ -407,8 +407,8 @@ Application.Controllers.controller('ShowMachineController', ['$scope', '$state',
|
||||
* This controller workflow is pretty similar to the trainings reservation controller.
|
||||
*/
|
||||
|
||||
Application.Controllers.controller('ReserveMachineController', ['$scope', '$stateParams', '_t', 'moment', 'Auth', '$timeout', 'Member', 'Availability', 'plansPromise', 'groupsPromise', 'machinePromise', 'settingsPromise', 'uiCalendarConfig', 'CalendarConfig',
|
||||
function ($scope, $stateParams, _t, moment, Auth, $timeout, Member, Availability, plansPromise, groupsPromise, machinePromise, settingsPromise, uiCalendarConfig, CalendarConfig) {
|
||||
Application.Controllers.controller('ReserveMachineController', ['$scope', '$stateParams', '_t', 'moment', 'Auth', '$timeout', 'Member', 'Availability', 'plansPromise', 'groupsPromise', 'machinePromise', 'settingsPromise', 'uiCalendarConfig', 'CalendarConfig', 'Reservation',
|
||||
function ($scope, $stateParams, _t, moment, Auth, $timeout, Member, Availability, plansPromise, groupsPromise, machinePromise, settingsPromise, uiCalendarConfig, CalendarConfig, Reservation) {
|
||||
/* PRIVATE STATIC CONSTANTS */
|
||||
|
||||
// Slot free to be booked
|
||||
@ -643,36 +643,38 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat
|
||||
/**
|
||||
* Once the reservation is booked (payment process successfully completed), change the event style
|
||||
* in fullCalendar, update the user's subscription and free-credits if needed
|
||||
* @param reservation {Object}
|
||||
* @param paymentDocument {Invoice|PaymentSchedule}
|
||||
*/
|
||||
$scope.afterPayment = function (reservation) {
|
||||
angular.forEach($scope.events.reserved, function (machineSlot, key) {
|
||||
machineSlot.is_reserved = true;
|
||||
machineSlot.can_modify = true;
|
||||
if ($scope.currentUser.role === 'admin' || ($scope.currentUser.role === 'manager' && reservation.user_id !== $scope.currentUser.id)) {
|
||||
// an admin or a manager booked for someone else
|
||||
machineSlot.title = _t('app.logged.machines_reserve.not_available');
|
||||
machineSlot.borderColor = UNAVAILABLE_SLOT_BORDER_COLOR;
|
||||
updateMachineSlot(machineSlot, reservation, $scope.ctrl.member);
|
||||
} else {
|
||||
// booked for "myself"
|
||||
machineSlot.title = _t('app.logged.machines_reserve.i_ve_reserved');
|
||||
machineSlot.borderColor = BOOKED_SLOT_BORDER_COLOR;
|
||||
updateMachineSlot(machineSlot, reservation, $scope.currentUser);
|
||||
$scope.afterPayment = function (paymentDocument) {
|
||||
Reservation.get({ id: paymentDocument.main_object.id }, function (reservation) {
|
||||
angular.forEach($scope.events.reserved, function (machineSlot, key) {
|
||||
machineSlot.is_reserved = true;
|
||||
machineSlot.can_modify = true;
|
||||
if ($scope.currentUser.role === 'admin' || ($scope.currentUser.role === 'manager' && reservation.user_id !== $scope.currentUser.id)) {
|
||||
// an admin or a manager booked for someone else
|
||||
machineSlot.title = _t('app.logged.machines_reserve.not_available');
|
||||
machineSlot.borderColor = UNAVAILABLE_SLOT_BORDER_COLOR;
|
||||
updateMachineSlot(machineSlot, reservation, $scope.ctrl.member);
|
||||
} else {
|
||||
// booked for "myself"
|
||||
machineSlot.title = _t('app.logged.machines_reserve.i_ve_reserved');
|
||||
machineSlot.borderColor = BOOKED_SLOT_BORDER_COLOR;
|
||||
updateMachineSlot(machineSlot, reservation, $scope.currentUser);
|
||||
}
|
||||
machineSlot.backgroundColor = 'white';
|
||||
});
|
||||
|
||||
if ($scope.selectedPlan) {
|
||||
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
|
||||
if ($scope.ctrl.member.id === Auth._currentUser.id) {
|
||||
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan);
|
||||
}
|
||||
$scope.plansAreShown = false;
|
||||
$scope.selectedPlan = null;
|
||||
}
|
||||
machineSlot.backgroundColor = 'white';
|
||||
|
||||
refetchCalendar();
|
||||
});
|
||||
|
||||
if ($scope.selectedPlan) {
|
||||
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
|
||||
if ($scope.ctrl.member.id === Auth._currentUser.id) {
|
||||
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan);
|
||||
}
|
||||
$scope.plansAreShown = false;
|
||||
$scope.selectedPlan = null;
|
||||
}
|
||||
|
||||
refetchCalendar();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -89,6 +89,20 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
|
||||
}, 50);
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the modal dialog allowing the user to log into the system
|
||||
*/
|
||||
$scope.userLogin = function () {
|
||||
console.log('userLogin');
|
||||
setTimeout(() => {
|
||||
console.log('going throught timeout');
|
||||
if (!$scope.isAuthenticated()) {
|
||||
console.log('! authenticated');
|
||||
$scope.login();
|
||||
}
|
||||
}, 50);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the provided plan is currently selected
|
||||
* @param plan {Object} Resource plan
|
||||
|
@ -307,8 +307,8 @@ Application.Controllers.controller('ShowSpaceController', ['$scope', '$state', '
|
||||
* per slots.
|
||||
*/
|
||||
|
||||
Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateParams', 'Auth', '$timeout', 'Availability', 'Member', 'plansPromise', 'groupsPromise', 'settingsPromise', 'spacePromise', '_t', 'uiCalendarConfig', 'CalendarConfig',
|
||||
function ($scope, $stateParams, Auth, $timeout, Availability, Member, plansPromise, groupsPromise, settingsPromise, spacePromise, _t, uiCalendarConfig, CalendarConfig) {
|
||||
Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateParams', 'Auth', '$timeout', 'Availability', 'Member', 'plansPromise', 'groupsPromise', 'settingsPromise', 'spacePromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Reservation',
|
||||
function ($scope, $stateParams, Auth, $timeout, Availability, Member, plansPromise, groupsPromise, settingsPromise, spacePromise, _t, uiCalendarConfig, CalendarConfig, Reservation) {
|
||||
/* PRIVATE STATIC CONSTANTS */
|
||||
|
||||
// Color of the selected event backgound
|
||||
@ -556,35 +556,37 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateP
|
||||
/**
|
||||
* Once the reservation is booked (payment process successfully completed), change the event style
|
||||
* in fullCalendar, update the user's subscription and free-credits if needed
|
||||
* @param reservation {Object}
|
||||
* @param paymentDocument {Invoice|PaymentSchedule}
|
||||
*/
|
||||
$scope.afterPayment = function (reservation) {
|
||||
angular.forEach($scope.events.paid, function (spaceSlot, key) {
|
||||
spaceSlot.is_reserved = true;
|
||||
spaceSlot.can_modify = true;
|
||||
spaceSlot.title = _t('app.logged.space_reserve.i_ve_reserved');
|
||||
spaceSlot.backgroundColor = 'white';
|
||||
spaceSlot.borderColor = RESERVED_SLOT_BORDER_COLOR;
|
||||
updateSpaceSlotId(spaceSlot, reservation);
|
||||
updateEvents(spaceSlot);
|
||||
});
|
||||
$scope.afterPayment = function (paymentDocument) {
|
||||
Reservation.get({ id: paymentDocument.main_object.id }, function (reservation) {
|
||||
angular.forEach($scope.events.paid, function (spaceSlot, key) {
|
||||
spaceSlot.is_reserved = true;
|
||||
spaceSlot.can_modify = true;
|
||||
spaceSlot.title = _t('app.logged.space_reserve.i_ve_reserved');
|
||||
spaceSlot.backgroundColor = 'white';
|
||||
spaceSlot.borderColor = RESERVED_SLOT_BORDER_COLOR;
|
||||
updateSpaceSlotId(spaceSlot, reservation);
|
||||
updateEvents(spaceSlot);
|
||||
});
|
||||
|
||||
if ($scope.selectedPlan) {
|
||||
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
|
||||
if ($scope.ctrl.member.id === Auth._currentUser.id) {
|
||||
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan);
|
||||
if ($scope.selectedPlan) {
|
||||
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
|
||||
if ($scope.ctrl.member.id === Auth._currentUser.id) {
|
||||
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan);
|
||||
}
|
||||
$scope.plansAreShown = false;
|
||||
$scope.selectedPlan = null;
|
||||
}
|
||||
$scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits);
|
||||
$scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits);
|
||||
if ($scope.ctrl.member.id === Auth._currentUser.id) {
|
||||
Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits);
|
||||
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits);
|
||||
}
|
||||
$scope.plansAreShown = false;
|
||||
$scope.selectedPlan = null;
|
||||
}
|
||||
$scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits);
|
||||
$scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits);
|
||||
if ($scope.ctrl.member.id === Auth._currentUser.id) {
|
||||
Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits);
|
||||
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits);
|
||||
}
|
||||
|
||||
refetchCalendar();
|
||||
refetchCalendar();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -91,8 +91,8 @@ Application.Controllers.controller('ShowTrainingController', ['$scope', '$state'
|
||||
* training can be reserved during the reservation process (the shopping cart may contains only one training and a subscription).
|
||||
*/
|
||||
|
||||
Application.Controllers.controller('ReserveTrainingController', ['$scope', '$stateParams', 'Auth', 'AuthService', '$timeout', 'Availability', 'Member', 'plansPromise', 'groupsPromise', 'settingsPromise', 'trainingPromise', '_t', 'uiCalendarConfig', 'CalendarConfig',
|
||||
function ($scope, $stateParams, Auth, AuthService, $timeout, Availability, Member, plansPromise, groupsPromise, settingsPromise, trainingPromise, _t, uiCalendarConfig, CalendarConfig) {
|
||||
Application.Controllers.controller('ReserveTrainingController', ['$scope', '$stateParams', 'Auth', 'AuthService', '$timeout', 'Availability', 'Member', 'plansPromise', 'groupsPromise', 'settingsPromise', 'trainingPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Reservation',
|
||||
function ($scope, $stateParams, Auth, AuthService, $timeout, Availability, Member, plansPromise, groupsPromise, settingsPromise, trainingPromise, _t, uiCalendarConfig, CalendarConfig, Reservation) {
|
||||
/* PRIVATE STATIC CONSTANTS */
|
||||
|
||||
// Color of the selected event backgound
|
||||
@ -346,35 +346,37 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$sta
|
||||
/**
|
||||
* Once the reservation is booked (payment process successfully completed), change the event style
|
||||
* in fullCalendar, update the user's subscription and free-credits if needed
|
||||
* @param reservation {Object}
|
||||
* @param paymentDocument {Invoice|PaymentSchedule}
|
||||
*/
|
||||
$scope.afterPayment = function (reservation) {
|
||||
angular.forEach($scope.events.paid, function (trainingSlot, key) {
|
||||
trainingSlot.backgroundColor = 'white';
|
||||
trainingSlot.is_reserved = true;
|
||||
trainingSlot.can_modify = true;
|
||||
updateTrainingSlotId(trainingSlot, reservation);
|
||||
trainingSlot.borderColor = '#b2e774';
|
||||
trainingSlot.title = trainingSlot.training.name + ' - ' + _t('app.logged.trainings_reserve.i_ve_reserved');
|
||||
updateEvents(trainingSlot);
|
||||
});
|
||||
$scope.afterPayment = function (paymentDocument) {
|
||||
Reservation.get({ id: paymentDocument.main_object.id }, function (reservation) {
|
||||
angular.forEach($scope.events.paid, function (trainingSlot, key) {
|
||||
trainingSlot.backgroundColor = 'white';
|
||||
trainingSlot.is_reserved = true;
|
||||
trainingSlot.can_modify = true;
|
||||
updateTrainingSlotId(trainingSlot, reservation);
|
||||
trainingSlot.borderColor = '#b2e774';
|
||||
trainingSlot.title = trainingSlot.training.name + ' - ' + _t('app.logged.trainings_reserve.i_ve_reserved');
|
||||
updateEvents(trainingSlot);
|
||||
});
|
||||
|
||||
if ($scope.selectedPlan) {
|
||||
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
|
||||
if ($scope.ctrl.member.id === Auth._currentUser.id) {
|
||||
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan);
|
||||
if ($scope.selectedPlan) {
|
||||
$scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan);
|
||||
if ($scope.ctrl.member.id === Auth._currentUser.id) {
|
||||
Auth._currentUser.subscribed_plan = angular.copy($scope.selectedPlan);
|
||||
}
|
||||
$scope.plansAreShown = false;
|
||||
$scope.selectedPlan = null;
|
||||
}
|
||||
$scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits);
|
||||
$scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits);
|
||||
if ($scope.ctrl.member.id === Auth._currentUser.id) {
|
||||
Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits);
|
||||
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits);
|
||||
}
|
||||
$scope.plansAreShown = false;
|
||||
$scope.selectedPlan = null;
|
||||
}
|
||||
$scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits);
|
||||
$scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits);
|
||||
if ($scope.ctrl.member.id === Auth._currentUser.id) {
|
||||
Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits);
|
||||
Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits);
|
||||
}
|
||||
|
||||
refetchCalendar();
|
||||
refetchCalendar();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -327,11 +327,19 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
|
||||
/**
|
||||
* Invoked atfer a successful card payment
|
||||
* @param result {*} may be a reservation or a subscription
|
||||
* @param invoice {*} may be an Invoice or a paymentSchedule
|
||||
*/
|
||||
$scope.afterOnlinePaymentSuccess = (result) => {
|
||||
$scope.afterOnlinePaymentSuccess = (invoice) => {
|
||||
$scope.toggleOnlinePaymentModal();
|
||||
afterPayment(result);
|
||||
afterPayment(invoice);
|
||||
};
|
||||
|
||||
/**
|
||||
* Invoked when something wrong occurred during the payment dialog initialization
|
||||
* @param message {string}
|
||||
*/
|
||||
$scope.onOnlinePaymentError = (message) => {
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
@ -843,13 +851,21 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
|
||||
/**
|
||||
* After creating a payment schedule by card, from an administrator.
|
||||
* @param result {*} Reservation or Subscription
|
||||
* @param result {*} PaymentSchedule
|
||||
*/
|
||||
$scope.afterCreatePaymentSchedule = function (result) {
|
||||
$scope.toggleOnlinePaymentModal();
|
||||
$uibModalInstance.close(result);
|
||||
};
|
||||
|
||||
/**
|
||||
* Invoked when something wrong occurred during the payment dialog initialization
|
||||
* @param message {string}
|
||||
*/
|
||||
$scope.onCreatePaymentScheduleError = (message) => {
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/**
|
||||
@ -889,19 +905,19 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
initialize();
|
||||
}
|
||||
]
|
||||
}).result.finally(null).then(function (reservation) { afterPayment(reservation); });
|
||||
}).result.finally(null).then(function (paymentSchedule) { afterPayment(paymentSchedule); });
|
||||
};
|
||||
|
||||
/**
|
||||
* Actions to run after the payment was successful
|
||||
* @param paymentResult {*} may be a reservation or a subscription
|
||||
* @param paymentDocument {*} may be an Invoice or a PaymentSchedule
|
||||
*/
|
||||
const afterPayment = function (paymentResult) {
|
||||
const afterPayment = function (paymentDocument) {
|
||||
// we set the cart content as 'paid' to display a summary of the transaction
|
||||
$scope.events.paid = $scope.events.reserved;
|
||||
$scope.amountPaid = $scope.amountTotal;
|
||||
// we call the external callback if present
|
||||
if (typeof $scope.afterPayment === 'function') { $scope.afterPayment(paymentResult); }
|
||||
if (typeof $scope.afterPayment === 'function') { $scope.afterPayment(paymentDocument); }
|
||||
// we reset the coupon, and the cart content, and we unselect the slot
|
||||
$scope.coupon.applied = undefined;
|
||||
if ($scope.slot) {
|
||||
|
26
app/frontend/src/javascript/models/invoice.ts
Normal file
26
app/frontend/src/javascript/models/invoice.ts
Normal file
@ -0,0 +1,26 @@
|
||||
export interface Invoice {
|
||||
id: number,
|
||||
created_at: Date,
|
||||
reference: string,
|
||||
avoir_date: Date,
|
||||
description: string
|
||||
user_id: number,
|
||||
total: number,
|
||||
name: string,
|
||||
has_avoir: boolean,
|
||||
is_avoir: boolean,
|
||||
is_subscription_invoice: boolean,
|
||||
is_online_card: boolean,
|
||||
date: Date,
|
||||
chained_footprint: boolean,
|
||||
main_object: {
|
||||
type: string,
|
||||
id: number
|
||||
},
|
||||
items: {
|
||||
id: number,
|
||||
amount: number,
|
||||
description: string,
|
||||
avoir_item_id: number
|
||||
}
|
||||
}
|
@ -25,8 +25,7 @@ export interface PaymentScheduleItem {
|
||||
recurring: number,
|
||||
adjustment?: number,
|
||||
other_items?: number,
|
||||
without_coupon?: number,
|
||||
subscription_id: number
|
||||
without_coupon?: number
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,6 +42,10 @@ export interface PaymentSchedule {
|
||||
items: Array<PaymentScheduleItem>,
|
||||
created_at: Date,
|
||||
chained_footprint: boolean,
|
||||
main_object: {
|
||||
type: string,
|
||||
id: number
|
||||
},
|
||||
user: {
|
||||
id: number,
|
||||
name: string
|
||||
|
@ -9,10 +9,6 @@ export interface CreateTokenResponse {
|
||||
|
||||
export interface CreatePaymentResponse extends CreateTokenResponse {}
|
||||
|
||||
export interface ConfirmPaymentResponse {
|
||||
todo?: any
|
||||
}
|
||||
|
||||
export interface CheckHashResponse {
|
||||
validity: boolean
|
||||
}
|
||||
|
@ -209,6 +209,7 @@
|
||||
<payment-modal is-open="onlinePayment.showModal"
|
||||
toggle-modal="toggleOnlinePaymentModal"
|
||||
after-success="afterOnlinePaymentSuccess"
|
||||
on-error="onOnlinePaymentError"
|
||||
cart="onlinePayment.cartItems"
|
||||
current-user="currentUser"
|
||||
customer="ctrl.member"/>
|
||||
|
@ -29,6 +29,7 @@
|
||||
subscribed-plan-id="ctrl.member.subscribed_plan.id"
|
||||
operator="currentUser"
|
||||
on-select-plan="selectPlan"
|
||||
on-login-requested="userLogin"
|
||||
is-selected="isSelected(plan)">
|
||||
</plan-card>
|
||||
</div>
|
||||
|
@ -203,6 +203,7 @@
|
||||
<payment-modal is-open="onlinePayment.showModal"
|
||||
toggle-modal="toggleOnlinePaymentModal"
|
||||
after-success="afterOnlinePaymentSuccess"
|
||||
on-error="onOnlinePaymentError"
|
||||
cart="onlinePayment.cartItems"
|
||||
current-user="currentUser"
|
||||
customer="user"
|
||||
|
@ -52,6 +52,7 @@
|
||||
<payment-modal is-open="isOpenOnlinePaymentModal"
|
||||
toggle-modal="toggleOnlinePaymentModal"
|
||||
after-success="afterCreatePaymentSchedule"
|
||||
on-error="onCreatePaymentScheduleError"
|
||||
cart="cartItems"
|
||||
current-user="currentUser"
|
||||
schedule="schedule"
|
||||
|
@ -7,7 +7,7 @@ json.name invoice.user.profile.full_name
|
||||
json.has_avoir invoice.refunded?
|
||||
json.is_avoir invoice.is_a?(Avoir)
|
||||
json.is_subscription_invoice invoice.subscription_invoice?
|
||||
json.stripe invoice.paid_by_card?
|
||||
json.is_online_card invoice.paid_by_card?
|
||||
json.date invoice.is_a?(Avoir) ? invoice.avoir_date : invoice.created_at
|
||||
json.chained_footprint invoice.check_footprint
|
||||
json.main_object do
|
||||
|
@ -511,3 +511,6 @@ en:
|
||||
cancel_subscription: "Cancel the subscription"
|
||||
confirm_cancel_subscription: "You're about to cancel this payment schedule and the related subscription. Are you sure?"
|
||||
please_ask_reception: "For any questions, please contact the FabLab's reception."
|
||||
payment_modal:
|
||||
online_payment_disabled: "Online payment is not available. Please contact the FabLab's reception directly."
|
||||
unexpected_error: "An error occurred. Please report this issue to the Fab-Manager's team."
|
||||
|
@ -511,3 +511,6 @@ fr:
|
||||
cancel_subscription: "Annuler l'abonnement"
|
||||
confirm_cancel_subscription: "Vous êtes sur le point d'annuler cet échéancier de paiement ainsi que l'abonnement lié. Êtes-vous sur ?"
|
||||
please_ask_reception: "Pour toute question, merci de contacter l'accueil du FabLab."
|
||||
payment_modal:
|
||||
online_payment_disabled: "Le paiement par carte bancaire n'est pas disponible. Merci de contacter directement l'accueil du FabLab."
|
||||
unexpected_error: "Une erreur est survenue. Merci de rapporter cet incident à l'équipe de Fab-Manager."
|
||||
|
Loading…
x
Reference in New Issue
Block a user