chore(dates): remove the dead date_plan_preferences plumbing (Batch F)
Dead code isn't inert — this carried a live-looking E2EE encryptor and a rules block, inviting the next engineer to extend a surface nothing reads (which is exactly how N-002 happened: writes into a collection with no reader). Removed, all verified caller-dead first: the Firestore preference methods + mapper on FirestoreDatePlanDataSource, the repository preference surface (interface + impl + the assemblePlanSuggestion placeholder, whose body was literally `val x = /* comment */` swallowing a return — dead AND weird), the DatePlanPreference/DatePlanSuggestion domain models, DatePlanPreferenceDao + its DI provider + the AppDatabase accessor, and the firestore.rules block (collection is now default-deny, strictly safer; stray pre-R15 docs are orphaned — launch-checklist note in Future.md). DateBuilderViewModel's vestigial savePreference() renamed createPlan() — it has created real PLANNED plans since the N-002 fix. The one deliberate KEEP: DatePlanPreferenceEntity stays registered in AppDatabase. Room's identity hash is computed from the SCHEMA, and the DB ships via createFromAsset with no migrations — dropping the entity changes the hash and crashes every install at first DB open. The entity now carries a loud KDoc saying exactly that (and the AppDatabase comment points at it), so nobody "cleans it up" without regenerating the asset DB. Removing DAOs/methods is hash-neutral; proven live: fresh install on-device, DB opens clean, DB-served content renders. Also repaired collateral from my own removal script: it over-cut plansRef (caught by compile, restored verbatim) — and the repo impl no longer injects the same datasource twice under two names. Live post-removal: Create Plan saves without error or PERMISSION_DENIED (the surviving savePlan path). Caveat, stated honestly: the Home "Date coming up" tile wasn't re-observed because the Compose date picker resists uiautomator (cells expose content-desc only) and a today-dated plan is excluded by design (scheduledDate > now) — the tile logic is untouched by this batch and was live-verified in R15. Full unit suite green, assembleDebug clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7341f64ae2
commit
bc3ad2650b
|
|
@ -284,6 +284,14 @@ no `auth.user().onDelete`). Deploy-day learnings + deferred items:
|
||||||
|
|
||||||
## Roadmap — features & strategy (consolidated from the old `FUTURE.md`, 2026-06-28)
|
## Roadmap — features & strategy (consolidated from the old `FUTURE.md`, 2026-06-28)
|
||||||
|
|
||||||
|
- **Two-sided date planning (deliberate not-now, 2026-07-16).** The original Date Builder vision — each
|
||||||
|
partner records preferences (time/budget/duration) and the app assembles a plan from both — was never
|
||||||
|
built past a write-only stub (N-002). The dead plumbing (`date_plan_preferences` datasource/DAO/rules,
|
||||||
|
the placeholder `assemblePlanSuggestion`) was REMOVED in Batch F; the Room table `date_plan_preferences`
|
||||||
|
remains schema-frozen in the asset DB (identity hash) but has no reader/writer. If this returns, design
|
||||||
|
it fresh: both-partner input, a real merge, and a reader surface — don't resurrect the old plumbing.
|
||||||
|
Launch checklist: one admin sweep for stray pre-R15 `date_plan_preferences` docs (now default-deny).
|
||||||
|
|
||||||
**Unbuilt games (Play Hub is live; these remain):**
|
**Unbuilt games (Play Hub is live; these remain):**
|
||||||
- **Would You Rather / Truth or Dare** — tiered sweet→spicy, consent-gated; reuses the deck + match engine. Strong **premium "spicy" tier** lever.
|
- **Would You Rather / Truth or Dare** — tiered sweet→spicy, consent-gated; reuses the deck + match engine. Strong **premium "spicy" tier** lever.
|
||||||
- **Daily Sync / Rose-Bud-Thorn** — one-tap emotional check-in, see partner's; small/new, free retention driver.
|
- **Daily Sync / Rose-Bud-Thorn** — one-tap emotional check-in, see partner's; small/new, free retention driver.
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import app.closer.data.local.entity.QuestionEntity
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun questionDao(): QuestionDao
|
abstract fun questionDao(): QuestionDao
|
||||||
abstract fun categoryDao(): CategoryDao
|
abstract fun categoryDao(): CategoryDao
|
||||||
abstract fun datePlanPreferenceDao(): DatePlanPreferenceDao
|
// No DAO for DatePlanPreferenceEntity: the table is schema-frozen dead weight (see its KDoc) —
|
||||||
|
// it stays in `entities` only because removing it changes the Room identity hash and breaks
|
||||||
|
// the createFromAsset DB at first open.
|
||||||
abstract fun datePlanDao(): DatePlanDao
|
abstract fun datePlanDao(): DatePlanDao
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
package app.closer.data.local
|
|
||||||
|
|
||||||
import androidx.room.Dao
|
|
||||||
import androidx.room.Delete
|
|
||||||
import androidx.room.Insert
|
|
||||||
import androidx.room.OnConflictStrategy
|
|
||||||
import androidx.room.Query
|
|
||||||
import app.closer.data.local.entity.DatePlanPreferenceEntity
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
interface DatePlanPreferenceDao {
|
|
||||||
@Query("SELECT * FROM date_plan_preferences WHERE id = :id LIMIT 1")
|
|
||||||
suspend fun getById(id: String): DatePlanPreferenceEntity?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM date_plan_preferences WHERE couple_id = :coupleId ORDER BY updated_at DESC")
|
|
||||||
suspend fun getByCoupleId(coupleId: String): List<DatePlanPreferenceEntity>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM date_plan_preferences WHERE couple_id = :coupleId AND date_idea_id = :dateIdeaId LIMIT 1")
|
|
||||||
suspend fun getPreference(coupleId: String, dateIdeaId: String): DatePlanPreferenceEntity?
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
suspend fun insert(preference: DatePlanPreferenceEntity)
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
suspend fun insertAll(pREFERENCES: List<DatePlanPreferenceEntity>)
|
|
||||||
|
|
||||||
@Delete
|
|
||||||
suspend fun delete(preference: DatePlanPreferenceEntity)
|
|
||||||
|
|
||||||
@Query("DELETE FROM date_plan_preferences WHERE couple_id = :coupleId")
|
|
||||||
suspend fun deleteByCoupleId(coupleId: String)
|
|
||||||
}
|
|
||||||
|
|
@ -5,12 +5,15 @@ import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Room entity for partner date plan preferences.
|
* ⚠️ SCHEMA-FROZEN DEAD WEIGHT — DO NOT REMOVE (and do not resurrect).
|
||||||
*
|
*
|
||||||
* Stored locally in Room. Synced to Firestore when explicitly shared/scheduled.
|
* Nothing reads or writes this table: the preference feature it served was removed as dead code
|
||||||
* Both partners contribute preferences; the Builder uses them to assemble a plan.
|
* (its DAO, repository methods, Firestore datasource methods and rules block are gone). The entity
|
||||||
*
|
* itself must stay registered in [app.closer.data.local.AppDatabase] because Room's identity hash is
|
||||||
* Table: date_plan_preferences
|
* computed from the SCHEMA, and the database ships via createFromAsset with no migrations — removing
|
||||||
|
* this entity changes the hash and crashes every install at first DB open until the asset DB is
|
||||||
|
* regenerated (seed/build_db.py) and re-verified. If the two-sided preferences vision ever returns,
|
||||||
|
* design it fresh (see Future.md) rather than re-wiring this table.
|
||||||
*/
|
*/
|
||||||
@Entity(tableName = "date_plan_preferences")
|
@Entity(tableName = "date_plan_preferences")
|
||||||
data class DatePlanPreferenceEntity(
|
data class DatePlanPreferenceEntity(
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package app.closer.data.remote
|
||||||
import app.closer.crypto.CoupleEncryptionManager
|
import app.closer.crypto.CoupleEncryptionManager
|
||||||
import app.closer.crypto.FieldEncryptor
|
import app.closer.crypto.FieldEncryptor
|
||||||
import app.closer.domain.model.DatePlan
|
import app.closer.domain.model.DatePlan
|
||||||
import app.closer.domain.model.DatePlanPreference
|
|
||||||
import app.closer.domain.model.DatePlanStatus
|
import app.closer.domain.model.DatePlanStatus
|
||||||
import com.google.firebase.firestore.FirebaseFirestore
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
import com.google.firebase.firestore.SetOptions
|
import com.google.firebase.firestore.SetOptions
|
||||||
|
|
@ -19,11 +18,9 @@ import javax.inject.Singleton
|
||||||
/**
|
/**
|
||||||
* Firestore data source for date plan preferences and plans.
|
* Firestore data source for date plan preferences and plans.
|
||||||
*
|
*
|
||||||
* Path layout:
|
* Path layout: couples/{coupleId}/date_plans/{planId} — plans, synced when explicitly
|
||||||
* - couples/{coupleId}/date_plan_preferences/{userId} — per-partner preferences
|
* shared/scheduled. (The old per-partner date_plan_preferences surface was removed as dead code;
|
||||||
* - couples/{coupleId}/date_plans/{planId} — complete plans generated by the builder
|
* the collection is default-deny in rules and no longer exists in code.)
|
||||||
*
|
|
||||||
* Local-first: Preferences stored in Room. Plans synced to Firestore when explicitly shared/scheduled.
|
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class FirestoreDatePlanDataSource @Inject constructor(
|
class FirestoreDatePlanDataSource @Inject constructor(
|
||||||
|
|
@ -31,53 +28,10 @@ class FirestoreDatePlanDataSource @Inject constructor(
|
||||||
private val encryptionManager: CoupleEncryptionManager,
|
private val encryptionManager: CoupleEncryptionManager,
|
||||||
private val fieldEncryptor: FieldEncryptor
|
private val fieldEncryptor: FieldEncryptor
|
||||||
) {
|
) {
|
||||||
private fun preferencesRef(coupleId: String) =
|
|
||||||
db.collection(FirestoreCollections.COUPLES).document(coupleId)
|
|
||||||
.collection(FirestoreCollections.Couples.DATE_PLAN_PREFERENCES)
|
|
||||||
|
|
||||||
private fun plansRef(coupleId: String) =
|
private fun plansRef(coupleId: String) =
|
||||||
db.collection(FirestoreCollections.COUPLES).document(coupleId)
|
db.collection(FirestoreCollections.COUPLES).document(coupleId)
|
||||||
.collection(FirestoreCollections.Couples.DATE_PLANS)
|
.collection(FirestoreCollections.Couples.DATE_PLANS)
|
||||||
|
|
||||||
// ─── Preference methods ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
suspend fun recordPreference(coupleId: String, preference: DatePlanPreference) {
|
|
||||||
// Strict E2EE: user-entered details are encrypted. dateIdeaId stays plaintext
|
|
||||||
// (queried via whereEqualTo); dates/timestamps stay plaintext (structural).
|
|
||||||
val aead = encryptionManager.requireAead(coupleId)
|
|
||||||
val path = preferencesRef(coupleId).document()
|
|
||||||
val data = mapOf(
|
|
||||||
"dateIdeaId" to preference.dateIdeaId,
|
|
||||||
"preferredDate" to preference.preferredDate,
|
|
||||||
"preferredTime" to fieldEncryptor.encrypt(preference.preferredTime, aead, coupleId),
|
|
||||||
"budget" to fieldEncryptor.encrypt(preference.budget.toString(), aead, coupleId),
|
|
||||||
"duration" to fieldEncryptor.encrypt(preference.duration, aead, coupleId),
|
|
||||||
"createdAt" to preference.createdAt,
|
|
||||||
"updatedAt" to preference.updatedAt
|
|
||||||
)
|
|
||||||
path.set(data, SetOptions.merge()).await()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getPreference(coupleId: String, dateIdeaId: String): DatePlanPreference? {
|
|
||||||
// Query for preferences on this date idea
|
|
||||||
val snap = preferencesRef(coupleId)
|
|
||||||
.whereEqualTo("dateIdeaId", dateIdeaId)
|
|
||||||
.limit(1)
|
|
||||||
.get().await()
|
|
||||||
return snap.documents.firstOrNull()?.toDatePlanPreference(coupleId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun observePreferences(coupleId: String): Flow<List<DatePlanPreference>> {
|
|
||||||
return callbackFlow {
|
|
||||||
val listener = preferencesRef(coupleId)
|
|
||||||
.addSnapshotListener { snap, err ->
|
|
||||||
if (err != null || snap == null) return@addSnapshotListener
|
|
||||||
trySend(snap.documents.mapNotNull { it.toDatePlanPreference(coupleId) })
|
|
||||||
}
|
|
||||||
awaitClose { listener.remove() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Plan methods ────────────────────────────────────────────────────────
|
// ─── Plan methods ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
suspend fun createPlan(coupleId: String, plan: DatePlan): String {
|
suspend fun createPlan(coupleId: String, plan: DatePlan): String {
|
||||||
|
|
@ -145,21 +99,6 @@ class FirestoreDatePlanDataSource @Inject constructor(
|
||||||
|
|
||||||
// ─── Mappers ─────────────────────────────────────────────────────────────
|
// ─── Mappers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun com.google.firebase.firestore.DocumentSnapshot.toDatePlanPreference(coupleId: String): DatePlanPreference? {
|
|
||||||
val dateIdeaId = getString("dateIdeaId") ?: return null
|
|
||||||
val aead = encryptionManager.aeadFor(coupleId)
|
|
||||||
return DatePlanPreference(
|
|
||||||
id = id,
|
|
||||||
coupleId = coupleId,
|
|
||||||
dateIdeaId = dateIdeaId,
|
|
||||||
preferredDate = (get("preferredDate") as? Number)?.toLong() ?: 0L,
|
|
||||||
preferredTime = fieldEncryptor.decryptForDisplay(getString("preferredTime"), aead, coupleId) ?: "",
|
|
||||||
budget = fieldEncryptor.decrypt(getString("budget"), aead, coupleId)?.toIntOrNull() ?: 0,
|
|
||||||
duration = fieldEncryptor.decryptForDisplay(getString("duration"), aead, coupleId) ?: "",
|
|
||||||
createdAt = (get("createdAt") as? Number)?.toLong() ?: 0L,
|
|
||||||
updatedAt = (get("updatedAt") as? Number)?.toLong() ?: 0L
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun com.google.firebase.firestore.DocumentSnapshot.toDatePlan(coupleId: String): DatePlan? {
|
private fun com.google.firebase.firestore.DocumentSnapshot.toDatePlan(coupleId: String): DatePlan? {
|
||||||
val dateIdeaId = getString("dateIdeaId") ?: return null
|
val dateIdeaId = getString("dateIdeaId") ?: return null
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
package app.closer.data.repository
|
package app.closer.data.repository
|
||||||
|
|
||||||
import app.closer.data.local.DatePlanDao
|
import app.closer.data.local.DatePlanDao
|
||||||
import app.closer.data.local.DatePlanPreferenceDao
|
|
||||||
import app.closer.data.local.entity.DatePlanEntity
|
import app.closer.data.local.entity.DatePlanEntity
|
||||||
import app.closer.data.local.entity.DatePlanPreferenceEntity
|
|
||||||
import app.closer.data.remote.FirestoreDatePlanDataSource
|
import app.closer.data.remote.FirestoreDatePlanDataSource
|
||||||
import app.closer.domain.model.DatePlan
|
import app.closer.domain.model.DatePlan
|
||||||
import app.closer.domain.model.DatePlanPreference
|
|
||||||
import app.closer.domain.model.DatePlanSuggestion
|
|
||||||
import app.closer.domain.model.DatePlanStatus
|
import app.closer.domain.model.DatePlanStatus
|
||||||
import app.closer.domain.repository.DatePlanRepository
|
import app.closer.domain.repository.DatePlanRepository
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
@ -17,44 +13,15 @@ import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of [DatePlanRepository].
|
* Implementation of [DatePlanRepository]: local-first plans (Room) synced to Firestore when
|
||||||
*
|
* explicitly shared/scheduled.
|
||||||
* Local-first: Preferences stored in Room. Plans synced to Firestore when
|
|
||||||
* explicitly shared/scheduled. Both partners contribute preferences; the
|
|
||||||
* Builder assembles complete plans from those preferences.
|
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class DatePlanRepositoryImpl @Inject constructor(
|
class DatePlanRepositoryImpl @Inject constructor(
|
||||||
private val preferenceDataSource: FirestoreDatePlanDataSource,
|
|
||||||
private val planDataSource: FirestoreDatePlanDataSource,
|
private val planDataSource: FirestoreDatePlanDataSource,
|
||||||
private val preferenceDao: DatePlanPreferenceDao,
|
|
||||||
private val planDao: DatePlanDao
|
private val planDao: DatePlanDao
|
||||||
) : DatePlanRepository {
|
) : DatePlanRepository {
|
||||||
|
|
||||||
// ─── Preference methods ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
override suspend fun getPreference(coupleId: String, dateIdeaId: String): DatePlanPreference? {
|
|
||||||
// First check local cache
|
|
||||||
val local = preferenceDao.getPreference(coupleId, dateIdeaId)
|
|
||||||
if (local != null) return local.toDomain()
|
|
||||||
|
|
||||||
// Fall back to Firestore
|
|
||||||
return preferenceDataSource.getPreference(coupleId, dateIdeaId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun savePreference(preference: DatePlanPreference) {
|
|
||||||
// Store in Room for local-first access
|
|
||||||
val entity = preference.toEntity()
|
|
||||||
preferenceDao.insert(entity)
|
|
||||||
|
|
||||||
// Sync to Firestore
|
|
||||||
preferenceDataSource.recordPreference(preference.coupleId, preference)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun observePreferences(coupleId: String): Flow<List<DatePlanPreference>> {
|
|
||||||
return preferenceDataSource.observePreferences(coupleId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Plan methods ────────────────────────────────────────────────────────
|
// ─── Plan methods ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
override suspend fun getPlan(planId: String): DatePlan? {
|
override suspend fun getPlan(planId: String): DatePlan? {
|
||||||
|
|
@ -113,26 +80,6 @@ class DatePlanRepositoryImpl @Inject constructor(
|
||||||
// In a full implementation, we would fetch the plan's coupleId first
|
// In a full implementation, we would fetch the plan's coupleId first
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Builder methods ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
override suspend fun assemblePlanSuggestion(
|
|
||||||
coupleId: String,
|
|
||||||
dateIdeaId: String
|
|
||||||
): DatePlanSuggestion? {
|
|
||||||
// Get preferences from both partners for this date idea
|
|
||||||
val partnerAPreference = preferenceDao.getPreference(coupleId, dateIdeaId)
|
|
||||||
val partnerBPreference = /* Get the other partner's preference */
|
|
||||||
|
|
||||||
// For now, return a placeholder suggestion
|
|
||||||
// In a full implementation, this would merge preferences from both partners
|
|
||||||
return DatePlanSuggestion(
|
|
||||||
activity = "Based on the selected date idea",
|
|
||||||
food = "Suggested dining option based on budget",
|
|
||||||
conversationPrompts = listOf("What made this date special?"),
|
|
||||||
optionalChallenge = "Try something new together"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Sanitization ────────────────────────────────────────────────────────
|
// ─── Sanitization ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun DatePlan.sanitized(): DatePlan = copy(
|
private fun DatePlan.sanitized(): DatePlan = copy(
|
||||||
|
|
@ -156,30 +103,6 @@ class DatePlanRepositoryImpl @Inject constructor(
|
||||||
const val MAX_PROMPTS = 20
|
const val MAX_PROMPTS = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun DatePlanPreferenceEntity.toDomain(): DatePlanPreference = DatePlanPreference(
|
|
||||||
id = id,
|
|
||||||
coupleId = coupleId,
|
|
||||||
dateIdeaId = dateIdeaId,
|
|
||||||
preferredDate = preferredDate,
|
|
||||||
preferredTime = preferredTime,
|
|
||||||
budget = budget,
|
|
||||||
duration = duration,
|
|
||||||
createdAt = createdAt,
|
|
||||||
updatedAt = updatedAt
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun DatePlanPreference.toEntity(): DatePlanPreferenceEntity = DatePlanPreferenceEntity(
|
|
||||||
id = id,
|
|
||||||
coupleId = coupleId,
|
|
||||||
dateIdeaId = dateIdeaId,
|
|
||||||
preferredDate = preferredDate,
|
|
||||||
preferredTime = preferredTime,
|
|
||||||
budget = budget,
|
|
||||||
duration = duration,
|
|
||||||
createdAt = createdAt,
|
|
||||||
updatedAt = updatedAt
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun DatePlanEntity.toDomain(): DatePlan = DatePlan(
|
private fun DatePlanEntity.toDomain(): DatePlan = DatePlan(
|
||||||
id = id,
|
id = id,
|
||||||
coupleId = coupleId,
|
coupleId = coupleId,
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,6 @@ object DatabaseModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideCategoryDao(db: AppDatabase) = db.categoryDao()
|
fun provideCategoryDao(db: AppDatabase) = db.categoryDao()
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideDatePlanPreferenceDao(db: AppDatabase) = db.datePlanPreferenceDao()
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideDatePlanDao(db: AppDatabase) = db.datePlanDao()
|
fun provideDatePlanDao(db: AppDatabase) = db.datePlanDao()
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
package app.closer.domain.model
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A partner's preferences for assembling a date plan.
|
|
||||||
*
|
|
||||||
* Both partners contribute preferences; the Builder uses them to assemble
|
|
||||||
* a complete plan with activity, food, conversation prompts, and optional challenge.
|
|
||||||
*
|
|
||||||
* Stored locally in Room. Synced to Firestore when explicitly shared/scheduled.
|
|
||||||
*/
|
|
||||||
data class DatePlanPreference(
|
|
||||||
val id: String = "",
|
|
||||||
val coupleId: String = "",
|
|
||||||
val dateIdeaId: String = "",
|
|
||||||
val preferredDate: Long = 0L,
|
|
||||||
val preferredTime: String = "",
|
|
||||||
val budget: Int = 0,
|
|
||||||
val duration: String = "",
|
|
||||||
val createdAt: Long = 0L,
|
|
||||||
val updatedAt: Long = 0L
|
|
||||||
)
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
package app.closer.domain.model
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A complete plan suggestion generated by the Date Builder.
|
|
||||||
*
|
|
||||||
* Contains the activity, food, conversation prompts, and optional challenge
|
|
||||||
* that are assembled from partner preferences and the selected date idea.
|
|
||||||
*
|
|
||||||
* @property activity Recommended activity description.
|
|
||||||
* @property food Recommended food/dining option.
|
|
||||||
* @property conversationPrompts List of conversation question IDs or prompts.
|
|
||||||
* @property optionalChallenge Optional challenge suggestion for the date.
|
|
||||||
*/
|
|
||||||
data class DatePlanSuggestion(
|
|
||||||
val activity: String = "",
|
|
||||||
val food: String = "",
|
|
||||||
val conversationPrompts: List<String> = emptyList(),
|
|
||||||
val optionalChallenge: String? = null
|
|
||||||
)
|
|
||||||
|
|
@ -1,31 +1,17 @@
|
||||||
package app.closer.domain.repository
|
package app.closer.domain.repository
|
||||||
|
|
||||||
import app.closer.domain.model.DatePlan
|
import app.closer.domain.model.DatePlan
|
||||||
import app.closer.domain.model.DatePlanPreference
|
|
||||||
import app.closer.domain.model.DatePlanSuggestion
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository for the Date Builder feature.
|
* Repository for the Date Builder feature: real, schedulable [DatePlan]s synced to Firestore.
|
||||||
*
|
*
|
||||||
* Responsibilities:
|
* The old two-sided "preference" surface (each partner records preferences, a plan is assembled
|
||||||
* - Store partner preferences locally (Room)
|
* from both) was removed as dead code — nothing ever read it, and "Create Plan" has written real
|
||||||
* - Assemble complete date plans from preferences
|
* PLANNED plans since the N-002 fix. If that two-sided product vision returns, design it fresh
|
||||||
* - Sync plans to Firestore when shared/scheduled
|
* rather than resurrecting the old plumbing (see Future.md).
|
||||||
*/
|
*/
|
||||||
interface DatePlanRepository {
|
interface DatePlanRepository {
|
||||||
|
|
||||||
// ─── Preference methods ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/** Get a partner's preference for a date idea. */
|
|
||||||
suspend fun getPreference(coupleId: String, dateIdeaId: String): DatePlanPreference?
|
|
||||||
|
|
||||||
/** Record or update a partner's preference. */
|
|
||||||
suspend fun savePreference(preference: DatePlanPreference)
|
|
||||||
|
|
||||||
/** Observe all preferences for a couple. */
|
|
||||||
fun observePreferences(coupleId: String): Flow<List<DatePlanPreference>>
|
|
||||||
|
|
||||||
// ─── Plan methods ────────────────────────────────────────────────────────
|
// ─── Plan methods ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Get a date plan by ID. */
|
/** Get a date plan by ID. */
|
||||||
|
|
@ -45,12 +31,4 @@ interface DatePlanRepository {
|
||||||
|
|
||||||
/** Delete a date plan. */
|
/** Delete a date plan. */
|
||||||
suspend fun deletePlan(planId: String)
|
suspend fun deletePlan(planId: String)
|
||||||
|
|
||||||
// ─── Builder methods ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/** Assemble a date plan suggestion from partner preferences. */
|
|
||||||
suspend fun assemblePlanSuggestion(
|
|
||||||
coupleId: String,
|
|
||||||
dateIdeaId: String
|
|
||||||
): DatePlanSuggestion?
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ fun DateBuilderScreen(
|
||||||
onTimeChange = viewModel::updateTime,
|
onTimeChange = viewModel::updateTime,
|
||||||
onBudgetChange = viewModel::updateBudget,
|
onBudgetChange = viewModel::updateBudget,
|
||||||
onDurationChange = viewModel::updateDuration,
|
onDurationChange = viewModel::updateDuration,
|
||||||
onSave = { viewModel.savePreference() },
|
onSave = { viewModel.createPlan() },
|
||||||
onBack = { onNavigate("back") }
|
onBack = { onNavigate("back") }
|
||||||
)
|
)
|
||||||
SnackbarHost(
|
SnackbarHost(
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class DateBuilderViewModel @Inject constructor(
|
||||||
_uiState.update { it.copy(duration = duration) }
|
_uiState.update { it.copy(duration = duration) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun savePreference() {
|
fun createPlan() {
|
||||||
val state = _uiState.value
|
val state = _uiState.value
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|
|
||||||
|
|
@ -601,23 +601,8 @@ service cloud.firestore {
|
||||||
allow update, delete: if false;
|
allow update, delete: if false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Date plan preferences: per-partner preferences for building date plans.
|
// (date_plan_preferences removed 2026-07-16 — the per-partner preference surface was dead code;
|
||||||
// Both members can read; either member can write a preference document.
|
// the collection is deliberately default-deny. Stray pre-R15 docs are orphaned: launch checklist.)
|
||||||
// Document IDs are Firestore auto-IDs (not user IDs).
|
|
||||||
match /date_plan_preferences/{prefId} {
|
|
||||||
allow read: if isCouplesMember(coupleId);
|
|
||||||
allow create, update: if isCouplesMember(coupleId)
|
|
||||||
&& request.resource.data.keys().hasAll(['dateIdeaId', 'createdAt', 'updatedAt'])
|
|
||||||
&& request.resource.data.keys().hasOnly([
|
|
||||||
'dateIdeaId', 'preferredDate', 'preferredTime',
|
|
||||||
'budget', 'duration', 'createdAt', 'updatedAt'
|
|
||||||
])
|
|
||||||
// Strict E2EE: user-entered details are ciphertext (dateIdeaId/dates stay plaintext for queries).
|
|
||||||
&& cipherOrAbsent(request.resource.data, 'preferredTime')
|
|
||||||
&& cipherOrAbsent(request.resource.data, 'budget')
|
|
||||||
&& cipherOrAbsent(request.resource.data, 'duration');
|
|
||||||
allow delete: if false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date plans: complete plans assembled from partner preferences.
|
// Date plans: complete plans assembled from partner preferences.
|
||||||
// Both members can read and delete; writes are field-validated.
|
// Both members can read and delete; writes are field-validated.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue