fix: database migrations FK relations.

fix: database columns indexing.
This commit is contained in:
Ahmed Bouhuolia
2020-10-03 12:08:11 +02:00
parent 1250eccc0d
commit 0114ed9f8b
86 changed files with 788 additions and 801 deletions

View File

@@ -20,7 +20,7 @@ TENANT_DB_NAME_PERFIX=bigcapital_tenant_
TENANT_DB_HOST=127.0.0.1
TENANT_DB_PASSWORD=root
TEANNT_DB_USER=root
TENANT_DB_CHARSET=charset
TENANT_DB_CHARSET=utf8
TENANT_MIGRATIONS_DIR=src/database/migrations
TENANT_SEEDS_DIR=src/database/seeds/core

View File

@@ -162,7 +162,7 @@ export default class CurrenciesController extends BaseController {
* @param {Response} res
* @param {NextFunction} next
*/
handlerServiceError(error, req, res, next) {
handlerServiceError(error: Error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) {
if (error.errorType === 'currency_not_found') {
return res.boom.badRequest(null, {

View File

@@ -7,7 +7,6 @@ import {
import ItemCategoriesService from 'services/ItemCategories/ItemCategoriesService';
import { Inject, Service } from 'typedi';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import validateMiddleware from 'api/middleware/validateMiddleware';
import { IItemCategoryOTD } from 'interfaces';
import { ServiceError } from 'exceptions';
import BaseController from 'api/controllers/BaseController';
@@ -27,42 +26,42 @@ export default class ItemsCategoriesController extends BaseController {
...this.categoryValidationSchema,
...this.specificCategoryValidationSchema,
],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.editCategory.bind(this)),
this.handlerServiceError,
);
router.post('/', [
...this.categoryValidationSchema,
],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.newCategory.bind(this)),
this.handlerServiceError,
);
router.delete('/', [
...this.categoriesBulkValidationSchema,
],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.bulkDeleteCategories.bind(this)),
this.handlerServiceError,
);
router.delete('/:id', [
...this.specificCategoryValidationSchema
],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.deleteItem.bind(this)),
this.handlerServiceError,
);
router.get('/:id', [
...this.specificCategoryValidationSchema,
],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.getCategory.bind(this)),
this.handlerServiceError,
);
router.get('/', [
...this.categoriesListValidationSchema
],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.getList.bind(this)),
this.handlerServiceError,
);

View File

@@ -23,8 +23,7 @@ export default class ItemsController extends BaseController {
router() {
const router = Router();
router.post(
'/', [
router.post('/', [
...this.validateItemSchema,
],
this.validationResult,

View File

@@ -3,7 +3,6 @@ import { check, param, query, matchedData } from 'express-validator';
import { Service, Inject } from 'typedi';
import { difference } from 'lodash';
import { BillOTD } from 'interfaces';
import validateMiddleware from 'api/middleware/validateMiddleware';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import BillsService from 'services/Purchases/Bills';
import BaseController from 'api/controllers/BaseController';
@@ -30,7 +29,7 @@ export default class BillsController extends BaseController {
router.post(
'/',
[...this.billValidationSchema],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateVendorExistance.bind(this)),
asyncMiddleware(this.validateItemsIds.bind(this)),
asyncMiddleware(this.validateBillNumberExists.bind(this)),
@@ -40,7 +39,7 @@ export default class BillsController extends BaseController {
router.post(
'/:id',
[...this.billValidationSchema, ...this.specificBillValidationSchema],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateBillExistance.bind(this)),
asyncMiddleware(this.validateVendorExistance.bind(this)),
asyncMiddleware(this.validateItemsIds.bind(this)),
@@ -51,20 +50,20 @@ export default class BillsController extends BaseController {
router.get(
'/:id',
[...this.specificBillValidationSchema],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateBillExistance.bind(this)),
asyncMiddleware(this.getBill.bind(this))
);
router.get(
'/',
[...this.billsListingValidationSchema],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.listingBills.bind(this))
);
router.delete(
'/:id',
[...this.specificBillValidationSchema],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateBillExistance.bind(this)),
asyncMiddleware(this.deleteBill.bind(this))
);

View File

@@ -30,7 +30,7 @@ export default class BillsPayments extends BaseController {
router.post('/', [
...this.billPaymentSchemaValidation,
],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateBillPaymentVendorExistance.bind(this)),
asyncMiddleware(this.validatePaymentAccount.bind(this)),
asyncMiddleware(this.validatePaymentNumber.bind(this)),
@@ -42,7 +42,7 @@ export default class BillsPayments extends BaseController {
...this.billPaymentSchemaValidation,
...this.specificBillPaymentValidateSchema,
],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateBillPaymentVendorExistance.bind(this)),
asyncMiddleware(this.validatePaymentAccount.bind(this)),
asyncMiddleware(this.validatePaymentNumber.bind(this)),
@@ -53,19 +53,19 @@ export default class BillsPayments extends BaseController {
)
router.delete('/:id',
this.specificBillPaymentValidateSchema,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateBillPaymentExistance.bind(this)),
asyncMiddleware(this.deleteBillPayment.bind(this)),
);
router.get('/:id',
this.specificBillPaymentValidateSchema,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateBillPaymentExistance.bind(this)),
asyncMiddleware(this.getBillPayment.bind(this)),
);
router.get('/',
this.listingValidationSchema,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.getBillsPayments.bind(this))
);
return router;

View File

@@ -4,7 +4,6 @@ import { difference } from 'lodash';
import { Inject, Service } from 'typedi';
import { IPaymentReceive, IPaymentReceiveOTD } from 'interfaces';
import BaseController from 'api/controllers/BaseController';
import validateMiddleware from 'api/middleware/validateMiddleware';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import PaymentReceiveService from 'services/Sales/PaymentsReceives';
import SaleInvoiceService from 'services/Sales/SalesInvoices';
@@ -34,7 +33,7 @@ export default class PaymentReceivesController extends BaseController {
router.post(
'/:id',
this.editPaymentReceiveValidation,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validatePaymentReceiveExistance.bind(this)),
asyncMiddleware(this.validatePaymentReceiveNoExistance.bind(this)),
asyncMiddleware(this.validateCustomerExistance.bind(this)),
@@ -47,7 +46,7 @@ export default class PaymentReceivesController extends BaseController {
router.post(
'/',
this.newPaymentReceiveValidation,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validatePaymentReceiveNoExistance.bind(this)),
asyncMiddleware(this.validateCustomerExistance.bind(this)),
asyncMiddleware(this.validateDepositAccount.bind(this)),
@@ -58,20 +57,20 @@ export default class PaymentReceivesController extends BaseController {
router.get(
'/:id',
this.paymentReceiveValidation,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validatePaymentReceiveExistance.bind(this)),
asyncMiddleware(this.getPaymentReceive.bind(this))
);
router.get(
'/',
this.validatePaymentReceiveList,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.getPaymentReceiveList.bind(this)),
);
router.delete(
'/:id',
this.paymentReceiveValidation,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validatePaymentReceiveExistance.bind(this)),
asyncMiddleware(this.deletePaymentReceive.bind(this)),
);

View File

@@ -3,7 +3,6 @@ import { check, param, query, matchedData } from 'express-validator';
import { Inject, Service } from 'typedi';
import { ISaleEstimate, ISaleEstimateOTD } from 'interfaces';
import BaseController from 'api/controllers/BaseController'
import validateMiddleware from 'api/middleware/validateMiddleware';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleEstimateService from 'services/Sales/SalesEstimate';
import ItemsService from 'services/Items/ItemsService';
@@ -25,7 +24,7 @@ export default class SalesEstimatesController extends BaseController {
router.post(
'/',
this.estimateValidationSchema,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)),
asyncMiddleware(this.validateEstimateNumberExistance.bind(this)),
asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)),
@@ -36,7 +35,7 @@ export default class SalesEstimatesController extends BaseController {
...this.validateSpecificEstimateSchema,
...this.estimateValidationSchema,
],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)),
asyncMiddleware(this.validateEstimateNumberExistance.bind(this)),
@@ -48,21 +47,21 @@ export default class SalesEstimatesController extends BaseController {
'/:id', [
this.validateSpecificEstimateSchema,
],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
asyncMiddleware(this.deleteEstimate.bind(this))
);
router.get(
'/:id',
this.validateSpecificEstimateSchema,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
asyncMiddleware(this.getEstimate.bind(this))
);
router.get(
'/',
this.validateEstimateListSchema,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.getEstimates.bind(this))
);
return router;

View File

@@ -3,14 +3,14 @@ import { check, param, query, matchedData } from 'express-validator';
import { difference } from 'lodash';
import { raw } from 'objection';
import { Service, Inject } from 'typedi';
import validateMiddleware from 'api/middleware/validateMiddleware';
import BaseController from '../BaseController';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleInvoiceService from 'services/Sales/SalesInvoices';
import ItemsService from 'services/Items/ItemsService';
import { ISaleInvoiceOTD } from 'interfaces';
@Service()
export default class SaleInvoicesController {
export default class SaleInvoicesController extends BaseController{
@Inject()
itemsService: ItemsService;
@@ -26,7 +26,7 @@ export default class SaleInvoicesController {
router.post(
'/',
this.saleInvoiceValidationSchema,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)),
asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)),
asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)),
@@ -39,7 +39,7 @@ export default class SaleInvoicesController {
...this.saleInvoiceValidationSchema,
...this.specificSaleInvoiceValidation,
],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateInvoiceExistance.bind(this)),
asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)),
asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)),
@@ -52,7 +52,7 @@ export default class SaleInvoicesController {
router.delete(
'/:id',
this.specificSaleInvoiceValidation,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateInvoiceExistance.bind(this)),
asyncMiddleware(this.deleteSaleInvoice.bind(this))
);
@@ -64,13 +64,14 @@ export default class SaleInvoicesController {
router.get(
'/:id',
this.specificSaleInvoiceValidation,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateInvoiceExistance.bind(this)),
asyncMiddleware(this.getSaleInvoice.bind(this))
);
router.get(
'/',
this.saleInvoiceListValidationSchema,
this.validationResult,
asyncMiddleware(this.getSalesInvoices.bind(this))
)
return router;

