Skip to content

feat(core/store): Phase B — consumer Store5 seam (Theme + Koin + DB v2 + RoomSubmitOutbox) - #2683

Merged
therajanmaurya merged 2 commits into
openMF:store5Migrationfrom
therajanmaurya:feat/store5-core-store-seam
May 21, 2026
Merged

feat(core/store): Phase B — consumer Store5 seam (Theme + Koin + DB v2 + RoomSubmitOutbox)#2683
therajanmaurya merged 2 commits into
openMF:store5Migrationfrom
therajanmaurya:feat/store5-core-store-seam

Conversation

@therajanmaurya

Copy link
Copy Markdown
Member

Summary

Phase B of the Store5-template-adoption epic. Wires field-officer's consumer customization seam (core/store) for offline-first reads/writes, hooks LocalScreenStateDefaults into MifosTheme, registers appStoreModule in central Koin, and adds the framework_submit_drafts table (DB v1 → v2) backed by RoomSubmitOutbox<P>.

This unblocks Phase C — feature waves can now migrate to Store5 with branded loading / error / empty / no-network visuals and offline-resilient mutation submission, with zero per-screen state-handling code.

Changes (17 files, +434 / -24)

1. core/store/ module — consumer customization seam (new module)

  • build.gradle.kts — Compose + Koin convention plugins; api(projects.coreBase.store) + api(projects.coreBase.ui) so consumers get LocalScreenStateDefaults, ScreenState*, Store5Registry from core/store alone
  • README.md — full pattern documentation (what you get for free, customization points, Phase C syncer pattern)
  • AppStoreRegistry.kt — named-qualifier registry; Phase C waves add val Foo = store("foo") here
  • AppErrorMapper.ktThrowable → user-message mapper. Routes through template.core.base.store.error.categorize with Fineract-aware copy; ready to extend with Fineract exception branches as features land
  • AppScreenStateDefaults.kt — branded ScreenStateDefaults (skeleton loading, branded empty/error/no-network copy, messageFor = ::mapErrorToUserMessage)
  • di/StoreModule.kt — Koin appStoreModule; empty entry point that Phase C waves extend with single<Store<K,V>>(qualifier = AppStoreRegistry.X) { ... } registrations

2. core/designsystem/.../Theme.ktLocalScreenStateDefaults wiring

val screenStateDefaults = appScreenStateDefaults()
KptMaterialTheme(theme = theme) {
    CompositionLocalProvider(
        LocalScreenStateDefaults provides screenStateDefaults,
    ) {
        content()
    }
}

Every screen under MifosTheme now inherits branded defaults — no per-screen wiring.

3. cmp-navigation/.../di/KoinModules.kt — central Koin registration

Adds appStoreModule to the allModules list. Feature waves register their Store<K,V> factories qualifier-bound via AppStoreRegistry.

4. MifosDatabase v1 → v2 migration — framework_submit_drafts table

  • New entity: com.mifos.room.entities.framework.DraftEntity (@Entity(tableName = "framework_submit_drafts")) — 7 columns matching template's reference impl
  • New DAO: com.mifos.room.dao.DraftDao — insert / getById / getPendingByFormKey / observePendingByFormKey / getAllPending / markRetrying / markSubmitted / markFailed / updatePayload / deleteByFormKey / deleteAll / deleteOlderThan
  • MifosDatabase: commonMain expect + 3 platform actuals (android / desktop / native) updated — entity added to entities[], abstract val draftDao: DraftDao added, VERSION bumped 1 → 2 with KDoc
  • Migration: MIGRATION_1_2 (raw SQL CREATE TABLE) added to each platform's DatabaseModule.<platform>.kt, chained onto AppDatabaseFactory builder via .addMigrations(). Portable via androidx.room3.migration.Migration + androidx.sqlite.execSQL.
  • DaoModule: + single { get<MifosDatabase>().draftDao }

5. RoomSubmitOutbox<P>SubmitOutbox bridge to DraftDao

