Skip to content

Add presented_workflow_id and presented_step_id to /receipts#3603

Merged
vegaro merged 8 commits into
mainfrom
cesar/receipt-presented-paywall-workflow-ids
Jun 23, 2026
Merged

Add presented_workflow_id and presented_step_id to /receipts#3603
vegaro merged 8 commits into
mainfrom
cesar/receipt-presented-paywall-workflow-ids

Conversation

@vegaro

@vegaro vegaro commented Jun 15, 2026

Copy link
Copy Markdown
Member

Motivation

The backend needs workflow and paywall identifiers on paywall events and post-receipt requests to properly attribute purchases and impressions to specific paywalls and workflow steps. This PR adds it to the post receipt

Source https://revenuecat.slack.com/archives/C0AGDQGUZ97/p1781206585550989

Example posted data:

Captura de pantalla 2026-06-16 a las 15 15 07

Note

Medium Risk
Changes the purchase receipt payload and attribution plumbing on a critical path, but fields are optional and backward-compatible; main risk is mis-attribution if workflow metadata resolution is wrong.

Overview
Adds workflow step attribution on purchase receipt posts by sending top-level presented_workflow_id and presented_step_id on /receipts, separate from the nested paywall object (workflow fields are not included in PaywallPostReceiptData).

Purchases core: Introduces WorkflowMetadata, persists it on LocalTransactionMetadata, and resolves it from the purchase-initiated paywall event (or cache) through PostReceiptHelper into Backend.postReceiptData.

Paywall events: PaywallEvent.Data gains optional stepId (with serialization/back-compat). RevenueCat UI fills workflowId/stepId when building event data and updates attribution when the same paywall is re-shown without a new impression (withCurrentWorkflowMetadata), so purchases and exit offers after a workflow step change still report the current step.

Reviewed by Cursor Bugbot for commit 39976ae. Bugbot is set up for automated code reviews on this repo. Configure here.

@vegaro vegaro changed the base branch from main to cesar/sdk-data-paywall-workflow-ids June 15, 2026 13:41
@vegaro vegaro changed the title Add paywall and workflow IDs to paywall events and post-receipt Add presented_workflow_id and presented_step_id to /receipts Jun 15, 2026
@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.28%. Comparing base (3357989) to head (39976ae).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
...tlin/com/revenuecat/purchases/PostReceiptHelper.kt 57.14% 0 Missing and 3 partials ⚠️
...uecat/purchases/common/caching/WorkflowMetadata.kt 66.66% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3603   +/-   ##
=======================================
  Coverage   80.27%   80.28%           
=======================================
  Files         379      380    +1     
  Lines       15557    15582   +25     
  Branches     2170     2179    +9     
=======================================
+ Hits        12489    12510   +21     
+ Misses       2203     2202    -1     
- Partials      865      870    +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Base automatically changed from cesar/sdk-data-paywall-workflow-ids to main June 16, 2026 08:39
The paywall-events workflow attribution (workflowId on PaywallEvent.Data and
the events payload) is already on main. This adds the post-receipt path so
native-store workflow purchases attribute to the exact screen, matching how
web funnels already attribute via presented_step_id on the RC Billing checkout
path.

- PaywallPostReceiptData carries workflowId and stepId as @transient fields so
  they are sent only as top-level presented_workflow_id / presented_step_id and
  do not leak into the nested paywall object (paywall_id stays in `paywall`).
- PaywallEvent.Data gains stepId with full serializer support; the step id is
  sourced from the current workflow step in PaywallViewModel.

presented_step_id is a no-op until khepri's /receipts endpoint accepts it
(WFL-336).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vegaro vegaro force-pushed the cesar/receipt-presented-paywall-workflow-ids branch from 1919b00 to f52062a Compare June 16, 2026 10:21
@vegaro vegaro marked this pull request as ready for review June 16, 2026 12:56
@vegaro vegaro requested review from a team as code owners June 16, 2026 12:56
workflowId and stepId were @transient on PaywallPostReceiptData, so they
were lost when the transaction was cached to disk. On retry or sync from
cached metadata, presented_workflow_id and presented_step_id would be
sent as null.

