Skip to content

feat(activate): Phase C Wave 1 — end-to-end Store5 migration - #2686

Merged
therajanmaurya merged 4 commits into
openMF:store5Migrationfrom
therajanmaurya:feat/store5-w1-activate-e2e
May 21, 2026
Merged

feat(activate): Phase C Wave 1 — end-to-end Store5 migration#2686
therajanmaurya merged 4 commits into
openMF:store5Migrationfrom
therajanmaurya:feat/store5-w1-activate-e2e

Conversation

@therajanmaurya

Copy link
Copy Markdown
Member

Summary

Phase C Wave 1 — first end-to-end feature migration. Establishes the migration template that Waves 2-7 replicate by touching all five layers of the architecture per the revised plan: core/networkcore/databasecore/datacore/domain (delete pure delegators) → feature/* ({ui, di, navigation}/ restructure).

Activate is the smallest mutation feature (3 isolated submit endpoints: client / center / group). Picking it first proves the pattern without offline-draft complexity.

Architectural deliverables (seeded for all future waves)

core/data + core/ui re-export layers

  • core/data/build.gradle.kts: api(projects.coreBase.store)api(projects.core.store) (route through the seam)
  • core/data/.../store/SubmitTypes.kt: typealias SubmitHandler<R> / SubmitState<R> + CoroutineScope.submitHandler<R>() extension
  • core/ui/build.gradle.kts: + api(projects.coreBase.ui)
  • core/ui/.../store/ViewModelTypes.kt: typealias BaseViewModel<S, E, A>

Feature modules import com.mifos.core.data.store.* and com.mifos.core.ui.store.* and never touch template.core.base.store.* directly in their gradle. Composables (SubmitProgressOverlay, SubmitResultHandler, LocalScreenStateDefaults) can't be typealias-ed in Kotlin — they're imported by name via the api chain (feature → core/ui → core-base/ui).

core/network per-resource API rewrite

NEW per-resource subpackages following kmp-project-template/core/network/<feature>/{api,dto}/:

  • core/network/client/api/ClientApi.kt — suspend activate() returns PostClientsClientIdResponse
  • core/network/center/api/CenterApi.kt — suspend activate() returns PostCentersCenterIdResponse
  • core/network/group/api/GroupApi.kt — suspend activate() returns HttpResponse (Fineract returns no typed body)

Each interface is per-resource (not per-action); future client/center/group feature waves add more methods to the same files.

core/network/di/NetworkModule.kt: 3 new single { get<Ktorfit>().createXxxApi() } factories.

Method-level @Deprecated annotations on legacy DataManagerClient.activateClient, DataManagerCenter.activateCenter, DataManagerGroups.activateGroup, ClientService.activateClient, CenterService.activateCenter, GroupService.activateGroup — those classes have many other live methods, so file-level deprecation isn't appropriate.

core/data/activate/ (relocate + rewrite)

git mv relocates:

  • core/data/repository/ActivateRepository.ktcore/data/activate/ActivateRepository.kt
  • core/data/repositoryImp/ActivateRepositoryImp.ktcore/data/activate/impl/ActivateRepositoryImpl.kt

ActivateRepository interface simplified: drops nullable ActivatePayload?; activateGroup() no longer returns HttpResponse — repository throws on non-2xx.

ActivateRepositoryImpl now injects (ClientApi, CenterApi, GroupApi) — no DataManager dependency.

core/data/.../di/RepositoryModule.kt: import update + singleOf(::ActivateRepositoryImpl) bind ActivateRepository::class. Repository binding lives in core/data per the Koin DI ownership boundary in C-features.md.

core/domain — delete 3 pure-delegator use cases

  • ActivateClientUseCase.kt (deleted)
  • ActivateCenterUseCase.kt (deleted)
  • ActivateGroupUseCase.kt (deleted)
  • UseCaseModule.kt: 3 factoryOf(::ActivateXxxUseCase) entries + their imports removed

feature/activate/{ui, di, navigation}/ restructure

Was flat (ActivateViewModel.kt, ActivateScreen.kt, ActivateUiState.kt, ActivateNavigation.kt all at root). Now:

feature/activate/.../
├── ui/
│   ├── ActivateState.kt        — MVI state with TargetType enum + success/failure StringResource lookups
│   ├── ActivateAction.kt       — sealed interface ActivateClient / ActivateCenter / ActivateGroup
│   ├── ActivateViewModel.kt    — extends BaseViewModel<State, Nothing, Action>
│   └── ActivateScreen.kt       — uses SubmitProgressOverlay + SubmitResultHandler; includes @Preview
├── di/
│   └── ActivateModule.kt       — viewModelOf(::ActivateViewModel)  (no Repository binding)
└── navigation/
    ├── ActivateRoute.kt        — @Serializable data class (split from Navigation)
    └── ActivateNavigation.kt   — NavGraphBuilder.activateDestination + NavController.navigateToActivateRoute

VM no longer collects Flow<DataState>. SubmitHandler<Unit> drives the Submitting → Submitted | Failed lifecycle. Screen-side terminal effects via SubmitResultHandler; Submitting overlay via SubmitProgressOverlay. Submit button enabled = !isSubmitting (framework also dedupes double-tap).

@Preview restored using org.jetbrains.compose.ui.tooling.preview.* (Compose Multiplatform-aware, NOT the Android-only variant).

feature/activate/build.gradle.kts: -implementation(projects.core.domain) + +implementation(projects.core.data) + +implementation(projects.core.ui). No direct coreBase.* deps.

Grep invariants (Wave 1 verification)

Check Result
grep -rn "DataState|asDataStateFlow" feature/activate/ zero matches
grep -rn "template\.core\.base" feature/activate/ 3 matches (KptTheme, SubmitProgressOverlay, SubmitResultHandler — Composables/Theme accessed via api chain; gradle direct deps still zero)
grep -rn "implementation\(projects.coreBase" feature/activate/build.gradle.kts zero matches
grep -rn "MifosProgressIndicator|MifosSweetError" feature/activate/ zero matches (replaced by framework's SubmitProgressOverlay + SubmitResultHandler)
Files outside {ui, di, navigation}/ in feature/activate/src/commonMain/kotlin/.../activate/ zero

Test plan

  • ./gradlew :feature:activate:build compiles cleanly
  • ./gradlew :cmp-android:assembleDebug succeeds; Koin DI graph resolves; app boots
  • Activate-client flow: tap Activate → SubmitProgressOverlay → Success dialog → onBack
  • Activate-center / Activate-group flows: same end-to-end check
  • Network error: SubmitProgressOverlay → Failure dialog → dialog dismiss resets handler
  • Rapid double-tap: only one submission proceeds (SubmitHandler idempotent during Submitting)
  • Compose Multiplatform Preview: ActivateScreenPreview renders in IDE preview pane

Migration template for Waves 2-7

Every subsequent wave follows this same shape. The infrastructure pieces (core/data/store/SubmitTypes, core/ui/store/ViewModelTypes, core/data api routing) are seeded by Wave 1 and Waves 2-7 don't redo them. Per-feature additions:

  1. core/network/<resource>/api/XxxApi.kt (new resource if first wave touching it, or new methods on existing API)
  2. core/data/<feature>/{XxxRepository.kt, impl/XxxRepositoryImpl.kt} + RepositoryModule registration
  3. core/database/<feature>/{dao, entity, mapper}/ (when caching is involved)
  4. Delete pure-delegator use cases from core/domain
  5. feature/<name>/{ui, di, navigation}/ restructure with BaseViewModel<S, E, A> + framework Composables + @Preview
  6. feature/<name>/build.gradle.kts deps: core/data + core/ui only
  7. Per-form RoomSubmitOutbox<P> + OfflineSubmitSyncer<P, R> in feature DI for offline-resilient flows (savings, recurringDeposit, loan, client)

Phase status

  • ✅ Phase A (infra sync)
  • ✅ Phase B (core/store seam)
  • 🟢 Phase C Wave 1 (this PR — activate) — end-to-end pattern established
  • 🔵 Wave 2 (auth) — next session, chained on this branch
  • 🔵 Waves 3-7
  • 🔵 Phase D — delete deprecated DataManagers + Services + BaseApiManager + FlowConverterFactory + DataState

Rajan Maurya added 3 commits May 21, 2026 19:05
Phase C scaffolding seeded ahead of Wave 1 implementation work.

core/data:
  - build.gradle.kts: api(projects.coreBase.store) -> api(projects.core.store)
    Routes the framework primitives through the core/store seam, matching
    the dependency-direction contract enforced for the rest of Phase C.
  - src/commonMain/.../store/SubmitTypes.kt (NEW): typealias SubmitHandler,
    SubmitState; CoroutineScope.submitHandler() delegating extension.
    Feature modules import com.mifos.core.data.store.* instead of reaching
    into template.core.base.store.submit.*.

core/ui:
  - build.gradle.kts: add api(projects.coreBase.ui) so the typealias chain
    resolves transitively for feature modules depending only on core/ui.
  - src/commonMain/.../store/ViewModelTypes.kt (NEW): typealias
    BaseViewModel<S, E, A>. Feature VMs import com.mifos.core.ui.store.*
    instead of template.core.base.ui.viewmodel.*.

Composables (MutationScreenContent / ScreenContent / PagingScreenContent
/ LocalScreenStateDefaults) are NOT re-exported because Kotlin typealias
doesn't apply to Composables or CompositionLocals. Feature Screens import
them directly from template.core.base.ui.* via the api dependency chain
(feature -> core/ui -> core-base/ui). That dependency direction still
satisfies the architectural contract — no feature gradle has a direct
coreBase.* implementation() dep.
First end-to-end feature wave (network -> data -> domain -> feature).
Establishes the migration template that Waves 2-7 replicate.

core/network (per-resource API rewrite):
  + client/api/ClientApi.kt   — suspend Ktorfit, POST clients/{id}?command=activate
  + center/api/CenterApi.kt   — POST centers/{id}?command=activate
  + group/api/GroupApi.kt     — POST groups/{id}?command=activate
  Each interface is per-resource (not per-action); future client/center/
  group feature waves add more methods to the same files.
  di/NetworkModule.kt: + single<ClientApi/CenterApi/GroupApi> factories
  Method-level @deprecated on legacy DataManagerClient.activateClient,
  DataManagerCenter.activateCenter, DataManagerGroups.activateGroup,
  ClientService.activateClient, CenterService.activateCenter,
  GroupService.activateGroup (those files have other live methods, so
  file-level deprecation isn't appropriate — granular method deprecation).

core/data (relocate + rewrite):
  git mv core/data/repository/ActivateRepository.kt
      -> core/data/activate/ActivateRepository.kt
  git mv core/data/repositoryImp/ActivateRepositoryImp.kt
      -> core/data/activate/impl/ActivateRepositoryImpl.kt (renamed)
  ActivateRepository interface signature simplified: drops nullable
  ActivatePayload? (always required); activateGroup() drops HttpResponse
  return (Repository throws on non-2xx).
  ActivateRepositoryImpl now injects (ClientApi, CenterApi, GroupApi)
  directly; no DataManager dependency.

core/domain (delete pure-delegator use cases):
  - core/domain/useCases/ActivateClientUseCase.kt    (deleted)
  - core/domain/useCases/ActivateCenterUseCase.kt    (deleted)
  - core/domain/useCases/ActivateGroupUseCase.kt     (deleted)
  - di/UseCaseModule.kt: drop 3 factoryOf(::ActivateXxxUseCase) entries
    + their imports.

feature/activate ({ui, di, navigation}/ restructure):
  Layout was flat (ActivateViewModel.kt, ActivateScreen.kt,
  ActivateUiState.kt, ActivateNavigation.kt all at root). Now:
    ui/
      ActivateState.kt        — MVI state data class with TargetType enum
                                + success/failure StringResource lookups
      ActivateAction.kt       — sealed interface ActivateClient/Center/Group
      ActivateViewModel.kt    — extends BaseViewModel<State, Nothing, Action>
      ActivateScreen.kt       — uses SubmitProgressOverlay + SubmitResultHandler
                                from template.core.base.ui.submit (via api chain
                                core/ui -> core-base/ui)
    di/
      ActivateModule.kt       — single<ActivateRepositoryImpl> bind ActivateRepository
                                + viewModelOf(::ActivateViewModel)
    navigation/
      ActivateRoute.kt        — @serializable data class (split from Navigation)
      ActivateNavigation.kt   — NavGraphBuilder.activateDestination +
                                NavController.navigateToActivateRoute

  VM no longer collects Flow<DataState>: SubmitHandler<Unit> drives the
  Submitting -> Submitted | Failed lifecycle. Screen-side terminal
  effects via SubmitResultHandler. Disabled-while-submitting UX via
  enabled = !isSubmitting on the activate button (idempotent
  double-tap protection at the framework layer too).

feature/activate/build.gradle.kts:
  - implementation(projects.core.domain)
  + implementation(projects.core.data)
  + implementation(projects.core.ui)
  No direct coreBase.* deps. Template Composables imported via api chain.
…+ restore Composable Previews

- core/data/.../di/RepositoryModule.kt: imports updated to the new
  com.mifos.core.data.activate package; singleOf(::ActivateRepositoryImp)
  -> singleOf(::ActivateRepositoryImpl) bind ActivateRepository::class
- feature/activate/.../di/ActivateModule.kt: reverted to viewModelOf-only.
  Repository binding belongs in core/data RepositoryModule per the
  Koin DI ownership boundary documented in C-features.md.
- feature/activate/.../ui/ActivateScreen.kt: restored @Preview using
  Compose Multiplatform's org.jetbrains.compose.ui.tooling.preview.*
  (NOT androidx.compose.ui.tooling.preview.*). PreviewParameterProvider
  emits 2 variants (idle, submitting) — minimal coverage; future iteration
  can add Success / Failure dialog states.
@therajanmaurya
therajanmaurya requested a review from a team May 21, 2026 13:54
@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: 8cf034c6-9178-4693-a88f-6f6546f66fa5

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.

ActivateScreen used 'Success' and 'Error' as raw String dialogTitles on
MifosAlertDialog. Per the no-hardcoded-user-facing-strings rule (just
added to C-features.md), every user-facing string in feature/* must
come from composeResources.

feature/activate/.../composeResources/values/strings.xml:
  + feature_activate_dialog_title_success: 'Success'
  + feature_activate_dialog_title_error:   'Error'

feature/activate/.../ui/ActivateScreen.kt:
  - dialogTitle = "Success"
  + dialogTitle = stringResource(Res.string.feature_activate_dialog_title_success)
  - dialogTitle = "Error"
  + dialogTitle = stringResource(Res.string.feature_activate_dialog_title_error)

Grep invariant now passes: zero user-facing literals in
feature/activate/src/commonMain/kotlin/.
@sonarqubecloud

Copy link
Copy Markdown

@therajanmaurya
therajanmaurya merged commit 953f7b0 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