[{"number":44,"title":"Phase 2 — BillingManagerImpl + DebugBillingOverride","body":"Implements the Google Play Billing 7.x wrapper (`BillingManagerImpl`) and debug override (`DebugBillingOverride`) in full isolation — not yet wired into the app. Part of the paywall foundation series.\n\n## Core design decisions\n\n- `launchPremiumPurchaseFlow` is **non-suspend** — returns `BillingLaunchResult` synchronously after dispatching the Play sheet; purchase outcome arrives via `isPremium: StateFlow`\n- `isPremium` is derived via `combine(_billingDerivedPremium, prefsFlow.map { it.isPremiumCached })` in `init {}` — `_billingDerivedPremium` starts `null` so the DataStore cache provides the initial value with zero race window against `startConnection()`\n- **PENDING purchases are explicitly ignored** — no `isPremium` flip, no acknowledgment\n- **`ITEM_ALREADY_OWNED`** triggers a fresh `queryPurchases()` to ensure the existing entitlement is reflected in `isPremium` and the DataStore cache even when Play omits the purchase list\n- Cache is preserved on billing query failure (offline resilience); DataStore write is guarded so it only fires when the premium value actually changes\n- `AtomicReference` used for `cachedProductDetails` to avoid visibility hazards on the billing callback thread\n- `DebugBillingOverride` override-mode decision is backed by a `StateFlow` seeded via `stateIn(SharingStarted.Eagerly)` — future preference updates are reflected in `.value` without an extra async step; note that `.value` is `NO_OVERRIDE` until the first DataStore emission arrives\n\n## New production files\n\n- **`BillingManagerImpl`** (`src/main`) — wraps `BillingClient` via `BillingClientProvider` abstraction (enables unit-testing without Play). On `startConnection` OK: queries existing purchases and pre-fetches `ProductDetails` in **parallel** coroutines so a slow purchase query cannot block product-details caching. Both suspending wrappers use `suspendCancellableCoroutine` with `isActive` guards so coroutine cancellation is handled gracefully. `restorePurchases()` returns a three-way `RestoreResult` (Success / BillingUnavailable / Error). Non-OK acknowledgment responses are logged via `Log.w` for diagnosability.\n\n- **`DebugBillingOverride`** (`src/debug`) — decorator over `BillingManager`, confined to the debug source set so it is compile-time absent from release builds. Set `AppPreferences.debugBillingOverrideMode = \u0026#34;FORCE_PREMIUM\u0026#34;` to short-circuit `launchPremiumPurchaseFlow` without touching BillingClient:\n\n```kotlin\n// In a debug settings screen or test setup:\npreferences.setDebugBillingOverrideMode(DebugBillingOverride.FORCE_PREMIUM)\n// → isPremium immediately becomes true; no Play dialog shown\n```\n\n## Test infrastructure\n\n- **`LocalOnly`** — JUnit4 `@Category` marker; `build.gradle.kts` excludes it via `useJUnit { excludeCategories(\u0026#34;com.servicebook.LocalOnly\u0026#34;) }` (JUnit4-only assumption documented inline). Run tagged tests from the IDE only.\n- **`FakeBillingManager`** — controllable test double exposing `premiumState: MutableStateFlow` for reactive assertions in future ViewModel tests.\n- **`BillingManagerImplTest`** (`@Category(LocalOnly)`) — 14 tests covering: PURCHASED flip, PENDING ignored, restore three-way result, query-failed cache preservation, and DebugBillingOverride short-circuit.","state":"open","draft":false,"merged":false,"html_url":"https://github.com/Mesya82/Service-Book/pull/44","user":{"login":"Copilot","id":198982749,"profile_url":"https://github.com/apps/copilot-swe-agent","avatar_url":"https://avatars.githubusercontent.com/in/1143301?v=4"},"assignees":["Copilot"],"head":{"ref":"copilot/implement-billing-manager-override","sha":"b3d5a7f71c31239b09bc446e57b1ff1112656693","repo":{"full_name":"Mesya82/Service-Book"}},"base":{"ref":"master","sha":"ec9ed8c10ea8edde04948cc190ffb6a73088d176","repo":{"full_name":"Mesya82/Service-Book"}},"created_at":"2026-04-28T08:05:12Z","updated_at":"2026-04-28T17:46:42Z"}]