From 4a99f6c0cf3e09845e52a4ef6580e3c8246cac03 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 22 Aug 2024 19:21:23 +0200 Subject: [PATCH] feat: one-click demo account --- .../src/api/middleware/SettingsMiddleware.ts | 21 +++++++----- packages/server/src/interfaces/Setup.ts | 4 +++ .../events/TriggerRecognizedTransactions.ts | 3 ++ .../UncategorizedTransactionsImportable.ts | 2 +- .../src/services/Import/ImportFilePreview.ts | 2 +- .../src/services/Import/ImportFileProcess.ts | 2 +- .../Import/ImportResourceApplication.ts | 2 +- .../src/services/Items/ItemsEntriesService.ts | 6 ++-- .../OneClickDemo/CreateOneClickDemo.ts | 20 +++++------ .../src/services/OneClickDemo/_constants.ts | 12 +++++++ .../events/SeedInitialDemoAccountData.ts | 33 ++++++++++--------- .../Organization/OrganizationService.ts | 12 +++++++ .../Sales/Invoices/InvoiceGLEntries.ts | 2 +- .../Invoices/InvoiceInventoryTransactions.ts | 3 +- packages/server/src/subscribers/events.ts | 2 ++ 15 files changed, 83 insertions(+), 43 deletions(-) create mode 100644 packages/server/src/services/OneClickDemo/_constants.ts diff --git a/packages/server/src/api/middleware/SettingsMiddleware.ts b/packages/server/src/api/middleware/SettingsMiddleware.ts index fbb49b475..9832993b5 100644 --- a/packages/server/src/api/middleware/SettingsMiddleware.ts +++ b/packages/server/src/api/middleware/SettingsMiddleware.ts @@ -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; } \ No newline at end of file diff --git a/packages/server/src/interfaces/Setup.ts b/packages/server/src/interfaces/Setup.ts index 4f443bb8c..da776b89e 100644 --- a/packages/server/src/interfaces/Setup.ts +++ b/packages/server/src/interfaces/Setup.ts @@ -33,3 +33,7 @@ export interface IOrganizationBuildEventPayload { buildDTO: IOrganizationBuildDTO; systemUser: ISystemUser; } + +export interface IOrganizationBuiltEventPayload { + tenantId: number; +} \ No newline at end of file diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions.ts b/packages/server/src/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions.ts index 466f0b3b1..eba643481 100644 --- a/packages/server/src/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions.ts +++ b/packages/server/src/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions.ts @@ -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); } } diff --git a/packages/server/src/services/Cashflow/UncategorizedTransactionsImportable.ts b/packages/server/src/services/Cashflow/UncategorizedTransactionsImportable.ts index 6350dc406..243affa24 100644 --- a/packages/server/src/services/Cashflow/UncategorizedTransactionsImportable.ts +++ b/packages/server/src/services/Cashflow/UncategorizedTransactionsImportable.ts @@ -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} parmas */ public transformParams(parmas: Record) { diff --git a/packages/server/src/services/Import/ImportFilePreview.ts b/packages/server/src/services/Import/ImportFilePreview.ts index 56a1df09e..b25c00a0e 100644 --- a/packages/server/src/services/Import/ImportFilePreview.ts +++ b/packages/server/src/services/Import/ImportFilePreview.ts @@ -19,7 +19,7 @@ export class ImportFilePreview { */ public async preview( tenantId: number, - importId: number + importId: string ): Promise { const knex = this.tenancy.knex(tenantId); const trx = await knex.transaction({ isolationLevel: 'read uncommitted' }); diff --git a/packages/server/src/services/Import/ImportFileProcess.ts b/packages/server/src/services/Import/ImportFileProcess.ts index 7d0548dc9..06b99a889 100644 --- a/packages/server/src/services/Import/ImportFileProcess.ts +++ b/packages/server/src/services/Import/ImportFileProcess.ts @@ -37,7 +37,7 @@ export class ImportFileProcess { */ public async import( tenantId: number, - importId: number, + importId: string, trx?: Knex.Transaction ): Promise { const importFile = await Import.query() diff --git a/packages/server/src/services/Import/ImportResourceApplication.ts b/packages/server/src/services/Import/ImportResourceApplication.ts index 97d8a00b2..6e000804c 100644 --- a/packages/server/src/services/Import/ImportResourceApplication.ts +++ b/packages/server/src/services/Import/ImportResourceApplication.ts @@ -67,7 +67,7 @@ export class ImportResourceApplication { * @param {number} importId - Import id. * @returns {Promise} */ - public async preview(tenantId: number, importId: number) { + public async preview(tenantId: number, importId: string) { return this.ImportFilePreviewService.preview(tenantId, importId); } diff --git a/packages/server/src/services/Items/ItemsEntriesService.ts b/packages/server/src/services/Items/ItemsEntriesService.ts index 1b97f6fa0..f307087b2 100644 --- a/packages/server/src/services/Items/ItemsEntriesService.ts +++ b/packages/server/src/services/Items/ItemsEntriesService.ts @@ -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 { 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'); diff --git a/packages/server/src/services/OneClickDemo/CreateOneClickDemo.ts b/packages/server/src/services/OneClickDemo/CreateOneClickDemo.ts index 787c4b019..687bb2032 100644 --- a/packages/server/src/services/OneClickDemo/CreateOneClickDemo.ts +++ b/packages/server/src/services/OneClickDemo/CreateOneClickDemo.ts @@ -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} @@ -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 }; diff --git a/packages/server/src/services/OneClickDemo/_constants.ts b/packages/server/src/services/OneClickDemo/_constants.ts new file mode 100644 index 000000000..27faecc17 --- /dev/null +++ b/packages/server/src/services/OneClickDemo/_constants.ts @@ -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', +} \ No newline at end of file diff --git a/packages/server/src/services/OneClickDemo/events/SeedInitialDemoAccountData.ts b/packages/server/src/services/OneClickDemo/events/SeedInitialDemoAccountData.ts index 576248a1b..4e18f2a39 100644 --- a/packages/server/src/services/OneClickDemo/events/SeedInitialDemoAccountData.ts +++ b/packages/server/src/services/OneClickDemo/events/SeedInitialDemoAccountData.ts @@ -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} */ 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; } } diff --git a/packages/server/src/services/Organization/OrganizationService.ts b/packages/server/src/services/Organization/OrganizationService.ts index 883db3ce5..6d7fa1621 100644 --- a/packages/server/src/services/Organization/OrganizationService.ts +++ b/packages/server/src/services/Organization/OrganizationService.ts @@ -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) } /** diff --git a/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts b/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts index 7b714e70e..61c575f67 100644 --- a/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts +++ b/packages/server/src/services/Sales/Invoices/InvoiceGLEntries.ts @@ -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( diff --git a/packages/server/src/services/Sales/Invoices/InvoiceInventoryTransactions.ts b/packages/server/src/services/Sales/Invoices/InvoiceInventoryTransactions.ts index 16cbb1b0f..4a6618727 100644 --- a/packages/server/src/services/Sales/Invoices/InvoiceInventoryTransactions.ts +++ b/packages/server/src/services/Sales/Invoices/InvoiceInventoryTransactions.ts @@ -32,7 +32,8 @@ export class InvoiceInventoryTransactions { const inventoryEntries = await this.itemsEntriesService.filterInventoryEntries( tenantId, - saleInvoice.entries + saleInvoice.entries, + trx ); const transaction = { transactionId: saleInvoice.id, diff --git a/packages/server/src/subscribers/events.ts b/packages/server/src/subscribers/events.ts index 2299df01b..478d336eb 100644 --- a/packages/server/src/subscribers/events.ts +++ b/packages/server/src/subscribers/events.ts @@ -35,6 +35,8 @@ export default { */ organization: { build: 'onOrganizationBuild', + built: 'onOrganizationBuilt', + seeded: 'onOrganizationSeeded', baseCurrencyUpdated: 'onOrganizationBaseCurrencyUpdated',