Skip to content

Commit

Permalink
Merge pull request #51 from dietmap/verify-user-orders
Browse files Browse the repository at this point in the history
Add endpoint for on-demand user orders re-submission/verification
  • Loading branch information
wojtekbauman authored Sep 10, 2020
2 parents 2097aa0 + 45b94ac commit 93edece
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import org.springframework.web.server.ResponseStatusException
import java.nio.charset.StandardCharsets.UTF_8
import javax.validation.Valid
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotEmpty


@ConditionalOnProperty("yaak.google-play.enabled", havingValue = "true")
Expand All @@ -40,6 +41,12 @@ class GooglePlaySubscriptionController(val subscriptionService: GooglePlaySubscr
subscriptionService.cancelPurchase(cancelRequest)
}

@PostMapping("/api/googleplay/subscriptions/orders/verify")
fun verifyOrders(@RequestBody @Valid ordersRequest: VerifyOrdersRequest): VerifyOrdersResponse {
logger.info { "Received user orders for verification: ${ordersRequest.orders}" }
return VerifyOrdersResponse(subscriptionService.verifyOrders(ordersRequest.orders))
}

/**
* Publicly accessible PubSub notification webhook
*/
Expand All @@ -59,7 +66,18 @@ data class PurchaseRequest(
@NotBlank
val purchaseToken: String,
val orderingUserId: String? = null,
val discountCode: String? = null
val discountCode: String? = null,
val purchaseTime: Long? = null,
val effectivePrice: Long? = null
)

data class VerifyOrdersRequest(
@NotEmpty
val orders: Collection<PurchaseRequest>
)

data class VerifyOrdersResponse(
val hasActiveSubscription: Boolean
)

