mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-11 10:20:30 +00:00
Compare commits
1 Commits
docker-dev
...
vercel-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a854b42ce5 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
data
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
version: '3.3'
|
|
||||||
|
|
||||||
services:
|
|
||||||
mysql:
|
|
||||||
build:
|
|
||||||
context: ./docker/mysql
|
|
||||||
args:
|
|
||||||
- MYSQL_DATABASE=bigcapital_system
|
|
||||||
- MYSQL_USER=default_user
|
|
||||||
- MYSQL_PASSWORD=secret
|
|
||||||
- MYSQL_ROOT_PASSWORD=root
|
|
||||||
volumes:
|
|
||||||
- ./data/mysql/:/var/lib/mysql
|
|
||||||
expose:
|
|
||||||
- '3306'
|
|
||||||
ports:
|
|
||||||
- '3306:3306'
|
|
||||||
|
|
||||||
mongo:
|
|
||||||
build: ./docker/mongo
|
|
||||||
expose:
|
|
||||||
- '27017'
|
|
||||||
volumes:
|
|
||||||
- ./data/mongo/:/var/lib/mongodb
|
|
||||||
ports:
|
|
||||||
- '27017:27017'
|
|
||||||
|
|
||||||
redis:
|
|
||||||
build:
|
|
||||||
context: ./docker/redis
|
|
||||||
expose:
|
|
||||||
- "6379"
|
|
||||||
volumes:
|
|
||||||
- ./data/redis:/data
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
FROM mongo:5.0
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
FROM mysql:5.7
|
|
||||||
|
|
||||||
ADD my.cnf /etc/mysql/conf.d/my.cnf
|
|
||||||
|
|
||||||
RUN chown -R mysql:root /var/lib/mysql/
|
|
||||||
|
|
||||||
ARG MYSQL_DATABASE=default_database
|
|
||||||
ARG MYSQL_USER=default_user
|
|
||||||
ARG MYSQL_PASSWORD=secret
|
|
||||||
ARG MYSQL_ROOT_PASSWORD=root
|
|
||||||
|
|
||||||
ENV MYSQL_DATABASE=$MYSQL_DATABASE
|
|
||||||
ENV MYSQL_USER=$MYSQL_USER
|
|
||||||
ENV MYSQL_PASSWORD=$MYSQL_PASSWORD
|
|
||||||
ENV MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD
|
|
||||||
|
|
||||||
CMD ["mysqld"]
|
|
||||||
EXPOSE 3306
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
[mysqld]
|
|
||||||
bind-address = 0.0.0.0
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
FROM redis:4.0
|
|
||||||
|
|
||||||
COPY redis.conf /usr/local/etc/redis/redis.conf
|
|
||||||
|
|
||||||
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
daemonize no
|
|
||||||
pidfile /var/run/redis.pid
|
|
||||||
port 6379
|
|
||||||
tcp-backlog 511
|
|
||||||
timeout 0
|
|
||||||
tcp-keepalive 0
|
|
||||||
loglevel notice
|
|
||||||
logfile ""
|
|
||||||
databases 16
|
|
||||||
save 900 1
|
|
||||||
save 300 10
|
|
||||||
save 60 10000
|
|
||||||
stop-writes-on-bgsave-error yes
|
|
||||||
rdbcompression yes
|
|
||||||
rdbchecksum yes
|
|
||||||
dbfilename dump.rdb
|
|
||||||
slave-serve-stale-data yes
|
|
||||||
slave-read-only yes
|
|
||||||
repl-diskless-sync no
|
|
||||||
repl-diskless-sync-delay 5
|
|
||||||
repl-disable-tcp-nodelay no
|
|
||||||
slave-priority 100
|
|
||||||
appendonly no
|
|
||||||
appendfilename "appendonly.aof"
|
|
||||||
appendfsync everysec
|
|
||||||
no-appendfsync-on-rewrite no
|
|
||||||
auto-aof-rewrite-percentage 100
|
|
||||||
auto-aof-rewrite-min-size 64mb
|
|
||||||
aof-load-truncated yes
|
|
||||||
lua-time-limit 5000
|
|
||||||
slowlog-log-slower-than 10000
|
|
||||||
slowlog-max-len 128
|
|
||||||
latency-monitor-threshold 0
|
|
||||||
notify-keyspace-events ""
|
|
||||||
hash-max-ziplist-entries 512
|
|
||||||
hash-max-ziplist-value 64
|
|
||||||
list-max-ziplist-entries 512
|
|
||||||
list-max-ziplist-value 64
|
|
||||||
set-max-intset-entries 512
|
|
||||||
zset-max-ziplist-entries 128
|
|
||||||
zset-max-ziplist-value 64
|
|
||||||
hll-sparse-max-bytes 3000
|
|
||||||
activerehashing yes
|
|
||||||
client-output-buffer-limit normal 0 0 0
|
|
||||||
client-output-buffer-limit slave 256mb 64mb 60
|
|
||||||
client-output-buffer-limit pubsub 32mb 8mb 60
|
|
||||||
hz 10
|
|
||||||
aof-rewrite-incremental-fsync yes
|
|
||||||
5697
package-lock.json
generated
5697
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@ import DynamicListingService from '@/services/DynamicListing/DynamicListService'
|
|||||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||||
import { AccountsApplication } from '@/services/Accounts/AccountsApplication';
|
import { AccountsApplication } from '@/services/Accounts/AccountsApplication';
|
||||||
import { MAX_ACCOUNTS_CHART_DEPTH } from 'services/Accounts/constants';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class AccountsController extends BaseController {
|
export default class AccountsController extends BaseController {
|
||||||
@@ -495,22 +494,6 @@ 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);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { check, ValidationChain } from 'express-validator';
|
|||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||||
|
import SubscriptionMiddleware from '@/api/middleware/SubscriptionMiddleware';
|
||||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||||
import OrganizationService from '@/services/Organization/OrganizationService';
|
import OrganizationService from '@/services/Organization/OrganizationService';
|
||||||
import {
|
import {
|
||||||
@@ -23,7 +24,7 @@ const ACCEPTED_LOCATIONS = ['libya'];
|
|||||||
@Service()
|
@Service()
|
||||||
export default class OrganizationController extends BaseController {
|
export default class OrganizationController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
private organizationService: OrganizationService;
|
organizationService: OrganizationService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router constructor.
|
* Router constructor.
|
||||||
@@ -31,10 +32,13 @@ export default class OrganizationController extends BaseController {
|
|||||||
router() {
|
router() {
|
||||||
const router = 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(JWTAuth);
|
||||||
router.use(AttachCurrentTenantUser);
|
router.use(AttachCurrentTenantUser);
|
||||||
router.use(TenancyMiddleware);
|
router.use(TenancyMiddleware);
|
||||||
|
|
||||||
|
router.use('/build', SubscriptionMiddleware('main'));
|
||||||
router.post(
|
router.post(
|
||||||
'/build',
|
'/build',
|
||||||
this.organizationValidationSchema,
|
this.organizationValidationSchema,
|
||||||
|
|||||||
102
packages/server/src/api/controllers/Setup.ts
Normal file
102
packages/server/src/api/controllers/Setup.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
250
packages/server/src/api/controllers/Subscription/Licenses.ts
Normal file
250
packages/server/src/api/controllers/Subscription/Licenses.ts
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
packages/server/src/api/controllers/Subscription/index.ts
Normal file
49
packages/server/src/api/controllers/Subscription/index.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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,6 +4,7 @@ import { Container } from 'typedi';
|
|||||||
// Middlewares
|
// Middlewares
|
||||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||||
|
import SubscriptionMiddleware from '@/api/middleware/SubscriptionMiddleware';
|
||||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||||
import EnsureTenantIsInitialized from '@/api/middleware/EnsureTenantIsInitialized';
|
import EnsureTenantIsInitialized from '@/api/middleware/EnsureTenantIsInitialized';
|
||||||
import SettingsMiddleware from '@/api/middleware/SettingsMiddleware';
|
import SettingsMiddleware from '@/api/middleware/SettingsMiddleware';
|
||||||
@@ -36,6 +37,8 @@ import Resources from './controllers/Resources';
|
|||||||
import ExchangeRates from '@/api/controllers/ExchangeRates';
|
import ExchangeRates from '@/api/controllers/ExchangeRates';
|
||||||
import Media from '@/api/controllers/Media';
|
import Media from '@/api/controllers/Media';
|
||||||
import Ping from '@/api/controllers/Ping';
|
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 InventoryAdjustments from '@/api/controllers/Inventory/InventoryAdjustments';
|
||||||
import asyncRenderMiddleware from './middleware/AsyncRenderMiddleware';
|
import asyncRenderMiddleware from './middleware/AsyncRenderMiddleware';
|
||||||
import Jobs from './controllers/Jobs';
|
import Jobs from './controllers/Jobs';
|
||||||
@@ -66,6 +69,8 @@ export default () => {
|
|||||||
|
|
||||||
app.use('/auth', Container.get(Authentication).router());
|
app.use('/auth', Container.get(Authentication).router());
|
||||||
app.use('/invite', Container.get(InviteUsers).nonAuthRouter());
|
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('/organization', Container.get(Organization).router());
|
||||||
app.use('/ping', Container.get(Ping).router());
|
app.use('/ping', Container.get(Ping).router());
|
||||||
app.use('/jobs', Container.get(Jobs).router());
|
app.use('/jobs', Container.get(Jobs).router());
|
||||||
@@ -78,6 +83,7 @@ export default () => {
|
|||||||
dashboard.use(JWTAuth);
|
dashboard.use(JWTAuth);
|
||||||
dashboard.use(AttachCurrentTenantUser);
|
dashboard.use(AttachCurrentTenantUser);
|
||||||
dashboard.use(TenancyMiddleware);
|
dashboard.use(TenancyMiddleware);
|
||||||
|
dashboard.use(SubscriptionMiddleware('main'));
|
||||||
dashboard.use(EnsureTenantIsInitialized);
|
dashboard.use(EnsureTenantIsInitialized);
|
||||||
dashboard.use(SettingsMiddleware);
|
dashboard.use(SettingsMiddleware);
|
||||||
dashboard.use(I18nAuthenticatedMiddlware);
|
dashboard.use(I18nAuthenticatedMiddlware);
|
||||||
|
|||||||
41
packages/server/src/api/middleware/SubscriptionMiddleware.ts
Normal file
41
packages/server/src/api/middleware/SubscriptionMiddleware.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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();
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export default class NoPaymentModelWithPricedPlan {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export default class NotAllowedChangeSubscriptionPlan {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.name = "NotAllowedChangeSubscriptionPlan";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export default class PaymentAmountInvalidWithPlan{
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/server/src/exceptions/PaymentInputInvalid.ts
Normal file
3
packages/server/src/exceptions/PaymentInputInvalid.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default class PaymentInputInvalid {
|
||||||
|
constructor() {}
|
||||||
|
}
|
||||||
5
packages/server/src/exceptions/VoucherCodeRequired.ts
Normal file
5
packages/server/src/exceptions/VoucherCodeRequired.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default class VoucherCodeRequired {
|
||||||
|
constructor() {
|
||||||
|
this.name = 'VoucherCodeRequired';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,25 @@
|
|||||||
|
import NotAllowedChangeSubscriptionPlan from './NotAllowedChangeSubscriptionPlan';
|
||||||
import ServiceError from './ServiceError';
|
import ServiceError from './ServiceError';
|
||||||
import ServiceErrors from './ServiceErrors';
|
import ServiceErrors from './ServiceErrors';
|
||||||
|
import NoPaymentModelWithPricedPlan from './NoPaymentModelWithPricedPlan';
|
||||||
|
import PaymentInputInvalid from './PaymentInputInvalid';
|
||||||
|
import PaymentAmountInvalidWithPlan from './PaymentAmountInvalidWithPlan';
|
||||||
import TenantAlreadyInitialized from './TenantAlreadyInitialized';
|
import TenantAlreadyInitialized from './TenantAlreadyInitialized';
|
||||||
import TenantAlreadySeeded from './TenantAlreadySeeded';
|
import TenantAlreadySeeded from './TenantAlreadySeeded';
|
||||||
import TenantDBAlreadyExists from './TenantDBAlreadyExists';
|
import TenantDBAlreadyExists from './TenantDBAlreadyExists';
|
||||||
import TenantDatabaseNotBuilt from './TenantDatabaseNotBuilt';
|
import TenantDatabaseNotBuilt from './TenantDatabaseNotBuilt';
|
||||||
|
import VoucherCodeRequired from './VoucherCodeRequired';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
NotAllowedChangeSubscriptionPlan,
|
||||||
|
NoPaymentModelWithPricedPlan,
|
||||||
|
PaymentAmountInvalidWithPlan,
|
||||||
ServiceError,
|
ServiceError,
|
||||||
ServiceErrors,
|
ServiceErrors,
|
||||||
|
PaymentInputInvalid,
|
||||||
TenantAlreadyInitialized,
|
TenantAlreadyInitialized,
|
||||||
TenantAlreadySeeded,
|
TenantAlreadySeeded,
|
||||||
TenantDBAlreadyExists,
|
TenantDBAlreadyExists,
|
||||||
TenantDatabaseNotBuilt,
|
TenantDatabaseNotBuilt,
|
||||||
};
|
VoucherCodeRequired,
|
||||||
|
};
|
||||||
34
packages/server/src/jobs/MailNotificationSubscribeEnd.ts
Normal file
34
packages/server/src/jobs/MailNotificationSubscribeEnd.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
packages/server/src/jobs/MailNotificationTrialEnd.ts
Normal file
34
packages/server/src/jobs/MailNotificationTrialEnd.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
packages/server/src/jobs/SMSNotificationSubscribeEnd.ts
Normal file
28
packages/server/src/jobs/SMSNotificationSubscribeEnd.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
packages/server/src/jobs/SMSNotificationTrialEnd.ts
Normal file
28
packages/server/src/jobs/SMSNotificationTrialEnd.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
packages/server/src/jobs/SendLicenseEmail.ts
Normal file
33
packages/server/src/jobs/SendLicenseEmail.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
packages/server/src/jobs/SendLicensePhone.ts
Normal file
33
packages/server/src/jobs/SendLicensePhone.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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,6 +4,12 @@ import WelcomeSMSJob from 'jobs/WelcomeSMS';
|
|||||||
import ResetPasswordMailJob from 'jobs/ResetPasswordMail';
|
import ResetPasswordMailJob from 'jobs/ResetPasswordMail';
|
||||||
import ComputeItemCost from 'jobs/ComputeItemCost';
|
import ComputeItemCost from 'jobs/ComputeItemCost';
|
||||||
import RewriteInvoicesJournalEntries from 'jobs/writeInvoicesJEntries';
|
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 UserInviteMailJob from 'jobs/UserInviteMail';
|
||||||
import OrganizationSetupJob from 'jobs/OrganizationSetup';
|
import OrganizationSetupJob from 'jobs/OrganizationSetup';
|
||||||
import OrganizationUpgrade from 'jobs/OrganizationUpgrade';
|
import OrganizationUpgrade from 'jobs/OrganizationUpgrade';
|
||||||
@@ -14,11 +20,33 @@ export default ({ agenda }: { agenda: Agenda }) => {
|
|||||||
new ResetPasswordMailJob(agenda);
|
new ResetPasswordMailJob(agenda);
|
||||||
new WelcomeSMSJob(agenda);
|
new WelcomeSMSJob(agenda);
|
||||||
new UserInviteMailJob(agenda);
|
new UserInviteMailJob(agenda);
|
||||||
|
new SendLicenseViaEmailJob(agenda);
|
||||||
|
new SendLicenseViaPhoneJob(agenda);
|
||||||
new ComputeItemCost(agenda);
|
new ComputeItemCost(agenda);
|
||||||
new RewriteInvoicesJournalEntries(agenda);
|
new RewriteInvoicesJournalEntries(agenda);
|
||||||
new OrganizationSetupJob(agenda);
|
new OrganizationSetupJob(agenda);
|
||||||
new OrganizationUpgrade(agenda);
|
new OrganizationUpgrade(agenda);
|
||||||
new SmsNotification(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();
|
agenda.start();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Container from 'typedi';
|
import Container from 'typedi';
|
||||||
import {
|
import {
|
||||||
SystemUserRepository,
|
SystemUserRepository,
|
||||||
|
SubscriptionRepository,
|
||||||
TenantRepository,
|
TenantRepository,
|
||||||
} from '@/system/repositories';
|
} from '@/system/repositories';
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ export default () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
systemUserRepository: new SystemUserRepository(knex, cache),
|
systemUserRepository: new SystemUserRepository(knex, cache),
|
||||||
|
subscriptionRepository: new SubscriptionRepository(knex, cache),
|
||||||
tenantRepository: new TenantRepository(knex, cache),
|
tenantRepository: new TenantRepository(knex, cache),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,9 @@ import './before';
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import loadersFactory from 'loaders';
|
import loadersFactory from 'loaders';
|
||||||
|
|
||||||
|
|
||||||
|
console.log("asdfasf");
|
||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import TenancyService from '@/services/Tenancy/TenancyService';
|
|||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { IAccountDTO, IAccount, IAccountCreateDTO } from '@/interfaces';
|
import { IAccountDTO, IAccount, IAccountCreateDTO } from '@/interfaces';
|
||||||
import AccountTypesUtils from '@/lib/AccountTypes';
|
import AccountTypesUtils from '@/lib/AccountTypes';
|
||||||
import { ERRORS, MAX_ACCOUNTS_CHART_DEPTH } from './constants';
|
import { ERRORS } from './constants';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CommandAccountValidators {
|
export class CommandAccountValidators {
|
||||||
@@ -154,13 +154,13 @@ export class CommandAccountValidators {
|
|||||||
* parent account.
|
* parent account.
|
||||||
* @param {IAccountCreateDTO} accountDTO
|
* @param {IAccountCreateDTO} accountDTO
|
||||||
* @param {IAccount} parentAccount
|
* @param {IAccount} parentAccount
|
||||||
* @param {string} baseCurrency -
|
* @param {string} baseCurrency -
|
||||||
* @throws {ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT)}
|
* @throws {ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT)}
|
||||||
*/
|
*/
|
||||||
public validateCurrentSameParentAccount = (
|
public validateCurrentSameParentAccount = (
|
||||||
accountDTO: IAccountCreateDTO,
|
accountDTO: IAccountCreateDTO,
|
||||||
parentAccount: IAccount,
|
parentAccount: IAccount,
|
||||||
baseCurrency: string
|
baseCurrency: string,
|
||||||
) => {
|
) => {
|
||||||
// If the account DTO currency not assigned and the parent account has no base currency.
|
// If the account DTO currency not assigned and the parent account has no base currency.
|
||||||
if (
|
if (
|
||||||
@@ -208,24 +208,4 @@ export class CommandAccountValidators {
|
|||||||
}
|
}
|
||||||
return account;
|
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,11 +70,6 @@ export class CreateAccount {
|
|||||||
parentAccount,
|
parentAccount,
|
||||||
baseCurrency
|
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.
|
// Validates the given account type supports the multi-currency.
|
||||||
this.validator.validateAccountTypeSupportCurrency(accountDTO, baseCurrency);
|
this.validator.validateAccountTypeSupportCurrency(accountDTO, baseCurrency);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import TenancyService from '@/services/Tenancy/TenancyService';
|
|||||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||||
import { AccountTransformer } from './AccountTransform';
|
import { AccountTransformer } from './AccountTransform';
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
import { flatToNestedArray } from '@/utils';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class GetAccounts {
|
export class GetAccounts {
|
||||||
@@ -54,17 +53,11 @@ export class GetAccounts {
|
|||||||
builder.modify('inactiveMode', filter.inactiveMode);
|
builder.modify('inactiveMode', filter.inactiveMode);
|
||||||
});
|
});
|
||||||
// Retrievs the formatted accounts collection.
|
// Retrievs the formatted accounts collection.
|
||||||
const preTransformedAccounts = await this.transformer.transform(
|
const transformedAccounts = await this.transformer.transform(
|
||||||
tenantId,
|
tenantId,
|
||||||
accounts,
|
accounts,
|
||||||
new AccountTransformer()
|
new AccountTransformer()
|
||||||
);
|
);
|
||||||
// Transform accounts to nested array.
|
|
||||||
const transformedAccounts = flatToNestedArray(preTransformedAccounts, {
|
|
||||||
id: 'id',
|
|
||||||
parentId: 'parentAccountId',
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accounts: transformedAccounts,
|
accounts: transformedAccounts,
|
||||||
filterMeta: dynamicList.getResponseMeta(),
|
filterMeta: dynamicList.getResponseMeta(),
|
||||||
|
|||||||
@@ -13,12 +13,8 @@ export const ERRORS = {
|
|||||||
CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE:
|
CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE:
|
||||||
'close_account_and_to_account_not_same_type',
|
'close_account_and_to_account_not_same_type',
|
||||||
ACCOUNTS_NOT_FOUND: 'accounts_not_found',
|
ACCOUNTS_NOT_FOUND: 'accounts_not_found',
|
||||||
ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY:
|
ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY: '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_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.
|
// Default views columns.
|
||||||
@@ -31,8 +27,6 @@ export const DEFAULT_VIEW_COLUMNS = [
|
|||||||
{ key: 'currencyCode', label: 'Currency' },
|
{ key: 'currencyCode', label: 'Currency' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const MAX_ACCOUNTS_CHART_DEPTH = 5;
|
|
||||||
|
|
||||||
// Accounts default views.
|
// Accounts default views.
|
||||||
export const DEFAULT_VIEWS = [
|
export const DEFAULT_VIEWS = [
|
||||||
{
|
{
|
||||||
@@ -49,12 +43,7 @@ export const DEFAULT_VIEWS = [
|
|||||||
slug: 'liabilities',
|
slug: 'liabilities',
|
||||||
rolesLogicExpression: '1',
|
rolesLogicExpression: '1',
|
||||||
roles: [
|
roles: [
|
||||||
{
|
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'liability' },
|
||||||
fieldKey: 'root_type',
|
|
||||||
index: 1,
|
|
||||||
comparator: 'equals',
|
|
||||||
value: 'liability',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
columns: DEFAULT_VIEW_COLUMNS,
|
columns: DEFAULT_VIEW_COLUMNS,
|
||||||
},
|
},
|
||||||
@@ -63,12 +52,7 @@ export const DEFAULT_VIEWS = [
|
|||||||
slug: 'equity',
|
slug: 'equity',
|
||||||
rolesLogicExpression: '1',
|
rolesLogicExpression: '1',
|
||||||
roles: [
|
roles: [
|
||||||
{
|
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'equity' },
|
||||||
fieldKey: 'root_type',
|
|
||||||
index: 1,
|
|
||||||
comparator: 'equals',
|
|
||||||
value: 'equity',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
columns: DEFAULT_VIEW_COLUMNS,
|
columns: DEFAULT_VIEW_COLUMNS,
|
||||||
},
|
},
|
||||||
@@ -77,12 +61,7 @@ export const DEFAULT_VIEWS = [
|
|||||||
slug: 'income',
|
slug: 'income',
|
||||||
rolesLogicExpression: '1',
|
rolesLogicExpression: '1',
|
||||||
roles: [
|
roles: [
|
||||||
{
|
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'income' },
|
||||||
fieldKey: 'root_type',
|
|
||||||
index: 1,
|
|
||||||
comparator: 'equals',
|
|
||||||
value: 'income',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
columns: DEFAULT_VIEW_COLUMNS,
|
columns: DEFAULT_VIEW_COLUMNS,
|
||||||
},
|
},
|
||||||
@@ -91,12 +70,7 @@ export const DEFAULT_VIEWS = [
|
|||||||
slug: 'expenses',
|
slug: 'expenses',
|
||||||
rolesLogicExpression: '1',
|
rolesLogicExpression: '1',
|
||||||
roles: [
|
roles: [
|
||||||
{
|
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'expense' },
|
||||||
fieldKey: 'root_type',
|
|
||||||
index: 1,
|
|
||||||
comparator: 'equals',
|
|
||||||
value: 'expense',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
columns: DEFAULT_VIEW_COLUMNS,
|
columns: DEFAULT_VIEW_COLUMNS,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ export default class OrganizationService {
|
|||||||
public async currentOrganization(tenantId: number): Promise<ITenant> {
|
public async currentOrganization(tenantId: number): Promise<ITenant> {
|
||||||
const tenant = await Tenant.query()
|
const tenant = await Tenant.query()
|
||||||
.findById(tenantId)
|
.findById(tenantId)
|
||||||
|
.withGraphFetched('subscriptions')
|
||||||
.withGraphFetched('metadata');
|
.withGraphFetched('metadata');
|
||||||
|
|
||||||
this.throwIfTenantNotExists(tenant);
|
this.throwIfTenantNotExists(tenant);
|
||||||
|
|||||||
185
packages/server/src/services/Payment/License.ts
Normal file
185
packages/server/src/services/Payment/License.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
packages/server/src/services/Payment/LicenseMailMessages.ts
Normal file
26
packages/server/src/services/Payment/LicenseMailMessages.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
67
packages/server/src/services/Payment/LicensePaymentMethod.ts
Normal file
67
packages/server/src/services/Payment/LicensePaymentMethod.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
packages/server/src/services/Payment/LicenseSMSMessages.ts
Normal file
17
packages/server/src/services/Payment/LicenseSMSMessages.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/server/src/services/Payment/PaymentMethod.ts
Normal file
6
packages/server/src/services/Payment/PaymentMethod.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import { IPaymentModel } from '@/interfaces';
|
||||||
|
|
||||||
|
export default class PaymentMethod implements IPaymentModel {
|
||||||
|
|
||||||
|
}
|
||||||
22
packages/server/src/services/Payment/index.ts
Normal file
22
packages/server/src/services/Payment/index.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
packages/server/src/services/Subscription/MailMessages.ts
Normal file
30
packages/server/src/services/Subscription/MailMessages.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
packages/server/src/services/Subscription/SMSMessages.ts
Normal file
40
packages/server/src/services/Subscription/SMSMessages.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
packages/server/src/services/Subscription/Subscription.ts
Normal file
80
packages/server/src/services/Subscription/Subscription.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
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')
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
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')
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
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');
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
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');
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
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');
|
||||||
|
};
|
||||||
129
packages/server/src/system/models/Subscriptions/License.ts
Normal file
129
packages/server/src/system/models/Subscriptions/License.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
packages/server/src/system/models/Subscriptions/Plan.ts
Normal file
82
packages/server/src/system/models/Subscriptions/Plan.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
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,8 +1,10 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Model } from 'objection';
|
import { Model } from 'objection';
|
||||||
import uniqid from 'uniqid';
|
import uniqid from 'uniqid';
|
||||||
|
import SubscriptionPeriod from '@/services/Subscription/SubscriptionPeriod';
|
||||||
import BaseModel from 'models/Model';
|
import BaseModel from 'models/Model';
|
||||||
import TenantMetadata from './TenantMetadata';
|
import TenantMetadata from './TenantMetadata';
|
||||||
|
import PlanSubscription from './Subscriptions/PlanSubscription';
|
||||||
|
|
||||||
export default class Tenant extends BaseModel {
|
export default class Tenant extends BaseModel {
|
||||||
/**
|
/**
|
||||||
@@ -47,13 +49,33 @@ export default class Tenant extends BaseModel {
|
|||||||
return !!this.upgradeJobId;
|
return !!this.upgradeJobId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query modifiers.
|
||||||
|
*/
|
||||||
|
static modifiers() {
|
||||||
|
return {
|
||||||
|
subscriptions(builder) {
|
||||||
|
builder.withGraphFetched('subscriptions');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relations mappings.
|
* Relations mappings.
|
||||||
*/
|
*/
|
||||||
static get relationMappings() {
|
static get relationMappings() {
|
||||||
|
const PlanSubscription = require('./Subscriptions/PlanSubscription');
|
||||||
const TenantMetadata = require('./TenantMetadata');
|
const TenantMetadata = require('./TenantMetadata');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
subscriptions: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: PlanSubscription.default,
|
||||||
|
join: {
|
||||||
|
from: 'tenants.id',
|
||||||
|
to: 'subscription_plan_subscriptions.tenantId',
|
||||||
|
},
|
||||||
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
relation: Model.HasOneRelation,
|
relation: Model.HasOneRelation,
|
||||||
modelClass: TenantMetadata.default,
|
modelClass: TenantMetadata.default,
|
||||||
@@ -64,6 +86,55 @@ 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.
|
* Creates a new tenant with random organization id.
|
||||||
*/
|
*/
|
||||||
@@ -114,9 +185,9 @@ export default class Tenant extends BaseModel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the given tenant as upgrading.
|
* Marks the given tenant as upgrading.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {string} upgradeJobId
|
* @param {string} upgradeJobId
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static markAsUpgrading(tenantId, upgradeJobId) {
|
static markAsUpgrading(tenantId, upgradeJobId) {
|
||||||
return this.query().update({ upgradeJobId }).where({ id: tenantId });
|
return this.query().update({ upgradeJobId }).where({ id: tenantId });
|
||||||
@@ -124,8 +195,8 @@ export default class Tenant extends BaseModel {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Markes the given tenant as upgraded.
|
* Markes the given tenant as upgraded.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static markAsUpgraded(tenantId) {
|
static markAsUpgraded(tenantId) {
|
||||||
return this.query().update({ upgradeJobId: null }).where({ id: tenantId });
|
return this.query().update({ upgradeJobId: null }).where({ id: tenantId });
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
|
|
||||||
|
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 Tenant from './Tenant';
|
||||||
import TenantMetadata from './TenantMetadata';
|
import TenantMetadata from './TenantMetadata';
|
||||||
import SystemUser from './SystemUser';
|
import SystemUser from './SystemUser';
|
||||||
import PasswordReset from './PasswordReset';
|
import PasswordReset from './PasswordReset';
|
||||||
import Invite from './Invite';
|
import Invite from './Invite';
|
||||||
|
|
||||||
export { Tenant, TenantMetadata, SystemUser, PasswordReset, Invite };
|
export {
|
||||||
|
Plan,
|
||||||
|
PlanFeature,
|
||||||
|
PlanSubscription,
|
||||||
|
License,
|
||||||
|
Tenant,
|
||||||
|
TenantMetadata,
|
||||||
|
SystemUser,
|
||||||
|
PasswordReset,
|
||||||
|
Invite,
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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,4 +1,9 @@
|
|||||||
import SystemUserRepository from '@/system/repositories/SystemUserRepository';
|
import SystemUserRepository from '@/system/repositories/SystemUserRepository';
|
||||||
|
import SubscriptionRepository from '@/system/repositories/SubscriptionRepository';
|
||||||
import TenantRepository from '@/system/repositories/TenantRepository';
|
import TenantRepository from '@/system/repositories/TenantRepository';
|
||||||
|
|
||||||
export { SystemUserRepository, TenantRepository };
|
export {
|
||||||
|
SystemUserRepository,
|
||||||
|
SubscriptionRepository,
|
||||||
|
TenantRepository,
|
||||||
|
};
|
||||||
66
packages/server/src/system/seeds/seed_subscriptions_plans.js
Normal file
66
packages/server/src/system/seeds/seed_subscriptions_plans.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
"yup": "^0.28.1"
|
"yup": "^0.28.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "PORT=4000 craco start",
|
"dev": "craco start",
|
||||||
"build": "craco build",
|
"build": "craco build",
|
||||||
"test": "node scripts/test.js",
|
"test": "node scripts/test.js",
|
||||||
"storybook": "start-storybook -p 6006"
|
"storybook": "start-storybook -p 6006"
|
||||||
|
|||||||
@@ -14,16 +14,19 @@ import GlobalHotkeys from './GlobalHotkeys';
|
|||||||
import DashboardProvider from './DashboardProvider';
|
import DashboardProvider from './DashboardProvider';
|
||||||
import DrawersContainer from '@/components/DrawersContainer';
|
import DrawersContainer from '@/components/DrawersContainer';
|
||||||
import AlertsContainer from '@/containers/AlertsContainer';
|
import AlertsContainer from '@/containers/AlertsContainer';
|
||||||
|
import EnsureSubscriptionIsActive from '../Guards/EnsureSubscriptionIsActive';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dashboard preferences.
|
* Dashboard preferences.
|
||||||
*/
|
*/
|
||||||
function DashboardPreferences() {
|
function DashboardPreferences() {
|
||||||
return (
|
return (
|
||||||
<DashboardSplitPane>
|
<EnsureSubscriptionIsActive>
|
||||||
<Sidebar />
|
<DashboardSplitPane>
|
||||||
<PreferencesPage />
|
<Sidebar />
|
||||||
</DashboardSplitPane>
|
<PreferencesPage />
|
||||||
|
</DashboardSplitPane>
|
||||||
|
</EnsureSubscriptionIsActive>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,20 +7,26 @@ import {
|
|||||||
} from '@/hooks/query';
|
} from '@/hooks/query';
|
||||||
import { useSplashLoading } from '@/hooks/state';
|
import { useSplashLoading } from '@/hooks/state';
|
||||||
import { useWatch, useWatchImmediate, useWhen } from '@/hooks';
|
import { useWatch, useWatchImmediate, useWhen } from '@/hooks';
|
||||||
|
import { useSubscription } from '@/hooks/state';
|
||||||
import { setCookie, getCookie } from '@/utils';
|
import { setCookie, getCookie } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dashboard meta async booting.
|
* Dashboard meta async booting.
|
||||||
* - Fetches the dashboard meta in booting state.
|
* - Fetches the dashboard meta only if the organization subscribe is active.
|
||||||
* - Once the dashboard meta query started loading display dashboard splash screen.
|
* - Once the dashboard meta query is loading display dashboard splash screen.
|
||||||
*/
|
*/
|
||||||
export function useDashboardMetaBoot() {
|
export function useDashboardMetaBoot() {
|
||||||
|
const { isSubscriptionActive } = useSubscription();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: dashboardMeta,
|
data: dashboardMeta,
|
||||||
isLoading: isDashboardMetaLoading,
|
isLoading: isDashboardMetaLoading,
|
||||||
isSuccess: isDashboardMetaSuccess,
|
isSuccess: isDashboardMetaSuccess,
|
||||||
} = useDashboardMeta({
|
} = useDashboardMeta({
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
|
|
||||||
|
// Avoid run the query if the organization subscription is not active.
|
||||||
|
enabled: isSubscriptionActive,
|
||||||
});
|
});
|
||||||
const [startLoading, stopLoading] = useSplashLoading();
|
const [startLoading, stopLoading] = useSplashLoading();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import {
|
import {
|
||||||
CollapsibleList,
|
CollapsibleList,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
@@ -8,29 +7,29 @@ import {
|
|||||||
Boundary,
|
Boundary,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import withBreadcrumbs from 'react-router-breadcrumbs-hoc';
|
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();
|
const history = useHistory();
|
||||||
|
|
||||||
return (
|
return(
|
||||||
<CollapsibleList
|
<CollapsibleList
|
||||||
className={Classes.BREADCRUMBS}
|
className={Classes.BREADCRUMBS}
|
||||||
dropdownTarget={<span className={Classes.BREADCRUMBS_COLLAPSED} />}
|
dropdownTarget={<span className={Classes.BREADCRUMBS_COLLAPSED} />}
|
||||||
collapseFrom={Boundary.START}
|
collapseFrom={Boundary.START}
|
||||||
visibleItemCount={0}
|
visibleItemCount={0}>
|
||||||
>
|
{
|
||||||
{breadcrumbs.map(({ breadcrumb, match }) => {
|
breadcrumbs.map(({ breadcrumb,match })=>{
|
||||||
return (
|
return (<MenuItem
|
||||||
<MenuItem
|
key={match.url}
|
||||||
key={match.url}
|
icon={'folder-close'}
|
||||||
icon={'folder-close'}
|
text={breadcrumb}
|
||||||
text={breadcrumb}
|
onClick={() => history.push(match.url) } />)
|
||||||
onClick={() => history.push(match.url)}
|
})
|
||||||
/>
|
}
|
||||||
);
|
|
||||||
})}
|
|
||||||
</CollapsibleList>
|
</CollapsibleList>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withBreadcrumbs([])(DashboardBreadcrumbs);
|
export default withBreadcrumbs([])(DashboardBreadcrumbs)
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ import React from 'react';
|
|||||||
import { Route, Switch } from 'react-router-dom';
|
import { Route, Switch } from 'react-router-dom';
|
||||||
|
|
||||||
import { getDashboardRoutes } from '@/routes/dashboard';
|
import { getDashboardRoutes } from '@/routes/dashboard';
|
||||||
|
import EnsureSubscriptionsIsActive from '../Guards/EnsureSubscriptionsIsActive';
|
||||||
|
import EnsureSubscriptionsIsInactive from '../Guards/EnsureSubscriptionsIsInactive';
|
||||||
import DashboardPage from './DashboardPage';
|
import DashboardPage from './DashboardPage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dashboard inner route content.
|
* Dashboard inner route content.
|
||||||
*/
|
*/
|
||||||
function DashboardContentRouteContent({ route }) {
|
function DashboardContentRouteContent({ route }) {
|
||||||
return (
|
const content = (
|
||||||
<DashboardPage
|
<DashboardPage
|
||||||
name={route.name}
|
name={route.name}
|
||||||
Component={route.component}
|
Component={route.component}
|
||||||
@@ -21,6 +23,21 @@ function DashboardContentRouteContent({ route }) {
|
|||||||
defaultSearchResource={route.defaultSearchResource}
|
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,19 +10,57 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Position,
|
Position,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import { FormattedMessage as T, Icon, Hint, If } from '@/components';
|
import { FormattedMessage as T } from '@/components';
|
||||||
|
|
||||||
import DashboardTopbarUser from '@/components/Dashboard/TopbarUser';
|
import DashboardTopbarUser from '@/components/Dashboard/TopbarUser';
|
||||||
import DashboardBreadcrumbs from '@/components/Dashboard/DashboardBreadcrumbs';
|
import DashboardBreadcrumbs from '@/components/Dashboard/DashboardBreadcrumbs';
|
||||||
import DashboardBackLink from '@/components/Dashboard/DashboardBackLink';
|
import DashboardBackLink from '@/components/Dashboard/DashboardBackLink';
|
||||||
|
import { Icon, Hint, If } from '@/components';
|
||||||
|
|
||||||
import withUniversalSearchActions from '@/containers/UniversalSearch/withUniversalSearchActions';
|
import withUniversalSearchActions from '@/containers/UniversalSearch/withUniversalSearchActions';
|
||||||
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
||||||
import withDashboard from '@/containers/Dashboard/withDashboard';
|
import withDashboard from '@/containers/Dashboard/withDashboard';
|
||||||
|
|
||||||
import QuickNewDropdown from '@/containers/QuickNewDropdown/QuickNewDropdown';
|
import QuickNewDropdown from '@/containers/QuickNewDropdown/QuickNewDropdown';
|
||||||
import { DashboardHamburgerButton, DashboardQuickSearchButton } from './_components';
|
|
||||||
import { compose } from '@/utils';
|
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.
|
* Dashboard topbar.
|
||||||
@@ -41,6 +79,10 @@ function DashboardTopbar({
|
|||||||
|
|
||||||
// #withGlobalSearch
|
// #withGlobalSearch
|
||||||
openGlobalSearch,
|
openGlobalSearch,
|
||||||
|
|
||||||
|
// #withSubscriptions
|
||||||
|
isSubscriptionActive,
|
||||||
|
isSubscriptionInactive,
|
||||||
}) {
|
}) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@@ -95,22 +137,28 @@ function DashboardTopbar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboard__topbar-right">
|
<div class="dashboard__topbar-right">
|
||||||
|
<If condition={isSubscriptionInactive}>
|
||||||
|
<DashboardTopbarSubscriptionMessage />
|
||||||
|
</If>
|
||||||
|
|
||||||
<Navbar class="dashboard__topbar-navbar">
|
<Navbar class="dashboard__topbar-navbar">
|
||||||
<NavbarGroup>
|
<NavbarGroup>
|
||||||
<DashboardQuickSearchButton
|
<If condition={isSubscriptionActive}>
|
||||||
onClick={() => openGlobalSearch(true)}
|
<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>
|
<QuickNewDropdown />
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
content={<T id={'notifications'} />}
|
||||||
|
position={Position.BOTTOM}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
icon={<Icon icon={'notification-24'} iconSize={20} />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</If>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
@@ -138,4 +186,31 @@ export default compose(
|
|||||||
pageHint,
|
pageHint,
|
||||||
})),
|
})),
|
||||||
withDashboardActions,
|
withDashboardActions,
|
||||||
|
withSubscriptions(
|
||||||
|
({ isSubscriptionActive, isSubscriptionInactive }) => ({
|
||||||
|
isSubscriptionActive,
|
||||||
|
isSubscriptionInactive,
|
||||||
|
}),
|
||||||
|
'main',
|
||||||
|
),
|
||||||
)(DashboardTopbar);
|
)(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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// @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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import DashboardTopbar from './DashboardTopbar';
|
|
||||||
|
|
||||||
export default DashboardTopbar;
|
|
||||||
@@ -9,21 +9,25 @@ import {
|
|||||||
Popover,
|
Popover,
|
||||||
Position,
|
Position,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import { FormattedMessage as T } from '@/components';
|
import { If, FormattedMessage as T } from '@/components';
|
||||||
|
|
||||||
|
import { firstLettersArgs } from '@/utils';
|
||||||
import { useAuthActions } from '@/hooks/state';
|
import { useAuthActions } from '@/hooks/state';
|
||||||
|
|
||||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
|
import withSubscriptions from '@/containers/Subscriptions/withSubscriptions';
|
||||||
|
|
||||||
import { useAuthenticatedAccount } from '@/hooks/query';
|
import { useAuthenticatedAccount } from '@/hooks/query';
|
||||||
import { firstLettersArgs, compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dashboard topbar user.
|
* Dashboard topbar user.
|
||||||
*/
|
*/
|
||||||
function DashboardTopbarUser({
|
function DashboardTopbarUser({
|
||||||
// #withDialogActions
|
|
||||||
openDialog,
|
openDialog,
|
||||||
|
|
||||||
|
// #withSubscriptions
|
||||||
|
isSubscriptionActive,
|
||||||
}) {
|
}) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { setLogout } = useAuthActions();
|
const { setLogout } = useAuthActions();
|
||||||
@@ -58,14 +62,16 @@ function DashboardTopbarUser({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
<MenuItem
|
<If condition={isSubscriptionActive}>
|
||||||
text={<T id={'keyboard_shortcuts'} />}
|
<MenuItem
|
||||||
onClick={onKeyboardShortcut}
|
text={<T id={'keyboard_shortcuts'} />}
|
||||||
/>
|
onClick={onKeyboardShortcut}
|
||||||
<MenuItem
|
/>
|
||||||
text={<T id={'preferences'} />}
|
<MenuItem
|
||||||
onClick={() => history.push('/preferences')}
|
text={<T id={'preferences'} />}
|
||||||
/>
|
onClick={() => history.push('/preferences')}
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
<MenuItem text={<T id={'logout'} />} onClick={onClickLogout} />
|
<MenuItem text={<T id={'logout'} />} onClick={onClickLogout} />
|
||||||
</Menu>
|
</Menu>
|
||||||
}
|
}
|
||||||
@@ -81,4 +87,8 @@ function DashboardTopbarUser({
|
|||||||
}
|
}
|
||||||
export default compose(
|
export default compose(
|
||||||
withDialogActions,
|
withDialogActions,
|
||||||
|
withSubscriptions(
|
||||||
|
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
|
||||||
|
'main',
|
||||||
|
),
|
||||||
)(DashboardTopbarUser);
|
)(DashboardTopbarUser);
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ export default function TableCell({ cell, row, index }) {
|
|||||||
[`td-${cell.column.id}`]: cell.column.id,
|
[`td-${cell.column.id}`]: cell.column.id,
|
||||||
[`td-${cellType}-type`]: !!cellType,
|
[`td-${cellType}-type`]: !!cellType,
|
||||||
}),
|
}),
|
||||||
tabindex: 0,
|
|
||||||
onClick: handleCellClick,
|
onClick: handleCellClick,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
|
|
||||||
export const getSetupWizardSteps = () => [
|
export const getSetupWizardSteps = () => [
|
||||||
|
{
|
||||||
|
label: intl.get('setup.plan.plans'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: intl.get('setup.plan.getting_started'),
|
label: intl.get('setup.plan.getting_started'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Features } from '@/constants/features';
|
|||||||
import {
|
import {
|
||||||
ISidebarMenuItemType,
|
ISidebarMenuItemType,
|
||||||
ISidebarMenuOverlayIds,
|
ISidebarMenuOverlayIds,
|
||||||
|
ISidebarSubscriptionAbility,
|
||||||
} from '@/containers/Dashboard/Sidebar/interfaces';
|
} from '@/containers/Dashboard/Sidebar/interfaces';
|
||||||
import {
|
import {
|
||||||
ReportsAction,
|
ReportsAction,
|
||||||
@@ -23,7 +24,9 @@ import {
|
|||||||
ManualJournalAction,
|
ManualJournalAction,
|
||||||
ExpenseAction,
|
ExpenseAction,
|
||||||
CashflowAction,
|
CashflowAction,
|
||||||
|
ProjectAction,
|
||||||
PreferencesAbility,
|
PreferencesAbility,
|
||||||
|
SubscriptionBillingAbility,
|
||||||
} from '@/constants/abilityOption';
|
} from '@/constants/abilityOption';
|
||||||
|
|
||||||
export const SidebarMenu = [
|
export const SidebarMenu = [
|
||||||
@@ -778,6 +781,19 @@ export const SidebarMenu = [
|
|||||||
ability: PreferencesAbility.Mutate,
|
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,7 +4,6 @@ import intl from 'react-intl-universal';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Formik, Form } from 'formik';
|
import { Formik, Form } from 'formik';
|
||||||
import { Intent } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { CLASSES } from '@/constants/classes';
|
import { CLASSES } from '@/constants/classes';
|
||||||
import { CreateCustomerForm, EditCustomerForm } from './CustomerForm.schema';
|
import { CreateCustomerForm, EditCustomerForm } from './CustomerForm.schema';
|
||||||
@@ -105,9 +104,9 @@ function CustomerFormFormik({
|
|||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<CustomerFormHeaderPrimary>
|
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
|
||||||
<CustomerFormPrimarySection />
|
<CustomerFormPrimarySection />
|
||||||
</CustomerFormHeaderPrimary>
|
</div>
|
||||||
|
|
||||||
<div className={'page-form__after-priamry-section'}>
|
<div className={'page-form__after-priamry-section'}>
|
||||||
<CustomerFormAfterPrimarySection />
|
<CustomerFormAfterPrimarySection />
|
||||||
@@ -124,12 +123,4 @@ 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);
|
export default compose(withCurrentOrganization())(CustomerFormFormik);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Scrollbar } from 'react-scrollbars-custom';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import withDashboard from '@/containers/Dashboard/withDashboard';
|
import withDashboard from '@/containers/Dashboard/withDashboard';
|
||||||
|
import withSubscriptions from '@/containers/Subscriptions/withSubscriptions';
|
||||||
|
|
||||||
import { useObserveSidebarExpendedBodyclass } from './hooks';
|
import { useObserveSidebarExpendedBodyclass } from './hooks';
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
@@ -18,6 +19,9 @@ function SidebarContainerJSX({
|
|||||||
|
|
||||||
// #withDashboard
|
// #withDashboard
|
||||||
sidebarExpended,
|
sidebarExpended,
|
||||||
|
|
||||||
|
// #withSubscription
|
||||||
|
isSubscriptionActive,
|
||||||
}) {
|
}) {
|
||||||
const sidebarScrollerRef = React.useRef();
|
const sidebarScrollerRef = React.useRef();
|
||||||
|
|
||||||
@@ -47,6 +51,7 @@ function SidebarContainerJSX({
|
|||||||
<div
|
<div
|
||||||
className={classNames('sidebar', {
|
className={classNames('sidebar', {
|
||||||
'sidebar--mini-sidebar': !sidebarExpended,
|
'sidebar--mini-sidebar': !sidebarExpended,
|
||||||
|
'is-subscription-inactive': !isSubscriptionActive,
|
||||||
})}
|
})}
|
||||||
id="sidebar"
|
id="sidebar"
|
||||||
onMouseLeave={handleSidebarMouseLeave}
|
onMouseLeave={handleSidebarMouseLeave}
|
||||||
@@ -67,4 +72,8 @@ export const SidebarContainer = compose(
|
|||||||
withDashboard(({ sidebarExpended }) => ({
|
withDashboard(({ sidebarExpended }) => ({
|
||||||
sidebarExpended,
|
sidebarExpended,
|
||||||
})),
|
})),
|
||||||
|
withSubscriptions(
|
||||||
|
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
|
||||||
|
'main',
|
||||||
|
),
|
||||||
)(SidebarContainerJSX);
|
)(SidebarContainerJSX);
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Menu } from '@blueprintjs/core';
|
import { Menu } from '@blueprintjs/core';
|
||||||
|
import * as R from 'ramda';
|
||||||
|
|
||||||
import { MenuItem, MenuItemLabel } from '@/components';
|
import { MenuItem, MenuItemLabel } from '@/components';
|
||||||
import { ISidebarMenuItemType } from '@/containers/Dashboard/Sidebar/interfaces';
|
import { ISidebarMenuItemType } from '@/containers/Dashboard/Sidebar/interfaces';
|
||||||
import { useIsSidebarMenuItemActive } from './hooks';
|
import { useIsSidebarMenuItemActive } from './hooks';
|
||||||
|
|
||||||
|
import withSubscriptions from '@/containers/Subscriptions/withSubscriptions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sidebar menu item.
|
* Sidebar menu item.
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
@@ -52,7 +55,7 @@ function SidebarMenuItemComposer({ item, index }) {
|
|||||||
* Sidebar menu.
|
* Sidebar menu.
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
*/
|
*/
|
||||||
export function SidebarMenu({ menu }) {
|
function SidebarMenuJSX({ menu }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Menu className="sidebar-menu">
|
<Menu className="sidebar-menu">
|
||||||
@@ -63,3 +66,10 @@ export function SidebarMenu({ menu }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const SidebarMenu = R.compose(
|
||||||
|
withSubscriptions(
|
||||||
|
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
|
||||||
|
'main',
|
||||||
|
),
|
||||||
|
)(SidebarMenuJSX);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
import { filterValuesDeep, deepdash } from '@/utils';
|
import { filterValuesDeep, deepdash } from '@/utils';
|
||||||
|
|
||||||
|
|
||||||
const deepDashConfig = {
|
const deepDashConfig = {
|
||||||
childrenPath: 'children',
|
childrenPath: 'children',
|
||||||
pathFormat: 'array',
|
pathFormat: 'array',
|
||||||
@@ -135,7 +136,9 @@ function useFilterSidebarMenuAbility(menu) {
|
|||||||
|
|
||||||
return deepdash.filterDeep(
|
return deepdash.filterDeep(
|
||||||
menu,
|
menu,
|
||||||
(item) => predFeature(item) && predAbility(item),
|
(item) => {
|
||||||
|
return predFeature(item) && predAbility(item) && predSubscription(item);
|
||||||
|
},
|
||||||
deepDashConfig,
|
deepDashConfig,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, { useCallback } from 'react';
|
|||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { Intent } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
|
import { omit } from 'lodash';
|
||||||
import { AppToaster } from '@/components';
|
import { AppToaster } from '@/components';
|
||||||
|
|
||||||
import AccountDialogFormContent from './AccountDialogFormContent';
|
import AccountDialogFormContent from './AccountDialogFormContent';
|
||||||
@@ -13,11 +14,7 @@ import {
|
|||||||
CreateAccountFormSchema,
|
CreateAccountFormSchema,
|
||||||
} from './AccountForm.schema';
|
} from './AccountForm.schema';
|
||||||
import { compose, transformToForm } from '@/utils';
|
import { compose, transformToForm } from '@/utils';
|
||||||
import {
|
import { transformApiErrors, transformAccountToForm } from './utils';
|
||||||
transformApiErrors,
|
|
||||||
transformAccountToForm,
|
|
||||||
transformFormToReq,
|
|
||||||
} from './utils';
|
|
||||||
|
|
||||||
import '@/style/pages/Accounts/AccountFormDialog.scss';
|
import '@/style/pages/Accounts/AccountFormDialog.scss';
|
||||||
import { useAccountDialogContext } from './AccountDialogProvider';
|
import { useAccountDialogContext } from './AccountDialogProvider';
|
||||||
@@ -29,7 +26,7 @@ const defaultInitialValues = {
|
|||||||
name: '',
|
name: '',
|
||||||
code: '',
|
code: '',
|
||||||
description: '',
|
description: '',
|
||||||
currency_code: '',
|
currency_code:'',
|
||||||
subaccount: false,
|
subaccount: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,6 +43,7 @@ function AccountFormDialogContent({
|
|||||||
createAccountMutate,
|
createAccountMutate,
|
||||||
account,
|
account,
|
||||||
|
|
||||||
|
accountId,
|
||||||
payload,
|
payload,
|
||||||
isNewMode,
|
isNewMode,
|
||||||
dialogName,
|
dialogName,
|
||||||
@@ -58,7 +56,7 @@ function AccountFormDialogContent({
|
|||||||
|
|
||||||
// Callbacks handles form submit.
|
// Callbacks handles form submit.
|
||||||
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
||||||
const form = transformFormToReq(values);
|
const form = omit(values, ['subaccount']);
|
||||||
const toastAccountName = values.code
|
const toastAccountName = values.code
|
||||||
? `${values.code} - ${values.name}`
|
? `${values.code} - ${values.name}`
|
||||||
: values.name;
|
: values.name;
|
||||||
@@ -92,8 +90,8 @@ function AccountFormDialogContent({
|
|||||||
setErrors({ ...errorsTransformed });
|
setErrors({ ...errorsTransformed });
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
};
|
};
|
||||||
if (payload.accountId) {
|
if (accountId) {
|
||||||
editAccountMutate([payload.accountId, form])
|
editAccountMutate([accountId, form])
|
||||||
.then(handleSuccess)
|
.then(handleSuccess)
|
||||||
.catch(handleError);
|
.catch(handleError);
|
||||||
} else {
|
} else {
|
||||||
@@ -115,6 +113,7 @@ function AccountFormDialogContent({
|
|||||||
defaultInitialValues,
|
defaultInitialValues,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handles dialog close.
|
// Handles dialog close.
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
closeDialog(dialogName);
|
closeDialog(dialogName);
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import { inputIntent, compose } from '@/utils';
|
|||||||
import { useAutofocus } from '@/hooks';
|
import { useAutofocus } from '@/hooks';
|
||||||
import { FOREIGN_CURRENCY_ACCOUNTS } from '@/constants/accountTypes';
|
import { FOREIGN_CURRENCY_ACCOUNTS } from '@/constants/accountTypes';
|
||||||
import { useAccountDialogContext } from './AccountDialogProvider';
|
import { useAccountDialogContext } from './AccountDialogProvider';
|
||||||
import { parentAccountShouldUpdate } from './utils';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Account form dialogs fields.
|
* Account form dialogs fields.
|
||||||
@@ -116,7 +115,12 @@ function AccountFormDialogFields({
|
|||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
inline={true}
|
inline={true}
|
||||||
label={<T id={'sub_account'} />}
|
label={
|
||||||
|
<>
|
||||||
|
<T id={'sub_account'} />
|
||||||
|
<Hint />
|
||||||
|
</>
|
||||||
|
}
|
||||||
name={'subaccount'}
|
name={'subaccount'}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@@ -124,36 +128,37 @@ function AccountFormDialogFields({
|
|||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<FastField
|
<If condition={values.subaccount}>
|
||||||
name={'parent_account_id'}
|
<FastField name={'parent_account_id'}>
|
||||||
shouldUpdate={parentAccountShouldUpdate}
|
{({
|
||||||
>
|
form: { values, setFieldValue },
|
||||||
{({
|
field: { value },
|
||||||
form: { values, setFieldValue },
|
meta: { error, touched },
|
||||||
field: { value },
|
}) => (
|
||||||
meta: { error, touched },
|
<FormGroup
|
||||||
}) => (
|
label={<T id={'parent_account'} />}
|
||||||
<FormGroup
|
className={classNames(
|
||||||
label={<T id={'parent_account'} />}
|
'form-group--parent-account',
|
||||||
className={classNames('form-group--parent-account', Classes.FILL)}
|
Classes.FILL,
|
||||||
inline={true}
|
)}
|
||||||
intent={inputIntent({ error, touched })}
|
inline={true}
|
||||||
helperText={<ErrorMessage name="parent_account_id" />}
|
intent={inputIntent({ error, touched })}
|
||||||
>
|
helperText={<ErrorMessage name="parent_account_id" />}
|
||||||
<AccountsSelectList
|
>
|
||||||
accounts={accounts}
|
<AccountsSelectList
|
||||||
onAccountSelected={(account) => {
|
accounts={accounts}
|
||||||
setFieldValue('parent_account_id', account.id);
|
onAccountSelected={(account) => {
|
||||||
}}
|
setFieldValue('parent_account_id', account.id);
|
||||||
defaultSelectText={<T id={'select_parent_account'} />}
|
}}
|
||||||
selectedAccountId={value}
|
defaultSelectText={<T id={'select_parent_account'} />}
|
||||||
popoverFill={true}
|
selectedAccountId={value}
|
||||||
filterByTypes={values.account_type}
|
popoverFill={true}
|
||||||
disabled={!values.subaccount}
|
filterByTypes={values.account_type}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
)}
|
)}
|
||||||
</FastField>
|
</FastField>
|
||||||
|
</If>
|
||||||
|
|
||||||
<If condition={FOREIGN_CURRENCY_ACCOUNTS.includes(values.account_type)}>
|
<If condition={FOREIGN_CURRENCY_ACCOUNTS.includes(values.account_type)}>
|
||||||
{/*------------ Currency -----------*/}
|
{/*------------ Currency -----------*/}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { isUndefined } from 'lodash';
|
import { isUndefined } from 'lodash';
|
||||||
import { defaultFastFieldShouldUpdate } from '@/utils';
|
|
||||||
|
|
||||||
export const AccountDialogAction = {
|
export const AccountDialogAction = {
|
||||||
Edit: 'edit',
|
Edit: 'edit',
|
||||||
@@ -34,7 +33,7 @@ export const transformApiErrors = (errors) => {
|
|||||||
/**
|
/**
|
||||||
* Payload transformer in account edit mode.
|
* Payload transformer in account edit mode.
|
||||||
*/
|
*/
|
||||||
function tranformNewChildAccountPayload(account, payload) {
|
function tranformNewChildAccountPayload(payload) {
|
||||||
return {
|
return {
|
||||||
parent_account_id: payload.parentAccountId || '',
|
parent_account_id: payload.parentAccountId || '',
|
||||||
account_type: payload.accountType || '',
|
account_type: payload.accountType || '',
|
||||||
@@ -45,7 +44,7 @@ function tranformNewChildAccountPayload(account, payload) {
|
|||||||
/**
|
/**
|
||||||
* Payload transformer in new account with defined type.
|
* Payload transformer in new account with defined type.
|
||||||
*/
|
*/
|
||||||
function transformNewDefinedTypePayload(account, payload) {
|
function transformNewDefinedTypePayload(payload) {
|
||||||
return {
|
return {
|
||||||
account_type: payload.accountType || '',
|
account_type: payload.accountType || '',
|
||||||
};
|
};
|
||||||
@@ -64,9 +63,7 @@ const mergeWithAccount = R.curry((transformed, account) => {
|
|||||||
/**
|
/**
|
||||||
* Default account payload transformer.
|
* Default account payload transformer.
|
||||||
*/
|
*/
|
||||||
const defaultPayloadTransform = (account, payload) => ({
|
const defaultPayloadTransform = () => ({});
|
||||||
subaccount: !!account.parent_account_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defined payload transformers.
|
* Defined payload transformers.
|
||||||
@@ -92,7 +89,7 @@ export const transformAccountToForm = (account, payload) => {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
condition[0] === payload.action ? R.T : R.F,
|
condition[0] === payload.action ? R.T : R.F,
|
||||||
mergeWithAccount(transformer(account, payload)),
|
mergeWithAccount(transformer(payload)),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
return R.cond(results)(account);
|
return R.cond(results)(account);
|
||||||
@@ -109,29 +106,3 @@ export const getDisabledFormFields = (account, payload) => {
|
|||||||
payload.action === AccountDialogAction.NewDefinedType,
|
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,13 +4,12 @@ import * as R from 'ramda';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { Card, DrawerLoading } from '@/components';
|
import { Card, DrawerLoading } from '@/components';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CustomerFormProvider,
|
CustomerFormProvider,
|
||||||
useCustomerFormContext,
|
useCustomerFormContext,
|
||||||
} from '@/containers/Customers/CustomerForm/CustomerFormProvider';
|
} from '@/containers/Customers/CustomerForm/CustomerFormProvider';
|
||||||
import CustomerFormFormik, {
|
import CustomerFormFormik from '@/containers/Customers/CustomerForm/CustomerFormFormik';
|
||||||
CustomerFormHeaderPrimary,
|
|
||||||
} from '@/containers/Customers/CustomerForm/CustomerFormFormik';
|
|
||||||
|
|
||||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||||
|
|
||||||
@@ -56,18 +55,12 @@ export default R.compose(withDrawerActions)(QuickCustomerFormDrawer);
|
|||||||
|
|
||||||
const CustomerFormCard = styled(Card)`
|
const CustomerFormCard = styled(Card)`
|
||||||
margin: 15px;
|
margin: 15px;
|
||||||
padding: 25px;
|
|
||||||
margin-bottom: calc(15px + 65px);
|
margin-bottom: calc(15px + 65px);
|
||||||
|
|
||||||
${CustomerFormHeaderPrimary} {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
.page-form {
|
.page-form {
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
&__floating-actions {
|
&__floating-actions {
|
||||||
margin-left: -41px;
|
margin-left: -36px;
|
||||||
margin-right: -41px;
|
margin-right: -36px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ import * as R from 'ramda';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { Card, DrawerLoading } from '@/components';
|
import { Card, DrawerLoading } from '@/components';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
VendorFormProvider,
|
VendorFormProvider,
|
||||||
useVendorFormContext,
|
useVendorFormContext,
|
||||||
} from '@/containers/Vendors/VendorForm/VendorFormProvider';
|
} from '@/containers/Vendors/VendorForm/VendorFormProvider';
|
||||||
import VendorFormFormik, {
|
import VendorFormFormik from '@/containers/Vendors/VendorForm/VendorFormFormik';
|
||||||
VendorFormHeaderPrimary,
|
|
||||||
} from '@/containers/Vendors/VendorForm/VendorFormFormik';
|
|
||||||
|
|
||||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||||
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
||||||
@@ -76,18 +75,12 @@ export default R.compose(
|
|||||||
|
|
||||||
const VendorFormCard = styled(Card)`
|
const VendorFormCard = styled(Card)`
|
||||||
margin: 15px;
|
margin: 15px;
|
||||||
padding: 25px;
|
|
||||||
margin-bottom: calc(15px + 65px);
|
margin-bottom: calc(15px + 65px);
|
||||||
|
|
||||||
${VendorFormHeaderPrimary} {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
.page-form {
|
.page-form {
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
&__floating-actions {
|
&__floating-actions {
|
||||||
margin-left: -41px;
|
margin-left: -36px;
|
||||||
margin-right: -41px;
|
margin-right: -36px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export function ItemsActionMenuList({
|
|||||||
</Can>
|
</Can>
|
||||||
<Can I={ItemAction.Create} a={AbilitySubject.Item}>
|
<Can I={ItemAction.Create} a={AbilitySubject.Item}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<Icon icon="content-copy" iconSize={16} />}
|
icon={<Icon icon="duplicate-16" />}
|
||||||
text={intl.get('duplicate')}
|
text={intl.get('duplicate')}
|
||||||
onClick={safeCallback(onDuplicate, original)}
|
onClick={safeCallback(onDuplicate, original)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ function SetupLeftSectionFooter() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'content__footer'}>
|
<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'}>
|
<div className={'content__links'}>
|
||||||
<For render={FooterLinkItem} of={footerLinks} />
|
<For render={FooterLinkItem} of={footerLinks} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ function SetupOrganizationPage({ wizard }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'setup-organization'}>
|
<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
|
<Formik
|
||||||
validationSchema={validationSchema}
|
validationSchema={validationSchema}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React from 'react';
|
|||||||
import SetupDialogs from './SetupDialogs';
|
import SetupDialogs from './SetupDialogs';
|
||||||
import SetupWizardContent from './SetupWizardContent';
|
import SetupWizardContent from './SetupWizardContent';
|
||||||
|
|
||||||
|
import withSubscriptions from '@/containers/Subscriptions/withSubscriptions';
|
||||||
import withOrganization from '@/containers/Organization/withOrganization';
|
import withOrganization from '@/containers/Organization/withOrganization';
|
||||||
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
|
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
|
||||||
import withSetupWizard from '@/store/organizations/withSetupWizard';
|
import withSetupWizard from '@/store/organizations/withSetupWizard';
|
||||||
@@ -22,6 +23,9 @@ function SetupRightSection({
|
|||||||
// #withSetupWizard
|
// #withSetupWizard
|
||||||
setupStepId,
|
setupStepId,
|
||||||
setupStepIndex,
|
setupStepIndex,
|
||||||
|
|
||||||
|
// #withSubscriptions
|
||||||
|
isSubscriptionActive,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className={'setup-page__right-section'}>
|
<section className={'setup-page__right-section'}>
|
||||||
@@ -53,6 +57,12 @@ export default compose(
|
|||||||
isOrganizationBuildRunning,
|
isOrganizationBuildRunning,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
withSubscriptions(
|
||||||
|
({ isSubscriptionActive }) => ({
|
||||||
|
isSubscriptionActive,
|
||||||
|
}),
|
||||||
|
'main',
|
||||||
|
),
|
||||||
withSetupWizard(({ setupStepId, setupStepIndex }) => ({
|
withSetupWizard(({ setupStepId, setupStepIndex }) => ({
|
||||||
setupStepId,
|
setupStepId,
|
||||||
setupStepIndex,
|
setupStepIndex,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React from 'react';
|
|||||||
import SetupSteps from './SetupSteps';
|
import SetupSteps from './SetupSteps';
|
||||||
import WizardSetupSteps from './WizardSetupSteps';
|
import WizardSetupSteps from './WizardSetupSteps';
|
||||||
|
|
||||||
|
import SetupSubscription from './SetupSubscription';
|
||||||
import SetupOrganizationPage from './SetupOrganizationPage';
|
import SetupOrganizationPage from './SetupOrganizationPage';
|
||||||
import SetupInitializingForm from './SetupInitializingForm';
|
import SetupInitializingForm from './SetupInitializingForm';
|
||||||
import SetupCongratsPage from './SetupCongratsPage';
|
import SetupCongratsPage from './SetupCongratsPage';
|
||||||
@@ -18,6 +19,7 @@ export default function SetupWizardContent({ setupStepIndex, setupStepId }) {
|
|||||||
|
|
||||||
<div class="setup-page-form">
|
<div class="setup-page-form">
|
||||||
<SetupSteps step={{ id: setupStepId }}>
|
<SetupSteps step={{ id: setupStepId }}>
|
||||||
|
<SetupSubscription id="subscription" />
|
||||||
<SetupOrganizationPage id="organization" />
|
<SetupOrganizationPage id="organization" />
|
||||||
<SetupInitializingForm id={'initializing'} />
|
<SetupInitializingForm id={'initializing'} />
|
||||||
<SetupCongratsPage id="congrats" />
|
<SetupCongratsPage id="congrats" />
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export default function WizardSetupSteps({ currentStep = 1 }) {
|
|||||||
<WizardSetupStep
|
<WizardSetupStep
|
||||||
label={step.label}
|
label={step.label}
|
||||||
isActive={index + 1 === currentStep}
|
isActive={index + 1 === currentStep}
|
||||||
key={index}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import intl from 'react-intl-universal';
|
|||||||
import { Formik, Form } from 'formik';
|
import { Formik, Form } from 'formik';
|
||||||
import { Intent } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import { CLASSES } from '@/constants/classes';
|
import { CLASSES } from '@/constants/classes';
|
||||||
import { AppToaster } from '@/components';
|
import { AppToaster } from '@/components';
|
||||||
@@ -116,9 +115,9 @@ function VendorFormFormik({
|
|||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<VendorFormHeaderPrimary>
|
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
|
||||||
<VendorFormPrimarySection />
|
<VendorFormPrimarySection />
|
||||||
</VendorFormHeaderPrimary>
|
</div>
|
||||||
|
|
||||||
<div className={'page-form__after-priamry-section'}>
|
<div className={'page-form__after-priamry-section'}>
|
||||||
<VendorFormAfterPrimarySection />
|
<VendorFormAfterPrimarySection />
|
||||||
@@ -135,12 +134,4 @@ 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);
|
export default compose(withCurrentOrganization())(VendorFormFormik);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { omit } from 'lodash';
|
|
||||||
import { useMutation, useQueryClient } from 'react-query';
|
import { useMutation, useQueryClient } from 'react-query';
|
||||||
import { batch } from 'react-redux';
|
import { batch } from 'react-redux';
|
||||||
import t from './types';
|
import t from './types';
|
||||||
import useApiRequest from '../useRequest';
|
import useApiRequest from '../useRequest';
|
||||||
import { useRequestQuery } from '../useQueryRequest';
|
import { useRequestQuery } from '../useQueryRequest';
|
||||||
import { useSetOrganizations } from '../state';
|
import { useSetOrganizations, useSetSubscriptions } from '../state';
|
||||||
|
import { omit } from 'lodash';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve organizations of the authenticated user.
|
* Retrieve organizations of the authenticated user.
|
||||||
@@ -32,6 +32,7 @@ export function useOrganizations(props) {
|
|||||||
*/
|
*/
|
||||||
export function useCurrentOrganization(props) {
|
export function useCurrentOrganization(props) {
|
||||||
const setOrganizations = useSetOrganizations();
|
const setOrganizations = useSetOrganizations();
|
||||||
|
const setSubscriptions = useSetSubscriptions();
|
||||||
|
|
||||||
return useRequestQuery(
|
return useRequestQuery(
|
||||||
[t.ORGANIZATION_CURRENT],
|
[t.ORGANIZATION_CURRENT],
|
||||||
@@ -43,6 +44,9 @@ export function useCurrentOrganization(props) {
|
|||||||
const organization = omit(data, ['subscriptions']);
|
const organization = omit(data, ['subscriptions']);
|
||||||
|
|
||||||
batch(() => {
|
batch(() => {
|
||||||
|
// Sets subscriptions.
|
||||||
|
setSubscriptions(data.subscriptions);
|
||||||
|
|
||||||
// Sets organizations.
|
// Sets organizations.
|
||||||
setOrganizations([organization]);
|
setOrganizations([organization]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1237,7 +1237,7 @@
|
|||||||
"expense.details.total": "Total",
|
"expense.details.total": "Total",
|
||||||
"manual_journal.details.subtotal": "Subtotal",
|
"manual_journal.details.subtotal": "Subtotal",
|
||||||
"manual_journal.details.total": "Total",
|
"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.description": "You have a Bigcapital account",
|
||||||
"setup.left_side.footer_help": "We’re Here to Help!",
|
"setup.left_side.footer_help": "We’re Here to Help!",
|
||||||
"setup.plan.plans": "Plans & Payment",
|
"setup.plan.plans": "Plans & Payment",
|
||||||
|
|||||||
23
packages/webapp/src/routes/register.tsx
Normal file
23
packages/webapp/src/routes/register.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// @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,10 +559,4 @@ export default {
|
|||||||
],
|
],
|
||||||
viewBox: '0 0 24 24',
|
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,15 +6,18 @@ export default (mapState) => {
|
|||||||
const {
|
const {
|
||||||
isOrganizationSetupCompleted,
|
isOrganizationSetupCompleted,
|
||||||
isOrganizationReady,
|
isOrganizationReady,
|
||||||
|
isSubscriptionActive,
|
||||||
isOrganizationBuildRunning
|
isOrganizationBuildRunning
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const condits = {
|
const condits = {
|
||||||
isCongratsStep: isOrganizationSetupCompleted,
|
isCongratsStep: isOrganizationSetupCompleted,
|
||||||
|
isSubscriptionStep: !isSubscriptionActive,
|
||||||
isInitializingStep: isOrganizationBuildRunning,
|
isInitializingStep: isOrganizationBuildRunning,
|
||||||
isOrganizationStep: !isOrganizationReady && !isOrganizationBuildRunning,
|
isOrganizationStep: !isOrganizationReady && !isOrganizationBuildRunning,
|
||||||
};
|
};
|
||||||
const scenarios = [
|
const scenarios = [
|
||||||
|
{ condition: condits.isSubscriptionStep, step: 'subscription' },
|
||||||
{ condition: condits.isOrganizationStep, step: 'organization' },
|
{ condition: condits.isOrganizationStep, step: 'organization' },
|
||||||
{ condition: condits.isInitializingStep, step: 'initializing' },
|
{ condition: condits.isInitializingStep, step: 'initializing' },
|
||||||
{ condition: condits.isCongratsStep, step: 'congrats' },
|
{ condition: condits.isCongratsStep, step: 'congrats' },
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.thead .thead-inner,
|
.thead .thead-inner,
|
||||||
.tbody .tbody-inner {
|
.tbody .tbody-inner{
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
border-bottom: 1px solid #d2dde2;
|
border-bottom: 1px solid #d2dde2;
|
||||||
|
|
||||||
> div {
|
>div {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -208,10 +208,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:focus {
|
|
||||||
outline: 1px solid rgba(0, 82, 204, 0.7);
|
|
||||||
outline-offset: -1px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tr:hover .td {
|
.tr:hover .td {
|
||||||
@@ -361,9 +357,13 @@
|
|||||||
position: sticky;
|
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 {
|
&.has-virtualized-rows {
|
||||||
|
|||||||
@@ -1,17 +1,30 @@
|
|||||||
|
|
||||||
.setup-organization {
|
.setup-organization {
|
||||||
max-width: 600px;
|
width: 580px;
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 45px 25px 20px;
|
padding: 45px 0 20px;
|
||||||
|
|
||||||
|
&__title-wrap {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #565e6c;
|
||||||
|
}
|
||||||
|
.paragraph {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
h3 {
|
h3 {
|
||||||
color: #868f9f;
|
color: #6b7382;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1.6rem;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bp3-form-group {
|
.bp3-form-group {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
|
||||||
@@ -51,10 +64,10 @@
|
|||||||
|
|
||||||
.register-org-button {
|
.register-org-button {
|
||||||
.bp3-button {
|
.bp3-button {
|
||||||
background-color: #1c2448;
|
background-color: #0052cc;
|
||||||
|
min-width: 175px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,17 +13,12 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
max-width: 600px;
|
max-width: 350px;
|
||||||
min-width: 600px;
|
min-width: 350px;
|
||||||
|
|
||||||
@media only screen and (max-width: 1200px) {
|
@media only screen and (max-width: 1200px) {
|
||||||
min-width: 500px;
|
min-width: 300px;
|
||||||
max-width: 500px;
|
max-width: 350px;
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 1024px) {
|
|
||||||
min-width: 400px;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,20 +40,17 @@
|
|||||||
|
|
||||||
&__left-section {
|
&__left-section {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background-color: #2f3d6f;
|
background-color: #01115e;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 600px;
|
width: 350px;
|
||||||
|
|
||||||
@media only screen and (max-width: 1200px) {
|
@media only screen and (max-width: 1200px) {
|
||||||
width: 500px;
|
width: 300px;
|
||||||
}
|
|
||||||
@media only screen and (max-width: 1024px) {
|
|
||||||
width: 400px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -71,29 +63,24 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&__logo {
|
&__logo {
|
||||||
opacity: 0.75;
|
opacity: 0.65;
|
||||||
margin-bottom: 60px;
|
margin-bottom: 60px;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
font-size: 50px;
|
font-size: 26px;
|
||||||
font-weight: 100;
|
font-weight: 600;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 20px;
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
color: rgba(255, 255, 255, 0.75);
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
|
||||||
@media only screen and (max-width: 1024px) {
|
|
||||||
font-size: 45px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__text {
|
&__text {
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-weight: 200;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__organization {
|
&__organization {
|
||||||
@@ -109,21 +96,31 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__divider {
|
&__divider {
|
||||||
height: 1px;
|
height: 3px;
|
||||||
width: 60%;
|
width: 100px;
|
||||||
background: rgba(255, 255, 255, 0.25);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
margin: 18px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.25);
|
}
|
||||||
padding-top: 20px;
|
|
||||||
|
&__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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__links {
|
&__links {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
opacity: 0.65;
|
opacity: 0.55;
|
||||||
|
|
||||||
>div {
|
>div {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -151,14 +148,12 @@
|
|||||||
|
|
||||||
ul {
|
ul {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 20px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
position: relative;
|
position: relative;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
width: 33%;
|
width: 25%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
Reference in New Issue
Block a user