Compare commits
35 Commits
bigcapital
...
fix-vercel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5ac758d7f | ||
|
|
19191ac089 | ||
|
|
d2ffa6e484 | ||
|
|
a7c55dea7b | ||
|
|
3b2ab568e4 | ||
|
|
8c3d6b61d6 | ||
|
|
0ce9c93077 | ||
|
|
c3a2ea5064 | ||
|
|
28de827a99 | ||
|
|
b4559703f9 | ||
|
|
7532b44a57 | ||
|
|
a142b734d3 | ||
|
|
f26ced97fe | ||
|
|
0c1bf302e5 | ||
|
|
57e3f68219 | ||
|
|
3b79ac66ae | ||
|
|
44fc26b156 | ||
|
|
d46f8faf26 | ||
|
|
2263cf5657 | ||
|
|
058d525afc | ||
|
|
490b8e09f2 | ||
|
|
e488c0eea9 | ||
|
|
f093239a15 | ||
|
|
5c537e094d | ||
|
|
a371fd44f7 | ||
|
|
59cb168331 | ||
|
|
8a5fbfc041 | ||
|
|
e3a072e267 | ||
|
|
b03606406e | ||
|
|
a1a7ee2b5b | ||
|
|
228ae71a1c | ||
|
|
71a8d3e77f | ||
|
|
4ddeb927cc | ||
|
|
708d971717 | ||
|
|
7781d092ca |
5
.github/workflows/docker-build.yml
vendored
5
.github/workflows/docker-build.yml
vendored
@@ -19,7 +19,7 @@ on:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: abouhuolia/bigcapital-client
|
||||
IMAGE_NAME: abouhuolia/bigcapital-webapp
|
||||
|
||||
jobs:
|
||||
setup-build-publish-deploy:
|
||||
@@ -50,8 +50,9 @@ jobs:
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./packages/webapp/Dockerfile
|
||||
push: true
|
||||
tags: ghcr.io/bigcapitalhq/client:latest
|
||||
tags: ghcr.io/bigcapitalhq/webapp:latest
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# Send notification to Slack channel.
|
||||
- name: Slack Notification built and published successfully.
|
||||
|
||||
5
.vercelignore
Normal file
5
.vercelignore
Normal file
@@ -0,0 +1,5 @@
|
||||
/*
|
||||
!package.json
|
||||
!package-lock.json
|
||||
!yarn.lock
|
||||
!packages/webapp
|
||||
5697
package-lock.json
generated
Normal file
5697
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import DynamicListingService from '@/services/DynamicListing/DynamicListService'
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AccountsApplication } from '@/services/Accounts/AccountsApplication';
|
||||
import { MAX_ACCOUNTS_CHART_DEPTH } from 'services/Accounts/constants';
|
||||
|
||||
@Service()
|
||||
export default class AccountsController extends BaseController {
|
||||
@@ -494,6 +495,22 @@ export default class AccountsController extends BaseController {
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL') {
|
||||
return res.boom.badRequest(
|
||||
'The parent account exceeded the depth level of accounts chart.',
|
||||
{
|
||||
errors: [
|
||||
{
|
||||
type: 'PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL',
|
||||
code: 1500,
|
||||
data: {
|
||||
maxDepth: MAX_ACCOUNTS_CHART_DEPTH,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { check, ValidationChain } from 'express-validator';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||
import SubscriptionMiddleware from '@/api/middleware/SubscriptionMiddleware';
|
||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||
import OrganizationService from '@/services/Organization/OrganizationService';
|
||||
import {
|
||||
@@ -24,7 +23,7 @@ const ACCEPTED_LOCATIONS = ['libya'];
|
||||
@Service()
|
||||
export default class OrganizationController extends BaseController {
|
||||
@Inject()
|
||||
organizationService: OrganizationService;
|
||||
private organizationService: OrganizationService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
@@ -32,13 +31,10 @@ export default class OrganizationController extends BaseController {
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
// Should before build tenant database the user be authorized and
|
||||
// most important than that, should be subscribed to any plan.
|
||||
router.use(JWTAuth);
|
||||
router.use(AttachCurrentTenantUser);
|
||||
router.use(TenancyMiddleware);
|
||||
|
||||
router.use('/build', SubscriptionMiddleware('main'));
|
||||
router.post(
|
||||
'/build',
|
||||
this.organizationValidationSchema,
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, ValidationChain } from 'express-validator';
|
||||
import BaseController from './BaseController';
|
||||
import SetupService from '@/services/Setup/SetupService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { IOrganizationSetupDTO } from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
// Middlewares
|
||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||
import SubscriptionMiddleware from '@/api/middleware/SubscriptionMiddleware';
|
||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||
import EnsureTenantIsInitialized from '@/api/middleware/EnsureTenantIsInitialized';
|
||||
import SettingsMiddleware from '@/api/middleware/SettingsMiddleware';
|
||||
|
||||
@Service()
|
||||
export default class SetupController extends BaseController {
|
||||
@Inject()
|
||||
setupService: SetupService;
|
||||
|
||||
router() {
|
||||
const router = Router('/setup');
|
||||
|
||||
router.use(JWTAuth);
|
||||
router.use(AttachCurrentTenantUser);
|
||||
router.use(TenancyMiddleware);
|
||||
router.use(SubscriptionMiddleware('main'));
|
||||
router.use(EnsureTenantIsInitialized);
|
||||
router.use(SettingsMiddleware);
|
||||
router.post(
|
||||
'/organization',
|
||||
this.organizationSetupSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.organizationSetup.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Organization setup schema.
|
||||
*/
|
||||
private get organizationSetupSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('organization_name').exists().trim(),
|
||||
check('base_currency').exists(),
|
||||
check('time_zone').exists(),
|
||||
check('fiscal_year').exists(),
|
||||
check('industry').optional(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Organization setup.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns
|
||||
*/
|
||||
async organizationSetup(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const setupDTO: IOrganizationSetupDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.setupService.organizationSetup(tenantId, setupDTO);
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'The setup settings set successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handleServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'TENANT_IS_ALREADY_SETUPED') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'TENANT_IS_ALREADY_SETUPED', code: 1000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BASE_CURRENCY_INVALID') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BASE_CURRENCY_INVALID', code: 110 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, oneOf, ValidationChain } from 'express-validator';
|
||||
import basicAuth from 'express-basic-auth';
|
||||
import config from '@/config';
|
||||
import { License } from '@/system/models';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import LicenseService from '@/services/Payment/License';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import { ILicensesFilter, ISendLicenseDTO } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class LicensesController extends BaseController {
|
||||
@Inject()
|
||||
licenseService: LicenseService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use(
|
||||
basicAuth({
|
||||
users: {
|
||||
[config.licensesAuth.user]: config.licensesAuth.password,
|
||||
},
|
||||
challenge: true,
|
||||
})
|
||||
);
|
||||
router.post(
|
||||
'/generate',
|
||||
this.generateLicenseSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.generateLicense.bind(this)),
|
||||
this.catchServiceErrors,
|
||||
);
|
||||
router.post(
|
||||
'/disable/:licenseId',
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.disableLicense.bind(this)),
|
||||
this.catchServiceErrors,
|
||||
);
|
||||
router.post(
|
||||
'/send',
|
||||
this.sendLicenseSchemaValidation,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.sendLicense.bind(this)),
|
||||
this.catchServiceErrors,
|
||||
);
|
||||
router.delete(
|
||||
'/:licenseId',
|
||||
asyncMiddleware(this.deleteLicense.bind(this)),
|
||||
this.catchServiceErrors,
|
||||
);
|
||||
router.get('/', asyncMiddleware(this.listLicenses.bind(this)));
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate license validation schema.
|
||||
*/
|
||||
get generateLicenseSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('loop').exists().isNumeric().toInt(),
|
||||
check('period').exists().isNumeric().toInt(),
|
||||
check('period_interval')
|
||||
.exists()
|
||||
.isIn(['month', 'months', 'year', 'years', 'day', 'days']),
|
||||
check('plan_slug').exists().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific license validation schema.
|
||||
*/
|
||||
get specificLicenseSchema(): ValidationChain[] {
|
||||
return [
|
||||
oneOf(
|
||||
[check('license_id').exists().isNumeric().toInt()],
|
||||
[check('license_code').exists().isNumeric().toInt()]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send license validation schema.
|
||||
*/
|
||||
get sendLicenseSchemaValidation(): ValidationChain[] {
|
||||
return [
|
||||
check('period').exists().isNumeric(),
|
||||
check('period_interval').exists().trim().escape(),
|
||||
check('plan_slug').exists().trim().escape(),
|
||||
oneOf([
|
||||
check('phone_number').exists().trim().escape(),
|
||||
check('email').exists().trim().escape(),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate licenses codes with given period in bulk.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async generateLicense(req: Request, res: Response, next: Function) {
|
||||
const { loop = 10, period, periodInterval, planSlug } = this.matchedBodyData(
|
||||
req
|
||||
);
|
||||
|
||||
try {
|
||||
await this.licenseService.generateLicenses(
|
||||
loop,
|
||||
period,
|
||||
periodInterval,
|
||||
planSlug
|
||||
);
|
||||
return res.status(200).send({
|
||||
code: 100,
|
||||
type: 'LICENSEES.GENERATED.SUCCESSFULLY',
|
||||
message: 'The licenses have been generated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the given license on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async disableLicense(req: Request, res: Response, next: Function) {
|
||||
const { licenseId } = req.params;
|
||||
|
||||
try {
|
||||
await this.licenseService.disableLicense(licenseId);
|
||||
|
||||
return res.status(200).send({ license_id: licenseId });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given license code on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async deleteLicense(req: Request, res: Response, next: Function) {
|
||||
const { licenseId } = req.params;
|
||||
|
||||
try {
|
||||
await this.licenseService.deleteLicense(licenseId);
|
||||
|
||||
return res.status(200).send({ license_id: licenseId });
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send license code in the given period to the customer via email or phone number
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async sendLicense(req: Request, res: Response, next: Function) {
|
||||
const sendLicenseDTO: ISendLicenseDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.licenseService.sendLicenseToCustomer(sendLicenseDTO);
|
||||
|
||||
return res.status(200).send({
|
||||
status: 100,
|
||||
code: 'LICENSE.CODE.SENT',
|
||||
message: 'The license has been sent to the given customer.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listing licenses.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async listLicenses(req: Request, res: Response) {
|
||||
const filter: ILicensesFilter = {
|
||||
disabled: false,
|
||||
used: false,
|
||||
sent: false,
|
||||
active: false,
|
||||
...req.query,
|
||||
};
|
||||
const licenses = await License.query().onBuild((builder) => {
|
||||
builder.modify('filter', filter);
|
||||
builder.orderBy('createdAt', 'ASC');
|
||||
});
|
||||
return res.status(200).send({ licenses });
|
||||
}
|
||||
|
||||
/**
|
||||
* Catches all service errors.
|
||||
*/
|
||||
catchServiceErrors(error, req: Request, res: Response, next: NextFunction) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'PLAN_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{
|
||||
type: 'PLAN.NOT.FOUND',
|
||||
code: 100,
|
||||
message: 'The given plan not found.',
|
||||
}],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'LICENSE_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{
|
||||
type: 'LICENSE_NOT_FOUND',
|
||||
code: 200,
|
||||
message: 'The given license id not found.'
|
||||
}],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'LICENSE_ALREADY_DISABLED') {
|
||||
return res.status(400).send({
|
||||
errors: [{
|
||||
type: 'LICENSE.ALREADY.DISABLED',
|
||||
code: 200,
|
||||
message: 'License is already disabled.'
|
||||
}],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'NO_AVALIABLE_LICENSE_CODE') {
|
||||
return res.status(400).send({
|
||||
status: 110,
|
||||
message: 'There is no licenses availiable right now with the given period and plan.',
|
||||
code: 'NO.AVALIABLE.LICENSE.CODE',
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Inject } from 'typedi';
|
||||
import { Request, Response } from 'express';
|
||||
import { Plan } from '@/system/models';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import SubscriptionService from '@/services/Subscription/SubscriptionService';
|
||||
|
||||
export default class PaymentMethodController extends BaseController {
|
||||
@Inject()
|
||||
subscriptionService: SubscriptionService;
|
||||
|
||||
/**
|
||||
* Validate the given plan slug exists on the storage.
|
||||
*
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*
|
||||
* @return {Response|void}
|
||||
*/
|
||||
async validatePlanSlugExistance(req: Request, res: Response, next: Function) {
|
||||
const { planSlug } = this.matchedBodyData(req);
|
||||
const foundPlan = await Plan.query().where('slug', planSlug).first();
|
||||
|
||||
if (!foundPlan) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PLAN.SLUG.NOT.EXISTS', code: 110 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { NextFunction, Router, Request, Response } from 'express';
|
||||
import { check } from 'express-validator';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import PaymentMethodController from '@/api/controllers/Subscription/PaymentMethod';
|
||||
import {
|
||||
NotAllowedChangeSubscriptionPlan,
|
||||
NoPaymentModelWithPricedPlan,
|
||||
PaymentAmountInvalidWithPlan,
|
||||
PaymentInputInvalid,
|
||||
VoucherCodeRequired,
|
||||
} from '@/exceptions';
|
||||
import { ILicensePaymentModel } from '@/interfaces';
|
||||
import instance from 'tsyringe/dist/typings/dependency-container';
|
||||
|
||||
@Service()
|
||||
export default class PaymentViaLicenseController extends PaymentMethodController {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/payment',
|
||||
this.paymentViaLicenseSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.validatePlanSlugExistance.bind(this)),
|
||||
asyncMiddleware(this.paymentViaLicense.bind(this)),
|
||||
this.handleErrors,
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment via license validation schema.
|
||||
*/
|
||||
get paymentViaLicenseSchema() {
|
||||
return [
|
||||
check('plan_slug').exists().trim().escape(),
|
||||
check('license_code').exists().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the subscription payment via license code.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async paymentViaLicense(req: Request, res: Response, next: Function) {
|
||||
const { planSlug, licenseCode } = this.matchedBodyData(req);
|
||||
const { tenant } = req;
|
||||
|
||||
try {
|
||||
const licenseModel: ILicensePaymentModel = { licenseCode };
|
||||
|
||||
await this.subscriptionService.subscriptionViaLicense(
|
||||
tenant.id,
|
||||
planSlug,
|
||||
licenseModel
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
code: 'PAYMENT.SUCCESSFULLY.MADE',
|
||||
message: 'Payment via license has been made successfully.',
|
||||
});
|
||||
} catch (exception) {
|
||||
next(exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handleErrors(
|
||||
exception: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const errorReasons = [];
|
||||
|
||||
if (exception instanceof VoucherCodeRequired) {
|
||||
errorReasons.push({
|
||||
type: 'VOUCHER_CODE_REQUIRED',
|
||||
code: 100,
|
||||
});
|
||||
}
|
||||
if (exception instanceof NoPaymentModelWithPricedPlan) {
|
||||
errorReasons.push({
|
||||
type: 'NO_PAYMENT_WITH_PRICED_PLAN',
|
||||
code: 140,
|
||||
});
|
||||
}
|
||||
if (exception instanceof NotAllowedChangeSubscriptionPlan) {
|
||||
errorReasons.push({
|
||||
type: 'NOT.ALLOWED.RENEW.SUBSCRIPTION.WHILE.ACTIVE',
|
||||
code: 120,
|
||||
});
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
if (exception instanceof PaymentInputInvalid) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'LICENSE.CODE.IS.INVALID', code: 120 }],
|
||||
});
|
||||
}
|
||||
if (exception instanceof PaymentAmountInvalidWithPlan) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'LICENSE.NOT.FOR.GIVEN.PLAN' }],
|
||||
});
|
||||
}
|
||||
next(exception);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
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 {
|
||||
@Inject()
|
||||
subscriptionService: SubscriptionService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use(JWTAuth);
|
||||
router.use(AttachCurrentTenantUser);
|
||||
router.use(TenancyMiddleware);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { Container } from 'typedi';
|
||||
// Middlewares
|
||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||
import SubscriptionMiddleware from '@/api/middleware/SubscriptionMiddleware';
|
||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||
import EnsureTenantIsInitialized from '@/api/middleware/EnsureTenantIsInitialized';
|
||||
import SettingsMiddleware from '@/api/middleware/SettingsMiddleware';
|
||||
@@ -37,8 +36,6 @@ import Resources from './controllers/Resources';
|
||||
import ExchangeRates from '@/api/controllers/ExchangeRates';
|
||||
import Media from '@/api/controllers/Media';
|
||||
import Ping from '@/api/controllers/Ping';
|
||||
import Subscription from '@/api/controllers/Subscription';
|
||||
import Licenses from '@/api/controllers/Subscription/Licenses';
|
||||
import InventoryAdjustments from '@/api/controllers/Inventory/InventoryAdjustments';
|
||||
import asyncRenderMiddleware from './middleware/AsyncRenderMiddleware';
|
||||
import Jobs from './controllers/Jobs';
|
||||
@@ -69,8 +66,6 @@ export default () => {
|
||||
|
||||
app.use('/auth', Container.get(Authentication).router());
|
||||
app.use('/invite', Container.get(InviteUsers).nonAuthRouter());
|
||||
app.use('/licenses', Container.get(Licenses).router());
|
||||
app.use('/subscription', Container.get(Subscription).router());
|
||||
app.use('/organization', Container.get(Organization).router());
|
||||
app.use('/ping', Container.get(Ping).router());
|
||||
app.use('/jobs', Container.get(Jobs).router());
|
||||
@@ -83,7 +78,6 @@ export default () => {
|
||||
dashboard.use(JWTAuth);
|
||||
dashboard.use(AttachCurrentTenantUser);
|
||||
dashboard.use(TenancyMiddleware);
|
||||
dashboard.use(SubscriptionMiddleware('main'));
|
||||
dashboard.use(EnsureTenantIsInitialized);
|
||||
dashboard.use(SettingsMiddleware);
|
||||
dashboard.use(I18nAuthenticatedMiddlware);
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
|
||||
export default (subscriptionSlug = 'main') => async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenant, tenantId } = req;
|
||||
const Logger = Container.get('logger');
|
||||
const { subscriptionRepository } = Container.get('repositories');
|
||||
|
||||
if (!tenant) {
|
||||
throw new Error('Should load `TenancyMiddlware` before this middleware.');
|
||||
}
|
||||
Logger.info('[subscription_middleware] trying get tenant main subscription.');
|
||||
const subscription = await subscriptionRepository.getBySlugInTenant(
|
||||
subscriptionSlug,
|
||||
tenantId
|
||||
);
|
||||
// Validate in case there is no any already subscription.
|
||||
if (!subscription) {
|
||||
Logger.info('[subscription_middleware] tenant has no subscription.', {
|
||||
tenantId,
|
||||
});
|
||||
return res.boom.badRequest('Tenant has no subscription.', {
|
||||
errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }],
|
||||
});
|
||||
}
|
||||
// Validate in case the subscription is inactive.
|
||||
else if (subscription.inactive()) {
|
||||
Logger.info(
|
||||
'[subscription_middleware] tenant main subscription is expired.',
|
||||
{ tenantId }
|
||||
);
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
|
||||
export default class NoPaymentModelWithPricedPlan {
|
||||
|
||||
constructor() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
|
||||
export default class NotAllowedChangeSubscriptionPlan {
|
||||
|
||||
constructor() {
|
||||
this.name = "NotAllowedChangeSubscriptionPlan";
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
|
||||
|
||||
export default class PaymentAmountInvalidWithPlan{
|
||||
constructor() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default class PaymentInputInvalid {
|
||||
constructor() {}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export default class VoucherCodeRequired {
|
||||
constructor() {
|
||||
this.name = 'VoucherCodeRequired';
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,15 @@
|
||||
import NotAllowedChangeSubscriptionPlan from './NotAllowedChangeSubscriptionPlan';
|
||||
import ServiceError from './ServiceError';
|
||||
import ServiceErrors from './ServiceErrors';
|
||||
import NoPaymentModelWithPricedPlan from './NoPaymentModelWithPricedPlan';
|
||||
import PaymentInputInvalid from './PaymentInputInvalid';
|
||||
import PaymentAmountInvalidWithPlan from './PaymentAmountInvalidWithPlan';
|
||||
import TenantAlreadyInitialized from './TenantAlreadyInitialized';
|
||||
import TenantAlreadySeeded from './TenantAlreadySeeded';
|
||||
import TenantDBAlreadyExists from './TenantDBAlreadyExists';
|
||||
import TenantDatabaseNotBuilt from './TenantDatabaseNotBuilt';
|
||||
import VoucherCodeRequired from './VoucherCodeRequired';
|
||||
|
||||
export {
|
||||
NotAllowedChangeSubscriptionPlan,
|
||||
NoPaymentModelWithPricedPlan,
|
||||
PaymentAmountInvalidWithPlan,
|
||||
ServiceError,
|
||||
ServiceErrors,
|
||||
PaymentInputInvalid,
|
||||
TenantAlreadyInitialized,
|
||||
TenantAlreadySeeded,
|
||||
TenantDBAlreadyExists,
|
||||
TenantDatabaseNotBuilt,
|
||||
VoucherCodeRequired,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import Container from 'typedi';
|
||||
import SubscriptionService from '@/services/Subscription/Subscription';
|
||||
|
||||
export default class MailNotificationSubscribeEnd {
|
||||
/**
|
||||
* Job handler.
|
||||
* @param {Job} job -
|
||||
*/
|
||||
handler(job) {
|
||||
const { tenantId, phoneNumber, remainingDays } = job.attrs.data;
|
||||
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
Logger.info(
|
||||
`Send mail notification subscription end soon - started: ${job.attrs.data}`
|
||||
);
|
||||
|
||||
try {
|
||||
subscriptionService.mailMessages.sendRemainingTrialPeriod(
|
||||
phoneNumber,
|
||||
remainingDays
|
||||
);
|
||||
Logger.info(
|
||||
`Send mail notification subscription end soon - finished: ${job.attrs.data}`
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.info(
|
||||
`Send mail notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`
|
||||
);
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import Container from 'typedi';
|
||||
import SubscriptionService from '@/services/Subscription/Subscription';
|
||||
|
||||
export default class MailNotificationTrialEnd {
|
||||
/**
|
||||
*
|
||||
* @param {Job} job -
|
||||
*/
|
||||
handler(job) {
|
||||
const { tenantId, phoneNumber, remainingDays } = job.attrs.data;
|
||||
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
Logger.info(
|
||||
`Send mail notification subscription end soon - started: ${job.attrs.data}`
|
||||
);
|
||||
|
||||
try {
|
||||
subscriptionService.mailMessages.sendRemainingTrialPeriod(
|
||||
phoneNumber,
|
||||
remainingDays
|
||||
);
|
||||
Logger.info(
|
||||
`Send mail notification subscription end soon - finished: ${job.attrs.data}`
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.info(
|
||||
`Send mail notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`
|
||||
);
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import Container from 'typedi';
|
||||
import SubscriptionService from '@/services/Subscription/Subscription';
|
||||
|
||||
export default class SMSNotificationSubscribeEnd {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Job}job
|
||||
*/
|
||||
handler(job) {
|
||||
const { tenantId, phoneNumber, remainingDays } = job.attrs.data;
|
||||
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
Logger.info(`Send SMS notification subscription end soon - started: ${job.attrs.data}`);
|
||||
|
||||
try {
|
||||
subscriptionService.smsMessages.sendRemainingSubscriptionPeriod(
|
||||
phoneNumber, remainingDays,
|
||||
);
|
||||
Logger.info(`Send SMS notification subscription end soon - finished: ${job.attrs.data}`);
|
||||
} catch(error) {
|
||||
Logger.info(`Send SMS notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import Container from 'typedi';
|
||||
import SubscriptionService from '@/services/Subscription/Subscription';
|
||||
|
||||
export default class SMSNotificationTrialEnd {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Job}job
|
||||
*/
|
||||
handler(job) {
|
||||
const { tenantId, phoneNumber, remainingDays } = job.attrs.data;
|
||||
|
||||
const subscriptionService = Container.get(SubscriptionService);
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
Logger.info(`Send notification subscription end soon - started: ${job.attrs.data}`);
|
||||
|
||||
try {
|
||||
subscriptionService.smsMessages.sendRemainingTrialPeriod(
|
||||
phoneNumber, remainingDays,
|
||||
);
|
||||
Logger.info(`Send notification subscription end soon - finished: ${job.attrs.data}`);
|
||||
} catch(error) {
|
||||
Logger.info(`Send notification subscription end soon - failed: ${job.attrs.data}, error: ${e}`);
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Container } from 'typedi';
|
||||
import LicenseService from '@/services/Payment/License';
|
||||
|
||||
export default class SendLicenseViaEmailJob {
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param agenda
|
||||
*/
|
||||
constructor(agenda) {
|
||||
agenda.define(
|
||||
'send-license-via-email',
|
||||
{ priority: 'high', concurrency: 1, },
|
||||
this.handler,
|
||||
);
|
||||
}
|
||||
|
||||
public async handler(job, done: Function): Promise<void> {
|
||||
const Logger = Container.get('logger');
|
||||
const licenseService = Container.get(LicenseService);
|
||||
const { email, licenseCode } = job.attrs.data;
|
||||
|
||||
Logger.info(`[send_license_via_mail] started: ${job.attrs.data}`);
|
||||
|
||||
try {
|
||||
await licenseService.mailMessages.sendMailLicense(licenseCode, email);
|
||||
Logger.info(`[send_license_via_mail] completed: ${job.attrs.data}`);
|
||||
done();
|
||||
} catch(e) {
|
||||
Logger.error(`[send_license_via_mail] ${job.attrs.data}, error: ${e}`);
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Container } from 'typedi';
|
||||
import LicenseService from '@/services/Payment/License';
|
||||
|
||||
export default class SendLicenseViaPhoneJob {
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor(agenda) {
|
||||
agenda.define(
|
||||
'send-license-via-phone',
|
||||
{ priority: 'high', concurrency: 1, },
|
||||
this.handler,
|
||||
);
|
||||
}
|
||||
|
||||
public async handler(job, done: Function): Promise<void> {
|
||||
const { phoneNumber, licenseCode } = job.attrs.data;
|
||||
|
||||
const Logger = Container.get('logger');
|
||||
const licenseService = Container.get(LicenseService);
|
||||
|
||||
Logger.debug(`Send license via phone number - started: ${job.attrs.data}`);
|
||||
|
||||
try {
|
||||
await licenseService.smsMessages.sendLicenseSMSMessage(phoneNumber, licenseCode);
|
||||
Logger.debug(`Send license via phone number - completed: ${job.attrs.data}`);
|
||||
done();
|
||||
} catch(e) {
|
||||
Logger.error(`Send license via phone number: ${job.attrs.data}, error: ${e}`);
|
||||
done(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,6 @@ import WelcomeSMSJob from 'jobs/WelcomeSMS';
|
||||
import ResetPasswordMailJob from 'jobs/ResetPasswordMail';
|
||||
import ComputeItemCost from 'jobs/ComputeItemCost';
|
||||
import RewriteInvoicesJournalEntries from 'jobs/writeInvoicesJEntries';
|
||||
import SendLicenseViaPhoneJob from 'jobs/SendLicensePhone';
|
||||
import SendLicenseViaEmailJob from 'jobs/SendLicenseEmail';
|
||||
import SendSMSNotificationSubscribeEnd from 'jobs/SMSNotificationSubscribeEnd';
|
||||
import SendSMSNotificationTrialEnd from 'jobs/SMSNotificationTrialEnd';
|
||||
import SendMailNotificationSubscribeEnd from 'jobs/MailNotificationSubscribeEnd';
|
||||
import SendMailNotificationTrialEnd from 'jobs/MailNotificationTrialEnd';
|
||||
import UserInviteMailJob from 'jobs/UserInviteMail';
|
||||
import OrganizationSetupJob from 'jobs/OrganizationSetup';
|
||||
import OrganizationUpgrade from 'jobs/OrganizationUpgrade';
|
||||
@@ -20,33 +14,11 @@ export default ({ agenda }: { agenda: Agenda }) => {
|
||||
new ResetPasswordMailJob(agenda);
|
||||
new WelcomeSMSJob(agenda);
|
||||
new UserInviteMailJob(agenda);
|
||||
new SendLicenseViaEmailJob(agenda);
|
||||
new SendLicenseViaPhoneJob(agenda);
|
||||
new ComputeItemCost(agenda);
|
||||
new RewriteInvoicesJournalEntries(agenda);
|
||||
new OrganizationSetupJob(agenda);
|
||||
new OrganizationUpgrade(agenda);
|
||||
new SmsNotification(agenda);
|
||||
|
||||
agenda.define(
|
||||
'send-sms-notification-subscribe-end',
|
||||
{ priority: 'nromal', concurrency: 1, },
|
||||
new SendSMSNotificationSubscribeEnd().handler,
|
||||
);
|
||||
agenda.define(
|
||||
'send-sms-notification-trial-end',
|
||||
{ priority: 'normal', concurrency: 1, },
|
||||
new SendSMSNotificationTrialEnd().handler,
|
||||
);
|
||||
agenda.define(
|
||||
'send-mail-notification-subscribe-end',
|
||||
{ priority: 'high', concurrency: 1, },
|
||||
new SendMailNotificationSubscribeEnd().handler
|
||||
);
|
||||
agenda.define(
|
||||
'send-mail-notification-trial-end',
|
||||
{ priority: 'high', concurrency: 1, },
|
||||
new SendMailNotificationTrialEnd().handler
|
||||
);
|
||||
agenda.start();
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Container from 'typedi';
|
||||
import {
|
||||
SystemUserRepository,
|
||||
SubscriptionRepository,
|
||||
TenantRepository,
|
||||
} from '@/system/repositories';
|
||||
|
||||
@@ -11,7 +10,6 @@ export default () => {
|
||||
|
||||
return {
|
||||
systemUserRepository: new SystemUserRepository(knex, cache),
|
||||
subscriptionRepository: new SubscriptionRepository(knex, cache),
|
||||
tenantRepository: new TenantRepository(knex, cache),
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { IAccountDTO, IAccount, IAccountCreateDTO } from '@/interfaces';
|
||||
import AccountTypesUtils from '@/lib/AccountTypes';
|
||||
import { ERRORS } from './constants';
|
||||
import { ERRORS, MAX_ACCOUNTS_CHART_DEPTH } from './constants';
|
||||
|
||||
@Service()
|
||||
export class CommandAccountValidators {
|
||||
@@ -154,13 +154,13 @@ export class CommandAccountValidators {
|
||||
* parent account.
|
||||
* @param {IAccountCreateDTO} accountDTO
|
||||
* @param {IAccount} parentAccount
|
||||
* @param {string} baseCurrency -
|
||||
* @param {string} baseCurrency -
|
||||
* @throws {ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT)}
|
||||
*/
|
||||
public validateCurrentSameParentAccount = (
|
||||
accountDTO: IAccountCreateDTO,
|
||||
parentAccount: IAccount,
|
||||
baseCurrency: string,
|
||||
baseCurrency: string
|
||||
) => {
|
||||
// If the account DTO currency not assigned and the parent account has no base currency.
|
||||
if (
|
||||
@@ -208,4 +208,24 @@ export class CommandAccountValidators {
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the max depth level of accounts chart.
|
||||
* @param {numebr} tenantId - Tenant id.
|
||||
* @param {number} parentAccountId - Parent account id.
|
||||
*/
|
||||
public async validateMaxParentAccountDepthLevels(
|
||||
tenantId: number,
|
||||
parentAccountId: number
|
||||
) {
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
const parentDependantsIds = accountsGraph.dependantsOf(parentAccountId);
|
||||
|
||||
if (parentDependantsIds.length >= MAX_ACCOUNTS_CHART_DEPTH) {
|
||||
throw new ServiceError(ERRORS.PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,11 @@ export class CreateAccount {
|
||||
parentAccount,
|
||||
baseCurrency
|
||||
);
|
||||
// Validates the max depth level of accounts chart.
|
||||
await this.validator.validateMaxParentAccountDepthLevels(
|
||||
tenantId,
|
||||
accountDTO.parentAccountId
|
||||
);
|
||||
}
|
||||
// Validates the given account type supports the multi-currency.
|
||||
this.validator.validateAccountTypeSupportCurrency(accountDTO, baseCurrency);
|
||||
|
||||
@@ -5,6 +5,7 @@ import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { AccountTransformer } from './AccountTransform';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { flatToNestedArray } from '@/utils';
|
||||
|
||||
@Service()
|
||||
export class GetAccounts {
|
||||
@@ -53,11 +54,17 @@ export class GetAccounts {
|
||||
builder.modify('inactiveMode', filter.inactiveMode);
|
||||
});
|
||||
// Retrievs the formatted accounts collection.
|
||||
const transformedAccounts = await this.transformer.transform(
|
||||
const preTransformedAccounts = await this.transformer.transform(
|
||||
tenantId,
|
||||
accounts,
|
||||
new AccountTransformer()
|
||||
);
|
||||
// Transform accounts to nested array.
|
||||
const transformedAccounts = flatToNestedArray(preTransformedAccounts, {
|
||||
id: 'id',
|
||||
parentId: 'parentAccountId',
|
||||
});
|
||||
|
||||
return {
|
||||
accounts: transformedAccounts,
|
||||
filterMeta: dynamicList.getResponseMeta(),
|
||||
|
||||
@@ -13,8 +13,12 @@ export const ERRORS = {
|
||||
CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE:
|
||||
'close_account_and_to_account_not_same_type',
|
||||
ACCOUNTS_NOT_FOUND: 'accounts_not_found',
|
||||
ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY: 'ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY',
|
||||
ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT: 'ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT',
|
||||
ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY:
|
||||
'ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY',
|
||||
ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT:
|
||||
'ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT',
|
||||
PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL:
|
||||
'PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL',
|
||||
};
|
||||
|
||||
// Default views columns.
|
||||
@@ -27,6 +31,8 @@ export const DEFAULT_VIEW_COLUMNS = [
|
||||
{ key: 'currencyCode', label: 'Currency' },
|
||||
];
|
||||
|
||||
export const MAX_ACCOUNTS_CHART_DEPTH = 5;
|
||||
|
||||
// Accounts default views.
|
||||
export const DEFAULT_VIEWS = [
|
||||
{
|
||||
@@ -43,7 +49,12 @@ export const DEFAULT_VIEWS = [
|
||||
slug: 'liabilities',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'liability' },
|
||||
{
|
||||
fieldKey: 'root_type',
|
||||
index: 1,
|
||||
comparator: 'equals',
|
||||
value: 'liability',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
@@ -52,7 +63,12 @@ export const DEFAULT_VIEWS = [
|
||||
slug: 'equity',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'equity' },
|
||||
{
|
||||
fieldKey: 'root_type',
|
||||
index: 1,
|
||||
comparator: 'equals',
|
||||
value: 'equity',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
@@ -61,7 +77,12 @@ export const DEFAULT_VIEWS = [
|
||||
slug: 'income',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'income' },
|
||||
{
|
||||
fieldKey: 'root_type',
|
||||
index: 1,
|
||||
comparator: 'equals',
|
||||
value: 'income',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
@@ -70,7 +91,12 @@ export const DEFAULT_VIEWS = [
|
||||
slug: 'expenses',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'expense' },
|
||||
{
|
||||
fieldKey: 'root_type',
|
||||
index: 1,
|
||||
comparator: 'equals',
|
||||
value: 'expense',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
|
||||
@@ -150,7 +150,6 @@ export default class OrganizationService {
|
||||
public async currentOrganization(tenantId: number): Promise<ITenant> {
|
||||
const tenant = await Tenant.query()
|
||||
.findById(tenantId)
|
||||
.withGraphFetched('subscriptions')
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
this.throwIfTenantNotExists(tenant);
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
import { Service, Container, Inject } from 'typedi';
|
||||
import cryptoRandomString from 'crypto-random-string';
|
||||
import { times } from 'lodash';
|
||||
import { License, Plan } from '@/system/models';
|
||||
import { ILicense, ISendLicenseDTO } from '@/interfaces';
|
||||
import LicenseMailMessages from '@/services/Payment/LicenseMailMessages';
|
||||
import LicenseSMSMessages from '@/services/Payment/LicenseSMSMessages';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
|
||||
const ERRORS = {
|
||||
PLAN_NOT_FOUND: 'PLAN_NOT_FOUND',
|
||||
LICENSE_NOT_FOUND: 'LICENSE_NOT_FOUND',
|
||||
LICENSE_ALREADY_DISABLED: 'LICENSE_ALREADY_DISABLED',
|
||||
NO_AVALIABLE_LICENSE_CODE: 'NO_AVALIABLE_LICENSE_CODE',
|
||||
};
|
||||
|
||||
@Service()
|
||||
export default class LicenseService {
|
||||
@Inject()
|
||||
smsMessages: LicenseSMSMessages;
|
||||
|
||||
@Inject()
|
||||
mailMessages: LicenseMailMessages;
|
||||
|
||||
/**
|
||||
* Validate the plan existance on the storage.
|
||||
* @param {number} tenantId -
|
||||
* @param {string} planSlug - Plan slug.
|
||||
*/
|
||||
private async getPlanOrThrowError(planSlug: string) {
|
||||
const foundPlan = await Plan.query().where('slug', planSlug).first();
|
||||
|
||||
if (!foundPlan) {
|
||||
throw new ServiceError(ERRORS.PLAN_NOT_FOUND);
|
||||
}
|
||||
return foundPlan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valdiate the license existance on the storage.
|
||||
* @param {number} licenseId - License id.
|
||||
*/
|
||||
private async getLicenseOrThrowError(licenseId: number) {
|
||||
const foundLicense = await License.query().findById(licenseId);
|
||||
|
||||
if (!foundLicense) {
|
||||
throw new ServiceError(ERRORS.LICENSE_NOT_FOUND);
|
||||
}
|
||||
return foundLicense;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates whether the license id is disabled.
|
||||
* @param {ILicense} license
|
||||
*/
|
||||
private validateNotDisabledLicense(license: ILicense) {
|
||||
if (license.disabledAt) {
|
||||
throw new ServiceError(ERRORS.LICENSE_ALREADY_DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the license code in the given period.
|
||||
* @param {number} licensePeriod
|
||||
* @return {Promise<ILicense>}
|
||||
*/
|
||||
public async generateLicense(
|
||||
licensePeriod: number,
|
||||
periodInterval: string = 'days',
|
||||
planSlug: string
|
||||
): ILicense {
|
||||
let licenseCode: string;
|
||||
let repeat: boolean = true;
|
||||
|
||||
// Retrieve plan or throw not found error.
|
||||
const plan = await this.getPlanOrThrowError(planSlug);
|
||||
|
||||
while (repeat) {
|
||||
licenseCode = cryptoRandomString({ length: 10, type: 'numeric' });
|
||||
const foundLicenses = await License.query().where(
|
||||
'license_code',
|
||||
licenseCode
|
||||
);
|
||||
|
||||
if (foundLicenses.length === 0) {
|
||||
repeat = false;
|
||||
}
|
||||
}
|
||||
return License.query().insert({
|
||||
licenseCode,
|
||||
licensePeriod,
|
||||
periodInterval,
|
||||
planId: plan.id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates licenses.
|
||||
* @param {number} loop
|
||||
* @param {number} licensePeriod
|
||||
* @param {string} periodInterval
|
||||
* @param {number} planId
|
||||
*/
|
||||
public async generateLicenses(
|
||||
loop = 1,
|
||||
licensePeriod: number,
|
||||
periodInterval: string = 'days',
|
||||
planSlug: string
|
||||
) {
|
||||
const asyncOpers: Promise<any>[] = [];
|
||||
|
||||
times(loop, () => {
|
||||
const generateOper = this.generateLicense(
|
||||
licensePeriod,
|
||||
periodInterval,
|
||||
planSlug
|
||||
);
|
||||
asyncOpers.push(generateOper);
|
||||
});
|
||||
return Promise.all(asyncOpers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the given license id on the storage.
|
||||
* @param {string} licenseSlug - License slug.
|
||||
* @return {Promise}
|
||||
*/
|
||||
public async disableLicense(licenseId: number) {
|
||||
const license = await this.getLicenseOrThrowError(licenseId);
|
||||
|
||||
this.validateNotDisabledLicense(license);
|
||||
|
||||
return License.markLicenseAsDisabled(license.id, 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given license id from the storage.
|
||||
* @param licenseSlug {string} - License slug.
|
||||
*/
|
||||
public async deleteLicense(licenseSlug: string) {
|
||||
const license = await this.getPlanOrThrowError(licenseSlug);
|
||||
|
||||
return License.query().where('id', license.id).delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends license code to the given customer via SMS or mail message.
|
||||
* @param {string} licenseCode - License code.
|
||||
* @param {string} phoneNumber - Phone number.
|
||||
* @param {string} email - Email address.
|
||||
*/
|
||||
public async sendLicenseToCustomer(sendLicense: ISendLicenseDTO) {
|
||||
const agenda = Container.get('agenda');
|
||||
const { phoneNumber, email, period, periodInterval } = sendLicense;
|
||||
|
||||
// Retreive plan details byt the given plan slug.
|
||||
const plan = await this.getPlanOrThrowError(sendLicense.planSlug);
|
||||
|
||||
const license = await License.query()
|
||||
.modify('filterActiveLicense')
|
||||
.where('license_period', period)
|
||||
.where('period_interval', periodInterval)
|
||||
.where('plan_id', plan.id)
|
||||
.first();
|
||||
|
||||
if (!license) {
|
||||
throw new ServiceError(ERRORS.NO_AVALIABLE_LICENSE_CODE)
|
||||
}
|
||||
// Mark the license as used.
|
||||
await License.markLicenseAsSent(license.licenseCode);
|
||||
|
||||
if (sendLicense.email) {
|
||||
await agenda.schedule('1 second', 'send-license-via-email', {
|
||||
licenseCode: license.licenseCode,
|
||||
email,
|
||||
});
|
||||
}
|
||||
if (phoneNumber) {
|
||||
await agenda.schedule('1 second', 'send-license-via-phone', {
|
||||
licenseCode: license.licenseCode,
|
||||
phoneNumber,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Container } from 'typedi';
|
||||
import Mail from '@/lib/Mail';
|
||||
import config from '@/config';
|
||||
export default class SubscriptionMailMessages {
|
||||
/**
|
||||
* Send license code to the given mail address.
|
||||
* @param {string} licenseCode
|
||||
* @param {email} email
|
||||
*/
|
||||
public async sendMailLicense(licenseCode: string, email: string) {
|
||||
const Logger = Container.get('logger');
|
||||
|
||||
const mail = new Mail()
|
||||
.setView('mail/LicenseReceive.html')
|
||||
.setSubject('Bigcapital - License code')
|
||||
.setTo(email)
|
||||
.setData({
|
||||
licenseCode,
|
||||
successEmail: config.customerSuccess.email,
|
||||
successPhoneNumber: config.customerSuccess.phoneNumber,
|
||||
});
|
||||
|
||||
await mail.send();
|
||||
Logger.info('[license_mail] sent successfully.');
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { License } from '@/system/models';
|
||||
import PaymentMethod from '@/services/Payment/PaymentMethod';
|
||||
import { Plan } from '@/system/models';
|
||||
import { IPaymentMethod, ILicensePaymentModel } from '@/interfaces';
|
||||
import {
|
||||
PaymentInputInvalid,
|
||||
PaymentAmountInvalidWithPlan,
|
||||
VoucherCodeRequired,
|
||||
} from '@/exceptions';
|
||||
|
||||
export default class LicensePaymentMethod
|
||||
extends PaymentMethod
|
||||
implements IPaymentMethod
|
||||
{
|
||||
/**
|
||||
* Payment subscription of organization via license code.
|
||||
* @param {ILicensePaymentModel} licensePaymentModel -
|
||||
*/
|
||||
public async payment(licensePaymentModel: ILicensePaymentModel, plan: Plan) {
|
||||
this.validateLicensePaymentModel(licensePaymentModel);
|
||||
|
||||
const license = await this.getLicenseOrThrowInvalid(licensePaymentModel);
|
||||
this.validatePaymentAmountWithPlan(license, plan);
|
||||
|
||||
// Mark the license code as used.
|
||||
return License.markLicenseAsUsed(licensePaymentModel.licenseCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the license code activation on the storage.
|
||||
* @param {ILicensePaymentModel} licensePaymentModel -
|
||||
*/
|
||||
private async getLicenseOrThrowInvalid(
|
||||
licensePaymentModel: ILicensePaymentModel
|
||||
) {
|
||||
const foundLicense = await License.query()
|
||||
.modify('filterActiveLicense')
|
||||
.where('license_code', licensePaymentModel.licenseCode)
|
||||
.first();
|
||||
|
||||
if (!foundLicense) {
|
||||
throw new PaymentInputInvalid();
|
||||
}
|
||||
return foundLicense;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the payment amount with given plan price.
|
||||
* @param {License} license
|
||||
* @param {Plan} plan
|
||||
*/
|
||||
private validatePaymentAmountWithPlan(license: License, plan: Plan) {
|
||||
if (license.planId !== plan.id) {
|
||||
throw new PaymentAmountInvalidWithPlan();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate voucher payload.
|
||||
* @param {ILicensePaymentModel} licenseModel -
|
||||
*/
|
||||
private validateLicensePaymentModel(licenseModel: ILicensePaymentModel) {
|
||||
if (!licenseModel || !licenseModel.licenseCode) {
|
||||
throw new VoucherCodeRequired();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Container, Inject } from 'typedi';
|
||||
import SMSClient from '@/services/SMSClient';
|
||||
|
||||
export default class SubscriptionSMSMessages {
|
||||
@Inject('SMSClient')
|
||||
smsClient: SMSClient;
|
||||
|
||||
/**
|
||||
* Sends license code to the given phone number via SMS message.
|
||||
* @param {string} phoneNumber
|
||||
* @param {string} licenseCode
|
||||
*/
|
||||
public async sendLicenseSMSMessage(phoneNumber: string, licenseCode: string) {
|
||||
const message: string = `Your license card number: ${licenseCode}. If you need any help please contact us. Bigcapital.`;
|
||||
return this.smsClient.sendMessage(phoneNumber, message);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import { IPaymentModel } from '@/interfaces';
|
||||
|
||||
export default class PaymentMethod implements IPaymentModel {
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { IPaymentMethod, IPaymentContext } from "interfaces";
|
||||
import { Plan } from '@/system/models';
|
||||
|
||||
export default class PaymentContext<PaymentModel> implements IPaymentContext{
|
||||
paymentMethod: IPaymentMethod;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IPaymentMethod} paymentMethod
|
||||
*/
|
||||
constructor(paymentMethod: IPaymentMethod) {
|
||||
this.paymentMethod = paymentMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {<PaymentModel>} paymentModel
|
||||
*/
|
||||
makePayment(paymentModel: PaymentModel, plan: Plan) {
|
||||
return this.paymentMethod.payment(paymentModel, plan);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Service } from "typedi";
|
||||
|
||||
@Service()
|
||||
export default class SubscriptionMailMessages {
|
||||
/**
|
||||
*
|
||||
* @param phoneNumber
|
||||
* @param remainingDays
|
||||
*/
|
||||
public async sendRemainingSubscriptionPeriod(phoneNumber: string, remainingDays: number) {
|
||||
const message: string = `
|
||||
Your remaining subscription is ${remainingDays} days,
|
||||
please renew your subscription before expire.
|
||||
`;
|
||||
this.smsClient.sendMessage(phoneNumber, message);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param phoneNumber
|
||||
* @param remainingDays
|
||||
*/
|
||||
public async sendRemainingTrialPeriod(phoneNumber: string, remainingDays: number) {
|
||||
const message: string = `
|
||||
Your remaining free trial is ${remainingDays} days,
|
||||
please subscription before ends, if you have any quation to contact us.`;
|
||||
|
||||
this.smsClient.sendMessage(phoneNumber, message);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import SMSClient from '@/services/SMSClient';
|
||||
|
||||
@Service()
|
||||
export default class SubscriptionSMSMessages {
|
||||
@Inject('SMSClient')
|
||||
smsClient: SMSClient;
|
||||
|
||||
/**
|
||||
* Send remaining subscription period SMS message.
|
||||
* @param {string} phoneNumber -
|
||||
* @param {number} remainingDays -
|
||||
*/
|
||||
public async sendRemainingSubscriptionPeriod(
|
||||
phoneNumber: string,
|
||||
remainingDays: number
|
||||
): Promise<void> {
|
||||
const message: string = `
|
||||
Your remaining subscription is ${remainingDays} days,
|
||||
please renew your subscription before expire.
|
||||
`;
|
||||
this.smsClient.sendMessage(phoneNumber, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send remaining trial period SMS message.
|
||||
* @param {string} phoneNumber -
|
||||
* @param {number} remainingDays -
|
||||
*/
|
||||
public async sendRemainingTrialPeriod(
|
||||
phoneNumber: string,
|
||||
remainingDays: number
|
||||
): Promise<void> {
|
||||
const message: string = `
|
||||
Your remaining free trial is ${remainingDays} days,
|
||||
please subscription before ends, if you have any quation to contact us.`;
|
||||
|
||||
this.smsClient.sendMessage(phoneNumber, message);
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { Inject } from 'typedi';
|
||||
import { Tenant, Plan } from '@/system/models';
|
||||
import { IPaymentContext } from '@/interfaces';
|
||||
import { NotAllowedChangeSubscriptionPlan } from '@/exceptions';
|
||||
|
||||
export default class Subscription<PaymentModel> {
|
||||
paymentContext: IPaymentContext | null;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IPaymentContext}
|
||||
*/
|
||||
constructor(payment?: IPaymentContext) {
|
||||
this.paymentContext = payment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Give the tenant a new subscription.
|
||||
* @param {Tenant} tenant
|
||||
* @param {Plan} plan
|
||||
* @param {string} invoiceInterval
|
||||
* @param {number} invoicePeriod
|
||||
* @param {string} subscriptionSlug
|
||||
*/
|
||||
protected async newSubscribtion(
|
||||
tenant,
|
||||
plan,
|
||||
invoiceInterval: string,
|
||||
invoicePeriod: number,
|
||||
subscriptionSlug: string = 'main'
|
||||
) {
|
||||
const subscription = await tenant
|
||||
.$relatedQuery('subscriptions')
|
||||
.modify('subscriptionBySlug', subscriptionSlug)
|
||||
.first();
|
||||
|
||||
// No allowed to re-new the the subscription while the subscription is active.
|
||||
if (subscription && subscription.active()) {
|
||||
throw new NotAllowedChangeSubscriptionPlan();
|
||||
|
||||
// In case there is already subscription associated to the given tenant renew it.
|
||||
} else if (subscription && subscription.inactive()) {
|
||||
await subscription.renew(invoiceInterval, invoicePeriod);
|
||||
|
||||
// No stored past tenant subscriptions create new one.
|
||||
} else {
|
||||
await tenant.newSubscription(
|
||||
plan.id,
|
||||
invoiceInterval,
|
||||
invoicePeriod,
|
||||
subscriptionSlug
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscripe to the given plan.
|
||||
* @param {Plan} plan
|
||||
* @throws {NotAllowedChangeSubscriptionPlan}
|
||||
*/
|
||||
public async subscribe(
|
||||
tenant: Tenant,
|
||||
plan: Plan,
|
||||
paymentModel?: PaymentModel,
|
||||
subscriptionSlug: string = 'main'
|
||||
) {
|
||||
await this.paymentContext.makePayment(paymentModel, plan);
|
||||
|
||||
return this.newSubscribtion(
|
||||
tenant,
|
||||
plan,
|
||||
plan.invoiceInterval,
|
||||
plan.invoicePeriod,
|
||||
subscriptionSlug
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import moment from 'moment';
|
||||
|
||||
export default class SubscriptionPeriod {
|
||||
start: Date;
|
||||
end: Date;
|
||||
interval: string;
|
||||
count: number;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {string} interval -
|
||||
* @param {number} count -
|
||||
* @param {Date} start -
|
||||
*/
|
||||
constructor(interval: string = 'month', count: number, start?: Date) {
|
||||
this.interval = interval;
|
||||
this.count = count;
|
||||
this.start = start;
|
||||
|
||||
if (!start) {
|
||||
this.start = moment().toDate();
|
||||
}
|
||||
this.end = moment(start).add(count, interval).toDate();
|
||||
}
|
||||
|
||||
getStartDate() {
|
||||
return this.start;
|
||||
}
|
||||
|
||||
getEndDate() {
|
||||
return this.end;
|
||||
}
|
||||
|
||||
getInterval() {
|
||||
return this.interval;
|
||||
}
|
||||
|
||||
getIntervalCount() {
|
||||
return this.interval;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Plan, PlanSubscription, Tenant } from '@/system/models';
|
||||
import Subscription from '@/services/Subscription/Subscription';
|
||||
import LicensePaymentMethod from '@/services/Payment/LicensePaymentMethod';
|
||||
import PaymentContext from '@/services/Payment';
|
||||
import SubscriptionSMSMessages from '@/services/Subscription/SMSMessages';
|
||||
import SubscriptionMailMessages from '@/services/Subscription/MailMessages';
|
||||
import { ILicensePaymentModel } from '@/interfaces';
|
||||
import SubscriptionViaLicense from './SubscriptionViaLicense';
|
||||
|
||||
@Service()
|
||||
export default class SubscriptionService {
|
||||
@Inject()
|
||||
smsMessages: SubscriptionSMSMessages;
|
||||
|
||||
@Inject()
|
||||
mailMessages: SubscriptionMailMessages;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject('repositories')
|
||||
sysRepositories: any;
|
||||
|
||||
/**
|
||||
* Handles the payment process via license code and than subscribe to
|
||||
* the given tenant.
|
||||
* @param {number} tenantId
|
||||
* @param {String} planSlug
|
||||
* @param {string} licenseCode
|
||||
* @return {Promise}
|
||||
*/
|
||||
public async subscriptionViaLicense(
|
||||
tenantId: number,
|
||||
planSlug: string,
|
||||
paymentModel: ILicensePaymentModel,
|
||||
subscriptionSlug: string = 'main'
|
||||
) {
|
||||
// Retrieve plan details.
|
||||
const plan = await Plan.query().findOne('slug', planSlug);
|
||||
|
||||
// Retrieve tenant details.
|
||||
const tenant = await Tenant.query().findById(tenantId);
|
||||
|
||||
// License payment method.
|
||||
const paymentViaLicense = new LicensePaymentMethod();
|
||||
|
||||
// Payment context.
|
||||
const paymentContext = new PaymentContext(paymentViaLicense);
|
||||
|
||||
// Subscription.
|
||||
const subscription = new SubscriptionViaLicense(paymentContext);
|
||||
|
||||
// Subscribe.
|
||||
await subscription.subscribe(tenant, plan, paymentModel, subscriptionSlug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all subscription of the given tenant.
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
public async getSubscriptions(tenantId: number) {
|
||||
const subscriptions = await PlanSubscription.query().where(
|
||||
'tenant_id',
|
||||
tenantId
|
||||
);
|
||||
return subscriptions;
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { License, Tenant, Plan } from '@/system/models';
|
||||
import Subscription from './Subscription';
|
||||
import { PaymentModel } from '@/interfaces';
|
||||
|
||||
export default class SubscriptionViaLicense extends Subscription<PaymentModel> {
|
||||
/**
|
||||
* Subscripe to the given plan.
|
||||
* @param {Plan} plan
|
||||
* @throws {NotAllowedChangeSubscriptionPlan}
|
||||
*/
|
||||
public async subscribe(
|
||||
tenant: Tenant,
|
||||
plan: Plan,
|
||||
paymentModel?: PaymentModel,
|
||||
subscriptionSlug: string = 'main'
|
||||
): Promise<void> {
|
||||
await this.paymentContext.makePayment(paymentModel, plan);
|
||||
|
||||
return this.newSubscriptionFromLicense(
|
||||
tenant,
|
||||
plan,
|
||||
paymentModel.licenseCode,
|
||||
subscriptionSlug
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* New subscription from the given license.
|
||||
* @param {Tanant} tenant
|
||||
* @param {Plab} plan
|
||||
* @param {string} licenseCode
|
||||
* @param {string} subscriptionSlug
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private async newSubscriptionFromLicense(
|
||||
tenant,
|
||||
plan,
|
||||
licenseCode: string,
|
||||
subscriptionSlug: string = 'main'
|
||||
): Promise<void> {
|
||||
// License information.
|
||||
const licenseInfo = await License.query().findOne(
|
||||
'licenseCode',
|
||||
licenseCode
|
||||
);
|
||||
return this.newSubscribtion(
|
||||
tenant,
|
||||
plan,
|
||||
licenseInfo.periodInterval,
|
||||
licenseInfo.licensePeriod,
|
||||
subscriptionSlug
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('subscriptions_plans', table => {
|
||||
table.increments();
|
||||
|
||||
table.string('name');
|
||||
table.string('description');
|
||||
table.decimal('price');
|
||||
table.string('currency', 3);
|
||||
|
||||
table.integer('trial_period');
|
||||
table.string('trial_interval');
|
||||
|
||||
table.integer('invoice_period');
|
||||
table.string('invoice_interval');
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('subscriptions_plans')
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('subscription_plans', table => {
|
||||
table.increments();
|
||||
table.string('slug');
|
||||
table.string('name');
|
||||
table.string('desc');
|
||||
table.boolean('active');
|
||||
|
||||
table.decimal('price').unsigned();
|
||||
table.string('currency', 3);
|
||||
|
||||
table.decimal('trial_period').nullable();
|
||||
table.string('trial_interval').nullable();
|
||||
|
||||
table.decimal('invoice_period').nullable();
|
||||
table.string('invoice_interval').nullable();
|
||||
|
||||
table.integer('index').unsigned();
|
||||
table.timestamps();
|
||||
}).then(() => {
|
||||
return knex.seed.run({
|
||||
specific: 'seed_subscriptions_plans.js',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('subscription_plans')
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('subscription_plan_features', table => {
|
||||
table.increments();
|
||||
table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
|
||||
table.string('slug');
|
||||
table.string('name');
|
||||
table.string('description');
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('subscription_plan_features');
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('subscription_plan_subscriptions', table => {
|
||||
table.increments('id');
|
||||
table.string('slug');
|
||||
|
||||
table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
|
||||
table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants');
|
||||
|
||||
table.dateTime('starts_at').nullable();
|
||||
table.dateTime('ends_at').nullable();
|
||||
|
||||
table.dateTime('cancels_at').nullable();
|
||||
table.dateTime('canceled_at').nullable();
|
||||
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('subscription_plan_subscriptions');
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('subscription_licenses', (table) => {
|
||||
table.increments();
|
||||
|
||||
table.string('license_code').unique().index();
|
||||
table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
|
||||
|
||||
table.integer('license_period').unsigned();
|
||||
table.string('period_interval');
|
||||
|
||||
table.dateTime('sent_at').index();
|
||||
table.dateTime('disabled_at').index();
|
||||
table.dateTime('used_at').index();
|
||||
|
||||
table.timestamps();
|
||||
})
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('subscription_licenses');
|
||||
};
|
||||
@@ -1,129 +0,0 @@
|
||||
import { Model, mixin } from 'objection';
|
||||
import moment from 'moment';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
|
||||
export default class License extends SystemModel {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'subscription_licenses';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
// Filters active licenses.
|
||||
filterActiveLicense(query) {
|
||||
query.where('disabled_at', null);
|
||||
query.where('used_at', null);
|
||||
},
|
||||
|
||||
// Find license by its code or id.
|
||||
findByCodeOrId(query, id, code) {
|
||||
if (id) {
|
||||
query.where('id', id);
|
||||
}
|
||||
if (code) {
|
||||
query.where('license_code', code);
|
||||
}
|
||||
},
|
||||
|
||||
// Filters licenses list.
|
||||
filter(builder, licensesFilter) {
|
||||
if (licensesFilter.active) {
|
||||
builder.modify('filterActiveLicense');
|
||||
}
|
||||
if (licensesFilter.disabled) {
|
||||
builder.whereNot('disabled_at', null);
|
||||
}
|
||||
if (licensesFilter.used) {
|
||||
builder.whereNot('used_at', null);
|
||||
}
|
||||
if (licensesFilter.sent) {
|
||||
builder.whereNot('sent_at', null);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Plan = require('system/models/Subscriptions/Plan');
|
||||
|
||||
return {
|
||||
plan: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Plan.default,
|
||||
join: {
|
||||
from: 'subscription_licenses.planId',
|
||||
to: 'subscriptions_plans.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given license code from the storage.
|
||||
* @param {string} licenseCode
|
||||
* @return {Promise}
|
||||
*/
|
||||
static deleteLicense(licenseCode, viaAttribute = 'license_code') {
|
||||
return this.query().where(viaAttribute, licenseCode).delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given license code as disabled on the storage.
|
||||
* @param {string} licenseCode
|
||||
* @return {Promise}
|
||||
*/
|
||||
static markLicenseAsDisabled(licenseCode, viaAttribute = 'license_code') {
|
||||
return this.query().where(viaAttribute, licenseCode).patch({
|
||||
disabled_at: moment().toMySqlDateTime(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given license code as sent on the storage.
|
||||
* @param {string} licenseCode
|
||||
*/
|
||||
static markLicenseAsSent(licenseCode, viaAttribute = 'license_code') {
|
||||
return this.query().where(viaAttribute, licenseCode).patch({
|
||||
sent_at: moment().toMySqlDateTime(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the given license code as used on the storage.
|
||||
* @param {string} licenseCode
|
||||
* @return {Promise}
|
||||
*/
|
||||
static markLicenseAsUsed(licenseCode, viaAttribute = 'license_code') {
|
||||
return this.query().where(viaAttribute, licenseCode).patch({
|
||||
used_at: moment().toMySqlDateTime(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {IIPlan} plan
|
||||
* @return {boolean}
|
||||
*/
|
||||
isEqualPlanPeriod(plan) {
|
||||
return (
|
||||
this.invoicePeriod === plan.invoiceInterval &&
|
||||
license.licensePeriod === license.periodInterval
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { Model, mixin } from 'objection';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
import { PlanSubscription } from '..';
|
||||
|
||||
export default class Plan extends mixin(SystemModel) {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'subscription_plans';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Defined virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['isFree', 'hasTrial'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
getFeatureBySlug(builder, featureSlug) {
|
||||
builder.where('slug', featureSlug);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const PlanSubscription = require('system/models/Subscriptions/PlanSubscription');
|
||||
|
||||
return {
|
||||
/**
|
||||
* The plan may have many subscriptions.
|
||||
*/
|
||||
subscriptions: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: PlanSubscription.default,
|
||||
join: {
|
||||
from: 'subscription_plans.id',
|
||||
to: 'subscription_plan_subscriptions.planId',
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if plan is free.
|
||||
* @return {boolean}
|
||||
*/
|
||||
isFree() {
|
||||
return this.price <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if plan is paid.
|
||||
* @return {boolean}
|
||||
*/
|
||||
isPaid() {
|
||||
return !this.isFree();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if plan has trial.
|
||||
* @return {boolean}
|
||||
*/
|
||||
hasTrial() {
|
||||
return this.trialPeriod && this.trialInterval;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Model, mixin } from 'objection';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
|
||||
export default class PlanFeature extends mixin(SystemModel) {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'subscriptions.plan_features';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Plan = require('system/models/Subscriptions/Plan');
|
||||
|
||||
return {
|
||||
plan: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Plan.default,
|
||||
join: {
|
||||
from: 'subscriptions.plan_features.planId',
|
||||
to: 'subscriptions.plans.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
import { Model, mixin } from 'objection';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
import moment from 'moment';
|
||||
import SubscriptionPeriod from '@/services/Subscription/SubscriptionPeriod';
|
||||
|
||||
export default class PlanSubscription extends mixin(SystemModel) {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'subscription_plan_subscriptions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Defined virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['active', 'inactive', 'ended', 'onTrial'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifiers queries.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
activeSubscriptions(builder) {
|
||||
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
const now = moment().format(dateFormat);
|
||||
|
||||
builder.where('ends_at', '>', now);
|
||||
builder.where('trial_ends_at', '>', now);
|
||||
},
|
||||
|
||||
inactiveSubscriptions() {
|
||||
builder.modify('endedTrial');
|
||||
builder.modify('endedPeriod');
|
||||
},
|
||||
|
||||
subscriptionBySlug(builder, subscriptionSlug) {
|
||||
builder.where('slug', subscriptionSlug);
|
||||
},
|
||||
|
||||
endedTrial(builder) {
|
||||
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
const endDate = moment().format(dateFormat);
|
||||
|
||||
builder.where('ends_at', '<=', endDate);
|
||||
},
|
||||
|
||||
endedPeriod(builder) {
|
||||
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
const endDate = moment().format(dateFormat);
|
||||
|
||||
builder.where('trial_ends_at', '<=', endDate);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relations mappings.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Tenant = require('system/models/Tenant');
|
||||
const Plan = require('system/models/Subscriptions/Plan');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Plan subscription belongs to tenant.
|
||||
*/
|
||||
tenant: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Tenant.default,
|
||||
join: {
|
||||
from: 'subscription_plan_subscriptions.tenantId',
|
||||
to: 'tenants.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Plan description belongs to plan.
|
||||
*/
|
||||
plan: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Plan.default,
|
||||
join: {
|
||||
from: 'subscription_plan_subscriptions.planId',
|
||||
to: 'subscription_plans.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if subscription is active.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
active() {
|
||||
return !this.ended() || this.onTrial();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if subscription is inactive.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
inactive() {
|
||||
return !this.active();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if subscription period has ended.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
ended() {
|
||||
return this.endsAt ? moment().isAfter(this.endsAt) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if subscription is currently on trial.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
onTrial() {
|
||||
return this.trailEndsAt ? moment().isAfter(this.trailEndsAt) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new period from the given details.
|
||||
* @param {string} invoiceInterval
|
||||
* @param {number} invoicePeriod
|
||||
* @param {string} start
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
static setNewPeriod(invoiceInterval, invoicePeriod, start) {
|
||||
const period = new SubscriptionPeriod(
|
||||
invoiceInterval,
|
||||
invoicePeriod,
|
||||
start,
|
||||
);
|
||||
|
||||
const startsAt = period.getStartDate();
|
||||
const endsAt = period.getEndDate();
|
||||
|
||||
return { startsAt, endsAt };
|
||||
}
|
||||
|
||||
/**
|
||||
* Renews subscription period.
|
||||
* @Promise
|
||||
*/
|
||||
renew(invoiceInterval, invoicePeriod) {
|
||||
const { startsAt, endsAt } = PlanSubscription.setNewPeriod(
|
||||
invoiceInterval,
|
||||
invoicePeriod,
|
||||
);
|
||||
return this.$query().update({ startsAt, endsAt });
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import moment from 'moment';
|
||||
import { Model } from 'objection';
|
||||
import uniqid from 'uniqid';
|
||||
import SubscriptionPeriod from '@/services/Subscription/SubscriptionPeriod';
|
||||
import BaseModel from 'models/Model';
|
||||
import TenantMetadata from './TenantMetadata';
|
||||
import PlanSubscription from './Subscriptions/PlanSubscription';
|
||||
|
||||
export default class Tenant extends BaseModel {
|
||||
/**
|
||||
@@ -49,33 +47,13 @@ export default class Tenant extends BaseModel {
|
||||
return !!this.upgradeJobId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query modifiers.
|
||||
*/
|
||||
static modifiers() {
|
||||
return {
|
||||
subscriptions(builder) {
|
||||
builder.withGraphFetched('subscriptions');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relations mappings.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const PlanSubscription = require('./Subscriptions/PlanSubscription');
|
||||
const TenantMetadata = require('./TenantMetadata');
|
||||
|
||||
return {
|
||||
subscriptions: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: PlanSubscription.default,
|
||||
join: {
|
||||
from: 'tenants.id',
|
||||
to: 'subscription_plan_subscriptions.tenantId',
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
relation: Model.HasOneRelation,
|
||||
modelClass: TenantMetadata.default,
|
||||
@@ -86,55 +64,6 @@ export default class Tenant extends BaseModel {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the subscribed plans ids.
|
||||
* @return {number[]}
|
||||
*/
|
||||
async subscribedPlansIds() {
|
||||
const { subscriptions } = this;
|
||||
return chain(subscriptions).map('planId').unq();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} planId
|
||||
* @param {*} invoiceInterval
|
||||
* @param {*} invoicePeriod
|
||||
* @param {*} subscriptionSlug
|
||||
* @returns
|
||||
*/
|
||||
newSubscription(planId, invoiceInterval, invoicePeriod, subscriptionSlug) {
|
||||
return Tenant.newSubscription(
|
||||
this.id,
|
||||
planId,
|
||||
invoiceInterval,
|
||||
invoicePeriod,
|
||||
subscriptionSlug,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a new subscription for the associated tenant.
|
||||
*/
|
||||
static newSubscription(
|
||||
tenantId,
|
||||
planId,
|
||||
invoiceInterval,
|
||||
invoicePeriod,
|
||||
subscriptionSlug
|
||||
) {
|
||||
const period = new SubscriptionPeriod(invoiceInterval, invoicePeriod);
|
||||
|
||||
return PlanSubscription.query().insert({
|
||||
tenantId,
|
||||
slug: subscriptionSlug,
|
||||
planId,
|
||||
startsAt: period.getStartDate(),
|
||||
endsAt: period.getEndDate(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tenant with random organization id.
|
||||
*/
|
||||
@@ -185,9 +114,9 @@ export default class Tenant extends BaseModel {
|
||||
|
||||
/**
|
||||
* Marks the given tenant as upgrading.
|
||||
* @param {number} tenantId
|
||||
* @param {string} upgradeJobId
|
||||
* @returns
|
||||
* @param {number} tenantId
|
||||
* @param {string} upgradeJobId
|
||||
* @returns
|
||||
*/
|
||||
static markAsUpgrading(tenantId, upgradeJobId) {
|
||||
return this.query().update({ upgradeJobId }).where({ id: tenantId });
|
||||
@@ -195,8 +124,8 @@ export default class Tenant extends BaseModel {
|
||||
|
||||
/**
|
||||
* Markes the given tenant as upgraded.
|
||||
* @param {number} tenantId
|
||||
* @returns
|
||||
* @param {number} tenantId
|
||||
* @returns
|
||||
*/
|
||||
static markAsUpgraded(tenantId) {
|
||||
return this.query().update({ upgradeJobId: null }).where({ id: tenantId });
|
||||
|
||||
@@ -1,22 +1,7 @@
|
||||
|
||||
import Plan from './Subscriptions/Plan';
|
||||
import PlanFeature from './Subscriptions/PlanFeature';
|
||||
import PlanSubscription from './Subscriptions/PlanSubscription';
|
||||
import License from './Subscriptions/License';
|
||||
import Tenant from './Tenant';
|
||||
import TenantMetadata from './TenantMetadata';
|
||||
import SystemUser from './SystemUser';
|
||||
import PasswordReset from './PasswordReset';
|
||||
import Invite from './Invite';
|
||||
|
||||
export {
|
||||
Plan,
|
||||
PlanFeature,
|
||||
PlanSubscription,
|
||||
License,
|
||||
Tenant,
|
||||
TenantMetadata,
|
||||
SystemUser,
|
||||
PasswordReset,
|
||||
Invite,
|
||||
}
|
||||
export { Tenant, TenantMetadata, SystemUser, PasswordReset, Invite };
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import SystemRepository from '@/system/repositories/SystemRepository';
|
||||
import { PlanSubscription } from '@/system/models';
|
||||
|
||||
export default class SubscriptionRepository extends SystemRepository {
|
||||
/**
|
||||
* Gets the repository's model.
|
||||
*/
|
||||
get model() {
|
||||
return PlanSubscription.bindKnex(this.knex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve subscription from a given slug in specific tenant.
|
||||
* @param {string} slug
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
getBySlugInTenant(slug: string, tenantId: number) {
|
||||
const cacheKey = this.getCacheKey('getBySlugInTenant', slug, tenantId);
|
||||
|
||||
return this.cache.get(cacheKey, () => {
|
||||
return PlanSubscription.query()
|
||||
.findOne('slug', slug)
|
||||
.where('tenant_id', tenantId);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
import SystemUserRepository from '@/system/repositories/SystemUserRepository';
|
||||
import SubscriptionRepository from '@/system/repositories/SubscriptionRepository';
|
||||
import TenantRepository from '@/system/repositories/TenantRepository';
|
||||
|
||||
export {
|
||||
SystemUserRepository,
|
||||
SubscriptionRepository,
|
||||
TenantRepository,
|
||||
};
|
||||
export { SystemUserRepository, TenantRepository };
|
||||
|
||||
0
packages/server/src/system/seeds/.gitkeep
Normal file
0
packages/server/src/system/seeds/.gitkeep
Normal file
@@ -1,66 +0,0 @@
|
||||
|
||||
exports.seed = (knex) => {
|
||||
// Deletes ALL existing entries
|
||||
return knex('subscription_plans').del()
|
||||
.then(() => {
|
||||
// Inserts seed entries
|
||||
return knex('subscription_plans').insert([
|
||||
{
|
||||
name: 'Essentials',
|
||||
slug: 'essentials-monthly',
|
||||
price: 100,
|
||||
active: true,
|
||||
currency: 'LYD',
|
||||
trial_period: 7,
|
||||
trial_interval: 'days',
|
||||
},
|
||||
{
|
||||
name: 'Essentials',
|
||||
slug: 'essentials-yearly',
|
||||
price: 1200,
|
||||
active: true,
|
||||
currency: 'LYD',
|
||||
trial_period: 12,
|
||||
trial_interval: 'months',
|
||||
},
|
||||
{
|
||||
name: 'Pro',
|
||||
slug: 'pro-monthly',
|
||||
price: 200,
|
||||
active: true,
|
||||
currency: 'LYD',
|
||||
trial_period: 1,
|
||||
trial_interval: 'months',
|
||||
},
|
||||
{
|
||||
name: 'Pro',
|
||||
slug: 'pro-yearly',
|
||||
price: 500,
|
||||
active: true,
|
||||
currency: 'LYD',
|
||||
invoice_period: 12,
|
||||
invoice_interval: 'month',
|
||||
index: 2,
|
||||
},
|
||||
{
|
||||
name: 'Plus',
|
||||
slug: 'plus-monthly',
|
||||
price: 200,
|
||||
active: true,
|
||||
currency: 'LYD',
|
||||
trial_period: 1,
|
||||
trial_interval: 'months',
|
||||
},
|
||||
{
|
||||
name: 'Plus',
|
||||
slug: 'plus-yearly',
|
||||
price: 500,
|
||||
active: true,
|
||||
currency: 'LYD',
|
||||
invoice_period: 12,
|
||||
invoice_interval: 'month',
|
||||
index: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
};
|
||||
@@ -4,17 +4,20 @@ USER root
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./package.json /app/package.json
|
||||
COPY ./package-lock.json /app/package-lock.json
|
||||
# Install dependencies
|
||||
COPY package.json ./
|
||||
COPY lerna.json ./
|
||||
|
||||
COPY ./packages/webapp/package.json /app/packages/webapp/package.json
|
||||
|
||||
RUN npm install
|
||||
RUN npm run bootstrap
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
# Build webapp package
|
||||
COPY ./packages/webapp /app/packages/webapp
|
||||
RUN npm run build:webapp
|
||||
|
||||
FROM nginx
|
||||
|
||||
COPY ./nginx/sites/default.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
COPY --from=build /app/build /usr/share/nginx/html
|
||||
COPY ./packages/webapp/nginx/sites/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /app/packages/webapp/build /usr/share/nginx/html
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"yup": "^0.28.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "craco start",
|
||||
"dev": "PORT=4000 craco start",
|
||||
"build": "craco build",
|
||||
"test": "node scripts/test.js",
|
||||
"storybook": "start-storybook -p 6006"
|
||||
|
||||
@@ -14,19 +14,16 @@ import GlobalHotkeys from './GlobalHotkeys';
|
||||
import DashboardProvider from './DashboardProvider';
|
||||
import DrawersContainer from '@/components/DrawersContainer';
|
||||
import AlertsContainer from '@/containers/AlertsContainer';
|
||||
import EnsureSubscriptionIsActive from '../Guards/EnsureSubscriptionIsActive';
|
||||
|
||||
/**
|
||||
* Dashboard preferences.
|
||||
*/
|
||||
function DashboardPreferences() {
|
||||
return (
|
||||
<EnsureSubscriptionIsActive>
|
||||
<DashboardSplitPane>
|
||||
<Sidebar />
|
||||
<PreferencesPage />
|
||||
</DashboardSplitPane>
|
||||
</EnsureSubscriptionIsActive>
|
||||
<DashboardSplitPane>
|
||||
<Sidebar />
|
||||
<PreferencesPage />
|
||||
</DashboardSplitPane>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,26 +7,20 @@ import {
|
||||
} from '@/hooks/query';
|
||||
import { useSplashLoading } from '@/hooks/state';
|
||||
import { useWatch, useWatchImmediate, useWhen } from '@/hooks';
|
||||
import { useSubscription } from '@/hooks/state';
|
||||
import { setCookie, getCookie } from '@/utils';
|
||||
|
||||
/**
|
||||
* Dashboard meta async booting.
|
||||
* - Fetches the dashboard meta only if the organization subscribe is active.
|
||||
* - Once the dashboard meta query is loading display dashboard splash screen.
|
||||
* - Fetches the dashboard meta in booting state.
|
||||
* - Once the dashboard meta query started loading display dashboard splash screen.
|
||||
*/
|
||||
export function useDashboardMetaBoot() {
|
||||
const { isSubscriptionActive } = useSubscription();
|
||||
|
||||
const {
|
||||
data: dashboardMeta,
|
||||
isLoading: isDashboardMetaLoading,
|
||||
isSuccess: isDashboardMetaSuccess,
|
||||
} = useDashboardMeta({
|
||||
keepPreviousData: true,
|
||||
|
||||
// Avoid run the query if the organization subscription is not active.
|
||||
enabled: isSubscriptionActive,
|
||||
});
|
||||
const [startLoading, stopLoading] = useSplashLoading();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
CollapsibleList,
|
||||
MenuItem,
|
||||
@@ -7,29 +8,29 @@ import {
|
||||
Boundary,
|
||||
} from '@blueprintjs/core';
|
||||
import withBreadcrumbs from 'react-router-breadcrumbs-hoc';
|
||||
import { getDashboardRoutes } from '@/routes/dashboard';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
function DashboardBreadcrumbs({ breadcrumbs }){
|
||||
function DashboardBreadcrumbs({ breadcrumbs }) {
|
||||
const history = useHistory();
|
||||
|
||||
return(
|
||||
return (
|
||||
<CollapsibleList
|
||||
className={Classes.BREADCRUMBS}
|
||||
dropdownTarget={<span className={Classes.BREADCRUMBS_COLLAPSED} />}
|
||||
collapseFrom={Boundary.START}
|
||||
visibleItemCount={0}>
|
||||
{
|
||||
breadcrumbs.map(({ breadcrumb,match })=>{
|
||||
return (<MenuItem
|
||||
key={match.url}
|
||||
icon={'folder-close'}
|
||||
text={breadcrumb}
|
||||
onClick={() => history.push(match.url) } />)
|
||||
})
|
||||
}
|
||||
className={Classes.BREADCRUMBS}
|
||||
dropdownTarget={<span className={Classes.BREADCRUMBS_COLLAPSED} />}
|
||||
collapseFrom={Boundary.START}
|
||||
visibleItemCount={0}
|
||||
>
|
||||
{breadcrumbs.map(({ breadcrumb, match }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={match.url}
|
||||
icon={'folder-close'}
|
||||
text={breadcrumb}
|
||||
onClick={() => history.push(match.url)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</CollapsibleList>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default withBreadcrumbs([])(DashboardBreadcrumbs)
|
||||
export default withBreadcrumbs([])(DashboardBreadcrumbs);
|
||||
|
||||
@@ -3,15 +3,13 @@ import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { getDashboardRoutes } from '@/routes/dashboard';
|
||||
import EnsureSubscriptionsIsActive from '../Guards/EnsureSubscriptionsIsActive';
|
||||
import EnsureSubscriptionsIsInactive from '../Guards/EnsureSubscriptionsIsInactive';
|
||||
import DashboardPage from './DashboardPage';
|
||||
|
||||
/**
|
||||
* Dashboard inner route content.
|
||||
*/
|
||||
function DashboardContentRouteContent({ route }) {
|
||||
const content = (
|
||||
return (
|
||||
<DashboardPage
|
||||
name={route.name}
|
||||
Component={route.component}
|
||||
@@ -23,21 +21,6 @@ function DashboardContentRouteContent({ route }) {
|
||||
defaultSearchResource={route.defaultSearchResource}
|
||||
/>
|
||||
);
|
||||
return route.subscriptionActive ? (
|
||||
<EnsureSubscriptionsIsInactive
|
||||
subscriptionTypes={route.subscriptionActive}
|
||||
children={content}
|
||||
redirectTo={'/billing'}
|
||||
/>
|
||||
) : route.subscriptionInactive ? (
|
||||
<EnsureSubscriptionsIsActive
|
||||
subscriptionTypes={route.subscriptionInactive}
|
||||
children={content}
|
||||
redirectTo={'/'}
|
||||
/>
|
||||
) : (
|
||||
content
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,57 +10,19 @@ import {
|
||||
Tooltip,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import { FormattedMessage as T, Icon, Hint, If } from '@/components';
|
||||
|
||||
import DashboardTopbarUser from '@/components/Dashboard/TopbarUser';
|
||||
import DashboardBreadcrumbs from '@/components/Dashboard/DashboardBreadcrumbs';
|
||||
import DashboardBackLink from '@/components/Dashboard/DashboardBackLink';
|
||||
import { Icon, Hint, If } from '@/components';
|
||||
|
||||
import withUniversalSearchActions from '@/containers/UniversalSearch/withUniversalSearchActions';
|
||||
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
||||
import withDashboard from '@/containers/Dashboard/withDashboard';
|
||||
|
||||
import QuickNewDropdown from '@/containers/QuickNewDropdown/QuickNewDropdown';
|
||||
import { DashboardHamburgerButton, DashboardQuickSearchButton } from './_components';
|
||||
import { compose } from '@/utils';
|
||||
import withSubscriptions from '@/containers/Subscriptions/withSubscriptions';
|
||||
import { useGetUniversalSearchTypeOptions } from '@/containers/UniversalSearch/utils';
|
||||
|
||||
function DashboardTopbarSubscriptionMessage() {
|
||||
return (
|
||||
<div class="dashboard__topbar-subscription-msg">
|
||||
<span>
|
||||
<T id={'dashboard.subscription_msg.period_over'} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardHamburgerButton({ ...props }) {
|
||||
return (
|
||||
<Button minimal={true} {...props}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
role="img"
|
||||
focusable="false"
|
||||
>
|
||||
<title>
|
||||
<T id={'menu'} />
|
||||
</title>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-miterlimit="5"
|
||||
stroke-width="2"
|
||||
d="M4 7h15M4 12h15M4 17h15"
|
||||
></path>
|
||||
</svg>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard topbar.
|
||||
@@ -79,10 +41,6 @@ function DashboardTopbar({
|
||||
|
||||
// #withGlobalSearch
|
||||
openGlobalSearch,
|
||||
|
||||
// #withSubscriptions
|
||||
isSubscriptionActive,
|
||||
isSubscriptionInactive,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
|
||||
@@ -137,28 +95,22 @@ function DashboardTopbar({
|
||||
</div>
|
||||
|
||||
<div class="dashboard__topbar-right">
|
||||
<If condition={isSubscriptionInactive}>
|
||||
<DashboardTopbarSubscriptionMessage />
|
||||
</If>
|
||||
|
||||
<Navbar class="dashboard__topbar-navbar">
|
||||
<NavbarGroup>
|
||||
<If condition={isSubscriptionActive}>
|
||||
<DashboardQuickSearchButton
|
||||
onClick={() => openGlobalSearch(true)}
|
||||
/>
|
||||
<QuickNewDropdown />
|
||||
<DashboardQuickSearchButton
|
||||
onClick={() => openGlobalSearch(true)}
|
||||
/>
|
||||
<QuickNewDropdown />
|
||||
|
||||
<Tooltip
|
||||
content={<T id={'notifications'} />}
|
||||
position={Position.BOTTOM}
|
||||
>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'notification-24'} iconSize={20} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</If>
|
||||
<Tooltip
|
||||
content={<T id={'notifications'} />}
|
||||
position={Position.BOTTOM}
|
||||
>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'notification-24'} iconSize={20} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
@@ -186,31 +138,4 @@ export default compose(
|
||||
pageHint,
|
||||
})),
|
||||
withDashboardActions,
|
||||
withSubscriptions(
|
||||
({ isSubscriptionActive, isSubscriptionInactive }) => ({
|
||||
isSubscriptionActive,
|
||||
isSubscriptionInactive,
|
||||
}),
|
||||
'main',
|
||||
),
|
||||
)(DashboardTopbar);
|
||||
|
||||
/**
|
||||
* Dashboard quick search button.
|
||||
*/
|
||||
function DashboardQuickSearchButton({ ...rest }) {
|
||||
const searchTypeOptions = useGetUniversalSearchTypeOptions();
|
||||
|
||||
// Can't continue if there is no any search type option.
|
||||
if (searchTypeOptions.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'search-24'} iconSize={20} />}
|
||||
text={<T id={'quick_find'} />}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Button, Classes } from '@blueprintjs/core';
|
||||
import { useGetUniversalSearchTypeOptions } from '@/containers/UniversalSearch/utils';
|
||||
import { Icon, FormattedMessage as T } from '@/components';
|
||||
|
||||
export function DashboardTopbarSubscriptionMessage() {
|
||||
return (
|
||||
<div class="dashboard__topbar-subscription-msg">
|
||||
<span>
|
||||
<T id={'dashboard.subscription_msg.period_over'} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DashboardHamburgerButton({ ...props }) {
|
||||
return (
|
||||
<Button minimal={true} {...props}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
role="img"
|
||||
focusable="false"
|
||||
>
|
||||
<title>
|
||||
<T id={'menu'} />
|
||||
</title>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-miterlimit="5"
|
||||
stroke-width="2"
|
||||
d="M4 7h15M4 12h15M4 17h15"
|
||||
></path>
|
||||
</svg>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard quick search button.
|
||||
*/
|
||||
export function DashboardQuickSearchButton({ ...rest }) {
|
||||
const searchTypeOptions = useGetUniversalSearchTypeOptions();
|
||||
|
||||
// Can't continue if there is no any search type option.
|
||||
if (searchTypeOptions.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'search-24'} iconSize={20} />}
|
||||
text={<T id={'quick_find'} />}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import DashboardTopbar from './DashboardTopbar';
|
||||
|
||||
export default DashboardTopbar;
|
||||
@@ -9,25 +9,21 @@ import {
|
||||
Popover,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import { If, FormattedMessage as T } from '@/components';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
|
||||
import { firstLettersArgs } from '@/utils';
|
||||
import { useAuthActions } from '@/hooks/state';
|
||||
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import withSubscriptions from '@/containers/Subscriptions/withSubscriptions';
|
||||
|
||||
import { useAuthenticatedAccount } from '@/hooks/query';
|
||||
import { compose } from '@/utils';
|
||||
import { firstLettersArgs, compose } from '@/utils';
|
||||
|
||||
/**
|
||||
* Dashboard topbar user.
|
||||
*/
|
||||
function DashboardTopbarUser({
|
||||
// #withDialogActions
|
||||
openDialog,
|
||||
|
||||
// #withSubscriptions
|
||||
isSubscriptionActive,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const { setLogout } = useAuthActions();
|
||||
@@ -62,16 +58,14 @@ function DashboardTopbarUser({
|
||||
}
|
||||
/>
|
||||
<MenuDivider />
|
||||
<If condition={isSubscriptionActive}>
|
||||
<MenuItem
|
||||
text={<T id={'keyboard_shortcuts'} />}
|
||||
onClick={onKeyboardShortcut}
|
||||
/>
|
||||
<MenuItem
|
||||
text={<T id={'preferences'} />}
|
||||
onClick={() => history.push('/preferences')}
|
||||
/>
|
||||
</If>
|
||||
<MenuItem
|
||||
text={<T id={'keyboard_shortcuts'} />}
|
||||
onClick={onKeyboardShortcut}
|
||||
/>
|
||||
<MenuItem
|
||||
text={<T id={'preferences'} />}
|
||||
onClick={() => history.push('/preferences')}
|
||||
/>
|
||||
<MenuItem text={<T id={'logout'} />} onClick={onClickLogout} />
|
||||
</Menu>
|
||||
}
|
||||
@@ -87,8 +81,4 @@ function DashboardTopbarUser({
|
||||
}
|
||||
export default compose(
|
||||
withDialogActions,
|
||||
withSubscriptions(
|
||||
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
|
||||
'main',
|
||||
),
|
||||
)(DashboardTopbarUser);
|
||||
|
||||
@@ -72,6 +72,7 @@ export default function TableCell({ cell, row, index }) {
|
||||
[`td-${cell.column.id}`]: cell.column.id,
|
||||
[`td-${cellType}-type`]: !!cellType,
|
||||
}),
|
||||
tabindex: 0,
|
||||
onClick: handleCellClick,
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
export const getSetupWizardSteps = () => [
|
||||
{
|
||||
label: intl.get('setup.plan.plans'),
|
||||
},
|
||||
{
|
||||
label: intl.get('setup.plan.getting_started'),
|
||||
},
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Features } from '@/constants/features';
|
||||
import {
|
||||
ISidebarMenuItemType,
|
||||
ISidebarMenuOverlayIds,
|
||||
ISidebarSubscriptionAbility,
|
||||
} from '@/containers/Dashboard/Sidebar/interfaces';
|
||||
import {
|
||||
ReportsAction,
|
||||
@@ -24,9 +23,7 @@ import {
|
||||
ManualJournalAction,
|
||||
ExpenseAction,
|
||||
CashflowAction,
|
||||
ProjectAction,
|
||||
PreferencesAbility,
|
||||
SubscriptionBillingAbility,
|
||||
} from '@/constants/abilityOption';
|
||||
|
||||
export const SidebarMenu = [
|
||||
@@ -781,19 +778,6 @@ export const SidebarMenu = [
|
||||
ability: PreferencesAbility.Mutate,
|
||||
},
|
||||
},
|
||||
{
|
||||
text: <T id={'billing'} />,
|
||||
href: '/billing',
|
||||
type: ISidebarMenuItemType.Link,
|
||||
subscription: [
|
||||
ISidebarSubscriptionAbility.Expired,
|
||||
ISidebarSubscriptionAbility.Active,
|
||||
],
|
||||
permission: {
|
||||
subject: AbilitySubject.SubscriptionBilling,
|
||||
ability: SubscriptionBillingAbility.View,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -4,6 +4,7 @@ import intl from 'react-intl-universal';
|
||||
import classNames from 'classnames';
|
||||
import { Formik, Form } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { CreateCustomerForm, EditCustomerForm } from './CustomerForm.schema';
|
||||
@@ -104,9 +105,9 @@ function CustomerFormFormik({
|
||||
onSubmit={handleFormSubmit}
|
||||
>
|
||||
<Form>
|
||||
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
|
||||
<CustomerFormHeaderPrimary>
|
||||
<CustomerFormPrimarySection />
|
||||
</div>
|
||||
</CustomerFormHeaderPrimary>
|
||||
|
||||
<div className={'page-form__after-priamry-section'}>
|
||||
<CustomerFormAfterPrimarySection />
|
||||
@@ -123,4 +124,12 @@ function CustomerFormFormik({
|
||||
);
|
||||
}
|
||||
|
||||
export const CustomerFormHeaderPrimary = styled.div`
|
||||
padding: 10px 0 0;
|
||||
margin: 0 0 20px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #e4e4e4;
|
||||
max-width: 1000px;
|
||||
`;
|
||||
|
||||
export default compose(withCurrentOrganization())(CustomerFormFormik);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Scrollbar } from 'react-scrollbars-custom';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import withDashboard from '@/containers/Dashboard/withDashboard';
|
||||
import withSubscriptions from '@/containers/Subscriptions/withSubscriptions';
|
||||
|
||||
import { useObserveSidebarExpendedBodyclass } from './hooks';
|
||||
import { compose } from '@/utils';
|
||||
@@ -19,9 +18,6 @@ function SidebarContainerJSX({
|
||||
|
||||
// #withDashboard
|
||||
sidebarExpended,
|
||||
|
||||
// #withSubscription
|
||||
isSubscriptionActive,
|
||||
}) {
|
||||
const sidebarScrollerRef = React.useRef();
|
||||
|
||||
@@ -51,7 +47,6 @@ function SidebarContainerJSX({
|
||||
<div
|
||||
className={classNames('sidebar', {
|
||||
'sidebar--mini-sidebar': !sidebarExpended,
|
||||
'is-subscription-inactive': !isSubscriptionActive,
|
||||
})}
|
||||
id="sidebar"
|
||||
onMouseLeave={handleSidebarMouseLeave}
|
||||
@@ -72,8 +67,4 @@ export const SidebarContainer = compose(
|
||||
withDashboard(({ sidebarExpended }) => ({
|
||||
sidebarExpended,
|
||||
})),
|
||||
withSubscriptions(
|
||||
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
|
||||
'main',
|
||||
),
|
||||
)(SidebarContainerJSX);
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Menu } from '@blueprintjs/core';
|
||||
import * as R from 'ramda';
|
||||
|
||||
import { MenuItem, MenuItemLabel } from '@/components';
|
||||
import { ISidebarMenuItemType } from '@/containers/Dashboard/Sidebar/interfaces';
|
||||
import { useIsSidebarMenuItemActive } from './hooks';
|
||||
|
||||
import withSubscriptions from '@/containers/Subscriptions/withSubscriptions';
|
||||
|
||||
/**
|
||||
* Sidebar menu item.
|
||||
* @returns {JSX.Element}
|
||||
@@ -55,7 +52,7 @@ function SidebarMenuItemComposer({ item, index }) {
|
||||
* Sidebar menu.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
function SidebarMenuJSX({ menu }) {
|
||||
export function SidebarMenu({ menu }) {
|
||||
return (
|
||||
<div>
|
||||
<Menu className="sidebar-menu">
|
||||
@@ -66,10 +63,3 @@ function SidebarMenuJSX({ menu }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const SidebarMenu = R.compose(
|
||||
withSubscriptions(
|
||||
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
|
||||
'main',
|
||||
),
|
||||
)(SidebarMenuJSX);
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from './interfaces';
|
||||
import { filterValuesDeep, deepdash } from '@/utils';
|
||||
|
||||
|
||||
const deepDashConfig = {
|
||||
childrenPath: 'children',
|
||||
pathFormat: 'array',
|
||||
@@ -136,9 +135,7 @@ function useFilterSidebarMenuAbility(menu) {
|
||||
|
||||
return deepdash.filterDeep(
|
||||
menu,
|
||||
(item) => {
|
||||
return predFeature(item) && predAbility(item) && predSubscription(item);
|
||||
},
|
||||
(item) => predFeature(item) && predAbility(item),
|
||||
deepDashConfig,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import React, { useCallback } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { Formik } from 'formik';
|
||||
import { omit } from 'lodash';
|
||||
import { AppToaster } from '@/components';
|
||||
|
||||
import AccountDialogFormContent from './AccountDialogFormContent';
|
||||
@@ -14,7 +13,11 @@ import {
|
||||
CreateAccountFormSchema,
|
||||
} from './AccountForm.schema';
|
||||
import { compose, transformToForm } from '@/utils';
|
||||
import { transformApiErrors, transformAccountToForm } from './utils';
|
||||
import {
|
||||
transformApiErrors,
|
||||
transformAccountToForm,
|
||||
transformFormToReq,
|
||||
} from './utils';
|
||||
|
||||
import '@/style/pages/Accounts/AccountFormDialog.scss';
|
||||
import { useAccountDialogContext } from './AccountDialogProvider';
|
||||
@@ -26,7 +29,7 @@ const defaultInitialValues = {
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
currency_code:'',
|
||||
currency_code: '',
|
||||
subaccount: false,
|
||||
};
|
||||
|
||||
@@ -43,7 +46,6 @@ function AccountFormDialogContent({
|
||||
createAccountMutate,
|
||||
account,
|
||||
|
||||
accountId,
|
||||
payload,
|
||||
isNewMode,
|
||||
dialogName,
|
||||
@@ -56,7 +58,7 @@ function AccountFormDialogContent({
|
||||
|
||||
// Callbacks handles form submit.
|
||||
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
||||
const form = omit(values, ['subaccount']);
|
||||
const form = transformFormToReq(values);
|
||||
const toastAccountName = values.code
|
||||
? `${values.code} - ${values.name}`
|
||||
: values.name;
|
||||
@@ -90,8 +92,8 @@ function AccountFormDialogContent({
|
||||
setErrors({ ...errorsTransformed });
|
||||
setSubmitting(false);
|
||||
};
|
||||
if (accountId) {
|
||||
editAccountMutate([accountId, form])
|
||||
if (payload.accountId) {
|
||||
editAccountMutate([payload.accountId, form])
|
||||
.then(handleSuccess)
|
||||
.catch(handleError);
|
||||
} else {
|
||||
@@ -113,7 +115,6 @@ function AccountFormDialogContent({
|
||||
defaultInitialValues,
|
||||
),
|
||||
};
|
||||
|
||||
// Handles dialog close.
|
||||
const handleClose = useCallback(() => {
|
||||
closeDialog(dialogName);
|
||||
|
||||
@@ -26,6 +26,7 @@ import { inputIntent, compose } from '@/utils';
|
||||
import { useAutofocus } from '@/hooks';
|
||||
import { FOREIGN_CURRENCY_ACCOUNTS } from '@/constants/accountTypes';
|
||||
import { useAccountDialogContext } from './AccountDialogProvider';
|
||||
import { parentAccountShouldUpdate } from './utils';
|
||||
|
||||
/**
|
||||
* Account form dialogs fields.
|
||||
@@ -115,12 +116,7 @@ function AccountFormDialogFields({
|
||||
>
|
||||
<Checkbox
|
||||
inline={true}
|
||||
label={
|
||||
<>
|
||||
<T id={'sub_account'} />
|
||||
<Hint />
|
||||
</>
|
||||
}
|
||||
label={<T id={'sub_account'} />}
|
||||
name={'subaccount'}
|
||||
{...field}
|
||||
/>
|
||||
@@ -128,37 +124,36 @@ function AccountFormDialogFields({
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<If condition={values.subaccount}>
|
||||
<FastField name={'parent_account_id'}>
|
||||
{({
|
||||
form: { values, setFieldValue },
|
||||
field: { value },
|
||||
meta: { error, touched },
|
||||
}) => (
|
||||
<FormGroup
|
||||
label={<T id={'parent_account'} />}
|
||||
className={classNames(
|
||||
'form-group--parent-account',
|
||||
Classes.FILL,
|
||||
)}
|
||||
inline={true}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name="parent_account_id" />}
|
||||
>
|
||||
<AccountsSelectList
|
||||
accounts={accounts}
|
||||
onAccountSelected={(account) => {
|
||||
setFieldValue('parent_account_id', account.id);
|
||||
}}
|
||||
defaultSelectText={<T id={'select_parent_account'} />}
|
||||
selectedAccountId={value}
|
||||
popoverFill={true}
|
||||
filterByTypes={values.account_type}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
</If>
|
||||
<FastField
|
||||
name={'parent_account_id'}
|
||||
shouldUpdate={parentAccountShouldUpdate}
|
||||
>
|
||||
{({
|
||||
form: { values, setFieldValue },
|
||||
field: { value },
|
||||
meta: { error, touched },
|
||||
}) => (
|
||||
<FormGroup
|
||||
label={<T id={'parent_account'} />}
|
||||
className={classNames('form-group--parent-account', Classes.FILL)}
|
||||
inline={true}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name="parent_account_id" />}
|
||||
>
|
||||
<AccountsSelectList
|
||||
accounts={accounts}
|
||||
onAccountSelected={(account) => {
|
||||
setFieldValue('parent_account_id', account.id);
|
||||
}}
|
||||
defaultSelectText={<T id={'select_parent_account'} />}
|
||||
selectedAccountId={value}
|
||||
popoverFill={true}
|
||||
filterByTypes={values.account_type}
|
||||
disabled={!values.subaccount}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
|
||||
<If condition={FOREIGN_CURRENCY_ACCOUNTS.includes(values.account_type)}>
|
||||
{/*------------ Currency -----------*/}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import intl from 'react-intl-universal';
|
||||
import * as R from 'ramda';
|
||||
import { isUndefined } from 'lodash';
|
||||
import { defaultFastFieldShouldUpdate } from '@/utils';
|
||||
|
||||
export const AccountDialogAction = {
|
||||
Edit: 'edit',
|
||||
@@ -33,7 +34,7 @@ export const transformApiErrors = (errors) => {
|
||||
/**
|
||||
* Payload transformer in account edit mode.
|
||||
*/
|
||||
function tranformNewChildAccountPayload(payload) {
|
||||
function tranformNewChildAccountPayload(account, payload) {
|
||||
return {
|
||||
parent_account_id: payload.parentAccountId || '',
|
||||
account_type: payload.accountType || '',
|
||||
@@ -44,7 +45,7 @@ function tranformNewChildAccountPayload(payload) {
|
||||
/**
|
||||
* Payload transformer in new account with defined type.
|
||||
*/
|
||||
function transformNewDefinedTypePayload(payload) {
|
||||
function transformNewDefinedTypePayload(account, payload) {
|
||||
return {
|
||||
account_type: payload.accountType || '',
|
||||
};
|
||||
@@ -63,7 +64,9 @@ const mergeWithAccount = R.curry((transformed, account) => {
|
||||
/**
|
||||
* Default account payload transformer.
|
||||
*/
|
||||
const defaultPayloadTransform = () => ({});
|
||||
const defaultPayloadTransform = (account, payload) => ({
|
||||
subaccount: !!account.parent_account_id,
|
||||
});
|
||||
|
||||
/**
|
||||
* Defined payload transformers.
|
||||
@@ -89,7 +92,7 @@ export const transformAccountToForm = (account, payload) => {
|
||||
|
||||
return [
|
||||
condition[0] === payload.action ? R.T : R.F,
|
||||
mergeWithAccount(transformer(payload)),
|
||||
mergeWithAccount(transformer(account, payload)),
|
||||
];
|
||||
});
|
||||
return R.cond(results)(account);
|
||||
@@ -106,3 +109,29 @@ export const getDisabledFormFields = (account, payload) => {
|
||||
payload.action === AccountDialogAction.NewDefinedType,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether should update the parent account field.
|
||||
* @param newProps
|
||||
* @param oldProps
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const parentAccountShouldUpdate = (newProps, oldProps) => {
|
||||
return (
|
||||
newProps.formik.values.subaccount !== oldProps.formik.values.subaccount ||
|
||||
defaultFastFieldShouldUpdate(newProps, oldProps)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the form values to the request.
|
||||
*/
|
||||
export const transformFormToReq = (form) => {
|
||||
return R.compose(
|
||||
R.omit(['subaccount']),
|
||||
R.when(
|
||||
R.propSatisfies(R.equals(R.__, false), 'subaccount'),
|
||||
R.assoc(['parent_account_id'], ''),
|
||||
),
|
||||
)(form);
|
||||
};
|
||||
|
||||
@@ -4,12 +4,13 @@ import * as R from 'ramda';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Card, DrawerLoading } from '@/components';
|
||||
|
||||
import {
|
||||
CustomerFormProvider,
|
||||
useCustomerFormContext,
|
||||
} from '@/containers/Customers/CustomerForm/CustomerFormProvider';
|
||||
import CustomerFormFormik from '@/containers/Customers/CustomerForm/CustomerFormFormik';
|
||||
import CustomerFormFormik, {
|
||||
CustomerFormHeaderPrimary,
|
||||
} from '@/containers/Customers/CustomerForm/CustomerFormFormik';
|
||||
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
|
||||
@@ -55,12 +56,18 @@ export default R.compose(withDrawerActions)(QuickCustomerFormDrawer);
|
||||
|
||||
const CustomerFormCard = styled(Card)`
|
||||
margin: 15px;
|
||||
padding: 25px;
|
||||
margin-bottom: calc(15px + 65px);
|
||||
|
||||
${CustomerFormHeaderPrimary} {
|
||||
padding-top: 0;
|
||||
}
|
||||
.page-form {
|
||||
padding: 0;
|
||||
|
||||
&__floating-actions {
|
||||
margin-left: -36px;
|
||||
margin-right: -36px;
|
||||
margin-left: -41px;
|
||||
margin-right: -41px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -4,12 +4,13 @@ import * as R from 'ramda';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Card, DrawerLoading } from '@/components';
|
||||
|
||||
import {
|
||||
VendorFormProvider,
|
||||
useVendorFormContext,
|
||||
} from '@/containers/Vendors/VendorForm/VendorFormProvider';
|
||||
import VendorFormFormik from '@/containers/Vendors/VendorForm/VendorFormFormik';
|
||||
import VendorFormFormik, {
|
||||
VendorFormHeaderPrimary,
|
||||
} from '@/containers/Vendors/VendorForm/VendorFormFormik';
|
||||
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
||||
@@ -75,12 +76,18 @@ export default R.compose(
|
||||
|
||||
const VendorFormCard = styled(Card)`
|
||||
margin: 15px;
|
||||
padding: 25px;
|
||||
margin-bottom: calc(15px + 65px);
|
||||
|
||||
${VendorFormHeaderPrimary} {
|
||||
padding-top: 0;
|
||||
}
|
||||
.page-form {
|
||||
padding: 0;
|
||||
|
||||
&__floating-actions {
|
||||
margin-left: -36px;
|
||||
margin-right: -36px;
|
||||
margin-left: -41px;
|
||||
margin-right: -41px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -105,7 +105,7 @@ export function ItemsActionMenuList({
|
||||
</Can>
|
||||
<Can I={ItemAction.Create} a={AbilitySubject.Item}>
|
||||
<MenuItem
|
||||
icon={<Icon icon="duplicate-16" />}
|
||||
icon={<Icon icon="content-copy" iconSize={16} />}
|
||||
text={intl.get('duplicate')}
|
||||
onClick={safeCallback(onDuplicate, original)}
|
||||
/>
|
||||
|
||||
@@ -27,13 +27,6 @@ function SetupLeftSectionFooter() {
|
||||
|
||||
return (
|
||||
<div className={'content__footer'}>
|
||||
<div className={'content__contact-info'}>
|
||||
<p>
|
||||
<T id={'setup.left_side.footer_help'} />{' '}
|
||||
<span>{'+21892-738-1987'}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={'content__links'}>
|
||||
<For render={FooterLinkItem} of={footerLinks} />
|
||||
</div>
|
||||
|
||||
@@ -54,15 +54,6 @@ function SetupOrganizationPage({ wizard }) {
|
||||
|
||||
return (
|
||||
<div className={'setup-organization'}>
|
||||
<div className={'setup-organization__title-wrap'}>
|
||||
<h1>
|
||||
<T id={'setup.organization.title'} />
|
||||
</h1>
|
||||
<p class="paragraph">
|
||||
<T id={'setup.organization.description'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Formik
|
||||
validationSchema={validationSchema}
|
||||
initialValues={initialValues}
|
||||
|
||||
@@ -4,7 +4,6 @@ import React from 'react';
|
||||
import SetupDialogs from './SetupDialogs';
|
||||
import SetupWizardContent from './SetupWizardContent';
|
||||
|
||||
import withSubscriptions from '@/containers/Subscriptions/withSubscriptions';
|
||||
import withOrganization from '@/containers/Organization/withOrganization';
|
||||
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
|
||||
import withSetupWizard from '@/store/organizations/withSetupWizard';
|
||||
@@ -23,9 +22,6 @@ function SetupRightSection({
|
||||
// #withSetupWizard
|
||||
setupStepId,
|
||||
setupStepIndex,
|
||||
|
||||
// #withSubscriptions
|
||||
isSubscriptionActive,
|
||||
}) {
|
||||
return (
|
||||
<section className={'setup-page__right-section'}>
|
||||
@@ -57,12 +53,6 @@ export default compose(
|
||||
isOrganizationBuildRunning,
|
||||
}),
|
||||
),
|
||||
withSubscriptions(
|
||||
({ isSubscriptionActive }) => ({
|
||||
isSubscriptionActive,
|
||||
}),
|
||||
'main',
|
||||
),
|
||||
withSetupWizard(({ setupStepId, setupStepIndex }) => ({
|
||||
setupStepId,
|
||||
setupStepIndex,
|
||||
|
||||
@@ -4,7 +4,6 @@ import React from 'react';
|
||||
import SetupSteps from './SetupSteps';
|
||||
import WizardSetupSteps from './WizardSetupSteps';
|
||||
|
||||
import SetupSubscription from './SetupSubscription';
|
||||
import SetupOrganizationPage from './SetupOrganizationPage';
|
||||
import SetupInitializingForm from './SetupInitializingForm';
|
||||
import SetupCongratsPage from './SetupCongratsPage';
|
||||
@@ -19,7 +18,6 @@ export default function SetupWizardContent({ setupStepIndex, setupStepId }) {
|
||||
|
||||
<div class="setup-page-form">
|
||||
<SetupSteps step={{ id: setupStepId }}>
|
||||
<SetupSubscription id="subscription" />
|
||||
<SetupOrganizationPage id="organization" />
|
||||
<SetupInitializingForm id={'initializing'} />
|
||||
<SetupCongratsPage id="congrats" />
|
||||
|
||||
@@ -25,6 +25,7 @@ export default function WizardSetupSteps({ currentStep = 1 }) {
|
||||
<WizardSetupStep
|
||||
label={step.label}
|
||||
isActive={index + 1 === currentStep}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -4,6 +4,7 @@ import intl from 'react-intl-universal';
|
||||
import { Formik, Form } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { AppToaster } from '@/components';
|
||||
@@ -115,9 +116,9 @@ function VendorFormFormik({
|
||||
onSubmit={handleFormSubmit}
|
||||
>
|
||||
<Form>
|
||||
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
|
||||
<VendorFormHeaderPrimary>
|
||||
<VendorFormPrimarySection />
|
||||
</div>
|
||||
</VendorFormHeaderPrimary>
|
||||
|
||||
<div className={'page-form__after-priamry-section'}>
|
||||
<VendorFormAfterPrimarySection />
|
||||
@@ -134,4 +135,12 @@ function VendorFormFormik({
|
||||
);
|
||||
}
|
||||
|
||||
export const VendorFormHeaderPrimary = styled.div`
|
||||
padding: 10px 0 0;
|
||||
margin: 0 0 20px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #e4e4e4;
|
||||
max-width: 1000px;
|
||||
`;
|
||||
|
||||
export default compose(withCurrentOrganization())(VendorFormFormik);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// @ts-nocheck
|
||||
import { omit } from 'lodash';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { batch } from 'react-redux';
|
||||
import t from './types';
|
||||
import useApiRequest from '../useRequest';
|
||||
import { useRequestQuery } from '../useQueryRequest';
|
||||
import { useSetOrganizations, useSetSubscriptions } from '../state';
|
||||
import { omit } from 'lodash';
|
||||
import { useSetOrganizations } from '../state';
|
||||
|
||||
/**
|
||||
* Retrieve organizations of the authenticated user.
|
||||
@@ -32,7 +32,6 @@ export function useOrganizations(props) {
|
||||
*/
|
||||
export function useCurrentOrganization(props) {
|
||||
const setOrganizations = useSetOrganizations();
|
||||
const setSubscriptions = useSetSubscriptions();
|
||||
|
||||
return useRequestQuery(
|
||||
[t.ORGANIZATION_CURRENT],
|
||||
@@ -44,9 +43,6 @@ export function useCurrentOrganization(props) {
|
||||
const organization = omit(data, ['subscriptions']);
|
||||
|
||||
batch(() => {
|
||||
// Sets subscriptions.
|
||||
setSubscriptions(data.subscriptions);
|
||||
|
||||
// Sets organizations.
|
||||
setOrganizations([organization]);
|
||||
});
|
||||
|
||||
@@ -1237,7 +1237,7 @@
|
||||
"expense.details.total": "Total",
|
||||
"manual_journal.details.subtotal": "Subtotal",
|
||||
"manual_journal.details.total": "Total",
|
||||
"setup.left_side.title": "Register a New Organization now!.",
|
||||
"setup.left_side.title": "Register a New Organization now!",
|
||||
"setup.left_side.description": "You have a Bigcapital account",
|
||||
"setup.left_side.footer_help": "We’re Here to Help!",
|
||||
"setup.plan.plans": "Plans & Payment",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import LazyLoader from '@/components/LazyLoader';
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/register/subscription',
|
||||
component: LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/Register/RegisterSubscriptionForm'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/register/organization',
|
||||
component: LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/Register/RegisterOrganizationForm'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: `/`,
|
||||
component: LazyLoader({
|
||||
loader: () => import('@/containers/Authentication/Register/RegisterUserForm'),
|
||||
}),
|
||||
},
|
||||
];
|
||||
@@ -559,4 +559,10 @@ export default {
|
||||
],
|
||||
viewBox: '0 0 24 24',
|
||||
},
|
||||
'content-copy': {
|
||||
path: [
|
||||
'M15 0H5c-.55 0-1 .45-1 1v2h2V2h8v7h-1v2h2c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1zm-4 4H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zm-1 10H2V6h8v8z'
|
||||
],
|
||||
viewBox: '0 0 16 16'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,18 +6,15 @@ export default (mapState) => {
|
||||
const {
|
||||
isOrganizationSetupCompleted,
|
||||
isOrganizationReady,
|
||||
isSubscriptionActive,
|
||||
isOrganizationBuildRunning
|
||||
} = props;
|
||||
|
||||
const condits = {
|
||||
isCongratsStep: isOrganizationSetupCompleted,
|
||||
isSubscriptionStep: !isSubscriptionActive,
|
||||
isInitializingStep: isOrganizationBuildRunning,
|
||||
isOrganizationStep: !isOrganizationReady && !isOrganizationBuildRunning,
|
||||
};
|
||||
const scenarios = [
|
||||
{ condition: condits.isSubscriptionStep, step: 'subscription' },
|
||||
{ condition: condits.isOrganizationStep, step: 'organization' },
|
||||
{ condition: condits.isInitializingStep, step: 'initializing' },
|
||||
{ condition: condits.isCongratsStep, step: 'congrats' },
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
display: block;
|
||||
|
||||
.thead .thead-inner,
|
||||
.tbody .tbody-inner{
|
||||
.tbody .tbody-inner {
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid #d2dde2;
|
||||
|
||||
>div {
|
||||
> div {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
@@ -208,6 +208,10 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&:focus {
|
||||
outline: 1px solid rgba(0, 82, 204, 0.7);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.tr:hover .td {
|
||||
@@ -357,13 +361,9 @@
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
[data-sticky-last-left-td] {
|
||||
[data-sticky-last-left-td] {}
|
||||
|
||||
}
|
||||
|
||||
[data-sticky-first-right-td] {
|
||||
|
||||
}
|
||||
[data-sticky-first-right-td] {}
|
||||
}
|
||||
|
||||
&.has-virtualized-rows {
|
||||
|
||||
@@ -1,30 +1,17 @@
|
||||
|
||||
.setup-organization {
|
||||
width: 580px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 45px 0 20px;
|
||||
|
||||
&__title-wrap {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #565e6c;
|
||||
}
|
||||
.paragraph {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
padding: 45px 25px 20px;
|
||||
|
||||
form {
|
||||
h3 {
|
||||
color: #6b7382;
|
||||
margin-bottom: 1.6rem;
|
||||
font-weight: 500;
|
||||
color: #868f9f;
|
||||
margin-bottom: 2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-form-group {
|
||||
margin-bottom: 24px;
|
||||
|
||||
@@ -64,10 +51,10 @@
|
||||
|
||||
.register-org-button {
|
||||
.bp3-button {
|
||||
background-color: #0052cc;
|
||||
min-width: 175px;
|
||||
background-color: #1c2448;
|
||||
height: 40px;
|
||||
font-size: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,17 @@
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
max-width: 350px;
|
||||
min-width: 350px;
|
||||
max-width: 600px;
|
||||
min-width: 600px;
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
min-width: 300px;
|
||||
max-width: 350px;
|
||||
min-width: 500px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1024px) {
|
||||
min-width: 400px;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,17 +45,20 @@
|
||||
|
||||
&__left-section {
|
||||
position: fixed;
|
||||
background-color: #01115e;
|
||||
background-color: #2f3d6f;
|
||||
overflow: auto;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 350px;
|
||||
width: 600px;
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
width: 300px;
|
||||
width: 500px;
|
||||
}
|
||||
@media only screen and (max-width: 1024px) {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -63,24 +71,29 @@
|
||||
height: 100%;
|
||||
|
||||
&__logo {
|
||||
opacity: 0.65;
|
||||
opacity: 0.75;
|
||||
margin-bottom: 60px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
font-size: 50px;
|
||||
font-weight: 100;
|
||||
line-height: normal;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 14px;
|
||||
margin-top: 14px;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
|
||||
@media only screen and (max-width: 1024px) {
|
||||
font-size: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
opacity: 0.75;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
&__organization {
|
||||
@@ -96,31 +109,21 @@
|
||||
}
|
||||
|
||||
&__divider {
|
||||
height: 3px;
|
||||
width: 100px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
margin: 10px 0;
|
||||
height: 1px;
|
||||
width: 60%;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
margin: 18px 0;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
&__contact-info {
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.75;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
|
||||
|
||||
p>span {
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.25);
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
&__links {
|
||||
text-align: left;
|
||||
opacity: 0.55;
|
||||
opacity: 0.65;
|
||||
|
||||
>div {
|
||||
font-size: 13px;
|
||||
@@ -148,12 +151,14 @@
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
padding: 0 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
list-style-type: none;
|
||||
width: 25%;
|
||||
width: 33%;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"buildCommand": "npm run build:webapp",
|
||||
"devCommand": "npm run dev:webapp",
|
||||
"installCommand": "npm install && npm run bootstrap",
|
||||
"outputDirectory": "packages/webapp/build",
|
||||
"env": {
|
||||
"CI": "false"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user