Merge branch 'cart_utils' into 'master'
Cart utils See merge request touchinstinct/RoboSwag!3
This commit is contained in:
commit
54d2482064
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
apply from: "../android-configs/lib-config.gradle"
|
||||
|
||||
dependencies {
|
||||
def coroutinesVersion = '1.6.4'
|
||||
def junitVersion = '4.13.2'
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
|
||||
testImplementation("junit:junit")
|
||||
|
||||
constraints {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") {
|
||||
version {
|
||||
require(coroutinesVersion)
|
||||
}
|
||||
}
|
||||
testImplementation("junit:junit") {
|
||||
version {
|
||||
require(junitVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<manifest package="ru.touchin.roboswag.core.cart_utils" />
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package ru.touchin.roboswag.cart_utils.models
|
||||
|
||||
abstract class CartModel<TProductModel : ProductModel> {
|
||||
|
||||
abstract val products: List<TProductModel>
|
||||
|
||||
open val promocodeList: List<PromocodeModel> = emptyList()
|
||||
|
||||
open val availableBonuses: Int = 0
|
||||
open val usedBonuses: Int = 0
|
||||
|
||||
val availableProducts: List<TProductModel>
|
||||
get() = products.filter { it.isAvailable && !it.isDeleted }
|
||||
|
||||
val totalPrice: Int
|
||||
get() = availableProducts.sumOf { it.countInCart * it.price }
|
||||
|
||||
val totalBonuses: Int
|
||||
get() = availableProducts.sumOf { it.countInCart * (it.bonuses ?: 0) }
|
||||
|
||||
fun getPriceWithPromocode(): Int = promocodeList
|
||||
.sortedByDescending { it.discount is PromocodeDiscount.ByPercent }
|
||||
.fold(initial = totalPrice) { price, promo ->
|
||||
promo.discount.applyTo(price)
|
||||
}
|
||||
|
||||
abstract fun <TCart> copyWith(
|
||||
products: List<TProductModel> = this.products,
|
||||
promocodeList: List<PromocodeModel> = this.promocodeList,
|
||||
usedBonuses: Int = this.usedBonuses
|
||||
): TCart
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <TCart> asCart() = this as TCart
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package ru.touchin.roboswag.cart_utils.models
|
||||
|
||||
abstract class ProductModel {
|
||||
abstract val id: Int
|
||||
abstract val countInCart: Int
|
||||
abstract val price: Int
|
||||
abstract val isAvailable: Boolean
|
||||
abstract val isDeleted: Boolean
|
||||
|
||||
open val bonuses: Int? = null
|
||||
|
||||
open val variants: List<ProductModel> = emptyList()
|
||||
open val selectedVariantId: Int? = null
|
||||
|
||||
val selectedVariant get() = variants.find { it.id == selectedVariantId }
|
||||
|
||||
abstract fun <TProduct> copyWith(
|
||||
countInCart: Int = this.countInCart,
|
||||
isDeleted: Boolean = this.isDeleted,
|
||||
selectedVariantId: Int? = this.selectedVariantId
|
||||
): TProduct
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <TProduct> asProduct(): TProduct = this as TProduct
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package ru.touchin.roboswag.cart_utils.models
|
||||
|
||||
open class PromocodeModel(
|
||||
val code: String,
|
||||
val discount: PromocodeDiscount,
|
||||
)
|
||||
|
||||
abstract class PromocodeDiscount {
|
||||
|
||||
abstract fun applyTo(totalPrice: Int): Int
|
||||
|
||||
class ByValue(private val value: Int) : PromocodeDiscount() {
|
||||
override fun applyTo(totalPrice: Int): Int = totalPrice - value
|
||||
}
|
||||
|
||||
class ByPercent(private val percent: Int) : PromocodeDiscount() {
|
||||
override fun applyTo(totalPrice: Int): Int = totalPrice - totalPrice * percent / 100
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package ru.touchin.roboswag.cart_utils.repositories
|
||||
|
||||
import ru.touchin.roboswag.cart_utils.models.CartModel
|
||||
import ru.touchin.roboswag.cart_utils.models.ProductModel
|
||||
|
||||
/**
|
||||
* Interface for server-side cart repository where each request should return updated [CartModel]
|
||||
*/
|
||||
interface IRemoteCartRepository<TCart : CartModel<TProduct>, TProduct : ProductModel> {
|
||||
|
||||
suspend fun getCart(): TCart
|
||||
|
||||
suspend fun addProduct(product: TProduct): TCart
|
||||
|
||||
suspend fun removeProduct(id: Int): TCart
|
||||
|
||||
suspend fun editProductCount(id: Int, count: Int): TCart
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package ru.touchin.roboswag.cart_utils.repositories
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import ru.touchin.roboswag.cart_utils.models.CartModel
|
||||
import ru.touchin.roboswag.cart_utils.models.ProductModel
|
||||
import ru.touchin.roboswag.cart_utils.models.PromocodeModel
|
||||
|
||||
/**
|
||||
* Class that contains StateFlow of current [CartModel] which can be subscribed in ViewModels
|
||||
*/
|
||||
class LocalCartRepository<TCart : CartModel<TProduct>, TProduct : ProductModel>(
|
||||
initialCart: TCart
|
||||
) {
|
||||
|
||||
private val _currentCart = MutableStateFlow(initialCart)
|
||||
val currentCart = _currentCart.asStateFlow()
|
||||
|
||||
fun updateCart(cart: TCart) {
|
||||
_currentCart.value = cart
|
||||
}
|
||||
|
||||
fun addProduct(product: TProduct) {
|
||||
updateCartProducts {
|
||||
add(product)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeProduct(id: Int) {
|
||||
updateCartProducts {
|
||||
remove(find { it.id == id })
|
||||
}
|
||||
}
|
||||
|
||||
fun editProductCount(id: Int, count: Int) {
|
||||
updateCartProducts {
|
||||
updateProduct(id) { copyWith(countInCart = count) }
|
||||
}
|
||||
}
|
||||
|
||||
fun markProductDeleted(id: Int) {
|
||||
updateCartProducts {
|
||||
updateProduct(id) { copyWith(isDeleted = true) }
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreDeletedProduct(id: Int) {
|
||||
updateCartProducts {
|
||||
updateProduct(id) { copyWith(isDeleted = false) }
|
||||
}
|
||||
}
|
||||
|
||||
fun applyPromocode(promocode: PromocodeModel) {
|
||||
updatePromocodeList { add(promocode) }
|
||||
}
|
||||
|
||||
fun removePromocode(code: String) {
|
||||
updatePromocodeList { removeAt(indexOfFirst { it.code == code }) }
|
||||
}
|
||||
|
||||
fun useBonuses(bonuses: Int) {
|
||||
require(currentCart.value.availableBonuses >= bonuses) { "Can't use bonuses more than available" }
|
||||
_currentCart.update { it.copyWith(usedBonuses = bonuses) }
|
||||
}
|
||||
|
||||
fun chooseVariant(productId: Int, variantId: Int?) {
|
||||
updateCartProducts {
|
||||
updateProduct(productId) {
|
||||
if (variantId != null) {
|
||||
check(variants.any { it.id == variantId }) {
|
||||
"Product with id=$productId doesn't have variant with id=$variantId"
|
||||
}
|
||||
}
|
||||
copyWith(selectedVariantId = variantId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCartProducts(updateAction: MutableList<TProduct>.() -> Unit) {
|
||||
_currentCart.update { cart ->
|
||||
cart.copyWith(products = cart.products.toMutableList().apply(updateAction))
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePromocodeList(updateAction: MutableList<PromocodeModel>.() -> Unit) {
|
||||
_currentCart.update { cart ->
|
||||
cart.copyWith(promocodeList = cart.promocodeList.toMutableList().apply(updateAction))
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<TProduct>.updateProduct(id: Int, updateAction: TProduct.() -> TProduct) {
|
||||
val index = indexOfFirst { it.id == id }
|
||||
if (index >= 0) this[index] = updateAction.invoke(this[index])
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package ru.touchin.roboswag.cart_utils.requests_qeue
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
/**
|
||||
* Queue for abstract requests which will be executed one after another
|
||||
*/
|
||||
typealias Request<TResponse> = suspend () -> TResponse
|
||||
|
||||
class RequestsQueue<TRequest : Request<*>> {
|
||||
|
||||
private val requestChannel = Channel<TRequest>(capacity = Channel.BUFFERED)
|
||||
|
||||
fun initRequestsExecution(
|
||||
coroutineScope: CoroutineScope,
|
||||
executeRequestAction: suspend (TRequest) -> Unit,
|
||||
) {
|
||||
requestChannel
|
||||
.consumeAsFlow()
|
||||
.onEach { executeRequestAction.invoke(it) }
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
fun addToQueue(request: TRequest) {
|
||||
requestChannel.trySend(request)
|
||||
}
|
||||
|
||||
fun clearQueue() {
|
||||
while (hasPendingRequests()) requestChannel.tryReceive()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun hasPendingRequests() = !requestChannel.isEmpty
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package ru.touchin.roboswag.cart_utils.update_manager
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import ru.touchin.roboswag.cart_utils.models.CartModel
|
||||
import ru.touchin.roboswag.cart_utils.models.ProductModel
|
||||
import ru.touchin.roboswag.cart_utils.repositories.IRemoteCartRepository
|
||||
import ru.touchin.roboswag.cart_utils.repositories.LocalCartRepository
|
||||
import ru.touchin.roboswag.cart_utils.requests_qeue.Request
|
||||
import ru.touchin.roboswag.cart_utils.requests_qeue.RequestsQueue
|
||||
|
||||
/**
|
||||
* Combines local and remote cart update actions
|
||||
*/
|
||||
open class CartUpdateManager<TCart : CartModel<TProduct>, TProduct : ProductModel>(
|
||||
private val localCartRepository: LocalCartRepository<TCart, TProduct>,
|
||||
private val remoteCartRepository: IRemoteCartRepository<TCart, TProduct>,
|
||||
private val maxRequestAttemptsCount: Int = MAX_REQUEST_CART_ATTEMPTS_COUNT,
|
||||
private val errorHandler: (Throwable) -> Unit = {},
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val MAX_REQUEST_CART_ATTEMPTS_COUNT = 3
|
||||
}
|
||||
|
||||
private val requestsQueue = RequestsQueue<Request<TCart>>()
|
||||
|
||||
@Volatile
|
||||
var lastRemoteCart: TCart? = null
|
||||
private set
|
||||
|
||||
fun initCartRequestsQueue(
|
||||
coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
|
||||
) {
|
||||
requestsQueue.initRequestsExecution(coroutineScope) { request ->
|
||||
runCatching {
|
||||
lastRemoteCart = request.invoke()
|
||||
if (!requestsQueue.hasPendingRequests()) updateLocalCartWithRemote()
|
||||
}.onFailure { error ->
|
||||
errorHandler.invoke(error)
|
||||
requestsQueue.clearQueue()
|
||||
tryToGetRemoteCartAgain()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun addProduct(product: TProduct, restoreDeleted: Boolean = false) {
|
||||
with(localCartRepository) {
|
||||
if (restoreDeleted) restoreDeletedProduct(product.id) else addProduct(product)
|
||||
}
|
||||
|
||||
requestsQueue.addToQueue {
|
||||
remoteCartRepository.addProduct(product)
|
||||
}
|
||||
}
|
||||
|
||||
open fun removeProduct(id: Int, markDeleted: Boolean = false) {
|
||||
with(localCartRepository) {
|
||||
if (markDeleted) markProductDeleted(id) else removeProduct(id)
|
||||
}
|
||||
|
||||
requestsQueue.addToQueue {
|
||||
remoteCartRepository.removeProduct(id)
|
||||
}
|
||||
}
|
||||
|
||||
open fun editProductCount(id: Int, count: Int) {
|
||||
localCartRepository.editProductCount(id, count)
|
||||
|
||||
requestsQueue.addToQueue {
|
||||
remoteCartRepository.editProductCount(id, count)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun tryToGetRemoteCartAgain() {
|
||||
repeat(maxRequestAttemptsCount) {
|
||||
runCatching {
|
||||
lastRemoteCart = remoteCartRepository.getCart()
|
||||
updateLocalCartWithRemote()
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLocalCartWithRemote() {
|
||||
val remoteCart = lastRemoteCart ?: return
|
||||
val remoteProducts = remoteCart.products
|
||||
val localProducts = localCartRepository.currentCart.value.products
|
||||
|
||||
val newProductsFromRemoteCart = remoteProducts.filter { remoteProduct ->
|
||||
localProducts.none { it.id == remoteProduct.id }
|
||||
}
|
||||
|
||||
val mergedProducts = localProducts.mapNotNull { localProduct ->
|
||||
val sameRemoteProduct = remoteProducts.find { it.id == localProduct.id }
|
||||
|
||||
when {
|
||||
sameRemoteProduct != null -> sameRemoteProduct
|
||||
localProduct.isDeleted -> localProduct
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
val mergedCart = remoteCart.copyWith<TCart>(products = mergedProducts + newProductsFromRemoteCart)
|
||||
localCartRepository.updateCart(mergedCart)
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue