diff --git a/packages/server/src/api/middleware/TenantDependencyInjection.ts b/packages/server/src/api/middleware/TenantDependencyInjection.ts index a635ad254..56c38cf43 100644 --- a/packages/server/src/api/middleware/TenantDependencyInjection.ts +++ b/packages/server/src/api/middleware/TenantDependencyInjection.ts @@ -4,6 +4,7 @@ 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; @@ -16,7 +17,7 @@ export default (req: Request, tenant: ITenant) => { const tenantContainer = tenantServices.tenantContainer(tenantId); - tenantContainer.set('i18n', injectI18nUtils(req)); + tenantContainer.set('i18n', injectI18nUtils()); const knexInstance = tenantServices.knex(tenantId); const models = tenantServices.models(tenantId); @@ -33,14 +34,35 @@ export default (req: Request, tenant: ITenant) => { }; export const injectI18nUtils = (req) => { - const locale = req.getLocale(); + const globalI18n = Container.get('i18n'); + const locale = globalI18n.getLocale(); const direction = rtlDetect.getLangDir(locale); return { locale, - __: req.__, + __: globalI18n.__, 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); +}; diff --git a/packages/server/src/loaders/eventEmitter.ts b/packages/server/src/loaders/eventEmitter.ts index 5a5b02d03..fa58b4ca6 100644 --- a/packages/server/src/loaders/eventEmitter.ts +++ b/packages/server/src/loaders/eventEmitter.ts @@ -116,6 +116,7 @@ import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashfl 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'; export default () => { return new EventPublisher(); @@ -282,5 +283,8 @@ export const susbcribers = () => { // Loops LoopsEventsSubscriber + + // Demo Account + SeedInitialDemoAccountDataOnOrgBuild ]; }; diff --git a/packages/server/src/loaders/i18n.ts b/packages/server/src/loaders/i18n.ts index 42706fb5b..3d703b0c9 100644 --- a/packages/server/src/loaders/i18n.ts +++ b/packages/server/src/loaders/i18n.ts @@ -2,6 +2,7 @@ import { I18n } from 'i18n'; export default () => new I18n({ locales: ['en', 'ar'], + defaultLocale: 'en', register: global, directory: global.__locales_dir, updateFiles: false, diff --git a/packages/server/src/services/Import/ImportFileMapping.ts b/packages/server/src/services/Import/ImportFileMapping.ts index 036fde757..62098bc6b 100644 --- a/packages/server/src/services/Import/ImportFileMapping.ts +++ b/packages/server/src/services/Import/ImportFileMapping.ts @@ -23,7 +23,7 @@ export class ImportFileMapping { */ public async mapping( tenantId: number, - importId: number, + importId: string, maps: ImportMappingAttr[] ): Promise { const importFile = await Import.query() diff --git a/packages/server/src/services/Import/ImportFileProcessCommit.ts b/packages/server/src/services/Import/ImportFileProcessCommit.ts index 14c229fec..689b956c7 100644 --- a/packages/server/src/services/Import/ImportFileProcessCommit.ts +++ b/packages/server/src/services/Import/ImportFileProcessCommit.ts @@ -25,7 +25,7 @@ export class ImportFileProcessCommit { */ public async commit( 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/ImportResourceApplication.ts b/packages/server/src/services/Import/ImportResourceApplication.ts index 34df746c6..97d8a00b2 100644 --- a/packages/server/src/services/Import/ImportResourceApplication.ts +++ b/packages/server/src/services/Import/ImportResourceApplication.ts @@ -55,7 +55,7 @@ export class ImportResourceApplication { */ public async mapping( tenantId: number, - importId: number, + importId: string, maps: ImportMappingAttr[] ) { return this.importMappingService.mapping(tenantId, importId, maps); @@ -77,7 +77,7 @@ export class ImportResourceApplication { * @param {number} importId * @returns {Promise} */ - public async process(tenantId: number, importId: number) { + public async process(tenantId: number, importId: string) { return this.importProcessCommit.commit(tenantId, importId); } diff --git a/packages/server/src/services/OneClickDemo/CreateOneClickDemo.ts b/packages/server/src/services/OneClickDemo/CreateOneClickDemo.ts index c167cda04..787c4b019 100644 --- a/packages/server/src/services/OneClickDemo/CreateOneClickDemo.ts +++ b/packages/server/src/services/OneClickDemo/CreateOneClickDemo.ts @@ -7,6 +7,7 @@ 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'; @Service() export class CreateOneClickDemo { @@ -33,6 +34,9 @@ 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 }); diff --git a/packages/server/src/services/OneClickDemo/DemoSeeders/SeedDemoAbstract.ts b/packages/server/src/services/OneClickDemo/DemoSeeders/SeedDemoAbstract.ts new file mode 100644 index 000000000..f078485f2 --- /dev/null +++ b/packages/server/src/services/OneClickDemo/DemoSeeders/SeedDemoAbstract.ts @@ -0,0 +1,6 @@ + + + +export class SeedDemoAbstract{ + +} \ No newline at end of file diff --git a/packages/server/src/services/OneClickDemo/DemoSeeders/SeedDemoItems.ts b/packages/server/src/services/OneClickDemo/DemoSeeders/SeedDemoItems.ts new file mode 100644 index 000000000..7542b8474 --- /dev/null +++ b/packages/server/src/services/OneClickDemo/DemoSeeders/SeedDemoItems.ts @@ -0,0 +1,42 @@ +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: 'cost_price' }, + { from: 'Cost Account', to: 'costAccount' }, + { from: 'Sell Account', to: 'sellAccount' }, + { from: 'Inventory Account', to: 'inventoryAccount' }, + { 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'; + } +} diff --git a/packages/server/src/services/OneClickDemo/events/SeedInitialDemoAccountData.ts b/packages/server/src/services/OneClickDemo/events/SeedInitialDemoAccountData.ts new file mode 100644 index 000000000..bd4e0a3d5 --- /dev/null +++ b/packages/server/src/services/OneClickDemo/events/SeedInitialDemoAccountData.ts @@ -0,0 +1,85 @@ +import { Inject } from 'typedi'; +import { promises as fs } from 'fs'; +import path from 'path'; +import events from '@/subscribers/events'; +import { PromisePool } from '@supercharge/promise-pool'; +import { IOrganizationBuildEventPayload } 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'; + +export class SeedInitialDemoAccountDataOnOrgBuild { + @Inject() + private importApp: ImportResourceApplication; + + /** + * Attaches events with handlers. + */ + public attach = (bus) => { + bus.subscribe( + events.organization.build, + this.seedInitialDemoAccountDataOnOrgBuild.bind(this) + ); + }; + + /** + * Demo account seeder. + */ + get seedDemoAccountSeeders() { + return [SeedDemoAccountItems]; + } + + /** + * Initialize the seeder sheet file to the import storage first. + * @param {string} fileName - + * @returns {Promise} + */ + async initiateSeederFile(fileName: string) { + const destination = path.join(getImportsStoragePath(), fileName); + const source = path.join(global.__views_dir, `/demo-sheets`, fileName); + + // Use the fs.promises.copyFile method to copy the file + await fs.copyFile(source, destination); + } + + /** + * Seeds initial demo account data on organization build + * @param {IOrganizationBuildEventPayload} + */ + async seedInitialDemoAccountDataOnOrgBuild({ + tenantId, + }: IOrganizationBuildEventPayload) { + 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(); + + await this.initiateSeederFile(seederInstance.importFileName); + + const importedFile = await this.importApp.import( + tenantId, + seederInstance.resource, + seederInstance.importFileName, + {} + ); + await this.importApp.mapping( + tenantId, + importedFile.import.importId, + seederInstance.mapping + ); + await this.importApp.process(tenantId, importedFile.import.importId); + }); + + if (results.errors) { + throw results.errors; + } + } +} diff --git a/packages/server/views/demo-sheets/items.csv b/packages/server/views/demo-sheets/items.csv new file mode 100644 index 000000000..754ed4d4c --- /dev/null +++ b/packages/server/views/demo-sheets/items.csv @@ -0,0 +1,5 @@ +Item Type,Item Name,Item Code,Sellable,Purchasable,Cost Price,Sell Price,Cost Account,Sell Account,Inventory Account,Sell Description,Purchase Description,Category,Note,Active +Inventory,"Hettinger, Schumm and Bartoletti",1000,T,T,10000,1000,Cost of Goods Sold,Other Income,Inventory Asset,Description ....,Description ....,sdafasdfsadf,At dolor est non tempore et quisquam.,TRUE +Inventory,Schmitt Group,1001,T,T,10000,1000,Cost of Goods Sold,Other Income,Inventory Asset,Description ....,Description ....,sdafasdfsadf,Id perspiciatis at adipisci minus accusamus dolor iure dolore.,TRUE +Inventory,Marks - Carroll,1002,T,T,10000,1000,Cost of Goods Sold,Other Income,Inventory Asset,Description ....,Description ....,sdafasdfsadf,Odio odio minus similique.,TRUE +Inventory,"VonRueden, Ruecker and Hettinger",1003,T,T,10000,1000,Cost of Goods Sold,Other Income,Inventory Asset,Description ....,Description ....,sdafasdfsadf,Quibusdam dolores illo.,TRUE \ No newline at end of file