Coverage Summary for Class: RecurringRuleDtoMappersKt (com.georgeci.moneysurfer.data.sync)

Class Class, % Method, % Branch, % Line, % Instruction, %
RecurringRuleDtoMappersKt 100% (1/1) 100% (5/5) 100% (14/14) 100% (40/40) 100% (290/290)


 package com.georgeci.moneysurfer.data.sync
 
 import com.georgeci.moneysurfer.data.db.entity.RecurringRuleEntity
 import com.georgeci.moneysurfer.data.remote.RecurringRuleDoc
 import com.georgeci.moneysurfer.domain.model.MissingDayPolicy
 import com.georgeci.moneysurfer.domain.model.RecurringFrequency
 import kotlinx.datetime.DayOfWeek
 
 /**
  * Recurring-rule Room ↔ Firestore-DTO mappers. Same contract as the entity mappers in
  * `SyncDtoMappers.kt`, kept in their own file because that one is at its function ceiling.
  *
  * The schedule day sets are CSV columns in Room and real lists on the wire — the same
  * conversion the budget mappers make for `categoryIds`. Blank segments are dropped so a
  * legacy `""` or a trailing comma decodes to an empty list rather than a blank day name.
  *
  * Both directions normalize the day sets, because a remote doc is untrusted: the Firestore
  * rules gate membership and client version, not field shape, so any co-member's client can
  * write `"MONDAY "` or day `32`. Those survive the wire type check but not
  * [com.georgeci.moneysurfer.data.repository.RecurringRuleRepositoryImpl]'s parse, which
  * matches exact enum names and would silently drop the day — a rule that renders with the
  * wrong cadence rather than failing visibly. Normalizing here keeps the bad segment out of
  * Room in the first place.
  */
 fun RecurringRuleEntity.toDoc(): RecurringRuleDoc = RecurringRuleDoc(
     title = title,
     amount = amount,
     categoryId = categoryId,
     scheduleFrequency = scheduleFrequency,
     scheduleInterval = scheduleInterval,
     scheduleDaysOfWeek = scheduleDaysOfWeek.split(',').toDayOfWeekNames(),
     scheduleDaysOfMonth = scheduleDaysOfMonth.split(',').toDaysOfMonth(),
     scheduleMissingDayPolicy = scheduleMissingDayPolicy,
     startDate = startDate,
     nextRunAt = nextRunAt,
     autoCreate = autoCreate,
     isActive = isActive,
     createdAt = createdAt,
     updatedAt = updatedAt,
 )
 
 fun RecurringRuleDoc.toEntity(id: String, workspaceId: String): RecurringRuleEntity =
     RecurringRuleEntity(
         id = id,
         workspaceId = workspaceId,
         title = title,
         amount = amount,
         categoryId = categoryId,
         // A legacy/partial doc may omit the enum names; blank stores an unparseable value,
         // so fall back to the same defaults RecurringRuleRepositoryImpl parses to.
         scheduleFrequency = scheduleFrequency.ifBlank { RecurringFrequency.MONTHLY.name },
         scheduleInterval = scheduleInterval,
         scheduleDaysOfWeek = scheduleDaysOfWeek.toDayOfWeekNames().joinToString(","),
         scheduleDaysOfMonth = scheduleDaysOfMonth
             .filter { it in DAYS_OF_MONTH_RANGE }
             .joinToString(","),
         scheduleMissingDayPolicy = scheduleMissingDayPolicy.ifBlank {
             MissingDayPolicy.LAST_DAY_OF_MONTH.name
         },
         startDate = startDate,
         nextRunAt = nextRunAt,
         autoCreate = autoCreate,
         isActive = isActive,
         createdAt = createdAt,
         updatedAt = updatedAt,
     )
 
 /**
  * Trims each segment and keeps only real [kotlinx.datetime.DayOfWeek] names, so a padded
  * `"MONDAY "` normalizes rather than being dropped downstream and a junk name never reaches Room.
  */
 private fun List<String>.toDayOfWeekNames(): List<String> =
     mapNotNull { segment ->
         val trimmed = segment.trim()
         DayOfWeek.entries.firstOrNull { it.name == trimmed }?.name
     }
 
 /** Trims each segment and keeps only days that exist in some month. */
 private fun List<String>.toDaysOfMonth(): List<Int> =
     mapNotNull { it.trim().toIntOrNull() }.filter { it in DAYS_OF_MONTH_RANGE }
 
 /** Widest calendar month — February is clamped by `MissingDayPolicy`, not by this filter. */
 private val DAYS_OF_MONTH_RANGE = 1..31