M7d: feature settle, S-9 settle up and S-10 settlement detail
SettleScreen (global, per account, editing) and SettlementDetailScreen with Hilt ViewModels over the :core:model interfaces; proposals, remaining, warnings and enablement come from SettlementRepository. Fake scenarios in fakes/settle/, 19 states x sv/en x light/dark goldens (+ 200 % for long), unit tests for amount input and both state holders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
package se.hagstedt.kvitt.fakes.settle
|
||||
|
||||
import se.hagstedt.kvitt.fakes.FakeExpense
|
||||
import se.hagstedt.kvitt.fakes.FakeScenario
|
||||
import se.hagstedt.kvitt.fakes.FakeScenarios
|
||||
import se.hagstedt.kvitt.fakes.FakeSettlement
|
||||
import se.hagstedt.kvitt.model.Account
|
||||
import se.hagstedt.kvitt.model.AccountId
|
||||
import se.hagstedt.kvitt.model.AuditAction
|
||||
import se.hagstedt.kvitt.model.AuditSource
|
||||
import se.hagstedt.kvitt.model.ExpenseId
|
||||
import se.hagstedt.kvitt.model.FieldChange
|
||||
import se.hagstedt.kvitt.model.SettlementId
|
||||
import se.hagstedt.kvitt.model.Share
|
||||
import se.hagstedt.kvitt.model.SplitMode
|
||||
import se.hagstedt.kvitt.model.TimelineLine
|
||||
import se.hagstedt.kvitt.model.User
|
||||
import se.hagstedt.kvitt.model.UserId
|
||||
|
||||
/**
|
||||
* Scenarios of `:feature:settle` (S-9 settle up, S-10 settlement detail; PLAN-M7d §5). Every balance comes from the
|
||||
* scenario's expenses and settlements through the fakes' §9.3 rules, so the proposals are the real §9.4 ones.
|
||||
*
|
||||
* The base household (the design-spec frames): Jacob (the viewer) owes 3 084,50 kr in Huset and 1 350,00 kr in
|
||||
* Japan 2027; Anna owes 1 184,50 kr in Vardag; the archived account Stugan is settled. Globally Jacob owes 3 250,00 kr.
|
||||
*/
|
||||
object SettleScenarios {
|
||||
private val jacob: User = FakeScenarios.owner
|
||||
private val anna: User = FakeScenarios.partner
|
||||
|
||||
val vardag = account(0xb1, "Vardag", "2025-01-10T08:00:00.000Z")
|
||||
val huset = account(0xb2, "Huset", "2025-02-01T08:00:00.000Z")
|
||||
val japan = account(0xb3, "Japan 2027", "2026-03-15T08:00:00.000Z")
|
||||
val stugan = account(0xb4, "Stugan", "2024-06-01T08:00:00.000Z", archived = true)
|
||||
|
||||
/** Anna → Jacob 800,00 kr on 6 October, all to Vardag, note "Swish", created by Anna (frame S-10-settlement). */
|
||||
val swish = SettlementId(id(0xc1))
|
||||
|
||||
/** Jacob → Anna 2 500,00 kr over Huset and Japan 2027, no note, created, changed, and a pending local change. */
|
||||
val split = SettlementId(id(0xc2))
|
||||
|
||||
/** Anna → Jacob 450,00 kr, deleted by Anna on 17 October (frame S-10-settlement-deleted). */
|
||||
val deleted = SettlementId(id(0xc3))
|
||||
|
||||
/** Jacob → Anna 1 234 567,89 kr over six long-named accounts (long content). */
|
||||
val long = SettlementId(id(0xc4))
|
||||
|
||||
/** The settlement id that no scenario holds (the missing state). */
|
||||
val missing = SettlementId(id(0xcf))
|
||||
|
||||
/** S-9 global, loaded: the proposal Huset 3 084,50 + Japan 2027 165,50 and the info line about Vardag. */
|
||||
val proposal: FakeScenario = FakeScenarios.empty.copy(
|
||||
accounts = listOf(stugan, vardag, huset, japan),
|
||||
expenses = listOf(
|
||||
expense(0xe1, huset, anna, 616_900, "2026-10-02"),
|
||||
expense(0xe2, japan, anna, 270_000, "2026-10-05"),
|
||||
expense(0xe3, vardag, jacob, 236_900, "2026-10-09"),
|
||||
),
|
||||
lastSuccessAt = "2026-10-17T11:59:00.000Z",
|
||||
today = "2026-10-17",
|
||||
now = "2026-10-17T12:02:00.000Z",
|
||||
)
|
||||
|
||||
/** S-9 global with a zero balance: the amount is empty and no rows are proposed. */
|
||||
val settledUp: FakeScenario = proposal.copy(
|
||||
expenses = proposal.expenses + listOf(expense(0xe4, vardag, jacob, 650_000, "2026-10-12")),
|
||||
)
|
||||
|
||||
/** S-9 with the partner owing globally: the direction defaults to "Anna betalar dig". */
|
||||
val partnerOwes: FakeScenario = proposal.copy(
|
||||
expenses = listOf(
|
||||
expense(0xe1, huset, jacob, 616_900, "2026-10-02"),
|
||||
expense(0xe2, japan, jacob, 270_000, "2026-10-05"),
|
||||
expense(0xe3, vardag, anna, 236_900, "2026-10-09"),
|
||||
),
|
||||
)
|
||||
|
||||
/** S-9 offline with pending changes: settling up works unchanged (AC-11.6). */
|
||||
val offline: FakeScenario = proposal.copy(
|
||||
online = false,
|
||||
expenses = proposal.expenses.map { if (it.id.value == id(0xe3)) it.copy(pending = true) else it },
|
||||
lastSuccessAt = "2026-10-17T09:30:00.000Z",
|
||||
)
|
||||
|
||||
/** S-10 and S-9 editing: the four settlements above on top of [proposal]. */
|
||||
val detail: FakeScenario = proposal.copy(
|
||||
settlements = listOf(
|
||||
FakeSettlement(
|
||||
id = swish, from = anna.id, to = jacob.id, amountOre = 80_000, date = "2026-10-06", note = "Swish",
|
||||
allocations = mapOf(vardag.id to 80_000L), createdAt = "2026-10-06T19:15:00.000Z",
|
||||
timeline = listOf(TimelineLine(anna, AuditAction.CREATE, AuditSource.APP, "2026-10-06T19:15:00.000Z", emptyList(), pendingLocal = false)),
|
||||
),
|
||||
FakeSettlement(
|
||||
id = split, from = jacob.id, to = anna.id, amountOre = 250_000, date = "2026-10-14", note = "",
|
||||
allocations = linkedMapOf(huset.id to 200_000L, japan.id to 50_000L), createdAt = "2026-10-14T07:40:00.000Z",
|
||||
pending = true,
|
||||
timeline = listOf(
|
||||
TimelineLine(jacob, AuditAction.CREATE, AuditSource.APP, "2026-10-14T07:40:00.000Z", emptyList(), pendingLocal = false),
|
||||
TimelineLine(
|
||||
anna, AuditAction.UPDATE, AuditSource.APP, "2026-10-15T16:05:00.000Z",
|
||||
listOf(FieldChange("amount_ore", "200000", "250000"), FieldChange("settlement_date", "2026-10-13", "2026-10-14")),
|
||||
pendingLocal = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
FakeSettlement(
|
||||
id = deleted, from = anna.id, to = jacob.id, amountOre = 45_000, date = "2026-10-11", note = "Swish för hyran",
|
||||
allocations = mapOf(vardag.id to 45_000L), createdAt = "2026-10-11T10:20:00.000Z", deleted = true,
|
||||
timeline = listOf(
|
||||
TimelineLine(anna, AuditAction.CREATE, AuditSource.APP, "2026-10-11T10:20:00.000Z", emptyList(), pendingLocal = false),
|
||||
TimelineLine(anna, AuditAction.DELETE, AuditSource.APP, "2026-10-17T07:12:00.000Z", emptyList(), pendingLocal = false),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
/** S-10 offline: the detail of [split] with the phone offline. */
|
||||
val detailOffline: FakeScenario = detail.copy(online = false, lastSuccessAt = "2026-10-17T09:30:00.000Z")
|
||||
|
||||
private val longAccounts: List<Account> = listOf(
|
||||
account(0xd1, "Sommarhuset i Rättvik med bryggbygget", "2025-01-02T08:00:00.000Z"),
|
||||
account(0xd2, "Gemensamma hushållsutgifter och matinköp", "2025-01-03T08:00:00.000Z"),
|
||||
account(0xd3, "Bilen, försäkringarna och parkeringstillstånden", "2025-01-04T08:00:00.000Z"),
|
||||
account(0xd4, "Renoveringen av badrummet på övervåningen", "2025-01-05T08:00:00.000Z"),
|
||||
account(0xd5, "Bröllopsresan till Nya Zeeland och Australien", "2025-01-06T08:00:00.000Z"),
|
||||
account(0xd6, "Barnens fritidsaktiviteter och utrustning", "2025-01-07T08:00:00.000Z"),
|
||||
)
|
||||
|
||||
/** Long content (S-9): six long-named accounts where Jacob owes 7-digit amounts (1 235 677,89 kr globally). */
|
||||
val longContent: FakeScenario = FakeScenarios.empty.copy(
|
||||
accounts = longAccounts,
|
||||
expenses = listOf(
|
||||
expense(0xf1, longAccounts[0], anna, 104_691_356, "2026-10-01"),
|
||||
expense(0xf2, longAccounts[1], anna, 82_469_134, "2026-10-02"),
|
||||
expense(0xf3, longAccounts[2], anna, 37_530_864, "2026-10-03"),
|
||||
expense(0xf4, longAccounts[3], anna, 24_691_356, "2026-10-04"),
|
||||
expense(0xf5, longAccounts[4], anna, 6_913_578, "2026-10-05"),
|
||||
expense(0xf6, longAccounts[5], jacob, 9_160_710, "2026-10-06"),
|
||||
),
|
||||
lastSuccessAt = "2026-10-17T11:59:00.000Z",
|
||||
today = "2026-10-17",
|
||||
now = "2026-10-17T12:02:00.000Z",
|
||||
)
|
||||
|
||||
/** Long content (S-10): Jacob → Anna 1 234 567,89 kr over the six long-named accounts, with a long note. */
|
||||
val longDetail: FakeScenario = longContent.copy(
|
||||
settlements = listOf(
|
||||
FakeSettlement(
|
||||
id = long, from = jacob.id, to = anna.id, amountOre = 123_456_789, date = "2026-09-30",
|
||||
note = "Överföring från sparkontot inför flytten, enligt överenskommelsen vi gjorde på midsommar",
|
||||
allocations = linkedMapOf(
|
||||
longAccounts[0].id to 50_000_000L,
|
||||
longAccounts[1].id to 40_000_000L,
|
||||
longAccounts[2].id to 15_000_000L,
|
||||
longAccounts[3].id to 10_000_000L,
|
||||
longAccounts[4].id to 5_000_000L,
|
||||
longAccounts[5].id to 3_456_789L,
|
||||
),
|
||||
createdAt = "2026-09-30T18:00:00.000Z",
|
||||
timeline = listOf(TimelineLine(jacob, AuditAction.CREATE, AuditSource.APP, "2026-09-30T18:00:00.000Z", emptyList(), pendingLocal = false)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
/** The long scenario's accounts in created order. */
|
||||
val longAccountIds: List<AccountId> = longAccounts.map { it.id }
|
||||
|
||||
private fun id(suffix: Int): String = "01920000-0000-7000-8000-" + suffix.toString(16).padStart(12, '0')
|
||||
|
||||
private fun account(suffix: Int, name: String, createdAt: String, archived: Boolean = false): Account =
|
||||
Account(AccountId(id(suffix)), name, mapOf(jacob.id to 5000, anna.id to 5000), createdAt, archived)
|
||||
|
||||
/** An expense split 50/50 that [payer] paid: the other person owes half of it in [account]. */
|
||||
private fun expense(suffix: Int, account: Account, payer: User, amountOre: Long, date: String): FakeExpense {
|
||||
val half = amountOre / 2
|
||||
val shares = listOf(jacob.id, anna.id).map { user: UserId -> Share(user, if (user == payer.id) amountOre - half else half, 5000) }
|
||||
return FakeExpense(
|
||||
id = ExpenseId(id(suffix)),
|
||||
accountId = account.id,
|
||||
categoryId = FakeScenarios.defaultCategories.first().id,
|
||||
amountOre = amountOre,
|
||||
description = "Inköp",
|
||||
notes = "",
|
||||
date = date,
|
||||
payer = payer.id,
|
||||
mode = SplitMode.DEFAULT,
|
||||
shares = shares,
|
||||
createdAt = "${date}T10:00:00.000Z",
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user