From ad00f140d147ce0bc8ee96e7d04ca605fdee6641 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Wed, 9 Sep 2020 21:30:19 +0200 Subject: [PATCH] feat: licenses administration basic authentication. feat: accounts slug. feat: duplicate accounts_balance table and merge balance with accounts table. feat: refactoring customers and vendors. feat: system user soft deleting. feat: preventing build tenant database without any subscription. feat: remove 'password' property from 'req.user' object. feat: refactoring JournalPoster class. feat: delete duplicated directories and files. --- server/config/config.js | 5 + .../20190822214304_create_accounts_table.js | 3 + ...0822214305_create_account_balance_table.js | 11 - ...2647_create_accounts_transactions_table.js | 1 + .../20200607212203_create_contacts_table.js | 49 ++ server/src/database/seeds/seed_accounts.js | 14 + server/src/http/controllers/Accounting.js | 91 +--- server/src/http/controllers/Accounts.ts | 78 +-- .../src/http/controllers/Contacts/Contacts.ts | 70 +++ .../http/controllers/Contacts/Customers.ts | 196 +++++++ .../src/http/controllers/Contacts/Vendors.ts | 195 +++++++ server/src/http/controllers/Customers.js | 424 --------------- server/src/http/controllers/OAuth2.js | 23 - server/src/http/controllers/Organization.ts | 25 +- .../http/controllers/Subscription/Licenses.ts | 11 +- server/src/http/controllers/Users.ts | 6 +- server/src/http/controllers/Vendors.js | 377 ------------- server/src/http/index.ts | 12 +- .../middleware/AttachCurrentTenantUser.ts | 2 + .../src/http/middleware/TenancyMiddleware.ts | 12 +- server/src/interfaces/Contact.ts | 170 ++++++ server/src/interfaces/Journal.ts | 40 ++ server/src/interfaces/index.ts | 30 ++ server/src/loaders/tenantModels.ts | 14 +- server/src/loaders/tenantRepositories.ts | 14 + server/src/models/Account.js | 49 +- server/src/models/AccountBalance.js | 29 - server/src/models/AccountTransaction.js | 14 +- server/src/models/AccountType.js | 3 +- server/src/models/Bill.js | 8 +- server/src/models/BillPayment.js | 7 +- server/src/models/Contact.js | 124 +++++ server/src/models/PaymentReceive.js | 7 +- server/src/models/PaymentReceiveEntry.js | 12 +- server/src/models/Resource.js | 13 +- server/src/models/SaleEstimate.js | 4 +- server/src/models/SaleEstimateEntry.js | 21 +- server/src/models/SaleInvoice.js | 5 +- server/src/models/SaleInvoiceEntry.js | 21 +- server/src/models/SaleReceipt.js | 5 +- server/src/models/SaleReceiptEntry.js | 20 +- server/src/models/Setting.js | 7 - server/src/models/TenantModel.js | 12 - server/src/models/Vendor.js | 2 +- server/src/models/View.js | 11 +- server/src/models/ViewColumn.js | 8 - server/src/models/ViewRole.js | 7 - server/src/repositories/AccountRepository.ts | 55 ++ .../src/repositories/AccountTypeRepository.ts | 30 ++ server/src/repositories/CustomerRepository.ts | 71 +++ server/src/repositories/TenantRepository.ts | 16 + server/src/repositories/VendorRepository.ts | 50 ++ server/src/server.js | 10 +- server/src/services/Accounting/Accounting.js | 0 .../services/Accounting/JournalCommands.ts | 73 ++- .../{JournalEntry.js => JournalEntry.ts} | 0 .../services/Accounting/JournalFinancial.ts | 207 ++++++++ .../src/services/Accounting/JournalPoster.js | 502 ------------------ .../src/services/Accounting/JournalPoster.ts | 337 ++++++++++++ .../src/services/Accounts/AccountsService.ts | 2 + server/src/services/Authentication/index.ts | 4 + .../src/services/Cache/{index.js => index.ts} | 21 +- .../src/services/Contacts/ContactsService.ts | 131 +++++ .../src/services/Contacts/CustomersService.ts | 171 ++++++ .../src/services/Contacts/VendorsService.ts | 167 ++++++ .../services/Customers/CustomersService.js | 10 - server/src/services/Items/ItemsService.ts | 4 + server/src/services/Purchases/BillPayments.ts | 4 +- server/src/services/Purchases/Bills.ts | 54 +- server/src/services/Sales/SalesInvoices.ts | 8 +- server/src/services/Tenancy/TenancyService.ts | 29 +- server/src/services/Users/UsersService.ts | 11 +- server/src/services/Vendors/VendorsService.js | 15 - .../20190822214242_create_users_table.js | 2 + server/src/system/models/SubscriptionPlan.js | 10 - server/src/system/models/SystemOption.js | 11 - server/src/system/models/SystemUser.js | 12 +- 77 files changed, 2431 insertions(+), 1848 deletions(-) delete mode 100644 server/src/database/migrations/20190822214305_create_account_balance_table.js create mode 100644 server/src/database/migrations/20200607212203_create_contacts_table.js create mode 100644 server/src/http/controllers/Contacts/Contacts.ts create mode 100644 server/src/http/controllers/Contacts/Customers.ts create mode 100644 server/src/http/controllers/Contacts/Vendors.ts delete mode 100644 server/src/http/controllers/Customers.js delete mode 100644 server/src/http/controllers/OAuth2.js delete mode 100644 server/src/http/controllers/Vendors.js create mode 100644 server/src/interfaces/Contact.ts create mode 100644 server/src/interfaces/Journal.ts create mode 100644 server/src/loaders/tenantRepositories.ts delete mode 100644 server/src/models/AccountBalance.js create mode 100644 server/src/models/Contact.js create mode 100644 server/src/repositories/AccountRepository.ts create mode 100644 server/src/repositories/AccountTypeRepository.ts create mode 100644 server/src/repositories/CustomerRepository.ts create mode 100644 server/src/repositories/TenantRepository.ts create mode 100644 server/src/repositories/VendorRepository.ts delete mode 100644 server/src/services/Accounting/Accounting.js rename server/src/services/Accounting/{JournalEntry.js => JournalEntry.ts} (100%) create mode 100644 server/src/services/Accounting/JournalFinancial.ts delete mode 100644 server/src/services/Accounting/JournalPoster.js create mode 100644 server/src/services/Accounting/JournalPoster.ts rename server/src/services/Cache/{index.js => index.ts} (75%) create mode 100644 server/src/services/Contacts/ContactsService.ts create mode 100644 server/src/services/Contacts/CustomersService.ts create mode 100644 server/src/services/Contacts/VendorsService.ts delete mode 100644 server/src/services/Customers/CustomersService.js delete mode 100644 server/src/services/Vendors/VendorsService.js delete mode 100644 server/src/system/models/SubscriptionPlan.js delete mode 100644 server/src/system/models/SystemOption.js diff --git a/server/config/config.js b/server/config/config.js index 3528ecc9e..5ad33c6ed 100644 --- a/server/config/config.js +++ b/server/config/config.js @@ -81,4 +81,9 @@ module.exports = { prefix: '/api' }, resetPasswordSeconds: 600, + + licensesAuth: { + user: 'admin', + password: 'admin', + } }; diff --git a/server/src/database/migrations/20190822214304_create_accounts_table.js b/server/src/database/migrations/20190822214304_create_accounts_table.js index bb323222e..fe188a06b 100644 --- a/server/src/database/migrations/20190822214304_create_accounts_table.js +++ b/server/src/database/migrations/20190822214304_create_accounts_table.js @@ -3,6 +3,7 @@ exports.up = function (knex) { return knex.schema.createTable('accounts', (table) => { table.bigIncrements('id').comment('Auto-generated id');; table.string('name'); + table.string('slug'); table.integer('account_type_id').unsigned(); table.integer('parent_account_id').unsigned(); table.string('code', 10); @@ -10,6 +11,8 @@ exports.up = function (knex) { table.boolean('active').defaultTo(true); table.integer('index').unsigned(); table.boolean('predefined').defaultTo(false); + table.decimal('amount', 15, 5); + table.string('currency_code', 3); table.timestamps(); }).raw('ALTER TABLE `ACCOUNTS` AUTO_INCREMENT = 1000').then(() => { return knex.seed.run({ diff --git a/server/src/database/migrations/20190822214305_create_account_balance_table.js b/server/src/database/migrations/20190822214305_create_account_balance_table.js deleted file mode 100644 index 86fe5e8ed..000000000 --- a/server/src/database/migrations/20190822214305_create_account_balance_table.js +++ /dev/null @@ -1,11 +0,0 @@ - -exports.up = function (knex) { - return knex.schema.createTable('account_balances', (table) => { - table.increments(); - table.integer('account_id'); - table.decimal('amount', 15, 5); - table.string('currency_code', 3); - }); -}; - -exports.down = (knex) => knex.schema.dropTableIfExists('account_balances'); diff --git a/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js b/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js index 38e8bb90a..bc02d6b4a 100644 --- a/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js +++ b/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js @@ -13,6 +13,7 @@ exports.up = function(knex) { table.string('note'); table.boolean('draft').defaultTo(false); table.integer('user_id').unsigned(); + table.integer('index').unsigned(); table.date('date'); table.timestamps(); }).raw('ALTER TABLE `ACCOUNTS_TRANSACTIONS` AUTO_INCREMENT = 1000'); diff --git a/server/src/database/migrations/20200607212203_create_contacts_table.js b/server/src/database/migrations/20200607212203_create_contacts_table.js new file mode 100644 index 000000000..ccc7e13f0 --- /dev/null +++ b/server/src/database/migrations/20200607212203_create_contacts_table.js @@ -0,0 +1,49 @@ + +exports.up = function(knex) { + return knex.schema.createTable('contacts', table => { + table.increments(); + + table.string('contact_service'); + table.string('contact_type'); + + table.decimal('balance', 13, 3).defaultTo(0); + table.decimal('opening_balance', 13, 3).defaultTo(0); + + table.string('first_name').nullable(); + table.string('last_name').nullable(); + table.string('company_name').nullable(); + + table.string('display_name'); + + table.string('email').nullable(); + table.string('work_phone').nullable(); + table.string('personal_phone').nullable(); + + table.string('billing_address_1').nullable(); + table.string('billing_address_2').nullable(); + table.string('billing_address_city').nullable(); + table.string('billing_address_country').nullable(); + table.string('billing_address_email').nullable(); + table.string('billing_address_zipcode').nullable(); + table.string('billing_address_phone').nullable(); + table.string('billing_address_state').nullable(), + + table.string('shipping_address_1').nullable(); + table.string('shipping_address_2').nullable(); + table.string('shipping_address_city').nullable(); + table.string('shipping_address_country').nullable(); + table.string('shipping_address_email').nullable(); + table.string('shipping_address_zipcode').nullable(); + table.string('shipping_address_phone').nullable(); + table.string('shipping_address_state').nullable(); + + table.text('note'); + table.boolean('active').defaultTo(true); + + table.timestamps(); + }); +}; + +exports.down = function(knex) { + return knex.schema.dropTableIfExists('contacts'); +}; diff --git a/server/src/database/seeds/seed_accounts.js b/server/src/database/seeds/seed_accounts.js index 36c45d2d5..44ca2ed86 100644 --- a/server/src/database/seeds/seed_accounts.js +++ b/server/src/database/seeds/seed_accounts.js @@ -8,6 +8,7 @@ exports.seed = (knex) => { { id: 1, name: 'Petty Cash', + slug: 'petty-cash', account_type_id: 2, parent_account_id: null, code: '1000', @@ -19,6 +20,7 @@ exports.seed = (knex) => { { id: 2, name: 'Bank', + slug: 'bank', account_type_id: 2, parent_account_id: null, code: '2000', @@ -30,6 +32,7 @@ exports.seed = (knex) => { { id: 3, name: 'Other Income', + slug: 'other-income', account_type_id: 7, parent_account_id: null, code: '1000', @@ -41,6 +44,7 @@ exports.seed = (knex) => { { id: 4, name: 'Interest Income', + slug: 'interest-income', account_type_id: 7, parent_account_id: null, code: '1000', @@ -52,6 +56,7 @@ exports.seed = (knex) => { { id: 5, name: 'Opening Balance', + slug: 'opening-balance', account_type_id: 5, parent_account_id: null, code: '1000', @@ -63,6 +68,7 @@ exports.seed = (knex) => { { id: 6, name: 'Depreciation Expense', + slug: 'depreciation-expense', account_type_id: 6, parent_account_id: null, code: '1000', @@ -74,6 +80,7 @@ exports.seed = (knex) => { { id: 7, name: 'Interest Expense', + slug: 'interest-expense', account_type_id: 6, parent_account_id: null, code: '1000', @@ -85,6 +92,7 @@ exports.seed = (knex) => { { id: 8, name: 'Payroll Expenses', + slug: 'payroll-expenses', account_type_id: 6, parent_account_id: null, code: '1000', @@ -96,6 +104,7 @@ exports.seed = (knex) => { { id: 9, name: 'Other Expenses', + slug: 'other-expenses', account_type_id: 6, parent_account_id: null, code: '1000', @@ -107,6 +116,7 @@ exports.seed = (knex) => { { id: 10, name: 'Accounts Receivable', + slug: 'accounts-receivable', account_type_id: 8, parent_account_id: null, code: '1000', @@ -118,6 +128,7 @@ exports.seed = (knex) => { { id: 11, name: 'Accounts Payable', + slug: 'accounts-payable', account_type_id: 9, parent_account_id: null, code: '1000', @@ -129,6 +140,7 @@ exports.seed = (knex) => { { id: 12, name: 'Cost of Goods Sold (COGS)', + slug: 'cost-of-goods-sold', account_type_id: 12, predefined: 1, parent_account_id: null, @@ -139,6 +151,7 @@ exports.seed = (knex) => { { id: 13, name: 'Inventory Asset', + slug: 'inventory-asset', account_type_id: 14, predefined: 1, parent_account_id: null, @@ -149,6 +162,7 @@ exports.seed = (knex) => { { id: 14, name: 'Sales of Product Income', + slug: 'sales-of-product-income', account_type_id: 7, predefined: 1, parent_account_id: null, diff --git a/server/src/http/controllers/Accounting.js b/server/src/http/controllers/Accounting.js index fd0e0c911..60f63dedb 100644 --- a/server/src/http/controllers/Accounting.js +++ b/server/src/http/controllers/Accounting.js @@ -67,11 +67,6 @@ export default { this.recurringJournalEntries.validation, asyncMiddleware(this.recurringJournalEntries.handler) ); - router.post( - 'quick-journal-entries', - this.quickJournalEntries.validation, - asyncMiddleware(this.quickJournalEntries.handler) - ); return router; }, @@ -132,7 +127,6 @@ export default { builder.withGraphFetched('roles.field'); builder.withGraphFetched('columns'); builder.first(); - builder.remember(); }); const resourceFieldsKeys = manualJournalsResource.fields.map( @@ -443,6 +437,7 @@ export default { ...req.body, }; const { ManualJournal, Account, MediaLink } = req.models; + const { tenantId } = req; let totalCredit = 0; let totalDebit = 0; @@ -506,25 +501,22 @@ export default { status: form.status, user_id: user.id, }); - - const accountsDepGraph = await Account.depGraph().query(); - const journalPoster = new JournalPoster(accountsDepGraph); + const journalPoster = new JournalPoster(tenantId); entries.forEach((entry) => { - const account = accounts.find((a) => a.id === entry.account_id); const jouranlEntry = new JournalEntry({ debit: entry.debit, credit: entry.credit, - account: account.id, + account: entry.account_id, referenceType: 'Journal', referenceId: manualJournal.id, - accountNormal: account.type.normal, contactType: entry.contact_type, contactId: entry.contact_id, note: entry.note, date: formattedDate, userId: user.id, draft: !form.status, + index: entry.index, }); if (entry.debit) { journalPoster.debit(jouranlEntry); @@ -655,6 +647,7 @@ export default { let totalDebit = 0; const { user } = req; + const { tenantId } = req; const errorReasons = [...(req.errorReasons || [])]; const entries = form.entries.filter( (entry) => entry.credit || entry.debit @@ -714,22 +707,18 @@ export default { .where('reference_id', manualJournal.id) .withGraphFetched('account.type'); - const accountsDepGraph = await Account.depGraph().query().remember(); - const journal = new JournalPoster(accountsDepGraph); + const journal = new JournalPoster(tenantId); journal.loadEntries(transactions); journal.removeEntries(); entries.forEach((entry) => { - const account = accounts.find((a) => a.id === entry.account_id); - const jouranlEntry = new JournalEntry({ debit: entry.debit, credit: entry.credit, - account: account.id, + account: entry.account_id, referenceType: 'Journal', referenceId: manualJournal.id, - accountNormal: account.type.normal, note: entry.note, date: formattedDate, userId: user.id, @@ -780,6 +769,7 @@ export default { const { ManualJournal, AccountTransaction, Account } = req.models; const { id } = req.params; + const { tenantId } = req; const manualJournal = await ManualJournal.query().where('id', id).first(); if (!manualJournal) { @@ -801,8 +791,7 @@ export default { .where('reference_id', manualJournal.id) .withGraphFetched('account.type'); - const accountsDepGraph = await Account.depGraph().query().remember(); - const journal = new JournalPoster(accountsDepGraph); + const journal = new JournalPoster(tenantId); journal.loadEntries(transactions); journal.calculateEntriesBalanceChange(); @@ -876,6 +865,7 @@ export default { }); } const { id } = req.params; + const { tenantId } = req; const { ManualJournal, AccountTransaction, @@ -895,8 +885,7 @@ export default { .where('reference_id', manualJournal.id) .withGraphFetched('account.type'); - const accountsDepGraph = await Account.depGraph().query().remember(); - const journal = new JournalPoster(accountsDepGraph); + const journal = new JournalPoster(tenantId); journal.loadEntries(transactions); journal.removeEntries(); @@ -931,60 +920,6 @@ export default { }, }, - quickJournalEntries: { - validation: [ - check('date').exists().isISO8601(), - check('amount').exists().isNumeric().toFloat(), - check('credit_account_id').exists().isNumeric().toInt(), - check('debit_account_id').exists().isNumeric().toInt(), - check('transaction_type').exists(), - check('note').optional(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', - ...validationErrors, - }); - } - const errorReasons = []; - const form = { ...req.body }; - const { Account } = req.models; - - const foundAccounts = await Account.query() - .where('id', form.credit_account_id) - .orWhere('id', form.debit_account_id); - - const creditAccount = foundAccounts.find( - (a) => a.id === form.credit_account_id - ); - const debitAccount = foundAccounts.find( - (a) => a.id === form.debit_account_id - ); - - if (!creditAccount) { - errorReasons.push({ type: 'CREDIT_ACCOUNT.NOT.EXIST', code: 100 }); - } - if (!debitAccount) { - errorReasons.push({ type: 'DEBIT_ACCOUNT.NOT.EXIST', code: 200 }); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - - // const journalPoster = new JournalPoster(); - // const journalCredit = new JournalEntry({ - // debit: - // account: debitAccount.id, - // referenceId: - // }) - - return res.status(200).send(); - }, - }, - /** * Deletes bulk manual journals. */ @@ -1003,6 +938,7 @@ export default { }); } const filter = { ...req.query }; + const { tenantId } = req; const { ManualJournal, AccountTransaction, @@ -1029,8 +965,7 @@ export default { .whereIn('reference_type', ['Journal', 'ManualJournal']) .whereIn('reference_id', filter.ids); - const accountsDepGraph = await Account.depGraph().query().remember(); - const journal = new JournalPoster(accountsDepGraph); + const journal = new JournalPoster(tenantId); journal.loadEntries(transactions); journal.removeEntries(); diff --git a/server/src/http/controllers/Accounts.ts b/server/src/http/controllers/Accounts.ts index a90d31971..e0e23fbe1 100644 --- a/server/src/http/controllers/Accounts.ts +++ b/server/src/http/controllers/Accounts.ts @@ -88,19 +88,12 @@ export default class AccountsController extends BaseController{ this.bulkDeleteSchema, asyncMiddleware(this.deleteBulkAccounts.bind(this)) ); - - // router.post( - // '/:id/recalculate-balance', - // this.recalcualteBalanace.validation, - // asyncMiddleware(this.recalcualteBalanace.handler) - // ); // router.post( // '/:id/transfer_account/:toAccount', // this.transferToAnotherAccount.validation, // asyncMiddleware(this.transferToAnotherAccount.handler) // ); - - + return router; } @@ -548,73 +541,4 @@ export default class AccountsController extends BaseController{ // : {}), // }); // } - - // /** - // * Re-calculates balance of the given account. - // */ - // recalcualteBalanace: { - // validation: [param('id').isNumeric().toInt()], - // async handler(req, res) { - // const { id } = req.params; - // const { Account, AccountTransaction, AccountBalance } = req.models; - // const account = await Account.findById(id); - - // if (!account) { - // return res.status(400).send({ - // errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }], - // }); - // } - // const accountTransactions = AccountTransaction.query().where( - // 'account_id', - // account.id - // ); - - // const journalEntries = new JournalPoster(); - // journalEntries.loadFromCollection(accountTransactions); - - // // Delete the balance of the given account id. - // await AccountBalance.query().where('account_id', account.id).delete(); - - // // Save calcualted account balance. - // await journalEntries.saveBalance(); - - // return res.status(200).send(); - // }, - // }, - - - - // /** - // * Transfer all journal entries of the given account to another account. - // */ - // transferToAnotherAccount: { - // validation: [ - // param('id').exists().isNumeric().toInt(), - // param('toAccount').exists().isNumeric().toInt(), - // ], - // async handler(req, res) { - // const validationErrors = validationResult(req); - - // if (!validationErrors.isEmpty()) { - // return res.boom.badData(null, { - // code: 'validation_error', - // ...validationErrors, - // }); - // } - - // // const { id, toAccount: toAccountId } = req.params; - - // // const [fromAccount, toAccount] = await Promise.all([ - // // Account.query().findById(id), - // // Account.query().findById(toAccountId), - // // ]); - - // // const fromAccountTransactions = await AccountTransaction.query() - // // .where('account_id', fromAccount); - - // // return res.status(200).send(); - // }, - // }, - - }; diff --git a/server/src/http/controllers/Contacts/Contacts.ts b/server/src/http/controllers/Contacts/Contacts.ts new file mode 100644 index 000000000..533278f45 --- /dev/null +++ b/server/src/http/controllers/Contacts/Contacts.ts @@ -0,0 +1,70 @@ +import { check, param, query } from 'express-validator'; +import BaseController from "@/http/controllers/BaseController"; + +export default class ContactsController extends BaseController { + + /** + * Contact DTO schema. + */ + get contactDTOSchema() { + return [ + check('first_name').optional().trim().escape(), + check('last_name').optional().trim().escape(), + + check('company_name').optional().trim().escape(), + check('display_name').exists().trim().escape(), + + check('email').optional().isEmail().trim().escape(), + check('work_phone').optional().trim().escape(), + check('personal_phone').optional().trim().escape(), + + check('billing_address_city').optional().trim().escape(), + check('billing_address_country').optional().trim().escape(), + check('billing_address_email').optional().isEmail().trim().escape(), + check('billing_address_zipcode').optional().trim().escape(), + check('billing_address_phone').optional().trim().escape(), + check('billing_address_state').optional().trim().escape(), + + check('shipping_address_city').optional().trim().escape(), + check('shipping_address_country').optional().trim().escape(), + check('shipping_address_email').optional().isEmail().trim().escape(), + check('shipping_address_zip_code').optional().trim().escape(), + check('shipping_address_phone').optional().trim().escape(), + check('shipping_address_state').optional().trim().escape(), + + check('note').optional().trim().escape(), + check('active').optional().isBoolean().toBoolean(), + ]; + } + + /** + * Contact new DTO schema. + */ + get contactNewDTOSchema() { + return [ + check('balance').optional().isNumeric().toInt(), + ]; + } + + /** + * Contact edit DTO schema. + */ + get contactEditDTOSchema() { + return [ + + ] + } + + get specificContactSchema() { + return [ + param('id').exists().isNumeric().toInt(), + ]; + } + + get bulkContactsSchema() { + return [ + query('ids').isArray({ min: 2 }), + query('ids.*').isNumeric().toInt(), + ] + } +} \ No newline at end of file diff --git a/server/src/http/controllers/Contacts/Customers.ts b/server/src/http/controllers/Contacts/Customers.ts new file mode 100644 index 000000000..0b8aa1f4f --- /dev/null +++ b/server/src/http/controllers/Contacts/Customers.ts @@ -0,0 +1,196 @@ +import { Request, Response, Router, NextFunction } from 'express'; +import { Service, Inject } from 'typedi'; +import { check } from 'express-validator'; +import ContactsController from '@/http/controllers/Contacts/Contacts'; +import CustomersService from '@/services/Contacts/CustomersService'; +import { ServiceError } from '@/exceptions'; +import { ICustomerNewDTO, ICustomerEditDTO } from '@/interfaces'; +import asyncMiddleware from '@/http/middleware/asyncMiddleware'; + +@Service() +export default class CustomersController extends ContactsController { + @Inject() + customersService: CustomersService; + + /** + * Express router. + */ + router() { + const router = Router(); + + router.post('/', [ + ...this.contactDTOSchema, + ...this.contactNewDTOSchema, + ...this.customerDTOSchema, + ], + this.validationResult, + asyncMiddleware(this.newCustomer.bind(this)) + ); + router.post('/:id', [ + ...this.contactDTOSchema, + ...this.contactEditDTOSchema, + ...this.customerDTOSchema, + ], + this.validationResult, + asyncMiddleware(this.editCustomer.bind(this)) + ); + router.delete('/:id', [ + ...this.specificContactSchema, + ], + this.validationResult, + asyncMiddleware(this.deleteCustomer.bind(this)) + ); + router.delete('/', [ + ...this.bulkContactsSchema, + ], + this.validationResult, + asyncMiddleware(this.deleteBulkCustomers.bind(this)) + ); + router.get('/:id', [ + ...this.specificContactSchema, + ], + this.validationResult, + asyncMiddleware(this.getCustomer.bind(this)) + ); + return router; + } + + /** + * Customer DTO schema. + */ + get customerDTOSchema() { + return [ + check('customer_type').exists().trim().escape(), + check('opening_balance').optional().isNumeric().toInt(), + ]; + } + + /** + * Creates a new customer. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async newCustomer(req: Request, res: Response, next: NextFunction) { + const contactDTO: ICustomerNewDTO = this.matchedBodyData(req); + const { tenantId } = req; + + try { + const contact = await this.customersService.newCustomer(tenantId, contactDTO); + return res.status(200).send({ id: contact.id }); + } catch (error) { + next(error); + } + } + + /** + * Edits the given customer details. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async editCustomer(req: Request, res: Response, next: NextFunction) { + const contactDTO: ICustomerEditDTO = this.matchedBodyData(req); + const { tenantId } = req; + const { id: contactId } = req.params; + + try { + await this.customersService.editCustomer(tenantId, contactId, contactDTO;) + return res.status(200).send({ id: contactId }); + } catch (error) { + if (error instanceof ServiceError) { + if (error.errorType === 'contact_not_found') { + return res.boom.badRequest(null, { + errors: [{ type: 'CUSTOMER.NOT.FOUND', code: 100 }], + }); + } + } + next(error); + } + } + + /** + * Deletes the given customer from the storage. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async deleteCustomer(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: contactId } = req.params; + + try { + await this.customersService.deleteCustomer(tenantId, contactId) + return res.status(200).send({ id: contactId }); + } catch (error) { + if (error instanceof ServiceError) { + if (error.errorType === 'contact_not_found') { + return res.boom.badRequest(null, { + errors: [{ type: 'CUSTOMER.NOT.FOUND', code: 100 }], + }); + } + if (error.errorType === 'customer_has_invoices') { + return res.boom.badRequest(null, { + errors: [{ type: 'CUSTOMER.HAS.SALES_INVOICES', code: 200 }], + }); + } + } + next(error); + } + } + + /** + * Retrieve details of the given customer id. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async getCustomer(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: contactId } = req.params; + + try { + const contact = await this.customersService.getCustomer(tenantId, contactId) + return res.status(200).send({ contact }); + } catch (error) { + if (error instanceof ServiceError) { + if (error.errorType === 'contact_not_found') { + return res.boom.badRequest(null, { + errors: [{ type: 'CONTACT.NOT.FOUND', code: 100 }], + }); + } + } + next(error); + } + } + + /** + * Deletes customers in bulk. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async deleteBulkCustomers(req: Request, res: Response, next: NextFunction) { + const { ids: contactsIds } = req.query; + const { tenantId } = req; + + try { + await this.customersService.deleteBulkCustomers(tenantId, contactsIds) + return res.status(200).send({ ids: contactsIds }); + } catch (error) { + if (error instanceof ServiceError) { + if (error.errorType === 'contacts_not_found') { + return res.boom.badRequest(null, { + errors: [{ type: 'CUSTOMERS.NOT.FOUND', code: 100 }], + }); + } + if (error.errorType === 'some_customers_have_invoices') { + return res.boom.badRequest(null, { + errors: [{ type: 'SOME.CUSTOMERS.HAVE.SALES_INVOICES', code: 200 }], + }); + } + } + next(error); + } + } +} \ No newline at end of file diff --git a/server/src/http/controllers/Contacts/Vendors.ts b/server/src/http/controllers/Contacts/Vendors.ts new file mode 100644 index 000000000..d4166e53e --- /dev/null +++ b/server/src/http/controllers/Contacts/Vendors.ts @@ -0,0 +1,195 @@ +import { Request, Response, Router, NextFunction } from 'express'; +import { Service, Inject } from 'typedi'; +import { check } from 'express-validator'; +import ContactsController from '@/http/controllers/Contacts/Contacts'; +import VendorsService from '@/services/Contacts/VendorsService'; +import { ServiceError } from '@/exceptions'; +import { IVendorNewDTO, IVendorEditDTO } from '@/interfaces'; +import asyncMiddleware from '@/http/middleware/asyncMiddleware'; + +@Service() +export default class VendorsController extends ContactsController { + @Inject() + vendorsService: VendorsService; + + /** + * Express router. + */ + router() { + const router = Router(); + + router.post('/', [ + ...this.contactDTOSchema, + ...this.contactNewDTOSchema, + ...this.vendorDTOSchema, + ], + this.validationResult, + asyncMiddleware(this.newVendor.bind(this)) + ); + router.post('/:id', [ + ...this.contactDTOSchema, + ...this.contactEditDTOSchema, + ...this.vendorDTOSchema, + ], + this.validationResult, + asyncMiddleware(this.editVendor.bind(this)) + ); + router.delete('/:id', [ + ...this.specificContactSchema, + ], + this.validationResult, + asyncMiddleware(this.deleteVendor.bind(this)) + ); + router.delete('/', [ + ...this.bulkContactsSchema, + ], + this.validationResult, + asyncMiddleware(this.deleteBulkVendors.bind(this)) + ); + router.get('/:id', [ + ...this.specificContactSchema, + ], + this.validationResult, + asyncMiddleware(this.getVendor.bind(this)) + ); + return router; + } + + /** + * Vendor DTO schema. + */ + get vendorDTOSchema() { + return [ + check('opening_balance').optional().isNumeric().toInt(), + ]; + } + + /** + * Creates a new vendor. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async newVendor(req: Request, res: Response, next: NextFunction) { + const contactDTO: IVendorNewDTO = this.matchedBodyData(req); + const { tenantId } = req; + + try { + const contact = await this.vendorsService.newVendor(tenantId, contactDTO); + return res.status(200).send({ id: contact.id }); + } catch (error) { + next(error); + } + } + + /** + * Edits the given vendor details. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async editVendor(req: Request, res: Response, next: NextFunction) { + const contactDTO: IVendorEditDTO = this.matchedBodyData(req); + const { tenantId } = req; + const { id: contactId } = req.params; + + try { + await this.vendorsService.editVendor(tenantId, contactId, contactDTO;) + return res.status(200).send({ id: contactId }); + } catch (error) { + if (error instanceof ServiceError) { + if (error.errorType === 'contact_not_found') { + return res.status(400).send({ + errors: [{ type: 'VENDOR.NOT.FOUND', code: 100 }], + }); + } + } + next(error); + } + } + + /** + * Deletes the given vendor from the storage. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async deleteVendor(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: contactId } = req.params; + + try { + await this.vendorsService.deleteVendor(tenantId, contactId) + return res.status(200).send({ id: contactId }); + } catch (error) { + if (error instanceof ServiceError) { + if (error.errorType === 'contact_not_found') { + return res.status(400).send({ + errors: [{ type: 'VENDOR.NOT.FOUND', code: 100 }], + }); + } + if (error.errorType === 'vendor_has_bills') { + return res.status(400).send({ + errors: [{ type: 'VENDOR.HAS.BILLS', code: 200 }], + }); + } + } + next(error); + } + } + + /** + * Retrieve details of the given vendor id. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async getVendor(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: contactId } = req.params; + + try { + const contact = await this.vendorsService.getVendor(tenantId, contactId) + return res.status(200).send({ contact }); + } catch (error) { + if (error instanceof ServiceError) { + if (error.errorType === 'contact_not_found') { + return res.status(400).send({ + errors: [{ type: 'VENDOR.NOT.FOUND', code: 100 }], + }); + } + } + next(error); + } + } + + /** + * Deletes vendors in bulk. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async deleteBulkVendors(req: Request, res: Response, next: NextFunction) { + const { ids: contactsIds } = req.query; + const { tenantId } = req; + + try { + await this.vendorsService.deleteBulkVendors(tenantId, contactsIds) + return res.status(200).send({ ids: contactsIds }); + } catch (error) { + if (error instanceof ServiceError) { + if (error.errorType === 'contacts_not_found') { + return res.boom.badRequest(null, { + errors: [{ type: 'VENDORS.NOT.FOUND', code: 100 }], + }); + } + if (error.errorType === 'some_vendors_have_bills') { + return res.boom.badRequest(null, { + errors: [{ type: 'SOME.VENDORS.HAVE.BILLS', code: 200 }], + }); + } + } + next(error); + } + } +} \ No newline at end of file diff --git a/server/src/http/controllers/Customers.js b/server/src/http/controllers/Customers.js deleted file mode 100644 index 6daa912fb..000000000 --- a/server/src/http/controllers/Customers.js +++ /dev/null @@ -1,424 +0,0 @@ -import express from 'express'; -import { - check, - param, - query, - validationResult, -} from 'express-validator'; -import { pick, difference } from 'lodash'; -import asyncMiddleware from '@/http/middleware/asyncMiddleware'; -import { - mapViewRolesToConditionals, - mapFilterRolesToDynamicFilter, -} from '@/lib/ViewRolesBuilder'; -import { - DynamicFilter, - DynamicFilterSortBy, - DynamicFilterViews, - DynamicFilterFilterRoles, -} from '@/lib/DynamicFilter'; - - -const validatioRoles = [ - check('customer_type') - .exists() - .isIn(['individual', 'business']) - .trim() - .escape(), - check('first_name').optional().trim().escape(), - check('last_name').optional().trim().escape(), - - check('company_name').optional().trim().escape(), - - check('display_name').exists().trim().escape(), - - check('email').optional().isEmail().trim().escape(), - check('work_phone').optional().trim().escape(), - check('personal_phone').optional().trim().escape(), - - check('billing_address_city').optional().trim().escape(), - check('billing_address_country').optional().trim().escape(), - check('billing_address_email').optional().isEmail().trim().escape(), - check('billing_address_zipcode').optional().trim().escape(), - check('billing_address_phone').optional().trim().escape(), - check('billing_address_state').optional().trim().escape(), - - check('shipping_address_city').optional().trim().escape(), - check('shipping_address_country').optional().trim().escape(), - check('shipping_address_email').optional().isEmail().trim().escape(), - check('shipping_address_zip_code').optional().trim().escape(), - check('shipping_address_phone').optional().trim().escape(), - check('shipping_address_state').optional().trim().escape(), - - check('note').optional().trim().escape(), - check('active').optional().isBoolean().toBoolean(), - - check('custom_fields').optional().isArray({ min: 1 }), - check('custom_fields.*.key').exists().trim().escape(), - check('custom_fields.*.value').exists(), -]; - -export default { - /** - * Router constructor. - */ - router() { - const router = express.Router(); - - router.post('/', - this.newCustomer.validation, - asyncMiddleware(this.newCustomer.handler)); - - router.post('/:id', - this.editCustomer.validation, - asyncMiddleware(this.editCustomer.handler)); - - router.delete('/:id', - this.deleteCustomer.validation, - asyncMiddleware(this.deleteCustomer.handler)); - - router.delete('/', - this.deleteBulkCustomers.validation, - asyncMiddleware(this.deleteBulkCustomers.handler)); - - router.get('/', - this.listCustomers.validation, - asyncMiddleware(this.listCustomers.handler)); - - router.get('/:id', - this.getCustomer.validation, - asyncMiddleware(this.getCustomer.handler)); - - return router; - }, - - /** - * Retrieve customers list with pagination and custom view metadata. - */ - listCustomers: { - validation: [ - query('column_sort_order').optional().isIn(['created_at']), - query('sort_order').optional().isIn(['desc', 'asc']), - query('page').optional().isNumeric().toInt(), - query('page_size').optional().isNumeric().toInt(), - query('custom_view_id').optional().isNumeric().toInt(), - query('stringified_filter_roles').optional().isJSON(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { Resource, View, Customer } = req.models; - const errorReasons = []; - - const customersResource = await Resource.query() - .where('name', 'customers') - .withGraphFetched('fields') - .first(); - - if (!customersResource) { - return res.status(400).send({ - errors: [{ type: 'CUSTOMERS.RESOURCE.NOT.FOUND', code: 200 }], - }); - } - - const filter = { - column_sort_order: '', - sort_order: '', - page: 1, - page_size: 10, - custom_view_id: null, - filter_roles: [], - ...req.query, - }; - if (filter.stringified_filter_roles) { - filter.filter_roles = JSON.parse(filter.stringified_filter_roles); - } - const view = await View.query().onBuild((builder) => { - if (filter.custom_view_id) { - builder.where('id', filter.custom_view_id); - } else { - builder.where('favourite', true); - } - builder.where('resource_id', customersResource.id); - builder.withGraphFetched('roles.field'); - builder.withGraphFetched('columns'); - builder.first(); - }); - const resourceFieldsKeys = customersResource.fields.map((c) => c.key); - const dynamicFilter = new DynamicFilter(Customer.tableName); - - // Dynamic filter with view roles. - if (view && view.roles.length > 0) { - const viewFilter = new DynamicFilterViews( - mapViewRolesToConditionals(view.roles), - view.rolesLogicExpression, - ); - if (!viewFilter.validateFilterRoles()) { - errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 }); - } - dynamicFilter.setFilter(viewFilter); - } - - // Dynamic filter with filter roles. - if (filter.filter_roles.length > 0) { - // Validate the accounts resource fields. - const filterRoles = new DynamicFilterFilterRoles( - mapFilterRolesToDynamicFilter(filter.filter_roles), - customersResource.fields, - ); - dynamicFilter.setFilter(filterRoles); - - if (filterRoles.validateFilterRoles().length > 0) { - errorReasons.push({ type: 'ITEMS.RESOURCE.HAS.NO.FIELDS', code: 500 }); - } - } - - // Dynamic filter with column sort order. - if (filter.column_sort_order) { - if (resourceFieldsKeys.indexOf(filter.column_sort_order) === -1) { - errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 }); - } - const sortByFilter = new DynamicFilterSortBy( - filter.column_sort_order, - filter.sort_order, - ); - dynamicFilter.setFilter(sortByFilter); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - // Customers query. - const customers = await Customer.query().onBuild((builder) => { - dynamicFilter.buildQuery()(builder); - }).pagination(filter.page - 1, filter.page_size); - - return res.status(200).send({ - customers, - ...(view) ? { - customViewId: view.id, - } : {}, - }); - } - }, - - /** - * Submit a new customer details. - */ - newCustomer: { - validation: [ - ...validatioRoles, - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { Customer } = req.models; - const form = { ...req.body }; - - const customer = await Customer.query().insertAndFetch({ - balance: 0, - ...pick(form, [ - 'customer_type', - 'first_name', - 'last_name', - 'company_name', - 'display_name', - - 'email', - 'work_phone', - 'personal_phone', - - 'billing_address_1', - 'billing_address_2', - 'billing_address_city', - 'billing_address_country', - 'billing_address_email', - 'billing_address_zipcode', - 'billing_address_phone', - 'billing_address_state', - - 'shipping_address_1', - 'shipping_address_2', - 'shipping_address_city', - 'shipping_address_country', - 'shipping_address_email', - 'shipping_address_zipcode', - 'shipping_address_phone', - 'shipping_address_state', - - 'note', - 'active', - ]), - }); - - return res.status(200).send({ id: customer.id }); - }, - }, - - /** - * Edit details of the given customer id. - */ - editCustomer: { - validation: [ - param('id').exists().isNumeric().toInt(), - ...validatioRoles, - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { id } = req.params; - const form = { ...req.body }; - const { Customer } = req.models; - const customer = await Customer.query().where('id', id).first(); - - if (!customer) { - return res.status(404).send({ - errors: [{ type: 'CUSTOMER.NOT.FOUND', code: 200 }], - }); - } - - await Customer.query().where('id', id).patch({ - ...pick(form, [ - 'customer_type', - 'first_name', - 'last_name', - 'company_name', - 'display_name', - - 'email', - 'work_phone', - 'personal_phone', - - 'billing_address_1', - 'billing_address_2', - 'billing_address_city', - 'billing_address_country', - 'billing_address_email', - 'billing_address_zipcode', - 'billing_address_phone', - 'billing_address_state', - - 'shipping_address_1', - 'shipping_address_2', - 'shipping_address_city', - 'shipping_address_country', - 'shipping_address_email', - 'shipping_address_zipcode', - 'shipping_address_phone', - 'shipping_address_state', - - 'note', - 'active', - ]), - }); - return res.status(200).send({ id }); - }, - }, - - /** - * Retrieve details of the given customer id. - */ - getCustomer: { - validation: [ - param('id').exists().isNumeric().toInt(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { Customer } = req.models; - const { id } = req.params; - const customer = await Customer.query().where('id', id).first(); - - if (!customer) { - return res.status(404).send({ - errors: [{ type: 'CUSTOMER.NOT.FOUND', code: 200 }], - }); - } - return res.status(200).send({ customer }); - }, - }, - - /** - * Delete the given customer. - */ - deleteCustomer: { - validation: [ - param('id').exists().isNumeric().toInt(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { Customer } = req.models; - const { id } = req.params; - const customer = await Customer.query().where('id', id).first(); - - if (!customer) { - return res.status(404).send({ - errors: [{ type: 'CUSTOMER.NOT.FOUND', code: 200 }], - }); - } - await Customer.query().where('id', id).delete(); - - return res.status(200).send(); - }, - }, - - /** - * Bulk delete customers. - */ - deleteBulkCustomers: { - validation: [ - query('ids').isArray({ min: 2 }), - query('ids.*').isNumeric().toInt(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const filter = { ...req.query }; - const { Customer } = req.models; - - const customers = await Customer.query().whereIn('id', filter.ids); - const storedCustomersIds = customers.map((customer) => customer.id); - - const notFoundCustomers = difference(filter.ids, storedCustomersIds); - - if (notFoundCustomers.length > 0) { - return res.status(404).send({ - errors: [{ type: 'CUSTOMERS.NOT.FOUND', code: 200 }], - }); - } - await Customer.query().whereIn('id', storedCustomersIds).delete(); - - return res.status(200).send({ ids: storedCustomersIds }); - } - } -}; diff --git a/server/src/http/controllers/OAuth2.js b/server/src/http/controllers/OAuth2.js deleted file mode 100644 index 9263d1fb8..000000000 --- a/server/src/http/controllers/OAuth2.js +++ /dev/null @@ -1,23 +0,0 @@ -import express from 'express'; -import OAuthServer from 'express-oauth-server'; -import OAuthServerModel from '@/models/OAuthServerModel'; - -export default { - - /** - * Router constructor method. - */ - router() { - const router = express.Router(); - - router.oauth = new OAuthServer({ - model: OAuthServerModel, - }); - - router.post('/token', router.oauth.token()); - // router.get('authorize', this.getAuthorize); - // router.post('authorize', this.postAuthorize); - - return router; - }, -}; diff --git a/server/src/http/controllers/Organization.ts b/server/src/http/controllers/Organization.ts index bba6ccf27..2536dd175 100644 --- a/server/src/http/controllers/Organization.ts +++ b/server/src/http/controllers/Organization.ts @@ -1,14 +1,17 @@ import { Inject, Service } from 'typedi'; import { Router, Request, Response } from 'express'; -import { check, matchedData } from 'express-validator'; -import { mapKeys, camelCase } from 'lodash'; +import { check } from 'express-validator'; import asyncMiddleware from "@/http/middleware/asyncMiddleware"; -import validateMiddleware from '@/http/middleware/validateMiddleware'; +import JWTAuth from '@/http/middleware/jwtAuth'; +import TenancyMiddleware from '@/http/middleware/TenancyMiddleware'; +import SubscriptionMiddleware from '@/http/middleware/SubscriptionMiddleware'; +import AttachCurrentTenantUser from '@/http/middleware/AttachCurrentTenantUser'; import OrganizationService from '@/services/Organization'; import { ServiceError } from '@/exceptions'; +import BaseController from '@/http/controllers/BaseController'; @Service() -export default class OrganizationController { +export default class OrganizationController extends BaseController{ @Inject() organizationService: OrganizationService; @@ -18,11 +21,18 @@ export default class OrganizationController { router() { const router = Router(); + // Should before build tenant database the user be authorized and + // most important than that, should be subscribed to any plan. + router.use(JWTAuth); + router.use(AttachCurrentTenantUser); + router.use(TenancyMiddleware); + router.use(SubscriptionMiddleware('main')); + router.post( '/build', [ check('organization_id').exists().trim().escape(), ], - validateMiddleware, + this.validationResult, asyncMiddleware(this.build.bind(this)) ); return router; @@ -35,10 +45,7 @@ export default class OrganizationController { * @param {NextFunction} next */ async build(req: Request, res: Response, next: Function) { - const buildOTD = mapKeys(matchedData(req, { - locations: ['body'], - includeOptionals: true, - }), (v, k) => camelCase(k)); + const buildOTD = this.matchedBodyData(req); try { await this.organizationService.build(buildOTD.organizationId); diff --git a/server/src/http/controllers/Subscription/Licenses.ts b/server/src/http/controllers/Subscription/Licenses.ts index 56cb1a15c..ae0604447 100644 --- a/server/src/http/controllers/Subscription/Licenses.ts +++ b/server/src/http/controllers/Subscription/Licenses.ts @@ -1,6 +1,8 @@ +import { Service, Inject } from 'typedi'; import { Router, Request, Response } from 'express' import { check, oneOf, ValidationChain } from 'express-validator'; -import { Service, Inject } from 'typedi'; +import basicAuth from 'express-basic-auth'; +import config from '@/../config/config'; import { License, Plan } from '@/system/models'; import BaseController from '@/http/controllers/BaseController'; import LicenseService from '@/services/Payment/License'; @@ -19,6 +21,13 @@ export default class LicensesController extends BaseController { router() { const router = Router(); + router.use(basicAuth({ + users: { + [config.licensesAuth.user]: config.licensesAuth.password, + }, + challenge: true, + })); + router.post( '/generate', this.generateLicenseSchema, diff --git a/server/src/http/controllers/Users.ts b/server/src/http/controllers/Users.ts index dabbdeb74..4783a1cdb 100644 --- a/server/src/http/controllers/Users.ts +++ b/server/src/http/controllers/Users.ts @@ -129,6 +129,8 @@ export default class UsersController extends BaseController{ const { id } = req.params; const { tenantId } = req; + debugger; + try { await this.usersService.deleteUser(tenantId, id); return res.status(200).send({ id }); @@ -141,8 +143,8 @@ export default class UsersController extends BaseController{ }); } } + next(); } - next(); } /** @@ -159,6 +161,7 @@ export default class UsersController extends BaseController{ const user = await this.usersService.getUser(tenantId, userId); return res.status(200).send({ user }); } catch (error) { + console.log(error); if (error instanceof ServiceError) { if (error.errorType === 'user_not_found') { @@ -246,6 +249,7 @@ export default class UsersController extends BaseController{ }); } } + next(); } } }; \ No newline at end of file diff --git a/server/src/http/controllers/Vendors.js b/server/src/http/controllers/Vendors.js deleted file mode 100644 index a942c0127..000000000 --- a/server/src/http/controllers/Vendors.js +++ /dev/null @@ -1,377 +0,0 @@ -import express from 'express'; -import { - check, - param, - query, - validationResult, -} from 'express-validator'; -import { pick } from 'lodash'; -import asyncMiddleware from '@/http/middleware/asyncMiddleware'; -import { - mapViewRolesToConditionals, - mapFilterRolesToDynamicFilter, -} from '@/lib/ViewRolesBuilder'; -import { - DynamicFilter, - DynamicFilterSortBy, - DynamicFilterViews, - DynamicFilterFilterRoles, -} from '@/lib/DynamicFilter'; - - -const validatioRoles = [ - check('first_name').optional().trim().escape(), - check('last_name').optional().trim().escape(), - - check('company_name').optional().trim().escape(), - - check('display_name').exists().trim().escape(), - - check('email').optional().isEmail().trim().escape(), - check('work_phone').optional().trim().escape(), - check('personal_phone').optional().trim().escape(), - - check('billing_address_city').optional().trim().escape(), - check('billing_address_country').optional().trim().escape(), - check('billing_address_email').optional().isEmail().trim().escape(), - check('billing_address_zipcode').optional().trim().escape(), - check('billing_address_phone').optional().trim().escape(), - check('billing_address_state').optional().trim().escape(), - - check('shipping_address_city').optional().trim().escape(), - check('shipping_address_country').optional().trim().escape(), - check('shipping_address_email').optional().isEmail().trim().escape(), - check('shipping_address_zip_code').optional().trim().escape(), - check('shipping_address_phone').optional().trim().escape(), - check('shipping_address_state').optional().trim().escape(), - - check('note').optional().trim().escape(), - check('active').optional().isBoolean().toBoolean(), - - check('custom_fields').optional().isArray({ min: 1 }), - check('custom_fields.*.key').exists().trim().escape(), - check('custom_fields.*.value').exists(), -]; - -export default { - /** - * Router constructor. - */ - router() { - const router = express.Router(); - - router.post('/', - this.newVendor.validation, - asyncMiddleware(this.newVendor.handler)); - - router.post('/:id', - this.editVendor.validation, - asyncMiddleware(this.editVendor.handler)); - - router.delete('/:id', - this.deleteVendor.validation, - asyncMiddleware(this.deleteVendor.handler)); - - router.get('/', - this.listVendors.validation, - asyncMiddleware(this.listVendors.handler)); - - router.get('/:id', - this.getVendor.validation, - asyncMiddleware(this.getVendor.handler)); - - return router; - }, - - /** - * Retrieve vendors list with pagination and custom view metadata. - */ - listVendors: { - validation: [ - query('column_sort_order').optional().isIn(['created_at']), - query('sort_order').optional().isIn(['desc', 'asc']), - query('page').optional().isNumeric().toInt(), - query('page_size').optional().isNumeric().toInt(), - query('custom_view_id').optional().isNumeric().toInt(), - query('stringified_filter_roles').optional().isJSON(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { Resource, View, Vendor } = req.models; - const errorReasons = []; - - const vendorsResource = await Resource.query() - .where('name', 'vendors') - .withGraphFetched('fields') - .first(); - - if (!vendorsResource) { - return res.status(400).send({ - errors: [{ type: 'VENDORS.RESOURCE.NOT.FOUND', code: 200 }], - }); - } - - const filter = { - column_sort_order: '', - sort_order: '', - page: 1, - page_size: 10, - custom_view_id: null, - filter_roles: [], - ...req.query, - }; - if (filter.stringified_filter_roles) { - filter.filter_roles = JSON.parse(filter.stringified_filter_roles); - } - const view = await View.query().onBuild((builder) => { - if (filter.custom_view_id) { - builder.where('id', filter.custom_view_id); - } else { - builder.where('favourite', true); - } - builder.where('resource_id', vendorsResource.id); - builder.withGraphFetched('roles.field'); - builder.withGraphFetched('columns'); - builder.first(); - }); - const resourceFieldsKeys = vendorsResource.fields.map((c) => c.key); - const dynamicFilter = new DynamicFilter(Vendor.tableName); - - // Dynamic filter with view roles. - if (view && view.roles.length > 0) { - const viewFilter = new DynamicFilterViews( - mapViewRolesToConditionals(view.roles), - view.rolesLogicExpression, - ); - if (!viewFilter.validateFilterRoles()) { - errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 }); - } - dynamicFilter.setFilter(viewFilter); - } - - // Dynamic filter with filter roles. - if (filter.filter_roles.length > 0) { - // Validate the accounts resource fields. - const filterRoles = new DynamicFilterFilterRoles( - mapFilterRolesToDynamicFilter(filter.filter_roles), - vendorsResource.fields, - ); - dynamicFilter.setFilter(filterRoles); - - if (filterRoles.validateFilterRoles().length > 0) { - errorReasons.push({ type: 'ITEMS.RESOURCE.HAS.NO.FIELDS', code: 500 }); - } - } - - // Dynamic filter with column sort order. - if (filter.column_sort_order) { - if (resourceFieldsKeys.indexOf(filter.column_sort_order) === -1) { - errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 }); - } - const sortByFilter = new DynamicFilterSortBy( - filter.column_sort_order, - filter.sort_order, - ); - dynamicFilter.setFilter(sortByFilter); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - // Vendors query. - const vendors = await Vendor.query().onBuild((builder) => { - dynamicFilter.buildQuery()(builder); - }).pagination(filter.page - 1, filter.page_size); - - return res.status(200).send({ - vendors, - ...(view) ? { - customViewId: view.id, - } : {}, - }); - } - }, - - /** - * Submit a new vendor details. - */ - newVendor: { - validation: [ - ...validatioRoles, - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { Vendor } = req.models; - const form = { ...req.body }; - - const vendor = await Vendor.query().insertAndFetch({ - ...pick(form, [ - 'first_name', - 'last_name', - 'company_name', - 'display_name', - - 'email', - 'work_phone', - 'personal_phone', - - 'billing_address_1', - 'billing_address_2', - 'billing_address_city', - 'billing_address_country', - 'billing_address_email', - 'billing_address_zipcode', - 'billing_address_phone', - 'billing_address_state', - - 'shipping_address_1', - 'shipping_address_2', - 'shipping_address_city', - 'shipping_address_country', - 'shipping_address_email', - 'shipping_address_zipcode', - 'shipping_address_phone', - 'shipping_address_state', - - 'note', - 'active', - ]), - }); - - return res.status(200).send({ id: vendor.id }); - }, - }, - - /** - * Edit details of the given vendor id. - */ - editVendor: { - validation: [ - param('id').exists().isNumeric().toInt(), - ...validatioRoles, - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { id } = req.params; - const form = { ...req.body }; - const { Vendor } = req.models; - const vendor = await Vendor.query().where('id', id).first(); - - if (!vendor) { - return res.status(404).send({ - errors: [{ type: 'VENDOR.NOT.FOUND', code: 200 }], - }); - } - - await Vendor.query().where('id', id).patch({ - ...pick(form, [ - 'first_name', - 'last_name', - 'company_name', - 'display_name', - - 'email', - 'work_phone', - 'personal_phone', - - 'billing_address_1', - 'billing_address_2', - 'billing_address_city', - 'billing_address_country', - 'billing_address_email', - 'billing_address_zipcode', - 'billing_address_phone', - 'billing_address_state', - - 'shipping_address_1', - 'shipping_address_2', - 'shipping_address_city', - 'shipping_address_country', - 'shipping_address_email', - 'shipping_address_zipcode', - 'shipping_address_phone', - 'shipping_address_state', - - 'note', - 'active', - ]), - }); - return res.status(200).send({ id }); - }, - }, - - /** - * Retrieve details of the given vendor id. - */ - getVendor: { - validation: [ - param('id').exists().isNumeric().toInt(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { Vendor } = req.models; - const { id } = req.params; - const vendor = await Vendor.query().where('id', id).first(); - - if (!vendor) { - return res.status(404).send({ - errors: [{ type: 'VENDOR.NOT.FOUND', code: 200 }], - }); - } - return res.status(200).send({ vendor }); - }, - }, - - /** - * Delete the given vendor. - */ - deleteVendor: { - validation: [ - param('id').exists().isNumeric().toInt(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { Vendor } = req.models; - const { id } = req.params; - const vendor = await Vendor.query().where('id', id).first(); - - if (!vendor) { - return res.status(404).send({ - errors: [{ type: 'VENDOR.NOT.FOUND', code: 200 }], - }); - } - await Vendor.query().where('id', id).delete(); - - return res.status(200).send(); - }, - } -}; diff --git a/server/src/http/index.ts b/server/src/http/index.ts index d2c1702a2..2035983d3 100644 --- a/server/src/http/index.ts +++ b/server/src/http/index.ts @@ -25,8 +25,8 @@ import FinancialStatements from '@/http/controllers/FinancialStatements'; import Expenses from '@/http/controllers/Expenses'; import Settings from '@/http/controllers/Settings'; import Currencies from '@/http/controllers/Currencies'; -import Customers from '@/http/controllers/Customers'; -import Vendors from '@/http/controllers/Vendors'; +import Customers from '@/http/controllers/Contacts/Customers'; +import Vendors from '@/http/controllers/Contacts/Vendors'; import Sales from '@/http/controllers/Sales' import Purchases from '@/http/controllers/Purchases'; import Resources from './controllers/Resources'; @@ -43,15 +43,15 @@ export default () => { app.use('/auth', Container.get(Authentication).router()); app.use('/invite', Container.get(InviteUsers).nonAuthRouter()); - app.use('/organization', Container.get(Organization).router()); app.use('/licenses', Container.get(Licenses).router()); app.use('/subscription', Container.get(Subscription).router()); app.use('/ping', Container.get(Ping).router()); + app.use('/organization', Container.get(Organization).router()); const dashboard = Router(); dashboard.use(JWTAuth); - dashboard.use(AttachCurrentTenantUser) + dashboard.use(AttachCurrentTenantUser); dashboard.use(TenancyMiddleware); dashboard.use(I18nMiddleware); dashboard.use(SubscriptionMiddleware('main')); @@ -71,8 +71,8 @@ export default () => { dashboard.use('/financial_statements', FinancialStatements.router()); dashboard.use('/settings', Container.get(Settings).router()); dashboard.use('/sales', Sales.router()); - dashboard.use('/customers', Customers.router()); - dashboard.use('/vendors', Vendors.router()); + dashboard.use('/customers', Container.get(Customers).router()); + dashboard.use('/vendors', Container.get(Vendors).router()); dashboard.use('/purchases', Purchases.router()); dashboard.use('/resources', Resources.router()); dashboard.use('/exchange_rates', ExchangeRates.router()); diff --git a/server/src/http/middleware/AttachCurrentTenantUser.ts b/server/src/http/middleware/AttachCurrentTenantUser.ts index 981bc2aab..f2da447d2 100644 --- a/server/src/http/middleware/AttachCurrentTenantUser.ts +++ b/server/src/http/middleware/AttachCurrentTenantUser.ts @@ -18,6 +18,8 @@ const attachCurrentUser = async (req: Request, res: Response, next: Function) => Logger.info('[attach_user_middleware] the system user not found.'); return res.boom.unauthorized(); } + // Delete password property from user object. + Reflect.deleteProperty(user, 'password'); req.user = user; return next(); } catch (e) { diff --git a/server/src/http/middleware/TenancyMiddleware.ts b/server/src/http/middleware/TenancyMiddleware.ts index 7ecd651bc..3cf4f1191 100644 --- a/server/src/http/middleware/TenancyMiddleware.ts +++ b/server/src/http/middleware/TenancyMiddleware.ts @@ -2,6 +2,8 @@ import { Container } from 'typedi'; import { Request, Response, NextFunction } from 'express'; import TenantsManager from '@/system/TenantsManager'; import tenantModelsLoader from '@/loaders/tenantModels'; +import tenantRepositoriesLoader from '@/loaders/tenantRepositories'; +import Cache from '@/services/Cache'; export default async (req: Request, res: Response, next: NextFunction) => { const Logger = Container.get('logger'); @@ -36,7 +38,7 @@ export default async (req: Request, res: Response, next: NextFunction) => { return res.boom.unauthorized(); } const models = tenantModelsLoader(tenantKnex); - + req.knex = tenantKnex; req.organizationId = organizationId; req.tenant = tenant; @@ -44,10 +46,18 @@ export default async (req: Request, res: Response, next: NextFunction) => { req.models = models; const tenantContainer = Container.of(`tenant-${tenant.id}`); + const cacheInstance = new Cache(); tenantContainer.set('models', models); tenantContainer.set('knex', tenantKnex); tenantContainer.set('tenant', tenant); + tenantContainer.set('cache', cacheInstance); + + const repositories = tenantRepositoriesLoader(tenant.id) + tenantContainer.set('repositories', repositories); + + req.repositories = repositories; + Logger.info('[tenancy_middleware] tenant dependencies injected to container.'); if (res.locals) { diff --git a/server/src/interfaces/Contact.ts b/server/src/interfaces/Contact.ts new file mode 100644 index 000000000..af9b8b0b3 --- /dev/null +++ b/server/src/interfaces/Contact.ts @@ -0,0 +1,170 @@ + +// Contact Interfaces. +// ---------------------------------- +export interface IContactAddress { + billingAddress1: string, + billingAddress2: string, + billingAddressCity: string, + billingAddressCountry: string, + billingAddressEmail: string, + billingAddressZipcode: string, + billingAddressPhone: string, + billingAddressState: string, + + shippingAddress1: string, + shippingAddress2: string, + shippingAddressCity: string, + shippingAddressCountry: string, + shippingAddressEmail: string, + shippingAddressZipcode: string, + shippingAddressPhone: string, + shippingAddressState: string, +} +export interface IContactAddressDTO { + billingAddress1?: string, + billingAddress2?: string, + billingAddressCity?: string, + billingAddressCountry?: string, + billingAddressEmail?: string, + billingAddressZipcode?: string, + billingAddressPhone?: string, + billingAddressState?: string, + + shippingAddress1?: string, + shippingAddress2?: string, + shippingAddressCity?: string, + shippingAddressCountry?: string, + shippingAddressEmail?: string, + shippingAddressZipcode?: string, + shippingAddressPhone?: string, + shippingAddressState?: string, +}; +export interface IContact extends IContactAddress{ + contactService: 'customer' | 'vendor', + contactType: string, + + balance: number, + openingBalance: number, + + firstName: string, + lastName: string, + companyName: string, + displayName: string, + + email: string, + workPhone: string, + personalPhone: string, + + note: string, + active: boolean, +} +export interface IContactNewDTO { + contactType?: string, + + openingBalance?: number, + + firstName?: string, + lastName?: string, + companyName?: string, + displayName: string, + + email?: string, + workPhone?: string, + personalPhone?: string, + + note?: string, + active: boolean, +} +export interface IContactEditDTO { + contactType?: string, + + openingBalance?: number, + + firstName?: string, + lastName?: string, + companyName?: string, + displayName: string, + + email?: string, + workPhone?: string, + personalPhone?: string, + + note?: string, + active: boolean, +} + +// Customer Interfaces. +// ---------------------------------- +export interface ICustomer extends IContact { + contactService: 'customer', +} +export interface ICustomerNewDTO extends IContactAddressDTO { + customerType: string, + + openingBalance?: number, + + firstName?: string, + lastName?: string, + companyName?: string, + displayName: string, + + email?: string, + workPhone?: string, + personalPhone?: string, + + note?: string, + active?: boolean, +}; +export interface ICustomerEditDTO extends IContactAddressDTO { + customerType: string, + + openingBalance?: number, + + firstName?: string, + lastName?: string, + companyName?: string, + displayName: string, + + email?: string, + workPhone?: string, + personalPhone?: string, + + note?: string, + active?: boolean, +}; + +// Vendor Interfaces. +// ---------------------------------- +export interface IVendor extends IContact { + contactService: 'vendor', +} +export interface IVendorNewDTO extends IContactAddressDTO { + openingBalance?: number, + + firstName?: string, + lastName?: string, + companyName?: string, + displayName: string, + + email?: string, + workPhone?: string, + personalPhone?: string, + + note?: string, + active?: boolean, +}; +export interface IVendorEditDTO extends IContactAddressDTO { + openingBalance?: number, + + firstName?: string, + lastName?: string, + companyName?: string, + displayName?: string, + + email?: string, + workPhone?: string, + personalPhone?: string, + + note?: string, + active?: boolean, +}; \ No newline at end of file diff --git a/server/src/interfaces/Journal.ts b/server/src/interfaces/Journal.ts new file mode 100644 index 000000000..ef3dab7b1 --- /dev/null +++ b/server/src/interfaces/Journal.ts @@ -0,0 +1,40 @@ + + +export interface IJournalEntry { + index?: number, + + date: Date, + credit: number, + debit: number, + account: number, + referenceType: string, + referenceId: number, + + transactionType?: string, + note?: string, + userId?: number, + contactType?: string, + contactId?: number, +}; + +export interface IJournalPoster { + credit(entry: IJournalEntry): void; + debit(entry: IJournalEntry): void; + + removeEntries(ids: number[]): void; + + saveEntries(): void; + saveBalance(): void; + deleteEntries(): void; +} + +export type TEntryType = 'credit' | 'debit'; + +export interface IAccountChange { + credit: number, + debit: number, +}; + +export interface IAccountsChange { + [key: string]: IAccountChange, +}; diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index 7b0c3355f..ad0bfc76f 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -50,6 +50,22 @@ import { IAccount, IAccountDTO, } from './Account'; +import { + IJournalEntry, + IJournalPoster, + TEntryType, + IAccountChange, + IAccountsChange, +} from './Journal'; +import { + IContactAddress, + IContact, + IContactNewDTO, + IContactEditDTO, + ICustomer, + ICustomerNewDTO, + ICustomerEditDTO, +} from './Contact'; export { IAccount, @@ -96,4 +112,18 @@ export { IOptionDTO, IOptionsDTO, + + IJournalEntry, + IJournalPoster, + TEntryType, + IAccountChange, + IAccountsChange, + + IContactAddress, + IContact, + IContactNewDTO, + IContactEditDTO, + ICustomer, + ICustomerNewDTO, + ICustomerEditDTO, }; \ No newline at end of file diff --git a/server/src/loaders/tenantModels.ts b/server/src/loaders/tenantModels.ts index 33354921e..3ce016b45 100644 --- a/server/src/loaders/tenantModels.ts +++ b/server/src/loaders/tenantModels.ts @@ -1,14 +1,16 @@ import { mapValues } from 'lodash'; import Account from '@/models/Account'; -import AccountBalance from '@/models/AccountBalance'; import AccountTransaction from '@/models/AccountTransaction'; import AccountType from '@/models/AccountType'; +import Item from '@/models/Item'; +import ItemEntry from '@/models/ItemEntry'; import Bill from '@/models/Bill'; import BillPayment from '@/models/BillPayment'; import BillPaymentEntry from '@/models/BillPaymentEntry'; import Currency from '@/models/Currency'; import Customer from '@/models/Customer'; +import Contact from '@/models/Contact'; import Vendor from '@/models/Vendor'; import ExchangeRate from '@/models/ExchangeRate'; import Expense from '@/models/Expense'; @@ -31,14 +33,19 @@ import InventoryCostLotTracker from '@/models/InventoryCostLotTracker'; import InventoryTransaction from '@/models/InventoryTransaction'; import ResourceField from '@/models/ResourceField'; import ResourceFieldMetadata from '@/models/ResourceFieldMetadata'; +import ManualJournal from '@/models/ManualJournal'; +import Media from '@/models/Media'; +import MediaLink from '@/models/MediaLink'; export default (knex) => { const models = { Option, Account, - AccountBalance, AccountTransaction, AccountType, + Item, + ItemEntry, + ManualJournal, Bill, BillPayment, BillPaymentEntry, @@ -65,6 +72,9 @@ export default (knex) => { InventoryCostLotTracker, ResourceField, ResourceFieldMetadata, + Media, + MediaLink, + Contact, }; return mapValues(models, (model) => model.bindKnex(knex)); } \ No newline at end of file diff --git a/server/src/loaders/tenantRepositories.ts b/server/src/loaders/tenantRepositories.ts new file mode 100644 index 000000000..ca793ca70 --- /dev/null +++ b/server/src/loaders/tenantRepositories.ts @@ -0,0 +1,14 @@ +import AccountRepository from '@/repositories/AccountRepository'; +import AccountTypeRepository from '@/repositories/AccountTypeRepository'; +import VendorRepository from '@/repositories/VendorRepository'; +import CustomerRepository from '@/repositories/CustomerRepository'; + + +export default (tenantId: number) => { + return { + accountRepository: new AccountRepository(tenantId), + accountTypeRepository: new AccountTypeRepository(tenantId), + customerRepository: new CustomerRepository(tenantId), + vendorRepository: new VendorRepository(tenantId), + }; +}; \ No newline at end of file diff --git a/server/src/models/Account.js b/server/src/models/Account.js index 6b2d50914..bd6351177 100644 --- a/server/src/models/Account.js +++ b/server/src/models/Account.js @@ -1,17 +1,15 @@ /* eslint-disable global-require */ -import { Model, mixin } from 'objection'; +import { Model } from 'objection'; import { flatten } from 'lodash'; import TenantModel from '@/models/TenantModel'; import { buildFilterQuery, buildSortColumnQuery, } from '@/lib/ViewRolesBuilder'; -import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; -import CachableModel from '@/lib/Cachable/CachableModel'; import { flatToNestedArray } from '@/utils'; import DependencyGraph from '@/lib/DependencyGraph'; -export default class Account extends mixin(TenantModel, [CachableModel]) { +export default class Account extends TenantModel { /** * Table name */ @@ -26,36 +24,6 @@ export default class Account extends mixin(TenantModel, [CachableModel]) { return ['createdAt', 'updatedAt']; } - /** - * Extend query builder model. - */ - static get QueryBuilder() { - return CachableQueryBuilder; - } - - /** - * Query return override. - * @param {...any} args - */ - static query(...args) { - return super.query(...args).runAfter((result) => { - if (Array.isArray(result)) { - return this.isDepGraph ? - Account.toDependencyGraph(result) : - this.collection.from(result); - } - return result; - }); - } - - /** - * Convert the array result to dependency graph. - */ - static depGraph() { - this.isDepGraph = true; - return this; - } - /** * Model modifiers. */ @@ -87,7 +55,6 @@ export default class Account extends mixin(TenantModel, [CachableModel]) { */ static get relationMappings() { const AccountType = require('@/models/AccountType'); - const AccountBalance = require('@/models/AccountBalance'); const AccountTransaction = require('@/models/AccountTransaction'); return { @@ -103,18 +70,6 @@ export default class Account extends mixin(TenantModel, [CachableModel]) { }, }, - /** - * Account model may has many balances accounts. - */ - balance: { - relation: Model.HasOneRelation, - modelClass: this.relationBindKnex(AccountBalance.default), - join: { - from: 'accounts.id', - to: 'account_balances.accountId', - }, - }, - /** * Account model may has many transactions. */ diff --git a/server/src/models/AccountBalance.js b/server/src/models/AccountBalance.js deleted file mode 100644 index e1ca55ad6..000000000 --- a/server/src/models/AccountBalance.js +++ /dev/null @@ -1,29 +0,0 @@ -import { Model } from 'objection'; -import TenantModel from '@/models/TenantModel'; - -export default class AccountBalance extends TenantModel { - /** - * Table name - */ - static get tableName() { - return 'account_balances'; - } - - /** - * Relationship mapping. - */ - static get relationMappings() { - const Account = require('@/models/Account'); - - return { - account: { - relation: Model.BelongsToOneRelation, - modelClass: this.relationBindKnex(Account.default), - join: { - from: 'account_balances.account_id', - to: 'accounts.id', - }, - }, - }; - } -} diff --git a/server/src/models/AccountTransaction.js b/server/src/models/AccountTransaction.js index 8150a7b2a..14ccf4db3 100644 --- a/server/src/models/AccountTransaction.js +++ b/server/src/models/AccountTransaction.js @@ -1,11 +1,8 @@ -import { Model, mixin } from 'objection'; +import { Model } from 'objection'; import moment from 'moment'; import TenantModel from '@/models/TenantModel'; -import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; -import CachableModel from '@/lib/Cachable/CachableModel'; - -export default class AccountTransaction extends mixin(TenantModel, [CachableModel]) { +export default class AccountTransaction extends TenantModel { /** * Table name */ @@ -20,13 +17,6 @@ export default class AccountTransaction extends mixin(TenantModel, [CachableMode return ['createdAt']; } - /** - * Extend query builder model. - */ - static get QueryBuilder() { - return CachableQueryBuilder; - } - /** * Model modifiers. */ diff --git a/server/src/models/AccountType.js b/server/src/models/AccountType.js index 06b9a0690..03e75839e 100644 --- a/server/src/models/AccountType.js +++ b/server/src/models/AccountType.js @@ -1,9 +1,8 @@ // import path from 'path'; import { Model, mixin } from 'objection'; import TenantModel from '@/models/TenantModel'; -import CachableModel from '@/lib/Cachable/CachableModel'; -export default class AccountType extends mixin(TenantModel, [CachableModel]) { +export default class AccountType extends TenantModel { /** * Table name */ diff --git a/server/src/models/Bill.js b/server/src/models/Bill.js index bbe44d344..310bc051d 100644 --- a/server/src/models/Bill.js +++ b/server/src/models/Bill.js @@ -1,12 +1,8 @@ -import { Model, mixin } from 'objection'; -import moment from 'moment'; +import { Model } from 'objection'; import { difference } from 'lodash'; import TenantModel from '@/models/TenantModel'; -import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; -import CachableModel from '@/lib/Cachable/CachableModel'; - -export default class Bill extends mixin(TenantModel, [CachableModel]) { +export default class Bill extends TenantModel { /** * Virtual attributes. */ diff --git a/server/src/models/BillPayment.js b/server/src/models/BillPayment.js index 585e7fa1a..a43463596 100644 --- a/server/src/models/BillPayment.js +++ b/server/src/models/BillPayment.js @@ -1,10 +1,7 @@ -import { Model, mixin } from 'objection'; +import { Model } from 'objection'; import TenantModel from '@/models/TenantModel'; -import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; -import CachableModel from '@/lib/Cachable/CachableModel'; - -export default class BillPayment extends mixin(TenantModel, [CachableModel]) { +export default class BillPayment extends TenantModel { /** * Table name */ diff --git a/server/src/models/Contact.js b/server/src/models/Contact.js new file mode 100644 index 000000000..a3cfdbedf --- /dev/null +++ b/server/src/models/Contact.js @@ -0,0 +1,124 @@ +import { Model } from 'objection'; +import TenantModel from '@/models/TenantModel'; + +export default class Contact extends TenantModel { + /** + * Table name + */ + static get tableName() { + return 'contacts'; + } + + /** + * Model timestamps. + */ + get timestamps() { + return ['createdAt', 'updatedAt']; + } + + /** + * Model modifiers. + */ + static get modifiers() { + return { + filterContactIds(query, customerIds) { + query.whereIn('id', customerIds); + }, + + customer(query) { + query.where('contact_service', 'customer'); + }, + + vendor(query){ + query.where('contact_service', 'vendor'); + } + }; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const SaleInvoice = require('@/models/SaleInvoice'); + const Bill = require('@/models/Bill'); + + return { + salesInvoices: { + relation: Model.HasManyRelation, + modelClass: this.relationBindKnex(SaleInvoice.default), + join: { + from: 'contacts.id', + to: 'sales_invoices.customerId', + }, + }, + + bills: { + relation: Model.HasManyRelation, + modelClass: this.relationBindKnex(Bill.default), + join: { + from: 'contacts.id', + to: 'bills.vendorId', + }, + } + }; + } + + /** + * Change vendor balance. + * @param {Integer} customerId + * @param {Numeric} amount + */ + static async changeBalance(customerId, amount) { + const changeMethod = (amount > 0) ? 'increment' : 'decrement'; + + return this.query() + .where('id', customerId) + [changeMethod]('balance', Math.abs(amount)); + } + + /** + * Increment the given customer balance. + * @param {Integer} customerId + * @param {Integer} amount + */ + static async incrementBalance(customerId, amount) { + return this.query() + .where('id', customerId) + .increment('balance', amount); + } + + /** + * Decrement the given customer balance. + * @param {integer} customerId - + * @param {integer} amount - + */ + static async decrementBalance(customerId, amount) { + await this.query() + .where('id', customerId) + .decrement('balance', amount); + } + + /** + * + * @param {number} customerId + * @param {number} oldCustomerId + * @param {number} amount + * @param {number} oldAmount + */ + static changeDiffBalance(customerId, oldCustomerId, amount, oldAmount) { + const diffAmount = amount - oldAmount; + const asyncOpers = []; + + if (customerId != oldCustomerId) { + const oldCustomerOper = this.changeBalance(oldCustomerId, (oldAmount * -1)); + const customerOper = this.changeBalance(customerId, amount); + + asyncOpers.push(customerOper); + asyncOpers.push(oldCustomerOper); + } else { + const balanceChangeOper = this.changeBalance(customerId, diffAmount); + asyncOpers.push(balanceChangeOper); + } + return Promise.all(asyncOpers); + } +} diff --git a/server/src/models/PaymentReceive.js b/server/src/models/PaymentReceive.js index 8d76b3202..ea0b7bd19 100644 --- a/server/src/models/PaymentReceive.js +++ b/server/src/models/PaymentReceive.js @@ -1,10 +1,7 @@ -import { Model, mixin } from 'objection'; -import moment from 'moment'; +import { Model } from 'objection'; import TenantModel from '@/models/TenantModel'; -import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; -import CachableModel from '@/lib/Cachable/CachableModel'; -export default class PaymentReceive extends mixin(TenantModel, [CachableModel]) { +export default class PaymentReceive extends TenantModel { /** * Table name */ diff --git a/server/src/models/PaymentReceiveEntry.js b/server/src/models/PaymentReceiveEntry.js index 72a74883f..ed0e4397a 100644 --- a/server/src/models/PaymentReceiveEntry.js +++ b/server/src/models/PaymentReceiveEntry.js @@ -1,9 +1,7 @@ import { Model, mixin } from 'objection'; import TenantModel from '@/models/TenantModel'; -import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; -import CachableModel from '@/lib/Cachable/CachableModel'; -export default class PaymentReceiveEntry extends mixin(TenantModel, [CachableModel]) { +export default class PaymentReceiveEntry extends TenantModel { /** * Table name */ @@ -18,13 +16,6 @@ export default class PaymentReceiveEntry extends mixin(TenantModel, [CachableMod return []; } - /** - * Extend query builder model. - */ - static get QueryBuilder() { - return CachableQueryBuilder; - } - /** * Relationship mapping. */ @@ -34,7 +25,6 @@ export default class PaymentReceiveEntry extends mixin(TenantModel, [CachableMod return { /** - * */ entries: { relation: Model.HasManyRelation, diff --git a/server/src/models/Resource.js b/server/src/models/Resource.js index a913c8778..40a9d18ff 100644 --- a/server/src/models/Resource.js +++ b/server/src/models/Resource.js @@ -1,9 +1,7 @@ -import { Model, mixin } from 'objection'; +import { Model } from 'objection'; import TenantModel from '@/models/TenantModel'; -import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; -import CachableModel from '@/lib/Cachable/CachableModel'; -export default class Resource extends mixin(TenantModel, [CachableModel]) { +export default class Resource extends TenantModel { /** * Table name. */ @@ -11,13 +9,6 @@ export default class Resource extends mixin(TenantModel, [CachableModel]) { return 'resources'; } - /** - * Extend query builder model. - */ - static get QueryBuilder() { - return CachableQueryBuilder; - } - /** * Timestamp columns. */ diff --git a/server/src/models/SaleEstimate.js b/server/src/models/SaleEstimate.js index c17f562b2..55b043a45 100644 --- a/server/src/models/SaleEstimate.js +++ b/server/src/models/SaleEstimate.js @@ -1,9 +1,7 @@ import { Model, mixin } from 'objection'; import TenantModel from '@/models/TenantModel'; -import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; -import CachableModel from '@/lib/Cachable/CachableModel'; -export default class SaleEstimate extends mixin(TenantModel, [CachableModel]) { +export default class SaleEstimate extends TenantModel { /** * Table name */ diff --git a/server/src/models/SaleEstimateEntry.js b/server/src/models/SaleEstimateEntry.js index 3bf5a6789..cf1866588 100644 --- a/server/src/models/SaleEstimateEntry.js +++ b/server/src/models/SaleEstimateEntry.js @@ -1,9 +1,8 @@ -import { Model, mixin } from 'objection'; +import { Model } from 'objection'; import TenantModel from '@/models/TenantModel'; -import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; -import CachableModel from '@/lib/Cachable/CachableModel'; -export default class SaleEstimateEntry extends mixin(TenantModel, [CachableModel]) { + +export default class SaleEstimateEntry extends TenantModel { /** * Table name */ @@ -11,20 +10,6 @@ export default class SaleEstimateEntry extends mixin(TenantModel, [CachableModel return 'sales_estimate_entries'; } - /** - * Timestamps columns. - */ - get timestamps() { - return []; - } - - /** - * Extend query builder model. - */ - static get QueryBuilder() { - return CachableQueryBuilder; - } - /** * Relationship mapping. */ diff --git a/server/src/models/SaleInvoice.js b/server/src/models/SaleInvoice.js index f08d9cb5c..fe92e554d 100644 --- a/server/src/models/SaleInvoice.js +++ b/server/src/models/SaleInvoice.js @@ -1,11 +1,8 @@ import { Model, mixin } from 'objection'; import moment from 'moment'; import TenantModel from '@/models/TenantModel'; -import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; -import CachableModel from '@/lib/Cachable/CachableModel'; -import InventoryCostLotTracker from './InventoryCostLotTracker'; -export default class SaleInvoice extends mixin(TenantModel, [CachableModel]) { +export default class SaleInvoice extends TenantModel { /** * Virtual attributes. */ diff --git a/server/src/models/SaleInvoiceEntry.js b/server/src/models/SaleInvoiceEntry.js index 64a5c7d1f..2bcb483c7 100644 --- a/server/src/models/SaleInvoiceEntry.js +++ b/server/src/models/SaleInvoiceEntry.js @@ -1,10 +1,7 @@ -import { Model, mixin } from 'objection'; -import moment from 'moment'; +import { Model } from 'objection'; import TenantModel from '@/models/TenantModel'; -import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; -import CachableModel from '@/lib/Cachable/CachableModel'; -export default class SaleInvoiceEntry extends mixin(TenantModel, [CachableModel]) { +export default class SaleInvoiceEntry extends TenantModel { /** * Table name */ @@ -12,20 +9,6 @@ export default class SaleInvoiceEntry extends mixin(TenantModel, [CachableModel] return 'sales_invoices_entries'; } - /** - * Timestamps columns. - */ - get timestamps() { - return []; - } - - /** - * Extend query builder model. - */ - static get QueryBuilder() { - return CachableQueryBuilder; - } - /** * Relationship mapping. */ diff --git a/server/src/models/SaleReceipt.js b/server/src/models/SaleReceipt.js index 160634847..60f3d930b 100644 --- a/server/src/models/SaleReceipt.js +++ b/server/src/models/SaleReceipt.js @@ -1,10 +1,7 @@ import { Model, mixin } from 'objection'; -import moment from 'moment'; import TenantModel from '@/models/TenantModel'; -import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; -import CachableModel from '@/lib/Cachable/CachableModel'; -export default class SaleReceipt extends mixin(TenantModel, [CachableModel]) { +export default class SaleReceipt extends TenantModel { /** * Table name */ diff --git a/server/src/models/SaleReceiptEntry.js b/server/src/models/SaleReceiptEntry.js index 23f5a5bc2..abc71cd2b 100644 --- a/server/src/models/SaleReceiptEntry.js +++ b/server/src/models/SaleReceiptEntry.js @@ -1,9 +1,7 @@ -import { Model, mixin } from 'objection'; +import { Model } from 'objection'; import TenantModel from '@/models/TenantModel'; -import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; -import CachableModel from '@/lib/Cachable/CachableModel'; -export default class SaleReceiptEntry extends mixin(TenantModel, [CachableModel]) { +export default class SaleReceiptEntry extends TenantModel { /** * Table name */ @@ -11,20 +9,6 @@ export default class SaleReceiptEntry extends mixin(TenantModel, [CachableModel] return 'sales_receipt_entries'; } - /** - * Timestamps columns. - */ - get timestamps() { - return []; - } - - /** - * Extend query builder model. - */ - static get QueryBuilder() { - return CachableQueryBuilder; - } - /** * Relationship mapping. */ diff --git a/server/src/models/Setting.js b/server/src/models/Setting.js index 92cf7d625..86ef5c15b 100644 --- a/server/src/models/Setting.js +++ b/server/src/models/Setting.js @@ -9,13 +9,6 @@ export default class Setting extends TenantModel { return 'settings'; } - /** - * Timestamp columns. - */ - static get hasTimestamps() { - return false; - } - /** * Extra metadata query to query with the current authenticate user. * @param {Object} query diff --git a/server/src/models/TenantModel.js b/server/src/models/TenantModel.js index 84ba3bf10..ae3a6d56e 100644 --- a/server/src/models/TenantModel.js +++ b/server/src/models/TenantModel.js @@ -1,17 +1,5 @@ import BaseModel from '@/models/Model'; export default class TenantModel extends BaseModel { - static tenant() { - if (!this.knexBinded) { - throw new Error('Tenant knex is not binded yet.'); - } - return super.bindKnex(this.knexBinded); - } - /** - * Allow to embed models to express request. - */ - static requestModel() { - return true; - } } diff --git a/server/src/models/Vendor.js b/server/src/models/Vendor.js index 0b66b14d7..68f21b963 100644 --- a/server/src/models/Vendor.js +++ b/server/src/models/Vendor.js @@ -12,7 +12,7 @@ export default class Vendor extends TenantModel { /** * Model timestamps. */ - static get timestamps() { + get timestamps() { return ['createdAt', 'updatedAt']; } diff --git a/server/src/models/View.js b/server/src/models/View.js index 87d366a6c..02bdc54e0 100644 --- a/server/src/models/View.js +++ b/server/src/models/View.js @@ -1,9 +1,7 @@ import { Model, mixin } from 'objection'; import TenantModel from '@/models/TenantModel'; -import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; -import CachableModel from '@/lib/Cachable/CachableModel'; -export default class View extends mixin(TenantModel, [CachableModel]) { +export default class View extends TenantModel { /** * Table name. */ @@ -18,13 +16,6 @@ export default class View extends mixin(TenantModel, [CachableModel]) { return ['createdAt', 'updatedAt']; } - /** - * Extend query builder model. - */ - static get QueryBuilder() { - return CachableQueryBuilder; - } - static get modifiers() { const TABLE_NAME = View.tableName; diff --git a/server/src/models/ViewColumn.js b/server/src/models/ViewColumn.js index a96979fe1..0ad44bcb9 100644 --- a/server/src/models/ViewColumn.js +++ b/server/src/models/ViewColumn.js @@ -9,14 +9,6 @@ export default class ViewColumn extends TenantModel { return 'view_has_columns'; } - /** - * Timestamp columns. - */ - static get hasTimestamps() { - return false; - } - - /** * Relationship mapping. */ diff --git a/server/src/models/ViewRole.js b/server/src/models/ViewRole.js index 0a89e7217..6e0c89989 100644 --- a/server/src/models/ViewRole.js +++ b/server/src/models/ViewRole.js @@ -23,13 +23,6 @@ export default class ViewRole extends TenantModel { return 'view_roles'; } - /** - * Timestamp columns. - */ - static get hasTimestamps() { - return false; - } - /** * Relationship mapping. */ diff --git a/server/src/repositories/AccountRepository.ts b/server/src/repositories/AccountRepository.ts new file mode 100644 index 000000000..396d8d893 --- /dev/null +++ b/server/src/repositories/AccountRepository.ts @@ -0,0 +1,55 @@ +import TenantRepository from '@/repositories/TenantRepository'; + +export default class AccountRepository extends TenantRepository { + models: any; + repositories: any; + cache: any; + + /** + * Constructor method. + * @param {number} tenantId - The given tenant id. + */ + constructor( + tenantId: number, + ) { + super(tenantId); + + this.models = this.tenancy.models(tenantId); + this.cache = this.tenancy.cache(tenantId); + } + + /** + * Retrieve accounts dependency graph. + * @returns {} + */ + async getDependencyGraph() { + const { Account } = this.models; + const accounts = await this.allAccounts(); + + return this.cache.get('accounts.depGraph', async () => { + return Account.toDependencyGraph(accounts); + }); + } + + /** + * Retrieve all accounts on the storage. + * @return {} + */ + async allAccounts() { + const { Account } = this.models; + return this.cache.get('accounts', async () => { + return Account.query(); + }); + } + + /** + * Retrieve account of the given account slug. + * @param {string} slug + */ + async getBySlug(slug: string) { + const { Account } = this.models; + return this.cache.get(`accounts.slug.${slug}`, () => { + return Account.query().findOne('slug', slug); + }); + } +} \ No newline at end of file diff --git a/server/src/repositories/AccountTypeRepository.ts b/server/src/repositories/AccountTypeRepository.ts new file mode 100644 index 000000000..ec47bbdcc --- /dev/null +++ b/server/src/repositories/AccountTypeRepository.ts @@ -0,0 +1,30 @@ +import TenantRepository from '@/repositories/TenantRepository'; + +export default class AccountTypeRepository extends TenantRepository { + cache: any; + models: any; + + /** + * Constructor method. + * @param {number} tenantId - The given tenant id. + */ + constructor( + tenantId: number, + ) { + super(tenantId); + + this.models = this.tenancy.models(tenantId); + this.cache = this.tenancy.cache(tenantId); + } + + /** + * Retrieve account type meta. + * @param {number} accountTypeId + */ + getTypeMeta(accountTypeId: number) { + const { AccountType } = this.models; + return this.cache.get(`accountType.${accountTypeId}`, () => { + return AccountType.query().findById(accountTypeId); + }); + } +} \ No newline at end of file diff --git a/server/src/repositories/CustomerRepository.ts b/server/src/repositories/CustomerRepository.ts new file mode 100644 index 000000000..ce019acc6 --- /dev/null +++ b/server/src/repositories/CustomerRepository.ts @@ -0,0 +1,71 @@ +import TenantRepository from "./TenantRepository"; + +export default class CustomerRepository extends TenantRepository { + models: any; + cache: any; + + /** + * Constructor method. + * @param {number} tenantId + */ + constructor(tenantId: number) { + super(tenantId); + + this.models = this.tenancy.models(tenantId); + this.cache = this.tenancy.cache(tenantId); + } + + /** + * Retrieve customer details of the given id. + * @param {number} customerId - + */ + getById(customerId: number) { + const { Contact } = this.models; + + return this.cache.get(`customers.id.${customerId}`, () => { + return Contact.query().modifier('customer').findById(customerId); + }); + } + + /** + * Detarmines the given customer exists. + * @param {number} customerId + * @returns {boolean} + */ + isExists(customerId: number) { + return !!this.getById(customerId); + } + + /** + * Retrieve the sales invoices that assocaited to the given customer. + * @param {number} customerId + */ + getSalesInvoices(customerId: number) { + const { SaleInvoice } = this.models; + + return this.cache.get(`customers.invoices.${customerId}`, () => { + return SaleInvoice.query().where('customer_id', customerId); + }); + } + + /** + * Retrieve customers details of the given ids. + * @param {number[]} customersIds - Customers ids. + * @return {IContact[]} + */ + customers(customersIds: number[]) { + const { Contact } = this.models; + return Contact.query().modifier('customer').whereIn('id', customersIds); + } + + /** + * Retrieve customers of the given ids with associated sales invoices. + * @param {number[]} customersIds - Customers ids. + */ + customersWithSalesInvoices(customersIds: number[]) { + const { Contact } = this.models; + return Contact.query().modify('customer') + .whereIn('id', customersIds) + .withGraphFetched('salesInvoices'); + } +} \ No newline at end of file diff --git a/server/src/repositories/TenantRepository.ts b/server/src/repositories/TenantRepository.ts new file mode 100644 index 000000000..076dcf677 --- /dev/null +++ b/server/src/repositories/TenantRepository.ts @@ -0,0 +1,16 @@ +import { Container } from 'typedi'; +import TenancyService from '@/services/Tenancy/TenancyService'; + +export default class TenantRepository { + tenantId: number; + tenancy: TenancyService; + + /** + * Constructor method. + * @param {number} tenantId + */ + constructor(tenantId: number) { + this.tenantId = tenantId; + this.tenancy = Container.get(TenancyService); + } +} \ No newline at end of file diff --git a/server/src/repositories/VendorRepository.ts b/server/src/repositories/VendorRepository.ts new file mode 100644 index 000000000..a02443a0b --- /dev/null +++ b/server/src/repositories/VendorRepository.ts @@ -0,0 +1,50 @@ +import TenantRepository from "./TenantRepository"; + + +export default class VendorRepository extends TenantRepository { + models: any; + cache: any; + + /** + * Constructor method. + * @param {number} tenantId + */ + constructor(tenantId: number) { + super(tenantId); + + this.models = this.tenancy.models(tenantId); + this.cache = this.tenancy.cache(tenantId); + } + + /** + * Retrieve the bill that associated to the given vendor id. + * @param {number} vendorId + */ + getBills(vendorId: number) { + const { Bill } = this.models; + + return this.cache.get(`vendors.bills.${vendorId}`, () => { + return Bill.query().where('vendor_id', vendorId); + }); + } + + /** + * + * @param {numner[]} vendorsIds + */ + vendors(vendorsIds: number[]) { + const { Contact } = this.models; + return Contact.query().modifier('vendor').whereIn('id', vendorsIds); + } + + /** + * + * @param {number[]} vendorIds + */ + vendorsWithBills(vendorIds: number[]) { + const { Contact } = this.models; + return Contact.query().modify('vendor') + .whereIn('id', vendorIds) + .withGraphFetched('bills'); + } +} diff --git a/server/src/server.js b/server/src/server.js index a49034724..2dbff50ec 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -1,14 +1,12 @@ import 'reflect-metadata'; // We need this in order to use @Decorators - +import moment from 'moment'; +moment.prototype.toMySqlDateTime = function () { + return this.format('YYYY-MM-DD HH:mm:ss'); +}; import express from 'express'; import rootPath from 'app-root-path'; import loadersFactory from '@/loaders'; import '../config'; -import moment from 'moment'; - -moment.prototype.toMySqlDateTime = function () { - return this.format('YYYY-MM-DD HH:mm:ss'); -}; global.rootPath = rootPath.path; diff --git a/server/src/services/Accounting/Accounting.js b/server/src/services/Accounting/Accounting.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/server/src/services/Accounting/JournalCommands.ts b/server/src/services/Accounting/JournalCommands.ts index 964becd23..6c880abea 100644 --- a/server/src/services/Accounting/JournalCommands.ts +++ b/server/src/services/Accounting/JournalCommands.ts @@ -40,13 +40,84 @@ interface NonInventoryJEntries { export default class JournalCommands{ journal: JournalPoster; + models: any; + repositories: any; + /** * Constructor method. * @param {JournalPoster} journal - */ constructor(journal: JournalPoster) { this.journal = journal; - Object.assign(this, arguments[1]); + + this.repositories = this.journal.repositories; + this.models = this.journal.models; + } + + /** + * Customer opening balance journals. + * @param {number} customerId + * @param {number} openingBalance + */ + async customerOpeningBalance(customerId: number, openingBalance: number) { + const { accountRepository } = this.repositories; + + const openingBalanceAccount = await accountRepository.getBySlug('opening-balance'); + const receivableAccount = await accountRepository.getBySlug('accounts-receivable'); + + const commonEntry = { + referenceType: 'CustomerOpeningBalance', + referenceId: customerId, + contactType: 'Customer', + contactId: customerId, + }; + const creditEntry = new JournalEntry({ + ...commonEntry, + credit: openingBalance, + debit: 0, + account: openingBalanceAccount.id, + }); + const debitEntry = new JournalEntry({ + ...commonEntry, + credit: 0, + debit: openingBalance, + account: receivableAccount.id, + }); + this.journal.debit(debitEntry); + this.journal.credit(creditEntry); + } + + /** + * Vendor opening balance journals + * @param {number} vendorId + * @param {number} openingBalance + */ + async vendorOpeningBalance(vendorId: number, openingBalance: number) { + const { accountRepository } = this.repositories; + + const payableAccount = await accountRepository.getBySlug('accounts-payable'); + const otherCost = await accountRepository.getBySlug('other-expenses'); + + const commonEntry = { + referenceType: 'VendorOpeningBalance', + referenceId: vendorId, + contactType: 'Vendor', + contactId: vendorId, + }; + const creditEntry = new JournalEntry({ + ...commonEntry, + account: payableAccount.id, + credit: openingBalance, + debit: 0, + }); + const debitEntry = new JournalEntry({ + ...commonEntry, + account: otherCost.id, + debit: openingBalance, + credit: 0, + }); + this.journal.debit(debitEntry); + this.journal.credit(creditEntry); } /** diff --git a/server/src/services/Accounting/JournalEntry.js b/server/src/services/Accounting/JournalEntry.ts similarity index 100% rename from server/src/services/Accounting/JournalEntry.js rename to server/src/services/Accounting/JournalEntry.ts diff --git a/server/src/services/Accounting/JournalFinancial.ts b/server/src/services/Accounting/JournalFinancial.ts new file mode 100644 index 000000000..aa8081d84 --- /dev/null +++ b/server/src/services/Accounting/JournalFinancial.ts @@ -0,0 +1,207 @@ +import moment from 'moment'; + +export default class JournalFinancial { + accountsBalanceTable: { [key: number]: number; } = {}; + + /** + * Retrieve the closing balance for the given account and closing date. + * @param {Number} accountId - + * @param {Date} closingDate - + * @param {string} dataType? - + * @return {number} + */ + getClosingBalance( + accountId: number, + closingDate: Date|string, + dateType: string = 'day' + ): number { + let closingBalance = 0; + const momentClosingDate = moment(closingDate); + + this.entries.forEach((entry) => { + // Can not continue if not before or event same closing date. + if ( + (!momentClosingDate.isAfter(entry.date, dateType) && + !momentClosingDate.isSame(entry.date, dateType)) || + (entry.account !== accountId && accountId) + ) { + return; + } + if (entry.accountNormal === 'credit') { + closingBalance += entry.credit ? entry.credit : -1 * entry.debit; + } else if (entry.accountNormal === 'debit') { + closingBalance += entry.debit ? entry.debit : -1 * entry.credit; + } + }); + return closingBalance; + } + + /** + * Retrieve the given account balance with dependencies accounts. + * @param {Number} accountId + * @param {Date} closingDate + * @param {String} dateType + * @return {Number} + */ + getAccountBalance(accountId: number, closingDate: Date|string, dateType: string) { + const accountNode = this.accountsDepGraph.getNodeData(accountId); + const depAccountsIds = this.accountsDepGraph.dependenciesOf(accountId); + const depAccounts = depAccountsIds + .map((id) => this.accountsDepGraph.getNodeData(id)); + + let balance: number = 0; + + [...depAccounts, accountNode].forEach((account) => { + const closingBalance = this.getClosingBalance( + account.id, + closingDate, + dateType + ); + this.accountsBalanceTable[account.id] = closingBalance; + balance += this.accountsBalanceTable[account.id]; + }); + return balance; + } + + /** + * Retrieve the credit/debit sumation for the given account and date. + * @param {Number} account - + * @param {Date|String} closingDate - + */ + getTrialBalance(accountId, closingDate, dateType) { + const momentClosingDate = moment(closingDate); + const result = { + credit: 0, + debit: 0, + balance: 0, + }; + this.entries.forEach((entry) => { + if ( + (!momentClosingDate.isAfter(entry.date, dateType) && + !momentClosingDate.isSame(entry.date, dateType)) || + (entry.account !== accountId && accountId) + ) { + return; + } + result.credit += entry.credit; + result.debit += entry.debit; + + if (entry.accountNormal === 'credit') { + result.balance += entry.credit - entry.debit; + } else if (entry.accountNormal === 'debit') { + result.balance += entry.debit - entry.credit; + } + }); + return result; + } + + /** + * Retrieve trial balance of the given account with depends. + * @param {Number} accountId + * @param {Date} closingDate + * @param {String} dateType + * @return {Number} + */ + + getTrialBalanceWithDepands(accountId: number, closingDate: Date, dateType: string) { + const accountNode = this.accountsDepGraph.getNodeData(accountId); + const depAccountsIds = this.accountsDepGraph.dependenciesOf(accountId); + const depAccounts = depAccountsIds.map((id) => + this.accountsDepGraph.getNodeData(id) + ); + const trialBalance = { credit: 0, debit: 0, balance: 0 }; + + [...depAccounts, accountNode].forEach((account) => { + const _trialBalance = this.getTrialBalance( + account.id, + closingDate, + dateType + ); + + trialBalance.credit += _trialBalance.credit; + trialBalance.debit += _trialBalance.debit; + trialBalance.balance += _trialBalance.balance; + }); + return trialBalance; + } + + getContactTrialBalance( + accountId: number, + contactId: number, + contactType: string, + closingDate: Date|string, + openingDate: Date|string, + ) { + const momentClosingDate = moment(closingDate); + const momentOpeningDate = moment(openingDate); + const trial = { + credit: 0, + debit: 0, + balance: 0, + }; + + this.entries.forEach((entry) => { + if ( + (closingDate && + !momentClosingDate.isAfter(entry.date, 'day') && + !momentClosingDate.isSame(entry.date, 'day')) || + (openingDate && + !momentOpeningDate.isBefore(entry.date, 'day') && + !momentOpeningDate.isSame(entry.date)) || + (accountId && entry.account !== accountId) || + (contactId && entry.contactId !== contactId) || + entry.contactType !== contactType + ) { + return; + } + if (entry.credit) { + trial.balance -= entry.credit; + trial.credit += entry.credit; + } + if (entry.debit) { + trial.balance += entry.debit; + trial.debit += entry.debit; + } + }); + return trial; + } + + /** + * Retrieve total balnace of the given customer/vendor contact. + * @param {Number} accountId + * @param {Number} contactId + * @param {String} contactType + * @param {Date} closingDate + */ + getContactBalance( + accountId: number, + contactId: number, + contactType: string, + closingDate: Date, + openingDate: Date, + ) { + const momentClosingDate = moment(closingDate); + let balance = 0; + + this.entries.forEach((entry) => { + if ( + (closingDate && + !momentClosingDate.isAfter(entry.date, 'day') && + !momentClosingDate.isSame(entry.date, 'day')) || + (entry.account !== accountId && accountId) || + (contactId && entry.contactId !== contactId) || + entry.contactType !== contactType + ) { + return; + } + if (entry.credit) { + balance -= entry.credit; + } + if (entry.debit) { + balance += entry.debit; + } + }); + return balance; + } + +} \ No newline at end of file diff --git a/server/src/services/Accounting/JournalPoster.js b/server/src/services/Accounting/JournalPoster.js deleted file mode 100644 index 652573533..000000000 --- a/server/src/services/Accounting/JournalPoster.js +++ /dev/null @@ -1,502 +0,0 @@ -import { pick } from 'lodash'; -import moment from 'moment'; -import JournalEntry from '@/services/Accounting/JournalEntry'; -import AccountTransaction from '@/models/AccountTransaction'; -import AccountBalance from '@/models/AccountBalance'; -import { promiseSerial } from '@/utils'; -import Account from '@/models/Account'; -import NestedSet from '../../collection/NestedSet'; - -export default class JournalPoster { - /** - * Journal poster constructor. - */ - constructor(accountsGraph) { - this.entries = []; - this.balancesChange = {}; - this.deletedEntriesIds = []; - - this.accountsBalanceTable = {}; - this.accountsGraph = accountsGraph; - } - - /** - * Writes the credit entry for the given account. - * @param {JournalEntry} entry - - */ - credit(entryModel) { - if (entryModel instanceof JournalEntry === false) { - throw new Error('The entry is not instance of JournalEntry.'); - } - this.entries.push(entryModel.entry); - this.setAccountBalanceChange(entryModel.entry, 'credit'); - } - - /** - * Writes the debit entry for the given account. - * @param {JournalEntry} entry - - */ - debit(entryModel) { - if (entryModel instanceof JournalEntry === false) { - throw new Error('The entry is not instance of JournalEntry.'); - } - this.entries.push(entryModel.entry); - this.setAccountBalanceChange(entryModel.entry, 'debit'); - } - - /** - * Sets account balance change. - * @param {JournalEntry} entry - * @param {String} type - */ - setAccountBalanceChange(entry, entryType) { - const depAccountsIds = this.accountsGraph.dependantsOf(entry.account); - - const balanceChangeEntry = { - debit: entry.debit, - credit: entry.credit, - entryType, - accountNormal: entry.accountNormal, - }; - this._setAccountBalanceChange({ - ...balanceChangeEntry, - accountId: entry.account, - }); - - if (entry.contactType && entry.contactId) { - - } - - // Effect parent accounts of the given account id. - depAccountsIds.forEach((accountId) => { - this._setAccountBalanceChange({ - ...balanceChangeEntry, - accountId, - }); - }); - } - - /** - * Sets account balance change. - * @private - */ - _setAccountBalanceChange({ - accountId, - accountNormal, - debit, - credit, - entryType, - }) { - if (!this.balancesChange[accountId]) { - this.balancesChange[accountId] = 0; - } - let change = 0; - - if (accountNormal === 'credit') { - change = entryType === 'credit' ? credit : -1 * debit; - } else if (accountNormal === 'debit') { - change = entryType === 'debit' ? debit : -1 * credit; - } - this.balancesChange[accountId] += change; - } - - /** - * Set contact balance change. - * @param {Object} param - - */ - _setContactBalanceChange({ - contactType, - contactId, - - accountNormal, - debit, - credit, - entryType, - }) { - - } - - /** - * Mapping the balance change to list. - */ - mapBalanceChangesToList() { - const mappedList = []; - - Object.keys(this.balancesChange).forEach((accountId) => { - const balance = this.balancesChange[accountId]; - - mappedList.push({ - account_id: accountId, - amount: balance, - }); - }); - return mappedList; - } - - /** - * Saves the balance change of journal entries. - */ - async saveBalance() { - const balancesList = this.mapBalanceChangesToList(); - const balanceUpdateOpers = []; - const balanceInsertOpers = []; - const balanceFindOneOpers = []; - let balanceAccounts = []; - - balancesList.forEach((balance) => { - const oper = AccountBalance.tenant() - .query() - .findOne('account_id', balance.account_id); - balanceFindOneOpers.push(oper); - }); - balanceAccounts = await Promise.all(balanceFindOneOpers); - - balancesList.forEach((balance) => { - const method = balance.amount < 0 ? 'decrement' : 'increment'; - - // Detarmine if the account balance is already exists or not. - const foundAccBalance = balanceAccounts.some( - (account) => account && account.account_id === balance.account_id - ); - - if (foundAccBalance) { - const query = AccountBalance.tenant() - .query() - [method]('amount', Math.abs(balance.amount)) - .where('account_id', balance.account_id); - - balanceUpdateOpers.push(query); - } else { - const query = AccountBalance.tenant().query().insert({ - account_id: balance.account_id, - amount: balance.amount, - currency_code: 'USD', - }); - balanceInsertOpers.push(query); - } - }); - await Promise.all([...balanceUpdateOpers, ...balanceInsertOpers]); - } - - /** - * Saves the stacked journal entries to the storage. - */ - async saveEntries() { - const saveOperations = []; - - this.entries.forEach((entry) => { - const oper = AccountTransaction.tenant() - .query() - .insert({ - accountId: entry.account, - ...pick(entry, [ - 'credit', - 'debit', - 'transactionType', - 'date', - 'userId', - 'referenceType', - 'referenceId', - 'note', - 'contactId', - 'contactType', - ]), - }); - saveOperations.push(() => oper); - }); - await promiseSerial(saveOperations); - } - - /** - * Reverses the stacked journal entries. - */ - reverseEntries() { - const reverseEntries = []; - - this.entries.forEach((entry) => { - const reverseEntry = { ...entry }; - - if (entry.credit) { - reverseEntry.debit = entry.credit; - } - if (entry.debit) { - reverseEntry.credit = entry.debit; - } - reverseEntries.push(reverseEntry); - }); - this.entries = reverseEntries; - } - - /** - * - * @param {Array} ids - - */ - removeEntries(ids = []) { - const targetIds = ids.length <= 0 ? this.entries.map((e) => e.id) : ids; - const removeEntries = this.entries.filter( - (e) => targetIds.indexOf(e.id) !== -1 - ); - - this.entries = this.entries.filter((e) => targetIds.indexOf(e.id) === -1); - - removeEntries.forEach((entry) => { - entry.credit = -1 * entry.credit; - entry.debit = -1 * entry.debit; - - this.setAccountBalanceChange(entry, entry.accountNormal); - }); - this.deletedEntriesIds.push(...removeEntries.map((entry) => entry.id)); - } - - /** - * Revert the given transactions. - * @param {*} entries - */ - removeTransactions(entries) { - this.loadEntries(entries); - - - this.deletedEntriesIds.push(...entriesIDsShouldDel); - } - - /** - * Delete all the stacked entries. - */ - async deleteEntries() { - if (this.deletedEntriesIds.length > 0) { - await AccountTransaction.tenant() - .query() - .whereIn('id', this.deletedEntriesIds) - .delete(); - } - } - - /** - * Retrieve the closing balance for the given account and closing date. - * @param {Number} accountId - - * @param {Date} closingDate - - */ - getClosingBalance(accountId, closingDate, dateType = 'day') { - let closingBalance = 0; - const momentClosingDate = moment(closingDate); - - this.entries.forEach((entry) => { - // Can not continue if not before or event same closing date. - if ( - (!momentClosingDate.isAfter(entry.date, dateType) && - !momentClosingDate.isSame(entry.date, dateType)) || - (entry.account !== accountId && accountId) - ) { - return; - } - if (entry.accountNormal === 'credit') { - closingBalance += entry.credit ? entry.credit : -1 * entry.debit; - } else if (entry.accountNormal === 'debit') { - closingBalance += entry.debit ? entry.debit : -1 * entry.credit; - } - }); - return closingBalance; - } - - /** - * Retrieve the given account balance with dependencies accounts. - * @param {Number} accountId - * @param {Date} closingDate - * @param {String} dateType - * @return {Number} - */ - getAccountBalance(accountId, closingDate, dateType) { - const accountNode = this.accountsGraph.getNodeData(accountId); - const depAccountsIds = this.accountsGraph.dependenciesOf(accountId); - const depAccounts = depAccountsIds.map((id) => - this.accountsGraph.getNodeData(id) - ); - let balance = 0; - - [...depAccounts, accountNode].forEach((account) => { - // if (!this.accountsBalanceTable[account.id]) { - const closingBalance = this.getClosingBalance( - account.id, - closingDate, - dateType - ); - this.accountsBalanceTable[account.id] = closingBalance; - // } - balance += this.accountsBalanceTable[account.id]; - }); - return balance; - } - - /** - * Retrieve the credit/debit sumation for the given account and date. - * @param {Number} account - - * @param {Date|String} closingDate - - */ - getTrialBalance(accountId, closingDate, dateType) { - const momentClosingDate = moment(closingDate); - const result = { - credit: 0, - debit: 0, - balance: 0, - }; - this.entries.forEach((entry) => { - if ( - (!momentClosingDate.isAfter(entry.date, dateType) && - !momentClosingDate.isSame(entry.date, dateType)) || - (entry.account !== accountId && accountId) - ) { - return; - } - result.credit += entry.credit; - result.debit += entry.debit; - - if (entry.accountNormal === 'credit') { - result.balance += entry.credit - entry.debit; - } else if (entry.accountNormal === 'debit') { - result.balance += entry.debit - entry.credit; - } - }); - return result; - } - - /** - * Retrieve trial balance of the given account with depends. - * @param {Number} accountId - * @param {Date} closingDate - * @param {String} dateType - * @return {Number} - */ - - getTrialBalanceWithDepands(accountId, closingDate, dateType) { - const accountNode = this.accountsGraph.getNodeData(accountId); - const depAccountsIds = this.accountsGraph.dependenciesOf(accountId); - const depAccounts = depAccountsIds.map((id) => - this.accountsGraph.getNodeData(id) - ); - - const trialBalance = { credit: 0, debit: 0, balance: 0 }; - - [...depAccounts, accountNode].forEach((account) => { - const _trialBalance = this.getTrialBalance( - account.id, - closingDate, - dateType - ); - - trialBalance.credit += _trialBalance.credit; - trialBalance.debit += _trialBalance.debit; - trialBalance.balance += _trialBalance.balance; - }); - return trialBalance; - } - - getContactTrialBalance( - accountId, - contactId, - contactType, - closingDate, - openingDate - ) { - const momentClosingDate = moment(closingDate); - const momentOpeningDate = moment(openingDate); - const trial = { - credit: 0, - debit: 0, - balance: 0, - }; - - this.entries.forEach((entry) => { - if ( - (closingDate && - !momentClosingDate.isAfter(entry.date, 'day') && - !momentClosingDate.isSame(entry.date, 'day')) || - (openingDate && - !momentOpeningDate.isBefore(entry.date, 'day') && - !momentOpeningDate.isSame(entry.date)) || - (accountId && entry.account !== accountId) || - (contactId && entry.contactId !== contactId) || - entry.contactType !== contactType - ) { - return; - } - if (entry.credit) { - trial.balance -= entry.credit; - trial.credit += entry.credit; - } - if (entry.debit) { - trial.balance += entry.debit; - trial.debit += entry.debit; - } - }); - return trial; - } - - /** - * Retrieve total balnace of the given customer/vendor contact. - * @param {Number} accountId - * @param {Number} contactId - * @param {String} contactType - * @param {Date} closingDate - */ - getContactBalance( - accountId, - contactId, - contactType, - closingDate, - openingDate - ) { - const momentClosingDate = moment(closingDate); - let balance = 0; - - this.entries.forEach((entry) => { - if ( - (closingDate && - !momentClosingDate.isAfter(entry.date, 'day') && - !momentClosingDate.isSame(entry.date, 'day')) || - (entry.account !== accountId && accountId) || - (contactId && entry.contactId !== contactId) || - entry.contactType !== contactType - ) { - return; - } - if (entry.credit) { - balance -= entry.credit; - } - if (entry.debit) { - balance += entry.debit; - } - }); - return balance; - } - - /** - * Load fetched accounts journal entries. - * @param {Array} entries - - */ - loadEntries(entries) { - entries.forEach((entry) => { - this.entries.push({ - ...entry, - account: entry.account ? entry.account.id : entry.accountId, - accountNormal: - entry.account && entry.account.type - ? entry.account.type.normal - : entry.accountNormal, - }); - }); - } - - /** - * Calculates the entries balance change. - */ - calculateEntriesBalanceChange() { - this.entries.forEach((entry) => { - if (entry.credit) { - this.setAccountBalanceChange(entry, 'credit'); - } - if (entry.debit) { - this.setAccountBalanceChange(entry, 'debit'); - } - }); - } -} diff --git a/server/src/services/Accounting/JournalPoster.ts b/server/src/services/Accounting/JournalPoster.ts new file mode 100644 index 000000000..5e6a62a13 --- /dev/null +++ b/server/src/services/Accounting/JournalPoster.ts @@ -0,0 +1,337 @@ +import { omit } from 'lodash'; +import { Container } from 'typedi'; +import JournalEntry from '@/services/Accounting/JournalEntry'; +import TenancyService from '@/services/Tenancy/TenancyService'; +import { + IJournalEntry, + IJournalPoster, + IAccountChange, + IAccountsChange, + TEntryType, +} from '@/interfaces'; + +export default class JournalPoster implements IJournalPoster { + tenantId: number; + tenancy: TenancyService; + logger: any; + models: any; + repositories: any; + + deletedEntriesIds: number[] = []; + entries: IJournalEntry[] = []; + balancesChange: IAccountsChange = {}; + accountsDepGraph: IAccountsChange = {}; + + /** + * Journal poster constructor. + * @param {number} tenantId - + */ + constructor( + tenantId: number, + ) { + this.initTenancy(); + + this.tenantId = tenantId; + this.models = this.tenancy.models(tenantId); + this.repositories = this.tenancy.repositories(tenantId); + } + + /** + * Initial tenancy. + * @private + */ + private initTenancy() { + try { + this.tenancy = Container.get(TenancyService); + this.logger = Container.get('logger'); + } catch (exception) { + throw new Error('Should execute this class inside tenancy area.'); + } + } + + /** + * Async initialize acccounts dependency graph. + * @private + * @returns {Promise} + */ + private async initializeAccountsDepGraph(): Promise { + const { accountRepository } = this.repositories; + const accountsDepGraph = await accountRepository.getDependencyGraph(); + this.accountsDepGraph = accountsDepGraph; + } + + /** + * Writes the credit entry for the given account. + * @param {IJournalEntry} entry - + */ + public credit(entryModel: IJournalEntry): void { + if (entryModel instanceof JournalEntry === false) { + throw new Error('The entry is not instance of JournalEntry.'); + } + this.entries.push(entryModel.entry); + this.setAccountBalanceChange(entryModel.entry); + } + + /** + * Writes the debit entry for the given account. + * @param {JournalEntry} entry - + */ + public debit(entryModel: IJournalEntry): void { + if (entryModel instanceof JournalEntry === false) { + throw new Error('The entry is not instance of JournalEntry.'); + } + this.entries.push(entryModel.entry); + this.setAccountBalanceChange(entryModel.entry); + } + + /** + * Sets account balance change. + * @param {JournalEntry} entry + * @param {String} type + */ + private setAccountBalanceChange(entry: IJournalEntry): void { + const accountChange: IAccountChange = { + debit: entry.debit, + credit: entry.credit, + }; + this._setAccountBalanceChange(entry.account, accountChange); + } + + /** + * Sets account balance change. + * @private + * @param {number} accountId - + * @param {IAccountChange} accountChange + */ + private _setAccountBalanceChange( + accountId: number, + accountChange: IAccountChange + ) { + this.balancesChange = this.accountBalanceChangeReducer( + this.balancesChange, accountId, accountChange, + ); + } + + /** + * Accounts balance change reducer. + * @param {IAccountsChange} balancesChange + * @param {number} accountId + * @param {IAccountChange} accountChange + * @return {IAccountChange} + */ + private accountBalanceChangeReducer( + balancesChange: IAccountsChange, + accountId: number, + accountChange: IAccountChange, + ) { + const change = { ...balancesChange }; + + if (!change[accountId]) { + change[accountId] = { credit: 0, debit: 0 }; + } + if (accountChange.credit) { + change[accountId].credit += accountChange.credit; + } + if (accountChange.debit) { + change[accountId].debit += accountChange.debit; + } + return change; + } + + /** + * Converts balance changes to array. + * @private + * @param {IAccountsChange} accountsChange - + * @return {Promise<{ account: number, change: number }>} + */ + private async convertBalanceChangesToArr( + accountsChange: IAccountsChange + ) : Promise<{ account: number, change: number }[]>{ + const { accountTypeRepository } = this.repositories; + const mappedList: { account: number, change: number }[] = []; + const accountsIds: number[] = Object.keys(accountsChange).map(id => parseInt(id, 10)); + + await Promise.all( + accountsIds.map(async (account: number) => { + const accountChange = accountsChange[account]; + const accountNode = this.accountsDepGraph.getNodeData(account); + const accountTypeMeta = await accountTypeRepository.getTypeMeta(accountNode.accountTypeId); + const { normal }: { normal: TEntryType } = accountTypeMeta; + let change = 0; + + if (accountChange.credit) { + change = (normal === 'credit') ? accountChange.credit : -1 * accountChange.credit; + } + if (accountChange.debit) { + change = (normal === 'debit') ? accountChange.debit : -1 * accountChange.debit; + } + mappedList.push({ account, change }); + }), + ); + return mappedList; + } + + /** + * Saves the balance change of journal entries. + * @returns {Promise} + */ + public async saveBalance() { + await this.initializeAccountsDepGraph(); + + const { Account } = this.models; + const accountsChange = this.balanceChangeWithDepends(this.balancesChange); + const balancesList = await this.convertBalanceChangesToArr(accountsChange); + const balancesAccounts = balancesList.map(b => b.account); + + // Ensure the accounts has atleast zero in amount. + await Account.query().where('amount', null).whereIn('id', balancesAccounts) + .patch({ amount: 0 }); + + const balanceUpdateOpers: Promise[] = []; + + balancesList.forEach((balance: { account: number, change: number }) => { + const method: string = (balance.change < 0) ? 'decrement' : 'increment'; + + this.logger.info('[journal_poster] increment/decrement account balance.', { + balance, tenantId: this.tenantId, + }) + const query = Account.query() + [method]('amount', Math.abs(balance.change)) + .where('id', balance.account); + + balanceUpdateOpers.push(query); + }); + + await Promise.all(balanceUpdateOpers); + this.resetAccountsBalanceChange(); + } + + /** + * Changes all accounts that dependencies of changed accounts. + * @param {IAccountsChange} accountsChange + * @returns {IAccountsChange} + */ + private balanceChangeWithDepends(accountsChange: IAccountsChange): IAccountsChange { + const accountsIds = Object.keys(accountsChange); + let changes: IAccountsChange = {}; + + accountsIds.forEach((accountId) => { + const accountChange = accountsChange[accountId]; + const depAccountsIds = this.accountsDepGraph.dependenciesOf(accountId); + + [accountId, ...depAccountsIds].forEach((account) => { + changes = this.accountBalanceChangeReducer(changes, account, accountChange); + }); + }); + return changes; + } + + /** + * Resets accounts balance change. + * @private + */ + private resetAccountsBalanceChange() { + this.balancesChange = {}; + } + + /** + * Saves the stacked journal entries to the storage. + * @returns {Promise} + */ + public async saveEntries() { + const { AccountTransaction } = this.models; + const saveOperations: Promise[] = []; + + this.entries.forEach((entry) => { + const oper = AccountTransaction.query() + .insert({ + accountId: entry.account, + ...omit(entry, ['account']), + }); + saveOperations.push(oper); + }); + await Promise.all(saveOperations); + } + + /** + * Reverses the stacked journal entries. + */ + public reverseEntries() { + const reverseEntries: IJournalEntry[] = []; + + this.entries.forEach((entry) => { + const reverseEntry = { ...entry }; + + if (entry.credit) { + reverseEntry.debit = entry.credit; + } + if (entry.debit) { + reverseEntry.credit = entry.debit; + } + reverseEntries.push(reverseEntry); + }); + this.entries = reverseEntries; + } + + /** + * Removes all stored entries or by the given in ids. + * @param {Array} ids - + */ + removeEntries(ids: number[] = []) { + const targetIds = ids.length <= 0 ? this.entries.map((e) => e.id) : ids; + const removeEntries = this.entries.filter( + (e) => targetIds.indexOf(e.id) !== -1 + ); + this.entries = this.entries.filter((e) => targetIds.indexOf(e.id) === -1); + + removeEntries.forEach((entry) => { + entry.credit = -1 * entry.credit; + entry.debit = -1 * entry.debit; + + this.setAccountBalanceChange(entry, entry.accountNormal); + }); + this.deletedEntriesIds.push(...removeEntries.map((entry) => entry.id)); + } + + /** + * Delete all the stacked entries. + * @return {Promise} + */ + public async deleteEntries() { + const { AccountTransaction } = this.models; + + if (this.deletedEntriesIds.length > 0) { + await AccountTransaction.query() + .whereIn('id', this.deletedEntriesIds) + .delete(); + } + } + + /** + * Load fetched accounts journal entries. + * @param {IJournalEntry[]} entries - + */ + loadEntries(entries: IJournalEntry[]): void { + entries.forEach((entry: IJournalEntry) => { + this.entries.push({ + ...entry, + account: entry.account ? entry.account.id : entry.accountId, + }); + }); + } + + /** + * Calculates the entries balance change. + * @public + */ + public calculateEntriesBalanceChange() { + this.entries.forEach((entry) => { + if (entry.credit) { + this.setAccountBalanceChange(entry, 'credit'); + } + if (entry.debit) { + this.setAccountBalanceChange(entry, 'debit'); + } + }); + } +} diff --git a/server/src/services/Accounts/AccountsService.ts b/server/src/services/Accounts/AccountsService.ts index 688b23e6a..e1195aaba 100644 --- a/server/src/services/Accounts/AccountsService.ts +++ b/server/src/services/Accounts/AccountsService.ts @@ -1,4 +1,5 @@ import { Inject, Service } from 'typedi'; +import { kebabCase } from 'lodash' import TenancyService from '@/services/Tenancy/TenancyService'; import { ServiceError } from '@/exceptions'; import { IAccountDTO, IAccount } from '@/interfaces'; @@ -174,6 +175,7 @@ export default class AccountsService { } const account = await Account.query().insertAndFetch({ ...accountDTO, + slug: kebabCase(accountDTO.name), }); this.logger.info('[account] account created successfully.', { account, accountDTO }); return account; diff --git a/server/src/services/Authentication/index.ts b/server/src/services/Authentication/index.ts index 66198c4a8..5fbee41ad 100644 --- a/server/src/services/Authentication/index.ts +++ b/server/src/services/Authentication/index.ts @@ -88,6 +88,10 @@ export default class AuthenticationService { this.eventDispatcher.dispatch(events.auth.login, { emailOrPhone, password, }); + + // Remove password property from user object. + Reflect.deleteProperty(user, 'password'); + return { user, token }; } diff --git a/server/src/services/Cache/index.js b/server/src/services/Cache/index.ts similarity index 75% rename from server/src/services/Cache/index.js rename to server/src/services/Cache/index.ts index 752b05ad7..31c3355f0 100644 --- a/server/src/services/Cache/index.js +++ b/server/src/services/Cache/index.ts @@ -1,18 +1,18 @@ import NodeCache from 'node-cache'; -class Cache { +export default class Cache { + cache: NodeCache; - constructor() { + constructor(config?: object) { this.cache = new NodeCache({ - // stdTTL: 9999999, - // checkperiod: 9999999 * 0.2, useClones: false, + ...config, }); } - get(key, storeFunction) { + get(key: string, storeFunction: () => Promise) { const value = this.cache.get(key); - + if (value) { return Promise.resolve(value); } @@ -22,11 +22,11 @@ class Cache { }); } - set(key, results) { + set(key: string, results: any) { this.cache.set(key, results); } - del(keys) { + del(keys: string) { this.cache.del(keys); } @@ -46,7 +46,4 @@ class Cache { flush() { this.cache.flushAll(); } -} - - -export default new Cache(); \ No newline at end of file +} \ No newline at end of file diff --git a/server/src/services/Contacts/ContactsService.ts b/server/src/services/Contacts/ContactsService.ts new file mode 100644 index 000000000..ff5e9090e --- /dev/null +++ b/server/src/services/Contacts/ContactsService.ts @@ -0,0 +1,131 @@ +import { Inject, Service } from 'typedi'; +import { difference } from 'lodash'; +import { ServiceError } from "@/exceptions"; +import TenancyService from '@/services/Tenancy/TenancyService'; +import { + IContact, + IContactNewDTO, + IContactEditDTO, + } from "@/interfaces"; + +type TContactService = 'customer' | 'vendor'; + +@Service() +export default class ContactsService { + @Inject() + tenancy: TenancyService; + + @Inject('logger') + logger: any; + + /** + * Get the given contact or throw not found contact. + * @param {number} tenantId + * @param {number} contactId + * @param {TContactService} contactService + * @return {Promise} + */ + private async getContactByIdOrThrowError(tenantId: number, contactId: number, contactService: TContactService) { + const { Contact } = this.tenancy.models(tenantId); + + this.logger.info('[contact] trying to validate contact existance.', { tenantId, contactId }); + const contact = await Contact.query().findById(contactId).where('contact_service', contactService); + + if (!contact) { + throw new ServiceError('contact_not_found'); + } + return contact; + } + + /** + * Creates a new contact on the storage. + * @param {number} tenantId + * @param {TContactService} contactService + * @param {IContactDTO} contactDTO + */ + async newContact(tenantId: number, contactDTO: IContactNewDTO, contactService: TContactService) { + const { Contact } = this.tenancy.models(tenantId); + + this.logger.info('[contacts] trying to insert contact to the storage.', { tenantId, contactDTO }); + const contact = await Contact.query().insert({ contactService, ...contactDTO }); + + this.logger.info('[contacts] contact inserted successfully.', { tenantId, contact }); + return contact; + } + + /** + * Edit details of the given on the storage. + * @param {number} tenantId + * @param {number} contactId + * @param {TContactService} contactService + * @param {IContactDTO} contactDTO + */ + async editContact(tenantId: number, contactId: number, contactDTO: IContactEditDTO, contactService: TContactService) { + const { Contact } = this.tenancy.models(tenantId); + const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService); + + this.logger.info('[contacts] trying to edit the given contact details.', { tenantId, contactId, contactDTO }); + await Contact.query().findById(contactId).patch({ ...contactDTO }) + } + + /** + * Deletes the given contact from the storage. + * @param {number} tenantId + * @param {number} contactId + * @param {TContactService} contactService + * @return {Promise} + */ + async deleteContact(tenantId: number, contactId: number, contactService: TContactService) { + const { Contact } = this.tenancy.models(tenantId); + const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService); + + this.logger.info('[contacts] trying to delete the given contact.', { tenantId, contactId }); + await Contact.query().findById(contactId).delete(); + } + + /** + * Get contact details of the given contact id. + * @param {number} tenantId + * @param {number} contactId + * @param {TContactService} contactService + * @returns {Promise} + */ + async getContact(tenantId: number, contactId: number, contactService: TContactService) { + return this.getContactByIdOrThrowError(tenantId, contactId, contactService); + } + + /** + * Retrieve contacts or throw not found error if one of ids were not found + * on the storage. + * @param {number} tenantId + * @param {number[]} contactsIds + * @param {TContactService} contactService + * @return {Promise} + */ + async getContactsOrThrowErrorNotFound(tenantId: number, contactsIds: number[], contactService: TContactService) { + const { Contact } = this.tenancy.models(tenantId); + const contacts = await Contact.query().whereIn('id', contactsIds).where('contact_service', contactService); + const storedContactsIds = contacts.map((contact: IContact) => contact.id); + + const notFoundCustomers = difference(contactsIds, storedContactsIds); + + if (notFoundCustomers.length > 0) { + throw new ServiceError('contacts_not_found'); + } + return contacts; + } + + /** + * Deletes the given contacts in bulk. + * @param {number} tenantId + * @param {number[]} contactsIds + * @param {TContactService} contactService + * @return {Promise} + */ + async deleteBulkContacts(tenantId: number, contactsIds: number[], contactService: TContactService) { + const { Contact } = this.tenancy.models(tenantId); + this.getContactsOrThrowErrorNotFound(tenantId, contactsIds, contactService); + + await Contact.query().whereIn('id', contactsIds).delete(); + } +} \ No newline at end of file diff --git a/server/src/services/Contacts/CustomersService.ts b/server/src/services/Contacts/CustomersService.ts new file mode 100644 index 000000000..6d52baa77 --- /dev/null +++ b/server/src/services/Contacts/CustomersService.ts @@ -0,0 +1,171 @@ +import { Inject, Service } from 'typedi'; +import { omit, difference } from 'lodash'; +import JournalPoster from "@/services/Accounting/JournalPoster"; +import JournalCommands from "@/services/Accounting/JournalCommands"; +import ContactsService from '@/services/Contacts/ContactsService'; +import { + ICustomerNewDTO, + ICustomerEditDTO, + } from '@/interfaces'; +import { ServiceError } from '@/exceptions'; +import TenancyService from '@/services/Tenancy/TenancyService'; +import { ICustomer } from 'src/interfaces'; + +@Service() +export default class CustomersService { + @Inject() + contactService: ContactsService; + + @Inject() + tenancy: TenancyService; + + /** + * Converts customer to contact DTO. + * @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO + * @returns {IContactDTO} + */ + customerToContactDTO(customerDTO: ICustomerNewDTO|ICustomerEditDTO) { + return { + ...omit(customerDTO, ['customerType']), + contactType: customerDTO.customerType, + active: (typeof customerDTO.active === 'undefined') ? + true : customerDTO.active, + }; + } + + /** + * Creates a new customer. + * @param {number} tenantId + * @param {ICustomerNewDTO} customerDTO + * @return {Promise} + */ + async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) { + const contactDTO = this.customerToContactDTO(customerDTO) + const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer'); + + // Writes the customer opening balance journal entries. + if (customer.openingBalance) { + await this.writeCustomerOpeningBalanceJournal( + tenantId, + customer.id, + customer.openingBalance, + ); + } + return customer; + } + + /** + * Edits details of the given customer. + * @param {number} tenantId + * @param {ICustomerEditDTO} customerDTO + */ + async editCustomer(tenantId: number, customerId: number, customerDTO: ICustomerEditDTO) { + const contactDTO = this.customerToContactDTO(customerDTO); + return this.contactService.editContact(tenantId, customerId, contactDTO, 'customer'); + } + + /** + * Deletes the given customer from the storage. + * @param {number} tenantId + * @param {number} customerId + * @return {Promise} + */ + async deleteCustomer(tenantId: number, customerId: number) { + await this.customerHasNoInvoicesOrThrowError(tenantId, customerId); + return this.contactService.deleteContact(tenantId, customerId, 'customer'); + } + + /** + * Retrieve the given customer details. + * @param {number} tenantId + * @param {number} customerId + */ + async getCustomer(tenantId: number, customerId: number) { + return this.contactService.getContact(tenantId, customerId, 'customer'); + } + + /** + * Writes customer opening balance journal entries. + * @param {number} tenantId + * @param {number} customerId + * @param {number} openingBalance + * @return {Promise} + */ + async writeCustomerOpeningBalanceJournal( + tenantId: number, + customerId: number, + openingBalance: number, + ) { + const journal = new JournalPoster(tenantId); + const journalCommands = new JournalCommands(journal); + + await journalCommands.customerOpeningBalance(customerId, openingBalance) + + await Promise.all([ + journal.saveBalance(), + journal.saveEntries(), + ]); + } + + /** + * Retrieve the given customers or throw error if one of them not found. + * @param {numebr} tenantId + * @param {number[]} customersIds + */ + getCustomersOrThrowErrorNotFound(tenantId: number, customersIds: number[]) { + return this.contactService.getContactsOrThrowErrorNotFound(tenantId, customersIds, 'customer'); + } + + /** + * Deletes the given customers from the storage. + * @param {number} tenantId + * @param {number[]} customersIds + * @return {Promise} + */ + async deleteBulkCustomers(tenantId: number, customersIds: number[]) { + const { Contact } = this.tenancy.models(tenantId); + + await this.getCustomersOrThrowErrorNotFound(tenantId, customersIds); + await this.customersHaveNoInvoicesOrThrowError(tenantId, customersIds); + + await Contact.query().whereIn('id', customersIds).delete(); + } + + /** + * Validates the customer has no associated sales invoice + * or throw service error. + * @param {number} tenantId + * @param {number} customerId + */ + async customerHasNoInvoicesOrThrowError(tenantId: number, customerId: number) { + const { customerRepository } = this.tenancy.repositories(tenantId); + const salesInvoice = await customerRepository.getSalesInvoices(customerId); + + if (salesInvoice.length > 0) { + throw new ServiceError('customer_has_invoices'); + } + } + + /** + * Throws error in case one of customers have associated sales invoices. + * @param {number} tenantId + * @param {number[]} customersIds + * @throws {ServiceError} + */ + async customersHaveNoInvoicesOrThrowError(tenantId: number, customersIds: number[]) { + const { customerRepository } = this.tenancy.repositories(tenantId); + + const customersWithInvoices = await customerRepository.customersWithSalesInvoices( + customersIds, + ); + const customersIdsWithInvoice = customersWithInvoices + .filter((customer: ICustomer) => customer.salesInvoices.length > 0) + .map((customer: ICustomer) => customer.id); + + const customersHaveInvoices = difference(customersIds, customersIdsWithInvoice); + + if (customersHaveInvoices.length > 0) { + throw new ServiceError('some_customers_have_invoices'); + } + } +} diff --git a/server/src/services/Contacts/VendorsService.ts b/server/src/services/Contacts/VendorsService.ts new file mode 100644 index 000000000..ecf7240c5 --- /dev/null +++ b/server/src/services/Contacts/VendorsService.ts @@ -0,0 +1,167 @@ +import { Inject, Service } from 'typedi'; +import { difference } from 'lodash'; +import JournalPoster from "@/services/Accounting/JournalPoster"; +import JournalCommands from "@/services/Accounting/JournalCommands"; +import ContactsService from '@/services/Contacts/ContactsService'; +import { + IVendorNewDTO, + IVendorEditDTO, + IVendor + } from '@/interfaces'; +import { ServiceError } from '@/exceptions'; +import TenancyService from '@/services/Tenancy/TenancyService'; + +@Service() +export default class VendorsService { + @Inject() + contactService: ContactsService; + + @Inject() + tenancy: TenancyService; + + /** + * Converts vendor to contact DTO. + * @param {IVendorNewDTO|IVendorEditDTO} vendorDTO + * @returns {IContactDTO} + */ + vendorToContactDTO(vendorDTO: IVendorNewDTO|IVendorEditDTO) { + return { + ...vendorDTO, + active: (typeof vendorDTO.active === 'undefined') ? + true : vendorDTO.active, + }; + } + + /** + * Creates a new vendor. + * @param {number} tenantId + * @param {IVendorNewDTO} vendorDTO + * @return {Promise} + */ + async newVendor(tenantId: number, vendorDTO: IVendorNewDTO) { + const contactDTO = this.vendorToContactDTO(vendorDTO) + const vendor = await this.contactService.newContact(tenantId, contactDTO, 'vendor'); + + // Writes the vendor opening balance journal entries. + if (vendor.openingBalance) { + await this.writeVendorOpeningBalanceJournal( + tenantId, + vendor.id, + vendor.openingBalance, + ); + } + return vendor; + } + + /** + * Edits details of the given vendor. + * @param {number} tenantId + * @param {IVendorEditDTO} vendorDTO + */ + async editVendor(tenantId: number, vendorId: number, vendorDTO: IVendorEditDTO) { + const contactDTO = this.vendorToContactDTO(vendorDTO); + return this.contactService.editContact(tenantId, vendorId, contactDTO, 'vendor'); + } + + /** + * Deletes the given vendor from the storage. + * @param {number} tenantId + * @param {number} vendorId + * @return {Promise} + */ + async deleteVendor(tenantId: number, vendorId: number) { + await this.vendorHasNoBillsOrThrowError(tenantId, vendorId); + return this.contactService.deleteContact(tenantId, vendorId, 'vendor'); + } + + /** + * Retrieve the given vendor details. + * @param {number} tenantId + * @param {number} vendorId + */ + async getVendor(tenantId: number, vendorId: number) { + return this.contactService.getContact(tenantId, vendorId, 'vendor'); + } + + /** + * Writes vendor opening balance journal entries. + * @param {number} tenantId + * @param {number} vendorId + * @param {number} openingBalance + * @return {Promise} + */ + async writeVendorOpeningBalanceJournal( + tenantId: number, + vendorId: number, + openingBalance: number, + ) { + const journal = new JournalPoster(tenantId); + const journalCommands = new JournalCommands(journal); + + await journalCommands.vendorOpeningBalance(vendorId, openingBalance) + + await Promise.all([ + journal.saveBalance(), + journal.saveEntries(), + ]); + } + + /** + * Retrieve the given vendors or throw error if one of them not found. + * @param {numebr} tenantId + * @param {number[]} vendorsIds + */ + getVendorsOrThrowErrorNotFound(tenantId: number, vendorsIds: number[]) { + return this.contactService.getContactsOrThrowErrorNotFound(tenantId, vendorsIds, 'vendor'); + } + + /** + * Deletes the given vendors from the storage. + * @param {number} tenantId + * @param {number[]} vendorsIds + * @return {Promise} + */ + async deleteBulkVendors(tenantId: number, vendorsIds: number[]) { + const { Contact } = this.tenancy.models(tenantId); + + await this.getVendorsOrThrowErrorNotFound(tenantId, vendorsIds); + await this.vendorsHaveNoBillsOrThrowError(tenantId, vendorsIds); + + await Contact.query().whereIn('id', vendorsIds).delete(); + } + + /** + * Validates the vendor has no associated bills or throw service error. + * @param {number} tenantId + * @param {number} vendorId + */ + async vendorHasNoBillsOrThrowError(tenantId: number, vendorId: number) { + const { vendorRepository } = this.tenancy.repositories(tenantId); + const bills = await vendorRepository.getBills(vendorId); + + if (bills) { + throw new ServiceError('vendor_has_bills') + } + } + + /** + * Throws error in case one of vendors have associated bills. + * @param {number} tenantId + * @param {number[]} customersIds + * @throws {ServiceError} + */ + async vendorsHaveNoBillsOrThrowError(tenantId: number, vendorsIds: number[]) { + const { vendorRepository } = this.tenancy.repositories(tenantId); + + const vendorsWithBills = await vendorRepository.vendorsWithBills(vendorsIds); + const vendorsIdsWithBills = vendorsWithBills + .filter((vendor: IVendor) => vendor.bills.length > 0) + .map((vendor: IVendor) => vendor.id); + + const vendorsHaveInvoices = difference(vendorsIds, vendorsIdsWithBills); + + if (vendorsHaveInvoices.length > 0) { + throw new ServiceError('some_vendors_have_bills'); + } + } +} diff --git a/server/src/services/Customers/CustomersService.js b/server/src/services/Customers/CustomersService.js deleted file mode 100644 index 7166a4c9a..000000000 --- a/server/src/services/Customers/CustomersService.js +++ /dev/null @@ -1,10 +0,0 @@ -import Customer from "../../models/Customer"; - - -export default class CustomersService { - - static async isCustomerExists(customerId) { - const foundCustomeres = await Customer.query().where('id', customerId); - return foundCustomeres.length > 0; - } -} \ No newline at end of file diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts index 595993dc2..94f5bc4fd 100644 --- a/server/src/services/Items/ItemsService.ts +++ b/server/src/services/Items/ItemsService.ts @@ -67,4 +67,8 @@ export default class ItemsService { ); return notFoundItemsIds; } + + writeItemInventoryOpeningQuantity(tenantId: number, itemId: number, openingQuantity: number, averageCost: number) { + + } } \ No newline at end of file diff --git a/server/src/services/Purchases/BillPayments.ts b/server/src/services/Purchases/BillPayments.ts index 338c0b072..a42608ee3 100644 --- a/server/src/services/Purchases/BillPayments.ts +++ b/server/src/services/Purchases/BillPayments.ts @@ -213,9 +213,7 @@ export default class BillPaymentsService { 'accounts_payable' ); - const accountsDepGraph = await Account.depGraph().query(); - const journal = new JournalPoster(accountsDepGraph); - + const journal = new JournalPoster(tenantId); const commonJournal = { debit: 0, credit: 0, diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index da30ad051..5d0636e16 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -10,7 +10,7 @@ import HasItemsEntries from '@/services/Sales/HasItemsEntries'; import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost'; import TenancyService from '@/services/Tenancy/TenancyService'; import { formatDateFields } from '@/utils'; -import{ IBillOTD } from '@/interfaces'; +import{ IBillOTD, IBill, IItem } from '@/interfaces'; /** * Vendor bills services. @@ -27,6 +27,35 @@ export default class BillsService extends SalesInvoicesCost { @Inject() tenancy: TenancyService; + /** + * Converts bill DTO to model. + * @param {number} tenantId + * @param {IBillDTO} billDTO + * @param {IBill} oldBill + * + * @returns {IBill} + */ + async billDTOToModel(tenantId: number, billDTO: IBillOTD, oldBill?: IBill) { + const { ItemEntry } = this.tenancy.models(tenantId); + let invLotNumber = oldBill?.invLotNumber; + + if (!invLotNumber) { + invLotNumber = await this.inventoryService.nextLotNumber(tenantId); + } + const entries = billDTO.entries.map((entry) => ({ + ...entry, + amount: ItemEntry.calcAmount(entry), + })); + const amount = sumBy(entries, 'amount'); + + return { + ...formatDateFields(billDTO, ['bill_date', 'due_date']), + amount, + invLotNumber, + entries, + }; + } + /** * Creates a new bill and stored it to the storage. * @@ -45,14 +74,7 @@ export default class BillsService extends SalesInvoicesCost { async createBill(tenantId: number, billDTO: IBillOTD) { const { Vendor, Bill, ItemEntry } = this.tenancy.models(tenantId); - const invLotNumber = await this.inventoryService.nextLotNumber(tenantId); - const amount = sumBy(billDTO.entries, e => ItemEntry.calcAmount(e)); - - const bill = { - ...formatDateFields(billDTO, ['bill_date', 'due_date']), - amount, - invLotNumber: billDTO.invLotNumber || invLotNumber - }; + const bill = await this.billDTOToModel(tenantId, billDTO); const saveEntriesOpers = []; const storedBill = await Bill.query() @@ -116,13 +138,8 @@ export default class BillsService extends SalesInvoicesCost { const { Bill, ItemEntry, Vendor } = this.tenancy.models(tenantId); const oldBill = await Bill.query().findById(billId); - const amount = sumBy(billDTO.entries, e => ItemEntry.calcAmount(e)); + const bill = this.billDTOToModel(tenantId, billDTO, oldBill); - const bill = { - ...formatDateFields(billDTO, ['bill_date', 'due_date']), - amount, - invLotNumber: oldBill.invLotNumber, - }; // Update the bill transaction. const updatedBill = await Bill.query() .where('id', billId) @@ -246,7 +263,7 @@ export default class BillsService extends SalesInvoicesCost { * @param {Integer} billId */ async recordJournalTransactions(tenantId: number, bill: any, billId?: number) { - const { AccountTransaction, Item, Account } = this.tenancy.models(tenantId); + const { AccountTransaction, Item } = this.tenancy.models(tenantId); const entriesItemsIds = bill.entries.map((entry) => entry.item_id); const payableTotal = sumBy(bill.entries, 'amount'); @@ -259,8 +276,7 @@ export default class BillsService extends SalesInvoicesCost { const payableAccount = await this.accountsService.getAccountByType( tenantId, 'accounts_payable' ); - const accountsDepGraph = await Account.depGraph().query(); - const journal = new JournalPoster(accountsDepGraph); + const journal = new JournalPoster(tenantId); const commonJournalMeta = { debit: 0, @@ -289,7 +305,7 @@ export default class BillsService extends SalesInvoicesCost { journal.credit(payableEntry); bill.entries.forEach((entry) => { - const item = storedItemsMap.get(entry.item_id); + const item: IItem = storedItemsMap.get(entry.item_id); const debitEntry = new JournalEntry({ ...commonJournalMeta, diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index 730aea20a..0ae82d20f 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -183,8 +183,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { .where('reference_id', saleInvoiceId) .withGraphFetched('account.type'); - const accountsDepGraph = await Account.depGraph().query(); - const journal = new JournalPoster(accountsDepGraph); + const journal = new JournalPoster(tenantId); journal.loadEntries(invoiceTransactions); journal.removeEntries(); @@ -385,10 +384,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost { saleInvoice: ISaleInvoice, override: boolean ) { - const { Account, AccountTransaction } = this.tenancy.models(tenantId); + const { AccountTransaction } = this.tenancy.models(tenantId); - const accountsDepGraph = await Account.depGraph().query(); - const journal = new JournalPoster(accountsDepGraph); + const journal = new JournalPoster(tenantId); if (override) { const oldTransactions = await AccountTransaction.query() diff --git a/server/src/services/Tenancy/TenancyService.ts b/server/src/services/Tenancy/TenancyService.ts index 5b01ce042..d4f604057 100644 --- a/server/src/services/Tenancy/TenancyService.ts +++ b/server/src/services/Tenancy/TenancyService.ts @@ -1,5 +1,6 @@ -import { Container } from 'typedi'; +import { Container, Service } from 'typedi'; +@Service() export default class HasTenancyService { /** * Retrieve the given tenant container. @@ -10,6 +11,14 @@ export default class HasTenancyService { return Container.of(`tenant-${tenantId}`); } + /** + * Retrieve knex instance of the given tenant id. + * @param {number} tenantId + */ + knex(tenantId: number) { + return this.tenantContainer(tenantId).get('knex'); + } + /** * Retrieve models of the givne tenant id. * @param {number} tenantId - The tenant id. @@ -18,11 +27,27 @@ export default class HasTenancyService { return this.tenantContainer(tenantId).get('models'); } + /** + * Retrieve repositories of the given tenant id. + * @param {number} tenantId + */ + repositories(tenantId: number) { + return this.tenantContainer(tenantId).get('repositories'); + } + /** * Retrieve i18n locales methods. * @param {number} tenantId */ i18n(tenantId: number) { - this.tenantContainer(tenantId).get('i18n'); + return this.tenantContainer(tenantId).get('i18n'); + } + + /** + * Retrieve tenant cache instance. + * @param {number} tenantId - + */ + cache(tenantId: number) { + return this.tenantContainer(tenantId).get('cache'); } } \ No newline at end of file diff --git a/server/src/services/Users/UsersService.ts b/server/src/services/Users/UsersService.ts index 56a4541c5..c7010f5c0 100644 --- a/server/src/services/Users/UsersService.ts +++ b/server/src/services/Users/UsersService.ts @@ -9,6 +9,9 @@ export default class UsersService { @Inject() tenancy: TenancyService; + @Inject('logger') + logger: any; + /** * Creates a new user. * @param {number} tenantId @@ -61,6 +64,7 @@ export default class UsersService { id: userId, tenant_id: tenantId, }); if (!user) { + this.logger.info('[users] the given user not found.', { tenantId, userId }); throw new ServiceError('user_not_found'); } return user; @@ -73,7 +77,12 @@ export default class UsersService { */ async deleteUser(tenantId: number, userId: number): Promise { await this.getUserOrThrowError(tenantId, userId); - await SystemUser.query().where('id', userId).delete(); + + this.logger.info('[users] trying to delete the given user.', { tenantId, userId }); + await SystemUser.query().where('tenant_id', tenantId) + .where('id', userId).delete(); + + this.logger.info('[users] the given user deleted successfully.', { tenantId, userId }); } /** diff --git a/server/src/services/Vendors/VendorsService.js b/server/src/services/Vendors/VendorsService.js deleted file mode 100644 index 4323f3367..000000000 --- a/server/src/services/Vendors/VendorsService.js +++ /dev/null @@ -1,15 +0,0 @@ -import { Vendor } from '@/models'; - - -export default class VendorsService { - - - static async isVendorExists(vendorId) { - const foundVendors = await Vendor.tenant().query().where('id', vendorId); - return foundVendors.length > 0; - } - - static async isVendorsExist(vendorsIds) { - - } -} \ No newline at end of file diff --git a/server/src/system/migrations/20190822214242_create_users_table.js b/server/src/system/migrations/20190822214242_create_users_table.js index df248f422..9b75f438b 100644 --- a/server/src/system/migrations/20190822214242_create_users_table.js +++ b/server/src/system/migrations/20190822214242_create_users_table.js @@ -15,6 +15,8 @@ exports.up = function (knex) { table.date('invite_accepted_at'); table.date('last_login_at'); + + table.dateTime('deleted_at'); table.timestamps(); }).then(() => { // knex.seed.run({ diff --git a/server/src/system/models/SubscriptionPlan.js b/server/src/system/models/SubscriptionPlan.js deleted file mode 100644 index d05cd4516..000000000 --- a/server/src/system/models/SubscriptionPlan.js +++ /dev/null @@ -1,10 +0,0 @@ -import SystemModel from '@/system/models/SystemModel'; - -export default class SubscriptionPlan extends SystemModel { - /** - * Table name - */ - static get tableName() { - return 'subscriptions_plans'; - } -} diff --git a/server/src/system/models/SystemOption.js b/server/src/system/models/SystemOption.js deleted file mode 100644 index 0c53588da..000000000 --- a/server/src/system/models/SystemOption.js +++ /dev/null @@ -1,11 +0,0 @@ -import { mixin } from 'objection'; -import SystemModel from '@/system/models/SystemModel'; - -export default class Option extends SystemModel { - /** - * Table name. - */ - static get tableName() { - return 'options'; - } -} diff --git a/server/src/system/models/SystemUser.js b/server/src/system/models/SystemUser.js index 2fec496b4..d6f0c5cff 100644 --- a/server/src/system/models/SystemUser.js +++ b/server/src/system/models/SystemUser.js @@ -1,9 +1,14 @@ import { Model, mixin } from 'objection'; import bcrypt from 'bcryptjs'; +import SoftDelete from 'objection-soft-delete'; import SystemModel from '@/system/models/SystemModel'; +import moment from 'moment'; - -export default class SystemUser extends mixin(SystemModel) { +export default class SystemUser extends mixin(SystemModel, [SoftDelete({ + columnName: 'deleted_at', + deletedValue: moment().format('YYYY-MM-DD HH:mm:ss'), + notDeletedValue: null, +})]) { /** * Table name. */ @@ -25,6 +30,9 @@ export default class SystemUser extends mixin(SystemModel) { const Tenant = require('@/system/models/Tenant'); return { + /** + * System user may belongs to tenant model. + */ tenant: { relation: Model.BelongsToOneRelation, modelClass: Tenant.default,