mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 12: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 { Request, Response, NextFunction } from 'express';
|
||||
|
||||
const SupportedMethods = ['POST', 'PUT'];
|
||||
|
||||
export default (subscriptionSlug = 'main') =>
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { tenant, tenantId } = req;
|
||||
@@ -19,8 +21,10 @@ export default (subscriptionSlug = 'main') =>
|
||||
errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }],
|
||||
});
|
||||
}
|
||||
// Validate in case the subscription is inactive.
|
||||
else if (subscription.inactive()) {
|
||||
const isMethodSupported = SupportedMethods.includes(req.method);
|
||||
const isSubscriptionInactive = subscription.inactive();
|
||||
|
||||
if (isMethodSupported && isSubscriptionInactive) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }],
|
||||
});
|
||||
|
||||
@@ -146,6 +146,7 @@ export default {
|
||||
name: 'vendor.field.opening_balance_at',
|
||||
type: 'date',
|
||||
printable: false,
|
||||
accessor: 'formattedOpeningBalanceAt'
|
||||
},
|
||||
currencyCode: {
|
||||
name: 'vendor.field.currency',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import XLSX from 'xlsx';
|
||||
import bluebird from 'bluebird';
|
||||
import * as R from 'ramda';
|
||||
import { Inject, Service } from 'typedi';
|
||||
@@ -27,23 +26,7 @@ export class ImportFileCommon {
|
||||
|
||||
@Inject()
|
||||
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.
|
||||
* @param {number} tenantId -
|
||||
|
||||
@@ -2,18 +2,14 @@ import { Inject, Service } from 'typedi';
|
||||
import { chain } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
ERRORS,
|
||||
getSheetColumns,
|
||||
getUnmappedSheetColumns,
|
||||
readImportFile,
|
||||
} from './_utils';
|
||||
import { ERRORS, getUnmappedSheetColumns, readImportFile } from './_utils';
|
||||
import { ImportFileCommon } from './ImportFileCommon';
|
||||
import { ImportFileDataTransformer } from './ImportFileDataTransformer';
|
||||
import ResourceService from '../Resource/ResourceService';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { ImportFilePreviewPOJO } from './interfaces';
|
||||
import { Import } from '@/system/models';
|
||||
import { parseSheetData } from './sheet_utils';
|
||||
|
||||
@Service()
|
||||
export class ImportFileProcess {
|
||||
@@ -49,10 +45,10 @@ export class ImportFileProcess {
|
||||
if (!importFile.isMapped) {
|
||||
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 sheetData = this.importCommon.parseXlsxSheet(buffer);
|
||||
const header = getSheetColumns(sheetData);
|
||||
const [sheetData, sheetColumns] = parseSheetData(buffer);
|
||||
|
||||
const resource = importFile.resource;
|
||||
const resourceFields = this.resource.getResourceFields2(tenantId, resource);
|
||||
@@ -87,7 +83,7 @@ export class ImportFileProcess {
|
||||
.flatten()
|
||||
.value();
|
||||
|
||||
const unmappedColumns = getUnmappedSheetColumns(header, mapping);
|
||||
const unmappedColumns = getUnmappedSheetColumns(sheetColumns, mapping);
|
||||
const totalCount = allData.length;
|
||||
|
||||
const createdCount = successedImport.length;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ImportFileCommon } from './ImportFileCommon';
|
||||
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
||||
import { ImportFileUploadPOJO } from './interfaces';
|
||||
import { Import } from '@/system/models';
|
||||
import { parseSheetData } from './sheet_utils';
|
||||
|
||||
@Service()
|
||||
export class ImportFileUploadService {
|
||||
@@ -77,14 +78,12 @@ export class ImportFileUploadService {
|
||||
const buffer = await readImportFile(filename);
|
||||
|
||||
// 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.
|
||||
validateSheetEmpty(sheetData);
|
||||
|
||||
const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData);
|
||||
const coumnsStringified = JSON.stringify(sheetColumns);
|
||||
|
||||
try {
|
||||
// Validates the params Yup schema.
|
||||
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 moment from 'moment';
|
||||
import SubscriptionPeriod from '@/services/Subscription/SubscriptionPeriod';
|
||||
import { SubscriptionPaymentStatus } from '@/interfaces';
|
||||
|
||||
export default class PlanSubscription extends mixin(SystemModel) {
|
||||
public lemonSubscriptionId: number;
|
||||
@@ -13,6 +14,8 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
||||
|
||||
public trialEndsAt: Date;
|
||||
|
||||
public paymentStatus: SubscriptionPaymentStatus;
|
||||
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
@@ -31,7 +34,16 @@ export default class PlanSubscription extends mixin(SystemModel) {
|
||||
* Defined virtual attributes.
|
||||
*/
|
||||
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);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Crtiria should be active:
|
||||
* - During the trial period should NOT be canceled.
|
||||
* - Out of trial period should NOT be ended.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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);
|
||||
});
|
||||
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) {
|
||||
AppToaster.show({
|
||||
message: intl.get('global_error.authorized_user_inactive'),
|
||||
|
||||
@@ -64,12 +64,20 @@ export default function useApiRequest() {
|
||||
setGlobalErrors({ too_many_requests: true });
|
||||
}
|
||||
if (status === 400) {
|
||||
const lockedError = data.errors.find(
|
||||
(error) => error.type === 'TRANSACTIONS_DATE_LOCKED',
|
||||
);
|
||||
if (lockedError) {
|
||||
if (
|
||||
data.errors.find(
|
||||
(error) => error.type === 'TRANSACTIONS_DATE_LOCKED',
|
||||
)
|
||||
) {
|
||||
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')) {
|
||||
setGlobalErrors({ userInactive: true });
|
||||
setLogout();
|
||||
|
||||
Reference in New Issue
Block a user