mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a79b9caff6 | ||
|
|
2227cead66 | ||
|
|
410c4ea3e2 | ||
|
|
f92acbcbe0 | ||
|
|
ee2d8d3065 |
@@ -1,6 +1,8 @@
|
|||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
const SupportedMethods = ['POST', 'PUT'];
|
||||||
|
|
||||||
export default (subscriptionSlug = 'main') =>
|
export default (subscriptionSlug = 'main') =>
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const { tenant, tenantId } = req;
|
const { tenant, tenantId } = req;
|
||||||
@@ -19,8 +21,10 @@ export default (subscriptionSlug = 'main') =>
|
|||||||
errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }],
|
errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Validate in case the subscription is inactive.
|
const isMethodSupported = SupportedMethods.includes(req.method);
|
||||||
else if (subscription.inactive()) {
|
const isSubscriptionInactive = subscription.inactive();
|
||||||
|
|
||||||
|
if (isMethodSupported && isSubscriptionInactive) {
|
||||||
return res.boom.badRequest(null, {
|
return res.boom.badRequest(null, {
|
||||||
errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }],
|
errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ export default {
|
|||||||
name: 'vendor.field.opening_balance_at',
|
name: 'vendor.field.opening_balance_at',
|
||||||
type: 'date',
|
type: 'date',
|
||||||
printable: false,
|
printable: false,
|
||||||
|
accessor: 'formattedOpeningBalanceAt'
|
||||||
},
|
},
|
||||||
currencyCode: {
|
currencyCode: {
|
||||||
name: 'vendor.field.currency',
|
name: 'vendor.field.currency',
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import XLSX from 'xlsx';
|
|
||||||
import bluebird from 'bluebird';
|
import bluebird from 'bluebird';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
@@ -27,23 +26,7 @@ export class ImportFileCommon {
|
|||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private resource: ResourceService;
|
private resource: ResourceService;
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps the columns of the imported data based on the provided mapping attributes.
|
|
||||||
* @param {Record<string, any>[]} body - The array of data objects to map.
|
|
||||||
* @param {ImportMappingAttr[]} map - The mapping attributes.
|
|
||||||
* @returns {Record<string, any>[]} - The mapped data objects.
|
|
||||||
*/
|
|
||||||
public parseXlsxSheet(buffer: Buffer): Record<string, unknown>[] {
|
|
||||||
const workbook = XLSX.read(buffer, { type: 'buffer', raw: true });
|
|
||||||
|
|
||||||
const firstSheetName = workbook.SheetNames[0];
|
|
||||||
const worksheet = workbook.Sheets[firstSheetName];
|
|
||||||
|
|
||||||
return XLSX.utils.sheet_to_json(worksheet, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imports the given parsed data to the resource storage through registered importable service.
|
* Imports the given parsed data to the resource storage through registered importable service.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
|
|||||||
@@ -2,18 +2,14 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { chain } from 'lodash';
|
import { chain } from 'lodash';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import {
|
import { ERRORS, getUnmappedSheetColumns, readImportFile } from './_utils';
|
||||||
ERRORS,
|
|
||||||
getSheetColumns,
|
|
||||||
getUnmappedSheetColumns,
|
|
||||||
readImportFile,
|
|
||||||
} from './_utils';
|
|
||||||
import { ImportFileCommon } from './ImportFileCommon';
|
import { ImportFileCommon } from './ImportFileCommon';
|
||||||
import { ImportFileDataTransformer } from './ImportFileDataTransformer';
|
import { ImportFileDataTransformer } from './ImportFileDataTransformer';
|
||||||
import ResourceService from '../Resource/ResourceService';
|
import ResourceService from '../Resource/ResourceService';
|
||||||
import UnitOfWork from '../UnitOfWork';
|
import UnitOfWork from '../UnitOfWork';
|
||||||
import { ImportFilePreviewPOJO } from './interfaces';
|
import { ImportFilePreviewPOJO } from './interfaces';
|
||||||
import { Import } from '@/system/models';
|
import { Import } from '@/system/models';
|
||||||
|
import { parseSheetData } from './sheet_utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFileProcess {
|
export class ImportFileProcess {
|
||||||
@@ -49,10 +45,10 @@ export class ImportFileProcess {
|
|||||||
if (!importFile.isMapped) {
|
if (!importFile.isMapped) {
|
||||||
throw new ServiceError(ERRORS.IMPORT_FILE_NOT_MAPPED);
|
throw new ServiceError(ERRORS.IMPORT_FILE_NOT_MAPPED);
|
||||||
}
|
}
|
||||||
// Read the imported file.
|
// Read the imported file and parse the given buffer to get columns
|
||||||
|
// and sheet data in json format.
|
||||||
const buffer = await readImportFile(importFile.filename);
|
const buffer = await readImportFile(importFile.filename);
|
||||||
const sheetData = this.importCommon.parseXlsxSheet(buffer);
|
const [sheetData, sheetColumns] = parseSheetData(buffer);
|
||||||
const header = getSheetColumns(sheetData);
|
|
||||||
|
|
||||||
const resource = importFile.resource;
|
const resource = importFile.resource;
|
||||||
const resourceFields = this.resource.getResourceFields2(tenantId, resource);
|
const resourceFields = this.resource.getResourceFields2(tenantId, resource);
|
||||||
@@ -87,7 +83,7 @@ export class ImportFileProcess {
|
|||||||
.flatten()
|
.flatten()
|
||||||
.value();
|
.value();
|
||||||
|
|
||||||
const unmappedColumns = getUnmappedSheetColumns(header, mapping);
|
const unmappedColumns = getUnmappedSheetColumns(sheetColumns, mapping);
|
||||||
const totalCount = allData.length;
|
const totalCount = allData.length;
|
||||||
|
|
||||||
const createdCount = successedImport.length;
|
const createdCount = successedImport.length;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { ImportFileCommon } from './ImportFileCommon';
|
|||||||
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
||||||
import { ImportFileUploadPOJO } from './interfaces';
|
import { ImportFileUploadPOJO } from './interfaces';
|
||||||
import { Import } from '@/system/models';
|
import { Import } from '@/system/models';
|
||||||
|
import { parseSheetData } from './sheet_utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFileUploadService {
|
export class ImportFileUploadService {
|
||||||
@@ -77,14 +78,12 @@ export class ImportFileUploadService {
|
|||||||
const buffer = await readImportFile(filename);
|
const buffer = await readImportFile(filename);
|
||||||
|
|
||||||
// Parse the buffer file to array data.
|
// Parse the buffer file to array data.
|
||||||
const sheetData = this.importFileCommon.parseXlsxSheet(buffer);
|
const [sheetData, sheetColumns] = parseSheetData(buffer);
|
||||||
|
const coumnsStringified = JSON.stringify(sheetColumns);
|
||||||
|
|
||||||
// Throws service error if the sheet data is empty.
|
// Throws service error if the sheet data is empty.
|
||||||
validateSheetEmpty(sheetData);
|
validateSheetEmpty(sheetData);
|
||||||
|
|
||||||
const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData);
|
|
||||||
const coumnsStringified = JSON.stringify(sheetColumns);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validates the params Yup schema.
|
// Validates the params Yup schema.
|
||||||
await this.importFileCommon.validateParamsSchema(resource, params);
|
await this.importFileCommon.validateParamsSchema(resource, params);
|
||||||
|
|||||||
56
packages/server/src/services/Import/sheet_utils.ts
Normal file
56
packages/server/src/services/Import/sheet_utils.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import XLSX from 'xlsx';
|
||||||
|
import { first } from 'lodash';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the given sheet buffer to worksheet.
|
||||||
|
* @param {Buffer} buffer
|
||||||
|
* @returns {XLSX.WorkSheet}
|
||||||
|
*/
|
||||||
|
export function parseFirstSheet(buffer: Buffer): XLSX.WorkSheet {
|
||||||
|
const workbook = XLSX.read(buffer, { type: 'buffer', raw: true });
|
||||||
|
|
||||||
|
const firstSheetName = workbook.SheetNames[0];
|
||||||
|
const worksheet = workbook.Sheets[firstSheetName];
|
||||||
|
|
||||||
|
return worksheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the given worksheet to columns.
|
||||||
|
* @param {XLSX.WorkSheet} worksheet
|
||||||
|
* @returns {Array<string>}
|
||||||
|
*/
|
||||||
|
export function extractSheetColumns(worksheet: XLSX.WorkSheet): Array<string> {
|
||||||
|
// By default, sheet_to_json scans the first row and uses the values as headers.
|
||||||
|
// With the header: 1 option, the function exports an array of arrays of values.
|
||||||
|
const sheetCells = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
||||||
|
const sheetCols = first(sheetCells) as Array<string>;
|
||||||
|
|
||||||
|
return sheetCols.filter((col) => col);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the given worksheet to json values. the keys are columns labels.
|
||||||
|
* @param {XLSX.WorkSheet} worksheet
|
||||||
|
* @returns {Array<Record<string, string>>}
|
||||||
|
*/
|
||||||
|
export function parseSheetToJson(
|
||||||
|
worksheet: XLSX.WorkSheet
|
||||||
|
): Array<Record<string, string>> {
|
||||||
|
return XLSX.utils.sheet_to_json(worksheet, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the given sheet buffer then retrieves the sheet data and columns.
|
||||||
|
* @param {Buffer} buffer
|
||||||
|
*/
|
||||||
|
export function parseSheetData(
|
||||||
|
buffer: Buffer
|
||||||
|
): [Array<Record<string, string>>, string[]] {
|
||||||
|
const worksheet = parseFirstSheet(buffer);
|
||||||
|
|
||||||
|
const columns = extractSheetColumns(worksheet);
|
||||||
|
const data = parseSheetToJson(worksheet);
|
||||||
|
|
||||||
|
return [data, columns];
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Model, mixin } from 'objection';
|
|||||||
import SystemModel from '@/system/models/SystemModel';
|
import SystemModel from '@/system/models/SystemModel';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import SubscriptionPeriod from '@/services/Subscription/SubscriptionPeriod';
|
import SubscriptionPeriod from '@/services/Subscription/SubscriptionPeriod';
|
||||||
|
import { SubscriptionPaymentStatus } from '@/interfaces';
|
||||||
|
|
||||||
export default class PlanSubscription extends mixin(SystemModel) {
|
export default class PlanSubscription extends mixin(SystemModel) {
|
||||||
public lemonSubscriptionId: number;
|
public lemonSubscriptionId: number;
|
||||||
@@ -13,6 +14,8 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
|||||||
|
|
||||||
public trialEndsAt: Date;
|
public trialEndsAt: Date;
|
||||||
|
|
||||||
|
public paymentStatus: SubscriptionPaymentStatus;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name.
|
* Table name.
|
||||||
*/
|
*/
|
||||||
@@ -31,7 +34,16 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
|||||||
* Defined virtual attributes.
|
* Defined virtual attributes.
|
||||||
*/
|
*/
|
||||||
static get virtualAttributes() {
|
static get virtualAttributes() {
|
||||||
return ['active', 'inactive', 'ended', 'canceled', 'onTrial', 'status'];
|
return [
|
||||||
|
'active',
|
||||||
|
'inactive',
|
||||||
|
'ended',
|
||||||
|
'canceled',
|
||||||
|
'onTrial',
|
||||||
|
'status',
|
||||||
|
'isPaymentFailed',
|
||||||
|
'isPaymentSucceed',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,6 +81,22 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
|||||||
|
|
||||||
builder.where('trial_ends_at', '<=', endDate);
|
builder.where('trial_ends_at', '<=', endDate);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the failed payment.
|
||||||
|
* @param builder
|
||||||
|
*/
|
||||||
|
failedPayment(builder) {
|
||||||
|
builder.where('payment_status', SubscriptionPaymentStatus.Failed);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the succeed payment.
|
||||||
|
* @param builder
|
||||||
|
*/
|
||||||
|
succeedPayment(builder) {
|
||||||
|
builder.where('payment_status', SubscriptionPaymentStatus.Succeed);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,10 +136,13 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the subscription is active.
|
* Check if the subscription is active.
|
||||||
|
* Crtiria should be active:
|
||||||
|
* - During the trial period should NOT be canceled.
|
||||||
|
* - Out of trial period should NOT be ended.
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
public active() {
|
public active() {
|
||||||
return this.onTrial() || !this.ended();
|
return this.onTrial() ? !this.canceled() : !this.ended();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -200,4 +231,20 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
|||||||
);
|
);
|
||||||
return this.$query().update({ startsAt, endsAt });
|
return this.$query().update({ startsAt, endsAt });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines the subscription payment whether is failed.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public isPaymentFailed() {
|
||||||
|
return this.paymentStatus === SubscriptionPaymentStatus.Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines the subscription payment whether is succeed.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public isPaymentSucceed() {
|
||||||
|
return this.paymentStatus === SubscriptionPaymentStatus.Succeed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,8 @@ export default class SubscriptionRepository extends SystemRepository {
|
|||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
*/
|
*/
|
||||||
getBySlugInTenant(slug: string, tenantId: number) {
|
getBySlugInTenant(slug: string, tenantId: number) {
|
||||||
const cacheKey = this.getCacheKey('getBySlugInTenant', slug, tenantId);
|
return PlanSubscription.query()
|
||||||
|
.findOne('slug', slug)
|
||||||
return this.cache.get(cacheKey, () => {
|
.where('tenant_id', tenantId);
|
||||||
return PlanSubscription.query()
|
|
||||||
.findOne('slug', slug)
|
|
||||||
.where('tenant_id', tenantId);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,15 @@ function GlobalErrors({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (globalErrors.subscriptionInactive) {
|
||||||
|
AppToaster.show({
|
||||||
|
message: `You can't add new data to Bigcapital because your subscription is inactive. Make sure your billing information is up-to-date from Preferences > Billing page.`,
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
onDismiss: () => {
|
||||||
|
globalErrorsSet({ subscriptionInactive: false });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
if (globalErrors.userInactive) {
|
if (globalErrors.userInactive) {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
message: intl.get('global_error.authorized_user_inactive'),
|
message: intl.get('global_error.authorized_user_inactive'),
|
||||||
|
|||||||
@@ -64,12 +64,20 @@ export default function useApiRequest() {
|
|||||||
setGlobalErrors({ too_many_requests: true });
|
setGlobalErrors({ too_many_requests: true });
|
||||||
}
|
}
|
||||||
if (status === 400) {
|
if (status === 400) {
|
||||||
const lockedError = data.errors.find(
|
if (
|
||||||
(error) => error.type === 'TRANSACTIONS_DATE_LOCKED',
|
data.errors.find(
|
||||||
);
|
(error) => error.type === 'TRANSACTIONS_DATE_LOCKED',
|
||||||
if (lockedError) {
|
)
|
||||||
|
) {
|
||||||
setGlobalErrors({ transactionsLocked: { ...lockedError.data } });
|
setGlobalErrors({ transactionsLocked: { ...lockedError.data } });
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
data.errors.find(
|
||||||
|
(e) => e.type === 'ORGANIZATION.SUBSCRIPTION.INACTIVE',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setGlobalErrors({ subscriptionInactive: true });
|
||||||
|
}
|
||||||
if (data.errors.find((e) => e.type === 'USER_INACTIVE')) {
|
if (data.errors.find((e) => e.type === 'USER_INACTIVE')) {
|
||||||
setGlobalErrors({ userInactive: true });
|
setGlobalErrors({ userInactive: true });
|
||||||
setLogout();
|
setLogout();
|
||||||
|
|||||||
Reference in New Issue
Block a user