Skip to content

feat(kilo-pass): handle Play real-time notifications - #5598

Merged
iscekic merged 50 commits into
android-iap-e895-s3from
android-iap-e895-s4
Sep 8, 2026
Merged

iscekic merged 50 commits into
android-iap-e895-s3from
android-iap-e895-s4

Conversation

@iscekic

@iscekic iscekic commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Kilo Pass subscriptions bought on Google Play for Android now follow the store's subscription events.

  • A purchase now activates the subscription immediately.
  • A renewal, recovery, or restart now extends the subscription automatically.
  • A cancellation now keeps the subscription active until the paid period ends.
  • A refund or revocation now ends the subscription and removes the credits the purchase granted.

A new processor turns Google Play Real-time Developer Notifications into Kilo Pass lifecycle actions. It decodes the base64 Pub/Sub payload, rejects a package-name mismatch, and routes each notification type: purchase, renewal, recovery, and restart complete the store purchase; canceled marks end-of-period cancellation; expired ends the subscription; revoked reverses the granted credits and ends it; unknown types are claimed and ignored. Each event claims a kilo_pass_store_events row before it acts, so a delayed or redelivered message cannot apply a purchase or a reversal twice, and the stored payload token is redacted.

Files
  • apps/web/src/lib/kilo-pass/google-play-notifications.ts — new processor. It base64-decodes the Pub/Sub payload, checks the package name, fetches and decodes the Play subscription purchase, and claims the kilo_pass_store_events row with a five-minute stale reclaim. It routes each notification type to its lifecycle action, reverses base, bonus, and promo credits on revocation, skips purchase completion when a processed revoked event already settled, and marks the event processed.

The notification endpoint authenticates each Pub/Sub push before it reaches the processor. It verifies the OIDC bearer token against the GOOGLE_PLAY_RTDN_PUSH_AUDIENCE audience and now also requires the token's verified email claim to equal GOOGLE_PLAY_RTDN_PUSH_SERVICE_ACCOUNT_EMAIL, so an arbitrary GCP service account cannot invoke the billing webhook. A missing, invalid, or mismatched token returns 401, a malformed body returns 400, a fresh in-flight duplicate returns 503 for Pub/Sub to retry, and a processing error returns 500 without leaking details.

Files
  • apps/web/src/app/api/kilo-pass/play/notifications/route.ts — new POST route. It parses the body shape, reads both environment values, verifies the bearer with OAuth2Client, checks the email and email_verified claims against the configured service account, delegates to the processor, and reports failures to Sentry.
  • ENVIRONMENT.md — catalogs the new GOOGLE_PLAY_RTDN_PUSH_SERVICE_ACCOUNT_EMAIL value.

trackKiloPassPurchaseCompleted now accepts a google_play channel next to app_store, and the property branch now splits on stripe. A Google Play completion therefore emits the same kilo_pass_purchase_completed event with the provider transaction, product, and environment properties.

Files
  • apps/web/src/lib/kilo-pass/posthog-tracking.ts — adds google_play to the store channel union and changes the property branch condition to compare against stripe.

Tests: 3 files changed — route.test.ts and google-play-notifications.test.ts added, posthog-tracking.test.ts updated.
Generated: none.


Verification

No manual test paths were run. No E2E report is attached for this level — a lower PR names its own change alone, and the tip PR carries the E2E report.

Visual Changes

Visual Changes: N/A

Reviewer Notes

  • before merge: set GOOGLE_PLAY_RTDN_PUSH_AUDIENCE to the public HTTPS URL of the notifications endpoint. Run pnpm web:env set GOOGLE_PLAY_RTDN_PUSH_AUDIENCE.
  • before merge: set GOOGLE_PLAY_RTDN_PUSH_SERVICE_ACCOUNT_EMAIL to the email of the Play service account that the Pub/Sub push uses. The route returns 401 for every notification until both values are set.
  • E2E: bot-e2e — runtime verification runs once on the tip PR (level 6) after all levels land. This level is reviewed on its own diff.

