Skip to content
This repository has been archived by the owner on Nov 5, 2024. It is now read-only.

Commit

Permalink
Fix time (#3435)
Browse files Browse the repository at this point in the history
* Handle edge-cases in the `TimeConverter`

* Add tests for the edge-cases

* Implement `TimeFormatter` API (not tested)

* Refactor `IvyTimeFormatter`

* WIP: Migrate `LocalDateTime` to `Instant`

* WIP: Migrate `LocalDateTime` to `Instant`

* WIP: Migrate `LocalDateTime` to `Instant`

* WIP: Migrate `LocalDateTime` to `Instant`

* WIP: Migrate `LocalDateTime` to `Instant`

* The app builds! (and crashes)

* Fix Long overflow

* Reduce the time range by 10 years to be safe

* Fix data layer tests

* Fix long overflow

* Re-implement IvyTimeFormatter (ChatGPT impl was bad!)

* WIP: `IvyTimeFormatterTest`

* WIP: `IvyTimeFormatterTest`

* Cover with tests

* Fix end-of-time handling

* Fix Detekt errors

* Fix build error

* Fix Detekt errors

* Fix unit tests on CI
  • Loading branch information
ILIYANGERMANOV authored Aug 29, 2024
1 parent 6c3937d commit 8ef3841
Show file tree
Hide file tree
Showing 85 changed files with 1,547 additions and 2,209 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ dependencies {
implementation(projects.shared.data.core)
implementation(projects.shared.domain)
implementation(projects.shared.ui.navigation)
implementation(projects.shared.ui.core)
implementation(projects.temp.legacyCode)
implementation(projects.temp.oldDesign)
implementation(projects.widget.addTransaction)
Expand Down
22 changes: 20 additions & 2 deletions app/src/main/java/com/ivy/wallet/RootActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
import com.google.android.play.core.review.ReviewManagerFactory
import com.ivy.IvyNavGraph
import com.ivy.base.time.TimeConverter
import com.ivy.base.time.TimeProvider
import com.ivy.design.api.IvyUI
import com.ivy.domain.RootScreen
import com.ivy.home.customerjourney.CustomerJourneyCardsProvider
Expand All @@ -45,6 +47,7 @@ import com.ivy.legacy.utils.timeNowLocal
import com.ivy.navigation.Navigation
import com.ivy.navigation.NavigationRoot
import com.ivy.ui.R
import com.ivy.ui.time.TimeFormatter
import com.ivy.wallet.ui.applocked.AppLockedScreen
import com.ivy.widget.balance.WalletBalanceWidgetReceiver
import com.ivy.widget.transaction.AddTransactionWidget
Expand All @@ -66,6 +69,15 @@ class RootActivity : AppCompatActivity(), RootScreen {
@Inject
lateinit var customerJourneyLogic: CustomerJourneyCardsProvider

@Inject
lateinit var timeConverter: TimeConverter

@Inject
lateinit var timeProvider: TimeProvider

@Inject
lateinit var timeFormatter: TimeFormatter

private lateinit var createFileLauncher: ActivityResultLauncher<String>
private lateinit var onFileCreated: (fileUri: Uri) -> Unit

Expand Down Expand Up @@ -110,7 +122,10 @@ class RootActivity : AppCompatActivity(), RootScreen {

true -> {
IvyUI(
design = appDesign(ivyContext)
design = appDesign(ivyContext),
timeConverter = timeConverter,
timeProvider = timeProvider,
timeFormatter = timeFormatter,
) {
AppLockedScreen(
onShowOSBiometricsModal = {
Expand All @@ -129,7 +144,10 @@ class RootActivity : AppCompatActivity(), RootScreen {
NavigationRoot(navigation = navigation) { screen ->
IvyUI(
design = appDesign(ivyContext),
includeSurface = screen?.isLegacy ?: true
includeSurface = screen?.isLegacy ?: true,
timeConverter = timeConverter,
timeProvider = timeProvider,
timeFormatter = timeFormatter,
) {
IvyNavGraph(screen)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.viewModelScope
import com.ivy.base.legacy.SharedPrefs
import com.ivy.base.time.TimeConverter
import com.ivy.base.time.TimeProvider
import com.ivy.data.DataObserver
import com.ivy.data.DataWriteEvent
import com.ivy.data.repository.AccountRepository
Expand Down Expand Up @@ -45,6 +47,8 @@ class AccountsViewModel @Inject constructor(
private val accountRepository: AccountRepository,
private val dataObserver: DataObserver,
private val features: Features,
private val timeProvider: TimeProvider,
private val timeConverter: TimeConverter,
) : ComposeViewModel<AccountsState, AccountsEvent>() {
private val baseCurrency = mutableStateOf("")
private val accountsData = mutableStateOf(listOf<AccountData>())
Expand Down Expand Up @@ -157,7 +161,7 @@ class AccountsViewModel @Inject constructor(
val period = com.ivy.legacy.data.model.TimePeriod.currentMonth(
startDayOfMonth = ivyContext.startDayOfMonth
) // this must be monthly
val range = period.toRange(ivyContext.startDayOfMonth)
val range = period.toRange(ivyContext.startDayOfMonth, timeConverter, timeProvider)

val baseCurrencyCode = baseCurrencyAct(Unit)
val accounts = accountRepository.findAll().toImmutableList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.viewModelScope
import com.ivy.base.time.TimeConverter
import com.ivy.base.time.TimeProvider
import com.ivy.ui.ComposeViewModel
import com.ivy.legacy.data.model.TimePeriod
import com.ivy.legacy.utils.ioThread
Expand All @@ -23,7 +25,9 @@ class BalanceViewModel @Inject constructor(
private val plannedPaymentsLogic: PlannedPaymentsLogic,
private val ivyContext: com.ivy.legacy.IvyWalletCtx,
private val baseCurrencyAct: BaseCurrencyAct,
private val calcWalletBalanceAct: CalcWalletBalanceAct
private val calcWalletBalanceAct: CalcWalletBalanceAct,
private val timeProvider: TimeProvider,
private val timeConverter: TimeConverter,
) : ComposeViewModel<BalanceState, BalanceEvent>() {

private val period = mutableStateOf(ivyContext.selectedPeriod)
Expand Down Expand Up @@ -69,7 +73,7 @@ class BalanceViewModel @Inject constructor(

plannedPaymentsAmount.doubleValue = ioThread {
plannedPaymentsLogic.plannedPaymentsAmountFor(
timePeriod.toRange(ivyContext.startDayOfMonth)
timePeriod.toRange(ivyContext.startDayOfMonth, timeConverter, timeProvider)
// + positive if Income > Expenses else - negative
) * if (numberOfMonthsAhead.intValue >= 0) {
numberOfMonthsAhead.intValue.toDouble()
Expand Down
96 changes: 4 additions & 92 deletions screen/budgets/src/main/java/com/ivy/budgets/BudgetScreen.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ivy.budgets

import android.annotation.SuppressLint
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand All @@ -20,15 +21,11 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.ivy.base.legacy.Theme
import com.ivy.budgets.model.DisplayBudget
import com.ivy.design.api.LocalTimeFormatter
import com.ivy.design.l0_system.UI
import com.ivy.design.l0_system.style
import com.ivy.legacy.data.model.Month
import com.ivy.legacy.data.model.TimePeriod
import com.ivy.legacy.datamodel.Budget
import com.ivy.legacy.legacy.ui.theme.components.BudgetBattery
import com.ivy.legacy.utils.clickableNoIndication
import com.ivy.legacy.utils.format
Expand All @@ -42,7 +39,6 @@ import com.ivy.wallet.ui.theme.components.IvyIcon
import com.ivy.wallet.ui.theme.components.ReorderButton
import com.ivy.wallet.ui.theme.components.ReorderModalSingleType
import com.ivy.wallet.ui.theme.wallet.AmountCurrencyB1
import kotlinx.collections.immutable.persistentListOf

@Composable
fun BoxWithConstraintsScope.BudgetScreen(screen: BudgetScreen) {
Expand Down Expand Up @@ -197,7 +193,7 @@ private fun Toolbar(
Spacer(Modifier.height(4.dp))

Text(
text = timeRange.toDisplay(),
text = timeRange.toDisplay(LocalTimeFormatter.current),
style = UI.typo.b2.style(
color = UI.colors.pureInverse,
fontWeight = FontWeight.Medium
Expand Down Expand Up @@ -267,6 +263,7 @@ private fun Toolbar(
}
}

@SuppressLint("ComposeContentEmitterReturningValues", "ComposeMultipleContentEmitters")
@Composable
private fun BudgetItem(
displayBudget: DisplayBudget,
Expand Down Expand Up @@ -367,89 +364,4 @@ private fun NoBudgetsEmptyState(

Spacer(Modifier.height(96.dp))
}
}

@Preview
@Composable
private fun Preview_Empty() {
com.ivy.legacy.IvyWalletPreview {
UI(
state = BudgetScreenState(
timeRange = com.ivy.legacy.data.model.TimePeriod.currentMonth(
startDayOfMonth = 1
).toRange(1), // preview
baseCurrency = "BGN",
categories = persistentListOf(),
accounts = persistentListOf(),
budgets = persistentListOf(),
appBudgetMax = 5000.0,
categoryBudgetsTotal = 2400.0,
totalRemainingBudgetText = "Total Remaining Budget: 150.00 BGN",
budgetModalData = null,
reorderModalVisible = false
)
)
}
}

@Preview
@Composable
private fun Preview_Budgets(theme: Theme) {
com.ivy.legacy.IvyWalletPreview(theme) {
UI(
state = BudgetScreenState(
timeRange = TimePeriod(month = Month.monthsList().first()).toRange(1), // preview
baseCurrency = "BGN",
categories = persistentListOf(),
accounts = persistentListOf(),
appBudgetMax = 5000.0,
categoryBudgetsTotal = 0.0,
totalRemainingBudgetText = "Budget exceeded by 50.00 BGN",
budgetModalData = null,
reorderModalVisible = false,
budgets = persistentListOf(
DisplayBudget(
budget = Budget(
name = "Ivy Marketing",
amount = 1000.0,
accountIdsSerialized = null,
categoryIdsSerialized = null,
orderId = 0.0
),
spentAmount = 260.0
),
DisplayBudget(
budget = Budget(
name = "Ivy Marketing 2",
amount = 1000.0,
accountIdsSerialized = null,
categoryIdsSerialized = null,
orderId = 0.0
),
spentAmount = 351.0
),
DisplayBudget(
budget = Budget(
name = "Baldr Products, Fidgets",
amount = 750.0,
accountIdsSerialized = null,
categoryIdsSerialized = "cat1,cat2,cat3",
orderId = 0.1
),
spentAmount = 50.0
)
)
)
)
}
}

/** For screenshot testing */
@Composable
fun BudgetScreenUiTest(isDark: Boolean) {
val theme = when (isDark) {
true -> Theme.DARK
false -> Theme.LIGHT
}
Preview_Budgets(theme)
}
50 changes: 34 additions & 16 deletions screen/budgets/src/main/java/com/ivy/budgets/BudgetViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewModelScope
import com.ivy.base.legacy.SharedPrefs
import com.ivy.base.time.TimeConverter
import com.ivy.base.time.TimeProvider
import com.ivy.budgets.model.DisplayBudget
import com.ivy.data.db.dao.write.WriteBudgetDao
import com.ivy.data.model.Category
import com.ivy.data.model.Expense
import com.ivy.data.model.Income
import com.ivy.data.model.Transaction
import com.ivy.data.model.Transfer
import com.ivy.data.repository.CategoryRepository
import com.ivy.data.temp.migration.getAccountId
import com.ivy.data.temp.migration.getValue
import com.ivy.ui.ComposeViewModel
import com.ivy.data.model.Category
import com.ivy.data.repository.CategoryRepository
import com.ivy.frp.sumOfSuspend
import com.ivy.legacy.data.model.FromToTimeRange
import com.ivy.legacy.data.model.toCloseTimeRange
Expand All @@ -27,6 +28,7 @@ import com.ivy.legacy.datamodel.Budget
import com.ivy.legacy.domain.deprecated.logic.BudgetCreator
import com.ivy.legacy.utils.format
import com.ivy.legacy.utils.isNotNullOrBlank
import com.ivy.ui.ComposeViewModel
import com.ivy.ui.R
import com.ivy.wallet.domain.action.account.AccountsAct
import com.ivy.wallet.domain.action.budget.BudgetsAct
Expand Down Expand Up @@ -57,6 +59,8 @@ class BudgetViewModel @Inject constructor(
private val baseCurrencyAct: BaseCurrencyAct,
private val historyTrnsAct: HistoryTrnsAct,
private val exchangeAct: ExchangeAct,
private val timeProvider: TimeProvider,
private val timeConverter: TimeConverter,
) : ComposeViewModel<BudgetScreenState, BudgetScreenEvent>() {

private val baseCurrency = mutableStateOf("")
Expand Down Expand Up @@ -139,6 +143,7 @@ class BudgetViewModel @Inject constructor(
abs(totalRemainingBudget.doubleValue).format(baseCurrency.value),
baseCurrency.value
)

else -> null
}
}
Expand All @@ -150,13 +155,26 @@ class BudgetViewModel @Inject constructor(

override fun onEvent(event: BudgetScreenEvent) {
when (event) {
is BudgetScreenEvent.OnCreateBudget -> { createBudget(event.budgetData) }
is BudgetScreenEvent.OnEditBudget -> { editBudget(event.budget) }
is BudgetScreenEvent.OnDeleteBudget -> { deleteBudget(event.budget) }
is BudgetScreenEvent.OnReorder -> { reorder(event.newOrder) }
is BudgetScreenEvent.OnCreateBudget -> {
createBudget(event.budgetData)
}

is BudgetScreenEvent.OnEditBudget -> {
editBudget(event.budget)
}

is BudgetScreenEvent.OnDeleteBudget -> {
deleteBudget(event.budget)
}

is BudgetScreenEvent.OnReorder -> {
reorder(event.newOrder)
}

is BudgetScreenEvent.OnReorderModalVisible -> {
reorderModalVisible.value = event.visible
}

is BudgetScreenEvent.OnBudgetModalData -> {
budgetModalData.value = event.budgetModalData
}
Expand All @@ -171,7 +189,7 @@ class BudgetViewModel @Inject constructor(
val startDateOfMonth = ivyContext.initStartDayOfMonthInMemory(sharedPrefs = sharedPrefs)
val timeRange = com.ivy.legacy.data.model.TimePeriod.currentMonth(
startDayOfMonth = startDateOfMonth
).toRange(startDateOfMonth = startDateOfMonth)
).toRange(startDateOfMonth = startDateOfMonth, timeConverter, timeProvider)
val budgets = budgetsAct(Unit)

appBudgetMax.doubleValue = budgets
Expand Down Expand Up @@ -286,11 +304,11 @@ class BudgetViewModel @Inject constructor(
}
}

fun calculateTotalRemainingBudget(
budgets: ImmutableList<DisplayBudget>,
categoryBudgetsTotal: Double
): Double {
return categoryBudgetsTotal - budgets
.filter { it.budget.categoryIdsSerialized.isNotNullOrBlank() }
.sumOf { it.spentAmount }
}
fun calculateTotalRemainingBudget(
budgets: ImmutableList<DisplayBudget>,
categoryBudgetsTotal: Double
): Double {
return categoryBudgetsTotal - budgets
.filter { it.budget.categoryIdsSerialized.isNotNullOrBlank() }
.sumOf { it.spentAmount }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.viewModelScope
import com.ivy.base.legacy.SharedPrefs
import com.ivy.base.legacy.Transaction
import com.ivy.base.time.TimeConverter
import com.ivy.base.time.TimeProvider
import com.ivy.ui.ComposeViewModel
import com.ivy.data.repository.CategoryRepository
import com.ivy.domain.features.Features
Expand Down Expand Up @@ -46,6 +48,8 @@ class CategoriesViewModel @Inject constructor(
private val trnsWithRangeAndAccFiltersAct: TrnsWithRangeAndAccFiltersAct,
private val categoryIncomeWithAccountFiltersAct: LegacyCategoryIncomeWithAccountFiltersAct,
private val features: Features,
private val timeProvider: TimeProvider,
private val timeConverter: TimeConverter,
) : ComposeViewModel<CategoriesScreenState, CategoriesScreenEvent>() {

private val baseCurrency = mutableStateOf("")
Expand Down Expand Up @@ -122,7 +126,7 @@ class CategoriesViewModel @Inject constructor(
ioThread {
val range = TimePeriod.currentMonth(
startDayOfMonth = ivyContext.startDayOfMonth
).toRange(ivyContext.startDayOfMonth) // this must be monthly
).toRange(ivyContext.startDayOfMonth, timeConverter, timeProvider) // this must be monthly

allAccounts = accountsAct(Unit)
baseCurrency.value = baseCurrencyAct(Unit)
Expand Down
Loading

0 comments on commit 8ef3841

Please sign in to comment.