Skip to content
This repository has been archived by the owner on May 11, 2021. It is now read-only.

Commit

Permalink
feat(telegram): add income and outcome commands in bot
Browse files Browse the repository at this point in the history
  • Loading branch information
igorkamyshev committed Feb 28, 2019
1 parent 0258751 commit 2cb8966
Show file tree
Hide file tree
Showing 28 changed files with 215 additions and 9 deletions.
1 change: 1 addition & 0 deletions back/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@solid-soda/evolutions": "^0.0.6",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"handlebars": "^4.1.0",
"morgan": "^1.9.1",
"nanoid": "^2.0.1",
"nest-telegram": "^0.1.1",
Expand Down
4 changes: 4 additions & 0 deletions back/src/money/money.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { ExchangeRateApi } from './insfrastructure/ExchangeRateApi'
import { HistoryController } from './presentation/http/controller/HistoryController'
import { StatisticsController } from './presentation/http/controller/StatisticsController'
import { TransactionController } from './presentation/http/controller/TransactionController'
import { TransactionActions } from './presentation/telegram/actions/TransactionActions'
import { UnexpectedParameterCatcher } from './presentation/telegram/catcher/UnexpectedParameterCatcher'

@Module({
imports: [
Expand All @@ -35,6 +37,8 @@ import { TransactionController } from './presentation/http/controller/Transactio
CurrencyConverter,
ExchangeRateApi,
ExchangeRateRepository,
TransactionActions,
UnexpectedParameterCatcher,
],
})
export class MoneyModule implements NestModule {
Expand Down
67 changes: 67 additions & 0 deletions back/src/money/presentation/telegram/actions/TransactionActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Injectable } from '@nestjs/common'
import { Context, PipeContext, TelegramActionHandler } from 'nest-telegram'

import { CurrentSender } from '@back/user/presentation/telegram/transformer/CurrentSender'
import { TokenPayload } from '@back/user/application/dto/TokenPayload'
import { Accountant } from '@back/money/application/Accountant'
import { Templating } from '@back/utils/infrastructure/Templating/Templating'
import { parseCurrency } from '../helpers/parseCurrency'
import { parseAmount } from '../helpers/parseAmount'

