Compare commits

...

7 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
a79b9caff6 Merge pull request #641 from bigcapitalhq/fix-getting-sheet-columns
fix: Getting the sheet columns in import sheet
2024-08-30 17:56:53 +02:00
Ahmed Bouhuolia
2227cead66 Merge pull request #624 from bigcapitalhq/subscription-middleware
fix: Subscription middleware
2024-08-30 17:56:02 +02:00
Ahmed Bouhuolia
410c4ea3e2 fix: Subscription active detarminer 2024-08-30 17:52:53 +02:00
Ahmed Bouhuolia
f92acbcbe0 fix: Getting the sheet columns in import sheet 2024-08-30 17:03:16 +02:00
Ahmed Bouhuolia
c986585cd9 Merge pull request #640 from bigcapitalhq/fix-typo-one-click-demo
fix: Typo one-click demo page
2024-08-30 00:15:29 +02:00
Ahmed Bouhuolia
250f0a30ef fix: Typo one-click demo page 2024-08-30 00:14:59 +02:00
Ahmed Bouhuolia
ee2d8d3065 fix: subscription middleare 2024-08-25 12:42:42 +02:00
11 changed files with 149 additions and 49 deletions

View File

@@ -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' }],
}); });

View File

@@ -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',

View File

@@ -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';
@@ -28,22 +27,6 @@ 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 -

View File

@@ -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;

View File

@@ -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);

View 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];
}

View File

@@ -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;
}
} }

View File

@@ -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);
});
} }
} }

View File

@@ -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'),

View File

@@ -73,8 +73,9 @@ export function OneClickDemoPageContent() {
)} )}
{running && ( {running && (
<Text className={style.waitingText}> <Text className={style.waitingText}>
We're preparing temporary environment for trial, It typically We're preparing the temporary environment for trial. It
take few seconds. Do not close or refresh the page. typically takes a few seconds. Do not close or refresh the
page.
</Text> </Text>
)} )}
</Stack> </Stack>

View File

@@ -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();