feat: one-click demo account

This commit is contained in:
Ahmed Bouhuolia
2024-08-22 19:21:23 +02:00
parent 59f480f9d5
commit 4a99f6c0cf
15 changed files with 83 additions and 43 deletions

View File

@@ -1,11 +1,21 @@
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 Logger = Container.get('logger');
const settings = await initializeTenantSettings(tenantId);
req.settings = settings;
res.on('finish', async () => {
await settings.save();
});
next();
}
export const initializeTenantSettings = async (tenantId: number) => {
const tenantContainer = Container.of(`tenant-${tenantId}`);
if (tenantContainer && !tenantContainer.has('settings')) {
@@ -18,10 +28,5 @@ export default async (req: Request, res: Response, next: NextFunction) => {
await settings.load();
req.settings = settings;
res.on('finish', async () => {
await settings.save();
});
next();
return settings;
}

View File

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

View File

@@ -106,6 +106,9 @@ export class TriggerRecognizedTransactions {
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

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

View File

@@ -19,7 +19,7 @@ export class ImportFilePreview {
*/
public async preview(
tenantId: number,
importId: number
importId: string
): Promise<ImportFilePreviewPOJO> {
const knex = this.tenancy.knex(tenantId);
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });

View File

@@ -37,7 +37,7 @@ export class ImportFileProcess {
*/
public async import(
tenantId: number,
importId: number,
importId: string,
trx?: Knex.Transaction
): Promise<ImportFilePreviewPOJO> {
const importFile = await Import.query()

View File

@@ -67,7 +67,7 @@ export class ImportResourceApplication {
* @param {number} importId - Import id.
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async preview(tenantId: number, importId: number) {
public async preview(tenantId: number, importId: string) {
return this.ImportFilePreviewService.preview(tenantId, importId);
}

View File

@@ -5,6 +5,7 @@ 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',
@@ -58,13 +59,14 @@ export default class ItemsEntriesService {
*/
public async filterInventoryEntries(
tenantId: number,
entries: IItemEntry[]
entries: IItemEntry[],
trx?: Knex.Transaction
): Promise<IItemEntry[]> {
const { Item } = this.tenancy.models(tenantId);
const entriesItemsIds = entries.map((e) => e.itemId);
// Retrieve entries inventory items.
const inventoryItems = await Item.query()
const inventoryItems = await Item.query(trx)
.whereIn('id', entriesItemsIds)
.where('type', 'inventory');

View File

@@ -7,7 +7,9 @@ import { OneClickDemo } from '@/system/models/OneclickDemo';
import { SystemUser } from '@/system/models';
import { IAuthSignInPOJO } from '@/interfaces';
import { ICreateOneClickDemoPOJO } from './interfaces';
import { initalizeTenantServices } from '@/api/middleware/TenantDependencyInjection';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { defaultDemoOrganizationDTO } from './_constants';
@Service()
export class CreateOneClickDemo {
@@ -17,6 +19,10 @@ export class CreateOneClickDemo {
@Inject()
private organizationService: OrganizationService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Creates one-click demo account.
* @returns {Promise<ICreateOneClickDemoPOJO>}
@@ -34,22 +40,12 @@ export class CreateOneClickDemo {
const tenantId = signedIn.tenant.id;
const userId = signedIn.user.id;
// Injects the given tenant IoC services.
await initalizeTenantServices(tenantId);
// Creates a new one-click demo.
await OneClickDemo.query().insert({ key: demoId, tenantId, userId });
const buildJob = await this.organizationService.buildRunJob(
tenantId,
{
name: 'BIGCAPITAL, INC',
base_currency: 'USD',
location: 'US',
language: 'en',
fiscal_year: 'march',
timezone: 'US/Central',
},
defaultDemoOrganizationDTO,
signedIn.user
);
return { email, demoId, signedIn, buildJob };

View File

@@ -0,0 +1,12 @@
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,9 +1,11 @@
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 { IOrganizationBuildEventPayload } from '@/interfaces';
import { IOrganizationBuiltEventPayload } from '@/interfaces';
import { SeedDemoAccountItems } from '../DemoSeeders/SeedDemoItems';
import { ImportResourceApplication } from '@/services/Import/ImportResourceApplication';
import { getImportsStoragePath } from '@/services/Import/_utils';
@@ -24,7 +26,7 @@ export class SeedInitialDemoAccountDataOnOrgBuild {
*/
public attach = (bus) => {
bus.subscribe(
events.organization.build,
events.organization.built,
this.seedInitialDemoAccountDataOnOrgBuild.bind(this)
);
};
@@ -35,12 +37,12 @@ export class SeedInitialDemoAccountDataOnOrgBuild {
get seedDemoAccountSeeders() {
return [
SeedDemoAccountItems,
SeedDemoBankTransactions,
SeedDemoAccountCustomers,
SeedDemoAccountVendors,
SeedDemoAccountManualJournals,
SeedDemoBankTransactions,
SeedDemoAccountExpenses,
SeedDemoSaleInvoices,
SeedDemoAccountExpenses,
];
}
@@ -50,11 +52,14 @@ export class SeedInitialDemoAccountDataOnOrgBuild {
* @returns {Promise<void>}
*/
async initiateSeederFile(fileName: string) {
const destination = path.join(getImportsStoragePath(), fileName);
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;
}
/**
@@ -63,7 +68,7 @@ export class SeedInitialDemoAccountDataOnOrgBuild {
*/
async seedInitialDemoAccountDataOnOrgBuild({
tenantId,
}: IOrganizationBuildEventPayload) {
}: IOrganizationBuiltEventPayload) {
const foundDemo = await OneClickDemo.query().findOne('tenantId', tenantId);
// Can't continue if the found demo is not exists.
@@ -77,14 +82,14 @@ export class SeedInitialDemoAccountDataOnOrgBuild {
const seederInstance = new SeedDemoAccountSeeder();
// Initialize the seeder sheet file before importing.
await this.initiateSeederFile(seederInstance.importFileName);
const importFileName = await this.initiateSeederFile(seederInstance.importFileName);
// Import the given seeder file.
const importedFile = await this.importApp.import(
tenantId,
seederInstance.resource,
seederInstance.importFileName,
seederInstance.importParams || {}
importFileName,
seederInstance.importParams
);
// Mapping the columns with resource fields.
await this.importApp.mapping(
@@ -92,18 +97,16 @@ export class SeedInitialDemoAccountDataOnOrgBuild {
importedFile.import.importId,
seederInstance.mapping
);
await this.importApp.preview(tenantId, importedFile.import.importId);
// Commit the imported file.
const re = await this.importApp.process(
await this.importApp.process(
tenantId,
importedFile.import.importId
);
console.log(re);
});
console.error(results.errors);
console.log(results.results);
if (results.errors) {
if (!isEmpty(results.errors)) {
throw results.errors;
}
}

View File

@@ -5,6 +5,7 @@ import { ServiceError } from '@/exceptions';
import {
IOrganizationBuildDTO,
IOrganizationBuildEventPayload,
IOrganizationBuiltEventPayload,
IOrganizationUpdateDTO,
ISystemUser,
ITenant,
@@ -17,6 +18,8 @@ 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 {
@@ -62,6 +65,10 @@ 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);
@@ -82,6 +89,11 @@ 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

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

View File

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

View File

@@ -35,6 +35,8 @@ export default {
*/
organization: {
build: 'onOrganizationBuild',
built: 'onOrganizationBuilt',
seeded: 'onOrganizationSeeded',
baseCurrencyUpdated: 'onOrganizationBaseCurrencyUpdated',