M12e: contract pass (pair invite kind, admin name in invite links, sync failed flag, rejection actor, device version name, template id, update check result, untouched accounts, D-027 re-stamp, PushEvent in :core:model, snackbar hosts)
Server: E-3 invite_kind; E-12 link ?from=<admin name>, E-4 passes it into kvitt://invite; X-Kvitt-App-Version-Name stored in devices.app_version_name (migration 0003, schema 3); OpenAPI and examples follow. Phone: :core:model, :core:fakes, :data, features and :app updated together; goldens re-recorded for S-20 (version name) and new S-1/S-22 states. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
@@ -9,7 +9,8 @@ data class BalanceView(val netOre: Long, val partner: User)
|
||||
|
||||
data class AccountSummary(val account: Account, val netOre: Long, val expensesThisMonth: Int)
|
||||
|
||||
data class SyncSummary(val lastSuccessAt: String?, val pendingCount: Int, val rejectedCount: Int, val clockSkew: Boolean, val running: Boolean)
|
||||
/** [lastAttemptFailed]: the last finished sync cycle did not succeed (network, server error, refused); false while one runs. */
|
||||
data class SyncSummary(val lastSuccessAt: String?, val pendingCount: Int, val rejectedCount: Int, val clockSkew: Boolean, val running: Boolean, val lastAttemptFailed: Boolean)
|
||||
|
||||
data class HomeState(val me: User, val partner: User?, val global: BalanceView?, val active: List<AccountSummary>, val archived: List<AccountSummary>, val sync: SyncSummary, val update: UpdateInfo?)
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package se.hagstedt.kvitt.model
|
||||
|
||||
// Push payload v1 as the project's own record (SPEC §6.2, §14.6); `:adapter:fcm` maps FCM data messages into it.
|
||||
|
||||
/**
|
||||
* One push of payload v1. [type] is kept as the open string the server sent so `:app` can treat an unknown type as
|
||||
* "sync only" (§14.6 phone step 4). Empty [entityType] and [entityId] for `sync_batch`. No personal content ever reaches
|
||||
* this record (AC-13.8).
|
||||
*/
|
||||
data class PushEvent(
|
||||
val type: String,
|
||||
val entityType: String,
|
||||
val entityId: String,
|
||||
val actorUserId: String,
|
||||
val count: Int,
|
||||
)
|
||||
@@ -11,8 +11,11 @@ sealed interface SessionState {
|
||||
data object Unrecognised : SessionState // 401 unauthorized: pair again, data kept (§14.4.8)
|
||||
}
|
||||
|
||||
/** A successful pairing: the paired user and the kind of the accepted invite (E-3); S-2 follows only [InviteKind.PARTNER] (AC-2.10). */
|
||||
data class Pairing(val user: User, val inviteKind: InviteKind)
|
||||
|
||||
interface SessionRepository {
|
||||
val session: Flow<SessionState>
|
||||
suspend fun pair(serverUrl: String, code: String, displayName: String?, deviceName: String): OnlineResult<User>
|
||||
suspend fun pair(serverUrl: String, code: String, displayName: String?, deviceName: String): OnlineResult<Pairing>
|
||||
suspend fun acknowledgeRevoked() // wipes local data (AC-3.3) and moves to Unpaired
|
||||
}
|
||||
|
||||
@@ -29,14 +29,18 @@ interface CategoryRepository {
|
||||
|
||||
data class RecurringTemplate(val id: TemplateId, val draft: ExpenseDraft, val dayOfMonth: Int, val startPeriod: String, val endPeriod: String?, val paused: Boolean, val nextOccurrence: String?)
|
||||
|
||||
/** A template to create: the repository (the server, E-28) mints the id; the next occurrence comes back through sync. */
|
||||
data class NewRecurringTemplate(val draft: ExpenseDraft, val dayOfMonth: Int, val startPeriod: String, val endPeriod: String?, val paused: Boolean)
|
||||
|
||||
interface RecurringRepository {
|
||||
fun all(): Flow<List<RecurringTemplate>>
|
||||
suspend fun create(t: RecurringTemplate): OnlineResult<TemplateId>
|
||||
suspend fun create(t: NewRecurringTemplate): OnlineResult<TemplateId>
|
||||
suspend fun update(t: RecurringTemplate): OnlineResult<Unit>
|
||||
suspend fun delete(id: TemplateId): OnlineResult<Unit>
|
||||
}
|
||||
|
||||
data class Device(val id: DeviceId, val user: User, val name: String, val pairedAt: String, val lastSeenAt: String, val appVersionCode: Int?, val current: Boolean, val revoked: Boolean)
|
||||
/** [appVersionName]: the `versionName` the device last reported ("1.1"), null until it has (schema 3). */
|
||||
data class Device(val id: DeviceId, val user: User, val name: String, val pairedAt: String, val lastSeenAt: String, val appVersionCode: Int?, val appVersionName: String?, val current: Boolean, val revoked: Boolean)
|
||||
|
||||
interface DeviceRepository {
|
||||
suspend fun list(): OnlineResult<List<Device>>
|
||||
|
||||
@@ -6,7 +6,14 @@ import kotlinx.coroutines.flow.Flow
|
||||
|
||||
data class AllocationLine(val account: Account, val payerNetOre: Long, val amountOre: Long, val overSettles: Boolean)
|
||||
|
||||
data class SettleState(val payer: User, val recipient: User, val amountOre: Long?, val lines: List<AllocationLine>, val remainingOre: Long, val canConfirm: Boolean, val date: String, val note: String)
|
||||
/** An account the settlement leaves as it is although the payer is owed there ([payerNetOre] > 0, §9.4 info line). */
|
||||
data class UntouchedAccount(val account: Account, val payerNetOre: Long)
|
||||
|
||||
/**
|
||||
* [untouched]: accounts not in [lines] where the payer is owed, nets without the settlement being edited (AC-11.6),
|
||||
* in `AccountRepository.all()` order; empty for a per-account settle-up (D-074).
|
||||
*/
|
||||
data class SettleState(val payer: User, val recipient: User, val amountOre: Long?, val lines: List<AllocationLine>, val remainingOre: Long, val canConfirm: Boolean, val date: String, val note: String, val untouched: List<UntouchedAccount>)
|
||||
|
||||
data class SettleRequest(val editing: SettlementId?, val payer: UserId, val amountOre: Long?, val fixedAccount: AccountId?, val allocations: Map<AccountId, Long>?, val date: String, val note: String)
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@ import kotlinx.coroutines.flow.Flow
|
||||
|
||||
// Sync status, update, about and connectivity (S-16, S-17, S-22, §9.9), SPEC §14.2.4.
|
||||
|
||||
data class Rejection(val mutationId: String, val entityType: EntityType, val entityId: String, val reason: SyncRejectionReason, val summary: String, val rejectedAt: String)
|
||||
/**
|
||||
* [actor]: who made the server's current version (its `updated_by_user_id`, the "<actor>" of S-16's stale copy, §14.4.6);
|
||||
* null when the server had no row or the user is unknown.
|
||||
*/
|
||||
data class Rejection(val mutationId: String, val entityType: EntityType, val entityId: String, val reason: SyncRejectionReason, val summary: String, val rejectedAt: String, val actor: User?)
|
||||
|
||||
data class SyncStatusState(val summary: SyncSummary, val pending: List<LedgerRow>, val rejections: List<Rejection>)
|
||||
|
||||
@@ -24,9 +28,20 @@ sealed interface UpdateState {
|
||||
data class NeedsInstallPermission(val info: UpdateInfo) : UpdateState
|
||||
}
|
||||
|
||||
/** The outcome of [UpdateRepository.check] (S-22, AC-16.6). */
|
||||
sealed interface UpdateCheck {
|
||||
/** No newer APK is published (or nothing is published); debug builds, which never check (AC-16.5), report this too. */
|
||||
data object UpToDate : UpdateCheck
|
||||
|
||||
data class Available(val info: UpdateInfo) : UpdateCheck
|
||||
|
||||
/** The server could not be reached or answered with an error; [UpdateRepository.state] is unchanged. */
|
||||
data object Failed : UpdateCheck
|
||||
}
|
||||
|
||||
interface UpdateRepository {
|
||||
val state: Flow<UpdateState>
|
||||
suspend fun check()
|
||||
suspend fun check(): UpdateCheck
|
||||
suspend fun install()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user