Skip to content

Commit

Permalink
Reports menu. Reports - business - year taxes. Resolves #32. Resolves #…
Browse files Browse the repository at this point in the history
  • Loading branch information
vsyerik committed Mar 19, 2022
1 parent 0cb15dc commit e30bc6f
Show file tree
Hide file tree
Showing 17 changed files with 404 additions and 6 deletions.
16 changes: 16 additions & 0 deletions server/api/__tests__/reportController.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict'

import { controller } from '../reportController.js'
import { initConfig } from '../../config/index.js'

/** calculateYearTaxes */
describe('calculateYearTaxes', () => {
beforeAll(async () => {
await initConfig()
})
it('returns year taxes', async () => {
const actual = await controller.calculateYearTaxes('ckkfm160400003e5iimcnpt4s', 'ckkfmwjgo00033e5i0z3hktbj', '2021')
console.log(actual)
expect(actual).toBeDefined()
})
})
4 changes: 2 additions & 2 deletions server/api/categoryController.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ const defaultCategories = [
{ name: 'Charity', isPersonal: true, type: c.transactionType.EXPENSE },
{ name: 'Home Insurance', isPersonal: true, type: c.transactionType.EXPENSE },
{ name: 'Bank Fee', isPersonal: true, type: c.transactionType.EXPENSE },
{ name: 'Loan', isPersonal: true, type: c.transactionType.EXPENSE },
{ name: 'Loan', isPersonal: true, type: c.transactionType.TRANSFER },
{ name: 'State Tax', isPersonal: true, type: c.transactionType.EXPENSE },
{ name: 'Check', isPersonal: true, type: c.transactionType.EXPENSE },
{ name: 'Advertising', isPersonal: false, type: c.transactionType.EXPENSE },
Expand All @@ -154,7 +154,7 @@ const defaultCategories = [
{ name: 'Other Misc Expenses', isPersonal: false, type: c.transactionType.EXPENSE },
{ name: 'Business Income', isPersonal: false, type: c.transactionType.INCOME },
{ name: 'Moving', isPersonal: false, type: c.transactionType.EXPENSE },
{ name: 'Loan', isPersonal: false, type: c.transactionType.INCOME },
{ name: 'Loan', isPersonal: false, type: c.transactionType.TRANSFER },
{ name: 'Federal Tax - Estimates', isPersonal: false, type: c.transactionType.EXPENSE },
{ name: 'Federal Tax', isPersonal: false, type: c.transactionType.EXPENSE },
{ name: 'Health Insurance', isPersonal: false, type: c.transactionType.EXPENSE },
Expand Down
2 changes: 1 addition & 1 deletion server/api/dashboardController.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const controller = {
* Gets tenant transactions by account and date range.
* @property {AppRequest} req
* @property {Object} req.query
* @property {string} req.query.perio
* @property {string} req.query.period month
* @property {string} req.query.year
* @property {string} req.query.businessId
* @property {boolean} req.query.reconciledOnly
Expand Down
2 changes: 2 additions & 0 deletions server/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import transactionController from './transactionController.js'
import tripController from './tripController.js'
import userController from './userController.js'
import budgetController from './budgetController.js'
import reportController from './reportController.js'

/** @type {import('express').Router} */
const router = express.Router()
Expand All @@ -39,6 +40,7 @@ router.use('/duplicateTransactions', duplicateTransactionController)
router.use('/imports', importController)
router.use('/importRules', importRuleController)
router.use('/importRuleTransactions', importRuleTransactionController)
router.use('/reports', reportController)
router.use('/transactions', transactionController)
router.use('/trips', tripController)
router.use('/users', userController)
Expand Down
92 changes: 92 additions & 0 deletions server/api/reportController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
'use strict'

import express from 'express'
import asyncHandler from 'express-async-handler'
import { DashboardDb, CategoryDb, BusinessDb } from '../db/index.js'
import { c } from '@psbfinances/shared/core/index.js'
import getLogger from '../core/logger.js'
import { controller as tripController } from './tripController.js'
import utils from './utils.js'
import validator from 'validator'

const logger = getLogger(import.meta.url)

const controller = {
/** @link module:psbf/api/reports */
/**
* Gets tenant transactions by account and date range.
* @property {AppRequest} req
* @property {YearTaxRequest} req.query
* @property {import(express).Response} res
*/
getYearTaxes: async (req, res) => {
const { tenantId, userId } = utils.getBasicRequestData(req)
const criteria = req.query
logger.info('list', { tenantId, userId, ...criteria })
const result = await controller.calculateYearTaxes(tenantId, criteria.businessId, criteria.year)
res.json(result)
},

/**
* Maps PL items to categories.
* @param {psbf.Category[]} categories
* @param {string} categoryType
* @param {PLItem[]} pl
* @return {*}
*/
mapAmountsToCategories: (categories, categoryType, pl) => {
const typeCategories = categories.filter(x => x.type === categoryType && !x.isPersonal).
sort((x, y) => x.name >= y.name ? 1 : 0)
return typeCategories.map(x => {
const plItem = pl.find(y => y.categoryId === x.id)
return {
categoryId: x.id,
categoryName: x.name,
amount: Math.abs(plItem ? plItem.amount / 100 : 0)
}
})

},

/**
* Calculates business year taxes (Schedule C).
* @param {string} tenantId
* @param {string} businessId
* @param {string} year
* @return {Promise<YearTaxResponse>}
*/
calculateYearTaxes: async (tenantId, businessId, year) => {
logger.info('calculateYearTaxes', { tenantId, businessId, year })
const isEmpty = validator.isEmpty
if (isEmpty(businessId) || isEmpty(tenantId) || isEmpty(year)) throw new Error('Invalid report criteria')
const businessDb = new BusinessDb()
const business = await businessDb.get(businessId, tenantId)
if (business.length === 0) throw new Error('Invalid business')

const dashboardDb = new DashboardDb()
const categoriesDb = new CategoryDb()
const pl = await dashboardDb.listBusinessPLCurrentYear(tenantId, businessId, year)

const categories = await categoriesDb.list(tenantId)
const businessCategories = categories.filter(x => !x.isPersonal).sort((x, y) => x.name >= y.name ? 1 : 0)
const income = controller.mapAmountsToCategories(businessCategories, c.transactionType.INCOME, pl)
const expenses = controller.mapAmountsToCategories(businessCategories, c.transactionType.EXPENSE, pl)

const totalIncome = income.reduce((t, x) => t += x.amount, 0)
const totalExpenses = expenses.reduce((t, x) => t += x.amount, 0)
const profit = totalIncome - totalExpenses

const mileage = await tripController.calcTotalByBusinessAndDates(tenantId, businessId, year)

return { year, businessId, income, expenses, totalIncome, totalExpenses, profit, mileage }
}
}

/** @type {import('express').Router} */
const router = express.Router()
router.route('/year-tax').get(asyncHandler(controller.getYearTaxes))
export default router

export {
controller
}
15 changes: 15 additions & 0 deletions server/api/tripController.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,21 @@ const controller = {
await dataChangeLogic.insert(tripDb.tableName, id, ops.DELETE, {transactionId})

res.json({})
},

/**
* Returns year total mileage for a bussiness.
* @param {string} tenantId
* @param {string} businessId
* @param {string} year
* @return {Promise<number>}
*/
calcTotalByBusinessAndDates: async (tenantId, businessId, year) => {
const tripDb = new TripDb()
const dateFrom = `${year}-01-01`
const dateTo = `${Number.parseInt(year) + 1}-01-01`
const tripTotal = await tripDb.calcTotalByBusinessAndDates(tenantId, businessId, dateFrom, dateTo)
return tripTotal[0].totalDistance
}
}

Expand Down
18 changes: 17 additions & 1 deletion server/db/dashboardDb.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,15 @@ export default class DashboardDb extends Db {
[tenantId, businessId, year, period])
}

async listBusinessPLCurrentYear (tenantId, businessId, year, reconciledOnly) {
/**
* Returns PL by categories.
* @param {string} tenantId
* @param {string} businessId
* @param {string} year
* @param {boolean} [reconciledOnly = true]
* @return {Promise<PLItem[]>}
*/
async listBusinessPLCurrentYear (tenantId, businessId, year, reconciledOnly = true) {
return this.raw(`SELECT c.type categoryType, categoryId, name, SUM(amount) amount
FROM transactions
INNER JOIN accounts a on transactions.accountId = a.id
Expand All @@ -120,3 +128,11 @@ export default class DashboardDb extends Db {
[tenantId, businessId, year])
}
}

/**
* @typedef {Object} PLItem
* @property {string} categoryId
* @property {string} categoryType
* @property {string} name
* @property {Number} amount
*/
20 changes: 18 additions & 2 deletions server/db/tripDb.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,24 @@ export default class TripDb extends Db {
super('trips')
}

async insert(data) {
const dbData = {...data}
/**
* Returns total business mileage for a date range.
* @param {string} tenantId
* @param {string} businessId
* @param {string} dateFrom
* @param {string} dateTo
* @return {Promise<{totalDistance: number}[]>}
*/
async calcTotalByBusinessAndDates (tenantId, businessId, dateFrom, dateTo) {
return this.raw(`SELECT SUM(distance) AS totalDistance
FROM trips
INNER JOIN transactions ON trips.transactionId = transactions.id
WHERE trips.tenantId = ? AND businessId = ? AND startDate >= ? AND endDate < ?;`,
[tenantId, businessId, dateFrom, dateTo])
}

async insert (data) {
const dbData = { ...data }
if (data.meta) dbData.meta = JSON.stringify(data.meta)
return this.knex.insert(dbData)
}
Expand Down
1 change: 1 addition & 0 deletions shared/apiClient/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const endpoint = {
DUPLICATE_TRANSACTIONS: 'duplicateTransactions',
IMPORTS: 'imports',
IMPORT_RULES: 'importRules',
REPORTS: 'reports',
TENANTS: 'tenants',
TRANSACTIONS: 'transactions',
TRIPS: 'trips',
Expand Down
2 changes: 2 additions & 0 deletions shared/apiClient/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import AuthApi from './authApi.js'
import DashboardApi from './dashboardApi.js'
import DuplicateTransactionApi from './duplicateTransactionApi.js'
import ImportApi from './importApi.js'
import ReportApi from './reportApi.js'
import TransactionApi from './transactionApi.js'

export const accountApi = new Api(endpoint.ACCOUNTS)
Expand All @@ -21,6 +22,7 @@ export const dashboardApi = new DashboardApi(endpoint.DASHBOARD)
export const duplicateTransactionApi = new DuplicateTransactionApi(endpoint.DUPLICATE_TRANSACTIONS)
export const importApi = new ImportApi(endpoint.IMPORTS)
export const importRuleApi = new Api(endpoint.IMPORT_RULES)
export const reportApi = new ReportApi(endpoint.REPORTS)
export const tenantApi = new TransactionApi(endpoint.TENANTS)
export const transactionApi = new TransactionApi(endpoint.TRANSACTIONS)
export const tripApi = new Api(endpoint.TRIPS)
Expand Down
20 changes: 20 additions & 0 deletions shared/apiClient/reportApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use strict'

import { Api } from './api.js'
import queryString from 'query-string'

export default class ReportApi extends Api {
constructor (endpoint) {
super(endpoint)
}

/**
* @param {string} year
* @param {string} businessId
* @return {AxiosResponse<YearTaxResponse>}
*/
async getYearTaxes (year, businessId) {
const query = queryString.stringify({year, businessId})
return this.api.get(`/reports/year-tax?${query}`)
}
}
32 changes: 32 additions & 0 deletions typeDef/server.api.td.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
'use strict'

/**
* @typedef {Object} InvalidRequestData
* @property {boolean} result
* @property {string} error
*/

/**
@typedef {Object} TransactionRequest
Expand Down Expand Up @@ -71,3 +76,30 @@
* @property {psbf.Attachment[]} attachments
* @property {string} deletedId
*/

/** @module psbf/api/reports **/
/**
* @typedef {Object} YearTaxRequest
* @property {string} businessId
* @property {string} year
*/

/**
* @typedef {Object} YearTaxExpenseItem
* @property {string} categoryId
* @property {string} categoryName
* @property {number} amount
*/

/**
* @typedef {Object} YearTaxResponse
* @property {string} year
* @property {string} businessId
* @property {YearTaxExpenseItem[]} income
* @property {YearTaxExpenseItem[]} expenses
* @property {number} totalIncome
* @property {number} totalExpenses
* @property {number} profit
* @property {number} mileage
*/

5 changes: 5 additions & 0 deletions web/components/appStyle.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ body {
overflow: hidden;
}

h4 {
color: #B88766;
margin-bottom: 10px;
}

table, .form-control, .col-form-label-sm {
}

Expand Down
5 changes: 5 additions & 0 deletions web/components/layout/business/BusinessLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import appController from '../../../../shared/core/appController.js'
import TenantList from '../../tenants/TenantList.jsx'
import TransactionList from '../../transactionList/TransactionList.jsx'
import Dashboard from '../../dashboard/Dashboard.jsx'
import ReportList from '../../reports/ReportList.jsx'
import Budget from '../../budget/Budget.jsx'
import { observer } from 'mobx-react'
import {
Expand All @@ -29,6 +30,7 @@ import classNames from 'classnames'
import CarList from '../../settings/cars/CarList.jsx'
import DuplicateList from '../../settings/duplicateTransactions/DuplicateList.jsx'
import ApplicationSettingsForm from '../../settings/application/ApplicationSettings.jsx'
import BusinessYearTaxes from '../../reports/BusinessYearTaxes.jsx'

const MustBeAuthenticated = ({ children }) => {
const navigate = useNavigate()
Expand Down Expand Up @@ -59,6 +61,8 @@ const BusinessLayout = observer(
<Route path='transactions' element={<TransactionList />} />
<Route path='dashboard' element={<Dashboard />} />
<Route path='budget' element={<Budget />} />
<Route path='reports' element={<ReportList />} />
<Route path='reports/year-taxes' element={<BusinessYearTaxes />} />
<Route exact path='settings' element={<Settings />} />
<Route path='settings/applicationSettings' element={<ApplicationSettingsForm />} />
<Route path='settings/accounts' element={<AccountList />} />
Expand Down Expand Up @@ -132,6 +136,7 @@ const NavItems = () => {
<HeaderNavItem label='Transactions' icon='fa-table' url='/app/transactions' />
<HeaderNavItem label='Dashboard' icon='fa-tachometer-alt' url='/app/dashboard' />
<HeaderNavItem label='Budget' icon='fa-book' url='/app/budget' />
<HeaderNavItem label='Reports' icon='fa-file' url='/app/reports' />
<HeaderNavItem label='Settings' icon='fa-cog' url='/app/settings' />
</ul>
}
Expand Down
Loading

0 comments on commit e30bc6f

Please sign in to comment.