Correctness verification, September 7

Provider state now updates grace, hold, pause, restore, recovery, and cancellation without replaying credits. Refunds reverse only the matching order and prevent later bonuses. Delayed refunded purchase events settle without endless retries.

  • Current head: 4d511fb84.
  • The stack passes 314 tests across eight scoped suites, web TypeScript, and changed-file lint and formatting.
  • Live Google Sandbox purchases pass for all three credit tiers. API-fixture accounting and lifecycle tests remain distinct from live E2E evidence.
  • Full runtime evidence remains on feat(mobile): offer Google Play Kilo Pass purchase #5618. No production deployment occurred.

Stacked PRs — merge bottom to top. Each level shows only its own diff.

Runtime verification (E2E, user advocacy, simplify) runs on the tip PR over every level.
Every level keeps its own checks, its own bot review, and its own threads; each one is answered on its own PR.
Each level is its own deliverable: it builds and passes its own checks alone.
A finding on a level is repaired on that level, then carried upward with stack.sh forward.

  1. android-iap-e895feat(commerce): add Play Kilo Pass presentation flag #5581
  2. android-iap-e895-s2feat(kilo-pass): add Google Play publisher SDK #5582
  3. android-iap-e895-s3feat(kilo-pass): add Google Play purchase verifier #5586
  4. android-iap-e895-s4feat(kilo-pass): handle Play real-time notifications #5598 ← this PR
  5. android-iap-e895-s5feat(kilo-pass): add completePlayPurchase mutation #5608
  6. android-iap-e895-s6feat(mobile): offer Google Play Kilo Pass purchase #5618 (tip)

Expanded correctness coverage

  • Tested stack head: 16015d47374b30cf28e3b1b9b7f7635e4abcca86.
  • 332 backend tests passed across nine scoped suites. 115 mobile tests passed across thirteen files.
  • Web and mobile TypeScript, scoped lint, formatting, and diff checks passed.
  • Expired store subscriptions now permit a new purchase before the expiry notification arrives.
  • Expiry cleanup locks the subscription before checking receipts. A PostgreSQL concurrency test reproduces the old renewal race.
  • Mobile acknowledgement precedes account refresh. Recovery covers slow refresh, failed acknowledgement, backend outage, pending purchases, and mixed ownership results.
  • Both Play completion routes acknowledge after committed credits. Notification retries cannot grant credits twice.
  • Play center resubscriptions retain the prior account identifier during acknowledgement. Decoder and handler tests cover this temporary provider field.
  • An older refunded order reverses only its own credits. The later paid period and its usage bonus remain valid.

Live evidence

  • Stopped the app immediately after Google test payment approval. Relaunch and restore granted credits exactly once.
  • A second purchase granted credits and became acknowledged while the app remained stopped. Duplicate notification delivery preserved the balance.
  • A final purchase through the updated completion route passed provider acknowledgement and active restore.
  • Final artifacts: parity-server-ack-interrupted, parity-server-ack-relaunch, parity-server-ack-restored, parity-final-server-route-active, and parity-final-server-route-restore.
  • All three tiers passed in the earlier correctness round. That round also covers decline, dismissal, ownership rejection, management, cancellation/restart, expiry, refund, and legal links.

Explicit limits

  • Calendar boundaries, concurrency, bonuses, provider failures, grace, hold, pause, deferral, and Play center resubscription use provider fixtures and the real database.
  • These fixture cases are integration tests, not live Google end-to-end passes.
  • The configured auto-renewing plans have no supported pending-payment test instrument. Pending transaction rejection has automated coverage.
  • Google Play tier changes now use deferred replacement at renewal. See the tier-change verification below.
  • iOS retains the earlier limited Sandbox smoke coverage.
  • A developer refund without revocation does not guarantee a voided event. Exact web parity is not claimed.
  • Production migration, deployment, authenticated delivery, and monitoring remain release gates.

