-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from Kshitij09/feature/place-order
* Support Placing an Order when you're logged-in and have added some items in the Cart * You can view your Order History when logged-in * Any action requiring LogIn will prompt you with an easy link to complete your authentication and will bring you right back where you left * Support viewing order details when clicked on the 'details' from the `OrdersFragment`. Currently this uses all the local details to populate the view, but this could further be improvised by making an actual API call to get the fresh, consistent information from the remote server * Order details retrieved from the remote server are mapped with local inventory details and thus total of an order is calculated completely offline. If an Order contains some inventory-ids that are not currently in the local database, we **Skip** that order from the final result. This should later be changed by making API calls that fetch respective inventory details
- Loading branch information
Showing
55 changed files
with
2,011 additions
and
236 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
19 changes: 19 additions & 0 deletions
19
api-client/src/main/java/com/kshitijpatil/tazabazar/api/OrderApi.kt
This file contains 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 com.kshitijpatil.tazabazar.api | ||
|
||
import com.kshitijpatil.tazabazar.api.dto.OrderLineDto | ||
import com.kshitijpatil.tazabazar.api.dto.OrderResponse | ||
import retrofit2.http.Body | ||
import retrofit2.http.GET | ||
import retrofit2.http.POST | ||
import retrofit2.http.Path | ||
|
||
interface OrderApi { | ||
@POST("/api/v2/orders") | ||
suspend fun placeOrder(@Body orderLines: List<OrderLineDto>): OrderResponse | ||
|
||
@GET("/api/v2/orders/{order_id}") | ||
suspend fun getOrderById(@Path("order_id") orderId: String): OrderResponse | ||
|
||
@GET("/api/v2/users/{username}/orders") | ||
suspend fun getOrdersByUsername(@Path("username") username: String): List<OrderResponse> | ||
} |
12 changes: 12 additions & 0 deletions
12
api-client/src/main/java/com/kshitijpatil/tazabazar/api/dto/OrderLineDto.kt
This file contains 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,12 @@ | ||
package com.kshitijpatil.tazabazar.api.dto | ||
|
||
import com.squareup.moshi.Json | ||
import com.squareup.moshi.JsonClass | ||
|
||
@JsonClass(generateAdapter = true) | ||
data class OrderLineDto( | ||
@Json(name = "inventoryId") | ||
val inventoryId: Int, | ||
@Json(name = "quantity") | ||
val quantity: Int | ||
) |
19 changes: 19 additions & 0 deletions
19
api-client/src/main/java/com/kshitijpatil/tazabazar/api/dto/OrderResponse.kt
This file contains 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 com.kshitijpatil.tazabazar.api.dto | ||
|
||
|
||
import com.squareup.moshi.Json | ||
import com.squareup.moshi.JsonClass | ||
|
||
@JsonClass(generateAdapter = true) | ||
data class OrderResponse( | ||
@Json(name = "created_at") | ||
val createdAt: String, | ||
@Json(name = "id") | ||
val id: String, | ||
@Json(name = "order_lines") | ||
val orderLines: List<OrderLineDto> = emptyList(), | ||
@Json(name = "status") | ||
val status: String, | ||
@Json(name = "username") | ||
val username: String | ||
) |
54 changes: 54 additions & 0 deletions
54
api-client/src/test/java/com/kshitijpatil/tazabazar/api/TestOrderApi.kt
This file contains 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,54 @@ | ||
package com.kshitijpatil.tazabazar.api | ||
|
||
import com.google.common.truth.Truth.assertThat | ||
import com.kshitijpatil.tazabazar.api.dto.LoginRequest | ||
import com.kshitijpatil.tazabazar.api.dto.OrderLineDto | ||
import kotlinx.coroutines.runBlocking | ||
import okhttp3.OkHttpClient | ||
import org.junit.Test | ||
|
||
/** | ||
* NOTE: These tests are just meant for development | ||
* and are not ready to run in CI pipelines | ||
*/ | ||
class TestOrderApi { | ||
private val client = OkHttpClient.Builder().build() | ||
private lateinit var api: OrderApi | ||
private val authApi = ApiModule.provideAuthApi(client) | ||
private val testLoginCredentials = LoginRequest("[email protected]", "1234") | ||
private val testOrderLines = listOf(OrderLineDto(1, 3), OrderLineDto(2, 3)) | ||
|
||
@Test | ||
fun test_placeOrder() = runBlocking { | ||
val response = authApi.login(testLoginCredentials) | ||
assertThat(response.isSuccessful).isTrue() | ||
val accessToken = response.body()!!.accessToken | ||
api = ApiModule.provideOrderApi(client, accessToken) | ||
val orderResponse = api.placeOrder(testOrderLines) | ||
assertThat(orderResponse).isNotNull() | ||
// TODO: Delete this order if POST call was successful | ||
} | ||
|
||
@Test | ||
fun test_getOrderById() = runBlocking { | ||
val testOrderId = "b8128611-f1e8-46bf-a934-25e719d5187a" | ||
val response = authApi.login(testLoginCredentials) | ||
assertThat(response.isSuccessful).isTrue() | ||
val accessToken = response.body()!!.accessToken | ||
api = ApiModule.provideOrderApi(client, accessToken) | ||
val orderResponse = api.getOrderById(testOrderId) | ||
assertThat(orderResponse).isNotNull() | ||
} | ||
|
||
@Test | ||
fun test_getOrdersByUsername() = runBlocking { | ||
val testOrderId = "b8128611-f1e8-46bf-a934-25e719d5187a" | ||
val response = authApi.login(testLoginCredentials) | ||
assertThat(response.isSuccessful).isTrue() | ||
val accessToken = response.body()!!.accessToken | ||
api = ApiModule.provideOrderApi(client, accessToken) | ||
val userOrders = api.getOrdersByUsername(testLoginCredentials.username) | ||
assertThat(userOrders).hasSize(1) | ||
assertThat(userOrders[0].id).isEqualTo(testOrderId) | ||
} | ||
} |
This file contains 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
This file contains 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
This file contains 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
119 changes: 119 additions & 0 deletions
119
app/src/main/java/com/kshitijpatil/tazabazar/data/OrderRepository.kt
This file contains 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,119 @@ | ||
package com.kshitijpatil.tazabazar.data | ||
|
||
import com.kshitijpatil.tazabazar.api.OrderApi | ||
import com.kshitijpatil.tazabazar.api.dto.OrderLineDto | ||
import com.kshitijpatil.tazabazar.data.local.dao.InventoryDao | ||
import com.kshitijpatil.tazabazar.data.local.entity.InventoryEntity | ||
import com.kshitijpatil.tazabazar.data.local.prefs.AuthPreferenceStore | ||
import com.kshitijpatil.tazabazar.data.mapper.OrderResponseToOrderMapper | ||
import com.kshitijpatil.tazabazar.di.OrderApiFactory | ||
import com.kshitijpatil.tazabazar.model.CartItem | ||
import com.kshitijpatil.tazabazar.model.Order | ||
import com.kshitijpatil.tazabazar.model.OrderLine | ||
import com.kshitijpatil.tazabazar.util.AppCoroutineDispatchers | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.flow.collect | ||
import kotlinx.coroutines.launch | ||
import kotlinx.coroutines.withContext | ||
import okio.IOException | ||
import org.threeten.bp.DateTimeException | ||
import timber.log.Timber | ||
|
||
abstract class OrderRepository { | ||
protected abstract var orderApi: OrderApi? | ||
abstract suspend fun placeOrder(cartItems: List<CartItem>) | ||
abstract suspend fun getOrdersOfCurrentUser(): List<Order> | ||
} | ||
|
||
class OrderRepositoryImpl( | ||
externalScope: CoroutineScope, | ||
private val dispatchers: AppCoroutineDispatchers, | ||
private val orderApiFactory: OrderApiFactory, | ||
private val orderMapper: OrderResponseToOrderMapper, | ||
private val inventoryDao: InventoryDao, | ||
private val authPreferenceStore: AuthPreferenceStore, | ||
) : OrderRepository() { | ||
override var orderApi: OrderApi? = null | ||
|
||
init { | ||
externalScope.launch(dispatchers.io) { observeAccessTokenToUpdateApi() } | ||
} | ||
|
||
private suspend fun observeAccessTokenToUpdateApi() { | ||
authPreferenceStore.observeAccessToken().collect { | ||
orderApi = if (it == null) { | ||
Timber.d("Access Token not found, resetting OrderApi") | ||
null | ||
} else { | ||
orderApiFactory.create(it) | ||
} | ||
} | ||
} | ||
|
||
override suspend fun placeOrder(cartItems: List<CartItem>) { | ||
val api = checkNotNull(orderApi) { "OrderApi was null, can't place an order" } | ||
Timber.d("Placing an order with ${cartItems.size} items") | ||
val orderLines = cartItems.map(CartItem::makeOrderLine) | ||
withContext(dispatchers.io) { | ||
api.placeOrder(orderLines) | ||
} | ||
} | ||
|
||
/** | ||
* Returns the orders of current user, with total calculated using | ||
* local inventory details. Note: Orders with non-local inventories | ||
* are skipped from the final result | ||
*/ | ||
@Throws( | ||
IllegalStateException::class, | ||
IOException::class, | ||
DateTimeException::class, | ||
IllegalArgumentException::class | ||
) | ||
override suspend fun getOrdersOfCurrentUser(): List<Order> { | ||
val loggedInUser = authPreferenceStore.getLoggedInUser().orNull() | ||
checkNotNull(loggedInUser) { "LoggedInUser must not be null to fetch orders" } | ||
val api = checkNotNull(orderApi) { "OrderApi was null, make sure user is Logged-In" } | ||
val fetchedOrders = withContext(dispatchers.io) { | ||
api.getOrdersByUsername(loggedInUser.email) | ||
} | ||
Timber.d("Received ${fetchedOrders.size} orders for the current user") | ||
val computedOrders = withContext(dispatchers.computation) { | ||
fetchedOrders.map(orderMapper::map) | ||
.map { associateWithInventories(it) } | ||
.filter(::skipOrderWithMissingInventories) | ||
.map(::updateOrderTotal) | ||
} | ||
return computedOrders | ||
} | ||
|
||
private suspend fun associateWithInventories(order: Order): Pair<Order, List<InventoryEntity>> { | ||
val invIds = order.orderLines.map(OrderLine::inventoryId) | ||
val inventories = inventoryDao.getInventoriesByIds(invIds) | ||
return Pair(order, inventories) | ||
} | ||
|
||
private fun skipOrderWithMissingInventories(orderInvPair: Pair<Order, List<InventoryEntity>>): Boolean { | ||
val (order, inventories) = orderInvPair | ||
if (order.orderLines.size != inventories.size) { | ||
Timber.d("Skipping Order: ${order.orderId} due to missing inventories") | ||
return false | ||
} | ||
return true | ||
} | ||
|
||
private fun updateOrderTotal(orderInvPair: Pair<Order, List<InventoryEntity>>): Order { | ||
val (order, inventories) = orderInvPair | ||
val orderTotal = order.orderLines.zip(inventories) | ||
.map { (ol, inv) -> ol.quantity * inv.price } | ||
.sum() | ||
return order.copy(total = orderTotal) | ||
} | ||
} | ||
|
||
private fun CartItem.makeOrderLine(): OrderLineDto { | ||
return OrderLineDto( | ||
inventoryId = inventoryId, | ||
quantity = quantity | ||
) | ||
} |
Oops, something went wrong.