-
Notifications
You must be signed in to change notification settings - Fork 2
feat(semantic-tags): Add CurrencyAmount struct with amount and currencyCode fields #213
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
Merged
Merged
Changes from all commits
Commits
Show all changes
3 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,105 @@ | ||
| defmodule ExPass.Structs.SemanticTags.CurrencyAmount do | ||
| @moduledoc """ | ||
| Represents a currency amount for use in Apple Wallet pass semantic tags. | ||
|
|
||
| This struct is used to represent monetary values with their associated currency, | ||
| commonly used in semantic tags for balance, price, or other financial information. | ||
|
|
||
| ## Fields | ||
|
|
||
| * `:amount` - (Required) The amount of money as a string. | ||
| * `:currency_code` - (Required) The ISO 4217 currency code for the amount (e.g., "USD", "EUR"). | ||
|
|
||
| ## Compatibility | ||
|
|
||
| - iOS 12.0+ | ||
| - iPadOS 6.0+ | ||
| - watchOS 2.0+ | ||
|
|
||
| ## Apple Documentation | ||
|
|
||
| See [Apple's PassKit documentation](https://developer.apple.com/documentation/walletpasses/semantictagtype/currencyamount) | ||
| for more information about currency amounts in passes. | ||
| """ | ||
|
|
||
| use ExPass.Structs.Base | ||
| use TypedStruct | ||
|
|
||
| alias ExPass.Utils.Converter | ||
| alias ExPass.Utils.Validators | ||
|
|
||
| typedstruct do | ||
| field :amount, String.t(), enforce: true | ||
| field :currency_code, String.t(), enforce: true | ||
| end | ||
|
|
||
| @doc """ | ||
| Creates a new CurrencyAmount struct. | ||
|
|
||
| ## Parameters | ||
|
|
||
| * `attrs` - A map of attributes for the CurrencyAmount struct. The map must include: | ||
| * `:amount` - (Required) The amount of money as a string. | ||
| * `:currency_code` - (Required) The ISO 4217 currency code (e.g., "USD", "EUR", "GBP"). | ||
|
|
||
| ## Returns | ||
|
|
||
| * A new `%CurrencyAmount{}` struct. | ||
|
|
||
| ## Examples | ||
|
|
||
| iex> CurrencyAmount.new(%{amount: "10.00", currency_code: "USD"}) | ||
| %CurrencyAmount{amount: "10.00", currency_code: "USD"} | ||
|
|
||
| iex> CurrencyAmount.new(%{amount: "25.50", currency_code: :EUR}) | ||
| %CurrencyAmount{amount: "25.50", currency_code: "EUR"} | ||
|
|
||
| iex> CurrencyAmount.new(%{amount: "100", currency_code: "JPY"}) | ||
| %CurrencyAmount{amount: "100", currency_code: "JPY"} | ||
|
|
||
| iex> CurrencyAmount.new(%{currency_code: "USD"}) | ||
| ** (ArgumentError) amount is a required field and must be a non-empty string | ||
|
|
||
| iex> CurrencyAmount.new(%{amount: "10.00"}) | ||
| ** (ArgumentError) currency_code is required | ||
|
|
||
| iex> CurrencyAmount.new(%{amount: "10.00", currency_code: "INVALID"}) | ||
| ** (ArgumentError) Invalid currency code INVALID | ||
|
|
||
| """ | ||
| @spec new(map()) :: %__MODULE__{} | ||
| def new(attrs \\ %{}) do | ||
| attrs = | ||
| attrs | ||
| |> Converter.trim_string_values() | ||
| |> normalize_currency_code() | ||
| |> validate(:amount, &Validators.validate_required_string(&1, :amount)) | ||
| |> validate(:currency_code, &validate_required_currency_code/1) | ||
|
|
||
| struct!(__MODULE__, attrs) | ||
| end | ||
|
|
||
| # Normalize currency_code from atom to string if provided as atom | ||
| defp normalize_currency_code(attrs) do | ||
| case Map.get(attrs, :currency_code) do | ||
| code when is_atom(code) and not is_nil(code) -> | ||
| Map.put(attrs, :currency_code, Atom.to_string(code)) | ||
|
|
||
| _ -> | ||
| attrs | ||
| end | ||
| end | ||
|
|
||
| # Validates that currency_code is present and valid | ||
| defp validate_required_currency_code(nil) do | ||
| {:error, "currency_code is required"} | ||
| end | ||
|
|
||
| defp validate_required_currency_code("") do | ||
| {:error, "currency_code is required"} | ||
| end | ||
|
|
||
| defp validate_required_currency_code(value) do | ||
| Validators.validate_currency_code(value) | ||
| end | ||
| end | ||
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 @@ | ||
| defmodule ExPass.Structs.SemanticTags.CurrencyAmountTest do | ||
| @moduledoc false | ||
|
|
||
| use ExUnit.Case, async: true | ||
| alias ExPass.Structs.SemanticTags.CurrencyAmount | ||
|
|
||
| doctest ExPass.Structs.SemanticTags.CurrencyAmount | ||
|
|
||
| describe "new/0" do | ||
| test "raises an error when called without arguments" do | ||
| assert_raise ArgumentError, | ||
| "amount is a required field and must be a non-empty string", | ||
| fn -> | ||
| CurrencyAmount.new() | ||
| end | ||
| end | ||
| end | ||
|
|
||
| describe "new/1 with required fields" do | ||
| test "creates a valid CurrencyAmount struct with required fields" do | ||
| params = %{ | ||
| amount: "10.00", | ||
| currency_code: "USD" | ||
| } | ||
|
|
||
| currency_amount = CurrencyAmount.new(params) | ||
|
|
||
| assert %CurrencyAmount{ | ||
| amount: "10.00", | ||
| currency_code: "USD" | ||
| } = currency_amount | ||
|
|
||
| encoded = Jason.encode!(currency_amount) | ||
| assert encoded =~ ~s("amount":"10.00") | ||
| assert encoded =~ ~s("currencyCode":"USD") | ||
| end | ||
|
|
||
| test "raises an error for missing amount" do | ||
| assert_raise ArgumentError, | ||
| "amount is a required field and must be a non-empty string", | ||
| fn -> | ||
| CurrencyAmount.new(%{currency_code: "USD"}) | ||
| end | ||
| end | ||
|
|
||
| test "raises an error for missing currency_code" do | ||
| assert_raise ArgumentError, "currency_code is required", fn -> | ||
| CurrencyAmount.new(%{amount: "10.00"}) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| describe "new/1 with amount field" do | ||
| test "accepts valid amount strings" do | ||
| assert %CurrencyAmount{amount: "10.00"} = | ||
| CurrencyAmount.new(%{amount: "10.00", currency_code: "USD"}) | ||
|
|
||
| assert %CurrencyAmount{amount: "0"} = | ||
| CurrencyAmount.new(%{amount: "0", currency_code: "USD"}) | ||
|
|
||
| assert %CurrencyAmount{amount: "1000000.99"} = | ||
| CurrencyAmount.new(%{amount: "1000000.99", currency_code: "USD"}) | ||
| end | ||
|
|
||
| test "trims whitespace from amount" do | ||
| currency_amount = CurrencyAmount.new(%{amount: " 10.00 ", currency_code: "USD"}) | ||
| assert currency_amount.amount == "10.00" | ||
| end | ||
|
|
||
| test "raises an error for empty amount" do | ||
| assert_raise ArgumentError, | ||
| "amount is a required field and must be a non-empty string", | ||
| fn -> | ||
| CurrencyAmount.new(%{amount: "", currency_code: "USD"}) | ||
| end | ||
| end | ||
|
|
||
| test "raises an error for non-string amount" do | ||
| assert_raise ArgumentError, | ||
| "amount is a required field and must be a non-empty string", | ||
| fn -> | ||
| CurrencyAmount.new(%{amount: 10.00, currency_code: "USD"}) | ||
| end | ||
| end | ||
|
|
||
| test "raises an error for nil amount" do | ||
| assert_raise ArgumentError, | ||
| "amount is a required field and must be a non-empty string", | ||
| fn -> | ||
| CurrencyAmount.new(%{amount: nil, currency_code: "USD"}) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| describe "new/1 with currency_code field" do | ||
| test "accepts valid ISO 4217 currency codes as strings" do | ||
| assert %CurrencyAmount{currency_code: "USD"} = | ||
| CurrencyAmount.new(%{amount: "10.00", currency_code: "USD"}) | ||
|
|
||
| assert %CurrencyAmount{currency_code: "EUR"} = | ||
| CurrencyAmount.new(%{amount: "10.00", currency_code: "EUR"}) | ||
|
|
||
| assert %CurrencyAmount{currency_code: "GBP"} = | ||
| CurrencyAmount.new(%{amount: "10.00", currency_code: "GBP"}) | ||
|
|
||
| assert %CurrencyAmount{currency_code: "JPY"} = | ||
| CurrencyAmount.new(%{amount: "10.00", currency_code: "JPY"}) | ||
|
|
||
| assert %CurrencyAmount{currency_code: "MYR"} = | ||
| CurrencyAmount.new(%{amount: "10.00", currency_code: "MYR"}) | ||
| end | ||
|
|
||
| test "accepts valid ISO 4217 currency codes as atoms and converts to strings" do | ||
| currency_amount = CurrencyAmount.new(%{amount: "10.00", currency_code: :EUR}) | ||
| assert currency_amount.currency_code == "EUR" | ||
| end | ||
|
|
||
| test "raises an error for invalid currency code" do | ||
| assert_raise ArgumentError, "Invalid currency code INVALID", fn -> | ||
| CurrencyAmount.new(%{amount: "10.00", currency_code: "INVALID"}) | ||
| end | ||
| end | ||
|
|
||
| test "raises an error for nil currency_code" do | ||
| assert_raise ArgumentError, "currency_code is required", fn -> | ||
| CurrencyAmount.new(%{amount: "10.00", currency_code: nil}) | ||
| end | ||
| end | ||
|
|
||
| test "raises an error for empty currency_code" do | ||
| assert_raise ArgumentError, "currency_code is required", fn -> | ||
| CurrencyAmount.new(%{amount: "10.00", currency_code: ""}) | ||
| end | ||
| end | ||
|
|
||
| test "raises an error for whitespace-only currency_code" do | ||
| assert_raise ArgumentError, "currency_code is required", fn -> | ||
| CurrencyAmount.new(%{amount: "10.00", currency_code: " "}) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| describe "JSON encoding" do | ||
| test "encodes to JSON with camelCase keys" do | ||
| currency_amount = CurrencyAmount.new(%{amount: "25.50", currency_code: "EUR"}) | ||
| encoded = Jason.encode!(currency_amount) | ||
|
|
||
| assert encoded =~ ~s("amount":"25.50") | ||
| assert encoded =~ ~s("currencyCode":"EUR") | ||
| refute encoded =~ "currency_code" | ||
| end | ||
|
|
||
| test "produces valid JSON that can be decoded" do | ||
| currency_amount = CurrencyAmount.new(%{amount: "100", currency_code: "JPY"}) | ||
| encoded = Jason.encode!(currency_amount) | ||
| decoded = Jason.decode!(encoded) | ||
|
|
||
| assert decoded["amount"] == "100" | ||
| assert decoded["currencyCode"] == "JPY" | ||
| end | ||
| end | ||
| end |
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.