Cleanup

  • All test subscriptions ended. The original account retains its initial 38 USD; the second account has zero credits.
  • Zero owned events remain unprocessed. Temporary IAM permission, local pull subscription, review databases, and saved purchase payloads are removed.

Google Play tier changes

  • Head: 7b70df79fcaf375e362fce70b5c7f990380d42e0.
  • Upgrades and downgrades take effect at the next renewal, matching monthly web subscriptions.
  • Profile Manage opens Kilo Pass plans. A different tier starts Google's deferred replacement flow. The current tier and Manage button open Google management.
  • The current paid tier, balance, and subscription history remain intact before renewal.
  • The replacement token is acknowledged immediately. A paid renewal updates the tier and uses the existing credit and streak rules.
  • Deferred callbacks can name the old product. The app now completes those callbacks.
  • The verifier selects the current entitlement from unordered old and future items. It validates the deferred order against the future product.
  • Token transfers preserve the subscription ID. Refunds use original receipt tokens despite the database cascade.

Verification

  • 348 backend tests passed across nine suites. 123 mobile tests passed across thirteen files.
  • All six transitions between 19, 49, and 199 have request and database coverage.
  • Tests cover deferred acknowledgement, duplicate completion, reordered expiry notifications, renewal, retargeting, stale tokens, refunded receipts, account mismatch, and another active provider.
  • Web and mobile TypeScript, scoped lint, formatting, and diff checks passed.
  • The earlier format-check failure named store-subscription-completion.test.ts. The final commit formats that file; local read-back passes.
  • Live downgrade 49 to19 passed approval, acknowledgement, retained current credits, renewal, and restore.
  • Live upgrade 19 to199 passed the same checks. Profile Manage opened the plan screen successfully.
  • Authenticated Google notifications and duplicate replay passed. Refund cleanup returned both accounts to their initial balances.
  • Accelerated Sandbox renewals stay in one calendar month. Calendar-boundary fixtures verify the new month's credit grant and streak.

Evidence and limits

  • Artifacts: tier-change-49-19, tier-change-deferred-restore, tier-change-renewed19-active, tier-change-19-199-active, and tier-change-renewed199.
  • The first 19 to49 attempt exposed the deferred-order and callback bugs. That replacement expired/refunded before acknowledgement. Its failure remains recorded.
  • The generic high-tier helper expected a fresh-purchase balance during the upgrade. The following active-state assertion and provider read-back passed with the correct unchanged balance.
  • Native tests prove one upgrade and one downgrade through renewal. All six combinations have automated fixture coverage, not six live Google passes.
  • Current native screenshots remain in the local artifacts. GitHub's browser session is signed out, so this round adds no hosted screenshot.
  • No backend deployment or mobile release occurred. Existing production release gates remain.

Cleanup

  • Original test account: 38 USD. Other test account: zero. No active test passes or unprocessed owned events remain.
  • Removed temporary IAM permission, local notification subscription, review databases, Jest configuration, and saved purchase payloads.

Comment thread apps/web/src/app/api/kilo-pass/play/notifications/route.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of Play replacement receipt identity (refund lookup by original purchase token, reconcile skip, deferred expiry/refund coverage) found no remaining issues.

Files Reviewed (3 files)
  • apps/web/src/lib/kilo-pass/google-play-notifications.ts
  • apps/web/src/lib/kilo-pass/google-play-notifications.test.ts
  • apps/web/src/lib/kilo-pass/google-play-subscription-state.ts
Previous Review Summaries (6 snapshots, latest commit 361e4c9)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 361e4c9)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of post-commit Play acknowledgement, retry-without-regrant, and older-voided-order coverage found no remaining issues.

Files Reviewed (2 files)
  • apps/web/src/lib/kilo-pass/google-play-notifications.ts
  • apps/web/src/lib/kilo-pass/google-play-notifications.test.ts

Previous review (commit 4d511fb)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of Play RTDN voided-purchase refunds, paid-order settlement, entitlement reconciliation, and bonus clawback found no remaining issues.