data class SubscriptionCancelRequest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,41 +20,29 @@ import java.math.BigDecimal
@ConditionalOnBean(AndroidPublisher::class)
@Service
class GooglePlaySubscriptionService(val androidPublisherApiClient: AndroidPublisher, val userAppClient: UserAppClient) {

private val PAYMENT_RECEIVED_CODE = 1
private val PAYMENT_FREE_TRIAL_CODE = 2
private val USER_ACCOUNT_ID_KEY = "obfuscatedExternalAccountId"
private val USER_APP_STATUS_ACTIVE = "ACTIVE"
private val logger = KotlinLogging.logger { }

fun handlePurchase(purchaseRequest: PurchaseRequest, initialPurchase: Boolean = true): SubscriptionPurchase? {
val subscription = androidPublisherApiClient.Purchases().Subscriptions().get(purchaseRequest.packageName, purchaseRequest.subscriptionId, purchaseRequest.purchaseToken).execute()
fun handlePurchase(purchaseRequest: PurchaseRequest): SubscriptionPurchase? {
val subscription = androidPublisherApiClient.purchases().subscriptions().get(purchaseRequest.packageName, purchaseRequest.subscriptionId, purchaseRequest.purchaseToken).execute()
checkArgument(subscription.paymentState in listOf(PAYMENT_RECEIVED_CODE, PAYMENT_FREE_TRIAL_CODE)) { "Subscription has not been paid yet, paymentState=${subscription.paymentState}" }

var effectivePrice = BigDecimal(subscription.priceAmountMicros).divide(BigDecimal(1000 * 1000))

// test purchase
if (subscription.purchaseType == 0) {
effectivePrice = BigDecimal(0.0)
}

// introductory price purchase
if (!subscription.orderId.contains("..")) {
if (subscription.introductoryPriceInfo?.introductoryPriceAmountMicros != null) {
effectivePrice = BigDecimal(subscription.introductoryPriceInfo.introductoryPriceAmountMicros).divide(BigDecimal(1000 * 1000))
}
}

logger.info { "Handling purchase: $subscription, initial: $initialPurchase" }
logger.info { "Handling purchase: $subscription, initial: ${subscription.isInitialPurchase()}" }

val notificationResponse = userAppClient.sendSubscriptionNotification(UserAppSubscriptionNotification(
notificationType = if (initialPurchase) NotificationType.SUBSCRIPTION_PURCHASED else NotificationType.SUBSCRIPTION_RENEWED,
notificationType = if (subscription.isInitialPurchase()) NotificationType.SUBSCRIPTION_PURCHASED else NotificationType.SUBSCRIPTION_RENEWED,
appMarketplace = AppMarketplace.GOOGLE_PLAY,
countryCode = subscription.countryCode,
price = effectivePrice,
price = subscription.calculateEffectivePrice(purchaseRequest.effectivePrice),
currencyCode = subscription.priceCurrencyCode,
transactionId = subscription.orderId,
originalTransactionId = toInitialOrderId(subscription.orderId),
originalTransactionId = subscription.getInitialOrderId(),
productId = purchaseRequest.subscriptionId,
description = "Google Play ${if (initialPurchase) "initial" else "renewal"} subscription order",
description = "Google Play ${if (subscription.isInitialPurchase()) "initial" else "renewal"} subscription order",
orderingUserId = purchaseRequest.orderingUserId ?: subscription[USER_ACCOUNT_ID_KEY] as String?,
discountCode = purchaseRequest.discountCode,
expiryTimeMillis = subscription.expiryTimeMillis,
Expand All @@ -68,7 +56,7 @@ class GooglePlaySubscriptionService(val androidPublisherApiClient: AndroidPublis
val content = SubscriptionPurchasesAcknowledgeRequest().setDeveloperPayload("{ applicationOrderId: ${notificationResponse?.orderId}, orderingUserId: ${purchaseRequest.orderingUserId} }")
androidPublisherApiClient.Purchases().Subscriptions().acknowledge(purchaseRequest.packageName, purchaseRequest.subscriptionId, purchaseRequest.purchaseToken, content).execute()
}
return subscription;
return subscription
}

fun cancelPurchase(cancelRequest: SubscriptionCancelRequest) {
Expand All @@ -81,20 +69,13 @@ class GooglePlaySubscriptionService(val androidPublisherApiClient: AndroidPublis
}
}

private fun toInitialOrderId(orderId: String?): String {
return if (orderId != null) {
val split = orderId.split("..")
return split[0]
} else ""
}

fun handleSubscriptionNotification(pubsubNotification: PubSubDeveloperNotification) {
pubsubNotification.subscriptionNotification?.let {
logger.info { "Handling PubSub notification of type: ${it.notificationType}" }
try {
when (it.notificationType) {
SUBSCRIPTION_PURCHASED -> handlePurchase(PurchaseRequest(pubsubNotification.packageName, it.subscriptionId, it.purchaseToken))
SUBSCRIPTION_RENEWED -> handlePurchase(PurchaseRequest(pubsubNotification.packageName, it.subscriptionId, it.purchaseToken),false)
SUBSCRIPTION_RENEWED -> handlePurchase(PurchaseRequest(pubsubNotification.packageName, it.subscriptionId, it.purchaseToken))
else -> handleStatusUpdate(pubsubNotification.packageName, it)
}
} catch (e: Exception) {
Expand All @@ -104,6 +85,13 @@ class GooglePlaySubscriptionService(val androidPublisherApiClient: AndroidPublis
}
}

fun verifyOrders(orders: Collection<PurchaseRequest>): Boolean {
orders
.map(::handlePurchase)
.also { logger.info { "Verified ${it.size} user orders" } }
return userAppClient.checkSubscription()?.status == USER_APP_STATUS_ACTIVE
}

private fun handleStatusUpdate(packageName: String, notification: GooglePlaySubscriptionNotification) {
val subscription = androidPublisherApiClient.Purchases().Subscriptions().get(packageName, notification.subscriptionId, notification.purchaseToken).execute()
logger.debug { "Google Play subscription details: $subscription" }
Expand All @@ -117,12 +105,30 @@ class GooglePlaySubscriptionService(val androidPublisherApiClient: AndroidPublis
price = BigDecimal(subscription.priceAmountMicros).divide(BigDecimal(1000 * 1000)),
currencyCode = subscription.priceCurrencyCode,
transactionId = subscription.orderId,
originalTransactionId = toInitialOrderId(subscription.orderId),
originalTransactionId = subscription.getInitialOrderId(),
appMarketplace = AppMarketplace.GOOGLE_PLAY,
expiryTimeMillis = subscription.expiryTimeMillis,
googlePlayPurchaseDetails = GooglePlayPurchaseDetails(packageName, notification.subscriptionId, notification.purchaseToken)
)
userAppClient.sendSubscriptionNotification(subscriptionUpdate)
logger.info { "Google Play subscription notification has been sent to user app: $subscriptionUpdate" }
}

private fun SubscriptionPurchase.calculateEffectivePrice(effectivePrice: Long?) = when {
effectivePrice != null -> effectivePrice
isTestPurchase() -> 0L
isIntroductoryPricePurchase() -> introductoryPriceInfo.introductoryPriceAmountMicros
else -> priceAmountMicros
}
.let(::BigDecimal)
.let { it.divide(BigDecimal(1000 * 1000)) }

private fun SubscriptionPurchase.isTestPurchase() = purchaseType == 0

private fun SubscriptionPurchase.isInitialPurchase() = !orderId.contains("..")

private fun SubscriptionPurchase.isIntroductoryPricePurchase() = isInitialPurchase() && introductoryPriceInfo?.introductoryPriceAmountMicros != null

private fun SubscriptionPurchase.getInitialOrderId() = orderId?.let { it.split("..")[0] } ?: ""

}
17 changes: 16 additions & 1 deletion src/main/kotlin/com/dietmap/yaak/domain/userapp/UserAppClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ import java.util.function.Consumer


@Component
class UserAppClient(val webClient: WebClient, @Value("\${yaak.user-app.subscription-webhook-url}") handleSubscriptionUpdateUrl: String) {
class UserAppClient(
val webClient: WebClient,
@Value("\${yaak.user-app.subscription-webhook-url}") handleSubscriptionUpdateUrl: String,
@Value("\${yaak.user-app.subscription-check-url}") checkSubscriptionUrl: String) {

private val subscriptionNotificationUrl: String = handleSubscriptionUpdateUrl
private val subscriptionCheckUrl: String = checkSubscriptionUrl
private val logger = KotlinLogging.logger { }

fun sendSubscriptionNotification(notification: UserAppSubscriptionNotification): UserAppSubscriptionOrder? {
Expand All @@ -47,6 +51,17 @@ class UserAppClient(val webClient: WebClient, @Value("\${yaak.user-app.subscript
.bodyToFlux(UserAppSubscriptionOrder::class.java)
.blockFirst()
}

fun checkSubscription(): UserAppSubscriptionStatus? {
logger.debug { "Checking user subscription in user app" }
return webClient.get()
.uri(subscriptionCheckUrl)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToFlux(UserAppSubscriptionStatus::class.java)
.blockFirst()
}

}

@Configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ data class UserAppSubscriptionOrder (
val status: String
)

data class UserAppSubscriptionStatus(
val status: String
)

data class GooglePlayPurchaseDetails(
val packageName: String,
val subscriptionId: String,
Expand Down
3 changes: 2 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ yaak:
api-key: MyApiKey123

user-app:
subscription-webhook-url: http://localhost:8080/subscriptions/notifications/yaak
subscription-webhook-url: http://localhost:8086/user-subscription/notifications/yaak
subscription-check-url: http://localhost:8086/user-subscription

google-play:
enabled: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ internal class GooglePlaySubscriptionControllerTest : SupportController() {
orderingUserId = "1",
discountCode = "234"
)
`when`(subscriptionService.handlePurchase(request, true)).thenThrow(ResponseStatusException(HttpStatus.BAD_REQUEST, "Error communicating with user app"))
`when`(subscriptionService.handlePurchase(request)).thenThrow(ResponseStatusException(HttpStatus.BAD_REQUEST, "Error communicating with user app"))

mockMvc.perform(
post("/api/googleplay/subscriptions/purchases")
Expand Down

0 comments on commit 93edece

Please sign in to comment.