View File

@@ -1,14 +1,14 @@
import { Router, Request, Response } from 'express';
import { check, param, query, matchedData } from 'express-validator';
import { Inject, Service } from 'typedi';
import validateMiddleware from 'api/middleware/validateMiddleware';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import AccountsService from 'services/Accounts/AccountsService';
import ItemsService from 'services/Items/ItemsService';
import SaleReceiptService from 'services/Sales/SalesReceipts';
import BaseController from '../BaseController';
@Service()
export default class SalesReceiptsController {
export default class SalesReceiptsController extends BaseController{
@Inject()
saleReceiptService: SaleReceiptService;
@@ -29,7 +29,7 @@ export default class SalesReceiptsController {
...this.specificReceiptValidationSchema,
...this.salesReceiptsValidationSchema,
],
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateSaleReceiptExistance.bind(this)),
asyncMiddleware(this.validateReceiptCustomerExistance.bind(this)),
asyncMiddleware(this.validateReceiptDepositAccountExistance.bind(this)),
@@ -40,7 +40,7 @@ export default class SalesReceiptsController {
router.post(
'/',
this.salesReceiptsValidationSchema,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateReceiptCustomerExistance.bind(this)),
asyncMiddleware(this.validateReceiptDepositAccountExistance.bind(this)),
asyncMiddleware(this.validateReceiptItemsIdsExistance.bind(this)),
@@ -49,14 +49,14 @@ export default class SalesReceiptsController {
router.delete(
'/:id',
this.specificReceiptValidationSchema,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateSaleReceiptExistance.bind(this)),
asyncMiddleware(this.deleteSaleReceipt.bind(this))
);
router.get(
'/',
this.listSalesReceiptsValidationSchema,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.listingSalesReceipts.bind(this))
);
return router;

View File

@@ -1,3 +1,4 @@
import { Service } from 'typedi';
import { Router, Request, Response } from 'express';
import { body, query } from 'express-validator';
import { pick } from 'lodash';
@@ -9,6 +10,7 @@ import {
isDefinedOptionConfigurable,
} from 'utils';
@Service()
export default class SettingsController extends BaseController{
/**
* Router constructor.

View File

@@ -6,7 +6,6 @@ import config from 'config';
import { License, Plan } from 'system/models';
import BaseController from 'api/controllers/BaseController';
import LicenseService from 'services/Payment/License';
import validateMiddleware from 'api/middleware/validateMiddleware';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import { ILicensesFilter } from 'interfaces';
@@ -31,13 +30,13 @@ export default class LicensesController extends BaseController {
router.post(
'/generate',
this.generateLicenseSchema,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validatePlanExistance.bind(this)),
asyncMiddleware(this.generateLicense.bind(this)),
);
router.post(
'/disable/:licenseId',
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validateLicenseExistance.bind(this)),
asyncMiddleware(this.validateNotDisabledLicense.bind(this)),
asyncMiddleware(this.disableLicense.bind(this)),
@@ -45,7 +44,7 @@ export default class LicensesController extends BaseController {
router.post(
'/send',
this.sendLicenseSchemaValidation,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.sendLicense.bind(this)),
);
router.delete(

View File

@@ -1,7 +1,6 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response } from 'express';
import { check } from 'express-validator';
import validateMiddleware from 'api/middleware/validateMiddleware';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import PaymentMethodController from 'api/controllers/Subscription/PaymentMethod';
import {
@@ -26,7 +25,7 @@ export default class PaymentViaLicenseController extends PaymentMethodController
router.post(
'/payment',
this.paymentViaLicenseSchema,
validateMiddleware,
this.validationResult,
asyncMiddleware(this.validatePlanSlugExistance.bind(this)),
asyncMiddleware(this.paymentViaLicense.bind(this)),
);

View File

@@ -1,13 +0,0 @@
import { validationResult } from 'express-validator';
export default (req, res, next) => {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error',
...validationErrors,
});
}
next();
}

View File

@@ -1,21 +0,0 @@
exports.up = function (knex) {
return knex.schema.createTable('users', (table) => {
table.increments();
table.string('first_name');
table.string('last_name');
table.string('email').unique();
table.string('phone_number').unique();
table.boolean('active');
table.integer('role_id').unique();
table.string('language');
table.date('last_login_at');
table.date('invite_accepted_at');
table.timestamps();
}).raw('ALTER TABLE `USERS` AUTO_INCREMENT = 1000');;
};
exports.down = function (knex) {
return knex.schema.dropTableIfExists('users');
};

View File

@@ -3,9 +3,9 @@ exports.up = (knex) => {
return knex.schema.createTable('account_types', (table) => {
table.increments();
table.string('name');
table.string('key');
table.string('normal');
table.string('root_type');
table.string('key').index();
table.string('normal').index();
table.string('root_type').index();
table.string('child_type');
table.boolean('balance_sheet');
table.boolean('income_sheet');

View File

@@ -0,0 +1,20 @@
exports.up = function (knex) {
return knex.schema.createTable('accounts', (table) => {
table.increments('id').comment('Auto-generated id');;
table.string('name').index();
table.string('slug');
table.integer('account_type_id').unsigned().references('id').inTable('account_types');
table.integer('parent_account_id').unsigned().references('id').inTable('accounts');
table.string('code', 10).index();
table.text('description');
table.boolean('active').defaultTo(true).index();
table.integer('index').unsigned();
table.boolean('predefined').defaultTo(false).index();
table.decimal('amount', 15, 5);
table.string('currency_code', 3).index();
table.timestamps();
}).raw('ALTER TABLE `ACCOUNTS` AUTO_INCREMENT = 1000');
};
exports.down = (knex) => knex.schema.dropTableIfExists('accounts');

View File

@@ -1,27 +0,0 @@
exports.up = function (knex) {
return knex.schema.createTable('items', (table) => {
table.increments();
table.string('name');
table.string('type');
table.string('sku');
table.boolean('sellable');
table.boolean('purchasable');
table.decimal('sell_price', 13, 3).unsigned();
table.decimal('cost_price', 13, 3).unsigned();
table.string('currency_code', 3);
table.string('picture_uri');
table.integer('cost_account_id').unsigned();
table.integer('sell_account_id').unsigned();
table.integer('inventory_account_id').unsigned();
table.text('sell_description').nullable();
table.text('purchase_description').nullable();
table.integer('quantity_on_hand');
table.text('note').nullable();
table.integer('category_id').unsigned();
table.integer('user_id').unsigned();
table.timestamps();
}).raw('ALTER TABLE `ITEMS` AUTO_INCREMENT = 1000');;
};
exports.down = (knex) => knex.schema.dropTableIfExists('items');

View File

@@ -1,20 +0,0 @@
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);
table.text('description');
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');
};
exports.down = (knex) => knex.schema.dropTableIfExists('accounts');

View File

@@ -0,0 +1,19 @@
exports.up = function (knex) {
return knex.schema.createTable('items_categories', (table) => {
table.increments();
table.string('name').index();
table.integer('parent_category_id').unsigned().references('id').inTable('items_categories');
table.text('description');
table.integer('user_id').unsigned().index();
table.integer('cost_account_id').unsigned().references('id').inTable('accounts');
table.integer('sell_account_id').unsigned().references('id').inTable('accounts');
table.integer('inventory_account_id').unsigned().references('id').inTable('accounts');
table.string('cost_method');
table.timestamps();
});
};
exports.down = (knex) => knex.schema.dropTableIfExists('items_categories');

View File

@@ -1,19 +0,0 @@
exports.up = function (knex) {
return knex.schema.createTable('items_categories', (table) => {
table.increments();
table.string('name');
table.integer('parent_category_id').unsigned();
table.text('description');
table.integer('user_id').unsigned();
table.integer('cost_account_id').unsigned();
table.integer('sell_account_id').unsigned();
table.integer('inventory_account_id').unsigned();
table.string('cost_method');
table.timestamps();
});
};
exports.down = (knex) => knex.schema.dropTableIfExists('items_categories');

View File

@@ -0,0 +1,27 @@
exports.up = function (knex) {
return knex.schema.createTable('items', (table) => {
table.increments();
table.string('name').index();
table.string('type').index();
table.string('sku');
table.boolean('sellable').index();
table.boolean('purchasable').index();
table.decimal('sell_price', 13, 3).unsigned();
table.decimal('cost_price', 13, 3).unsigned();
table.string('currency_code', 3);
table.string('picture_uri');
table.integer('cost_account_id').nullable().unsigned().references('id').inTable('accounts');
table.integer('sell_account_id').nullable().unsigned().references('id').inTable('accounts');
table.integer('inventory_account_id').unsigned().references('id').inTable('accounts');
table.text('sell_description').nullable();
table.text('purchase_description').nullable();
table.integer('quantity_on_hand');
table.text('note').nullable();
table.integer('category_id').unsigned().index().references('id').inTable('items_categories');
table.integer('user_id').unsigned().index();
table.timestamps();
}).raw('ALTER TABLE `ITEMS` AUTO_INCREMENT = 1000');
};
exports.down = (knex) => knex.schema.dropTableIfExists('items');

View File

@@ -2,9 +2,9 @@
exports.up = function (knex) {
return knex.schema.createTable('views', (table) => {
table.increments();
table.string('name');
table.string('name').index();
table.boolean('predefined');
table.string('resource_model');
table.string('resource_model').index();
table.boolean('favourite');
table.string('roles_logic_expression');
table.timestamps();

View File

@@ -2,10 +2,10 @@
exports.up = function (knex) {
return knex.schema.createTable('settings', (table) => {
table.increments();
table.integer('user_id').unsigned();
table.string('group');
table.integer('user_id').unsigned().index();
table.string('group').index();
table.string('type');
table.string('key');
table.string('key').index();
table.string('value');
}).raw('ALTER TABLE `SETTINGS` AUTO_INCREMENT = 2000');
};

View File

@@ -2,7 +2,7 @@
exports.up = function (knex) {
return knex.schema.createTable('view_has_columns', (table) => {
table.increments();
table.integer('view_id').unsigned();
table.integer('view_id').unsigned().index().references('id').inTable('views');
table.string('field_key');
table.integer('index').unsigned();
}).raw('ALTER TABLE `ITEMS_CATEGORIES` AUTO_INCREMENT = 1000');

View File

@@ -3,10 +3,10 @@ exports.up = function (knex) {
return knex.schema.createTable('view_roles', (table) => {
table.increments();
table.integer('index');
table.string('field_key');
table.string('field_key').index();
table.string('comparator');
table.string('value');
table.integer('view_id').unsigned();
table.integer('view_id').unsigned().index().references('id').inTable('views');
}).raw('ALTER TABLE `VIEW_ROLES` AUTO_INCREMENT = 1000');
};

View File

@@ -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');
};

View File

@@ -4,17 +4,17 @@ exports.up = function(knex) {
table.increments();
table.decimal('credit', 13, 3);
table.decimal('debit', 13, 3);
table.string('transaction_type');
table.string('reference_type');
table.integer('reference_id');
table.integer('account_id').unsigned();
table.string('contact_type').nullable();
table.integer('contact_id').unsigned().nullable();
table.string('transaction_type').index();
table.string('reference_type').index();
table.integer('reference_id').index();
table.integer('account_id').unsigned().index().references('id').inTable('accounts');
table.string('contact_type').nullable().index();
table.integer('contact_id').unsigned().nullable().index().references('id').inTable('contacts');
table.string('note');
table.boolean('draft').defaultTo(false);
table.integer('user_id').unsigned();
table.integer('user_id').unsigned().index();
table.integer('index').unsigned();
table.date('date');
table.date('date').index();
table.timestamps();
}).raw('ALTER TABLE `ACCOUNTS_TRANSACTIONS` AUTO_INCREMENT = 1000');
};

View File

@@ -1,14 +0,0 @@
exports.up = function(knex) {
return knex.schema.createTable('options', (table) => {
table.increments();
table.string('key');
table.string('value');
table.string('group');
table.string('type');
});
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('options');
};

View File

@@ -5,12 +5,12 @@ exports.up = function(knex) {
table.decimal('total_amount', 13, 3);
table.string('currency_code', 3);
table.text('description');
table.integer('payment_account_id').unsigned();
table.integer('payee_id').unsigned();
table.integer('payment_account_id').unsigned().references('id').inTable('accounts');
table.integer('payee_id').unsigned().references('id').inTable('contacts');;
table.string('reference_no');
table.date('published_at');
table.integer('user_id').unsigned();
table.date('payment_date');
table.date('published_at').index();
table.integer('user_id').unsigned().index();
table.date('payment_date').index();
table.timestamps();
}).raw('ALTER TABLE `EXPENSES_TRANSACTIONS` AUTO_INCREMENT = 1000');
};

View File

@@ -2,15 +2,15 @@
exports.up = function(knex) {
return knex.schema.createTable('manual_journals', (table) => {
table.increments();
table.string('journal_number');
table.string('reference');
table.string('journal_type');
table.string('journal_number').index();
table.string('reference').index();
table.string('journal_type').index();
table.decimal('amount', 13, 3);
table.date('date');
table.boolean('status').defaultTo(false);
table.date('date').index();
table.boolean('status').defaultTo(false).index();
table.string('description');
table.string('attachment_file');
table.integer('user_id').unsigned();
table.integer('user_id').unsigned().index();
table.timestamps();
}).raw('ALTER TABLE `MANUAL_JOURNALS` AUTO_INCREMENT = 1000');
};

View File

@@ -2,8 +2,8 @@
exports.up = function(knex) {
return knex.schema.createTable('currencies', table => {
table.increments();
table.string('currency_name');
table.string('currency_code', 4);
table.string('currency_name').index();
table.string('currency_code', 4).index();
table.timestamps();
}).raw('ALTER TABLE `CURRENCIES` AUTO_INCREMENT = 1000');
};

View File

@@ -2,9 +2,9 @@
exports.up = function(knex) {
return knex.schema.createTable('exchange_rates', table => {
table.increments();
table.string('currency_code', 4);
table.string('currency_code', 4).index();
table.decimal('exchange_rate');
table.date('date');
table.date('date').index();
table.timestamps();
}).raw('ALTER TABLE `EXCHANGE_RATES` AUTO_INCREMENT = 1000');
};

View File

@@ -2,9 +2,9 @@
exports.up = function(knex) {
return knex.schema.createTable('media_links', table => {
table.increments();
table.string('model_name');
table.integer('media_id').unsigned();
table.integer('model_id').unsigned();
table.string('model_name').index();
table.integer('media_id').unsigned().index();
table.integer('model_id').unsigned().index();
})
};

View File

@@ -2,11 +2,11 @@
exports.up = function(knex) {
return knex.schema.createTable('expense_transaction_categories', table => {
table.increments();
table.integer('expense_account_id').unsigned();
table.integer('expense_account_id').unsigned().index().references('id').inTable('accounts');
table.integer('index').unsigned();
table.text('description');
table.decimal('amount', 13, 3);
table.integer('expense_id').unsigned();
table.integer('expense_id').unsigned().index().references('id').inTable('expenses_transactions');
table.timestamps();
}).raw('ALTER TABLE `EXPENSE_TRANSACTION_CATEGORIES` AUTO_INCREMENT = 1000');;
};

View File

@@ -3,15 +3,15 @@ exports.up = function(knex) {
return knex.schema.createTable('sales_estimates', (table) => {
table.increments();
table.decimal('amount', 13, 3);
table.integer('customer_id').unsigned();
table.date('estimate_date');
table.date('expiration_date');
table.integer('customer_id').unsigned().index().references('id').inTable('contacts');
table.date('estimate_date').index();
table.date('expiration_date').index();
table.string('reference');
table.string('estimate_number');
table.string('estimate_number').index();
table.text('note');
table.text('terms_conditions');
table.integer('user_id').unsigned();
table.integer('user_id').unsigned().index();
table.timestamps();
});
};

View File

@@ -3,9 +3,9 @@ exports.up = function(knex) {
return knex.schema.createTable('sales_receipts', table => {
table.increments();
table.decimal('amount', 13, 3);
table.integer('deposit_account_id').unsigned();
table.integer('customer_id').unsigned();
table.date('receipt_date');
table.integer('deposit_account_id').unsigned().index().references('id').inTable('accounts');
table.integer('customer_id').unsigned().index().references('id').inTable('contacts');
table.date('receipt_date').index();
table.string('reference_no');
table.string('email_send_to');
table.text('receipt_message');

View File

@@ -2,12 +2,12 @@
exports.up = function(knex) {
return knex.schema.createTable('sales_invoices', table => {
table.increments();
table.integer('customer_id');
table.date('invoice_date');
table.integer('customer_id').unsigned().index().references('id').inTable('contacts')
table.date('invoice_date').index();
table.date('due_date');
table.string('invoice_no');
table.string('invoice_no').index();
table.string('reference_no');
table.string('status');
table.string('status').index();
table.text('invoice_message');
table.text('terms_conditions');
@@ -15,7 +15,7 @@ exports.up = function(knex) {
table.decimal('balance', 13, 3);
table.decimal('payment_amount', 13, 3);
table.string('inv_lot_number');
table.string('inv_lot_number').index();
table.timestamps();
});
};

View File

@@ -3,14 +3,14 @@ const { knexSnakeCaseMappers } = require("objection");
exports.up = function(knex) {
return knex.schema.createTable('payment_receives', (table) => {
table.increments();
table.integer('customer_id').unsigned();
table.date('payment_date');
table.integer('customer_id').unsigned().index().references('id').inTable('contacts');
table.date('payment_date').index();
table.decimal('amount', 13, 3).defaultTo(0);
table.string('reference_no');
table.integer('deposit_account_id').unsigned();
table.string('reference_no').index();
table.integer('deposit_account_id').unsigned().references('id').inTable('accounts');
table.string('payment_receive_no');
table.text('description');
table.integer('user_id').unsigned();
table.integer('user_id').unsigned().index();
table.timestamps();
});
};

View File

@@ -2,8 +2,8 @@
exports.up = function(knex) {
return knex.schema.createTable('payment_receives_entries', table => {
table.increments();
table.integer('payment_receive_id').unsigned();
table.integer('invoice_id').unsigned();
table.integer('payment_receive_id').unsigned().index().references('id').inTable('payment_receives');
table.integer('invoice_id').unsigned().index().references('id').inTable('sales_invoices');
table.decimal('payment_amount').unsigned();
})
};

View File

@@ -2,18 +2,18 @@
exports.up = function(knex) {
return knex.schema.createTable('bills', (table) => {
table.increments();
table.integer('vendor_id').unsigned();
table.integer('vendor_id').unsigned().index().references('id').inTable('contacts');
table.string('bill_number');
table.date('bill_date');
table.date('due_date');
table.date('bill_date').index();
table.date('due_date').index();
table.string('reference_no');
table.string('status');
table.string('status').index();
table.text('note');
table.decimal('amount', 13, 3).defaultTo(0);
table.decimal('payment_amount', 13, 3).defaultTo(0);
table.string('inv_lot_number');
table.string('inv_lot_number').index();
table.timestamps();
});
};

View File

@@ -2,14 +2,14 @@
exports.up = function(knex) {
return knex.schema.createTable('bills_payments', table => {
table.increments();
table.integer('vendor_id').unsigned();
table.integer('vendor_id').unsigned().index().references('id').inTable('contacts');
table.decimal('amount', 13, 3).defaultTo(0);
table.integer('payment_account_id');
table.string('payment_number');
table.date('payment_date');
table.integer('payment_account_id').unsigned().references('id').inTable('accounts');
table.string('payment_number').index();
table.date('payment_date').index();
table.string('payment_method');
table.string('reference');
table.integer('user_id').unsigned();
table.integer('user_id').unsigned().index();
table.text('description');
table.timestamps();
});

View File

@@ -2,20 +2,20 @@
exports.up = function(knex) {
return knex.schema.createTable('inventory_transactions', table => {
table.increments('id');
table.date('date');
table.date('date').index();
table.string('direction');
table.string('direction').index();
table.integer('item_id').unsigned();
table.integer('item_id').unsigned().index().references('id').inTable('items');
table.integer('quantity').unsigned();
table.decimal('rate', 13, 3).unsigned();
table.integer('lot_number');
table.integer('lot_number').index();
table.string('transaction_type');
table.integer('transaction_id').unsigned();
table.string('transaction_type').index();
table.integer('transaction_id').unsigned().index();
table.integer('entry_id').unsigned();
table.integer('entry_id').unsigned().index();
table.timestamps();
});
};

View File

@@ -2,11 +2,11 @@
exports.up = function(knex) {
return knex.schema.createTable('items_entries', (table) => {
table.increments();
table.string('reference_type');
table.string('reference_id');
table.string('reference_type').index();
table.string('reference_id').index();
table.integer('index').unsigned();
table.integer('item_id');
table.integer('item_id').unsigned().index().references('id').inTable('items');
table.text('description');
table.integer('discount').unsigned();
table.integer('quantity').unsigned();

View File

@@ -3,8 +3,8 @@ exports.up = function(knex) {
return knex.schema.createTable('bills_payments_entries', table => {
table.increments();
table.integer('bill_payment_id').unsigned();
table.integer('bill_id').unsigned();
table.integer('bill_payment_id').unsigned().index().references('id').inTable('bills_payments');
table.integer('bill_id').unsigned().index();
table.decimal('payment_amount', 13, 3).unsigned();
})
};

View File

@@ -2,19 +2,19 @@
exports.up = function(knex) {
return knex.schema.createTable('inventory_cost_lot_tracker', table => {
table.increments();
table.date('date');
table.string('direction');
table.date('date').index();
table.string('direction').index();
table.integer('item_id').unsigned();
table.integer('quantity').unsigned();
table.integer('item_id').unsigned().index();
table.integer('quantity').unsigned().index();
table.decimal('rate', 13, 3);
table.integer('remaining');
table.integer('cost');
table.integer('lot_number');
table.integer('lot_number').index();
table.string('transaction_type');
table.integer('transaction_id').unsigned();
table.integer('entry_id').unsigned();
table.string('transaction_type').index();
table.integer('transaction_id').unsigned().index();
table.integer('entry_id').unsigned().index();
});
};

View File

@@ -9,7 +9,7 @@ export * from './Payment';
export * from './SaleInvoice';
export * from './PaymentReceive';
export * from './SaleEstimate';
export * from './Register';
export * from './Authentication';
export * from './User';
export * from './Metable';
export * from './Options';

View File

@@ -1,4 +1,7 @@
import { Container } from 'typedi';
// Here we import all events.
import 'subscribers/authentication';
import 'subscribers/organization';
import 'subscribers/manualJournals';
import 'subscribers/expenses';

View File

@@ -10,9 +10,7 @@ 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';
import ExpenseCategory from 'models/ExpenseCategory';
@@ -49,8 +47,6 @@ export default (knex) => {
BillPayment,
BillPaymentEntry,
Currency,
Customer,
Vendor,
ExchangeRate,
Expense,
ExpenseCategory,

View File

@@ -36,17 +36,20 @@ export default class Bill extends TenantModel {
* Relationship mapping.
*/
static get relationMappings() {
const Vendor = require('models/Vendor');
const Contact = require('models/Contact');
const ItemEntry = require('models/ItemEntry');
return {
vendor: {
relation: Model.BelongsToOneRelation,
modelClass: Vendor.default,
modelClass: Contact.default,
join: {
from: 'bills.vendorId',
to: 'vendors.id',
to: 'contacts.id',
},
filter(query) {
query.where('contact_type', 'Vendor');
}
},
entries: {

View File

@@ -22,7 +22,7 @@ export default class BillPayment extends TenantModel {
static get relationMappings() {
const BillPaymentEntry = require('models/BillPaymentEntry');
const AccountTransaction = require('models/AccountTransaction');
const Vendor = require('models/Vendor');
const Contact = require('models/Contact');
const Account = require('models/Account');
return {
@@ -37,11 +37,14 @@ export default class BillPayment extends TenantModel {
vendor: {
relation: Model.BelongsToOneRelation,
modelClass: Vendor.default,
modelClass: Contact.default,
join: {
from: 'bills_payments.vendorId',
to: 'vendors.id',
to: 'contacts.id',
},
filter(query) {
query.where('contact_type', 'Vendor');
}
},
paymentAccount: {

View File

@@ -1,81 +0,0 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class Customer extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'customers';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
filterCustomerIds(query, customerIds) {
query.whereIn('id', customerIds);
},
};
}
/**
* 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);
}
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);
}
}

View File

@@ -22,17 +22,20 @@ export default class PaymentReceive extends TenantModel {
static get relationMappings() {
const PaymentReceiveEntry = require('models/PaymentReceiveEntry');
const AccountTransaction = require('models/AccountTransaction');
const Customer = require('models/Customer');
const Contact = require('models/Contact');
const Account = require('models/Account');
return {
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Customer.default,
modelClass: Contact.default,
join: {
from: 'payment_receives.customerId',
to: 'customers.id',
to: 'contacts.id',
},
filter(query) {
query.where('contact_type', 'Customer');
}
},
depositAccount: {

View File

@@ -21,16 +21,19 @@ export default class SaleEstimate extends TenantModel {
*/
static get relationMappings() {
const ItemEntry = require('models/ItemEntry');
const Customer = require('models/Customer');
const Contact = require('models/Contact');
return {
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Customer.default,
modelClass: Contact.default,
join: {
from: 'sales_estimates.customerId',
to: 'customers.id',
to: 'contacts.id',
},
filter(query) {
query.where('contact_type', 'Customer');
}
},
entries: {

View File

@@ -57,7 +57,7 @@ export default class SaleInvoice extends TenantModel {
static get relationMappings() {
const AccountTransaction = require('models/AccountTransaction');
const ItemEntry = require('models/ItemEntry');
const Customer = require('models/Customer');
const Contact = require('models/Contact');
const InventoryCostLotTracker = require('models/InventoryCostLotTracker');
return {
@@ -75,11 +75,14 @@ export default class SaleInvoice extends TenantModel {
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Customer.default,
modelClass: Contact.default,
join: {
from: 'sales_invoices.customerId',
to: 'customers.id',
to: 'contacts.id',
},
filter(query) {
query.where('contact_type', 'Customer');
}
},
transactions: {

View File

@@ -20,7 +20,7 @@ export default class SaleReceipt extends TenantModel {
* Relationship mapping.
*/
static get relationMappings() {
const Customer = require('models/Customer');
const Contact = require('models/Contact');
const Account = require('models/Account');
const AccountTransaction = require('models/AccountTransaction');
const ItemEntry = require('models/ItemEntry');
@@ -28,11 +28,14 @@ export default class SaleReceipt extends TenantModel {
return {
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Customer.default,
modelClass: Contact.default,
join: {
from: 'sales_receipts.customerId',
to: 'customers.id',
to: 'contacts.id',
},
filter(query) {
query.where('contact_type', 'Customer');
}
},
depositAccount: {

View File

@@ -1,61 +0,0 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class Vendor extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'vendors';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Changes the vendor balance.
* @param {Integer} customerId
* @param {Number} amount
* @return {Promise}
*/
static async changeBalance(vendorId, amount) {
const changeMethod = amount > 0 ? 'increment' : 'decrement';
return this.query()
.where('id', vendorId)
[changeMethod]('balance', Math.abs(amount));
}
/**
*
* @param {number} vendorId - Specific vendor id.
* @param {number} oldVendorId - The given old vendor id.
* @param {number} amount - The new change amount.
* @param {number} oldAmount - The old stored amount.
*/
static changeDiffBalance(vendorId, oldVendorId, amount, oldAmount) {
const diffAmount = (amount - oldAmount);
const asyncOpers = [];
if (vendorId != oldVendorId) {
const oldVendorOper = Vendor.changeBalance(
oldVendorId,
(oldAmount * -1)
);
const vendorOper = Vendor.changeBalance(
vendorId,
amount,
);
asyncOpers.push(vendorOper);
asyncOpers.push(oldVendorOper);
} else {
const balanceChangeOper = Vendor.changeBalance(vendorId, diffAmount);
asyncOpers.push(balanceChangeOper);
}
return Promise.all(asyncOpers);
}
}

View File

@@ -1,5 +1,3 @@
import Customer from './Customer';
import Vendor from './Vendor';
import Option from './Option';
import SaleEstimate from './SaleEstimate';
import SaleEstimateEntry from './SaleEstimateEntry';
@@ -22,8 +20,6 @@ import AccountType from './AccountType';
import InventoryLotCostTracker from './InventoryCostLotTracker';
export {
Customer,
Vendor,
SaleEstimate,
SaleEstimateEntry,
SaleReceipt,

View File

@@ -1,5 +1,6 @@
import TenantRepository from 'repositories/TenantRepository';
import { IAccount } from 'interfaces';
import { Account } from 'models';
export default class AccountRepository extends TenantRepository {
models: any;
@@ -57,7 +58,7 @@ export default class AccountRepository extends TenantRepository {
/**
* Retrieve the account by the given id.
* @param {number} id - Account id.
* @param {number} id - Account id.
* @return {IAccount}
*/
getById(id: number): IAccount {
@@ -67,4 +68,63 @@ export default class AccountRepository extends TenantRepository {
});
}
/**
* Retrieve accounts by the given ids.
* @param {number[]} ids -
* @return {IAccount[]}
*/
findByIds(accountsIds: number[]) {
const { Account } = this.models;
return Account.query().whereIn('id', accountsIds);
}
/**
* Activate the given account.
* @param {number} accountId -
* @return {void}
*/
async activate(accountId: number): Promise<void> {
const { Account } = this.models;
await Account.query().findById(accountId).patch({ active: 1 })
this.flushCache();
}
/**
* Inserts a new accounts to the storage.
* @param {IAccount} account
*/
async insert(account: IAccount): Promise<void> {
const { Account } = this.models;
await Account.query().insertAndFetch({ ...account });
this.flushCache();
}
/**
* Updates account of the given account.
* @param {number} accountId - Account id.
* @param {IAccount} account
* @return {void}
*/
async edit(accountId: number, account: IAccount): Promise<void> {
const { Account } = this.models;
await Account.query().findById(accountId).patch({ ...account });
this.flushCache();
}
/**
* Deletes the given account by id.
* @param {number} accountId - Account id.
*/
async deleteById(accountId: number): Promise<void> {
const { Account } = this.models;
await Account.query().deleteById(accountId);
this.flushCache();
}
/**
* Flush repository cache.
*/
flushCache(): void {
this.cache.delStartWith('accounts');
}
}

View File

@@ -76,4 +76,11 @@ export default class AccountTypeRepository extends TenantRepository {
return AccountType.query().where('root_type', rootType);
});
}
/**
* Flush repository cache.
*/
flushCache() {
this.cache.delStartWith('accountType');
}
}

View File

@@ -1,4 +1,6 @@
import TenantRepository from 'repositories/TenantRepository';
import { IContact } from 'interfaces';
import Contact from 'models/Contact';
export default class ContactRepository extends TenantRepository {
cache: any;
@@ -17,21 +19,70 @@ export default class ContactRepository extends TenantRepository {
this.cache = this.tenancy.cache(tenantId);
}
findById(contactId: number) {
/**
* Retrieve the given contact model.
* @param {number} contactId
*/
findById(contactId: number): IContact {
const { Contact } = this.models;
return this.cache.get(`contact.id.${contactId}`, () => {
return this.cache.get(`contacts.id.${contactId}`, () => {
return Contact.query().findById(contactId);
})
}
findByIds(contactIds: number[]) {
/**
* Retrieve the given contacts model.
* @param {number[]} contactIds - Contacts ids.
*/
findByIds(contactIds: number[]): IContact[] {
const { Contact } = this.models;
return this.cache.get(`contact.ids.${contactIds.join(',')}`, () => {
return this.cache.get(`contacts.ids.${contactIds.join(',')}`, () => {
return Contact.query().whereIn('id', contactIds);
});
}
insert(contact) {
/**
* Inserts a new contact model.
* @param contact
*/
async insert(contact) {
await Contact.query().insert({ ...contact })
this.flushCache();
}
/**
* Updates the contact details.
* @param {number} contactId - Contact id.
* @param {IContact} contact - Contact input.
*/
async update(contactId: number, contact: IContact) {
await Contact.query().findById(contactId).patch({ ...contact });
this.flushCache();
}
/**
* Deletes contact of the given id.
* @param {number} contactId -
* @return {Promise<void>}
*/
async deleteById(contactId: number): Promise<void> {
await Contact.query().where('id', contactId).delete();
this.flushCache();
}
/**
* Deletes contacts in bulk.
* @param {number[]} contactsIds
*/
async bulkDelete(contactsIds: number[]) {
await Contact.query().whereIn('id', contactsIds);
this.flushCache();
}
/**
* Flush contact repository cache.
*/
flushCache() {
this.cache.delStartWith(`contacts`);
}
}

View File

@@ -1,26 +0,0 @@
import { Customer } from 'models';
export default class CustomerRepository {
static changeDiffBalance(customerId, oldCustomerId, amount, oldAmount) {
const diffAmount = amount - oldAmount;
const asyncOpers = [];
if (customerId != oldCustomerId) {
const oldCustomerOper = Customer.changeBalance(
oldCustomerId,
(oldAmount * -1)
);
const customerOper = Customer.changeBalance(
customerId,
amount,
);
asyncOpers.push(customerOper);
asyncOpers.push(oldCustomerOper);
} else {
const balanceChangeOper = Customer.changeBalance(customerId, diffAmount);
asyncOpers.push(balanceChangeOper);
}
return Promise.all(asyncOpers);
}
}

View File

@@ -17,7 +17,7 @@ export default class CustomerRepository extends TenantRepository {
/**
* Retrieve customer details of the given id.
* @param {number} customerId -
* @param {number} customerId - Customer id.
*/
getById(customerId: number) {
const { Contact } = this.models;

View File

@@ -7,6 +7,10 @@ export default class ExpenseRepository extends TenantRepository {
repositories: any;
cache: any;
/**
* Constructor method.
* @param {number} tenantId
*/
constructor(tenantId: number) {
super(tenantId);
@@ -14,38 +18,97 @@ export default class ExpenseRepository extends TenantRepository {
this.cache = this.tenancy.cache(tenantId);
}
/**
* Retrieve the given expense by id.
* @param {number} expenseId
* @return {Promise<IExpense>}
*/
getById(expenseId: number) {
const { Expense } = this.models;
return this.cache.get(`expense.id.${expenseId}`, () => {
return Expense.query().findById(expenseId);
})
}
create(expense: IExpense) {
const { Expense } = this.models;
return Expense.query().insert({ ...expense });
}
update(expenseId: number, expense: IExpense) {
const { Expense } = this.models;
return Expense.query().patchAndFetchById(expenseId, { ...expense });
}
publish(expenseId: number) {
const { Expense } = this.models;
return Expense.query().findById(expenseId).patch({
publishedAt: moment().toMySqlDateTime(),
return Expense.query().findById(expenseId).withGraphFetched('categories');
});
}
delete(expenseId: number) {
/**
* Inserts a new expense object.
* @param {IExpense} expense -
*/
async create(expense: IExpense): Promise<void> {
const { Expense } = this.models;
return Expense.query().findById(expenseId).delete();
await Expense.query().insert({ ...expense });
this.flushCache();
}
bulkDelete(expensesIds: number[]) {
/**
* Updates the given expense details.
* @param {number} expenseId
* @param {IExpense} expense
*/
async update(expenseId: number, expense: IExpense) {
const { Expense } = this.models;
return Expense.query().whereIn('id', expensesIds).delete();
await Expense.query().findById(expenseId).patch({ ...expense });
this.flushCache();
}
/**
* Publish the given expense.
* @param {number} expenseId
*/
async publish(expenseId: number): Promise<void> {
const { Expense } = this.models;
await Expense.query().findById(expenseId).patch({
publishedAt: moment().toMySqlDateTime(),
});
this.flushCache();
}
/**
* Deletes the given expense.
* @param {number} expenseId
*/
async delete(expenseId: number): Promise<void> {
const { Expense } = this.models;
await Expense.query().where('id', expenseId).delete();
await Expense.query().where('expense_id', expenseId).delete();
this.flushCache();
}
/**
* Deletes expenses in bulk.
* @param {number[]} expensesIds
*/
async bulkDelete(expensesIds: number[]): Promise<void> {
const { Expense } = this.models;
await Expense.query().whereIn('expense_id', expensesIds).delete();
await Expense.query().whereIn('id', expensesIds).delete();
this.flushCache();
}
/**
* Publishes the given expenses in bulk.
* @param {number[]} expensesIds
* @return {Promise<void>}
*/
async bulkPublish(expensesIds: number): Promise<void> {
const { Expense } = this.models;
await Expense.query().whereIn('id', expensesIds).patch({
publishedAt: moment().toMySqlDateTime(),
});
this.flushCache();
}
/**
* Flushes repository cache.
*/
flushCache() {
this.cache.delStartWith(`expense`);
}
}

View File

@@ -1,3 +1,4 @@
import { IVendor } from "interfaces";
import TenantRepository from "./TenantRepository";
@@ -18,7 +19,7 @@ export default class VendorRepository extends TenantRepository {
/**
* Retrieve the bill that associated to the given vendor id.
* @param {number} vendorId
* @param {number} vendorId - Vendor id.
*/
getBills(vendorId: number) {
const { Bill } = this.models;
@@ -29,16 +30,17 @@ export default class VendorRepository extends TenantRepository {
}
/**
*
* Retrieve all the given vendors.
* @param {numner[]} vendorsIds
* @return {IVendor}
*/
vendors(vendorsIds: number[]) {
vendors(vendorsIds: number[]): IVendor[] {
const { Contact } = this.models;
return Contact.query().modifier('vendor').whereIn('id', vendorsIds);
}
/**
*
* Retrieve vendors with associated bills.
* @param {number[]} vendorIds
*/
vendorsWithBills(vendorIds: number[]) {

View File

@@ -2,9 +2,12 @@ import { sumBy, chain } from 'lodash';
import JournalPoster from "./JournalPoster";
import JournalEntry from "./JournalEntry";
import { AccountTransaction } from 'models';
import { IInventoryTransaction, IManualJournal } from 'interfaces';
import AccountsService from '../Accounts/AccountsService';
import { IInventoryTransaction, IInventoryTransaction } from '../../interfaces';
import {
IInventoryTransaction,
IManualJournal,
IExpense,
IExpenseCategory,
} from 'interfaces';
interface IInventoryCostEntity {
date: Date,
@@ -120,6 +123,36 @@ export default class JournalCommands{
this.journal.credit(creditEntry);
}
/**
* Writes journal entries of expense model object.
* @param {IExpense} expense
*/
expense(expense: IExpense) {
const mixinEntry = {
referenceType: 'Expense',
referenceId: expense.id,
date: expense.paymentDate,
userId: expense.userId,
draft: !expense.publishedAt,
};
const paymentJournalEntry = new JournalEntry({
credit: expense.totalAmount,
account: expense.paymentAccountId,
...mixinEntry,
});
this.journal.credit(paymentJournalEntry);
expense.categories.forEach((category: IExpenseCategory) => {
const expenseJournalEntry = new JournalEntry({
account: category.expenseAccountId,
debit: category.amount,
note: category.description,
...mixinEntry,
});
this.journal.debit(expenseJournalEntry);
});
}
/**
*
* @param {number|number[]} referenceId

View File

@@ -1,10 +1,15 @@
import { Inject, Service } from 'typedi';
import { difference } from 'lodash';
import { kebabCase } from 'lodash'
import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions';
import { IAccountDTO, IAccount, IAccountsFilter } from 'interfaces';
import { difference } from 'lodash';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import events from 'subscribers/events';
@Service()
export default class AccountsService {
@@ -17,6 +22,9 @@ export default class AccountsService {
@Inject('logger')
logger: any;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
/**
* Retrieve account type or throws service error.
* @param {number} tenantId -
@@ -104,10 +112,10 @@ export default class AccountsService {
* @return {IAccount}
*/
private async getAccountOrThrowError(tenantId: number, accountId: number) {
const { Account } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[accounts] validating the account existance.', { tenantId, accountId });
const account = await Account.query().findById(accountId);
const account = await accountRepository.findById(accountId);
if (!account) {
this.logger.info('[accounts] the given account not found.', { accountId });
@@ -159,8 +167,8 @@ export default class AccountsService {
* @returns {IAccount}
*/
public async newAccount(tenantId: number, accountDTO: IAccountDTO) {
const { Account } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
// Validate account name uniquiness.
await this.validateAccountNameUniquiness(tenantId, accountDTO.name);
@@ -176,11 +184,15 @@ export default class AccountsService {
);
this.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
}
const account = await Account.query().insertAndFetch({
const account = await accountRepository.insert({
...accountDTO,
slug: kebabCase(accountDTO.name),
});
this.logger.info('[account] account created successfully.', { account, accountDTO });
// Triggers `onAccountCreated` event.
this.eventDispatcher.dispatch(events.accounts.onCreated);
return account;
}
@@ -191,7 +203,7 @@ export default class AccountsService {
* @param {IAccountDTO} accountDTO
*/
public async editAccount(tenantId: number, accountId: number, accountDTO: IAccountDTO) {
const { Account } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
const oldAccount = await this.getAccountOrThrowError(tenantId, accountId);
// Validate account name uniquiness.
@@ -214,12 +226,13 @@ export default class AccountsService {
this.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
}
// Update the account on the storage.
const account = await Account.query().patchAndFetchById(
oldAccount.id, { ...accountDTO }
);
const account = await accountRepository.edit(oldAccount.id, accountDTO);
this.logger.info('[account] account edited successfully.', {
account, accountDTO, tenantId
});
// Triggers `onAccountEdited` event.
this.eventDispatcher.dispatch(events.accounts.onEdited);
return account;
}
@@ -309,7 +322,7 @@ export default class AccountsService {
* @param {number} accountId
*/
public async deleteAccount(tenantId: number, accountId: number) {
const { Account } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
const account = await this.getAccountOrThrowError(tenantId, accountId);
this.throwErrorIfAccountPredefined(account);
@@ -317,10 +330,13 @@ export default class AccountsService {
await this.throwErrorIfAccountHasChildren(tenantId, accountId);
await this.throwErrorIfAccountHasTransactions(tenantId, accountId);
await Account.query().deleteById(account.id);
await accountRepository.deleteById(account.id);
this.logger.info('[account] account has been deleted successfully.', {
tenantId, accountId,
})
});
// Triggers `onAccountDeleted` event.
this.eventDispatcher.dispatch(events.accounts.onDeleted);
}
/**
@@ -400,6 +416,9 @@ export default class AccountsService {
this.logger.info('[account] given accounts deleted in bulk successfully.', {
tenantId, accountsIds
});
// Triggers `onBulkDeleted` event.
this.eventDispatcher.dispatch(events.accounts.onBulkDeleted);
}
/**
@@ -418,6 +437,9 @@ export default class AccountsService {
active: activate ? 1 : 0,
});
this.logger.info('[account] accounts have been activated successfully.', { tenantId, accountsIds });
// Triggers `onAccountBulkActivated` event.
this.eventDispatcher.dispatch(events.accounts.onActivated);
}
/**
@@ -436,6 +458,9 @@ export default class AccountsService {
active: activate ? 1 : 0,
})
this.logger.info('[account] account have been activated successfully.', { tenantId, accountId });
// Triggers `onAccountActivated` event.
this.eventDispatcher.dispatch(events.accounts.onActivated);
}
/**

View File

@@ -7,12 +7,13 @@ import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import { SystemUser, PasswordReset } from 'system/models';
import { PasswordReset } from 'system/models';
import {
IRegisterDTO,
ITenant,
ISystemUser,
IPasswordReset,
IAuthenticationService,
} from 'interfaces';
import { hashPassword } from 'utils';
import { ServiceError, ServiceErrors } from 'exceptions';

View File

@@ -45,10 +45,10 @@ export default class ContactsService {
* @param {IContactDTO} contactDTO
*/
async newContact(tenantId: number, contactDTO: IContactNewDTO, contactService: TContactService) {
const { Contact } = this.tenancy.models(tenantId);
const { contactRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[contacts] trying to insert contact to the storage.', { tenantId, contactDTO });
const contact = await Contact.query().insert({ contactService, ...contactDTO });
const contact = await contactRepository.insert({ contactService, ...contactDTO });
this.logger.info('[contacts] contact inserted successfully.', { tenantId, contact });
return contact;
@@ -77,11 +77,11 @@ export default class ContactsService {
* @return {Promise<void>}
*/
async deleteContact(tenantId: number, contactId: number, contactService: TContactService) {
const { Contact } = this.tenancy.models(tenantId);
const { contactRepository } = this.tenancy.repositories(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();
await contactRepository.deleteById(contactId);
}
/**
@@ -124,10 +124,10 @@ export default class ContactsService {
* @return {Promise<void>}
*/
async deleteBulkContacts(tenantId: number, contactsIds: number[], contactService: TContactService) {
const { Contact } = this.tenancy.models(tenantId);
const { contactRepository } = this.tenancy.repositories(tenantId);
this.getContactsOrThrowErrorNotFound(tenantId, contactsIds, contactService);
await Contact.query().whereIn('id', contactsIds).delete();
await contactRepository.bulkDelete(contactsIds);
}
/**

View File

@@ -6,10 +6,10 @@ import ContactsService from 'services/Contacts/ContactsService';
import {
ICustomerNewDTO,
ICustomerEditDTO,
ICustomer,
} from 'interfaces';
import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService';
import { ICustomer } from 'src/interfaces';
@Service()
export default class CustomersService {

View File

@@ -1,154 +0,0 @@
import Resource from 'models/Resource';
import ResourceField from 'models/ResourceField';
import ResourceFieldMetadata from 'models/ResourceFieldMetadata';
import ResourceFieldMetadataCollection from 'collection/ResourceFieldMetadataCollection';
export default class ResourceCustomFieldRepository {
/**
* Class constructor.
*/
constructor(model) {
if (typeof model === 'function') {
this.resourceName = model.name;
} else if (typeof model === 'string') {
this.resourceName = model;
}
// Custom fields of the given resource.
this.customFields = [];
this.filledCustomFields = {};
// metadata of custom fields of the given resource.
this.fieldsMetadata = {};
this.resource = {};
}
/**
* Fetches metadata of custom fields of the given resource.
* @param {Integer} id - Resource item id.
*/
async fetchCustomFieldsMetadata(id) {
if (typeof id === 'undefined') {
throw new Error('Please define the resource item id.');
}
if (!this.resource) {
throw new Error('Target resource model is not found.');
}
const metadata = await ResourceFieldMetadata.query()
.where('resource_id', this.resource.id)
.where('resource_item_id', id);
this.fieldsMetadata[id] = metadata;
}
/**
* Load resource.
*/
async loadResource() {
const resource = await Resource.query().where('name', this.resourceName).first();
if (!resource) {
throw new Error('There is no stored resource in the storage with the given model name.');
}
this.setResource(resource);
}
/**
* Load metadata of the resource.
*/
async loadResourceCustomFields() {
if (typeof this.resource.id === 'undefined') {
throw new Error('Please fetch resource details before fetch custom fields of the resource.');
}
const customFields = await ResourceField.query()
.where('resource_id', this.resource.id)
.modify('whereNotPredefined');
this.setResourceCustomFields(customFields);
}
/**
* Sets resource model.
* @param {Resource} resource -
*/
setResource(resource) {
this.resource = resource;
}
/**
* Sets resource custom fields collection.
* @param {Array} customFields -
*/
setResourceCustomFields(customFields) {
this.customFields = customFields;
}
/**
* Retrieve metadata of the resource custom fields.
* @param {Integer} itemId -
*/
getMetadata(itemId) {
return this.fieldsMetadata[itemId] || this.fieldsMetadata;
}
/**
* Fill metadata of the custom fields that associated to the resource.
* @param {Inter} id - Resource item id.
* @param {Array} attributes -
*/
fillCustomFields(id, attributes) {
if (typeof this.filledCustomFields[id] === 'undefined') {
this.filledCustomFields[id] = [];
}
attributes.forEach((attr) => {
this.filledCustomFields[id].push(attr);
if (!this.fieldsMetadata[id]) {
this.fieldsMetadata[id] = new ResourceFieldMetadataCollection();
}
this.fieldsMetadata[id].setMeta(attr.key, attr.value, {
resource_id: this.resource.id,
resource_item_id: id,
});
});
}
/**
* Saves the instered, updated and deleted custom fields metadata.
* @param {Integer} id - Optional resource item id.
*/
async saveCustomFields(id) {
if (id) {
if (typeof this.fieldsMetadata[id] === 'undefined') {
throw new Error('There is no resource item with the given id.');
}
await this.fieldsMetadata[id].saveMeta();
} else {
const opers = [];
this.fieldsMetadata.forEach((metadata) => {
const oper = metadata.saveMeta();
opers.push(oper);
});
await Promise.all(opers);
}
}
/**
* Validates the exist custom fields.
*/
validateExistCustomFields() {
}
toArray() {
return this.fieldsMetadata.toArray();
}
async load() {
await this.loadResource();
await this.loadResourceCustomFields();
}
static forgeMetadataCollection() {
}
}

View File

@@ -124,7 +124,7 @@ export default class DynamicListService implements IDynamicListService {
this.validateFilterRolesSchema(filter.filterRoles);
this.validateRolesFieldsExistance(model, filter.filterRoles);
// Validate the accounts resource fields.
// Validate the model resource fields.
const filterRoles = new DynamicFilterFilterRoles(filter.filterRoles);
dynamicFilter.setFilter(filterRoles);
}

View File

@@ -1,13 +1,17 @@
import { Service, Inject } from "typedi";
import { difference, sumBy, omit } from 'lodash';
import moment from "moment";
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import { ServiceError } from "exceptions";
import TenancyService from 'services/Tenancy/TenancyService';
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalEntry from 'services/Accounting/JournalEntry';
import JournalCommands from 'services/Accounting/JournalCommands';
import { IExpense, IAccount, IExpenseDTO, IExpenseCategory, IExpensesService, ISystemUser } from 'interfaces';
import { IExpense, IAccount, IExpenseDTO, IExpensesService, ISystemUser } from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import events from 'subscribers/events';
const ERRORS = {
EXPENSE_NOT_FOUND: 'expense_not_found',
@@ -30,6 +34,9 @@ export default class ExpensesService implements IExpensesService {
@Inject('logger')
logger: any;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
/**
* Retrieve the payment account details or returns not found server error in case the
* given account not found on the storage.
@@ -158,11 +165,10 @@ export default class ExpensesService implements IExpensesService {
* @param {IExpense} expense
* @param {IUser} authorizedUser
*/
private async writeJournalEntries(
public async writeJournalEntries(
tenantId: number,
expense: IExpense,
revertOld: boolean,
authorizedUser: ISystemUser
) {
this.logger.info('[expense[ trying to write expense journal entries.', { tenantId, expense });
const journal = new JournalPoster(tenantId);
@@ -171,29 +177,8 @@ export default class ExpensesService implements IExpensesService {
if (revertOld) {
await journalCommands.revertJournalEntries(expense.id, 'Expense');
}
const mixinEntry = {
referenceType: 'Expense',
referenceId: expense.id,
date: expense.paymentDate,
userId: authorizedUser.id,
draft: !expense.publish,
};
const paymentJournalEntry = new JournalEntry({
credit: expense.totalAmount,
account: expense.paymentAccountId,
...mixinEntry,
});
journal.credit(paymentJournalEntry);
expense.categories.forEach((category: IExpenseCategory) => {
const expenseJournalEntry = new JournalEntry({
account: category.expenseAccountId,
debit: category.amount,
note: category.description,
...mixinEntry,
});
journal.debit(expenseJournalEntry);
});
journalCommands.expense(expense);
return Promise.all([
journal.saveBalance(),
journal.saveEntries(),
@@ -229,7 +214,7 @@ export default class ExpensesService implements IExpensesService {
* @param {IExpense} expense
*/
private validateExpenseIsNotPublished(expense: IExpense) {
if (expense.published) {
if (expense.publishedAt) {
throw new ServiceError(ERRORS.EXPENSE_ACCOUNT_ALREADY_PUBLISED);
}
}
@@ -291,33 +276,29 @@ export default class ExpensesService implements IExpensesService {
const { expenseRepository } = this.tenancy.repositories(tenantId);
const expense = await this.getExpenseOrThrowError(tenantId, expenseId);
// 1. Validate payment account existance on the storage.
// - Validate payment account existance on the storage.
const paymentAccount = await this.getPaymentAccountOrThrowError(
tenantId,
expenseDTO.paymentAccountId,
);
// 2. Validate expense accounts exist on the storage.
// - Validate expense accounts exist on the storage.
const expensesAccounts = await this.getExpensesAccountsOrThrowError(
tenantId,
this.mapExpensesAccountsIdsFromDTO(expenseDTO),
);
// 3. Validate payment account type.
// - Validate payment account type.
await this.validatePaymentAccountType(tenantId, paymentAccount);
// 4. Validate expenses accounts type.
// - Validate expenses accounts type.
await this.validateExpensesAccountsType(tenantId, expensesAccounts);
// 5. Validate the given expense categories not equal zero.
// - Validate the given expense categories not equal zero.
this.validateCategoriesNotEqualZero(expenseDTO);
// 6. Update the expense on the storage.
// - Update the expense on the storage.
const expenseObj = this.expenseDTOToModel(expenseDTO);
const expenseModel = await expenseRepository.update(expenseId, expenseObj, null);
// 7. In case expense published, write journal entries.
if (expenseObj.published) {
await this.writeJournalEntries(tenantId, expenseModel, true, authorizedUser);
}
this.logger.info('[expense] the expense updated on the storage successfully.', { tenantId, expenseDTO });
return expenseModel;
}
@@ -364,13 +345,12 @@ export default class ExpensesService implements IExpensesService {
// 6. Save the expense to the storage.
const expenseObj = this.expenseDTOToModel(expenseDTO, authorizedUser);
const expenseModel = await expenseRepository.create(expenseObj);
// 7. In case expense published, write journal entries.
if (expenseObj.published) {
await this.writeJournalEntries(tenantId, expenseModel, false, authorizedUser);
}
this.logger.info('[expense] the expense stored to the storage successfully.', { tenantId, expenseDTO });
// Triggers `onExpenseCreated` event.
this.eventDispatcher.dispatch(events.expenses.onCreated, { tenantId, expenseId: expenseModel.id });
return expenseModel;
}
@@ -394,6 +374,9 @@ export default class ExpensesService implements IExpensesService {
await expenseRepository.publish(expenseId);
this.logger.info('[expense] the expense published successfully.', { tenantId, expenseId });
// Triggers `onExpensePublished` event.
this.eventDispatcher.dispatch(events.expenses.onPublished, { tenantId, expenseId });
}
/**
@@ -409,10 +392,10 @@ export default class ExpensesService implements IExpensesService {
this.logger.info('[expense] trying to delete the expense.', { tenantId, expenseId });
await expenseRepository.delete(expenseId);
if (expense.published) {
await this.revertJournalEntries(tenantId, expenseId);
}
this.logger.info('[expense] the expense deleted successfully.', { tenantId, expenseId });
// Triggers `onExpenseDeleted` event.
this.eventDispatcher.dispatch(events.expenses.onDeleted, { tenantId, expenseId });
}
/**
@@ -427,9 +410,11 @@ export default class ExpensesService implements IExpensesService {
this.logger.info('[expense] trying to delete the given expenses.', { tenantId, expensesIds });
await expenseRepository.bulkDelete(expensesIds);
await this.revertJournalEntries(tenantId, expensesIds);
this.logger.info('[expense] the given expenses deleted successfully.', { tenantId, expensesIds });
// Triggers `onExpenseBulkDeleted` event.
this.eventDispatcher.dispatch(events.expenses.onBulkDeleted, { tenantId, expensesIds });
}
/**
@@ -443,9 +428,12 @@ export default class ExpensesService implements IExpensesService {
const { expenseRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[expense] trying to publish the given expenses.', { tenantId, expensesIds });
await expenseRepository.publishBulk(expensesIds);
await expenseRepository.bulkPublish(expensesIds);
this.logger.info('[expense] the given expenses ids published successfully.', { tenantId, expensesIds });
// Triggers `onExpenseBulkDeleted` event.
this.eventDispatcher.dispatch(events.expenses.onBulkPublished, { tenantId, expensesIds });
}
/**

View File

@@ -37,6 +37,18 @@ export default {
tenantSeeded: 'onTenantSeeded',
},
/**
* Accounts service.
*/
accounts: {
onCreated: 'onAccountCreated',
onEdited: 'onAccountEdited',
onDeleted: 'onAccountDeleted',
onBulkDeleted: 'onBulkDeleted',
onBulkActivated: 'onAccountBulkActivated',
onActivated: 'onAccountActivated'
},
/**
* Manual journals service.
*/
@@ -47,5 +59,18 @@ export default {
onDeletedBulk: 'onManualJournalCreatedBulk',
onPublished: 'onManualJournalPublished',
onPublishedBulk: 'onManualJournalPublishedBulk',
},
/**
* Expenses service.
*/
expenses: {
onCreated: 'onExpenseCreated',
onEdited: 'onExpenseEdited',
onDeleted: 'onExpenseDelted',
onPublished: 'onExpensePublished',
onBulkDeleted: 'onExpenseBulkDeleted',
onBulkPublished: 'onBulkPublished',
}
}

View File

@@ -0,0 +1,64 @@
import { Container, Inject, Service } from 'typedi';
import { EventSubscriber, On } from 'event-dispatch';
import events from 'subscribers/events';
import ExpensesService from 'services/Expenses/ExpensesService';
import TenancyService from 'services/Tenancy/TenancyService';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
@Service()
export default class ExpensesSubscriber {
constructor(
@Inject()
tenancy: TenancyService,
@EventDispatcher()
eventDispatcher: EventDispatcherInterface,
) {
console.log(this, 'XXXX');
// this.eventDispatcher.on(events.expenses.onCreated, this.onExpenseCreated);
}
public onExpenseCreated({ expenseId, tenantId }) {
console.log(this)
// // 7. In case expense published, write journal entries.
// if (expenseObj.publishedAt) {
// await this.writeJournalEntries(tenantId, expenseModel, false);
// }
}
// @On(events.expenses.onEdited)
public onExpenseEdited({ expenseId, tenantId }) {
// - In case expense published, write journal entries.
// if (expenseObj.publishedAt) {
// await this.writeJournalEntries(tenantId, expenseModel, true, authorizedUser);
// }
}
// @On(events.expenses.onDeleted)
public onExpenseDeleted({ expenseId, tenantId }) {
// if (expense.published) {
// await this.revertJournalEntries(tenantId, expenseId);
// }
}
// @On(events.expenses.onPublished)
public onExpensePublished({ expenseId, tenantId }) {
}
// @On(events.expenses.onBulkDeleted)
public onExpenseBulkDeleted({ expensesIds, tenantId }) {
}
// @On(events.expenses.onBulkPublished)
public onExpenseBulkPublished({ expensesIds, tenantId }) {
}
}

View File

@@ -1,8 +1,8 @@
exports.up = (knex) => knex.schema.createTable('password_resets', (table) => {
table.increments();
table.string('email');
table.string('token');
table.string('email').index();
table.string('token').index();
table.timestamp('created_at');
});

View File

@@ -2,7 +2,7 @@
exports.up = function(knex) {
return knex.schema.createTable('tenants', (table) => {
table.bigIncrements();
table.string('organization_id');
table.string('organization_id').index();
table.dateTime('under_maintenance_since').nullable();
table.dateTime('initialized_at').nullable();

View File

@@ -4,18 +4,15 @@ exports.up = function (knex) {
table.increments();
table.string('first_name');
table.string('last_name');
table.string('email').unique();
table.string('phone_number').unique();
table.string('email').unique().index();
table.string('phone_number').unique().index();
table.string('password');
table.boolean('active');
table.boolean('active').index();
table.string('language');
table.integer('tenant_id').unsigned();
table.date('invite_accepted_at');
table.date('last_login_at');
table.dateTime('deleted_at');
table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants');
table.date('invite_accepted_at').index();
table.date('last_login_at').index();
table.dateTime('deleted_at').index();
table.timestamps();
});
};

View File

@@ -2,9 +2,9 @@
exports.up = function(knex) {
return knex.schema.createTable('user_invites', (table) => {
table.increments();
table.string('email');
table.string('token').unique();
table.integer('tenant_id').unsigned();
table.string('email').index();
table.string('token').unique().index();
table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants');
table.datetime('created_at');
});
};

View File

@@ -1,18 +0,0 @@
exports.up = function(knex) {
return knex.schema.createTable('subscriptions_usage', table => {
table.increments();
table.integer('user_id');
table.integer('plan_id');
table.dateTime('trial_ends_at');
table.dateTime('subscription_starts_at');
table.dateTime('subscription_ends_at');
table.timestamps();
});
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('subscriptions_usage');
};

View File

@@ -17,7 +17,6 @@ exports.up = function(knex) {
table.string('invoice_interval').nullable();
table.integer('index').unsigned();
table.timestamps();
}).then(() => {
return knex.seed.run({

View File

@@ -2,12 +2,10 @@
exports.up = function(knex) {
return knex.schema.createTable('subscription_plan_features', table => {
table.increments();
table.integer('plan_id').unsigned();
table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
table.string('slug');
table.string('name');
table.string('description');
table.timestamps();
});
};

View File

@@ -4,8 +4,8 @@ exports.up = function(knex) {
table.increments('id');
table.string('slug');
table.integer('plan_id').unsigned();
table.integer('tenant_id').unsigned();
table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants');
table.dateTime('trial_started_at').nullable();
table.dateTime('trial_ends_at').nullable();

View File

@@ -3,15 +3,15 @@ exports.up = function(knex) {
return knex.schema.createTable('subscription_licenses', table => {
table.increments();
table.string('license_code').unique();
table.integer('plan_id').unsigned();
table.string('license_code').unique().index();
table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
table.integer('license_period').unsigned();
table.string('period_interval');
table.dateTime('sent_at');
table.dateTime('disabled_at');
table.dateTime('used_at');
table.dateTime('sent_at').index();
table.dateTime('disabled_at').index();
table.dateTime('used_at').index();
table.timestamps();
})

View File

@@ -12,21 +12,22 @@ export default class SystemUserRepository extends SystemRepository {
/**
* Patches the last login date to the given system user.
* @param {number} userId
* @return {Promise<void>}
*/
async patchLastLoginAt(userId: number) {
const user = await SystemUser.query().patchAndFetchById(userId, {
async patchLastLoginAt(userId: number): Promise<void> {
await SystemUser.query().patchAndFetchById(userId, {
last_login_at: moment().toMySqlDateTime()
});
this.flushUserCache(user);
return user;
this.flushCache();
}
/**
* Finds system user by crediential.
* @param {string} crediential - Phone number or email.
* @return {ISystemUser}
* @return {Promise<ISystemUser>}
*/
findByCrediential(crediential: string) {
findByCrediential(crediential: string): Promise<ISystemUser> {
return SystemUser.query().whereNotDeleted()
.findOne('email', crediential)
.orWhere('phone_number', crediential);
@@ -34,9 +35,10 @@ export default class SystemUserRepository extends SystemRepository {
/**
* Retrieve system user details of the given id.
* @param {number} userId
* @param {number} userId - User id.
* @return {Promise<ISystemUser>}
*/
getById(userId: number) {
getById(userId: number): Promise<ISystemUser> {
return this.cache.get(`systemUser.id.${userId}`, () => {
return SystemUser.query().whereNotDeleted().findById(userId);
});
@@ -44,10 +46,11 @@ export default class SystemUserRepository extends SystemRepository {
/**
* Retrieve user by id and tenant id.
* @param {number} userId
* @param {number} tenantId
* @param {number} userId - User id.
* @param {number} tenantId - Tenant id.
* @return {Promise<ISystemUser>}
*/
getByIdAndTenant(userId: number, tenantId: number) {
getByIdAndTenant(userId: number, tenantId: number): Promise<ISystemUser> {
return this.cache.get(`systemUser.id.${userId}.tenant.${tenantId}`, () => {
return SystemUser.query().whereNotDeleted()
.findOne({ id: userId, tenant_id: tenantId });
@@ -56,9 +59,10 @@ export default class SystemUserRepository extends SystemRepository {
/**
* Retrieve system user details by the given email.
* @param {string} email
* @param {string} email - Email
* @return {Promise<ISystemUser>}
*/
getByEmail(email: string) {
getByEmail(email: string): Promise<ISystemUser> {
return this.cache.get(`systemUser.email.${email}`, () => {
return SystemUser.query().whereNotDeleted().findOne('email', email);
});
@@ -66,9 +70,10 @@ export default class SystemUserRepository extends SystemRepository {
/**
* Retrieve user by phone number.
* @param {string} phoneNumber
* @param {string} phoneNumber - Phone number
* @return {Promise<ISystemUser>}
*/
getByPhoneNumber(phoneNumber: string) {
getByPhoneNumber(phoneNumber: string): Promise<ISystemUser> {
return this.cache.get(`systemUser.phoneNumber.${phoneNumber}`, () => {
return SystemUser.query().whereNotDeleted().findOne('phoneNumber', phoneNumber);
});
@@ -76,62 +81,61 @@ export default class SystemUserRepository extends SystemRepository {
/**
* Edits details.
* @param {number} userId
* @param {number} user
* @param {number} userId - User id.
* @param {number} user - User input.
* @return {Promise<void>}
*/
edit(userId: number, userInput: ISystemUser) {
const user = SystemUser.query().patchAndFetchById(userId, { ...userInput });
this.flushUserCache(user);
return user;
async edit(userId: number, userInput: ISystemUser): Promise<void> {
await SystemUser.query().patchAndFetchById(userId, { ...userInput });
this.flushCache();
}
/**
* Creates a new user.
* @param {IUser} userInput
* @param {IUser} userInput - User input.
* @return {Promise<ISystemUser>}
*/
create(userInput: ISystemUser) {
return SystemUser.query().insert({ ...userInput });
async create(userInput: ISystemUser): Promise<ISystemUser> {
const systemUser = await SystemUser.query().insert({ ...userInput });
this.flushCache();
return systemUser;
}
/**
* Deletes user by the given id.
* @param {number} userId
* @param {number} userId - User id.
* @return {Promise<void>}
*/
async deleteById(userId: number) {
const user = await this.getById(userId);
async deleteById(userId: number): Promise<void> {
await SystemUser.query().where('id', userId).delete();
this.flushUserCache(user);
this.flushCache();
}
/**
* Activate user by the given id.
* @param {number} userId
* @param {number} userId - User id.
* @return {Promise<void>}
*/
async activateById(userId: number) {
const user = await SystemUser.query().patchAndFetchById(userId, { active: 1 });
this.flushUserCache(user);
return user;
async activateById(userId: number): Promise<void> {
await SystemUser.query().patchAndFetchById(userId, { active: 1 });
this.flushCache();
}
/**
* Inactivate user by the given id.
* @param {number} userId
* @param {number} userId - User id.
* @return {Promise<void>}
*/
async inactivateById(userId: number) {
const user = await SystemUser.query().patchAndFetchById(userId, { active: 0 });
this.flushUserCache(user);
return user;
async inactivateById(userId: number): Promise<void> {
await SystemUser.query().patchAndFetchById(userId, { active: 0 });
this.flushCache();
}
/**
* Flush user cache.
* @param {IUser} user
* Flushes user repository cache.
*/
flushUserCache(user: ISystemUser) {
this.cache.del(`systemUser.phoneNumber.${user.phoneNumber}`);
this.cache.del(`systemUser.email.${user.email}`);
this.cache.del(`systemUser.id.${user.id}`);
this.cache.del(`systemUser.id.${user.id}.tenant.${user.tenantId}`);
flushCache() {
this.cache.delStartWith('systemUser');
}
}