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)
|
||||
|
||||
- **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):**
|
||||
- **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.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import app.closer.data.local.entity.QuestionEntity
|
|||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun questionDao(): QuestionDao
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Both partners contribute preferences; the Builder uses them to assemble a plan.
|
||||
*
|
||||
* Table: date_plan_preferences
|
||||
* Nothing reads or writes this table: the preference feature it served was removed as dead code
|
||||
* (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
|
||||
* 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")
|
||||
data class DatePlanPreferenceEntity(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package app.closer.data.remote
|
|||
import app.closer.crypto.CoupleEncryptionManager
|
||||
import app.closer.crypto.FieldEncryptor
|
||||
import app.closer.domain.model.DatePlan
|
||||
import app.closer.domain.model.DatePlanPreference
|
||||
import app.closer.domain.model.DatePlanStatus
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
import com.google.firebase.firestore.SetOptions
|
||||
|
|
@ -19,11 +18,9 @@ import javax.inject.Singleton
|
|||
/**
|
||||
* Firestore data source for date plan preferences and plans.
|
||||
*
|
||||
* Path layout:
|
||||
* - couples/{coupleId}/date_plan_preferences/{userId} — per-partner preferences
|
||||
* - couples/{coupleId}/date_plans/{planId} — complete plans generated by the builder
|
||||
*
|
||||
* Local-first: Preferences stored in Room. Plans synced to Firestore when explicitly shared/scheduled.
|
||||
* Path layout: couples/{coupleId}/date_plans/{planId} — plans, synced when explicitly
|
||||
* shared/scheduled. (The old per-partner date_plan_preferences surface was removed as dead code;
|
||||
* the collection is default-deny in rules and no longer exists in code.)
|
||||
*/
|
||||
@Singleton
|
||||
class FirestoreDatePlanDataSource @Inject constructor(
|
||||
|
|
@ -31,53 +28,10 @@ class FirestoreDatePlanDataSource @Inject constructor(
|
|||
private val encryptionManager: CoupleEncryptionManager,
|
||||
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) =
|
||||
db.collection(FirestoreCollections.COUPLES).document(coupleId)
|
||||
.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 ────────────────────────────────────────────────────────
|
||||
|
||||
suspend fun createPlan(coupleId: String, plan: DatePlan): String {
|
||||
|
|
@ -145,21 +99,6 @@ class FirestoreDatePlanDataSource @Inject constructor(
|
|||
|
||||
// ─── 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? {
|
||||
val dateIdeaId = getString("dateIdeaId") ?: return null
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
package app.closer.data.repository
|
||||
|
||||
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.DatePlanPreferenceEntity
|
||||
import app.closer.data.remote.FirestoreDatePlanDataSource
|
||||
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.repository.DatePlanRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -17,44 +13,15 @@ import javax.inject.Inject
|
|||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Implementation of [DatePlanRepository].
|
||||
*
|
||||
* 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.
|
||||
* Implementation of [DatePlanRepository]: local-first plans (Room) synced to Firestore when
|
||||
* explicitly shared/scheduled.
|
||||
*/
|
||||
@Singleton
|
||||
class DatePlanRepositoryImpl @Inject constructor(
|
||||
private val preferenceDataSource: FirestoreDatePlanDataSource,
|
||||
private val planDataSource: FirestoreDatePlanDataSource,
|
||||
private val preferenceDao: DatePlanPreferenceDao,
|
||||
private val planDao: DatePlanDao
|
||||
) : 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 ────────────────────────────────────────────────────────
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ─── 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 ────────────────────────────────────────────────────────
|
||||
|
||||
private fun DatePlan.sanitized(): DatePlan = copy(
|
||||
|
|
@ -156,30 +103,6 @@ class DatePlanRepositoryImpl @Inject constructor(
|
|||
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(
|
||||
id = id,
|
||||
coupleId = coupleId,
|
||||
|
|
|
|||
|
|
@ -40,10 +40,6 @@ object DatabaseModule {
|
|||
@Singleton
|
||||
fun provideCategoryDao(db: AppDatabase) = db.categoryDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatePlanPreferenceDao(db: AppDatabase) = db.datePlanPreferenceDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
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
|
||||
|
||||
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:
|
||||
* - Store partner preferences locally (Room)
|
||||
* - Assemble complete date plans from preferences
|
||||
* - Sync plans to Firestore when shared/scheduled
|
||||
* The old two-sided "preference" surface (each partner records preferences, a plan is assembled
|
||||
* from both) was removed as dead code — nothing ever read it, and "Create Plan" has written real
|
||||
* PLANNED plans since the N-002 fix. If that two-sided product vision returns, design it fresh
|
||||
* rather than resurrecting the old plumbing (see Future.md).
|
||||
*/
|
||||
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 ────────────────────────────────────────────────────────
|
||||
|
||||
/** Get a date plan by ID. */
|
||||
|
|
@ -45,12 +31,4 @@ interface DatePlanRepository {
|
|||
|
||||
/** Delete a date plan. */
|
||||
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,
|
||||
onBudgetChange = viewModel::updateBudget,
|
||||
onDurationChange = viewModel::updateDuration,
|
||||
onSave = { viewModel.savePreference() },
|
||||
onSave = { viewModel.createPlan() },
|
||||
onBack = { onNavigate("back") }
|
||||
)
|
||||
SnackbarHost(
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class DateBuilderViewModel @Inject constructor(
|
|||
_uiState.update { it.copy(duration = duration) }
|
||||
}
|
||||
|
||||
fun savePreference() {
|
||||
fun createPlan() {
|
||||
val state = _uiState.value
|
||||
|
||||
viewModelScope.launch {
|
||||
|
|
|
|||
|
|
@ -601,23 +601,8 @@ service cloud.firestore {
|
|||
allow update, delete: if false;
|
||||
}
|
||||
|
||||
// Date plan preferences: per-partner preferences for building date plans.
|
||||
// Both members can read; either member can write a preference document.
|
||||
// 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_plan_preferences removed 2026-07-16 — the per-partner preference surface was dead code;
|
||||
// the collection is deliberately default-deny. Stray pre-R15 docs are orphaned: launch checklist.)
|
||||
|
||||
// Date plans: complete plans assembled from partner preferences.
|
||||
// Both members can read and delete; writes are field-validated.
|
||||
|
|
|
|||
Loading…
Reference in New Issue