-
Notifications
You must be signed in to change notification settings - Fork 7
Cart utils #281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
duwna
wants to merge
6
commits into
master
Choose a base branch
from
cart_utils
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Cart utils #281
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
ec1d6e5
Add cart module and requests queue helper
duwna 28b362c
Add generic cart models and CartUpdateManager
duwna 8a0ed4c
Add promocode list and calculate discount
duwna 33a745e
Add bonuses field to cart and products
duwna 724c2ca
Add variants selecting
duwna 4dc9e7e
Fix detekt
duwna File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| /build |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| <manifest package="ru.touchin.roboswag.core.cart_utils" /> |
35 changes: 35 additions & 0 deletions
35
cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/models/CartModel.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
25 changes: 25 additions & 0 deletions
25
cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/models/ProductModel.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
duwna marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
19 changes: 19 additions & 0 deletions
19
cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/models/PromocodeModel.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
19 changes: 19 additions & 0 deletions
19
...-utils/src/main/java/ru/touchin/roboswag/cart_utils/repositories/IRemoteCartRepository.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
|
||
| } |
96 changes: 96 additions & 0 deletions
96
cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/repositories/LocalCartRepository.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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]) | ||
| } | ||
| } |
39 changes: 39 additions & 0 deletions
39
cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/requests_qeue/RequestsQueue.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
109 changes: 109 additions & 0 deletions
109
cart-utils/src/main/java/ru/touchin/roboswag/cart_utils/update_manager/CartUpdateManager.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| 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) | ||
| } | ||
|
|
||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.