Coverage Summary for Class: Transaction (com.georgeci.moneysurfer.domain.model)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| Transaction |
100%
(1/1)
|
100%
(1/1)
|
|
100%
(19/19)
|
100%
(149/149)
|
package com.georgeci.moneysurfer.domain.model
import com.georgeci.moneysurfer.domain.primitives.AccountId
import com.georgeci.moneysurfer.domain.primitives.CategoryId
import com.georgeci.moneysurfer.domain.primitives.CurrencyCode
import com.georgeci.moneysurfer.domain.primitives.Money
import com.georgeci.moneysurfer.domain.primitives.RecurringRuleId
import com.georgeci.moneysurfer.domain.primitives.SplitId
import com.georgeci.moneysurfer.domain.primitives.TransactionId
import com.georgeci.moneysurfer.domain.primitives.TransactionStatus
import com.georgeci.moneysurfer.domain.primitives.TransactionType
import com.georgeci.moneysurfer.domain.primitives.TransferId
import com.georgeci.moneysurfer.domain.primitives.WorkspaceId
import kotlinx.datetime.LocalDate
import kotlin.time.Instant
/**
* `operationAt` is the absolute moment of the operation (sortable). `operationDate` is
* the business date the user assigned the transaction to — what the budget treats it as.
* Storing both explicitly avoids re-deriving the date at every read site (timezone
* boundaries can flip the day) and keeps backdating intent explicit (`operationDate`
* stays as picked even if the user enters at midnight in a different zone).
*/
data class Transaction(
val id: TransactionId,
val workspaceId: WorkspaceId,
val accountId: AccountId,
val money: Money,
val currencyCode: CurrencyCode,
val categoryId: CategoryId?,
val note: String,
/**
* Who the money went to or came from ("Starbucks"), kept separate from [note] so the
* details screen can show both and search can weight them independently. Empty means
* "not recorded" — there is no null state to distinguish.
*/
val merchant: String = "",
/**
* Free-form labels, normalized through [TransactionTags.normalize] before it gets here.
* Order is the user's; duplicates and commas are already gone.
*/
val tags: List<String> = emptyList(),
/** Absolute moment. Sorting and audit only — never re-derive a calendar day from it. */
val operationAt: Instant,
/** Business date. What budgets count against and what SQL date windows filter on. */
val operationDate: LocalDate,
val type: TransactionType,
val status: TransactionStatus = TransactionStatus.ACTUAL,
// Required: technical timestamps — they intentionally differ from operationAt for
// backdated entries. createdAt is the local-row creation moment, NOT the operation
// moment. Aliasing them silently breaks (operationDate, operationAt, createdAt)
// sort and audit semantics.
val createdAt: Instant,
val updatedAt: Instant,
val transferId: TransferId? = null,
/**
* Shared id of the legs one receipt was split across — groceries and household chemicals paid
* for in one go — or null for an ordinary single-category transaction.
*
* Every leg is a whole transaction: it carries its own category and its own amount, so budgets,
* monthly totals and the account balance need no knowledge of splits at all. Only the places
* that would otherwise show one receipt as N rows — the transaction list, the recent-activity
* widget, search results — collapse a group back into a single row.
*
* The legs of a group share account, currency, business date and type; that invariant is
* established by `CreateSplitTransactionUseCase` and re-established by `UpdateTransactionUseCase`
* whenever a single leg is edited through the ordinary edit path.
*/
val splitId: SplitId? = null,
/**
* The rule that generated this row, or null for a manually entered one. A link rather
* than an `isRecurring` boolean: the schedule already lives on [RecurringRule], and a
* flag would be a second copy of that fact free to drift from it. "Recurring only"
* filters on `recurringRuleId != null`; the details screen resolves the rule to show
* its cadence.
*
* Deliberately not a Room foreign key — `recurring_rules` is not synced yet, so a pulled
* transaction can legitimately arrive before (or without) the rule it names.
*/
val recurringRuleId: RecurringRuleId? = null,
)