Files
Facundo MenzellaandClaude Opus 5.5 3d03673f61 other: attribute checkpoint offering purchases to their hit (#8058)
* Attribute checkpoint offering purchases to the hit trace id

Put the hit's trace id on the matched offering's presented context and send it as presented_trace_id on receipts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Require traceID in withCheckpointIdentifier

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Use the checkpoint trace id for the offering's workflow

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Carry the hit trace id on matchedOffering

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Require traceID in withPresentedCheckpointIdentifier and tighten trace id tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Create the matched offering and its trace id in one place

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 16:53:46 +02:00

157 lines
5.3 KiB
Swift

//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// CheckpointEventDataTests.swift
import Foundation
import Nimble
import XCTest
@_spi(Internal) @testable import RevenueCat
class CheckpointEventDataTests: TestCase {
private let date = Date(timeIntervalSince1970: 1_699_270_688.995)
func testMatchedWorkflowReportsPresentUIWithTheWorkflow() throws {
let workflow = Self.workflow(id: "wf_123")
let resolved = ResolvedCheckpoint(
.matchedWorkflow(.init(workflow: workflow,
uiConfig: .empty,
offerings: Self.offerings)),
checkpointRuleID: "rule_123"
)
let data = CheckpointEvent.Data(identifier: "onboarding_complete", date: self.date, resolved: resolved)
expect(data.result) == .presentUI
expect(data.workflowID) == "wf_123"
expect(data.checkpointRuleID) == "rule_123"
expect(data.offeringID).to(beNil())
}
/// The workflow's step events and receipt carry its trace id, so the hit has to report the same one.
func testMatchedWorkflowReportsTheWorkflowTraceId() throws {
let matched = ResolvedCheckpointWorkflow(workflow: Self.workflow(id: "wf_123"),
uiConfig: .empty,
offerings: Self.offerings)
let data = CheckpointEvent.Data(identifier: "onboarding_complete",
date: self.date,
resolved: .init(.matchedWorkflow(matched)))
expect(data.traceID) == matched.traceId
}
func testEveryResultReportsATraceId() throws {
let resolutions: [CheckpointResolution] = [
.matchedOffering(Self.offering, traceID: "trace_id"),
.noAction(.noMatch),
.noAction(.configurationUnavailable),
.noAction(.unknownCheckpoint)
]
for resolution in resolutions {
let data = CheckpointEvent.Data(identifier: "onboarding_complete",
date: self.date,
resolved: .init(resolution))
expect(data.traceID).toNot(beNil())
}
}
func testEachEvaluationGetsItsOwnTraceId() throws {
let first = ResolvedCheckpoint(.noAction(.noMatch))
let second = ResolvedCheckpoint(.noAction(.noMatch))
expect(first.traceID) != second.traceID
}
func testMatchedOfferingReportsReturnDataWithTheOffering() throws {
let resolved = ResolvedCheckpoint(
.matchedOffering(Self.offering, traceID: "trace_id"),
checkpointRuleID: "rule_123"
)
let data = CheckpointEvent.Data(identifier: "onboarding_complete", date: self.date, resolved: resolved)
expect(data.result) == .returnData
expect(data.offeringID) == "onboarding"
expect(data.checkpointRuleID) == "rule_123"
expect(data.workflowID).to(beNil())
}
func testEachNoActionReasonMapsToItsResult() throws {
let expected: [CheckpointResolutionReason: CheckpointHitResult] = [
.noMatch: .noMatch,
.configurationUnavailable: .configurationUnavailable,
.unknownCheckpoint: .unknownCheckpoint
]
for (reason, result) in expected {
let data = CheckpointEvent.Data(identifier: "onboarding_complete",
date: self.date,
resolved: .init(.noAction(reason)))
expect(data.result) == result
expect(data.workflowID).to(beNil())
expect(data.offeringID).to(beNil())
expect(data.checkpointRuleID).to(beNil())
}
}
func testEveryHitIsCustom() throws {
let data = CheckpointEvent.Data(identifier: "onboarding_complete",
date: self.date,
resolved: .init(.noAction(.noMatch)))
expect(data.checkpointType) == .custom
}
// MARK: - Helpers
private static let offering = Offering(
identifier: "onboarding",
serverDescription: "Onboarding offering",
availablePackages: [],
webCheckoutUrl: nil
)
private static var offerings: Offerings {
let response = OfferingsResponse(
currentOfferingId: nil,
offerings: [],
placements: nil,
targeting: nil,
uiConfig: nil
)
return Offerings(
offerings: [Self.offering.identifier: Self.offering],
currentOfferingID: nil,
placements: nil,
targeting: nil,
contents: Offerings.Contents(response: response, httpResponseOriginalSource: .mainServer),
loadedFromDiskCache: false
)
}
private static func workflow(id: String) -> PublishedWorkflow {
return PublishedWorkflow(
id: id,
displayName: "Test",
initialStepId: "step_1",
singleStepFallbackId: nil,
steps: [:],
screens: [:]
)
}
}