Compare commits

..

2 Commits

Author SHA1 Message Date
allcontributors[bot]
55ba66205c docs: update .all-contributorsrc [skip ci] 2024-08-14 14:20:36 +00:00
allcontributors[bot]
291bf80135 docs: update README.md [skip ci] 2024-08-14 14:20:35 +00:00
210 changed files with 1055 additions and 3822 deletions

View File

@@ -2,27 +2,6 @@
All notable changes to Bigcapital server-side will be in this file.
## [0.19.4] - 18-08-2024
* fix: Allow multi-lines to statements transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/594
* feat: Add amount comparators to amount bank rule field by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/595
* fix: Transaction type and description do not show in general ledger. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/596
* fix: Refresh accounts and account transactions. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/597
* fix: Typo payments made by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/598
* fix: Typo categories list by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/600
* fix: Autofill the quick created customer/vendor by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/601
* fix: Remove views tabs from receipts list by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/602
* fix: Typo payment receive messages by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/599
* fix: Enhance Dropzone visual of accept and reject modes by @Champetaman in https://github.com/bigcapitalhq/bigcapital/pull/603
* fix: Matching bank transactions should create associate payment transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/606
* fix: Change Dropzone title and subtitle by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/607
* fix: Inconsistance page size of paginated data tables by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/604
* fix: Database connection lost error by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/611
* fix: Language typos by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/613
* Fix: Correctly display Date, Published At, and Created At in ExpenseDrawerHeader by @Champetaman in https://github.com/bigcapitalhq/bigcapital/pull/612
* fix: Delete bank account with uncategorized transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/614
* feat: activate/inactivate account from drawer details by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/615
## [v0.18.0] - 10-08-2024
* feat: Bank rules for automated categorization by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511

View File

@@ -37,7 +37,6 @@
"agendash": "^3.1.0",
"app-root-path": "^3.0.0",
"async": "^3.2.0",
"async-mutex": "^0.5.0",
"axios": "^1.6.0",
"babel-loader": "^9.1.2",
"bcryptjs": "^2.4.3",
@@ -70,8 +69,9 @@
"is-my-json-valid": "^2.20.5",
"js-money": "^0.6.3",
"jsonwebtoken": "^8.5.1",
"knex": "^3.1.0",
"knex": "^0.95.15",
"knex-cleaner": "^1.3.0",
"knex-db-manager": "^0.6.1",
"libphonenumber-js": "^1.9.6",
"lodash": "^4.17.15",
"lru-cache": "^6.0.0",

View File

@@ -46,10 +46,6 @@ export class ExcludeBankTransactionsController extends BaseController {
query('account_id').optional().isNumeric().toInt(),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
query('max_amount').optional({ nullable: true }).isFloat().toFloat(),
],
this.validationResult,
this.getExcludedBankTransactions.bind(this)

View File

@@ -21,10 +21,6 @@ export class RecognizedTransactionsController extends BaseController {
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('account_id').optional().isNumeric().toInt(),
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
query('max_amount').optional({ nullable: true }).isFloat().isFloat(),
],
this.validationResult,
this.getRecognizedTransactions.bind(this)

View File

@@ -84,10 +84,6 @@ export default class NewCashflowTransactionController extends BaseController {
param('id').exists().isNumeric().toInt(),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
query('max_amount').optional({ nullable: true }).isFloat().toFloat(),
];
}

View File

