-
Notifications
You must be signed in to change notification settings - Fork 42
Pairing exercise: Let merchant to notify shipment of an order #31
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
nikhilJasani
wants to merge
4
commits into
ozean12:main
Choose a base branch
from
nikhilJasani:pairing-exercise
base: main
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
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
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,24 @@ | ||
| Thought Process on Programming Exercise | ||
| ============= | ||
| ### Assumptions | ||
|
|
||
| I understood how Billie deals with e-commerce business and high level requirements from [here](README.md). However, I made few assumptions | ||
| while implementing the exercise instead of waiting for answers raised on weekend. | ||
| 1. Requirement talks about _Merchant_ but the given code has _Organisation_ which somewhat similar but not completely same. **I assumed _Organisation_ as _Merchant_ domain for this exercise.** | ||
| 2. _Order_ doesn't exist without _Organisation_ and _Shipment_ doesn't exist without _Order_. | ||
| 3. The API requests are already validated from security point of view and authentication is implemented. | ||
| 4. Designed data model is very basic and kept limited to the demo. No complex modeling, validation and business cases such as currency exchange etc. considered. | ||
|
|
||
| ### What is done | ||
| 1. With availability of very basic domain information, I created two domains _Order_ and _Shipment_ including respective DB tables. | ||
| 2. Domains perform basic validations such as shipment notification validates existence of order and total amount within range of order amount. | ||
| 3. Added integration tests which tests possible use-cases including request validations. | ||
| 4. Separate domain and view models(Dto). | ||
|
|
||
| ### Exercise vs Production scope | ||
| From resilience, scalability and performance point of view, I think the design has below basic amendments meet production level. | ||
| 1. Multiple domains are deployed in single service, dividing them in separate microservices would be useful for scalability purpose. | ||
| 2. Domains would have their own database which helps maintainability and limiting the scope to domain requirements. | ||
| 4. When services communicate via REST end-points, it is possible to use contract testing to ensure neither server or client breaks the contact before moving to production. | ||
| 5. Post end-points to be Idempotent to avoid multiple try from client doesn't result in new resource creation. | ||
| 6. Optimize API timeouts, required retry strategy and circuit breaker to make services resilient. |
8 changes: 8 additions & 0 deletions
8
src/main/kotlin/io/billie/organisations/orders/data/OrderNotFoundException.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,8 @@ | ||
| package io.billie.organisations.orders.data | ||
|
|
||
| import io.billie.organisations.orders.domain.OrderId | ||
|
|
||
| /** | ||
| * Error condition representing non-existence of order | ||
| */ | ||
| class OrderNotFoundException(orderId: OrderId) : RuntimeException("No order found with id " + orderId.id) |
162 changes: 162 additions & 0 deletions
162
src/main/kotlin/io/billie/organisations/orders/data/OrderRepository.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,162 @@ | ||
| package io.billie.organisations.orders.data | ||
|
|
||
| import io.billie.organisations.orders.domain.* | ||
| import io.billie.organisations.viewmodel.Entity | ||
| import org.springframework.beans.factory.annotation.Autowired | ||
| import org.springframework.jdbc.core.JdbcTemplate | ||
| import org.springframework.jdbc.core.ResultSetExtractor | ||
| import org.springframework.jdbc.support.GeneratedKeyHolder | ||
| import org.springframework.jdbc.support.KeyHolder | ||
| import org.springframework.stereotype.Repository | ||
| import java.sql.ResultSet | ||
| import java.util.* | ||
|
|
||
| /** | ||
| * Offering service to query or create [Order] | ||
| */ | ||
| @Repository | ||
| class OrderRepository { | ||
|
|
||
| @Autowired | ||
| lateinit var jdbcTemplate: JdbcTemplate | ||
|
|
||
| fun findById(orderId: OrderId): Order? { | ||
| val orders = jdbcTemplate.query(orderQueryById(orderId), OrderExtractor()) | ||
| return if (orders!!.isEmpty()) null else orders[0] | ||
| } | ||
|
|
||
| fun findByOrganisation(organisationId: Entity): List<Order> { | ||
| if(!validateOrganisation(organisationId)) { | ||
| throw OrganisationNotFoundException(organisationId) | ||
| } | ||
| return jdbcTemplate.query(orderQuery(organisationId), OrderExtractor())?: listOf() | ||
| } | ||
|
|
||
| fun create(order: Order): OrderId { | ||
| if(!validateOrganisation(order.organisationId)) { | ||
| throw OrganisationNotFoundException(order.organisationId) | ||
| } | ||
| val orderId = OrderId(createOrder(order)) | ||
| order.shipments.stream().forEach { shipment -> addShipment(orderId, shipment)} | ||
| return orderId | ||
| } | ||
|
|
||
| fun update(order: Order): ShipmentId? { | ||
| val shipment = order.shipments.stream() | ||
| .filter { s -> s.shipmentId == null } | ||
| .findFirst() | ||
| return if (shipment.isPresent) ShipmentId(addShipment(order.orderId!!, shipment.get())) else null | ||
| } | ||
|
|
||
| private fun addShipment(orderId: OrderId, shipment: Shipment): UUID { | ||
| return createShipment(orderId, shipment) | ||
| } | ||
|
|
||
| private fun validateOrganisation(organisationId: Entity): Boolean { | ||
| val reply: Int? = jdbcTemplate.query( | ||
| { connection -> | ||
| val ps = connection.prepareStatement( | ||
| "select count(1) from organisations_schema.organisations " + | ||
| "WHERE id = ? " | ||
| ) | ||
| ps.setObject(1, organisationId.id) | ||
| ps | ||
| }, | ||
| ResultSetExtractor { | ||
| it.next() | ||
| it.getInt(1) | ||
| } | ||
| ) | ||
| return (reply != null) && (reply > 0) | ||
| } | ||
|
|
||
| private fun createOrder(order: Order): UUID { | ||
| val keyHolder: KeyHolder = GeneratedKeyHolder() | ||
| jdbcTemplate.update( | ||
| { connection -> | ||
| val ps = connection.prepareStatement( | ||
| "INSERT INTO organisations_schema.orders (" + | ||
| "organisation_id, " + | ||
| "amount" + | ||
| ") VALUES (?, ?)", | ||
| arrayOf("id") | ||
| ) | ||
| ps.setObject(1, order.organisationId.id) | ||
| ps.setBigDecimal(2, order.orderAmount.amount) | ||
| ps | ||
| }, keyHolder | ||
| ) | ||
| return keyHolder.getKeyAs(UUID::class.java)!! | ||
| } | ||
|
|
||
| private fun createShipment(orderId: OrderId, shipment: Shipment): UUID { | ||
| val keyHolder: KeyHolder = GeneratedKeyHolder() | ||
| jdbcTemplate.update( | ||
| { connection -> | ||
| val ps = connection.prepareStatement( | ||
| "INSERT INTO organisations_schema.shipments (" + | ||
| "order_id, " + | ||
| "amount" + | ||
| ") VALUES (?, ?)", | ||
| arrayOf("id") | ||
| ) | ||
| ps.setObject(1, orderId.id) | ||
| ps.setBigDecimal(2, shipment.shipmentAmount.amount) | ||
| ps | ||
| }, keyHolder | ||
| ) | ||
| return keyHolder.getKeyAs(UUID::class.java)!! | ||
| } | ||
|
|
||
| private fun orderQuery(organisationId: Entity) = "SELECT " + | ||
| "orders.id as order_id, " + | ||
| "orders.organisation_id as organisation_id, " + | ||
| "orders.amount as order_amount, " + | ||
| "shipments.id as shipment_id, " + | ||
| "shipments.amount as shipment_amount " + | ||
| "FROM " + | ||
| "organisations_schema.orders orders " + | ||
| "LEFT JOIN organisations_schema.shipments shipments ON orders.id = shipments.order_id " + | ||
| "WHERE orders.organisation_id = '" + organisationId.id.toString() + "'" | ||
|
|
||
| private fun orderQueryById(orderId: OrderId) = "SELECT " + | ||
| "orders.id as order_id, " + | ||
| "orders.organisation_id as organisation_id, " + | ||
| "orders.amount as order_amount, " + | ||
| "shipments.id as shipment_id, " + | ||
| "shipments.amount as shipment_amount " + | ||
| "FROM " + | ||
| "organisations_schema.orders orders " + | ||
| "LEFT JOIN organisations_schema.shipments shipments ON orders.id = shipments.order_id " + | ||
| "where orders.id = '" + orderId.id.toString() + "'" | ||
|
|
||
| class OrderExtractor : ResultSetExtractor<List<Order>> { | ||
| override fun extractData(rs: ResultSet): List<Order>? { | ||
|
|
||
| val orderMap = HashMap<OrderId, Order>() | ||
|
|
||
| while (rs.next()) { | ||
| var orderId = OrderId(rs.getObject("order_id", UUID::class.java)) | ||
| var order = orderMap.get(orderId) | ||
| if (order == null) { | ||
| order = Order( | ||
| orderId, | ||
| Entity(rs.getObject("organisation_id", UUID::class.java)), | ||
| OrderAmount(rs.getBigDecimal("order_amount")), | ||
| mutableSetOf() | ||
| ) | ||
| orderMap.put(orderId, order) | ||
| } | ||
| var shipmentId = rs.getObject("shipment_id", UUID::class.java) | ||
| if (shipmentId != null) { | ||
| order.addShipment(Shipment( | ||
| ShipmentId(shipmentId), | ||
| orderId, | ||
| ShipmentAmount(rs.getBigDecimal("shipment_amount")) | ||
| )) | ||
| } | ||
| } | ||
| return orderMap.values.toList() | ||
| } | ||
| } | ||
| } |
8 changes: 8 additions & 0 deletions
8
src/main/kotlin/io/billie/organisations/orders/data/OrganisationNotFoundException.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,8 @@ | ||
| package io.billie.organisations.orders.data | ||
|
|
||
| import io.billie.organisations.viewmodel.Entity | ||
|
|
||
| /** | ||
| * Error condition representing non-existence of organisation | ||
| */ | ||
| class OrganisationNotFoundException(organisationId: Entity) : RuntimeException("No organisation found with id " + organisationId.id) |
11 changes: 11 additions & 0 deletions
11
...ain/kotlin/io/billie/organisations/orders/data/ShipmentAmountExceedOrderTotalException.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,11 @@ | ||
| package io.billie.organisations.orders.data | ||
|
|
||
| import io.billie.organisations.orders.domain.OrderId | ||
| import io.billie.organisations.orders.domain.ShipmentAmount | ||
|
|
||
|
|
||
| /** | ||
| * Error condition representing total shipment amount exceeding order total amount | ||
| */ | ||
| class ShipmentAmountExceedOrderTotalException(shipmentAmount: ShipmentAmount, orderId: OrderId) | ||
| : RuntimeException("Shipments total ${shipmentAmount.amount} exceed order amount for order id ${orderId.id}") |
11 changes: 11 additions & 0 deletions
11
src/main/kotlin/io/billie/organisations/orders/data/ShipmentNotificationFailure.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,11 @@ | ||
| package io.billie.organisations.orders.data | ||
|
|
||
| import io.billie.organisations.orders.domain.OrderId | ||
| import io.billie.organisations.orders.domain.ShipmentAmount | ||
|
|
||
|
|
||
| /** | ||
| * Error condition representing shipment notification failure | ||
| */ | ||
| class ShipmentNotificationFailure(shipmentAmount: ShipmentAmount, orderId: OrderId) | ||
| : RuntimeException("Shipment notification for amount ${shipmentAmount.amount} failed for order id ${orderId.id}") |
30 changes: 30 additions & 0 deletions
30
src/main/kotlin/io/billie/organisations/orders/domain/Order.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,30 @@ | ||
| package io.billie.organisations.orders.domain | ||
|
|
||
| import io.billie.organisations.orders.data.ShipmentAmountExceedOrderTotalException | ||
| import io.billie.organisations.viewmodel.Entity | ||
| import org.springframework.data.relational.core.mapping.Table | ||
| import java.math.BigDecimal | ||
|
|
||
| /** | ||
| * Domain object represents order for organisation | ||
| */ | ||
| @Table("ORDERS") | ||
| data class Order ( | ||
| val orderId: OrderId?, | ||
| val organisationId: Entity, | ||
| val orderAmount: OrderAmount, | ||
| val shipments: MutableSet<Shipment> ) { | ||
|
|
||
| fun addShipment(shipment: Shipment) { | ||
| var existingShipmentTotal = BigDecimal.ZERO | ||
| for (existingShipment in shipments){ | ||
| existingShipmentTotal = existingShipmentTotal.add(existingShipment.shipmentAmount.amount) | ||
| } | ||
|
|
||
| val shipmentTotal = existingShipmentTotal.add(shipment.shipmentAmount.amount) | ||
| if (shipmentTotal.compareTo(orderAmount.amount) > 0) | ||
| throw ShipmentAmountExceedOrderTotalException(ShipmentAmount(shipmentTotal), orderId!!) | ||
|
|
||
| shipments.add(shipment) | ||
| } | ||
| } | ||
12 changes: 12 additions & 0 deletions
12
src/main/kotlin/io/billie/organisations/orders/domain/OrderAmount.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,12 @@ | ||
| package io.billie.organisations.orders.domain | ||
|
|
||
| import java.math.BigDecimal | ||
|
|
||
| /** | ||
| * Domain object represents order amount, should always be positive | ||
| */ | ||
| data class OrderAmount(val amount: BigDecimal) { | ||
| init { | ||
| require(amount.compareTo(BigDecimal.ZERO) > 0) { "Order amount must be positive" } | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Order amount and also shipment amount domain objects ensure that it always receives positive value |
||
| } | ||
| } | ||
8 changes: 8 additions & 0 deletions
8
src/main/kotlin/io/billie/organisations/orders/domain/OrderId.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,8 @@ | ||
| package io.billie.organisations.orders.domain | ||
|
|
||
| import java.util.* | ||
|
|
||
| /** | ||
| * Domain object represents UUID based order id | ||
| */ | ||
| data class OrderId(val id: UUID) |
13 changes: 13 additions & 0 deletions
13
src/main/kotlin/io/billie/organisations/orders/domain/Shipment.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,13 @@ | ||
| package io.billie.organisations.orders.domain | ||
|
|
||
| import org.springframework.data.relational.core.mapping.Table | ||
|
|
||
| /** | ||
| * Domain object represents shipments for an [Order] | ||
| */ | ||
| @Table("SHIPMENTS") | ||
| data class Shipment( | ||
| val shipmentId: ShipmentId?, | ||
| val orderId: OrderId, | ||
| val shipmentAmount: ShipmentAmount | ||
| ) |
12 changes: 12 additions & 0 deletions
12
src/main/kotlin/io/billie/organisations/orders/domain/ShipmentAmount.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,12 @@ | ||
| package io.billie.organisations.orders.domain | ||
|
|
||
| import java.math.BigDecimal | ||
|
|
||
| /** | ||
| * Domain object represents shipment amount, should always be positive | ||
| */ | ||
| data class ShipmentAmount(val amount: BigDecimal) { | ||
| init { | ||
| require(amount.compareTo(BigDecimal.ZERO) > 0) { "Shipment amount must be positive" } | ||
| } | ||
| } |
8 changes: 8 additions & 0 deletions
8
src/main/kotlin/io/billie/organisations/orders/domain/ShipmentId.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,8 @@ | ||
| package io.billie.organisations.orders.domain | ||
|
|
||
| import java.util.* | ||
|
|
||
| /** | ||
| * Domain object represents UUID based shipment id | ||
| */ | ||
| data class ShipmentId(val id: UUID) |
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Order domain keeps track of the business rule where total amount of all shipments don't exceed order amount.