At com.mifos.core.data.infra.impl.RoomSubmitOutbox. Serializes payloads via kotlinx.serialization, persists to framework_submit_drafts. Idempotent save (upserts on formKey collision). Used by Phase C feature waves to make form submissions offline-resilient — payload survives process death; auto-retried by OfflineSubmitSyncer on reconnect.

  • core/data/build.gradle.kts+ api(projects.coreBase.store)

6. OfflineSubmitSyncer pattern documented

OfflineSubmitSyncer<P, R> is parameterized by payload type. The original Phase B plan assumed a global syncer in MainActivity — corrected: syncers are instantiated per form within feature DI modules. Full pattern documented in core/store/README.md "Phase C syncer pattern".

Phase C syncer pattern (example for one feature)

single<SubmitOutbox<LoanApplicationPayload>> {
    RoomSubmitOutbox(
        dao = get<MifosDatabase>().draftDao,
        serializer = LoanApplicationPayload.serializer(),
    )
}
single { (scope: CoroutineScope) ->
    scope.offlineSubmitSyncer(
        outbox       = get<SubmitOutbox<LoanApplicationPayload>>(),
        isOnlineFlow = get<NetworkMonitor>().isOnline,
        submitBlock  = { payload -> get<LoanRepository>().submit(payload) },
    ).also { it.start() }
}

Test plan

  • Gradle compiles — ./gradlew :core:store:build :core:database:build :core:data:build :core:designsystem:build :cmp-navigation:build resolves all deps + compiles all modules
  • DB migration smoke test — install app on v1 schema, upgrade to v2, verify framework_submit_drafts table exists with correct columns (id, formKey, payloadJson, status, createdAtMs, updatedAtMs, errorMessage) and is empty
  • No screen breakage — MifosTheme-wrapped screens still render normally; no NPE on LocalScreenStateDefaults access from any screen
  • Koin start-up — startKoin { modules(KoinModules.allModules) } boots cleanly with appStoreModule added (no missing-dep errors)
  • CI green (CI failures, if any, will be the same coverage / SonarCloud ones expected at the final epic-merge gate; not blockers for store5Migration PR review)

Phase status

  • ✅ Phase A (infra sync) — kmp-project-template#158 + openMF/mifos-x-field-officer-app#2681 + #2682 merged
  • 🟢 Phase B (this PR) — core/store seam complete; ready for Phase C
  • 🔵 Phase C — 20 feature waves migrate to Store5 (drafted in plan, not started)
  • 🔵 Phase D — DataState bridge removal + final merge store5Migration → development

Related

  • Epic plan: plan-layer/project-plans/mifos-x/mifos-x-field-officer-app/active/store5-adoption/PLAN.md
  • Phase B sub-plan: plan-layer/project-plans/mifos-x/mifos-x-field-officer-app/active/store5-adoption/B-core.md
  • Phase A bootstrap PR: openMF/mifos-x-field-officer-app#2681
  • Phase A sync PR: openMF/mifos-x-field-officer-app#2682
  • Template SOT PR: openMF/kmp-project-template#158

…ption

Phase B baseline: creates the single-discoverable customization seam that
sits between consumer features and the synced core-base/store /
core-base/ui framework modules.

New module ':core:store' with 4 .kt files (mirroring kmp-project-template's
core/store reference):

  AppStoreRegistry.kt     Named-qualifier registry — Phase C feature waves
                          add 'val Foo = store("foo")' as features migrate.
                          Reserved slots noted in comments per wave.

  AppErrorMapper.kt       Throwable -> user-message mapper. Routes through
                          template's ErrorCategory.{Network, Auth, RateLimit,
                          Server, Generic}. Phase C feature waves extend
                          with Fineract-specific exception branches above
                          the categorize() fallback (e.g.,
                          FineractValidationException,
                          OfflineSubmissionRejectedException).

  AppScreenStateDefaults  Branded empty / error / no-network / loading
                          visuals — Lottie + telemetry hooks reserved as
                          TODO. messageFor wired through mapErrorToUserMessage.

  di/StoreModule.kt       Koin 'appStoreModule' — Phase C registers
                          'single(qualifier = AppStoreRegistry.X) { ... }'
                          here as features migrate.