Files Reviewed (4 files)
  • apps/web/src/lib/kilo-pass/google-play-notifications.ts
  • apps/web/src/lib/kilo-pass/google-play-notifications.test.ts
  • apps/web/src/lib/kilo-pass/google-play-subscription-state.ts
  • apps/web/src/lib/kilo-pass/usage-triggered-bonus.ts

Previous review (commit a15a860)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of Play RTDN refund rollback, stale cancel/expiry reconciliation, JSON 400 handling, and restart cancel-flag replay found no remaining issues.

Files Reviewed (4 files)
  • apps/web/src/app/api/kilo-pass/play/notifications/route.ts
  • apps/web/src/app/api/kilo-pass/play/notifications/route.test.ts
  • apps/web/src/lib/kilo-pass/google-play-notifications.ts
  • apps/web/src/lib/kilo-pass/google-play-notifications.test.ts

Previous review (commit 05b7315)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Play RTDN OIDC auth now verifies the configured service-account email claim; incremental review found no remaining issues.

Files Reviewed (3 files)
  • ENVIRONMENT.md
  • apps/web/src/app/api/kilo-pass/play/notifications/route.ts
  • apps/web/src/app/api/kilo-pass/play/notifications/route.test.ts

Previous review (commit 280d7cc)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/app/api/kilo-pass/play/notifications/route.ts 21 Pub/Sub OIDC auth does not verify the service-account email claim
Files Reviewed (6 files)
  • apps/web/src/app/api/kilo-pass/play/notifications/route.ts - 1 issue
  • apps/web/src/app/api/kilo-pass/play/notifications/route.test.ts
  • apps/web/src/lib/kilo-pass/google-play-notifications.ts
  • apps/web/src/lib/kilo-pass/google-play-notifications.test.ts
  • apps/web/src/lib/kilo-pass/posthog-tracking.ts
  • apps/web/src/lib/kilo-pass/posthog-tracking.test.ts

Fix these issues in Kilo Cloud

Previous review (commit b909b8c)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/app/api/kilo-pass/play/notifications/route.ts 21 Pub/Sub OIDC auth does not verify the service-account email claim
Files Reviewed (6 files)
  • apps/web/src/app/api/kilo-pass/play/notifications/route.ts - 1 issue
  • apps/web/src/app/api/kilo-pass/play/notifications/route.test.ts
  • apps/web/src/lib/kilo-pass/google-play-notifications.ts
  • apps/web/src/lib/kilo-pass/google-play-notifications.test.ts
  • apps/web/src/lib/kilo-pass/posthog-tracking.ts
  • apps/web/src/lib/kilo-pass/posthog-tracking.test.ts

Fix these issues in Kilo Cloud


Reviewed by grok-4.6 · Input: 148.5K · Output: 17.7K · Cached: 685.3K

Review guidance: REVIEW.md from base branch android-iap-e895-s3

@iscekic iscekic added the human-ready The PR is ready for human review. label Aug 27, 2026
Comment thread apps/web/src/lib/kilo-pass/google-play-notifications.ts
Comment thread apps/web/src/lib/kilo-pass/google-play-notifications.ts
Comment thread apps/web/src/app/api/kilo-pass/play/notifications/route.ts Outdated
Three defects in the Play notification path:
- A failed credit clawback wrote a success audit and marked the revoke event
  processed, so refunded credits stayed with the buyer and Pub/Sub never
  redelivered. The clawback error now rolls the transaction back.
- A delayed expiry or cancellation ended a subscription that had already
  renewed or recovered. Both now reconcile against the Play state the
  handler already fetched, which is the source of truth.
- A request body that is not JSON returned 500, so Pub/Sub redelivered a
  message that can never succeed. It now returns 400.
@iscekic
iscekic merged commit 026be4e into main Sep 8, 2026
50 checks passed
@iscekic
iscekic deleted the android-iap-e895-s4 branch September 8, 2026 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

human-ready The PR is ready for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants