Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.personalization.demo

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class TrackPurchaseDemoE2ETest {

@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)

@Test
fun trackPurchaseMinimal_tapButton_noCrash() {
onView(withId(R.id.btnTrackPurchaseMinimal)).perform(click())
Thread.sleep(800)
}

@Test
fun trackPurchaseFull_tapButton_noCrash() {
onView(withId(R.id.btnTrackPurchaseFull)).perform(click())
Thread.sleep(800)
}
}
114 changes: 114 additions & 0 deletions demo-app/src/main/java/com/personalization/demo/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import com.personalization.Params
import com.personalization.Params.TrackEvent
import com.personalization.SDK
import com.personalization.api.OnApiCallbackListener
import com.personalization.api.models.purchase.PurchaseItemRequest
import com.personalization.api.models.purchase.PurchaseTrackingRequest
import com.personalization.api.params.ProductItemParams
import com.personalization.api.params.PurchasePredictParams
import com.personalization.demo.BuildConfig
Expand Down Expand Up @@ -42,6 +44,16 @@ class MainActivity : AppCompatActivity() {
const val DEMO_AMOUNT = 1
}

private object DemoPurchaseTrackingConstants {
const val ORDER_ID_MINIMAL = "android-demo-order-minimal"
const val ORDER_ID_FULL = "android-demo-order-full"
const val ORDER_PRICE_MINIMAL = 199.0
const val ORDER_PRICE_FULL = 999.0
const val ITEM_ID = "android-demo-sku-001"
const val ITEM_AMOUNT = 1
const val ITEM_PRICE = 99.0
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Expand Down Expand Up @@ -101,6 +113,108 @@ class MainActivity : AppCompatActivity() {
PurchasePredictParams(email = getString(R.string.predict_demo_email))
)
}

findViewById<Button>(R.id.btnTrackPurchaseMinimal).setOnClickListener {
trackPurchaseMinimal()
}

findViewById<Button>(R.id.btnTrackPurchaseFull).setOnClickListener {
trackPurchaseFull()
}
}

private fun trackPurchaseMinimal() {
val request = PurchaseTrackingRequest(
orderId = DemoPurchaseTrackingConstants.ORDER_ID_MINIMAL,
orderPrice = DemoPurchaseTrackingConstants.ORDER_PRICE_MINIMAL,
items = listOf(
PurchaseItemRequest(
id = DemoPurchaseTrackingConstants.ITEM_ID,
amount = DemoPurchaseTrackingConstants.ITEM_AMOUNT,
price = DemoPurchaseTrackingConstants.ITEM_PRICE,
),
),
)
sdk.trackPurchase(
request,
object : OnApiCallbackListener() {
override fun onSuccess(response: JSONObject?) {
runOnUiThread {
Toast.makeText(
this@MainActivity,
getString(R.string.track_purchase_ok),
Toast.LENGTH_LONG,
).show()
}
}

override fun onError(code: Int, msg: String?) {
runOnUiThread {
Toast.makeText(
this@MainActivity,
"${getString(R.string.track_purchase_fail)}: $msg",
Toast.LENGTH_LONG,
).show()
}
}
},
)
}

private fun trackPurchaseFull() {
val request = PurchaseTrackingRequest(
orderId = DemoPurchaseTrackingConstants.ORDER_ID_FULL,
orderPrice = DemoPurchaseTrackingConstants.ORDER_PRICE_FULL,
items = listOf(
PurchaseItemRequest(
id = DemoPurchaseTrackingConstants.ITEM_ID,
amount = 2,
price = 49.99,
quantity = 2,
lineId = "demo-line-1",
fashionSize = "L",
),
),
deliveryType = "courier",
deliveryAddress = "Demo address",
paymentType = "card",
isTaxFree = true,
promocode = "DEMO10",
orderCash = 100.0,
orderBonuses = 10.0,
orderDelivery = 5.0,
orderDiscount = 15.0,
channel = "mobile",
custom = mapOf("demo_custom" to "android_demo"),
recommendedBy = Params.RecommendedBy(Params.RecommendedBy.TYPE.RECOMMENDATION, "demo-block"),
recommendedSource = JSONObject().put("source_key", "source_value"),
stream = "demo-stream",
segment = "A",
)
sdk.trackPurchase(
request,
object : OnApiCallbackListener() {
override fun onSuccess(response: JSONObject?) {
runOnUiThread {
Toast.makeText(
this@MainActivity,
getString(R.string.track_purchase_ok),
Toast.LENGTH_LONG,
).show()
}
}

override fun onError(code: Int, msg: String?) {
runOnUiThread {
Toast.makeText(
this@MainActivity,
"${getString(R.string.track_purchase_fail)}: $msg",
Toast.LENGTH_LONG,
).show()
}
}
},
)
}

private fun predictPurchase(params: PurchasePredictParams) {
Expand Down
14 changes: 14 additions & 0 deletions demo-app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@
android:text="@string/predict_with_email"
android:layout_marginTop="16dp" />

<Button
android:id="@+id/btnTrackPurchaseMinimal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/track_purchase_minimal"
android:layout_marginTop="24dp" />

<Button
android:id="@+id/btnTrackPurchaseFull"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/track_purchase_full"
android:layout_marginTop="16dp" />

</LinearLayout>


4 changes: 4 additions & 0 deletions demo-app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
<string name="predict_demo_email">predict-demo@example.com</string>
<string name="predict_ok">probability=%1$.4f client_id=%2$s</string>
<string name="predict_fail">predict failed: %1$d %2$s</string>
<string name="track_purchase_minimal">Track purchase (minimal)</string>
<string name="track_purchase_full">Track purchase (full)</string>
<string name="track_purchase_ok">trackPurchase: sent</string>
<string name="track_purchase_fail">trackPurchase failed</string>
</resources>


15 changes: 15 additions & 0 deletions personalization-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# personalization-sdk changelog

## Unreleased

### Features

* Strict purchase tracking: `SDK.trackPurchase(PurchaseTrackingRequest, …)` with `PurchaseItemRequest` / `PurchaseTrackingRequest` (camelCase in public API; wire keys snake_case inside serialization). Client validation before network; `tax_free` only when `isTaxFree` is true; optional fields omitted when unset. Demo app and Espresso e2e taps for minimal and full payloads.

### Deprecations

* `TrackEvent.PURCHASE` (and `track(TrackEvent.PURCHASE, Params, ...)`) — use `SDK.trackPurchase(PurchaseTrackingRequest, ...)` instead.

## Earlier releases

See the repository root and release tags for history prior to this file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.personalization.features.trackEvent.impl

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.personalization.Params
import com.personalization.api.models.purchase.PurchaseItemRequest
import com.personalization.api.models.purchase.PurchaseTrackingRequest
import com.personalization.api.models.purchase.PurchaseTrackingWireKeys
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class PurchaseTrackingJsonBuilderInstrumentedTest {

@Test
fun buildOrError_minimal_containsRequiredKeysAndOmitsOptionals() {
val request = PurchaseTrackingRequest(
orderId = "order-1",
orderPrice = 99.5,
items = listOf(
PurchaseItemRequest(id = "p1", amount = 2, price = 10.0),
),
)
val json = PurchaseTrackingJsonBuilder.buildOrError(request).getOrThrow()
assertEquals("purchase", json.getString(PurchaseTrackingWireKeys.EVENT))
assertEquals("order-1", json.getString(PurchaseTrackingWireKeys.ORDER_ID))
assertEquals(99.5, json.getDouble(PurchaseTrackingWireKeys.ORDER_PRICE), 0.0001)
assertFalse(json.has(PurchaseTrackingWireKeys.TAX_FREE))
assertFalse(json.has(PurchaseTrackingWireKeys.CUSTOM))
val items = json.getJSONArray(PurchaseTrackingWireKeys.ITEMS)
assertEquals(1, items.length())
val row = items.getJSONObject(0)
assertEquals("p1", row.getString(PurchaseTrackingWireKeys.ID))
assertEquals(2, row.getInt(PurchaseTrackingWireKeys.AMOUNT))
assertEquals(10.0, row.getDouble(PurchaseTrackingWireKeys.PRICE), 0.0001)
assertFalse(row.has(PurchaseTrackingWireKeys.QUANTITY))
}

@Test
fun buildOrError_full_includesOptionalFields() {
val request = PurchaseTrackingRequest(
orderId = "order-2",
orderPrice = 200.0,
items = listOf(
PurchaseItemRequest(
id = "p2",
amount = 1,
price = 50.0,
quantity = 1,
lineId = "line-1",
fashionSize = "M",
),
),
deliveryType = "pickup",
deliveryAddress = "Street 1",
paymentType = "cash",
isTaxFree = true,
promocode = "PROMO",
orderCash = 10.0,
orderBonuses = 5.0,
orderDelivery = 2.0,
orderDiscount = 1.0,
channel = "mobile",
custom = mapOf("k" to "v"),
recommendedBy = Params.RecommendedBy(Params.RecommendedBy.TYPE.RECOMMENDATION, "code-1"),
recommendedSource = JSONObject().put("src", 1),
stream = "s1",
segment = "B",
)
val json = PurchaseTrackingJsonBuilder.buildOrError(request).getOrThrow()
assertTrue(json.getBoolean(PurchaseTrackingWireKeys.TAX_FREE))
assertEquals("pickup", json.getString(PurchaseTrackingWireKeys.DELIVERY_TYPE))
assertTrue(json.has(PurchaseTrackingWireKeys.CUSTOM))
assertTrue(json.has(PurchaseTrackingWireKeys.RECOMMENDED_SOURCE))
assertEquals("dynamic", json.getString(Params.InternalParameter.RECOMMENDED_BY.value))
assertEquals("code-1", json.getString(Params.InternalParameter.RECOMMENDED_CODE.value))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ class Params : AbstractParams<Params>() {
CATEGORY("category"),
CART("cart"),
REMOVE_FROM_CART("remove_from_cart"),
@Deprecated(
message = "Use SDK.trackPurchase(PurchaseTrackingRequest, listener) instead of track(TrackEvent.PURCHASE, Params).",
level = DeprecationLevel.WARNING,
)
PURCHASE("purchase"),
SEARCH("search"),
WISH("wish"),
Expand Down
13 changes: 13 additions & 0 deletions personalization-sdk/src/main/kotlin/com/personalization/SDK.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.google.firebase.messaging.RemoteMessage
import com.personalization.Params.InternalParameter
import com.personalization.Params.TrackEvent
import com.personalization.api.OnApiCallbackListener
import com.personalization.api.models.purchase.PurchaseTrackingRequest
import com.personalization.api.managers.CartManager
import com.personalization.api.managers.InAppNotificationManager
import com.personalization.api.managers.ProductsManager
Expand Down Expand Up @@ -529,6 +530,18 @@ open class SDK {
)
}

/**
* Strict purchase tracking (`push`, event = `purchase`).
*
* Prefer this over [track] with [TrackEvent.PURCHASE] and manual [Params] assembly.
*/
fun trackPurchase(
request: PurchaseTrackingRequest,
listener: OnApiCallbackListener? = null,
) {
trackEventManager.trackPurchase(request, listener)
}

/**
* Tracking custom events
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.personalization.api.managers
import com.personalization.Params
import com.personalization.Params.TrackEvent
import com.personalization.api.OnApiCallbackListener
import com.personalization.api.models.purchase.PurchaseTrackingRequest

interface TrackEventManager {

Expand All @@ -27,6 +28,14 @@ interface TrackEventManager {
listener: OnApiCallbackListener? = null
)

/**
* Strict purchase tracking (`event` = `purchase`) with typed request (parity with iOS / React Native).
*/
fun trackPurchase(
request: PurchaseTrackingRequest,
listener: OnApiCallbackListener? = null,
)

/**
* Custom event tracking (aligned with the iOS SDK `trackEvent`).
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.personalization.api.models.purchase

/**
* One purchased line item (strict mobile contract).
*
* Required: [id], [amount], [price].
* Optional: [quantity], [lineId], [fashionSize] — omit from the constructor when not needed (defaults).
*/
data class PurchaseItemRequest(
val id: String,
val amount: Int,
val price: Double,
val quantity: Int? = null,
val lineId: String? = null,
val fashionSize: String? = null,
)
Loading
Loading