settings.gradle.kts:
  + ':core:store'
  + ':core-base:datastore', ':core-base:security', ':core-base:store'
    (synced via openMF#2682 but not previously declared)

Phase B remaining work (separate commits on this branch):
  1. Thread 'LocalScreenStateDefaults provides appScreenStateDefaults()'
     into MifosTheme in core/designsystem
  2. Wire 'appStoreModule' into Koin startup in cmp-android + cmp-ios
  3. Add framework_submit_drafts entity + v4->v5 AutoMigration to
     com.mifos.room.MifosDatabase
  4. Boot OfflineSubmitSyncer.start() in MainActivity.onCreate (Android)
     and iOS app entry point
@therajanmaurya
therajanmaurya requested a review from a team May 21, 2026 10:25
@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • development

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 62baac25-f7e5-41f6-8192-97fc6cb2ef39

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…SubmitOutbox

Phase B chunks 1-4 complete. Builds on the scaffold from prior commit:

  Chunk 1 — Theme wiring (core/designsystem/.../Theme.kt)
    + import com.mifos.core.store.appScreenStateDefaults
    + import template.core.base.ui.screen.LocalScreenStateDefaults
    + KptMaterialTheme now wraps content in
      CompositionLocalProvider(LocalScreenStateDefaults provides ...) — every
      screen under MifosTheme inherits branded ScreenState defaults.
    + core/designsystem/build.gradle.kts: + implementation(projects.core.store)

  Chunk 2 — Koin DI wiring
    + cmp-navigation/.../di/KoinModules.kt registers appStoreModule in
      'allModules' list. Feature waves (Phase C) bind their Store<K,V>
      factories qualifier-bound via AppStoreRegistry.
    + cmp-navigation/build.gradle.kts: + implementation(projects.core.store)

  Chunk 3 — Database v1 -> v2 migration (Store5 submit-draft outbox)
    + core/database/src/commonMain/.../entities/framework/DraftEntity.kt —
      @entity(tableName = 'framework_submit_drafts'); 7 columns matching
      template's reference impl.
    + core/database/src/commonMain/.../dao/DraftDao.kt — @dao interface with
      insert / getById / getPendingByFormKey / observePendingByFormKey /
      getAllPending / markRetrying / markSubmitted / markFailed /
      updatePayload / deleteByFormKey / deleteAll / deleteOlderThan.
    + commonMain MifosDatabase + 3 platform actuals (android, desktop,
      native): import DraftEntity / DraftDao, add to entities[], add
      'abstract val draftDao: DraftDao', bump VERSION 1 -> 2 with KDoc.
    + DatabaseModule.android.kt / desktop.kt / PlatformSpecificModule.kt:
      add MIGRATION_1_2 (raw SQL CREATE TABLE) and chain .addMigrations()
      onto the AppDatabaseFactory builder. SQL is platform-portable
      (androidx.room3.migration.Migration + androidx.sqlite.execSQL).
    + DaoModule.kt: + single { get<MifosDatabase>().draftDao }
    + core/data/.../infra/impl/RoomSubmitOutbox.kt — Room-backed
      SubmitOutbox<P>. Serializes payload via kotlinx.serialization,
      stores in framework_submit_drafts. Idempotent save (upserts on
      formKey collision). Wires markRetrying / markSubmitted /
      markFailed to DraftDao status transitions.
    + core/data/build.gradle.kts: + api(projects.coreBase.store)

  Chunk 4 — OfflineSubmitSyncer pattern documented in core/store/README.md
    Syncer is parameterized by payload type P, so it is instantiated per-form
    in feature DI modules (not globally in MainActivity). Pattern documented
    with full Koin DI example under 'Phase C syncer pattern' heading.

This makes core/store / core/database / core/data ready for Phase C feature
waves to start migrating to Store5 with offline-resilient mutations.
@therajanmaurya
therajanmaurya force-pushed the feat/store5-core-store-seam branch from 0640cf6 to bd07a56 Compare May 21, 2026 12:06
@sonarqubecloud

Copy link
Copy Markdown

@therajanmaurya
therajanmaurya merged commit dd14cda into openMF:store5Migration May 21, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant