feat: retrieve organization subscriptions list api.

feat: subscriptions reducers.
This commit is contained in:
Ahmed Bouhuolia
2020-10-13 21:46:32 +02:00
parent d71845a4c4
commit 8b97673100
23 changed files with 289 additions and 55 deletions

View File

@@ -13,7 +13,6 @@ export default function DashboardLoadingIndicator({
<div className={classNames('bigcapital-loading', className)}>
<div class="center">
<Icon icon="bigcapital" height={37} width={214} />
<span class="text">Please wait while resources loading...</span>
</div>
</div>
</Choose.When>

View File

@@ -13,9 +13,9 @@ function EnsureOrganizationIsReady({
redirectTo = '/setup',
// #withOrganizationByOrgId
isOrganizationBuilt,
isOrganizationInitialized,
}) {
return (isOrganizationBuilt) ? children : (
return (isOrganizationInitialized) ? children : (
<Redirect
to={{ pathname: redirectTo }}
/>
@@ -27,5 +27,5 @@ export default compose(
connect((state, props) => ({
organizationId: props.currentOrganizationId,
})),
withOrganization(({ isOrganizationBuilt }) => ({ isOrganizationBuilt })),
withOrganization(({ isOrganizationInitialized }) => ({ isOrganizationInitialized })),
)(EnsureOrganizationIsReady);

View File

@@ -7,6 +7,7 @@ import SetupWizardPage from 'containers/Setup/WizardSetupPage';
import DashboardLoadingIndicator from 'components/Dashboard/DashboardLoadingIndicator';
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
import withSubscriptionsActions from 'containers/Subscriptions/withSubscriptionsActions';
import { compose } from 'utils';
@@ -17,13 +18,26 @@ function DashboardPrivatePages({
// #withOrganizationActions
requestAllOrganizations,
// #withSubscriptionsActions
requestFetchSubscriptions,
}) {
// Fetch all user's organizatins.
const fetchOrganizations = useQuery(
['organizations'], () => requestAllOrganizations(),
);
// Fetchs organization subscriptions.
const fetchSuscriptions = useQuery(
['susbcriptions'], () => requestFetchSubscriptions(),
{ enabled: fetchOrganizations.data },
)
return (
<DashboardLoadingIndicator isLoading={fetchOrganizations.isFetching}>
<DashboardLoadingIndicator isLoading={
fetchOrganizations.isFetching ||
fetchSuscriptions.isFetching
}>
<Switch>
<Route path={'/setup'}>
<SetupWizardPage />
@@ -39,4 +53,5 @@ function DashboardPrivatePages({
export default compose(
withOrganizationActions,
withSubscriptionsActions,
)(DashboardPrivatePages);

View File

@@ -43,14 +43,18 @@ function SetupLeftSection({
<p className={'content__text'}>
<T id={'you_have_a_bigcapital_account'} />
</p>
<span class="content__divider"></span>
<div className={'content__organization'}>
<span class="organization-id">Your oragnization ID: <span class="id">{ currentOrganizationId }</span>,</span><br />
<span class="signout"><a onClick={onClickLogout} href="#"><T id={'sign_out'} /></a></span>
<span class="organization-id">
Your oragnization ID: <span class="id">{ currentOrganizationId }</span>,
</span>
<br />
<span class="signout">
<a onClick={onClickLogout} href="#"><T id={'sign_out'} /></a>
</span>
</div>
<span class="content__divider"></span>
<div className={'content__footer'}>
<div className={'content__contact-info'}>
<p><T id={'we_re_here_to_help'} /> {'+21892-791-8381'}</p>

View File

@@ -16,16 +16,20 @@ import classNames from 'classnames';
import { TimezonePicker } from '@blueprintjs/timezone';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { DateInput } from '@blueprintjs/datetime';
import { withWizard } from 'react-albus';
import { momentFormatter, tansformDateValue } from 'utils';
import { ListSelect, ErrorMessage, FieldRequiredHint } from 'components';
import { useHistory } from 'react-router-dom';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
import { compose, optionsMapToArray } from 'utils';
function SetupOrganizationForm({ requestSubmitOptions, requestSeedTenant }) {
function SetupOrganizationForm({
requestSubmitOptions,
requestOrganizationSeed,
wizard,
}) {
const { formatMessage } = useIntl();
const [selected, setSelected] = useState();
@@ -130,7 +134,7 @@ function SetupOrganizationForm({ requestSubmitOptions, requestSeedTenant }) {
name: Yup.string()
.required()
.label(formatMessage({ id: 'organization_name_' })),
date_start: Yup.date()
financial_date_start: Yup.date()
.required()
.label(formatMessage({ id: 'date_start_' })),
base_currency: Yup.string()
@@ -148,7 +152,7 @@ function SetupOrganizationForm({ requestSubmitOptions, requestSeedTenant }) {
const initialValues = useMemo(
() => ({
name: '',
date_start: moment(new Date()).format('YYYY-MM-DD'),
financial_date_start: moment(new Date()).format('YYYY-MM-DD'),
base_currency: '',
language: '',
fiscal_year: '',
@@ -176,7 +180,11 @@ function SetupOrganizationForm({ requestSubmitOptions, requestSeedTenant }) {
return { key: option.key, ...option, group: 'organization' };
});
requestSubmitOptions({ options })
.then(() => {
return requestOrganizationSeed();
})
.then((response) => {
wizard.next();
setSubmitting(false);
})
.catch((erros) => {
@@ -220,29 +228,29 @@ function SetupOrganizationForm({ requestSubmitOptions, requestSeedTenant }) {
const handleDateChange = useCallback(
(date) => {
const formatted = moment(date).format('YYYY-MM-DD');
setFieldValue('date_start', formatted);
setFieldValue('financial_date_start', formatted);
},
[setFieldValue],
);
return (
<div className={'register-organizaton-form'}>
<div className={'register-org-title'}>
<h2>
<div className={'setup-organization'}>
<div className={'setup-organization__title-wrap'}>
<h1>
<T id={'let_s_get_started'} />
</h2>
<p>
</h1>
<p class="paragraph">
<T id={'tell_the_system_a_little_bit_about_your_organization'} />
</p>
</div>
<form onClick={handleSubmit}>
<form class="setup-organization__form" onClick={handleSubmit}>
<h3>
<T id={'organization_details'} />
</h3>
<FormGroup
label={<T id={'name'} />}
label={<T id={'legal_organization_name'} />}
labelInfo={<FieldRequiredHint />}
className={'form-group--name'}
intent={errors.name && touched.name && Intent.DANGER}
@@ -258,15 +266,15 @@ function SetupOrganizationForm({ requestSubmitOptions, requestSeedTenant }) {
<FormGroup
label={<T id={'financial_starting_date'} />}
labelInfo={<FieldRequiredHint />}
intent={errors.date_start && touched.date_start && Intent.DANGER}
intent={errors.financial_date_start && touched.financial_date_start && Intent.DANGER}
helperText={
<ErrorMessage name="date_start" {...{ errors, touched }} />
<ErrorMessage name="financial_date_start" {...{ errors, touched }} />
}
className={classNames('form-group--select-list', Classes.FILL)}
>
<DateInput
{...momentFormatter('MMMM Do YYYY')}
value={tansformDateValue(values.date_start)}
value={tansformDateValue(values.financial_date_start)}
onChange={handleDateChange}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
/>
@@ -410,4 +418,5 @@ function SetupOrganizationForm({ requestSubmitOptions, requestSeedTenant }) {
export default compose(
withSettingsActions,
withOrganizationActions,
withWizard,
)(SetupOrganizationForm);

View File

@@ -5,6 +5,7 @@ import { useHistory } from "react-router-dom";
import { connect } from 'react-redux';
import WizardSetupSteps from './WizardSetupSteps';
import withSubscriptions from 'containers/Subscriptions/withSubscriptions';
import SetupSubscriptionForm from './SetupSubscriptionForm';
import SetupOrganizationForm from './SetupOrganizationForm';
@@ -23,16 +24,18 @@ function SetupRightSection ({
// #withOrganization
isOrganizationInitialized,
isOrganizationSubscribed: hasSubscriptions,
isOrganizationSeeded
isOrganizationSeeded,
// #withSubscriptions
isSubscriptionActive
}) {
const history = useHistory();
const handleSkip = useCallback(({ step, push }) => {
const scenarios = [
{ condition: !hasSubscriptions, redirectTo: 'subscription' },
{ condition: hasSubscriptions && !isOrganizationInitialized, redirectTo: 'initializing' },
{ condition: hasSubscriptions && !isOrganizationSeeded, redirectTo: 'organization' },
{ condition: !isSubscriptionActive, redirectTo: 'subscription' },
{ condition: isSubscriptionActive && !isOrganizationInitialized, redirectTo: 'initializing' },
{ condition: isSubscriptionActive && !isOrganizationSeeded, redirectTo: 'organization' },
];
const scenario = scenarios.find((scenario) => scenario.condition);
@@ -40,7 +43,7 @@ function SetupRightSection ({
push(scenario.redirectTo);
}
}, [
hasSubscriptions,
isSubscriptionActive,
isOrganizationInitialized,
isOrganizationSeeded,
]);
@@ -92,12 +95,15 @@ export default compose(
withOrganization(({
organization,
isOrganizationInitialized,
isOrganizationSubscribed,
isOrganizationSeeded,
}) => ({
organization,
isOrganizationInitialized,
isOrganizationSubscribed,
isOrganizationSeeded,
})),
withSubscriptions(({
isSubscriptionActive,
}) => ({
isSubscriptionActive
}), 'main'),
)(SetupRightSection);

View File

@@ -3,10 +3,11 @@ import * as Yup from 'yup';
import { useFormik } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Button, Intent } from '@blueprintjs/core';
import { withWizard } from 'react-albus';
import withSubscriptionsActions from 'containers/Subscriptions/withSubscriptionsActions';
import BillingPlans from 'containers/Subscriptions/billingPlans';
import BillingPeriods from 'containers/Subscriptions/billingPeriods';
import { BillingPaymentmethod } from 'containers/Subscriptions/billingPaymentmethod';
import withBillingActions from 'containers/Subscriptions/withBillingActions';
import { compose } from 'utils';
@@ -14,18 +15,23 @@ import { compose } from 'utils';
* Subscription step of wizard setup.
*/
function SetupSubscriptionForm({
//#withBillingActions
// #withBillingActions
requestSubmitBilling,
// #withWizard
wizard,
// #withSubscriptionsActions
requestFetchSubscriptions
}) {
const { formatMessage } = useIntl();
const validationSchema = Yup.object().shape({
plan_slug: Yup.string()
.required()
.label(formatMessage({ id: 'plan_slug' })),
license_code: Yup.string()
.min(7)
.max(7)
.min(10)
.max(10)
.required()
.label(formatMessage({ id: 'license_code_' }))
.trim(),
@@ -48,6 +54,10 @@ function SetupSubscriptionForm({
onSubmit: (values, { setSubmitting, setErrors }) => {
requestSubmitBilling(values)
.then((response) => {
return requestFetchSubscriptions();
})
.then(() => {
wizard.next();
setSubmitting(false);
})
.catch((errors) => {
@@ -76,4 +86,8 @@ function SetupSubscriptionForm({
);
}
export default compose(withBillingActions)(SetupSubscriptionForm);
export default compose(
withBillingActions,
withWizard,
withSubscriptionsActions,
)(SetupSubscriptionForm);

View File

@@ -45,7 +45,7 @@ function BillingPeriods({ formik, title, selected = 1 }) {
return (
<section class="billing-section">
<h1 className={'bg-title'}>
<h1>
<T id={title} />
</h1>
<p className='paragraph'>

View File

@@ -62,7 +62,7 @@ function BillingPlans({ formik, title, selected = 1 }) {
return (
<section class="billing-section">
<h1 className={'bg-title'}>
<h1>
<T id={title} />
</h1>
<p className='paragraph'>

View File

@@ -0,0 +1,22 @@
import { connect } from 'react-redux';
import {
isSubscriptionOnTrialFactory,
isSubscriptionInactiveFactory,
isSubscriptionActiveFactory,
} from 'store/subscription/subscription.selectors';
export default (mapState, slug) => {
const isSubscriptionOnTrial = isSubscriptionOnTrialFactory(slug);
const isSubscriptionInactive = isSubscriptionInactiveFactory(slug);
const isSubscriptionActive = isSubscriptionActiveFactory(slug);
const mapStateToProps = (state, props) => {
const mapped = {
isSubscriptionOnTrial: isSubscriptionOnTrial(state, props),
isSubscriptionInactive: isSubscriptionInactive(state, props),
isSubscriptionActive: isSubscriptionActive(state, props),
};
return (mapState) ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,10 @@
import { connect } from 'react-redux';
import {
fetchSubscriptions,
} from 'store/subscription/subscription.actions'
const mapDispatchToProps = (dispatch) => ({
requestFetchSubscriptions: () => dispatch(fetchSubscriptions()),
});
export default connect(null, mapDispatchToProps);

View File

@@ -765,4 +765,5 @@ export default {
something_wentwrong: 'Something went wrong.',
new_password: 'New password',
license_code_: 'License code',
legal_organization_name: 'Legal Organization Name'
};

View File

@@ -26,10 +26,12 @@ import vendors from './vendors/vendors.reducer';
import paymentReceives from './PaymentReceive/paymentReceive.reducer';
import paymentMades from './PaymentMades/paymentMade.reducer';
import organizations from './organizations/organizations.reducers';
import subscriptions from './subscription/subscription.reducer';
export default combineReducers({
authentication,
organizations,
subscriptions,
dashboard,
users,
accounts,
@@ -47,12 +49,11 @@ export default combineReducers({
exchangeRates,
globalErrors,
customers,
salesEstimates,
salesInvoices,
salesReceipts,
bills,
vendors,
paymentReceives,
paymentMades
paymentMades,
});

View File

@@ -0,0 +1,14 @@
import ApiService from 'services/ApiService';
import t from 'store/types';
export const fetchSubscriptions = () => (dispatch) => new Promise((resolve, reject) => {
ApiService.get('subscription').then((response) => {
dispatch({
type: t.SET_PLAN_SUBSCRIPTIONS_LIST,
payload: {
subscriptions: response.data.subscriptions,
},
});
resolve(response);
}).catch((error) => { reject(error); })
});

View File

@@ -0,0 +1,19 @@
import { createReducer } from '@reduxjs/toolkit';
import t from 'store/types';
const initialState = {
data: {},
};
export default createReducer(initialState, {
[t.SET_PLAN_SUBSCRIPTIONS_LIST]: (state, action) => {
const { subscriptions } = action.payload;
const _data = {};
subscriptions.forEach((subscription) => {
_data[subscription.id] = subscription;
});
state.data = _data;
},
});

View File

@@ -0,0 +1,23 @@
import { createSelector } from '@reduxjs/toolkit';
const subscriptionSelector = (slug) => (state, props) => {
const subscriptions = Object.values(state.subscriptions.data);
return subscriptions.find((subscription) => subscription.slug === slug);
};
export const isSubscriptionOnTrialFactory = (slug) => createSelector(
subscriptionSelector(slug),
(subscription) => !!subscription?.on_trial,
);
export const isSubscriptionActiveFactory = (slug) => createSelector(
subscriptionSelector(slug),
(subscription) => {
return !!subscription?.active;
}
);
export const isSubscriptionInactiveFactory = (slug) => createSelector(
subscriptionSelector(slug),
(subscription) => !!subscription?.inactive,
);

View File

@@ -0,0 +1,4 @@
export default {
SET_PLAN_SUBSCRIPTIONS_LIST: 'SET_PLAN_SUBSCRIPTIONS_LIST',
};

View File

@@ -25,6 +25,7 @@ import vendors from './vendors/vendors.types';
import paymentReceives from './PaymentReceive/paymentReceive.type';
import paymentMades from './PaymentMades/paymentMade.type';
import organizations from './organizations/organizations.types';
import subscription from './subscription/subscription.types';
export default {
...authentication,
@@ -54,4 +55,5 @@ export default {
...paymentReceives,
...paymentMades,
...organizations,
...subscription,
};

View File

@@ -20,13 +20,17 @@
h1{
font-size: 22px;
}
h1,
h3{
font-weight: 500;
color: #6d6d6d;
color: #6b7382;
}
}
&__content{
width: 70%;
padding-bottom: 40px;
}
&__left-section {
@@ -68,7 +72,7 @@
&__text {
font-size: 16px;
opacity: 0.75;
margin-bottom: 18px;
margin-bottom: 10px;
}
&__organization {
@@ -87,7 +91,7 @@
height: 3px;
width: 100px;
background: rgba(255, 255, 255, 0.15);
margin: 20px 0;
margin: 10px 0;
}
&__footer{
@@ -190,7 +194,54 @@
}
}
//Register Subscription form
.register-subscription-form {
}
.setup-organization {
width: 580px;
margin: 0 auto;
padding: 45px 0 20px;
&__title-wrap{
margin-bottom: 20px;
h1 {
margin-top: 0;
margin-bottom: 10px;
color: #565e6c;
}
}
&__form{
h3 {
margin-bottom: 1rem;
}
}
.bp3-form-group {
.bp3-input-group {
.bp3-input {
height: 38px;
}
}
}
.bp3-button:not([class*='bp3-intent-']):not(.bp3-minimal) {
width: 100%;
height: 38px;
}
.register-org-note{
font-size: 13px;
padding-bottom: 10px;
border-bottom: 1px solid #e1e1e1;
margin-bottom: 1.75rem;
color: #666;
}
.register-org-button {
.bp3-button {
background-color: #0052cc;
min-width: 175px;
height: 40px;
font-size: 15px;
}
}
}

View File

@@ -1,12 +1,17 @@
import { Router } from 'express'
import { Container, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express'
import { Container, Service, Inject } from 'typedi';
import JWTAuth from 'api/middleware/jwtAuth';
import TenancyMiddleware from 'api/middleware/TenancyMiddleware';
import AttachCurrentTenantUser from 'api/middleware/AttachCurrentTenantUser';
import PaymentViaLicenseController from 'api/controllers/Subscription/PaymentViaLicense';
import SubscriptionService from 'services/Subscription/SubscriptionService';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
@Service()
export default class SubscriptionController {
export default class SubscriptionController {
@Inject()
subscriptionService: SubscriptionService;
/**
* Router constructor.
*/
@@ -19,6 +24,26 @@ export default class SubscriptionController {
router.use('/license', Container.get(PaymentViaLicenseController).router());
router.get('/',
asyncMiddleware(this.getSubscriptions.bind(this))
);
return router;
}
/**
* Retrieve all subscriptions of the authenticated user's tenant.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getSubscriptions(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
try {
const subscriptions = await this.subscriptionService.getSubscriptions(tenantId);
return res.status(200).send({ subscriptions });
} catch (error) {
next(error);
}
}
}

View File

@@ -25,6 +25,10 @@ export default {
type: 'string',
// config: true,
},
{
key: 'financial_date_start',
type: 'string',
},
{
key: 'language',
type: 'string',

View File

@@ -100,7 +100,7 @@ export default class OrganizationService {
this.logger.info('[organization] trying to list all organizations.', { user });
const { tenantRepository } = this.sysRepositories;
const tenant = await tenantRepository.getByIdWithSubscriptions(user.tenantId);
const tenant = await tenantRepository.getById(user.tenantId);
return [tenant];
}

View File

@@ -1,5 +1,5 @@
import { Service, Inject } from 'typedi';
import { Plan, Tenant } from 'system/models';
import { Plan, PlanSubscription } from 'system/models';
import Subscription from 'services/Subscription/Subscription';
import LicensePaymentMethod from 'services/Payment/LicensePaymentMethod';
import PaymentContext from 'services/Payment';
@@ -29,7 +29,7 @@ export default class SubscriptionService {
* @param {string} licenseCode
* @return {Promise}
*/
async subscriptionViaLicense(
public async subscriptionViaLicense(
tenantId: number,
planSlug: string,
paymentModel?: ILicensePaymentModel,
@@ -53,4 +53,15 @@ export default class SubscriptionService {
tenantId, paymentModel
});
}
/**
* Retrieve all subscription of the given tenant.
* @param {number} tenantId
*/
public async getSubscriptions(tenantId: number) {
this.logger.info('[subscription] trying to get tenant subscriptions.', { tenantId });
const subscriptions = await PlanSubscription.query().where('tenant_id', tenantId);
return subscriptions;
}
}