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:
2026-10-18 15:00:50 +02:00
parent 6159e18198
commit 8c607153c0
96 changed files with 1090 additions and 298 deletions
@@ -1,20 +1,6 @@
package se.hagstedt.kvitt.adapter.fcm
/**
* One push of payload v1 (SPEC §14.6), as the project's own record. [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).
*
* SPEC §6.2 places `PushEvent` in `:core:model`; it is not there at the M8 start, so it lives here until a contract
* change moves it (contract request in the M8 report).
*/
data class PushEvent(
val type: String,
val entityType: String,
val entityId: String,
val actorUserId: String,
val count: Int,
)
import se.hagstedt.kvitt.model.PushEvent
/** What `:app` does with FCM: handle a push, and register this instance's id (AC-13.7). Bound by `:app` through Hilt. */
interface PushListener {
@@ -3,6 +3,7 @@ package se.hagstedt.kvitt.adapter.fcm
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import se.hagstedt.kvitt.model.PushEvent
/** SPEC §14.6: the data map of payload v1 to [PushEvent]; other versions are "sync only" (null). */
class PushMappingTest {