@Injectable()
export class TransactionActions {
public constructor(
private readonly accountant: Accountant,
private readonly templating: Templating,
) {}

@TelegramActionHandler({ command: '/income' })
public async income(
ctx: Context,
@PipeContext(CurrentSender) { login }: TokenPayload,
) {
const [_, rawAmount, rawCurrency, ...source] = ctx.message.text.split(' ')

// TODO: Add message parser to nest-telegram lib as decorator
const incomeFields = {
amount: parseAmount(rawAmount),
currency: parseCurrency(rawCurrency),
date: new Date(),
source: source.join(' '),
}

await this.accountant.income(login, incomeFields)

const responseText = await this.templating.render(
'telegram/income-created',
incomeFields,
)

await ctx.reply(responseText)
}

@TelegramActionHandler({ command: '/outcome' })
public async outcome(
ctx: Context,
@PipeContext(CurrentSender) { login }: TokenPayload,
) {
const [_, rawAmount, rawCurrency, ...category] = ctx.message.text.split(' ')

// TODO: Add message parser to nest-telegram lib as decorator
const outcomeFields = {
amount: parseAmount(rawAmount),
currency: parseCurrency(rawCurrency),
date: new Date(),
category: category.join(' '),
}

await this.accountant.outcome(login, outcomeFields)

const responseText = await this.templating.render(
'telegram/outcome-created',
outcomeFields,
)

await ctx.reply(responseText)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { TelegramErrorHandler, TelegramCatch, Context } from 'nest-telegram'

import { UnexpectedParameterException } from '@back/utils/infrastructure/exception/UnexpectedParameterException'

@TelegramCatch(UnexpectedParameterException)
export class UnexpectedParameterCatcher
implements TelegramErrorHandler<UnexpectedParameterException> {
public async catch(ctx: Context, exception: UnexpectedParameterException) {
await ctx.reply(exception.message)
}
}
2 changes: 2 additions & 0 deletions back/src/money/presentation/telegram/helpers/parseAmount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const parseAmount = (rawAmount: string): number =>
parseFloat(rawAmount.replace(/,/g, '.')) * 100
17 changes: 17 additions & 0 deletions back/src/money/presentation/telegram/helpers/parseCurrency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Currency } from '@shared/enum/Currency'
import { UnexpectedParameterException } from '@back/utils/infrastructure/exception/UnexpectedParameterException'

export const parseCurrency = (rawCurrency: string): Currency => {
const transformedCurrency = rawCurrency.toUpperCase()

if (!Object.values(Currency).includes(transformedCurrency)) {
throw new UnexpectedParameterException(
'currency',
`"${rawCurrency}" is invalid currency, please use one of follow insted: ${Object.values(
Currency,
).join(', ')}`,
)
}

return transformedCurrency as Currency
}
12 changes: 12 additions & 0 deletions back/src/user/domain/UserRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ class UserRepo {
return Option.of(user)
}

public async getOneByTeleram(telegramId: number): Promise<User> {
const user = await this.findOneByTelegram(telegramId)

if (user.nonEmpty()) {
return user.get()
}

throw new EntityNotFoundException(User.name, {
telegramId,
})
}

public async findOneByTelegram(telegramId: number): Promise<Option<User>> {
const user = await this.userRepo
.createQueryBuilder()
Expand Down
18 changes: 18 additions & 0 deletions back/src/user/presentation/telegram/transformer/CurrentSender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Injectable } from '@nestjs/common'
import { ContextTransformer, Context } from 'nest-telegram'

import { UserRepository } from '@back/user/domain/UserRepository'
import { TokenPayload } from '@back/user/application/dto/TokenPayload'

@Injectable()
export class CurrentSender implements ContextTransformer<TokenPayload> {
public constructor(private readonly userRepo: UserRepository) {}

public async transform(ctx: Context) {
const user = await this.userRepo.getOneByTeleram(ctx.from.id)

return {
login: user.login,
}
}
}
4 changes: 3 additions & 1 deletion back/src/user/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { JwtGuard } from './presentation/http/security/JwtGuard'
import { AuthActions } from './presentation/telegram/actions/AuthActions'
import { InvalidCredentialsCatcher } from './presentation/telegram/catcher/InvalidCredentialsCatcher'
import { IsKnownUser } from './presentation/telegram/transformer/IsKnownUser'
import { CurrentSender } from './presentation/telegram/transformer/CurrentSender'

import { User } from './domain/User.entity'
import { UserRepository } from './domain/UserRepository'
Expand Down Expand Up @@ -50,8 +51,9 @@ import { PasswordEncoder } from './infrastructure/PasswordEncoder/PasswordEncode
AuthActions,
InvalidCredentialsCatcher,
IsKnownUser,
CurrentSender,
],
exports: [UserRepository, JwtGuard, Authenticator],
exports: [UserRepository, JwtGuard, Authenticator, CurrentSender],
})
export class UserModule implements NestModule {
public configure(consumer: MiddlewareConsumer) {
Expand Down
45 changes: 45 additions & 0 deletions back/src/utils/infrastructure/Templating/HandlebarsTemplating.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common'
import * as Handlebars from 'handlebars'
import { readFile } from 'fs'
import { promisify } from 'util'
import { resolve } from 'path'

import { Templating } from './Templating'
import { money } from './handlebarsHelpers/money'

type PrecompiledTempltes = {
[name: string]: Handlebars.TemplateDelegate
}

@Injectable()
export class HandlebarsTemplating implements Templating {
private readonly precompiledTemplates = {}

public constructor() {
Handlebars.registerHelper('money', money)
}

public async render(templateName: string, context: object) {
const compiled = await this.compile(templateName)

return compiled(context)
}

private async compile(
templateName: string,
): Promise<Handlebars.TemplateDelegate> {
if (this.precompiledTemplates[templateName]) {
return this.precompiledTemplates[templateName]
}

const templateContent = (await promisify(readFile)(
resolve(__dirname, '../../../../templates', `${templateName}.twig`),
)).toString()

const compiled = Handlebars.compile(templateContent)

this.precompiledTemplates[templateName] = compiled

return compiled
}
}
3 changes: 3 additions & 0 deletions back/src/utils/infrastructure/Templating/Templating.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export abstract class Templating {
public abstract render(templateName: string, context: object): Promise<string>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as Handlebars from 'handlebars'

import { Currency } from '@shared/enum/Currency'
import { displayMoney } from '@shared/helpers/displayMoney'

export const money = (amount: number, currency: Currency) =>
new Handlebars.SafeString(displayMoney(currency)(amount))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class UnexpectedParameterException extends Error {
public constructor(public readonly parameter: string, message: string) {
super(message)
}
}
8 changes: 7 additions & 1 deletion back/src/utils/utils.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'
import { IdGenerator } from './infrastructure/IdGenerator/IdGenerator'
import { NanoIdGenerator } from './infrastructure/IdGenerator/NanoIdGenerator'
import { ParseDateRangePipe } from './presentation/http/pipes/dateRange/ParseDateRangePipe'
import { Templating } from './infrastructure/Templating/Templating'
import { HandlebarsTemplating } from './infrastructure/Templating/HandlebarsTemplating'

@Module({
providers: [
Expand All @@ -11,8 +13,12 @@ import { ParseDateRangePipe } from './presentation/http/pipes/dateRange/ParseDat
provide: IdGenerator,
useClass: NanoIdGenerator,
},
{
provide: Templating,
useClass: HandlebarsTemplating,
},
],
exports: [ParseDateRangePipe, IdGenerator],
exports: [ParseDateRangePipe, IdGenerator, Templating],
})
export class UtilsModule implements NestModule {
public configure(consumer: MiddlewareConsumer) {
Expand Down
3 changes: 3 additions & 0 deletions back/templates/telegram/income-created.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Income transaction created.

{{money amount currency}} ({{source}})
3 changes: 3 additions & 0 deletions back/templates/telegram/outcome-created.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Outcome transaction created.

{{money amount currency}} ({{category}})
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
Input,
InputMoney,
} from '@front/features/final-form'
import { getCurrencyName } from '@front/helpers/getCurrencyName'
import { getCurrencyName } from '@shared/helpers/getCurrencyName'
import { Label } from '@front/ui/components/form/label'
import { LoadingButton } from '@front/ui/components/form/loading-button'
import { Card } from '@front/ui/components/layout/card'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
Input,
InputMoney,
} from '@front/features/final-form'
import { getCurrencyName } from '@front/helpers/getCurrencyName'
import { getCurrencyName } from '@shared/helpers/getCurrencyName'
import { Label } from '@front/ui/components/form/label'
import { LoadingButton } from '@front/ui/components/form/loading-button'
import { Card } from '@front/ui/components/layout/card'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { displayMoney } from '@front/helpers/displayMoney'
import { displayMoney } from '@shared/helpers/displayMoney'
import { displayNullableDate } from '@front/helpers/displayNullableDtae'
import { Table } from '@front/ui/components/layout/table'
import { IncomeModel } from '@shared/models/money/IncomeModel'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { displayMoney } from '@front/helpers/displayMoney'
import { displayMoney } from '@shared/helpers/displayMoney'
import { displayNullableDate } from '@front/helpers/displayNullableDtae'
import { Table } from '@front/ui/components/layout/table'
import { OutcomeModel } from '@shared/models/money/OutcomeModel'
Expand Down
2 changes: 1 addition & 1 deletion front/src/features/app/features/stats/Stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getFirstTransactionDate } from '@front/domain/money/selectors/getFirstT
import { getStats } from '@front/domain/money/selectors/getStats'
import { getStatsFetchingStatus } from '@front/domain/money/selectors/getStatsFetchingStatus'
import { useThunk } from '@front/domain/store'
import { displayMoney } from '@front/helpers/displayMoney'
import { displayMoney } from '@shared/helpers/displayMoney'
import { BarChart } from '@front/ui/components/chart/bar-chart'
import { Period } from '@front/ui/components/form/period'
import { Loader } from '@front/ui/components/layout/loader'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback } from 'react'

import { getCurrencyName } from '@front/helpers/getCurrencyName'
import { getCurrencyName } from '@shared/helpers/getCurrencyName'
import { EnumSelect } from '@front/ui/components/form/select'
import { Currency } from '@shared/enum/Currency'

Expand Down
2 changes: 1 addition & 1 deletion front/src/ui/components/form/input-money/InputMoney.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getCurrencySign } from '@front/helpers/getCurrencySign'
import { getCurrencySign } from '@shared/helpers/getCurrencySign'
import { useCustomInput } from '@front/ui/hooks/useCustomInput'

import { Input } from '../input'
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 comments on commit 2cb8966

Please sign in to comment.