Commit e4907dcd authored by seykron's avatar seykron

Closes #27: improve UI with buttons

parent 9a07edf4
......@@ -7,13 +7,28 @@
<packaging>jar</packaging>
<version>1.4.2-SNAPSHOT</version>
<!-- SCM -->
<scm>
<connection>scm:git:git@git.rlab.be:seykron/domino.git</connection>
<developerConnection>scm:git:git@git.rlab.be:seykron/domino.git</developerConnection>
<url>https://git.rlab.be/seykron/domino</url>
<tag>HEAD</tag>
</scm>
<licenses>
<license>
<name>GPL Version 2.0</name>
<url>https://www.gnu.org/licenses/old-licenses/gpl-2.0.html</url>
<distribution>repo</distribution>
<comments>Free Software License</comments>
</license>
</licenses>
<developers>
<developer>
<id>seykron</id>
<name>seykron</name>
<email>seykron@rlab.be</email>
<organization>R'lyeh Hacklab</organization>
<organizationUrl>https://rlab.be</organizationUrl>
<roles>
<role>developer</role>
</roles>
<timezone>America/Argentina/Buenos_Aires</timezone>
</developer>
</developers>
<properties>
<!-- Maven -->
......@@ -34,16 +49,16 @@
<joda.version>2.10</joda.version>
<dockerfile-maven-version>1.4.10</dockerfile-maven-version>
<typesafe.version>1.3.3</typesafe.version>
<springframework.all.version>5.1.5.RELEASE</springframework.all.version>
<springframework.all.version>5.1.9.RELEASE</springframework.all.version>
<netty.version>0.8.9.RELEASE</netty.version>
<tehanu.version>2.0.3</tehanu.version>
<tehanu.version>2.2.0</tehanu.version>
<h2.version>1.4.197</h2.version>
<mariadb.version>2.4.2</mariadb.version>
<exposed.version>0.14.2</exposed.version>
<hikaricp.version>3.1.0</hikaricp.version>
<freemarker.version>2.3.28</freemarker.version>
<kotlin.version>1.3.40</kotlin.version>
<kotlin.version>1.3.50</kotlin.version>
<kotlin-coroutines.version>1.1.1</kotlin-coroutines.version>
<kotlin.code.style>official</kotlin.code.style>
......@@ -250,7 +265,12 @@
<!-- Tehanu -->
<dependency>
<groupId>be.rlab</groupId>
<artifactId>tehanu</artifactId>
<artifactId>tehanu-core</artifactId>
<version>${tehanu.version}</version>
</dependency>
<dependency>
<groupId>be.rlab</groupId>
<artifactId>tehanu-spring</artifactId>
<version>${tehanu.version}</version>
</dependency>
......@@ -320,8 +340,6 @@
<excludes>
<!-- exclude any logback configuration from the jar -->
<exclude>**/logback.xml</exclude>
<!-- this line excludes env folder entirely from the jar -->
<exclude>**/conf/env/**</exclude>
</excludes>
</configuration>
</plugin>
......@@ -349,6 +367,19 @@
<jvmTarget>1.8</jvmTarget>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
......@@ -419,6 +450,14 @@
</repository>
</repositories>
<!-- SCM -->
<scm>
<connection>scm:git:git@git.rlab.be:seykron/domino.git</connection>
<developerConnection>scm:git:git@git.rlab.be:seykron/domino.git</developerConnection>
<url>https://git.rlab.be/seykron/domino</url>
<tag>HEAD</tag>
</scm>
<distributionManagement>
<repository>
<id>internal</id>
......
......@@ -3,44 +3,37 @@ package be.rlab.domino
import be.rlab.domino.config.CommandBeans
import be.rlab.domino.config.DomainBeans
import be.rlab.domino.config.WebConfig
import be.rlab.tehanu.config.BotBeans
import be.rlab.tehanu.config.DataSourceBeans
import be.rlab.tehanu.config.MemoryBeans
import be.rlab.tehanu.domain.Tehanu
import be.rlab.tehanu.SpringApplication
import be.rlab.tehanu.util.persistence.DataSourceInitializer
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.bridge.SLF4JBridgeHandler
import org.springframework.beans.factory.getBean
import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.http.server.reactive.HttpHandler
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter
import org.springframework.web.server.adapter.WebHttpHandlerBuilder
import reactor.netty.http.server.HttpServer
class Application(port: Int = 8080) {
class Application(private val port: Int = 8080) : SpringApplication() {
private val logger: Logger = LoggerFactory.getLogger(Application::class.java)
private val httpHandler: HttpHandler
override fun initialize() {
logger.info("Initializing application context")
init {
val context = AnnotationConfigApplicationContext {
DataSourceBeans.beans().initialize(this)
BotBeans.beans().initialize(this)
applicationContext.apply {
DomainBeans.beans().initialize(this)
MemoryBeans.beans().initialize(this)
CommandBeans.beans().initialize(this)
register(WebConfig::class.java)
refresh()
}
}
override fun ready() {
logger.info("Initializing data source")
initDataSource(context.getBean())
initDataSource(applicationContext.getBean())
httpHandler = WebHttpHandlerBuilder
.applicationContext(context)
val httpHandler: HttpHandler = WebHttpHandlerBuilder
.applicationContext(applicationContext)
.build()
logger.info("Starting web server")
......@@ -49,13 +42,9 @@ class Application(port: Int = 8080) {
.handle(ReactorHttpHandlerAdapter(httpHandler))
.bindNow()
logger.info("Application started")
val domino: Tehanu = context.getBean()
logger.info("Domino is entering Telegram")
domino.start()
}
fun initDataSource(dataSourceInitializer: DataSourceInitializer) {
private fun initDataSource(dataSourceInitializer: DataSourceInitializer) {
dataSourceInitializer.dropIfRequired(if (dataSourceInitializer.isTest) {
"/db/drop.h2.sql"
} else {
......@@ -75,10 +64,5 @@ class Application(port: Int = 8080) {
}
fun main(args: Array<String>) {
// Sets up jul-to-slf4j bridge. I have not idea
// why the logging.properties strategy doesn't work.
SLF4JBridgeHandler.removeHandlersForRootLogger()
SLF4JBridgeHandler.install()
Application()
Application().start()
}
package be.rlab.domino.application.bot
import be.rlab.domino.application.model.AccountGroups.CREDITORS
import be.rlab.domino.application.model.BillingMode
import be.rlab.domino.application.model.MemorySlots.SERVICE_CONFIGURATIONS
import be.rlab.domino.application.model.ServiceConfig
import be.rlab.domino.domain.AccountService
import be.rlab.domino.domain.AgentService
import be.rlab.domino.domain.TreasuryService
import be.rlab.domino.domain.model.*
import be.rlab.tehanu.domain.Command
import be.rlab.tehanu.domain.Memory
import be.rlab.tehanu.domain.MessageContext
import be.rlab.tehanu.domain.model.ChatType
import be.rlab.tehanu.domain.model.TextMessage
import org.joda.time.DateTime
/** Charges a new bill to the treasury account.
*/
class AddBill(
override val name: String,
override val scope: List<ChatType>,
override val timeout: Long,
private val agentService: AgentService,
private val accountService: AccountService,
private val treasuryService: TreasuryService,
memory: Memory
) : Command {
data class BillInfo(
val serviceName: String,
val description: String,
val dueDate: DateTime,
val amount: Double,
val currency: Currency = Currency.ARS
)
private val configurations: List<ServiceConfig> by memory.slot(SERVICE_CONFIGURATIONS, emptyList<ServiceConfig>())
override fun handle(
context: MessageContext,
message: TextMessage
): MessageContext {
val creditorsAccounts: List<Account> = accountService.findByGroup(CREDITORS)
return if (creditorsAccounts.isEmpty()) {
context.answer("""
No hay cuentas de servicios registradas. Para registrar una cuenta de servicio
usá /new_service y después agregá una factura.
""".trimIndent())
} else {
createBill(context, message, creditorsAccounts)
}
}
private fun createBill(
context: MessageContext,
message: TextMessage,
creditorsAccounts: List<Account>
): MessageContext {
val serviceNames: String = creditorsAccounts.joinToString(", ") { account ->
account.owner.userName
}
return context.parseInput(message.text,
"Este comando carga una nueva factura a la cuenta del tesoro. Las facturas " +
"están asociadas a un servicio. Las cuentas de servicios existentes son " +
"las siguientes: $serviceNames\n\n" +
"""
service_name: NombreDeServicio
description: factura de luz de Junio/Julio
due_date: 2019-06-27
amount: 300.0 (monto, en pesos)
currency: ARS|DOLLAR|BITCOIN (opcional, el default es ARS)
""".trimIndent()
) { billInfo: BillInfo ->
val service: Agent = require(agentService.findByUserName(billInfo.serviceName),
"el nombre del servicio no es válido, usá uno de los nombres que te dije más arriba."
)
val config: ServiceConfig = require(
configurations.find { config ->
config.id == service.id
},
"el servicio no está configurado, usá /configure_service para " +
"establecer los parámetros de configuración"
)
val value: Value = Value.valueOf(billInfo.currency, billInfo.amount)
val newBill: Bill = treasuryService.newBill(
service = service,
description = billInfo.description,
value = value,
dueDate = billInfo.dueDate,
replace = config.billingMode == BillingMode.SINGLE
)
if (newBill.debt.value != value) {
answer(
"ya existe una factura anterior que está parcialmente paga, a la nueva " +
"factura le resté el monto que ya pagaste y quedó cargada con un monto de ${newBill.debt.value}"
)
} else {
answer("listo, la factura fue cargada en la cuenta del tesoro")
}
}
}
}
package be.rlab.domino.application.bot
import be.rlab.tehanu.domain.Command
import be.rlab.tehanu.domain.MessageContext
import be.rlab.tehanu.domain.model.ChatType
import be.rlab.tehanu.domain.model.TextMessage
import be.rlab.domino.domain.AgentService
import be.rlab.domino.domain.TreasuryService
import be.rlab.domino.domain.model.Agent
import be.rlab.domino.domain.model.Currency
import be.rlab.domino.domain.model.Value
/** Reports a new donation and adds the donation to the current's user account.
*/
class AddDonation(
override val name: String,
override val scope: List<ChatType>,
override val timeout: Long,
private val agentService: AgentService,
private val treasuryService: TreasuryService
) : Command {
data class Donation(
val from: String,
val amount: Double,
val currency: Currency = Currency.ARS
)
override fun handle(
context: MessageContext,
message: TextMessage
): MessageContext {
return context.parseInput(message.text,
"Este comando agrega una donación y la acredita en tu cuenta. Para " +
"agregar una donación pasame los siguientes datos: \n\n" +
"""
from: NombreDeUsuario (la cuenta tiene que existir)
amount: 500.0
currency: ARS|DOLLAR|BITCOIN (opcional, el default es ARS)
""".trimIndent()
) { donation: Donation ->
val donor: Agent = require(
agentService.findByUserName(donation.from),
"La cuenta del donante no existe, podés crearla usando /new_supporter"
)
val receiver: Agent = require(
agentService.findByUserId(context.user.id),
"Tu cuenta no existe, podés crearla usando /new_account"
)
treasuryService.newDonation(
donor = donor,
receiver = receiver,
value = Value.valueOf(donation.currency, donation.amount)
)
context.answer("listo, el dinero quedó registrado en tu cuenta")
}
}
}
package be.rlab.domino.application.bot
import be.rlab.domino.domain.AgentService
import be.rlab.domino.domain.TreasuryService
import be.rlab.domino.domain.model.Agent
import be.rlab.domino.domain.model.Currency
import be.rlab.domino.domain.model.Value
import be.rlab.tehanu.domain.Command
import be.rlab.tehanu.domain.MessageContext
import be.rlab.tehanu.domain.model.ChatType
import be.rlab.tehanu.domain.model.TextMessage
/** Makes a deposit into the current account.
*/
class Deposit(
override val name: String,
override val scope: List<ChatType>,
override val timeout: Long,
private val agentService: AgentService,
private val treasuryService: TreasuryService
) : Command {
data class Deposit(
val amount: Double,
val description: String,
val currency: Currency = Currency.ARS
)
override fun handle(
context: MessageContext,
message: TextMessage
): MessageContext {
return context.parseInput(message.text,
"Este comando realiza un depósito en la cuenta actual.\n" +
"Para hacer el depósito pasame los siguientes datos:\n\n" +
"""
amount: 500.0
description: regalo de los reyes magos
currency: ARS|DOLLAR|BITCOIN (opcional)
""".trimIndent()
) { deposit: Deposit ->
val userName: String = require(
context.user.userName,
"No tenés configurado un nombre de usuario, necesito que tengas nombre " +
"de usuario para hacer un depósito"
)
val target: Agent = require(
agentService.findByUserName(userName),
"Tu cuenta no existe, podés crearla usando /new_account"
)
treasuryService.newDeposit(
target = target,
description = deposit.description,
value = Value.valueOf(deposit.currency, deposit.amount)
)
context.answer("listo, el dinero quedó depositado en tu cuenta")
}
}
}
package be.rlab.domino.application.bot
import be.rlab.domino.domain.AgentService
import be.rlab.domino.domain.TreasuryService
import be.rlab.domino.domain.model.Agent
import be.rlab.domino.domain.model.Bill
import be.rlab.domino.domain.model.Value.Companion.valueOf
import be.rlab.tehanu.domain.Command
import be.rlab.tehanu.domain.MessageContext
import be.rlab.tehanu.domain.model.ChatType
import be.rlab.tehanu.domain.model.TextMessage
import org.joda.time.format.DateTimeFormat
/** Pays a pending bill from the user's account.
*/
class PayBill(
override val name: String,
override val scope: List<ChatType>,
override val timeout: Long,
private val agentService: AgentService,
private val treasuryService: TreasuryService
) : Command {
data class BillPayment(
val billId: Int,
val amount: Double?
)
override fun handle(
context: MessageContext,
message: TextMessage
): MessageContext {
val pendingBills: List<Bill> = treasuryService.listPendingBills()
return if (pendingBills.isEmpty()) {
context.answer("""
No tengo facturas pendientes :O
""".trimIndent())
} else {
payBill(context, message, pendingBills)
}
}
private fun payBill(
context: MessageContext,
message: TextMessage,
pendingBills: List<Bill>
): MessageContext {
val billsMessage: String = pendingBills.joinToString("\n") { bill ->
val dueDate: String? = bill.dueDate?.toString(DateTimeFormat.forPattern("dd/MM/YYYY"))
"[${bill.shortId()}] ${bill.creditor().userName}: ${bill.credit.debt} (vence $dueDate)"
}
return context.parseInput(message.text,
"Decime qué factura querés pagar y el monto. Si no ponés el monto es porque " +
"pagás toda la factura. Estas son las facturas pendientes, el número es el " +
"identificador de la factura:" + "\n\n$billsMessage\n\n" +
"""
bill_id: IdDeLaFactura
amount: 1400.0 (monto, en la moneda de la factura. Opcional)
""".trimIndent()
) { billPayment: BillPayment ->
val userName: String = require(
context.user.userName,
"No tenés configurado un nombre de usuario, necesito que tengas nombre " +
"de usuario para pagar una factura"
)
val payer: Agent = require(
agentService.findByUserName(userName),
"Tu cuenta no existe, podés crearla usando /new_account"
)
val bill: Bill = require(pendingBills.find { pendingBill ->
pendingBill.shortId() == billPayment.billId
}, "La factura con id ${billPayment.billId} no existe.")
if (!bill.pending) {
answer("la factura ${billPayment.billId} ya está paga")
} else {
treasuryService.payBill(
payer = payer,
bill = bill,
value = billPayment.amount?.let {
valueOf(bill.debt.value.currency, billPayment.amount)
} ?: bill.debt.value
)
answer("listo, la factura quedó pagada")
}
}
}
}
package be.rlab.domino.application.bot
import be.rlab.tehanu.domain.Command
import be.rlab.tehanu.domain.MessageContext
import be.rlab.tehanu.domain.model.ChatType
import be.rlab.tehanu.domain.model.TextMessage
import be.rlab.domino.domain.AgentService
import be.rlab.domino.domain.TreasuryService
import be.rlab.domino.domain.model.Agent
import be.rlab.domino.domain.model.Currency
import be.rlab.domino.domain.model.Value
/** Makes transfers from a source account to a destination account.
*/
class Transfer(
override val name: String,
override val scope: List<ChatType>,
override val timeout: Long,
private val agentService: AgentService,
private val treasuryService: TreasuryService
) : Command {
data class Transfer(
val from: String,
val to: String,
val amount: Double,
val currency: Currency
)
override fun handle(
context: MessageContext,
message: TextMessage
): MessageContext {
return context.parseInput(message.text,
"Este comando hace una transferencia entre dos cuentas. Ambas cuentas " +
"tienen que existir. Para hacer la transferencia pasame los siguientes datos: \n\n" +
"""
from: NombreDeUsuarioOrigen
to: NombreDeUsuarioDestino
amount: 500.0
currency: ARS|DOLLAR|BITCOIN
""".trimIndent()
) { transfer: Transfer ->
val source: Agent = require(
agentService.findByUserName(transfer.from),
"La cuenta del donante no existe, podés crearla usando /new_supporter"
)
val destination: Agent = require(
agentService.findByUserName(transfer.to),
"Tu cuenta no existe, podés crearla usando /new_account"
)
treasuryService.newTransfer(
source = source,
destination = destination,
value = Value.valueOf(transfer.currency, transfer.amount)
)
context.answer("listo, el dinero quedó transferido a la cuenta de @${destination.userName}")
}
}
}
package be.rlab.domino.application.bot.bills
import be.rlab.domino.application.bot.view.account
import be.rlab.domino.application.bot.view.value
import be.rlab.domino.application.model.AccountGroups.CREDITORS
import be.rlab.domino.application.model.BillingMode
import be.rlab.domino.application.model.MemorySlots.SERVICE_CONFIGURATIONS
import be.rlab.domino.application.model.ServiceConfig
import be.rlab.domino.domain.AccountService
import be.rlab.domino.domain.TreasuryService
import be.rlab.domino.domain.model.Account
import be.rlab.domino.domain.model.Bill
import be.rlab.domino.domain.model.Value
import be.rlab.tehanu.domain.Command
import be.rlab.tehanu.domain.Memory
import be.rlab.tehanu.domain.MessageContext
import be.rlab.tehanu.domain.model.ChatType
import be.rlab.tehanu.domain.model.Message
import be.rlab.tehanu.view.model.dateTime
import org.joda.time.DateTime
/** Charges a new bill to the treasury account.
*/
class AddBill(
override val name: String,
override val scope: List<ChatType>,
private val accountService: AccountService,
private val treasuryService: TreasuryService,
memory: Memory
) : Command {
private val configurations: List<ServiceConfig> by memory.slot(SERVICE_CONFIGURATIONS, emptyList<ServiceConfig>())
override fun handle(
context: MessageContext,
message: Message
): MessageContext = context.userInput(
"Este comando carga una nueva factura a la cuenta de la organización"
) {
val creditorsAccounts: List<Account> = accountService.findByGroup(CREDITORS)
require(creditorsAccounts.isNotEmpty()) {
"No hay cuentas de servicios registradas. Para registrar una cuenta de servicio " +
"usá /new_service y después agregá una factura."
}
val serviceAccount: Account by account(creditorsAccounts,
"Elegí el servicio para el que vas a cargar la factura. Si el servicio no existe " +
"podés crearlo con /new_service"
)
val description: String by field("Escribí una descripcion de la factura")
val dueDate: DateTime by dateTime(
"cuál es la fecha de vencimiento de la factura? Escribila con el " +
"siguiente formato: 17/09/2019"
)
val value: Value by value("cuál es el monto de la factura? ingresá un valor numérico")
onSubmit {
createBill(context, serviceAccount, description, dueDate, value)
}
}
private fun createBill(
context: MessageContext,
serviceAccount: Account,
description: String,
dueDate: DateTime,
value: Value
) {
val config: ServiceConfig = context.require(
configurations.find { config ->
config.id == serviceAccount.owner.id
},
"el servicio no está configurado, usá /configure_service para " +
"establecer los parámetros de configuración"
)
val newBill: Bill = treasuryService.newBill(
service = serviceAccount.owner,
description = description,
value = value,
dueDate = dueDate,
replace = config.billingMode == BillingMode.SINGLE
)
if (newBill.debt.value != value) {
context.answer(
"ya existe una factura anterior que está parcialmente paga, a la nueva " +
"factura le resté el monto que ya pagaste y quedó cargada con un monto de ${newBill.debt.value}"
)
} else {
context.answer("listo, la factura fue cargada en la cuenta del tesoro")
}
}
}
package be.rlab.domino.application.bot
package be.rlab.domino.application.bot.bills
import be.rlab.domino.application.model.BillingMode
import be.rlab.domino.application.model.MemorySlots.SERVICE_CONFIGURATIONS
......@@ -9,6 +9,7 @@ import be.rlab.tehanu.domain.Command
import be.rlab.tehanu.domain.Memory
import be.rlab.tehanu.domain.MessageContext
import be.rlab.tehanu.domain.model.ChatType
import be.rlab.tehanu.domain.model.Message
import be.rlab.tehanu.domain.model.TextMessage
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
......@@ -20,7 +21,6 @@ import org.joda.time.format.DateTimeFormatter
class Bills(
override val name: String,
override val scope: List<ChatType>,
override val timeout: Long,
private val treasuryService: TreasuryService,
memory: Memory
) : Command {
......@@ -33,13 +33,14 @@ class Bills(
override fun handle(
context: MessageContext,
message: TextMessage