@@ -1,87 +0,0 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Service, Inject } from 'typedi';
import { body } from 'express-validator';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController';
import { OneClickDemoApplication } from '@/services/OneClickDemo/OneClickDemoApplication';
import config from '@/config';
@Service()
export class OneClickDemoController extends BaseController {
@Inject()
private oneClickDemoApp: OneClickDemoApplication;
/**
* Router constructor method.
*/
router() {
const router = Router();
// Protects the endpoints if the feature is not enabled.
const protectMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
// Add your protection logic here
if (config.oneClickDemoAccounts) {
next();
} else {
res.status(403).send({ message: 'Forbidden' });
}
};
router.post(
'/one_click',
protectMiddleware,
asyncMiddleware(this.oneClickDemo.bind(this))
);
router.post(
'/one_click_signin',
[body('demo_id').exists()],
this.validationResult,
protectMiddleware,
asyncMiddleware(this.oneClickSignIn.bind(this))
);
return router;
}
/**
* One-click demo application.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
private async oneClickDemo(req: Request, res: Response, next: NextFunction) {
try {
const data = await this.oneClickDemoApp.createOneClick();
return res.status(200).send({
data,
message: 'The one-click demo has been created successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Sign-in to one-click demo account.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async oneClickSignIn(
req: Request,
res: Response,
next: NextFunction
) {
const { demoId } = this.matchedBodyData(req);
try {
const data = await this.oneClickDemoApp.autoSignIn(demoId);
return res.status(200).send(data);
} catch (error) {
next(error);
}
}
}

View File

@@ -113,7 +113,7 @@ export class SubscriptionController extends BaseController {
const { tenantId } = req;
try {
await this.subscriptionApp.cancelSubscription(tenantId);
await this.subscriptionApp.cancelSubscription(tenantId, '455610');
return res.status(200).send({
status: 200,

View File

@@ -35,7 +35,7 @@ export class Webhooks extends BaseController {
*/
public async lemonWebhooks(req: Request, res: Response, next: NextFunction) {
const data = req.body;
const signature = req.headers['x-signature'] as string ?? '';
const signature = req.headers['x-signature'] ?? '';
const rawBody = req.rawBody;
try {

View File

@@ -63,7 +63,6 @@ import { BankingController } from './controllers/Banking/BankingController';
import { Webhooks } from './controllers/Webhooks/Webhooks';
import { ExportController } from './controllers/Export/ExportController';
import { AttachmentsController } from './controllers/Attachments/AttachmentsController';
import { OneClickDemoController } from './controllers/OneClickDemo/OneClickDemoController';
export default () => {
const app = Router();
@@ -81,7 +80,6 @@ export default () => {
app.use('/jobs', Container.get(Jobs).router());
app.use('/account', Container.get(Account).router());
app.use('/webhooks', Container.get(Webhooks).router());
app.use('/demo', Container.get(OneClickDemoController).router())
// - Dashboard routes.
// ---------------------------

View File

@@ -1,21 +1,11 @@
import { Container } from 'typedi';
import { Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import SettingsStore from '@/services/Settings/SettingsStore';
export default async (req: Request, res: Response, next: NextFunction) => {
const { tenantId } = req.user;
const settings = await initializeTenantSettings(tenantId);
req.settings = settings;
res.on('finish', async () => {
await settings.save();
});
next();
}
export const initializeTenantSettings = async (tenantId: number) => {
const Logger = Container.get('logger');
const tenantContainer = Container.of(`tenant-${tenantId}`);
if (tenantContainer && !tenantContainer.has('settings')) {
@@ -28,5 +18,10 @@ export const initializeTenantSettings = async (tenantId: number) => {
await settings.load();
return settings;
req.settings = settings;
res.on('finish', async () => {
await settings.save();
});
next();
}

View File

@@ -4,7 +4,6 @@ import { Request } from 'express';
import TenancyService from '@/services/Tenancy/TenancyService';
import TenantsManagerService from '@/services/Tenancy/TenantsManager';
import rtlDetect from 'rtl-detect';
import { Tenant } from '@/system/models';
export default (req: Request, tenant: ITenant) => {
const { id: tenantId, organizationId } = tenant;
@@ -17,7 +16,7 @@ export default (req: Request, tenant: ITenant) => {
const tenantContainer = tenantServices.tenantContainer(tenantId);
tenantContainer.set('i18n', injectI18nUtils());
tenantContainer.set('i18n', injectI18nUtils(req));
const knexInstance = tenantServices.knex(tenantId);
const models = tenantServices.models(tenantId);
@@ -34,35 +33,14 @@ export default (req: Request, tenant: ITenant) => {
};
export const injectI18nUtils = (req) => {
const globalI18n = Container.get('i18n');
const locale = globalI18n.getLocale();
const locale = req.getLocale();
const direction = rtlDetect.getLangDir(locale);
return {
locale,
__: globalI18n.__,
__: req.__,
direction,
isRtl: direction === 'rtl',
isLtr: direction === 'ltr',
};
};
export const initalizeTenantServices = async (tenantId: number) => {
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
const tenantServices = Container.get(TenancyService);
const tenantsManager = Container.get(TenantsManagerService);
// Initialize the knex instance.
tenantsManager.setupKnexInstance(tenant);
const tenantContainer = tenantServices.tenantContainer(tenantId);
tenantContainer.set('i18n', injectI18nUtils());
tenantServices.knex(tenantId);
tenantServices.models(tenantId);
tenantServices.repositories(tenantId);
tenantServices.cache(tenantId);
};

View File

@@ -32,7 +32,7 @@ module.exports = {
*/
tenant: {
db_client: process.env.TENANT_DB_CLIENT || process.env.DB_CLIENT || 'mysql',
db_name_prefix: process.env.TENANT_DB_NAME_PERFIX || 'bigcapital_tenant_',
db_name_prefix: process.env.TENANT_DB_NAME_PERFIX,
db_host: process.env.TENANT_DB_HOST || process.env.DB_HOST,
db_user: process.env.TENANT_DB_USER || process.env.DB_USER,
db_password: process.env.TENANT_DB_PASSWORD || process.env.DB_PASSWORD,
@@ -245,12 +245,4 @@ module.exports = {
loops: {
apiKey: process.env.LOOPS_API_KEY,
},
/**
* One-click demo accounts.
*/
oneClickDemoAccounts: {
enable: parseBoolean(process.env.ONE_CLICK_DEMO_ACCOUNTS, false),
demoUrl: process.env.ONE_CLICK_DEMO_ACCOUNTS_URL || '',
},
};

View File

@@ -15,7 +15,6 @@ export default class SeedSettings extends TenantSeeder {
// Manual journals settings.
{ group: 'manual_journals', key: 'next_number', value: '00001' },
{ group: 'manual_journals', key: 'number_prefix', value: 'J-' },
{ group: 'manual_journals', key: 'auto_increment', value: true },
// Sale invoices settings.

View File

@@ -7,6 +7,7 @@ export interface IRegisterDTO {
lastName: string;
email: string;
password: string;
organizationName: string;
}
export interface ILoginDTO {
@@ -76,10 +77,6 @@ export interface IAuthSendedResetPassword {
export interface IAuthGetMetaPOJO {
signupDisabled: boolean;
oneClickDemo: {
enable: boolean;
demoUrl: string;
};
}
export interface IAuthSignUpVerifingEventPayload {

View File

@@ -167,18 +167,11 @@ export interface CategorizeTransactionAsExpenseDTO {
export interface IGetUncategorizedTransactionsQuery {
page?: number;
pageSize?: number;
minDate?: Date;
maxDate?: Date;
minAmount?: number;
maxAmount?: number;
}
export interface IGetRecognizedTransactionsQuery {
page?: number;
pageSize?: number;
accountId?: number;
minDate?: Date;
maxDate?: Date;
minAmount?: number;
maxAmount?: number;
}
}

View File

@@ -3,6 +3,6 @@ import { ImportFilePreviewPOJO } from "@/services/Import/interfaces";
export interface IImportFileCommitedEventPayload {
tenantId: number;
importId: string;
importId: number;
meta: ImportFilePreviewPOJO;
}

View File

@@ -67,7 +67,7 @@ export interface IPaymentReceivedEntry {
export interface IPaymentReceivedEntryDTO {
id?: number;
index: number;
paymentReceiveId?: number;
paymentReceiveId: number;
invoiceId: number;
paymentAmount: number;
}

View File

@@ -33,7 +33,3 @@ export interface IOrganizationBuildEventPayload {
buildDTO: IOrganizationBuildDTO;
systemUser: ISystemUser;
}
export interface IOrganizationBuiltEventPayload {
tenantId: number;
}

View File

@@ -1,8 +0,0 @@
export interface SubscriptionPayload {
lemonSqueezyId?: string;
}
export enum SubscriptionPaymentStatus {
Succeed = 'succeed',
Failed = 'failed',
}

View File

@@ -51,4 +51,5 @@ export interface ISystemService {
cache();
repositories();
knex();
dbManager();
}

View File

@@ -75,7 +75,6 @@ export * from './Times';
export * from './ProjectProfitabilitySummary';
export * from './TaxRate';
export * from './Plaid';
export * from './Subscription';
export interface I18nService {
__: (input: string) => string;

View File

@@ -21,7 +21,7 @@ export default class ComputeItemCostJob {
agenda.define(
'compute-item-cost',
{ priority: 'high', concurrency: 20 },
{ priority: 'high', concurrency: 1 },
this.handler.bind(this)
);
this.agenda.on('start:compute-item-cost', this.onJobStart.bind(this));

View File

@@ -8,7 +8,7 @@ export default class OrganizationSetupJob {
constructor(agenda) {
agenda.define(
'organization-setup',
{ priority: 'high', concurrency: 20 },
{ priority: 'high', concurrency: 1 },
this.handler
);
}

View File

@@ -15,7 +15,7 @@ export default class WriteInvoicesJournalEntries {
agenda.define(
eventName,
{ priority: 'normal', concurrency: 20 },
{ priority: 'normal', concurrency: 1 },
this.handler.bind(this)
);
agenda.on(`complete:${eventName}`, this.onJobCompleted.bind(this));

View File

@@ -2,7 +2,6 @@ import moment from 'moment';
import * as R from 'ramda';
import { includes, isFunction, isObject, isUndefined, omit } from 'lodash';
import { formatNumber, sortObjectKeysAlphabetically } from 'utils';
import { EXPORT_DTE_FORMAT } from '@/services/Export/constants';
export class Transformer {
public context: any;
@@ -156,35 +155,19 @@ export class Transformer {
this.dateFormat = format;
}
/**
* Format date.
* @param {} date
* @param {string} format -
* @returns {}
*/
protected formatDate(date, format?: string) {
// Use the export date format if the async operation is in exporting,
// otherwise use the given or default format.
const _format = this.context.exportAls.isExport
? EXPORT_DTE_FORMAT
: format || this.dateFormat;
return date ? moment(date).format(_format) : '';
}
/**
*
* @param date
* @returns {}
* @returns
*/
protected formatDateFromNow(date) {
return date ? moment(date).fromNow(true) : '';
protected formatDate(date) {
return date ? moment(date).format(this.dateFormat) : '';
}
/**
*
* @param number
* @returns {}
* @returns
*/
protected formatNumber(number, props?) {
return formatNumber(number, { money: false, ...props });
@@ -194,7 +177,7 @@ export class Transformer {
*
* @param money
* @param options
* @returns {}
* @returns
*/
protected formatMoney(money, options?) {
return formatNumber(money, {

View File

@@ -3,17 +3,12 @@ import { isNull } from 'lodash';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TenantMetadata } from '@/system/models';
import { Transformer } from './Transformer';
import { ImportAls } from '@/services/Import/ImportALS';
import { ExportAls } from '@/services/Export/ExportAls';
@Service()
export class TransformerInjectable {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private exportAls: ExportAls;
/**
* Retrieves the application context of all tenant transformers.
* @param {number} tenantId
@@ -22,12 +17,10 @@ export class TransformerInjectable {
async getApplicationContext(tenantId: number) {
const i18n = this.tenancy.i18n(tenantId);
const organization = await TenantMetadata.query().findOne({ tenantId });
const exportAls = this.exportAls;
return {
organization,
i18n,
exportAls,
};
}

View File

@@ -0,0 +1,7 @@
import knexManager from 'knex-db-manager';
import { systemKnexConfig, systemDbManager } from 'config/knexConfig';
export default () => knexManager.databaseManagerFactory({
knex: systemKnexConfig,
dbManager: systemDbManager,
});

View File

@@ -3,6 +3,7 @@ import LoggerInstance from '@/loaders/logger';
import agendaFactory from '@/loaders/agenda';
import SmsClientLoader from '@/loaders/smsClient';
import mailInstance from '@/loaders/mail';
import dbManagerFactory from '@/loaders/dbManager';
import i18n from '@/loaders/i18n';
import repositoriesLoader from '@/loaders/systemRepositories';
import Cache from '@/services/Cache';
@@ -15,6 +16,7 @@ export default ({ mongoConnection, knex }) => {
try {
const agendaInstance = agendaFactory({ mongoConnection });
const smsClientInstance = SmsClientLoader(config.easySMSGateway.api_key);
const dbManager = dbManagerFactory(knex);
const cacheInstance = new Cache();
Container.set('logger', LoggerInstance);
@@ -22,6 +24,7 @@ export default ({ mongoConnection, knex }) => {
Container.set('SMSClient', smsClientInstance);
Container.set('mail', mailInstance);
Container.set('dbManager', dbManager);
LoggerInstance.info(
'[DI] Database manager has been injected into container.'
);

View File

@@ -115,9 +115,6 @@ import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/E
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
import { DisconnectPlaidItemOnAccountDeleted } from '@/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted';
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
import { DeleteUncategorizedTransactionsOnAccountDeleting } from '@/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting';
import { SeedInitialDemoAccountDataOnOrgBuild } from '@/services/OneClickDemo/events/SeedInitialDemoAccountData';
import { TriggerInvalidateCacheOnSubscriptionChange } from '@/services/Subscription/events/TriggerInvalidateCacheOnSubscriptionChange';
export default () => {
return new EventPublisher();
@@ -248,10 +245,8 @@ export const susbcribers = () => {
DeleteCashflowTransactionOnUncategorize,
PreventDeleteTransactionOnDelete,
// Subscription
SubscribeFreeOnSignupCommunity,
SendVerfiyMailOnSignUp,
TriggerInvalidateCacheOnSubscriptionChange,
// Attachments
AttachmentsOnSaleInvoiceCreated,
@@ -282,12 +277,8 @@ export const susbcribers = () => {
// Plaid
RecognizeSyncedBankTranasctions,
DisconnectPlaidItemOnAccountDeleted,
DeleteUncategorizedTransactionsOnAccountDeleting,
// Loops
LoopsEventsSubscriber,
// Demo Account
SeedInitialDemoAccountDataOnOrgBuild,
LoopsEventsSubscriber
];
};

View File

@@ -2,7 +2,6 @@ import { I18n } from 'i18n';
export default () => new I18n({
locales: ['en', 'ar'],
defaultLocale: 'en',
register: global,
directory: global.__locales_dir,
updateFiles: false,

View File

@@ -95,11 +95,6 @@ export default {
},
},
columns: {
customerType: {
name: 'Customer Type',
type: 'text',
accessor: 'formattedCustomerType',
},
firstName: {
name: 'vendor.field.first_name',
type: 'text',
@@ -140,117 +135,116 @@ export default {
openingBalance: {
name: 'vendor.field.opening_balance',
type: 'number',
printable: false,
printable: false
},
openingBalanceAt: {
name: 'vendor.field.opening_balance_at',
type: 'date',
printable: false,
accessor: 'formattedOpeningBalanceAt'
printable: false
},
currencyCode: {
name: 'vendor.field.currency',
type: 'text',
printable: false,
printable: false
},
status: {
name: 'vendor.field.status',
printable: false,
printable: false
},
note: {
name: 'vendor.field.note',
printable: false,
printable: false
},
// Billing Address
billingAddress1: {
name: 'Billing Address 1',
column: 'billing_address1',
type: 'text',
printable: false,
printable: false
},
billingAddress2: {
name: 'Billing Address 2',
column: 'billing_address2',
type: 'text',
printable: false,
printable: false
},
billingAddressCity: {
name: 'Billing Address City',
column: 'billing_address_city',
type: 'text',
printable: false,
printable: false
},
billingAddressCountry: {
name: 'Billing Address Country',
column: 'billing_address_country',
type: 'text',
printable: false,
printable: false
},
billingAddressPostcode: {
name: 'Billing Address Postcode',
column: 'billing_address_postcode',
type: 'text',
printable: false,
printable: false
},
billingAddressState: {
name: 'Billing Address State',
column: 'billing_address_state',
type: 'text',
printable: false,
printable: false
},
billingAddressPhone: {
name: 'Billing Address Phone',
column: 'billing_address_phone',
type: 'text',
printable: false,
printable: false
},
// Shipping Address
shippingAddress1: {
name: 'Shipping Address 1',
column: 'shipping_address1',
type: 'text',
printable: false,
printable: false
},
shippingAddress2: {
name: 'Shipping Address 2',
column: 'shipping_address2',
type: 'text',
printable: false,
printable: false
},
shippingAddressCity: {
name: 'Shipping Address City',
column: 'shipping_address_city',
type: 'text',
printable: false,
printable: false
},
shippingAddressCountry: {
name: 'Shipping Address Country',
column: 'shipping_address_country',
type: 'text',
printable: false,
printable: false
},
shippingAddressPostcode: {
name: 'Shipping Address Postcode',
column: 'shipping_address_postcode',
type: 'text',
printable: false,
printable: false
},
shippingAddressPhone: {
name: 'Shipping Address Phone',
column: 'shipping_address_phone',
type: 'text',
printable: false,
printable: false
},
shippingAddressState: {
name: 'Shipping Address State',
column: 'shipping_address_state',
type: 'text',
printable: false,
printable: false
},
createdAt: {
name: 'vendor.field.created_at',
type: 'date',
printable: false,
printable: false
},
},
fields2: {

View File

@@ -257,25 +257,25 @@ export default {
name: 'item.field.sell_price',
fieldType: 'number',
},
costPrice: {
cost_price: {
name: 'item.field.cost_price',
fieldType: 'number',
},
costAccountId: {
costAccount: {
name: 'item.field.cost_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
importHint: 'Matches the account name or code.',
},
sellAccountId: {
sellAccount: {
name: 'item.field.sell_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
importHint: 'Matches the account name or code.',
},
inventoryAccountId: {
inventoryAccount: {
name: 'item.field.inventory_account',
fieldType: 'relation',
relationModel: 'Account',

View File

@@ -1,5 +1,4 @@
import { Model } from 'objection';
import { castArray, omit, pick } from 'lodash';
import { isEmpty } from 'lodash';
import { ServiceError } from '@/exceptions';
@@ -17,15 +16,7 @@ export default class PaginationQueryBuilder extends Model.QueryBuilder {
});
}
queryAndThrowIfHasRelations = ({
type,
message,
excludeRelations = [],
includedRelations = [],
}) => {
const _excludeRelations = castArray(excludeRelations);
const _includedRelations = castArray(includedRelations);
queryAndThrowIfHasRelations = ({ type, message }) => {
const model = this.modelClass();
const modelRelations = Object.keys(model.relationMappings).filter(
(relation) =>
@@ -34,20 +25,9 @@ export default class PaginationQueryBuilder extends Model.QueryBuilder {
) !== -1
);
const relations = model.secureDeleteRelations || modelRelations;
const filteredByIncluded = relations.filter((r) =>
_includedRelations.includes(r)
);
const filteredByExcluded = relations.filter(
(r) => !excludeRelations.includes(r)
);
const filteredRelations = !isEmpty(_includedRelations)
? filteredByIncluded
: !isEmpty(_excludeRelations)
? filteredByExcluded
: relations;
this.runAfter((model, query) => {
const nonEmptyRelations = filteredRelations.filter(
const nonEmptyRelations = relations.filter(
(relation) => !isEmpty(model[relation])
);
if (nonEmptyRelations.length > 0) {
@@ -56,7 +36,7 @@ export default class PaginationQueryBuilder extends Model.QueryBuilder {
return model;
});
return this.onBuild((query) => {
filteredRelations.forEach((relation) => {
relations.forEach((relation) => {
query.withGraphFetched(`${relation}(selectId)`).modifiers({
selectId(builder) {
builder.select('id');

View File

@@ -1,8 +1,9 @@
/* eslint-disable global-require */
import moment from 'moment';
import { Model, mixin } from 'objection';
import * as R from 'ramda';
import { Model, ModelOptions, QueryContext, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSettings from './ModelSetting';
import Account from './Account';
import UncategorizedCashflowTransactionMeta from './UncategorizedCashflowTransaction.meta';
export default class UncategorizedCashflowTransaction extends mixin(
@@ -166,28 +167,6 @@ export default class UncategorizedCashflowTransaction extends mixin(
pending(query) {
query.where('pending', true);
},
minAmount(query, minAmount) {
query.where('amount', '>=', minAmount);
},
maxAmount(query, maxAmount) {
query.where('amount', '<=', maxAmount);
},
toDate(query, toDate) {
const dateFormat = 'YYYY-MM-DD';
const _toDate = moment(toDate).endOf('day').format(dateFormat);
query.where('date', '<=', _toDate);
},
fromDate(query, fromDate) {
const dateFormat = 'YYYY-MM-DD';
const _fromDate = moment(fromDate).startOf('day').format(dateFormat);
query.where('date', '>=', _fromDate);
},
};
}

View File

@@ -43,8 +43,8 @@ export class AccountsApplication {
/**
* Creates a new account.
* @param {number} tenantId
* @param {IAccountCreateDTO} accountDTO
* @param {number} tenantId
* @param {IAccountCreateDTO} accountDTO
* @returns {Promise<IAccount>}
*/
public createAccount = (
@@ -108,8 +108,8 @@ export class AccountsApplication {
/**
* Retrieves the account details.
* @param {number} tenantId
* @param {number} accountId
* @param {number} tenantId
* @param {number} accountId
* @returns {Promise<IAccount>}
*/
public getAccount = (tenantId: number, accountId: number) => {

View File

@@ -2,7 +2,6 @@ import { Inject, Service } from 'typedi';
import { AccountsApplication } from './AccountsApplication';
import { Exportable } from '../Export/Exportable';
import { IAccountsFilter, IAccountsStructureType } from '@/interfaces';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service()
export class AccountsExportable extends Exportable {
@@ -21,7 +20,7 @@ export class AccountsExportable extends Exportable {
inactiveMode: false,
...query,
structure: IAccountsStructureType.Flat,
pageSize: EXPORT_SIZE_LIMIT,
pageSize: 12000,
page: 1,
} as IAccountsFilter;

View File

@@ -73,7 +73,6 @@ export class DeleteAccount {
.throwIfNotFound()
.queryAndThrowIfHasRelations({
type: ERRORS.ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS,
excludeRelations: ['uncategorizedTransactions', 'plaidItem']
});
// Authorize before delete account.
await this.authorize(tenantId, accountId, oldAccount);

View File

@@ -11,10 +11,6 @@ export class GetAuthMeta {
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
return {
signupDisabled: config.signupRestrictions.disabled,
oneClickDemo: {
enable: config.oneClickDemoAccounts.enable,
demoUrl: config.oneClickDemoAccounts.demoUrl,
},
};
}
}

View File

@@ -1,78 +0,0 @@
import { Inject, Service } from 'typedi';
import { initialize } from 'objection';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import events from '@/subscribers/events';
import { IAccountEventDeletePayload } from '@/interfaces';
import { DeleteBankRulesService } from '../../Rules/DeleteBankRules';
import { RevertRecognizedTransactions } from '../../RegonizeTranasctions/RevertRecognizedTransactions';
@Service()
export class DeleteUncategorizedTransactionsOnAccountDeleting {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private deleteBankRules: DeleteBankRulesService;
@Inject()
private revertRecognizedTransactins: RevertRecognizedTransactions;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.accounts.onDelete,
this.handleDeleteBankRulesOnAccountDeleting.bind(this)
);
}
/**
* Handles revert the recognized transactions and delete all the bank rules
* associated to the deleted bank account.
* @param {IAccountEventDeletePayload}
*/
private async handleDeleteBankRulesOnAccountDeleting({
tenantId,
oldAccount,
trx,
}: IAccountEventDeletePayload) {
const knex = this.tenancy.knex(tenantId);
const {
BankRule,
UncategorizedCashflowTransaction,
MatchedBankTransaction,
RecognizedBankTransaction,
} = this.tenancy.models(tenantId);
const foundAssociatedRules = await BankRule.query(trx).where(
'applyIfAccountId',
oldAccount.id
);
const foundAssociatedRulesIds = foundAssociatedRules.map((rule) => rule.id);
await initialize(knex, [
UncategorizedCashflowTransaction,
RecognizedBankTransaction,
MatchedBankTransaction,
]);
// Revert the recognized transactions of the given bank rules.
await this.revertRecognizedTransactins.revertRecognizedTransactions(
tenantId,
foundAssociatedRulesIds,
null,
trx
);
// Delete the associated uncategorized transactions.
await UncategorizedCashflowTransaction.query(trx)
.where('accountId', oldAccount.id)
.delete();
// Delete the given bank rules.
await this.deleteBankRules.deleteBankRules(
tenantId,
foundAssociatedRulesIds,
trx
);
}
}

View File

@@ -51,7 +51,6 @@ export class DisconnectPlaidItemOnAccountDeleted {
.findOne('plaidItemId', oldAccount.plaidItemId)
.delete();
// Remove Plaid item once the transaction resolve.
if (oldPlaidItem) {
const plaidInstance = PlaidClientWrapper.getClient();

View File

@@ -1,6 +1,5 @@
import { Inject, Service } from 'typedi';
import moment from 'moment';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { ExcludedBankTransactionsQuery } from './_types';
import { UncategorizedTransactionTransformer } from '@/services/Cashflow/UncategorizedTransactionTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@@ -40,18 +39,6 @@ export class GetExcludedBankTransactionsService {
if (_query.accountId) {
q.where('account_id', _query.accountId);
}
if (_query.minDate) {
q.modify('fromDate', _query.minDate);
}
if (_query.maxDate) {
q.modify('toDate', _query.maxDate);
}
if (_query.minAmount) {
q.modify('minAmount', _query.minAmount);
}
if (_query.maxAmount) {
q.modify('maxAmount', _query.maxAmount);
}
})
.pagination(_query.page - 1, _query.pageSize);

View File

@@ -4,10 +4,6 @@ export interface ExcludedBankTransactionsQuery {
page?: number;
pageSize?: number;
accountId?: number;
minDate?: Date;
maxDate?: Date;
minAmount?: number;
maxAmount?: number;
}
export interface IBankTransactionUnexcludingEventPayload {

View File

@@ -1,25 +1,18 @@
import { Inject, Service } from 'typedi';
import { initialize } from 'objection';
import { Knex } from 'knex';
import { first } from 'lodash';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer';
import {
GetMatchedTransactionsFilter,
IMatchTransactionDTO,
MatchedTransactionPOJO,
} from './types';
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { CreateBillPayment } from '@/services/Purchases/BillPayments/CreateBillPayment';
import { IBillPaymentDTO } from '@/interfaces';
@Service()
export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType {
@Inject()
private transformer: TransformerInjectable;
private tenancy: HasTenancyService;
@Inject()
private createPaymentMadeService: CreateBillPayment;
private transformer: TransformerInjectable;
/**
* Retrieves the matched transactions.
@@ -78,62 +71,4 @@ export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType
new GetMatchedTransactionBillsTransformer()
);
}
/**
* Creates the common matched transaction.
* @param {number} tenantId
* @param {Array<number>} uncategorizedTransactionIds
* @param {IMatchTransactionDTO} matchTransactionDTO
* @param {Knex.Transaction} trx
*/
public async createMatchedTransaction(
tenantId: number,
uncategorizedTransactionIds: Array<number>,
matchTransactionDTO: IMatchTransactionDTO,
trx?: Knex.Transaction
): Promise<void> {
await super.createMatchedTransaction(
tenantId,
uncategorizedTransactionIds,
matchTransactionDTO,
trx
);
const { Bill, UncategorizedCashflowTransaction, MatchedBankTransaction } =
this.tenancy.models(tenantId);
const uncategorizedTransactionId = first(uncategorizedTransactionIds);
const uncategorizedTransaction =
await UncategorizedCashflowTransaction.query(trx)
.findById(uncategorizedTransactionId)
.throwIfNotFound();
const bill = await Bill.query(trx)
.findById(matchTransactionDTO.referenceId)
.throwIfNotFound();
const createPaymentMadeDTO: IBillPaymentDTO = {
vendorId: bill.vendorId,
paymentAccountId: uncategorizedTransaction.accountId,
paymentDate: uncategorizedTransaction.date,
exchangeRate: 1,
entries: [
{
paymentAmount: bill.dueAmount,
billId: bill.id,
},
],
branchId: bill.branchId,
};
// Create a new bill payment associated to the matched bill.
const billPayment = await this.createPaymentMadeService.createBillPayment(
tenantId,
createPaymentMadeDTO,
trx
);
// Link the create bill payment with matched transaction.
await super.createMatchedTransaction(tenantId, uncategorizedTransactionIds, {
referenceType: 'BillPayment',
referenceId: billPayment.id,
}, trx);
}
}

View File

@@ -1,26 +1,22 @@
import { Inject, Service } from 'typedi';
import { initialize } from 'objection';
import { Knex } from 'knex';
import { first } from 'lodash';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
import {
GetMatchedTransactionsFilter,
IMatchTransactionDTO,
MatchedTransactionPOJO,
MatchedTransactionsPOJO,
} from './types';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { CreatePaymentReceived } from '@/services/Sales/PaymentReceived/CreatePaymentReceived';
import { IPaymentReceivedCreateDTO } from '@/interfaces';
@Service()
export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByType {
@Inject()
protected transformer: TransformerInjectable;
protected tenancy: HasTenancyService;
@Inject()
protected createPaymentReceivedService: CreatePaymentReceived;
protected transformer: TransformerInjectable;
/**
* Retrieves the matched transactions.
@@ -82,64 +78,4 @@ export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByTy
new GetMatchedTransactionInvoicesTransformer()
);
}
/**
* Creates the common matched transaction.
* @param {number} tenantId
* @param {Array<number>} uncategorizedTransactionIds
* @param {IMatchTransactionDTO} matchTransactionDTO
* @param {Knex.Transaction} trx
*/
public async createMatchedTransaction(
tenantId: number,
uncategorizedTransactionIds: Array<number>,
matchTransactionDTO: IMatchTransactionDTO,
trx?: Knex.Transaction
) {
await super.createMatchedTransaction(
tenantId,
uncategorizedTransactionIds,
matchTransactionDTO,
trx
);
const { SaleInvoice, UncategorizedCashflowTransaction, MatchedBankTransaction } =
this.tenancy.models(tenantId);
const uncategorizedTransactionId = first(uncategorizedTransactionIds);
const uncategorizedTransaction =
await UncategorizedCashflowTransaction.query(trx)
.findById(uncategorizedTransactionId)
.throwIfNotFound();
const invoice = await SaleInvoice.query(trx)
.findById(matchTransactionDTO.referenceId)
.throwIfNotFound();
const createPaymentReceivedDTO: IPaymentReceivedCreateDTO = {
customerId: invoice.customerId,
paymentDate: uncategorizedTransaction.date,
amount: invoice.dueAmount,
depositAccountId: uncategorizedTransaction.accountId,
entries: [
{
index: 1,
invoiceId: invoice.id,
paymentAmount: invoice.dueAmount,
},
],
branchId: invoice.branchId,
};
// Create a payment received associated to the matched invoice.
const paymentReceived = await this.createPaymentReceivedService.createPaymentReceived(
tenantId,
createPaymentReceivedDTO,
{},
trx
);
// Link the create payment received with matched invoice transaction.
await super.createMatchedTransaction(tenantId, uncategorizedTransactionIds, {
referenceType: 'PaymentReceive',
referenceId: paymentReceived.id,
}, trx)
}
}

View File

@@ -1,10 +1,10 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { PlaidClientWrapper } from '@/lib/Plaid/Plaid';
import { PlaidSyncDb } from './PlaidSyncDB';
import { PlaidFetchedTransactionsUpdates } from '@/interfaces';
import UnitOfWork from '@/services/UnitOfWork';
import { Knex } from 'knex';
@Service()
export class PlaidUpdateTransactions {
@@ -19,9 +19,9 @@ export class PlaidUpdateTransactions {
/**
* Handles sync the Plaid item to Bigcaptial under UOW.
* @param {number} tenantId - Tenant id.
* @param {number} plaidItemId - Plaid item id.
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
* @param {number} tenantId
* @param {number} plaidItemId
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
*/
public async updateTransactions(tenantId: number, plaidItemId: string) {
return this.uow.withTransaction(tenantId, (trx: Knex.Transaction) => {

View File

@@ -100,14 +100,12 @@ export class TriggerRecognizedTransactions {
private async triggerRecognizeTransactionsOnImportCommitted({
tenantId,
importId,
meta,
}: IImportFileCommitedEventPayload) {
const importFile = await Import.query().findOne({ importId });
const batch = importFile.paramsParsed.batch;
const payload = { tenantId, transactionsCriteria: { batch } };
// Cannot continue if the imported resource is not bank account transactions.
if (importFile.resource !== 'UncategorizedCashflowTransaction') return;
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
}
}

View File

@@ -26,39 +26,31 @@ export class DeleteBankRuleSerivce {
* @param {number} ruleId
* @returns {Promise<void>}
*/
public async deleteBankRule(
tenantId: number,
ruleId: number,
trx?: Knex.Transaction
): Promise<void> {
public async deleteBankRule(tenantId: number, ruleId: number): Promise<void> {
const { BankRule, BankRuleCondition } = this.tenancy.models(tenantId);
const oldBankRule = await BankRule.query()
.findById(ruleId)
.throwIfNotFound();
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onBankRuleDeleting` event.
await this.eventPublisher.emitAsync(events.bankRules.onDeleting, {
tenantId,
oldBankRule,
ruleId,
trx,
} as IBankRuleEventDeletingPayload);
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBankRuleDeleting` event.
await this.eventPublisher.emitAsync(events.bankRules.onDeleting, {
tenantId,
oldBankRule,
ruleId,
trx,
} as IBankRuleEventDeletingPayload);
await BankRuleCondition.query(trx).where('ruleId', ruleId).delete()
await BankRule.query(trx).findById(ruleId).delete();
await BankRuleCondition.query(trx).where('ruleId', ruleId).delete();
await BankRule.query(trx).findById(ruleId).delete();
// Triggers `onBankRuleDeleted` event.
await await this.eventPublisher.emitAsync(events.bankRules.onDeleted, {
tenantId,
ruleId,
trx,
} as IBankRuleEventDeletedPayload);
},
trx
);
// Triggers `onBankRuleDeleted` event.
await await this.eventPublisher.emitAsync(events.bankRules.onDeleted, {
tenantId,
ruleId,
trx,
} as IBankRuleEventDeletedPayload);
});
}
}

View File

@@ -1,34 +0,0 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import PromisePool from '@supercharge/promise-pool';
import { castArray, uniq } from 'lodash';
import { DeleteBankRuleSerivce } from './DeleteBankRule';
@Service()
export class DeleteBankRulesService {
@Inject()
private deleteBankRuleService: DeleteBankRuleSerivce;
/**
* Delete bank rules.
* @param {number} tenantId
* @param {number | Array<number>} bankRuleId
*/
async deleteBankRules(
tenantId: number,
bankRuleId: number | Array<number>,
trx?: Knex.Transaction
) {
const bankRulesIds = uniq(castArray(bankRuleId));
const results = await PromisePool.withConcurrency(1)
.for(bankRulesIds)
.process(async (bankRuleId: number) => {
await this.deleteBankRuleService.deleteBankRule(
tenantId,
bankRuleId,
trx
);
});
}
}

View File

@@ -8,12 +8,7 @@ export class CashflowAccountTransformer extends Transformer {
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return [
'formattedAmount',
'lastFeedsUpdatedAt',
'lastFeedsUpdatedAtFormatted',
'lastFeedsUpdatedFromNow',
];
return ['formattedAmount'];
};
/**
@@ -34,7 +29,7 @@ export class CashflowAccountTransformer extends Transformer {
/**
* Retrieve formatted account amount.
* @param {IAccount} invoice
* @param {IAccount} invoice
* @returns {string}
*/
protected formattedAmount = (account: IAccount): string => {
@@ -42,22 +37,4 @@ export class CashflowAccountTransformer extends Transformer {
currencyCode: account.currencyCode,
});
};
/**
* Retrieves the last feeds update at formatted date.
* @param {IAccount} account
* @returns {string}
*/
protected lastFeedsUpdatedAtFormatted(account: IAccount): string {
return this.formatDate(account.lastFeedsUpdatedAt);
}
/**
* Retrieves the last feeds updated from now.
* @param {IAccount} account
* @returns {string}
*/
protected lastFeedsUpdatedFromNow(account: IAccount): string {
return this.formatDateFromNow(account.lastFeedsUpdatedAt);
}
}

View File

@@ -23,7 +23,7 @@ export class GetRecognizedTransactionsService {
) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const _query = {
const _filter = {
page: 1,
pageSize: 20,
...filter,
@@ -41,26 +41,11 @@ export class GetRecognizedTransactionsService {
// Exclude the pending transactions.
q.modify('notPending');
if (_query.accountId) {
q.where('accountId', _query.accountId);
}
if (_query.minDate) {
q.modify('fromDate', _query.minDate);
}
if (_query.maxDate) {
q.modify('toDate', _query.maxDate);
}
if (_query.minAmount) {
q.modify('minAmount', _query.minAmount);
}
if (_query.maxAmount) {
q.modify('maxAmount', _query.maxAmount);
}
if (_query.accountId) {
q.where('accountId', _query.accountId);
if (_filter.accountId) {
q.where('accountId', _filter.accountId);
}
})
.pagination(_query.page - 1, _query.pageSize);
.pagination(_filter.page - 1, _filter.pageSize);
const data = await this.transformer.transform(
tenantId,

View File

@@ -62,19 +62,6 @@ export class GetUncategorizedTransactions {
q.whereNull('matchedBankTransactions.id');
q.orderBy('date', 'DESC');
if (_query.minDate) {
q.modify('fromDate', _query.minDate);
}
if (_query.maxDate) {
q.modify('toDate', _query.maxDate);
}
if (_query.minAmount) {
q.modify('minAmount', _query.minAmount);
}
if (_query.maxAmount) {
q.modify('maxAmount', _query.maxAmount);
}
})
.pagination(_query.page - 1, _query.pageSize);

View File

@@ -87,7 +87,7 @@ export class UncategorizedTransactionsImportable extends Importable {
}
/**
* Transforms the import params before storing them.
* Transformes the import params before storing them.
* @param {Record<string, any>} parmas
*/
public transformParams(parmas: Record<string, any>) {

View File

@@ -2,7 +2,6 @@ import { Inject, Service } from 'typedi';
import { IItemsFilter } from '@/interfaces';
import { CustomersApplication } from './CustomersApplication';
import { Exportable } from '@/services/Export/Exportable';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class CustomersExportable extends Exportable {
@@ -18,9 +17,9 @@ export class CustomersExportable extends Exportable {
const parsedQuery = {
sortOrder: 'DESC',
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
...query,
pageSize: 12,
} as IItemsFilter;
return this.customersApplication

View File

@@ -2,7 +2,6 @@ import { Inject, Service } from 'typedi';
import { IItemsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { VendorsApplication } from './VendorsApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class VendorsExportable extends Exportable {
@@ -18,9 +17,9 @@ export class VendorsExportable extends Exportable {
const parsedQuery = {
sortOrder: 'DESC',
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
...query,
pageSize: 12,
} as IItemsFilter;
return this.vendorsApplication

View File

@@ -16,7 +16,6 @@ export class ExpenseTransfromer extends Transformer {
'formattedAllocatedCostAmount',
'formattedDate',
'formattedCreatedAt',
'formattedPublishedAt',
'categories',
'attachments',
];
@@ -92,13 +91,4 @@ export class ExpenseTransfromer extends Transformer {
protected attachments = (expense: IExpense) => {
return this.item(expense.attachments, new AttachmentTransformer());
};
/**
* Retrieve formatted published at date.
* @param {IExpense} expense
* @returns {string}
*/
protected formattedPublishedAt = (expense: IExpense): string => {
return this.formatDate(expense.publishedAt);
}
}

View File

@@ -2,7 +2,6 @@ import { Inject, Service } from 'typedi';
import { Exportable } from '../Export/Exportable';
import { IExpensesFilter } from '@/interfaces';
import { ExpensesApplication } from './ExpensesApplication';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service()
export class ExpensesExportable extends Exportable {
@@ -20,7 +19,7 @@ export class ExpensesExportable extends Exportable {
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
pageSize: 12000,
} as IExpensesFilter;
return this.expensesApplication

View File

@@ -1,48 +0,0 @@
import { Service } from 'typedi';
import { AsyncLocalStorage } from 'async_hooks';
@Service()
export class ExportAls {
private als: AsyncLocalStorage<Map<string, any>>;
constructor() {
this.als = new AsyncLocalStorage();
}
/**
* Runs a callback function within the context of a new AsyncLocalStorage store.
* @param callback The function to be executed within the AsyncLocalStorage context.
* @returns The result of the callback function.
*/
public run<T>(callback: () => T): T {
return this.als.run<T>(new Map(), () => {
this.markAsExport();
return callback();
});
}
/**
* Retrieves the current AsyncLocalStorage store.
* @returns The current store or undefined if not in a valid context.
*/
public getStore(): Map<string, any> | undefined {
return this.als.getStore();
}
/**
* Marks the current context as an export operation.
* @param flag Boolean flag to set or unset the export status. Defaults to true.
*/
public markAsExport(flag: boolean = true): void {
const store = this.getStore();
store?.set('isExport', flag);
}
/**
* Checks if the current context is an export operation.
* @returns {boolean} True if the context is an export operation, false otherwise.
*/
public get isExport(): boolean {
return !!this.getStore()?.get('isExport');
}
}

View File

@@ -10,7 +10,6 @@ import { Errors, ExportFormat } from './common';
import { IModelMeta, IModelMetaColumn } from '@/interfaces';
import { flatDataCollections, getDataAccessor } from './utils';
import { ExportPdf } from './ExportPdf';
import { ExportAls } from './ExportAls';
@Service()
export class ExportResourceService {
@@ -23,33 +22,13 @@ export class ExportResourceService {
@Inject()
private exportPdf: ExportPdf;
@Inject()
private exportAls: ExportAls;
/**
*
* @param {number} tenantId
* @param {string} resourceName
* @param {ExportFormat} format
* @returns
*/
public async export(
tenantId: number,
resourceName: string,
format: ExportFormat = ExportFormat.Csv
) {
return this.exportAls.run(() =>
this.exportAlsRun(tenantId, resourceName, format)
);
}
/**
* Exports the given resource data through csv, xlsx or pdf.
* @param {number} tenantId - Tenant id.
* @param {string} resourceName - Resource name.
* @param {ExportFormat} format - File format.
*/
public async exportAlsRun(
public async export(
tenantId: number,
resourceName: string,
format: ExportFormat = ExportFormat.Csv

View File

@@ -1,2 +0,0 @@
export const EXPORT_SIZE_LIMIT = 9999999;
export const EXPORT_DTE_FORMAT = 'YYYY-MM-DD';

View File

@@ -1,105 +0,0 @@
import { Service } from 'typedi';
import { AsyncLocalStorage } from 'async_hooks';
@Service()
export class ImportAls {
private als: AsyncLocalStorage<Map<string, any>>;
constructor() {
this.als = new AsyncLocalStorage();
}
/**
* Runs a callback function within the context of a new AsyncLocalStorage store.
* @param callback The function to be executed within the AsyncLocalStorage context.
* @returns The result of the callback function.
*/
public run<T>(callback: () => T): T {
return this.als.run<T>(new Map(), callback);
}
/**
* Runs a callback function in preview mode within the AsyncLocalStorage context.
* @param callback The function to be executed in preview mode.
* @returns The result of the callback function.
*/
public runPreview<T>(callback: () => T): T {
return this.run(() => {
this.markAsImport();
this.markAsImportPreview();
return callback();
});
}
/**
* Runs a callback function in commit mode within the AsyncLocalStorage context.
* @param {() => T} callback - The function to be executed in commit mode.
* @returns {T} The result of the callback function.
*/
public runCommit<T>(callback: () => T): T {
return this.run(() => {
this.markAsImport();
this.markAsImportCommit();
return callback();
});
}
/**
* Retrieves the current AsyncLocalStorage store.
* @returns The current store or undefined if not in a valid context.
*/
public getStore(): Map<string, any> | undefined {
return this.als.getStore();
}
/**
* Marks the current context as an import operation.
* @param flag Boolean flag to set or unset the import status. Defaults to true.
*/
public markAsImport(flag: boolean = true): void {
const store = this.getStore();
store?.set('isImport', flag);
}
/**
* Marks the current context as an import commit operation.
* @param flag Boolean flag to set or unset the import commit status. Defaults to true.
*/
public markAsImportCommit(flag: boolean = true): void {
const store = this.getStore();
store?.set('isImportCommit', flag);
}
/**
* Marks the current context as an import preview operation.
* @param {boolean} flag - Boolean flag to set or unset the import preview status. Defaults to true.
*/
public markAsImportPreview(flag: boolean = true): void {
const store = this.getStore();
store?.set('isImportPreview', flag);
}
/**
* Checks if the current context is an import operation.
* @returns {boolean} True if the context is an import operation, false otherwise.
*/
public get isImport(): boolean {
return !!this.getStore()?.get('isImport');
}
/**
* Checks if the current context is an import commit operation.
* @returns {boolean} True if the context is an import commit operation, false otherwise.
*/
public get isImportCommit(): boolean {
return !!this.getStore()?.get('isImportCommit');
}
/**
* Checks if the current context is an import preview operation.
* @returns {boolean} True if the context is an import preview operation, false otherwise.
*/
public get isImportPreview(): boolean {
return !!this.getStore()?.get('isImportPreview');
}
}

View File

@@ -1,3 +1,4 @@
import XLSX from 'xlsx';
import bluebird from 'bluebird';
import * as R from 'ramda';
import { Inject, Service } from 'typedi';
@@ -26,7 +27,23 @@ 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 -

View File

@@ -23,7 +23,7 @@ export class ImportFileMapping {
*/
public async mapping(
tenantId: number,
importId: string,
importId: number,
maps: ImportMappingAttr[]
): Promise<ImportFileMapPOJO> {
const importFile = await Import.query()

View File

@@ -2,7 +2,6 @@ import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import { ImportFilePreviewPOJO } from './interfaces';
import { ImportFileProcess } from './ImportFileProcess';
import { ImportAls } from './ImportALS';
@Service()
export class ImportFilePreview {
@@ -12,33 +11,15 @@ export class ImportFilePreview {
@Inject()
private importFile: ImportFileProcess;
@Inject()
private importAls: ImportAls;
/**
* Preview the imported file results before commiting the transactions.
* @param {number} tenantId -
* @param {string} importId -
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async preview(
tenantId: number,
importId: string
): Promise<ImportFilePreviewPOJO> {
return this.importAls.runPreview<Promise<ImportFilePreviewPOJO>>(() =>
this.previewAlsRun(tenantId, importId)
);
}
/**
* Preview the imported file results before commiting the transactions.
* @param {number} tenantId
* @param {number} importId
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async previewAlsRun(
public async preview(
tenantId: number,
importId: string
importId: number
): Promise<ImportFilePreviewPOJO> {
const knex = this.tenancy.knex(tenantId);
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });

View File

@@ -2,14 +2,18 @@ import { Inject, Service } from 'typedi';
import { chain } from 'lodash';
import { Knex } from 'knex';
import { ServiceError } from '@/exceptions';
import { ERRORS, getUnmappedSheetColumns, readImportFile } from './_utils';
import {
ERRORS,
getSheetColumns,
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 {
@@ -33,7 +37,7 @@ export class ImportFileProcess {
*/
public async import(
tenantId: number,
importId: string,
importId: number,
trx?: Knex.Transaction
): Promise<ImportFilePreviewPOJO> {
const importFile = await Import.query()
@@ -45,10 +49,10 @@ export class ImportFileProcess {
if (!importFile.isMapped) {
throw new ServiceError(ERRORS.IMPORT_FILE_NOT_MAPPED);
}
// Read the imported file and parse the given buffer to get columns
// and sheet data in json format.
// Read the imported file.
const buffer = await readImportFile(importFile.filename);
const [sheetData, sheetColumns] = parseSheetData(buffer);
const sheetData = this.importCommon.parseXlsxSheet(buffer);
const header = getSheetColumns(sheetData);
const resource = importFile.resource;
const resourceFields = this.resource.getResourceFields2(tenantId, resource);
@@ -83,7 +87,7 @@ export class ImportFileProcess {
.flatten()
.value();
const unmappedColumns = getUnmappedSheetColumns(sheetColumns, mapping);
const unmappedColumns = getUnmappedSheetColumns(header, mapping);
const totalCount = allData.length;
const createdCount = successedImport.length;

View File

@@ -5,7 +5,6 @@ import { ImportFileProcess } from './ImportFileProcess';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
import { ImportAls } from './ImportALS';
@Service()
export class ImportFileProcessCommit {
@@ -15,36 +14,18 @@ export class ImportFileProcessCommit {
@Inject()
private importFile: ImportFileProcess;
@Inject()
private importAls: ImportAls;
@Inject()
private eventPublisher: EventPublisher;
/**
* Commits the imported file under ALS.
* @param {number} tenantId
* @param {string} importId
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public commit(
tenantId: number,
importId: string
): Promise<ImportFilePreviewPOJO> {
return this.importAls.runCommit<Promise<ImportFilePreviewPOJO>>(() =>
this.commitAlsRun(tenantId, importId)
);
}
/**
* Commits the imported file.
* @param {number} tenantId
* @param {number} importId
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async commitAlsRun(
public async commit(
tenantId: number,
importId: string
importId: number
): Promise<ImportFilePreviewPOJO> {
const knex = this.tenancy.knex(tenantId);
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });

View File

@@ -11,7 +11,6 @@ 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 {
@@ -78,12 +77,14 @@ export class ImportFileUploadService {
const buffer = await readImportFile(filename);
// Parse the buffer file to array data.
const [sheetData, sheetColumns] = parseSheetData(buffer);
const coumnsStringified = JSON.stringify(sheetColumns);
const sheetData = this.importFileCommon.parseXlsxSheet(buffer);
// 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);

View File

@@ -55,7 +55,7 @@ export class ImportResourceApplication {
*/
public async mapping(
tenantId: number,
importId: string,
importId: number,
maps: ImportMappingAttr[]
) {
return this.importMappingService.mapping(tenantId, importId, maps);
@@ -67,7 +67,7 @@ export class ImportResourceApplication {
* @param {number} importId - Import id.
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async preview(tenantId: number, importId: string) {
public async preview(tenantId: number, importId: number) {
return this.ImportFilePreviewService.preview(tenantId, importId);
}
@@ -77,7 +77,7 @@ export class ImportResourceApplication {
* @param {number} importId
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async process(tenantId: number, importId: string) {
public async process(tenantId: number, importId: number) {
return this.importProcessCommit.commit(tenantId, importId);
}

View File

@@ -1,56 +0,0 @@
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

@@ -139,25 +139,24 @@ export default class InventoryService {
) {
const agenda = Container.get('agenda');
const commonJobsQuery = {
name: 'compute-item-cost',
lastRunAt: { $exists: false },
'data.tenantId': tenantId,
'data.itemId': itemId,
};
// Cancel any `compute-item-cost` in the queue has upper starting date
// with the same given item.
await agenda.cancel({
...commonJobsQuery,
'data.startingDate': { $lte: startingDate },
name: 'compute-item-cost',
nextRunAt: { $ne: null },
'data.tenantId': tenantId,
'data.itemId': itemId,
'data.startingDate': { $gt: startingDate },
});
// Retrieve any `compute-item-cost` in the queue has lower starting date
// with the same given item.
const dependsJobs = await agenda.jobs({
...commonJobsQuery,
'data.startingDate': { $gte: startingDate },
name: 'compute-item-cost',
nextRunAt: { $ne: null },
'data.tenantId': tenantId,
'data.itemId': itemId,
'data.startingDate': { $lte: startingDate },
});
// If the depends jobs cleared.
if (dependsJobs.length === 0) {
await agenda.schedule(
config.scheduleComputeItemCost,
@@ -173,13 +172,6 @@ export default class InventoryService {
events.inventory.onComputeItemCostJobScheduled,
{ startingDate, itemId, tenantId } as IInventoryItemCostScheduledPayload
);
} else {
// Re-schedule the jobs that have higher date from current moment.
await Promise.all(
dependsJobs.map((job) =>
job.schedule(config.scheduleComputeItemCost).save()
)
);
}
}

View File

@@ -43,22 +43,12 @@ export class CreateItem {
itemDTO.sellAccountId
);
}
// Validate the income account id existance if the item is sellable.
this.validators.validateIncomeAccountExistance(
itemDTO.sellable,
itemDTO.sellAccountId
);
if (itemDTO.costAccountId) {
await this.validators.validateItemCostAccountExistance(
tenantId,
itemDTO.costAccountId
);
}
// Validate the cost account id existance if the item is purchasable.
this.validators.validateCostAccountExistance(
itemDTO.purchasable,
itemDTO.costAccountId
);
if (itemDTO.inventoryAccountId) {
await this.validators.validateItemInventoryAccountExistance(
tenantId,

View File

@@ -55,11 +55,6 @@ export class EditItem {
itemDTO.categoryId
);
}
// Validate the income account id existance if the item is sellable.
this.validators.validateIncomeAccountExistance(
itemDTO.sellable,
itemDTO.sellAccountId
);
// Validate the sell account existance on the storage.
if (itemDTO.sellAccountId) {
await this.validators.validateItemSellAccountExistance(
@@ -67,11 +62,6 @@ export class EditItem {
itemDTO.sellAccountId
);
}
// Validate the cost account id existance if the item is purchasable.
this.validators.validateCostAccountExistance(
itemDTO.purchasable,
itemDTO.costAccountId
);
// Validate the cost account existance on the storage.
if (itemDTO.costAccountId) {
await this.validators.validateItemCostAccountExistance(

View File

@@ -85,42 +85,6 @@ export class ItemsValidators {
}
}
/**
* Validates income account existance.
* @param {number|null} sellable - Detarmines if the item sellable.
* @param {number|null} incomeAccountId - Income account id.
* @throws {ServiceError(ERRORS.INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM)}
*/
public validateIncomeAccountExistance(
sellable?: boolean,
incomeAccountId?: number
) {
if (sellable && !incomeAccountId) {
throw new ServiceError(
ERRORS.INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM,
'Income account is require with sellable item.'
);
}
}
/**
* Validates the cost account existance.
* @param {boolean|null} purchasable - Detarmines if the item purchasble.
* @param {number|null} costAccountId - Cost account id.
* @throws {ServiceError(ERRORS.COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM)}
*/
public validateCostAccountExistance(
purchasable: boolean,
costAccountId?: number
) {
if (purchasable && !costAccountId) {
throw new ServiceError(
ERRORS.COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM,
'The cost account is required with purchasable item.'
);
}
}
/**
* Validate item inventory account existance and type.
* @param {number} tenantId

View File

@@ -5,7 +5,6 @@ import { ServiceError } from '@/exceptions';
import TenancyService from '@/services/Tenancy/TenancyService';
import { ItemEntry } from '@/models';
import { entriesAmountDiff } from 'utils';
import { Knex } from 'knex';
const ERRORS = {
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
@@ -59,14 +58,13 @@ export default class ItemsEntriesService {
*/
public async filterInventoryEntries(
tenantId: number,
entries: IItemEntry[],
trx?: Knex.Transaction
entries: IItemEntry[]
): Promise<IItemEntry[]> {
const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = entries.map((e) => e.itemId);
// Retrieve entries inventory items.
const inventoryItems = await Item.query(trx)
const inventoryItems = await Item.query()
.whereIn('id', entriesItemsIds)
.where('type', 'inventory');

View File

@@ -2,7 +2,6 @@ import { Inject, Service } from 'typedi';
import { Exportable } from '../Export/Exportable';
import { IItemsFilter } from '@/interfaces';
import { ItemsApplication } from './ItemsApplication';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service()
export class ItemsExportable extends Exportable {
@@ -20,7 +19,7 @@ export class ItemsExportable extends Exportable {
columnSortBy: 'created_at',
page: 1,
...query,
pageSize: EXPORT_SIZE_LIMIT,
pageSize: 12,
} as IItemsFilter;
return this.itemsApplication

View File

@@ -26,11 +26,6 @@ export const ERRORS = {
PURCHASE_TAX_RATE_NOT_FOUND: 'PURCHASE_TAX_RATE_NOT_FOUND',
SELL_TAX_RATE_NOT_FOUND: 'SELL_TAX_RATE_NOT_FOUND',
INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM:
'INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM',
COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM:
'COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM',
};
export const DEFAULT_VIEW_COLUMNS = [];

View File

@@ -2,7 +2,6 @@ import { Inject, Service } from 'typedi';
import { IManualJournalsFilter } from '@/interfaces';
import { Exportable } from '../Export/Exportable';
import { ManualJournalsApplication } from './ManualJournalsApplication';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service()
export class ManualJournalsExportable extends Exportable {
@@ -20,7 +19,7 @@ export class ManualJournalsExportable extends Exportable {
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
pageSize: 12000,
} as IManualJournalsFilter;
return this.manualJournalsApplication

View File

@@ -1,74 +0,0 @@
import { Inject, Service } from 'typedi';
import { faker } from '@faker-js/faker';
import uniqid from 'uniqid';
import AuthenticationApplication from '../Authentication/AuthApplication';
import OrganizationService from '../Organization/OrganizationService';
import { OneClickDemo } from '@/system/models/OneclickDemo';
import { SystemUser } from '@/system/models';
import { IAuthSignInPOJO } from '@/interfaces';
import { ICreateOneClickDemoPOJO } from './interfaces';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { defaultDemoOrganizationDTO } from './_constants';
@Service()
export class CreateOneClickDemo {
@Inject()
private authApp: AuthenticationApplication;
@Inject()
private organizationService: OrganizationService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Creates one-click demo account.
* @returns {Promise<ICreateOneClickDemoPOJO>}
*/
public async createOneClickDemo(): Promise<ICreateOneClickDemoPOJO> {
const firstName = faker.person.firstName();
const lastName = faker.person.lastName();
const email = faker.internet.email();
const password = '123123123';
const demoId = uniqid();
await this.authApp.signUp({ firstName, lastName, email, password });
const signedIn = await this.authApp.signIn(email, password);
const tenantId = signedIn.tenant.id;
const userId = signedIn.user.id;
// Creates a new one-click demo.
await OneClickDemo.query().insert({ key: demoId, tenantId, userId });
const buildJob = await this.organizationService.buildRunJob(
tenantId,
defaultDemoOrganizationDTO,
signedIn.user
);
return { email, demoId, signedIn, buildJob };
}
/**
* Sign-in automicatlly using the demo id one creating an account finish.
* @param {string} oneClickDemoId -
* @returns {Promise<IAuthSignInPOJO>}
*/
async autoSignIn(oneClickDemoId: string): Promise<IAuthSignInPOJO> {
const foundOneclickDemo = await OneClickDemo.query()
.findOne('key', oneClickDemoId)
.throwIfNotFound();
const userId = foundOneclickDemo.userId;
const user = await SystemUser.query().findById(userId);
const email = user.email;
const password = '123123123';
const signedIn = await this.authApp.signIn(email, password);
return signedIn;
}
}

View File

@@ -1,17 +0,0 @@
export class SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
* @returns {Array<>}
*/
get mapping() {
return [];
}
/**
* Retireves the seeder file import params.
* @returns {Record<string, any>}
*/
get importParams() {
return {};
}
}

View File

@@ -1,35 +0,0 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoBankTransactions extends SeedDemoAbstract {
get mapping() {
return [
{ from: 'Date', to: 'date' },
{ from: 'Payee', to: 'payee' },
{ from: 'Description', to: 'description' },
{ from: 'Reference No.', to: 'referenceNo' },
{ from: 'Amount', to: 'amount' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `bank-transactions.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'UncategorizedCashflowTransaction';
}
get importParams() {
return {
accountId: 1001,
};
}
}

View File

@@ -1,37 +0,0 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoAccountCustomers extends SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
*/
get mapping() {
return [
{ from: 'Customer Type', to: 'customerType' },
{ from: 'First Name', to: 'firstName' },
{ from: 'Last Name', to: 'lastName' },
{ from: 'Display Name', to: 'displayName' },
{ from: 'Email', to: 'email' },
{ from: 'Work Phone Number', to: 'workPhone' },
{ from: 'Personal Phone Number', to: 'personalPhone' },
{ from: 'Company Name', to: 'companyName' },
{ from: 'Website', to: 'website' },
{ from: 'Active', to: 'active' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `customers.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'Customer';
}
}

View File

@@ -1,39 +0,0 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoAccountExpenses extends SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
*/
get mapping() {
return [
{ from: 'Payment Account', to: 'paymentAccountId' },
{ from: 'Reference No.', to: 'referenceNo' },
{ from: 'Payment Date', to: 'paymentDate' },
{ from: 'Description', to: 'description' },
{ from: 'Publish', to: 'publish' },
{
from: 'Expense Account',
to: 'expenseAccountId',
group: 'categories',
},
{ from: 'Amount', to: 'amount', group: 'categories' },
{ from: 'Line Description', to: 'description', group: 'categories' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `Expenses.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'Expense';
}
}

View File

@@ -1,45 +0,0 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoAccountItems extends SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
*/
get mapping() {
return [
{ from: 'Item Type', to: 'type' },
{ from: 'Item Name', to: 'name' },
{ from: 'Item Code', to: 'code' },
{ from: 'Sellable', to: 'sellable' },
{ from: 'Purchasable', to: 'purchasable' },
{ from: 'Sell Price', to: 'sellPrice' },
{ from: 'Cost Price', to: 'costPrice' },
{ from: 'Cost Account', to: 'costAccountId' },
{ from: 'Sell Account', to: 'sellAccountId' },
{ from: 'Inventory Account', to: 'inventoryAccountId' },
{ from: 'Sell Description', to: 'sellDescription' },
{
from: 'Purchase Description',
to: 'purchaseDescription',
},
{ from: 'Note', to: 'note' },
{ from: 'Category', to: 'category' },
{ from: 'Active', to: 'active' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `items.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'Item';
}
}

View File

@@ -1,36 +0,0 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoAccountManualJournals extends SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
*/
get mapping() {
return [
{ from: 'Date', to: 'date' },
{ from: 'Journal No', to: 'journalNumber' },
{ from: 'Reference No.', to: 'reference' },
{ from: 'Description', to: 'description' },
{ from: 'Publish', to: 'publish' },
{ from: 'Credit', to: 'credit', group: 'entries' },
{ from: 'Debit', to: 'debit', group: 'entries' },
{ from: 'Account', to: 'accountId', group: 'entries' },
{ from: 'Note', to: 'note', group: 'entries' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `manual-journals.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'ManualJournal';
}
}

View File

@@ -1,37 +0,0 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoSaleInvoices extends SeedDemoAbstract {
get mapping() {
return [
{ from: 'Invoice Date', to: 'invoiceDate' },
{ from: 'Due Date', to: 'dueDate' },
{ from: 'Reference No.', to: 'referenceNo' },
{ from: 'Invoice No.', to: 'invoiceNo' },
{ from: 'Customer', to: 'customerId' },
{ from: 'Exchange Rate', to: 'exchangeRate' },
{ from: 'Invoice Message', to: 'invoiceMessage' },
{ from: 'Terms & Conditions', to: 'termsConditions' },
{ from: 'Delivered', to: 'delivered' },
{ from: 'Item', to: 'itemId', group: 'entries' },
{ from: 'Rate', to: 'rate', group: 'entries' },
{ from: 'Quantity', to: 'quantity', group: 'entries' },
{ from: 'Description', to: 'description', group: 'entries' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `sale-invoices.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'SaleInvoice';
}
}

View File

@@ -1,36 +0,0 @@
import { SeedDemoAbstract } from './SeedDemoAbstract';
export class SeedDemoAccountVendors extends SeedDemoAbstract {
/**
* Retrieves the seeder file mapping.
*/
get mapping() {
return [
{ from: 'First Name', to: 'firstName' },
{ from: 'Last Name', to: 'lastName' },
{ from: 'Display Name', to: 'displayName' },
{ from: 'Email', to: 'email' },
{ from: 'Work Phone Number', to: 'workPhone' },
{ from: 'Personal Phone Number', to: 'personalPhone' },
{ from: 'Company Name', to: 'companyName' },
{ from: 'Website', to: 'website' },
{ from: 'Active', to: 'active' },
];
}
/**
* Retrieves the seeder file name.
* @returns {string}
*/
get importFileName() {
return `vendors.csv`;
}
/**
* Retrieve the resource name of the seeder.
* @returns {string}
*/
get resource() {
return 'Vendor';
}
}

View File

@@ -1,25 +0,0 @@
import { Inject, Service } from 'typedi';
import { CreateOneClickDemo } from './CreateOneClickDemo';
@Service()
export class OneClickDemoApplication {
@Inject()
private createOneClickDemoService: CreateOneClickDemo;
/**
* Creates one-click demo account.
* @returns {Promise<ICreateOneClickDemoPOJO>}
*/
public createOneClick() {
return this.createOneClickDemoService.createOneClickDemo();
}
/**
* Auto-sign-in to created demo account.
* @param {string} demoId -
* @returns {Promise<IAuthSignInPOJO>}
*/
public autoSignIn(demoId: string) {
return this.createOneClickDemoService.autoSignIn(demoId);
}
}

View File

@@ -1,12 +0,0 @@
export const defaultDemoOrganizationDTO = {
name: 'BIGCAPITAL, INC',
baseCurrency: 'USD',
location: 'US',
language: 'en',
industry: 'Technology',
fiscalYear: 'march',
timezone: 'US/Central',
dateFormat: 'MM/DD/yyyy',
}

View File

@@ -1,113 +0,0 @@
import { Inject } from 'typedi';
import { promises as fs } from 'fs';
import path from 'path';
import uniqid from 'uniqid';
import { isEmpty } from 'lodash';
import events from '@/subscribers/events';
import { PromisePool } from '@supercharge/promise-pool';
import { IOrganizationBuiltEventPayload } from '@/interfaces';
import { SeedDemoAccountItems } from '../DemoSeeders/SeedDemoItems';
import { ImportResourceApplication } from '@/services/Import/ImportResourceApplication';
import { getImportsStoragePath } from '@/services/Import/_utils';
import { OneClickDemo } from '@/system/models/OneclickDemo';
import { SeedDemoAccountCustomers } from '../DemoSeeders/SeedDemoCustomers';
import { SeedDemoAccountVendors } from '../DemoSeeders/SeedDemoVendors';
import { SeedDemoAccountManualJournals } from '../DemoSeeders/SeedDemoManualJournals';
import { SeedDemoAccountExpenses } from '../DemoSeeders/SeedDemoExpenses';
import { SeedDemoBankTransactions } from '../DemoSeeders/SeedDemoBankTransactions';
import { SeedDemoSaleInvoices } from '../DemoSeeders/SeedDemoSaleInvoices';
export class SeedInitialDemoAccountDataOnOrgBuild {
@Inject()
private importApp: ImportResourceApplication;
/**
* Attaches events with handlers.
*/
public attach = (bus) => {
bus.subscribe(
events.organization.built,
this.seedInitialDemoAccountDataOnOrgBuild.bind(this)
);
};
/**
* Demo account seeder.
*/
get seedDemoAccountSeeders() {
return [
SeedDemoAccountItems,
SeedDemoBankTransactions,
SeedDemoAccountCustomers,
SeedDemoAccountVendors,
SeedDemoAccountManualJournals,
SeedDemoSaleInvoices,
SeedDemoAccountExpenses,
];
}
/**
* Initialize the seeder sheet file to the import storage first.
* @param {string} fileName -
* @returns {Promise<void>}
*/
async initiateSeederFile(fileName: string) {
const destFileName = uniqid();
const source = path.join(global.__views_dir, `/demo-sheets`, fileName);
const destination = path.join(getImportsStoragePath(), destFileName);
// Use the fs.promises.copyFile method to copy the file
await fs.copyFile(source, destination);
return destFileName;
}
/**
* Seeds initial demo account data on organization build
* @param {IOrganizationBuildEventPayload}
*/
async seedInitialDemoAccountDataOnOrgBuild({
tenantId,
}: IOrganizationBuiltEventPayload) {
const foundDemo = await OneClickDemo.query().findOne('tenantId', tenantId);
// Can't continue if the found demo is not exists.
// Means that account is not demo account.
if (!foundDemo) {
return null;
}
const results = await PromisePool.for(this.seedDemoAccountSeeders)
.withConcurrency(1)
.process(async (SeedDemoAccountSeeder) => {
const seederInstance = new SeedDemoAccountSeeder();
// Initialize the seeder sheet file before importing.
const importFileName = await this.initiateSeederFile(seederInstance.importFileName);
// Import the given seeder file.
const importedFile = await this.importApp.import(
tenantId,
seederInstance.resource,
importFileName,
seederInstance.importParams
);
// Mapping the columns with resource fields.
await this.importApp.mapping(
tenantId,
importedFile.import.importId,
seederInstance.mapping
);
await this.importApp.preview(tenantId, importedFile.import.importId);
// Commit the imported file.
await this.importApp.process(
tenantId,
importedFile.import.importId
);
});
if (!isEmpty(results.errors)) {
throw results.errors;
}
}
}

View File

@@ -1,8 +0,0 @@
import { IAuthSignInPOJO } from '@/interfaces';
export interface ICreateOneClickDemoPOJO {
email: string;
demoId: string;
signedIn: IAuthSignInPOJO;
buildJob: any;
}

View File

@@ -5,7 +5,6 @@ import { ServiceError } from '@/exceptions';
import {
IOrganizationBuildDTO,
IOrganizationBuildEventPayload,
IOrganizationBuiltEventPayload,
IOrganizationUpdateDTO,
ISystemUser,
ITenant,
@@ -18,8 +17,6 @@ import { Tenant } from '@/system/models';
import OrganizationBaseCurrencyLocking from './OrganizationBaseCurrencyLocking';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants';
import { initializeTenantSettings } from '@/api/middleware/SettingsMiddleware';
import { initalizeTenantServices } from '@/api/middleware/TenantDependencyInjection';
@Service()
export default class OrganizationService {
@@ -65,10 +62,6 @@ export default class OrganizationService {
// Migrated tenant.
const migratedTenant = await tenant.$query().withGraphFetched('metadata');
// Injects the given tenant IoC services.
await initalizeTenantServices(tenantId);
await initializeTenantSettings(tenantId);
// Creates a tenancy object from given tenant model.
const tenancyContext =
this.tenantsManager.getSeedMigrationContext(migratedTenant);
@@ -89,11 +82,6 @@ export default class OrganizationService {
//
await this.flagTenantDBBatch(tenantId);
// Triggers the organization built event.
await this.eventPublisher.emitAsync(events.organization.built, {
tenantId: tenant.id,
} as IOrganizationBuiltEventPayload)
}
/**

View File

@@ -1,7 +1,6 @@
import { Inject, Service } from 'typedi';
import { Exportable } from '@/services/Export/Exportable';
import { BillPaymentsApplication } from './BillPaymentsApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class BillPaymentExportable extends Exportable {
@@ -15,11 +14,11 @@ export class BillPaymentExportable extends Exportable {
*/
public exportable(tenantId: number, query: any) {
const parsedQuery = {
page: 1,
pageSize: 12,
...query,
sortOrder: 'desc',
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
} as any;
return this.billPaymentsApplication

View File

@@ -2,7 +2,6 @@ import { Inject, Service } from 'typedi';
import { IBillsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { BillsApplication } from './BillsApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class BillsExportable extends Exportable {
@@ -20,7 +19,7 @@ export class BillsExportable extends Exportable {
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
pageSize: 12000,
} as IBillsFilter;
return this.billsApplication

View File

@@ -2,7 +2,6 @@ import { Inject, Service } from 'typedi';
import { ISalesInvoicesFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { SaleEstimatesApplication } from './SaleEstimatesApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class SaleEstimatesExportable extends Exportable {
@@ -20,7 +19,7 @@ export class SaleEstimatesExportable extends Exportable {
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
pageSize: 12000,
} as ISalesInvoicesFilter;
return this.saleEstimatesApplication

View File

@@ -44,7 +44,7 @@ export class SaleInvoiceGLEntries {
// Find or create the A/R account.
const ARAccount = await accountRepository.findOrCreateAccountReceivable(
saleInvoice.currencyCode, {}, trx
saleInvoice.currencyCode
);
// Find or create tax payable account.
const taxPayableAccount = await accountRepository.findOrCreateTaxPayable(

View File

@@ -32,8 +32,7 @@ export class InvoiceInventoryTransactions {
const inventoryEntries =
await this.itemsEntriesService.filterInventoryEntries(
tenantId,
saleInvoice.entries,
trx
saleInvoice.entries
);
const transaction = {
transactionId: saleInvoice.id,

View File

@@ -2,7 +2,6 @@ import { Inject, Service } from 'typedi';
import { ISalesInvoicesFilter } from '@/interfaces';
import { SaleInvoiceApplication } from './SaleInvoicesApplication';
import { Exportable } from '@/services/Export/Exportable';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class SaleInvoicesExportable extends Exportable {
@@ -20,7 +19,7 @@ export class SaleInvoicesExportable extends Exportable {
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
pageSize: 120000,
} as ISalesInvoicesFilter;
return this.saleInvoicesApplication

View File

@@ -1,4 +1,3 @@
import { Mutex } from 'async-mutex';
import { Container, Service, Inject } from 'typedi';
import { chain } from 'lodash';
import moment from 'moment';
@@ -35,26 +34,17 @@ export class SaleInvoicesCost {
inventoryItemsIds: number[],
startingDate: Date
): Promise<void> {
const mutex = new Mutex();
const asyncOpers: Promise<[]>[] = [];
const asyncOpers = inventoryItemsIds.map(
async (inventoryItemId: number) => {
// @todo refactor the lock acquire to be distrbuted using Redis
// and run the cost schedule job after running invoice transaction.
const release = await mutex.acquire();
try {
await this.inventoryService.scheduleComputeItemCost(
tenantId,
inventoryItemId,
startingDate
);
} finally {
release();
}
}
);
await Promise.all(asyncOpers);
inventoryItemsIds.forEach((inventoryItemId: number) => {
const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost(
tenantId,
inventoryItemId,
startingDate
);
asyncOpers.push(oper);
});
await Promise.all([...asyncOpers]);
}
/**
@@ -96,22 +86,17 @@ export class SaleInvoicesCost {
tenantId: number,
inventoryTransactions: IInventoryTransaction[]
) {
const mutex = new Mutex();
const asyncOpers: Promise<[]>[] = [];
const reducedTransactions = this.getMaxDateInventoryTransactions(
inventoryTransactions
);
const asyncOpers = reducedTransactions.map(async (transaction) => {
const release = await mutex.acquire();
try {
await this.inventoryService.scheduleComputeItemCost(
tenantId,
transaction.itemId,
transaction.date
);
} finally {
release();
}
reducedTransactions.forEach((transaction) => {
const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost(
tenantId,
transaction.itemId,
transaction.date
);
asyncOpers.push(oper);
});
await Promise.all([...asyncOpers]);
}

View File

@@ -2,7 +2,6 @@ import { Inject, Service } from 'typedi';
import { IAccountsStructureType, IPaymentsReceivedFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { PaymentReceivesApplication } from './PaymentReceivedApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class PaymentsReceivedExportable extends Exportable {
@@ -22,8 +21,6 @@ export class PaymentsReceivedExportable extends Exportable {
inactiveMode: false,
...query,
structure: IAccountsStructureType.Flat,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
} as IPaymentsReceivedFilter;
return this.paymentReceivedApp

View File

@@ -2,7 +2,6 @@ import { Inject, Service } from 'typedi';
import { ISalesReceiptsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { SaleReceiptApplication } from './SaleReceiptApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class SaleReceiptsExportable extends Exportable {
@@ -20,7 +19,7 @@ export class SaleReceiptsExportable extends Exportable {
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
pageSize: 12,
} as ISalesReceiptsFilter;
return this.saleReceiptsApp

View File

@@ -4,8 +4,8 @@ import { configureLemonSqueezy } from './utils';
import { PlanSubscription } from '@/system/models';
import { ServiceError } from '@/exceptions';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { ERRORS, IOrganizationSubscriptionCancel } from './types';
import events from '@/subscribers/events';
import { ERRORS, IOrganizationSubscriptionCanceled } from './types';
@Service()
export class LemonCancelSubscription {
@@ -18,15 +18,12 @@ export class LemonCancelSubscription {
* @param {number} subscriptionId
* @returns {Promise<void>}
*/
public async cancelSubscription(
tenantId: number,
subscriptionSlug: string = 'main'
) {
public async cancelSubscription(tenantId: number) {
configureLemonSqueezy();
const subscription = await PlanSubscription.query().findOne({
tenantId,
slug: subscriptionSlug,
slug: 'main',
});
if (!subscription) {
throw new ServiceError(ERRORS.SUBSCRIPTION_ID_NOT_ASSOCIATED_TO_TENANT);
@@ -38,10 +35,13 @@ export class LemonCancelSubscription {
if (cancelledSub.error) {
throw new Error(cancelledSub.error.message);
}
// Triggers `onSubscriptionCancelled` event.
await PlanSubscription.query().findById(subscriptionId).patch({
canceledAt: new Date(),
});
// Triggers `onSubscriptionCanceled` event.
await this.eventPublisher.emitAsync(
events.subscription.onSubscriptionCancel,
{ tenantId, subscriptionId } as IOrganizationSubscriptionCancel
events.subscription.onSubscriptionCanceled,
{ tenantId, subscriptionId } as IOrganizationSubscriptionCanceled
);
}
}

Some files were not shown because too many files have changed in this diff Show More