1
0
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:
Sylvain 2021-06-01 11:01:38 +02:00
parent e08157b5f6
commit 81bc22c494
25 changed files with 261 additions and 155 deletions

View File

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

View File

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

View File

@ -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,
});

View File

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

View File

@ -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']));

View File

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

View File

@ -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}`);
}
}

View File

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

View File

@ -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']));

View File

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

View File

@ -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();
};
/**

View File

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

View File

@ -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();
});
};
/**

View File

@ -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();
});
};
/**

View File

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

View 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
}
}

View File

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

View File

@ -9,10 +9,6 @@ export interface CreateTokenResponse {
export interface CreatePaymentResponse extends CreateTokenResponse {}
export interface ConfirmPaymentResponse {
todo?: any
}
export interface CheckHashResponse {
validity: boolean
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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