Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

develop -> main #48

Merged
merged 8 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ jobs:
DB_NAME: ${{ secrets.DB_NAME }}
DASHBOARD: ${{ secrets.DASHBOARD }}
FIREBASE_ADMIN_AUTH_CREDENTIALS: ${{ secrets.FIREBASE_ADMIN_AUTH_CREDENTIALS }}
FOREX_API_KEY: ${{ secrets.FOREX_API_KEY }}
DASHBOARD_USER: ${{ secrets.DASHBOARD_USER }}
DASHBOARD_PASS: ${{ secrets.DASHBOARD_PASS }}

Expand Down
49 changes: 28 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
# Play Deals
[![Static Badge](https://img.shields.io/badge/Android-black?logo=android&logoColor=white&color=%234889f5)](https://play.google.com/store/apps/details?id=me.sujanpoudel.playdeals)  
[![Static Badge](https://img.shields.io/badge/IOS-grey?logo=apple)](https://github.com/psuzn/app-deals/releases/latest)   
[![Static Badge](https://img.shields.io/badge/macOS-black?logo=apple)](https://github.com/psuzn/app-deals/releases/latest)  
[![Static Badge](https://img.shields.io/badge/Windows-green?logo=windows&color=blue)](https://github.com/psuzn/app-deals/releases/latest)  
[![Static Badge](https://img.shields.io/badge/Linux-white?logo=linux&logoColor=white&color=grey)](https://github.com/psuzn/app-deals/releases/latest)  

[![Static Badge](https://img.shields.io/badge/Android-black?logo=android&logoColor=white&color=%234889f5)](https://play.google.com/store/apps/details?id=me.sujanpoudel.playdeals)
 
[![Static Badge](https://img.shields.io/badge/IOS-grey?logo=apple)](https://github.com/psuzn/app-deals/releases/latest)
  
[![Static Badge](https://img.shields.io/badge/macOS-black?logo=apple)](https://github.com/psuzn/app-deals/releases/latest)
 
[![Static Badge](https://img.shields.io/badge/Windows-green?logo=windows&color=blue)](https://github.com/psuzn/app-deals/releases/latest)
 
[![Static Badge](https://img.shields.io/badge/Linux-white?logo=linux&logoColor=white&color=grey)](https://github.com/psuzn/app-deals/releases/latest)
 

![Static Badge](https://img.shields.io/badge/License-GPL--v3-brightgreen)
[![play-deals-backend 1.0 CI](https://github.com/psuzn/play-deals-backend/actions/workflows/ci.yaml/badge.svg)](https://github.com/psuzn/play-deals-backend/actions/workflows/ci.yaml)

![Feature](./media/feature-graphic.jpeg)


| <a href="https://play.google.com/store/apps/details?id=me.sujanpoudel.playdeals"> <img src="media/badge-get-on-google-play.png" width="200" alt="Get it on Google play"> </a> | <a href="https://github.com/psuzn/app-deals/releases/latest"> <img src="media/badge-download-apk.png" width="160" alt="Download Apk"> </a> |
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------:|

Expand Down Expand Up @@ -41,21 +46,23 @@ Configuration can be done by passing environment variables listed below:
> 1. run `just dev-run` from terminal, OR
> 2. Install [Envfile](https://plugins.jetbrains.com/plugin/7861-envfile) plugin for IntelliJ and run using IntelliJ

| ENV_VAR | REQUIRED | DEFAULT | EXAMPLE | NOTES |
|------------------|----------|---------------|--------------|:--------------------------------------------------------------------|
| `DB_HOST` | `Y` | | `localhost` | |
| `DB_USERNAME` | `Y` | | `whatever` | |
| `DB_PASSWORD` | `N` | `password` | `whatever` | |
| `DB_PORT` | `N` | `5432` | `6868` | |
| `DB_NAME` | `N` | `play_deals` | `whatever` | |
| `DB_POOL_SIZE` | `N` | `5` | `6` | |
| `ENV` | `N` | `PRODUCTION` | `PRODUCTION` | one of `PRODUCTION or DEVELOPMENT or TEST ` |
| `APP_PORT` | `N` | `8888` | `9999` | |
| `POSTGRES_IMAGE` | `N` | `postgres:14` | | Useful for testing new versions of postgres. Used only in test code |
| `DASHBOARD` | `N` | `true` | `false` | Whether to enable or not the Jobrunr dashboard |
| `DASHBOARD_USER` | `N` | `admin` | `whatever` | Jobrunr dashboard login credential |
| `DASHBOARD_PASS` | `N` | `admin` | `whatever` | Jobrunr dashboard login credential |
| `CORS` | `N` | `*` | `whatever` | origins allowed for CORS |
| ENV_VAR | REQUIRED | DEFAULT | EXAMPLE | NOTES |
|-----------------------------------|----------|---------------|--------------|:-----------------------------------------------------------------------|
| `DB_HOST` | `Y` | | `localhost` | |
| `DB_USERNAME` | `Y` | | `whatever` | |
| `FIREBASE_ADMIN_AUTH_CREDENTIALS` | `Y` | | `whatever` | Firebase admin auth credentials |
| `FOREX_API_KEY` | `Y` | | `whatever` | Api key for [https://exchangeratesapi.io](https://exchangeratesapi.io) |
| `DB_PASSWORD` | `N` | `password` | `whatever` | |
| `DB_PORT` | `N` | `5432` | `6868` | |
| `DB_NAME` | `N` | `play_deals` | `whatever` | |
| `DB_POOL_SIZE` | `N` | `5` | `6` | |
| `ENV` | `N` | `PRODUCTION` | `PRODUCTION` | one of `PRODUCTION or DEVELOPMENT or TEST ` |
| `APP_PORT` | `N` | `8888` | `9999` | |
| `POSTGRES_IMAGE` | `N` | `postgres:14` | | Useful for testing new versions of postgres. Used only in test code |
| `DASHBOARD` | `N` | `true` | `false` | Whether to enable or not the Jobrunr dashboard |
| `DASHBOARD_USER` | `N` | `admin` | `whatever` | Jobrunr dashboard login credential |
| `DASHBOARD_PASS` | `N` | `admin` | `whatever` | Jobrunr dashboard login credential |
| `CORS` | `N` | `*` | `whatever` | origins allowed for CORS |

## License

Expand Down
3 changes: 2 additions & 1 deletion backend/src/main/kotlin/me/sujanpoudel/playdeals/Conf.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ data class Conf(
val api: Api,
val environment: Environment,
val backgroundTask: BackgroundTask,
val firebaseAuthCredential: String
val firebaseAuthCredential: String,
val forexApiKey: String
) {
data class DB(
val host: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import me.sujanpoudel.playdeals.jobs.AndroidAppExpiryCheckScheduler
import me.sujanpoudel.playdeals.jobs.AppDetailScrapper
import me.sujanpoudel.playdeals.jobs.BackgroundJobsVerticle
import me.sujanpoudel.playdeals.jobs.DealSummarizer
import me.sujanpoudel.playdeals.jobs.ForexFetcher
import me.sujanpoudel.playdeals.jobs.RedditPostsScrapper
import me.sujanpoudel.playdeals.repositories.DealRepository
import me.sujanpoudel.playdeals.repositories.KeyValuesRepository
Expand All @@ -28,6 +29,7 @@ import me.sujanpoudel.playdeals.repositories.persistent.PersistentKeyValuesRepos
import me.sujanpoudel.playdeals.services.MessagingService
import me.sujanpoudel.playdeals.usecases.DBHealthUseCase
import me.sujanpoudel.playdeals.usecases.GetDealsUseCase
import me.sujanpoudel.playdeals.usecases.GetForexUseCase
import me.sujanpoudel.playdeals.usecases.NewDealUseCase
import org.flywaydb.core.Flyway
import org.jobrunr.configuration.JobRunr
Expand Down Expand Up @@ -155,10 +157,17 @@ fun configureDI(
requestScheduler = instance()
)
}
bindSingleton {
ForexFetcher(
di = di,
conf = instance()
)
}

bindSingleton { DBHealthUseCase(di) }
bindSingleton { GetDealsUseCase(di) }
bindSingleton { NewDealUseCase(di) }
bindSingleton { GetForexUseCase(di) }

bindSingleton<FirebaseOptions> {
FirebaseOptions.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class ApiVerticle(
router.route().handler(CorsHandler.create().addRelativeOrigin(config.api.cors))
router.route("/health/*").subRouter(healthApi(di, vertx))
router.route("/api/deals/*").subRouter(appDealsApi(di, vertx))
router.route("/api/forex/*").subRouter(forexRateApi(di, vertx))

vertx.createHttpServer()
.requestHandler(router)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package me.sujanpoudel.playdeals.api

import io.vertx.ext.web.Router
import me.sujanpoudel.playdeals.common.coHandler
import me.sujanpoudel.playdeals.common.jsonResponse
import me.sujanpoudel.playdeals.usecases.GetForexUseCase
import me.sujanpoudel.playdeals.usecases.executeUseCase
import org.kodein.di.DirectDI
import org.kodein.di.instance

fun forexRateApi(
di: DirectDI,
vertx: io.vertx.core.Vertx
): Router = Router.router(vertx).apply {
get()
.coHandler { ctx ->
ctx.executeUseCase(
useCase = di.instance<GetForexUseCase>(),
toContext = { },
toInput = { }
) {
ctx.json(jsonResponse(data = it))
}
}
}
87 changes: 41 additions & 46 deletions backend/src/main/kotlin/me/sujanpoudel/playdeals/common/Conf.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:Suppress("UNCHECKED_CAST")

package me.sujanpoudel.playdeals.common

import me.sujanpoudel.playdeals.Conf
Expand All @@ -7,74 +9,67 @@ import java.util.Base64

class BootstrapException(val violations: List<String>) : RuntimeException()

fun buildConf(env: Map<String, String>) = com.github.michaelbull.result.runCatching {
fun buildConf(envs: Map<String, String>) = com.github.michaelbull.result.runCatching {
val violations = mutableListOf<String>()

val environment = env.getOrDefault("ENV", Environment.PRODUCTION.name).asEnumOrNull<Environment>()

if (environment == null) {
violations += "Invalid ENV"
}

val dashboardEnabled = env.getOrDefault("DASHBOARD", "true").toBooleanStrictOrNull()
if (dashboardEnabled == null) {
violations += "Invalid DASHBOARD"
@Suppress("UNCHECKED_CAST")
fun <T> env(
envVarName: String,
default: String? = null,
converter: (String) -> T? = { it as? T }
): T? = (
envs[envVarName] ?: default ?: run {
violations += "No '$envVarName' env var defined!".also { logger.error { it } }
null
}
)?.let(converter) ?: run {
violations += "Invalid '$envVarName'"
null
}

val appPort = env.getOrDefault("APP_PORT", "8888").toIntOrNull()

if (appPort == null) {
violations += "Invalid APP_PORT"
}
val environment = env("ENV", Environment.PRODUCTION.name) { it.asEnumOrNull<Environment>() }

val dbPort = env.getOrDefault("DB_PORT", "5432").toIntOrNull()
if (dbPort == null) {
violations += "Invalid DB_PORT"
}
val appPort = env("APP_PORT", "8888") { it.toIntOrNull() }
val cors = env<String>("CORS", ".*.")

val dbName = env.getOrDefault("DB_NAME", "play_deals")
val dbPoolSize = (env["DB_POOL_SIZE"] ?: "5").toIntOrNull()
if (dbPoolSize == null) {
violations += "Invalid DB_POOL_SIZE"
}
val dbPort = env("DB_PORT", "5432") { it.toIntOrNull() }
val dbName = env<String>("DB_NAME", "play_deals")
val dbPoolSize = env("DB_POOL_SIZE", "5") { it.toIntOrNull() }
val dbHost = env<String>("DB_HOST")
val dbUsername = env<String>("DB_USERNAME")
val dbPassword = env<String>("DB_PASSWORD", "password")

fun envVar(envVarName: String): String? {
val value = env[envVarName]
val dashboardEnabled = env("DASHBOARD", "true") { it.toBooleanStrictOrNull() }
val dashboardUser = env<String>("DASHBOARD_USER", "admin")
val dashboardPassword = env<String>("DASHBOARD_PASS", "admin")

return if (value.isNullOrBlank()) {
violations += "No $envVarName env var defined!".also { logger.error { it } }
null
} else {
value
}
val firebaseAuthCredential = env("FIREBASE_ADMIN_AUTH_CREDENTIALS") {
Base64.getDecoder().decode(it).decodeToString()
}

val dbHost: String = envVar("DB_HOST").orEmpty()
val dbUsername: String = envVar("DB_USERNAME").orEmpty()
val firebaseAuthCredential = envVar("FIREBASE_ADMIN_AUTH_CREDENTIALS")?.let {
Base64.getDecoder().decode(it).decodeToString()
}.orEmpty()
val forexApiKey = env<String>("FOREX_API_KEY")

if (violations.isNotEmpty()) {
throw BootstrapException(violations)
} else {
Conf(
api = Conf.Api(appPort!!, cors = env.getOrDefault("CORS", ".*.")),
api = Conf.Api(appPort!!, cors = cors!!),
environment = environment!!,
db = Conf.DB(
host = dbHost,
host = dbHost!!,
port = dbPort!!,
name = dbName,
username = dbUsername,
password = env.getOrDefault("DB_PASSWORD", "password"),
name = dbName!!,
username = dbUsername!!,
password = dbPassword!!,
poolSize = dbPoolSize!!
),
backgroundTask = Conf.BackgroundTask(
dashboardEnabled!!,
env.getOrDefault("DASHBOARD_USER", "admin"),
env.getOrDefault("DASHBOARD_PASS", "admin")
dashboardEnabled = dashboardEnabled!!,
dashboardUserName = dashboardUser!!,
dashboardPassword = dashboardPassword!!
),
firebaseAuthCredential = firebaseAuthCredential
firebaseAuthCredential = firebaseAuthCredential!!,
forexApiKey = forexApiKey!!
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package me.sujanpoudel.playdeals.domain

import java.time.OffsetDateTime

// Rates are USD based
data class ForexRate(
val timestamp: OffsetDateTime,
val rates: List<ConversionRate>
)

data class ConversionRate(val currency: String, val rate: Float)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package me.sujanpoudel.playdeals.jobs

import io.vertx.kotlin.coroutines.CoroutineVerticle
import me.sujanpoudel.playdeals.repositories.KeyValuesRepository
import org.jobrunr.configuration.JobRunr
import org.jobrunr.configuration.JobRunrConfiguration
import org.jobrunr.scheduling.JobRequestScheduler
Expand All @@ -14,16 +15,22 @@ class BackgroundJobsVerticle(
) : CoroutineVerticle(), DIAware {

private val jobRequestScheduler by instance<JobRequestScheduler>()
private val keyValuesRepository by instance<KeyValuesRepository>()

override suspend fun start() {
direct.instance<JobRunrConfiguration.JobRunrConfigurationResult>()
setupRecurringJobs()
}

private fun setupRecurringJobs() {
private suspend fun setupRecurringJobs() {
jobRequestScheduler.createRecurrently(RedditPostsScrapper.Request())
jobRequestScheduler.createRecurrently(AndroidAppExpiryCheckScheduler.Request())
jobRequestScheduler.createRecurrently(DealSummarizer.Request())
jobRequestScheduler.createRecurrently(ForexFetcher.Request())

if (keyValuesRepository.getForexRate() == null) {
jobRequestScheduler.enqueue(ForexFetcher.Request.immediate())
}
}

override suspend fun stop() {
Expand Down
Loading