Introduces WorkflowMetadata (workflowId, stepId) as a first-class field
on LocalTransactionMetadata so the values survive disk round-trips.
PaywallPostReceiptData is now purely about the paywall; Backend reads
presented_workflow_id and presented_step_id from WorkflowMetadata instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b2bfd86. Configure here.

@vegaro vegaro force-pushed the cesar/receipt-presented-paywall-workflow-ids branch from f149079 to 9e22cc1 Compare June 18, 2026 15:21
@vegaro vegaro force-pushed the cesar/receipt-presented-paywall-workflow-ids branch from 9e22cc1 to 264d2e9 Compare June 18, 2026 16:38

@facumenzella facumenzella left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened two drafts in case you want to take a look. PR looks solid besides that

facumenzella and others added 2 commits June 23, 2026 14:27
### Motivation

Follow-up test coverage for #3603. That PR's refactor commit (`b2bfd86`)
fixed a bug where `workflowId`/`stepId` were `@Transient` on the cached
transaction metadata and were lost on the disk round-trip, so
`presented_workflow_id` / `presented_step_id` would be sent as `null` on
retry or unsynced replay. The field was promoted to a first-class
`workflowMetadata` on `LocalTransactionMetadata` with
`@SerialName("workflow_metadata")`, but there was no regression test
asserting it actually survives serialization.

### What this adds

One test in `LocalTransactionMetadataStoreTest`,
`cacheLocalTransactionMetadata round-trips workflow metadata`, mirroring
the existing `handles paywall data` test. The JSON written to
`SharedPreferences` is captured and fed back to
`getLocalTransactionMetadata`, so the real `JsonTools.json`
encode/decode runs (only SharedPreferences I/O is mocked).

### Verification (RED → GREEN)

- **RED**: with `workflowMetadata` marked `@Transient`, the test fails
at the assertion (`retrieved?.workflowMetadata` is `null`).
- **GREEN**: with `@SerialName("workflow_metadata")` (current state), it
passes.

Ran `:purchases:testDefaultsBc8DebugUnitTest --tests
"...LocalTransactionMetadataStoreTest"` locally (JDK 21).

Companion to #3636 (which covers the `PostReceiptHelper` threading and
replay paths). Together they close the two test gaps identified in
review of #3603.

<details>
<summary>AI session context</summary>

**Task:** Review #3603 ("is anything missing?"), then close the
identified test gaps.

**Findings from review of #3603:**
1. (important) Disk round-trip of `workflowMetadata` untested, the exact
bug `b2bfd86` fixed. `LocalTransactionMetadataStoreTest` had the
analogous `handles paywall data` round-trip test but nothing for
`workflowMetadata`.
2. (important) `PostReceiptHelper` threading untested (only an `any()`
count bumped). → covered by #3636.

**This PR** addresses finding #1.

**Decisions:**
- Reused the existing capture-the-written-JSON pattern so the test
exercises real serialization, not a mock, which is what makes it a
genuine guard for the `@Transient` regression.
- Based on `cesar/receipt-presented-paywall-workflow-ids` because the
`workflowMetadata` field only exists there, not on `main`.

**Verified:** RED→GREEN as described above. Production code
(`LocalTransactionMetadata.kt`) left unchanged; only the test file is
added.

**Not done:** The remaining review items on #3603 were questions, not
test gaps (all-or-nothing `WorkflowMetadata.from`, impression/purchase
step-id divergence under dedup, `pr:other` vs `pr:fix` label). Not
addressed here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
</details>

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…3636)

### Motivation

Follow-up to #3603 (cc @vegaro). That PR threads `presented_workflow_id`
/ `presented_step_id` through the post-receipt pipeline. While reviewing
it I found two coverage gaps, one of which is significant. This PR
**targets the #3603 branch** so the tests land with the feature.

### Changes

**1. `PostReceiptHelper` resolution coverage** (`PostReceiptHelperTest`)

