-
Notifications
You must be signed in to change notification settings - Fork 128
Paidy Forex Proxy Coding Exercises #78
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
Closed
Closed
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
50781a4
feat: update dependencies and tooling config
Hibar3 9d4458b
chore: update one-frame url and token in config
Hibar3 f052481
feat: add HTTP client and configure Module to use sample OneFrame data
Hibar3 f291f8f
feat:: replace dummy rates with live OneFrame integration
Hibar3 1458286
feat: add bid and ask field to Rate model
Hibar3 0da1a23
feat: add unit tests for rate service interpreter
Hibar3 8478a96
feat: add in-memory cache with TTL for forex rates to reduce One-Fram…
Hibar3 74e9e95
chore: add readme for getting started with
Hibar3 f8be365
feat: add currency conversion endpoint to calculate converted amount…
Hibar3 4c7464b
fix: add graceful error handling and validation for rates API
Hibar3 e9d638a
refactor: update error handling message and minor code formatting
Hibar3 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
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,76 @@ | ||
| # Forex Proxy Service | ||
|
|
||
| A local proxy service for fetching currency exchange rates, built with Scala.This service acts as a middleware for the One-Frame API, providing caching and freshness guarantees to overcome upstream limitations. | ||
|
|
||
| ## 🚀 Quick Start | ||
|
|
||
| ### 1. Prerequisites | ||
| - **Java 11+** | ||
| - **sbt** (Scala Build Tool) | ||
| - **Docker** (to run the One-Frame upstream service) | ||
|
|
||
| ### 2. Run the Upstream Service (One-Frame) | ||
| The application requires the One-Frame API to be running locally. | ||
| ```bash | ||
| docker pull paidyinc/one-frame | ||
| docker run -p 8080:8080 paidyinc/one-frame | ||
| ``` | ||
|
|
||
| ### 3. Build and Run the Application | ||
| Navigate to the `forex-mtl` directory and start the server: | ||
| ```bash | ||
| cd forex-mtl | ||
| sbt run | ||
| ``` | ||
| The server starts on `http://localhost:3000`by default. You can change the port in `application.conf`. | ||
|
|
||
| ## 🛠 Usage | ||
|
|
||
| ### Get Exchange Rate | ||
| Fetch the current exchange rate between two supported currencies. | ||
|
|
||
| **Endpoint:** `GET /rates` | ||
|
|
||
| **Parameters:** | ||
| - `from`: Source currency code (e.g., USD) | ||
| - `to`: Target currency code (e.g., JPY) | ||
|
|
||
| **Example Request:** | ||
| ```bash | ||
| curl "http://localhost:3000/rates?from=USD&to=JPY" | ||
| ``` | ||
|
|
||
| **Example Response:** | ||
| ```json | ||
| { | ||
| "from": "USD", | ||
| "to": "JPY", | ||
| "bid": 0.61, | ||
| "ask": 0.82, | ||
| "price": 0.71, | ||
| "timestamp": "2023-10-27T10:00:00Z" | ||
| } | ||
| ``` | ||
|
|
||
| ## 🧪 Testing | ||
| Run the unit tests to verify caching and rate retrieval logic: | ||
| ```bash | ||
| sbt test | ||
| ``` | ||
| ## ⚙️ Configuration | ||
| Settings for the HTTP server, cache TTL, and One-Frame API can be found in: | ||
| `src/main/resources/application.conf` | ||
|
|
||
| ## 🔍 Test the Service via Curl | ||
| Run curl command to test the service: | ||
| ```bash | ||
| curl "http://localhost:{your-port}/rates?from=USD&to=JPY" | ||
| ``` | ||
|
|
||
| ## 💡 Key Features | ||
| - **Intelligent Caching**: Implements an in-memory cache with a 5-minute TTL. | ||
| - **API Limit Management**: Designed to support 10,000+ requests/day by minimizing calls to the One-Frame API (which has a 1,000 request/day limit). | ||
| - **Rate Freshness**: Automatically refreshes rates older than 5 minutes to ensure data accuracy. | ||
| - **Descriptive Errors**: Provides clear feedback for invalid currencies or upstream failures. | ||
|
|
||
|
|
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
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
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 |
|---|---|---|
| @@ -1 +1 @@ | ||
| sbt.version=1.8.0 | ||
| sbt.version=1.10.0 |
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 |
|---|---|---|
| @@ -1,8 +1,13 @@ | ||
| app { | ||
| http { | ||
| host = "0.0.0.0" | ||
| port = 8080 | ||
| port = 3000 | ||
| timeout = 40 seconds | ||
| } | ||
| one-frame { | ||
| base-url = "http://localhost:8080" | ||
| token = "10dc303535874aeccc86a8251e6992f5" | ||
| ttl = 5 minutes | ||
| } | ||
| } | ||
|
|
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
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
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
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
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
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 |
|---|---|---|
|
|
@@ -6,27 +6,64 @@ import forex.domain.Rate.Pair | |
| import forex.domain._ | ||
| import io.circe._ | ||
| import io.circe.generic.extras.Configuration | ||
| import io.circe.generic.extras.semiauto.deriveConfiguredEncoder | ||
| import io.circe.generic.extras.semiauto.{ | ||
| deriveConfiguredDecoder, | ||
| deriveConfiguredEncoder | ||
| } | ||
|
|
||
| object Protocol { | ||
|
|
||
| implicit val configuration: Configuration = Configuration.default.withSnakeCaseMemberNames | ||
| implicit val configuration: Configuration = | ||
| Configuration.default | ||
|
|
||
| // Request body for GET /rates | ||
| final case class GetApiRequest( | ||
| from: Currency, | ||
| to: Currency | ||
| ) | ||
|
|
||
| // Response body for GET /rates | ||
| final case class GetApiResponse( | ||
| from: Currency, | ||
| to: Currency, | ||
| price: Price, | ||
| bid: Price, | ||
| ask: Price, | ||
| timestamp: Timestamp | ||
| ) | ||
|
|
||
| // Request body for POST /rates | ||
| final case class PostApiRequest( | ||
| from: Currency, | ||
| to: Currency, | ||
| amount: BigDecimal | ||
| ) | ||
|
|
||
| // Response body for POST /rates | ||
| final case class PostApiResponse( | ||
| from: Currency, | ||
| to: Currency, | ||
| amount: BigDecimal, | ||
| exchangeRate: Price, | ||
| convertedAmount: BigDecimal, | ||
| timestamp: Timestamp | ||
| ) | ||
| // Response body for errors | ||
| final case class ErrorResponse( | ||
| message: String | ||
| ) | ||
|
|
||
| implicit val currencyEncoder: Encoder[Currency] = | ||
| Encoder.instance[Currency] { show.show _ andThen Json.fromString } | ||
|
|
||
| implicit val currencyDecoder: Decoder[Currency] = | ||
| Decoder.decodeString.emap { s => | ||
|
Comment on lines
+58
to
+59
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. added custom Decoder that validates currency strings against supported values in One Frame |
||
| scala.util | ||
| .Try(Currency.fromString(s)) | ||
| .toEither | ||
| .left | ||
| .map(_ => s"Invalid currency: $s") | ||
| } | ||
|
|
||
| implicit val pairEncoder: Encoder[Pair] = | ||
| deriveConfiguredEncoder[Pair] | ||
|
|
||
|
|
@@ -36,4 +73,28 @@ object Protocol { | |
| implicit val responseEncoder: Encoder[GetApiResponse] = | ||
| deriveConfiguredEncoder[GetApiResponse] | ||
|
|
||
| implicit val postRequestDecoder: Decoder[PostApiRequest] = | ||
| deriveConfiguredDecoder[PostApiRequest] | ||
| .emap { request => | ||
| val errors = scala.collection.mutable.ListBuffer[String]() | ||
|
|
||
| // Validate amount is not negative | ||
| if (request.amount < 0) { | ||
| errors += s"Field 'amount' must be a positive number" | ||
| } | ||
|
|
||
| // checks if there are any errors in the list | ||
| if (errors.nonEmpty) { | ||
| Left(errors.mkString("; ")) | ||
| } else { | ||
| Right(request) | ||
| } | ||
| } | ||
|
|
||
| implicit val postResponseEncoder: Encoder[PostApiResponse] = | ||
| deriveConfiguredEncoder[PostApiResponse] | ||
|
|
||
| implicit val errorResponseEncoder: Encoder[ErrorResponse] = | ||
| deriveConfiguredEncoder[ErrorResponse] | ||
|
|
||
| } | ||
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.
added
bidandaskfields to better match with response from OneFrame