The new resolution logic in `PostReceiptHelper` was only exercised
indirectly (`BackendTest` covers the wire format given explicit
metadata; `PaywallViewModelWorkflowTest` covers event creation). Added:

- resolves workflow metadata from the live presented paywall event
- omits workflow metadata when the presented event has no workflow (pins
the "both-or-neither" rule of `WorkflowMetadata.from`)
- replays cached workflow metadata on unsynced retry
(`postRemainingCachedTransactionMetadata`)

Supporting: `mockPostReceiptSuccess` now matches `workflowMetadata =
any()` (an omitted defaulted arg in MockK binds `eq(null)`), so the
success path can post non-null workflow metadata. Existing tests
unaffected.

**2. Fixed a vacuous re-presentation test + added EXIT_OFFER coverage**
(`PaywallViewModelWorkflowTest`)

The feature's own regression test, `purchase initiated after same
paywall workflow re-presentation carries current step metadata`, did
**not** actually exercise `withCurrentWorkflowMetadata`. Its two steps
pointed at different screens (`screen-1` vs `screen-2`), producing
different presentation fingerprints, so the second impression took the
**else** branch and recreated fresh event data, reading the live step.
The test passed even with `withCurrentWorkflowMetadata` removed.

- Reworked that test so both steps render the **same screen** (identical
fingerprint → de-dup branch is taken), with a "no new impression"
assertion proving the branch was reached.
- Added a sibling test for `EXIT_OFFER` after the same-fingerprint
re-presentation.
- Both verified **RED → GREEN**: with `withCurrentWorkflowMetadata`
neutralized they fail (`expected "step-2" but was "step-1"`); restored,
they pass.

### Validation

- `./gradlew :purchases:testDefaultsBc8DebugUnitTest --tests
"com.revenuecat.purchases.PostReceiptHelperTest"` , 86 tests, 0
failures.
- `./gradlew :ui:revenuecatui:testDefaultsBc8DebugUnitTest --tests
"com.revenuecat.purchases.ui.revenuecatui.data.PaywallViewModelWorkflowTest"`
, 0 failures.
- RED→GREEN proof captured by neutralizing the
`withCurrentWorkflowMetadata` line (run under JDK 21).

> [!IMPORTANT]
> Before #3603 merges: its `withCurrentWorkflowMetadata` change had no
test that exercised it (the existing regression test was vacuous). This
PR closes that gap. Recommend not approving #3603 until these tests are
in.

<details>
<summary>AI session context</summary>

# AI Context

## Metadata

- PR: this PR (base: `cesar/receipt-presented-paywall-workflow-ids`, the
#3603 branch)
- Branch: `facu/pr3603-postreceipt-workflow-tests`
- Author / human owner: facumenzella
- Agent(s): Claude Code, Opus 4.8 (1M context)
- Session source: current conversation
- Generated: 2026-06-22
- Context document version: 2

## Goal

Close test-coverage gaps found while reviewing #3603: (a)
`PostReceiptHelper`-layer workflow-metadata resolution, and (b) the
`withCurrentWorkflowMetadata` re-presentation behavior, whose existing
regression test turned out to be vacuous.

## Initial Prompt

Review #3603. The review surfaced coverage gaps; follow-ups were "write
a test for 1", "open a draft targeting this pr and mention this", then
"write it [finding #2] and put it in the same pr".

## Important Follow-up Prompts

- "Can you write a test for 1" , PostReceiptHelper coverage.
- "open a draft targetting this pr and mention this" , open this PR
against the #3603 branch.
- "write it and put it in the same pr" , add the finding #2
(CLOSE/EXIT_OFFER) test.
- Decision: "Fix it in my PR" , rework vegaro's vacuous
PURCHASE_INITIATED test here rather than only flagging it.

## Agent Contribution

- Added 3 `PostReceiptHelperTest` tests + made `mockPostReceiptSuccess`
workflow-agnostic.
- Added an `EXIT_OFFER` re-presentation test in
`PaywallViewModelWorkflowTest`.
- Discovered (via a neutralize + sentinel experiment) that the existing
PURCHASE_INITIATED re-presentation test never reached the
`withCurrentWorkflowMetadata` branch; reworked it to share one screen
and added a "no new impression" guard.
- Verified RED→GREEN for both UI tests by neutralizing the production
line.

## Human Decisions

- Open a separate branch / draft PR against the #3603 feature branch
rather than pushing onto @vegaro's branch.
- Fix the vacuous existing test inside this PR.

## Key Implementation Decisions

- Decision: force the same presentation fingerprint by pointing both
workflow steps at the same `screenId`.
- Rationale: the de-dup branch (where `withCurrentWorkflowMetadata`
lives) only runs when fingerprints match; different screens silently
routed the test through fresh re-creation.
- Rejected: leaving the original different-screen fixture (it does not
test the fix).
- Decision: assert "no new IMPRESSION" after the re-presentation.
- Rationale: proves the de-dup branch was taken; this is the guard that
distinguishes a real test from a vacuous one.
- Decision: `mockPostReceiptSuccess` uses `workflowMetadata = any()`.
  - Rationale: omitted defaulted MockK arg binds `eq(null)`.

## Files / Symbols Touched

-
`purchases/src/test/java/com/revenuecat/purchases/PostReceiptHelperTest.kt`
  - Why: PostReceiptHelper-layer resolution coverage.
- Symbols: `mockPostReceiptSuccess`, new `@Test`s (presented-paywall
resolution, no-workflow omission,
`postRemainingCachedTransactionMetadata`).
-
`ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelWorkflowTest.kt`
- Why: make the re-presentation test exercise
`withCurrentWorkflowMetadata`; add EXIT_OFFER coverage.
- Symbols: `purchase initiated after same paywall workflow
re-presentation...`, `exit offer after same paywall workflow
re-presentation...`.

## Dependencies / Config / Migrations

- None.

## Validation

- Commands run:
- `:purchases:testDefaultsBc8DebugUnitTest --tests
"...PostReceiptHelperTest"`: 86 tests, 0 failures.
- `:ui:revenuecatui:testDefaultsBc8DebugUnitTest --tests
"...PaywallViewModelWorkflowTest"`: 0 failures.
- RED check: with `withCurrentWorkflowMetadata` neutralized, both
re-presentation tests fail (`expected "step-2" but was "step-1"`);
restored, they pass.
- Manual verification: Not run (test-only change).
- CI: Not captured at time of writing.

## Validation Gaps

- The cached-vs-live precedence in `PostReceiptHelper` is exercised via
the live and retry paths but not via a dedicated synchronous-purchase
test where `getLocalTransactionMetadata` returns metadata.
- Local runs required JDK 21 (project `.sdkmanrc` pins `21.0.6-librca`);
default daemon here was JDK 17, which Robolectric rejects for Android
SDK 36.

## Review Focus

- Confirm the "no new impression" assertion correctly captures the
de-dup branch for both UI tests.
- Confirm `mockPostReceiptSuccess`'s `workflowMetadata = any()`
relaxation does not weaken the negative-case test (`...has no
workflow`), which asserts `workflowMetadata = null` explicitly.

## Risks / Reviewer Notes

- Risk: test-only change; no production behavior altered.
- Evidence: diff touches only `*Test.kt` files (the PaywallViewModel
edit during RED→GREEN was reverted; prod tree confirmed clean).
  - Mitigation: none needed.

## Non-goals / Out of Scope

- No production code changes; `withCurrentWorkflowMetadata` itself is
unchanged.

## Omitted Context

- Raw transcript, unrelated exploration, sensitive details, repetitive
attempts, and chain-of-thought-style content were omitted.

</details>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vegaro vegaro added this pull request to the merge queue Jun 23, 2026
Merged via the queue into main with commit c55de4e Jun 23, 2026
37 checks passed
@vegaro vegaro deleted the cesar/receipt-presented-paywall-workflow-ids branch June 23, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants