packages feed

circle-0.1.0.0: src/Circle/Types.hs

-------------------------------------------
-------------------------------------------
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -Wno-orphans #-}

-- |
-- Module      : Circle.Types
-- Copyright   : (c) Dylan Martin, 2022
-- Maintainer  : dmarticus@gmail.com
-- Stability   : experimental
-- Portability : POSIX
--
-- < https:\/\/developers.circle.com\/developer\/v1\/docs\/circle-api-resources >
module Circle.Types where

import Autodocodec
  ( Autodocodec (Autodocodec),
    HasCodec (codec),
    bimapCodec,
    dimapCodec,
    object,
    optionalField',
    requiredField,
    requiredField',
    shownBoundedEnumCodec,
    stringConstCodec,
    (.=),
  )
import Control.Monad (guard)
import Country
  ( Country,
    alphaTwoUpper,
  )
import Data.Aeson
  ( FromJSON (parseJSON),
    Result (Error, Success),
    ToJSON (toEncoding, toJSON),
    withObject,
    withText,
    (.:),
    (.:?),
  )
import Data.Aeson qualified as Aeson
import Data.Aeson.Types (fromJSON)
import Data.Bifunctor
import Data.ByteString.Char8 qualified as BS8
import Data.ByteString.Lazy.Char8 qualified as BSL
import Data.Coerce (coerce)
import Data.Foldable
import Data.List.NonEmpty qualified as NE
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Data.Time
import Data.UUID
import Data.UUID qualified as UUID
import GHC.Generics (Generic)
import Language.Haskell.TH (Exp, Q)
import Language.Haskell.TH.Quote (QuasiQuoter (..))
import Language.Haskell.TH.Syntax (Lift)
import Network.HTTP.Client (Response)
import Network.HTTP.Types.Method qualified as NHTM
import Refined
import Refined.Unsafe (reallyUnsafeRefine)
import System.Environment (getEnv)
import Text.Regex.PCRE.Heavy

---------------------------------------------------------------
-- Circle API wrapper
---------------------------------------------------------------

-- | Token type used for HTTP Bearer authentication.
newtype ApiToken = ApiToken
  { unApiToken :: BS8.ByteString
  }
  deriving (Read, Show, Eq)

-- | Type to represent the 3 main components (method, endpoint, and params)
-- needed to call Circle's API.
data CircleAPIRequest a b c = CircleAPIRequest
  { -- | Method of CircleAPIRequest
    rMethod :: !Method,
    -- | Endpoint of CircleAPIRequest
    endpoint :: !Text,
    -- | Request params of CircleAPIRequest
    params :: !(Params TupleBS8 BSL.ByteString)
  }
  deriving (Show)

-- | Create a @CircleAPIRequest@
mkCircleAPIRequest ::
  Method ->
  Text ->
  Params TupleBS8 BSL.ByteString ->
  CircleAPIRequest a b c
mkCircleAPIRequest = CircleAPIRequest

type family CircleRequest a :: *

-- | CircleErrors have contain both the error reason (`parseError`) and the
-- full error response body as a ByteString.
data CircleError = CircleError
  { parseError :: !Text,
    errorResponseBody :: !Reply
  }
  deriving (Show)

-- | The CircleResponseBody will have `Nothing` for the `circleResponseCode` and
-- `circleResponseMessage` if the request succeeds, and `Nothing` for the `circleResponseData`
-- if the request fails.
data CircleResponseBody a = CircleResponseBody
  { circleResponseCode :: !(Maybe ResponseStatus),
    circleResponseMessage :: !(Maybe ResponseMessage),
    circleResponseData :: !(Maybe a)
  }
  deriving (Eq, Show)

-- NB: This FromJSON instance parses every response from the Circle API, so I can't use Autodocodec to derive FromJSON here
-- because I'm using ThisOrThat (see below) as a smart parser for certain types, and that type doesn't have Autodocodec instances
instance FromJSON a => FromJSON (CircleResponseBody a) where
  parseJSON = withObject "CircleResponseBody" parse
    where
      parse o =
        CircleResponseBody
          <$> o .:? "status"
          <*> o .:? "message"
          <*> o .:? "data"

-- Utility types have for interacting with the data returned from Circle's actual API
newtype ResponseStatus = ResponseStatus
  { unResponseStatus :: Int
  }
  deriving (Eq, Show, FromJSON)

instance HasCodec ResponseStatus where
  codec = dimapCodec ResponseStatus unResponseStatus codec

newtype ResponseMessage = ResponseMessage
  { unResponseMessage :: Text
  }
  deriving (Eq, Show, FromJSON)

instance HasCodec ResponseMessage where
  codec = dimapCodec ResponseMessage unResponseMessage codec

type Reply = Response BSL.ByteString

type Method = NHTM.Method

-- | Either production or sandbox API host
type Host = Text

data CircleHost
  = CircleProduction
  | CircleSandbox
  deriving (Eq, Show)

hostUri :: CircleHost -> Text
hostUri CircleProduction = "https://api.circle.com/v1/"
hostUri CircleSandbox = "https://api-sandbox.circle.com/v1/"

data CircleConfig = CircleConfig
  { host :: !CircleHost,
    token :: !ApiToken
  }
  deriving (Eq, Show)

-- | Creates an API token using a secrete stored at \$CIRCLE_API_KEY (the default key for storing the Circle secret)
credentialsEnv :: Maybe String -> IO ApiToken
credentialsEnv mKey = do
  key <- case mKey of
    Just k -> pure k
    Nothing -> pure "CIRCLE_API_KEY"
  token <- getEnv key
  return (ApiToken $ BS8.pack token)

-- | Helper method for instantiating a Circle config that calls the production endpoint: https:\/\/api.circle.com\/v1\/
-- Example usage:
-- @
-- import Circle.Client
-- import Circle.Types
-- import Network.HTTP.Client (newManager)
-- import Network.HTTP.Client.TLS (tlsManagerSettings)
--
-- main :: IO ()
-- main = do
--   manager <- newManager tlsManagerSettings
--   config <- prodEnvConfig \"CIRCLE_API_KEY\"
--   result <- circle config manager getConfigurationInfo
--   case result of
--     Right CircleResponseBody b -> print bs
--     Left CircleError e -> print e
-- @
prodEnvConfig :: Maybe String -> IO CircleConfig
prodEnvConfig key = do
  CircleConfig CircleProduction <$> credentialsEnv key

-- | Helper method for instantiating a Circle config that calls the production endpoint: https:\/\/api-sandbox.circle.com\/v1\/
-- Example usage:
-- @
-- import Circle.Client
-- import Circle.Types
-- import Network.HTTP.Client (newManager)
-- import Network.HTTP.Client.TLS (tlsManagerSettings)
--
-- main :: IO ()
-- main = do
--   manager <- newManager tlsManagerSettings
--   config <- sandboxEnvConfig \"CIRCLE_API_KEY\"
--   result <- circle config manager getConfigurationInfo
--   case result of
--     Right CircleResponseBody b -> print bs
--     Left CircleError e -> print e
-- @
sandboxEnvConfig :: Maybe String -> IO CircleConfig
sandboxEnvConfig key = do
  CircleConfig CircleSandbox <$> credentialsEnv key

newtype Query = Query
  { unQuery :: TupleBS8
  }
  deriving (Eq, Show)

newtype Body = Body
  { unBody :: BSL.ByteString
  }
  deriving (Eq, Show)

-- | Parameters for each request which include both the query and the body of a request
data Params b c = Params
  { paramsBody :: Maybe Body,
    paramsQuery :: ![Query]
  }
  deriving (Show)

joinQueryParams :: Params b c -> Params b c -> Params b c
joinQueryParams (Params _ xs) (Params b ys) = Params b (xs ++ ys)

-- | Type alias for query parameters
type TupleBS8 = (BS8.ByteString, BS8.ByteString)

-- | Convert a parameter to a key\/value
class ToCircleParam param where
  toCircleParam :: param -> Params TupleBS8 c -> Params TupleBS8 c

class (ToCircleParam param) => CircleHasParam request param

-- | Supports adding an optional query parameter.
-- Example usage:
-- @
-- import Circle.Client
-- import Circle.Types
-- import Network.HTTP.Client (newManager)
-- import Network.HTTP.Client.TLS (tlsManagerSettings)
--
-- main :: IO ()
-- main = do
--   manager <- newManager tlsManagerSettings
--   config <- sandboxEnvConfig \"CIRCLE_API_KEY\"
--   result <- circle config manager listAllBalances -&- PaginationQueryParams (PageBefore "a8899b8e-782a-4526-b674-0efe1e04526d")
--   case result of
--     Right CircleResponseBody b -> print bs
--     Left CircleError e -> print e
-- @
(-&-) ::
  CircleHasParam r param =>
  CircleAPIRequest r b c ->
  param ->
  CircleAPIRequest r b c
circleAPIRequest -&- param =
  circleAPIRequest
    { params = toCircleParam param (params circleAPIRequest)
    }

---------------------------------------------------------------
-- Query parameters
---------------------------------------------------------------

newtype PaginationQueryParams = PaginationQueryParams
  { paginationQueryParams :: PaginationQueryParam
  }
  deriving (Eq, Show)

-- | Depending on which endpoint is being called, the IDs after the `PageBefore` and `PageAfter` params could either be UUIDs or non-UUIDs. Let's just keep them as text for now.
-- TODO maybe improve this one day.
data PaginationQueryParam = PageBefore !Text | PageAfter !Text deriving (Show, Eq)

-- | Circle has some BS pagination where they let users supply some canonical
-- collection ID, and then this pagination rule will return `n` entries before OR after that page,
-- where `n` is controlled by the pageSize param.  This type exists to prevent callers from providing both params, which would error out
instance ToCircleParam PaginationQueryParams where
  toCircleParam (PaginationQueryParams p) =
    case p of
      PageBefore a ->
        joinQueryParams $ Params Nothing [Query ("pageBefore", TE.encodeUtf8 a)]
      PageAfter a ->
        joinQueryParams $ Params Nothing [Query ("pageAfter", TE.encodeUtf8 a)]

newtype FromQueryParam = FromQueryParam
  { fromQueryParam :: UTCTime
  }
  deriving (Eq, Show)

instance ToCircleParam FromQueryParam where
  toCircleParam (FromQueryParam i) =
    joinQueryParams $ Params Nothing [Query ("from", TE.encodeUtf8 $ utcToCircle i)]

newtype ToQueryParam = ToQueryParam
  { toQueryParam :: UTCTime
  }
  deriving (Eq, Show)

instance ToCircleParam ToQueryParam where
  toCircleParam (ToQueryParam i) =
    joinQueryParams $ Params Nothing [Query ("to", TE.encodeUtf8 $ utcToCircle i)]

newtype PageSizeQueryParam = PageSizeQueryParam
  { pageSizeQueryParam :: Integer
  }
  deriving (Eq, Show)

instance ToCircleParam PageSizeQueryParam where
  toCircleParam (PageSizeQueryParam i) =
    joinQueryParams $ Params Nothing [Query ("pageSize", TE.encodeUtf8 $ (T.pack . show) i)]

newtype StatusQueryParams = StatusQueryParams
  { statusQueryParams :: [Status]
  }
  deriving (Eq, Show)

statusToBS8 :: Status -> BS8.ByteString
statusToBS8 Pending = "pending"
statusToBS8 Complete = "complete"
statusToBS8 Failed = "failed"

instance ToCircleParam StatusQueryParams where
  toCircleParam (StatusQueryParams xs) =
    joinQueryParams $ Params Nothing [Query ("status", BS8.intercalate "," (map statusToBS8 xs))]

newtype DestinationQueryParam = DestinationQueryParam
  { destinationQueryParam :: UUID
  }
  deriving (Eq, Show)

instance ToCircleParam DestinationQueryParam where
  toCircleParam (DestinationQueryParam i) =
    joinQueryParams $ Params Nothing [Query ("destination", TE.encodeUtf8 (tshow i))]

newtype TypeQueryParam = TypeQueryParam
  { typeQueryParam :: BankAccountType
  }
  deriving (Eq, Show)

bankAccountTypeToBS8 :: BankAccountType -> BS8.ByteString
bankAccountTypeToBS8 Wire = "wire"
bankAccountTypeToBS8 Sen = "sen"

instance ToCircleParam TypeQueryParam where
  toCircleParam (TypeQueryParam i) =
    joinQueryParams $ Params Nothing [Query ("type", bankAccountTypeToBS8 i)]

newtype CurrencyQueryParam = CurrencyQueryParam
  { currencyQueryParam :: SupportedCurrencies
  }
  deriving (Eq, Show)

currencyToBS8 :: SupportedCurrencies -> BS8.ByteString
currencyToBS8 USD = "USD"
currencyToBS8 EUR = "EUR"
currencyToBS8 BTC = "BTC"
currencyToBS8 ETH = "ETH"

instance ToCircleParam CurrencyQueryParam where
  toCircleParam (CurrencyQueryParam i) =
    joinQueryParams $ Params Nothing [Query ("currency", currencyToBS8 i)]

newtype SourceQueryParam = SourceQueryParam
  { sourceQueryParam :: UUID
  }
  deriving (Eq, Show)

instance ToCircleParam SourceQueryParam where
  toCircleParam (SourceQueryParam i) =
    joinQueryParams $ Params Nothing [Query ("source", TE.encodeUtf8 (tshow i))]

newtype SettlementIdQueryParam = SettlementIdQueryParam
  { settlementIdQueryParam :: UUID
  }
  deriving (Eq, Show)

instance ToCircleParam SettlementIdQueryParam where
  toCircleParam (SettlementIdQueryParam i) =
    joinQueryParams $ Params Nothing [Query ("settlementId", TE.encodeUtf8 (tshow i))]

newtype PaymentIntentIdQueryParam = PaymentIntentIdQueryParam
  { paymentIntentIdQueryParam :: UUID
  }
  deriving (Eq, Show)

instance ToCircleParam PaymentIntentIdQueryParam where
  toCircleParam (PaymentIntentIdQueryParam i) =
    joinQueryParams $ Params Nothing [Query ("paymentIntentId", TE.encodeUtf8 (tshow i))]

newtype PaymentIdQueryParam = PaymentIdQueryParam
  { paymentIdQueryParam :: UUID
  }
  deriving (Eq, Show)

instance ToCircleParam PaymentIdQueryParam where
  toCircleParam (PaymentIdQueryParam i) =
    joinQueryParams $ Params Nothing [Query ("paymentId", TE.encodeUtf8 (tshow i))]

newtype PaymentStatusQueryParams = PaymentStatusQueryParams
  { paymentStatusQueryParams :: [PaymentStatus]
  }
  deriving (Eq, Show)

paymentStatusToBS8 :: PaymentStatus -> BS8.ByteString
paymentStatusToBS8 PaymentPending = "pending"
paymentStatusToBS8 Confirmed = "confirmed"
paymentStatusToBS8 PaymentFailed = "failed"
paymentStatusToBS8 Paid = "paid"
paymentStatusToBS8 ActionRequired = "action_required"

instance ToCircleParam PaymentStatusQueryParams where
  toCircleParam (PaymentStatusQueryParams xs) =
    joinQueryParams $ Params Nothing [Query ("status", BS8.intercalate "," (map paymentStatusToBS8 xs))]

newtype PaymentIntentContextQueryParams = PaymentIntentContextQueryParams
  { paymentIntentContextQueryParams :: [PaymentIntentContext]
  }
  deriving (Eq, Show)

paymentIntentContextToBS8 :: PaymentIntentContext -> BS8.ByteString
paymentIntentContextToBS8 ContextUnderpaid = "underpaid"
paymentIntentContextToBS8 ContextPaid = "paid"
paymentIntentContextToBS8 ContextOverpaid = "overpaid"

instance ToCircleParam PaymentIntentContextQueryParams where
  toCircleParam (PaymentIntentContextQueryParams xs) =
    joinQueryParams $ Params Nothing [Query ("context", BS8.intercalate "," (map paymentIntentContextToBS8 xs))]

newtype WalletIdQueryParam = WalletIdQueryParam
  { walletIdQueryParam :: WalletId
  }
  deriving (Eq, Show)

instance ToCircleParam WalletIdQueryParam where
  toCircleParam (WalletIdQueryParam i) =
    joinQueryParams $ Params Nothing [Query ("walletId", TE.encodeUtf8 (unWalletId i))]

newtype SourceWalletIdQueryParam = SourceWalletIdQueryParam
  { sourceWalletIdQueryParam :: WalletId
  }
  deriving (Eq, Show)

instance ToCircleParam SourceWalletIdQueryParam where
  toCircleParam (SourceWalletIdQueryParam i) =
    joinQueryParams $ Params Nothing [Query ("sourceWalletId", TE.encodeUtf8 (unWalletId i))]

newtype DestinationWalletIdQueryParam = DestinationWalletIdQueryParam
  { destinationWalletIdQueryParam :: WalletId
  }
  deriving (Eq, Show)

instance ToCircleParam DestinationWalletIdQueryParam where
  toCircleParam (DestinationWalletIdQueryParam i) =
    joinQueryParams $ Params Nothing [Query ("destinationWalletId", TE.encodeUtf8 (unWalletId i))]

newtype ReturnIdentitiesQueryParam = ReturnIdentitiesQueryParam
  { returnIdentitiesQueryParam :: Bool
  }
  deriving (Eq, Show)

instance ToCircleParam ReturnIdentitiesQueryParam where
  toCircleParam (ReturnIdentitiesQueryParam i) =
    joinQueryParams $ Params Nothing [Query ("returnIdentities", TE.encodeUtf8 (T.pack (show i)))]

---------------------------------------------------------------
-- Balance Types
---------------------------------------------------------------

data BalanceRequest

type instance CircleRequest BalanceRequest = CircleResponseBody BalanceResponseBody

-- | Response body for the "businessAccount\/balances" and "\/balances" endpoints
data BalanceResponseBody = BalanceResponseBody
  { balanceResponseBodyAvailable :: ![MoneyAmount],
    balanceResponseBodyUnsettled :: ![MoneyAmount]
  }
  deriving (Eq, Show, Generic)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec BalanceResponseBody)

instance HasCodec BalanceResponseBody where
  codec =
    object "BalanceResponseBody" $
      BalanceResponseBody
        <$> requiredField' "available" .= balanceResponseBodyAvailable
        <*> requiredField' "unsettled" .= balanceResponseBodyAvailable

---------------------------------------------------------------
-- Payout Types
---------------------------------------------------------------
data PayoutRequest

type instance CircleRequest PayoutRequest = CircleResponseBody PayoutResponseBody

data PayoutsRequest

type instance CircleRequest PayoutsRequest = CircleResponseBody [PayoutResponseBody]

instance CircleHasParam PayoutsRequest PaginationQueryParams

instance CircleHasParam PayoutsRequest FromQueryParam

instance CircleHasParam PayoutsRequest ToQueryParam

instance CircleHasParam PayoutsRequest PageSizeQueryParam

instance CircleHasParam PayoutsRequest StatusQueryParams

instance CircleHasParam PayoutsRequest TypeQueryParam

instance CircleHasParam PayoutsRequest DestinationQueryParam

-- | Response body for methods that call "businessAccount\/payouts" or "payouts" endpoints
data PayoutResponseBody = PayoutResponseBody
  { payoutResponseBodyId :: !UUID,
    payoutResponseBodySourceWalletId :: !WalletId,
    payoutResponseBodyDestinationBankAccount :: !DestinationBankAccount,
    payoutResponseBodyAmount :: !MoneyAmount,
    payoutResponseBodyFees :: !MoneyAmount,
    payoutResponseBodyStatus :: !Status,
    payoutResponseBodyTrackingRef :: !TrackingReference,
    payoutResponseBodyPayoutErrorCode :: !PayoutErrorCode,
    payoutResponseBodyRiskEvaluation :: !RiskEvaluation,
    payoutResponseBodyAdjustments :: !Adjustments,
    payoutResponseBodyPayoutReturn :: !PayoutReturnResponseBody,
    payoutResponseBodyCreateDate :: !UTCTime,
    payoutResponseBodyUpdateDate :: !UTCTime
  }
  deriving (Eq, Show)
  deriving
    ( ToJSON,
      FromJSON
    )
    via (Autodocodec PayoutResponseBody)

instance HasCodec PayoutResponseBody where
  codec =
    object "PayoutResponseBody" $
      PayoutResponseBody
        <$> requiredField' "id" .= payoutResponseBodyId
        <*> requiredField' "sourceWalletId" .= payoutResponseBodySourceWalletId
        <*> requiredField' "destination" .= payoutResponseBodyDestinationBankAccount
        <*> requiredField' "amount" .= payoutResponseBodyAmount
        <*> requiredField' "fees" .= payoutResponseBodyFees
        <*> requiredField' "status" .= payoutResponseBodyStatus
        <*> requiredField' "trackingRef" .= payoutResponseBodyTrackingRef
        <*> requiredField' "errorCode" .= payoutResponseBodyPayoutErrorCode
        <*> requiredField' "riskEvaluation" .= payoutResponseBodyRiskEvaluation
        <*> requiredField' "adjustments" .= payoutResponseBodyAdjustments
        <*> requiredField' "payoutReturn" .= payoutResponseBodyPayoutReturn
        <*> requiredField' "createDate" .= payoutResponseBodyCreateDate
        <*> requiredField' "updateDate" .= payoutResponseBodyUpdateDate

-- | Request body for creating a new business account payout
data BusinessPayoutRequestBody = BusinessPayoutRequestBody
  { businessPayoutIdempotencyKey :: !UUID,
    businessPayoutDestination :: !DestinationBankAccount,
    businessPayoutAmount :: !MoneyAmount
  }
  deriving (Eq, Show)
  deriving
    ( ToJSON,
      FromJSON
    )
    via (Autodocodec BusinessPayoutRequestBody)

instance HasCodec BusinessPayoutRequestBody where
  codec =
    object "BusinessPayoutRequestBody" $
      BusinessPayoutRequestBody
        <$> requiredField' "idempotencyKey" .= businessPayoutIdempotencyKey
        <*> requiredField' "destination" .= businessPayoutDestination
        <*> requiredField' "amount" .= businessPayoutAmount

newtype PayoutMetadata = PayoutMetadata {payoutMetadataBeneficiaryEmail :: Email}
  deriving (Eq, Show)
  deriving (ToJSON, FromJSON) via (Autodocodec PayoutMetadata)

instance HasCodec PayoutMetadata where
  codec =
    object "PayoutMetadata" $
      PayoutMetadata
        <$> requiredField' "beneficiaryEmail" .= payoutMetadataBeneficiaryEmail

-- | Request body to create a payout.
data PayoutRequestBody = PayoutRequestBody
  { payoutIdempotencyKey :: !UUID,
    payoutSource :: !(Maybe PaymentSource),
    payoutDestination :: !DestinationBankAccount,
    payoutAmount :: !MoneyAmount,
    payoutMetadata :: !PayoutMetadata
  }
  deriving (Eq, Show)
  deriving
    ( ToJSON,
      FromJSON
    )
    via (Autodocodec PayoutRequestBody)

instance HasCodec PayoutRequestBody where
  codec =
    object "PayoutRequestBody" $
      PayoutRequestBody
        <$> requiredField' "idempotencyKey" .= payoutIdempotencyKey
        <*> optionalField' "source" .= payoutSource
        <*> requiredField' "destination" .= payoutDestination
        <*> requiredField' "amount" .= payoutAmount
        <*> requiredField' "metadata" .= payoutMetadata

-- | Response body for methods that call the "returns" endpoint
data PayoutReturnResponseBody = PayoutReturnResponseBody
  { payoutReturnResponseBodyId :: !UUID,
    payoutReturnResponseBodyOriginalPayoutId :: !UUID,
    payoutReturnResponseBodyAmount :: !MoneyAmount,
    payoutReturnResponseBodyFees :: !MoneyAmount,
    payoutReturnResponseBodyReason :: !Text,
    payoutReturnResponseBodyStatus :: !Status,
    payoutReturnResponseBodyCreateDate :: !UTCTime,
    payoutReturnResponseBodyUpdateDate :: !UTCTime
  }
  deriving (Eq, Show)
  deriving
    ( ToJSON,
      FromJSON
    )
    via (Autodocodec PayoutReturnResponseBody)

instance HasCodec PayoutReturnResponseBody where
  codec =
    object "PayoutReturnResponseBody" $
      PayoutReturnResponseBody
        <$> requiredField' "id" .= payoutReturnResponseBodyId
        <*> requiredField' "payoutId" .= payoutReturnResponseBodyOriginalPayoutId
        <*> requiredField' "amount" .= payoutReturnResponseBodyAmount
        <*> requiredField' "fees" .= payoutReturnResponseBodyFees
        <*> requiredField' "reason" .= payoutReturnResponseBodyReason
        <*> requiredField' "status" .= payoutReturnResponseBodyStatus
        <*> requiredField' "createDate" .= payoutReturnResponseBodyCreateDate
        <*> requiredField' "updateDate" .= payoutReturnResponseBodyUpdateDate

data PayoutErrorCode
  = InsufficientFunds
  | TransactionDenied
  | TransactionFailed
  | TransactionReturned
  | BankTransactionError
  | FiatAccountLimitExceeded
  | InvalidBankAccountNumber
  | InvalidACHRoutingTransitNumber
  | InvalidWireRoutingTransitNumber
  | VendorInactive
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec PayoutErrorCode)

instance HasCodec PayoutErrorCode where
  codec =
    stringConstCodec $
      NE.fromList
        [ (InsufficientFunds, "insufficient_funds"),
          (TransactionDenied, "transaction_denied"),
          (TransactionFailed, "transaction_failed"),
          (TransactionReturned, "transaction_returned"),
          (BankTransactionError, "bank_transaction_error"),
          (FiatAccountLimitExceeded, "fiat_account_limit_exceeded"),
          (InvalidBankAccountNumber, "invalid_bank_account_number"),
          (InvalidACHRoutingTransitNumber, "invalid_ach_rtn"),
          (InvalidWireRoutingTransitNumber, "invalid_wire_rtn"),
          (VendorInactive, "vendor_inactive")
        ]

---------------------------------------------------------------
-- Configuration Types
---------------------------------------------------------------

data ConfigurationRequest

type instance CircleRequest ConfigurationRequest = CircleResponseBody ConfigurationResponseBody

-- | Response body for the "\/configuration" endpoint
newtype ConfigurationResponseBody = ConfigurationResponseBody {configurationResponseBodyPayments :: WalletConfig}
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec ConfigurationResponseBody)

instance HasCodec ConfigurationResponseBody where
  codec =
    object "ConfigurationResponseBody" $
      ConfigurationResponseBody
        <$> requiredField' "payments" .= configurationResponseBodyPayments

newtype WalletConfig = WalletConfig {masterWalletId :: WalletId}
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec WalletConfig)

instance HasCodec WalletConfig where
  codec =
    object "WalletConfig" $
      WalletConfig
        <$> requiredField' "masterWalletId" .= masterWalletId

---------------------------------------------------------------
-- Encryption Types
---------------------------------------------------------------

data EncryptionRequest

type instance CircleRequest EncryptionRequest = CircleResponseBody EncryptionResponseBody

-- | Response body for the "\/encryption\/public" endpoint
data EncryptionResponseBody = EncryptionResponseBody
  { encryptionResponseBodyKeyId :: !Text, -- TODO this should actually be a UUID, but for the tests to work it needs to be relaxed a bit
    encryptionResponseBodyPublicKey :: !PGPKey
  }
  deriving (Eq, Show)
  deriving (ToJSON, FromJSON) via (Autodocodec EncryptionResponseBody)

newtype PGPKey = PGPKey
  { unPGPKey :: Text
  }
  deriving (Eq, Show, ToJSON, FromJSON)

instance HasCodec PGPKey where
  codec = dimapCodec PGPKey unPGPKey codec

instance HasCodec EncryptionResponseBody where
  codec =
    object "EncryptionResponseBody" $
      EncryptionResponseBody
        <$> requiredField' "keyId" .= encryptionResponseBodyKeyId
        <*> requiredField' "publicKey" .= encryptionResponseBodyPublicKey

---------------------------------------------------------------
-- Channels Types
---------------------------------------------------------------

data ChannelsRequest

type instance CircleRequest ChannelsRequest = CircleResponseBody ChannelResponseBody

-- | Response body for the "\/channels" endpoint
newtype ChannelResponseBody = ChannelResponseBody {channels :: [Channel]}
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec ChannelResponseBody)

instance HasCodec ChannelResponseBody where
  codec =
    object "ChannelResponseBody" $
      ChannelResponseBody
        <$> requiredField' "channels" .= channels

data Channel = Channel
  { channelId :: !UUID,
    channelDefault :: !Bool,
    channelCardDescriptor :: !Text,
    channelAchDescriptor :: !Text
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec Channel)

instance HasCodec Channel where
  codec =
    object "Channel" $
      Channel
        <$> requiredField' "id" .= channelId
        <*> requiredField' "default" .= channelDefault
        <*> requiredField' "cardDescriptor" .= channelCardDescriptor
        <*> requiredField' "achDescriptor" .= channelAchDescriptor

---------------------------------------------------------------
-- Stablecoin Types
---------------------------------------------------------------

data StablecoinsRequest

type instance CircleRequest StablecoinsRequest = CircleResponseBody [StablecoinResponseBody]

-- | Response body for the "\/stablecoins" endpoint
data StablecoinResponseBody = StablecoinResponseBody
  { stablecoinResponseBodyName :: !Text,
    stablecoinResponseBodySymbol :: !Stablecoin,
    stablecoinResponseBodyTotalAmount :: !Text, --TODO need an amount field that supports crypto depth.  Many of these coins go past a fixed integer depth.
    stablecoinResponseBodyChains :: ![ChainAmount]
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec StablecoinResponseBody)

instance HasCodec StablecoinResponseBody where
  codec =
    object "StablecoinResponseBody" $
      StablecoinResponseBody
        <$> requiredField' "name" .= stablecoinResponseBodyName
        <*> requiredField' "symbol" .= stablecoinResponseBodySymbol
        <*> requiredField' "totalAmount" .= stablecoinResponseBodyTotalAmount
        <*> requiredField' "chains" .= stablecoinResponseBodyChains

data ChainAmount = ChainAmount
  { chainAmountAmount :: !Text,
    chainAmountChain :: !Chain,
    chainAmountUpdateDate :: !UTCTime
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec ChainAmount)

instance HasCodec ChainAmount where
  codec =
    object "ChainAmount" $
      ChainAmount
        <$> requiredField' "amount" .= chainAmountAmount
        <*> requiredField' "chain" .= chainAmountChain
        <*> requiredField' "updateDate" .= chainAmountUpdateDate

data Chain = ALGO | ARB | AVAX | ChainBTC | ChainETH | FLOW | HBAR | MATIC | NEAR | OP | SOL | TRX | XLM
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec Chain)

instance HasCodec Chain where
  codec =
    stringConstCodec $
      NE.fromList
        [ (ALGO, "ALGO"),
          (ARB, "ARB"),
          (AVAX, "AVAX"),
          (ChainBTC, "BTC"),
          (ChainETH, "ETH"),
          (FLOW, "FLOW"),
          (HBAR, "HBAR"),
          (MATIC, "MATIC"),
          (NEAR, "NEAR"),
          (OP, "OP"),
          (SOL, "SOL"),
          (TRX, "TRX"),
          (XLM, "XLM")
        ]

data Stablecoin = USDC | EUROC | USDT
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec Stablecoin)

instance HasCodec Stablecoin where
  codec =
    stringConstCodec $
      NE.fromList
        [ (USDC, "USDC"),
          (EUROC, "EUROC"),
          (USDT, "USDT")
        ]

---------------------------------------------------------------
-- Subscription Types
---------------------------------------------------------------

data SubscriptionsRequest

type instance CircleRequest SubscriptionsRequest = CircleResponseBody [SubscriptionResponseBody]

data SubscriptionRequest

type instance CircleRequest SubscriptionRequest = CircleResponseBody SubscriptionResponseBody

-- | Response body for methods that call "notifications\/subscriptions"
data SubscriptionResponseBody = SubscriptionResponseBody
  { subscriptionResponseBodyId :: !UUID,
    subscriptionResponseBodyEndpoint :: !URL,
    subscriptionResponseBodySubscriptionDetails :: ![SubscriptionDetails]
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec SubscriptionResponseBody)

instance HasCodec SubscriptionResponseBody where
  codec =
    object "SubscriptionResponseBody" $
      SubscriptionResponseBody
        <$> requiredField' "id" .= subscriptionResponseBodyId
        <*> requiredField' "endpoint" .= subscriptionResponseBodyEndpoint
        <*> requiredField' "subscriptionDetails" .= subscriptionResponseBodySubscriptionDetails

data SubscriptionDetails = SubscriptionDetails
  { subscriptionDetailsUrl :: !URL,
    subscriptionDetailsStatus :: !Status
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec SubscriptionDetails)

instance HasCodec SubscriptionDetails where
  codec =
    object "SubscriptionDetails" $
      SubscriptionDetails
        <$> requiredField' "url" .= subscriptionDetailsUrl
        <*> requiredField' "status" .= subscriptionDetailsStatus

-- | Request body for creating a new subscription
newtype SubscriptionRequestBody = SubscriptionRequestBody
  { subscriptionRequestBodyEndpoint :: Text
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec SubscriptionRequestBody)

instance HasCodec SubscriptionRequestBody where
  codec =
    object "SubscriptionRequestBody" $
      SubscriptionRequestBody
        <$> requiredField' "endpoint" .= subscriptionRequestBodyEndpoint

---------------------------------------------------------------
-- Transfer Types
---------------------------------------------------------------

data TransfersRequest

type instance CircleRequest TransfersRequest = CircleResponseBody [TransferResponseBody]

instance CircleHasParam TransfersRequest PaginationQueryParams

instance CircleHasParam TransfersRequest FromQueryParam

instance CircleHasParam TransfersRequest ToQueryParam

instance CircleHasParam TransfersRequest PageSizeQueryParam

instance CircleHasParam TransfersRequest WalletIdQueryParam

instance CircleHasParam TransfersRequest SourceWalletIdQueryParam

instance CircleHasParam TransfersRequest DestinationWalletIdQueryParam

instance CircleHasParam TransfersRequest ReturnIdentitiesQueryParam

data TransferRequest

type instance CircleRequest TransferRequest = CircleResponseBody TransferResponseBody

instance CircleHasParam TransferRequest ReturnIdentitiesQueryParam

-- | Request body for creating a new business account transfer
data BusinessTransferRequestBody = BusinessTransferRequestBody
  { businessTransferRequestBodyIdempotencyKey :: !UUID,
    businessTransferRequestBodyDestination :: !TransferDestination,
    businessTransferRequestBodyAmount :: !MoneyAmount
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec BusinessTransferRequestBody)

instance HasCodec BusinessTransferRequestBody where
  codec =
    object "BusinessTransferRequestBody" $
      BusinessTransferRequestBody
        <$> requiredField' "idempotencyKey" .= businessTransferRequestBodyIdempotencyKey
        <*> requiredField' "destination" .= businessTransferRequestBodyDestination
        <*> requiredField' "amount" .= businessTransferRequestBodyAmount

-- | Request body for creating a new transfer
data TransferRequestBody = TransferRequestBody
  { transferRequestBodyIdempotencyKey :: !UUID,
    transferRequestBodySource :: !PaymentSource,
    transferRequestBodyDestination :: !TransferDestination,
    transferRequestBodyAmount :: !MoneyAmount
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec TransferRequestBody)

instance HasCodec TransferRequestBody where
  codec =
    object "TransferRequestBody" $
      TransferRequestBody
        <$> requiredField' "idempotencyKey" .= transferRequestBodyIdempotencyKey
        <*> requiredField' "source" .= transferRequestBodySource
        <*> requiredField' "destination" .= transferRequestBodyDestination
        <*> requiredField' "amount" .= transferRequestBodyAmount

data TransferDestination = TransferDestination
  { transferDestinationType :: !DestinationType,
    transferDestinationAddressId :: !UUID
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec TransferDestination)

instance HasCodec TransferDestination where
  codec =
    object "TransferDestination" $
      TransferDestination
        <$> requiredField' "type" .= transferDestinationType
        <*> requiredField' "addressId" .= transferDestinationAddressId

data DestinationType = VerifiedBlockchain
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec DestinationType)

instance HasCodec DestinationType where
  codec =
    stringConstCodec $
      NE.fromList
        [ (VerifiedBlockchain, "verified_blockchain")
        ]

-- | Response body for methods that call the "businessAccount\/transfers" and "transfers" endpoints
data TransferResponseBody = TransferResponseBody
  { transferResponseBodyId :: !UUID,
    transferResponseBodySource :: !(ThisOrThat SourceWallet SourceBlockchain),
    transferResponseBodyDestination :: !(ThisOrThat DestinationWallet DestinationBlockchain),
    transferResponseBodyAmount :: !MoneyAmount,
    transferResponseBodyFees :: !TransferFeeAmount,
    transferResponseBodyTransactionHash :: !(Maybe HexString),
    transferResponseBodyStatus :: !Status,
    transferResponseBodyTransferErrorCode :: !(Maybe TransferErrorCode),
    transferResponseBodyRiskEvaluation :: !(Maybe RiskEvaluation),
    transferResponseBodyCreateDate :: !(Maybe UTCTime)
  }
  deriving (Eq, Show)

-- NB: this doesn't use autodocodec for deriving ToJSON and FromJSON since I'm using the hand-rolled
-- ThisOrThat helper for smartly parsing types.
instance FromJSON TransferResponseBody where
  parseJSON = withObject "TransferResponseBody" parse
    where
      parse o =
        TransferResponseBody
          <$> o .: "id"
          <*> o .: "source"
          <*> o .: "destination"
          <*> o .: "amount"
          <*> o .: "fees"
          <*> o .:? "transactionHash"
          <*> o .: "status"
          <*> o .:? "errorCode"
          <*> o .:? "riskEvaluation"
          <*> o .:? "createDate"

data SourceWallet = SourceWallet
  { sourceWalletType :: !TransferType,
    sourceWalletId :: !WalletId,
    sourceWalletIdentities :: ![Identity]
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec SourceWallet)

instance HasCodec SourceWallet where
  codec =
    object "SourceWallet" $
      SourceWallet
        <$> requiredField' "type" .= sourceWalletType
        <*> requiredField' "id" .= sourceWalletId
        <*> requiredField' "identities" .= sourceWalletIdentities

data SourceBlockchain = SourceBlockchain
  { sourceBlockchainType :: !TransferType,
    sourceBlockchainChain :: !Chain,
    sourceBlockChainIdentities :: ![Identity]
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec SourceBlockchain)

instance HasCodec SourceBlockchain where
  codec =
    object "SourceBlockchain" $
      SourceBlockchain
        <$> requiredField' "type" .= sourceBlockchainType
        <*> requiredField' "chain" .= sourceBlockchainChain
        <*> requiredField' "identities" .= sourceBlockChainIdentities

data DestinationWallet = DestinationWallet
  { destinationWalletType :: !TransferType,
    destinationWalletId :: !WalletId,
    destinationWalletAddress :: !(Maybe Text),
    destinationWalletAddressTag :: !(Maybe Text)
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec DestinationWallet)

instance HasCodec DestinationWallet where
  codec =
    object "DestinationWallet" $
      DestinationWallet
        <$> requiredField' "type" .= destinationWalletType
        <*> requiredField' "id" .= destinationWalletId
        <*> optionalField' "address" .= destinationWalletAddress
        <*> optionalField' "addressTag" .= destinationWalletAddressTag

data DestinationBlockchain = DestinationBlockchain
  { destinationBlockchainType :: !TransferType,
    destinationBlockchainAddress :: !HexString,
    destinationBlockchainAddressTag :: !(Maybe Text),
    destinationBlockchainAddressChain :: !Chain
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec DestinationBlockchain)

instance HasCodec DestinationBlockchain where
  codec =
    object "DestinationBlockchain" $
      DestinationBlockchain
        <$> requiredField' "type" .= destinationBlockchainType
        <*> requiredField' "address" .= destinationBlockchainAddress
        <*> optionalField' "addressTag" .= destinationBlockchainAddressTag
        <*> requiredField' "chain" .= destinationBlockchainAddressChain

data Identity = Identity
  { identityType :: !IdentityType,
    identityName :: !Text,
    identityAddresses :: ![Address]
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec Identity)

instance HasCodec Identity where
  codec =
    object "Identity" $
      Identity
        <$> requiredField' "type" .= identityType
        <*> requiredField' "name" .= identityName
        <*> requiredField' "addresses" .= identityAddresses

data IdentityType = Individual | Business
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec IdentityType)

instance HasCodec IdentityType where
  codec = stringConstCodec $ NE.fromList [(Individual, "individual"), (Business, "business")]

data TransferType = Wallet | Blockchain
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec TransferType)

instance HasCodec TransferType where
  codec = stringConstCodec $ NE.fromList [(Wallet, "wallet"), (Blockchain, "blockchain")]

data TransferErrorCode
  = TransferInsufficientFunds
  | BlockchainError
  | TransferDenied
  | TransferFailed
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec TransferErrorCode)

instance HasCodec TransferErrorCode where
  codec =
    stringConstCodec $
      NE.fromList
        [ (TransferInsufficientFunds, "insufficient_funds"),
          (BlockchainError, "blockchain_error"),
          (TransferDenied, "transfer_denied"),
          (TransferFailed, "transfer_failed")
        ]

---------------------------------------------------------------
-- Address Types
---------------------------------------------------------------

data DepositAddressesRequest

type instance CircleRequest DepositAddressesRequest = CircleResponseBody [DepositAddressResponseBody]

data DepositAddressRequest

type instance CircleRequest DepositAddressRequest = CircleResponseBody DepositAddressResponseBody

-- | Response body for methods that call the "businessAccount\/wallets\/addresses\/deposit" endpoint
data DepositAddressResponseBody = DepositAddressResponseBody
  { depositAddressResponseBodyAddress :: !HexString,
    depositAddressResponseBodyAddressTag :: !(Maybe Text), -- The docs say it's on the response, but the sandbox API doesn't return in. Make it a `Maybe` for now.
    depositAddressResponseBodyCurrency :: !SupportedCurrencies,
    depositAddressResponseBodyChain :: !Chain
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec DepositAddressResponseBody)

instance HasCodec DepositAddressResponseBody where
  codec =
    object "DepositAddressResponseBody" $
      DepositAddressResponseBody
        <$> requiredField' "address" .= depositAddressResponseBodyAddress
        <*> optionalField' "addressTag" .= depositAddressResponseBodyAddressTag
        <*> requiredField' "currency" .= depositAddressResponseBodyCurrency
        <*> requiredField' "chain" .= depositAddressResponseBodyChain

-- | Request body for creating a new deposit address
data DepositAddressRequestBody = DepositAddressRequestBody
  { depositAddressRequestBodyIdempotencyKey :: !UUID,
    depositAddressRequestBodyCurrency :: !SupportedCurrencies,
    depositAddressRequestBodyChain :: !Chain
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec DepositAddressRequestBody)

instance HasCodec DepositAddressRequestBody where
  codec =
    object "DepositAddressRequestBody" $
      DepositAddressRequestBody
        <$> requiredField' "idempotencyKey" .= depositAddressRequestBodyIdempotencyKey
        <*> requiredField' "currency" .= depositAddressRequestBodyCurrency
        <*> requiredField' "chain" .= depositAddressRequestBodyChain

data RecipientAddressesRequest

type instance CircleRequest RecipientAddressesRequest = CircleResponseBody [RecipientAddressResponseBody]

instance CircleHasParam RecipientAddressesRequest PaginationQueryParams

instance CircleHasParam RecipientAddressesRequest FromQueryParam

instance CircleHasParam RecipientAddressesRequest ToQueryParam

instance CircleHasParam RecipientAddressesRequest PageSizeQueryParam

data RecipientAddressRequest

type instance CircleRequest RecipientAddressRequest = CircleResponseBody RecipientAddressResponseBody

-- | Response body for methods that call the "businessAccount\/wallets\/addresses\/recipient" endpoint
data RecipientAddressResponseBody = RecipientAddressResponseBody
  { recipientAddressResponseBodyId :: !UUID,
    recipientAddressResponseBodyAddress :: !HexString,
    recipientAddressResponseBodyAddressTag :: !(Maybe Text),
    recipientAddressResponseBodyChain :: !Chain,
    recipientAddressResponseBodyCurrency :: !SupportedCurrencies,
    recipientAddressResponseBodyDescription :: !Text
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec RecipientAddressResponseBody)

instance HasCodec RecipientAddressResponseBody where
  codec =
    object "RecipientAddressResponseBody" $
      RecipientAddressResponseBody
        <$> requiredField' "id" .= recipientAddressResponseBodyId
        <*> requiredField' "address" .= recipientAddressResponseBodyAddress
        <*> optionalField' "addressTag" .= recipientAddressResponseBodyAddressTag
        <*> requiredField' "chain" .= recipientAddressResponseBodyChain
        <*> requiredField' "currency" .= recipientAddressResponseBodyCurrency
        <*> requiredField' "description" .= recipientAddressResponseBodyDescription

-- | Request body for creating a new recipient address
data RecipientAddressRequestBody = RecipientAddressRequestBody
  { recipientAddressRequestBodyIdempotencyKey :: !UUID,
    recipientAddressRequestBodyAddress :: !HexString,
    recipientAddressRequestBodyAddressTag :: !(Maybe Text),
    recipientAddressRequestBodyChain :: !Chain,
    recipientAddressRequestBodyCurrency :: !SupportedCurrencies,
    recipientAddressRequestBodyDescription :: !Text
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec RecipientAddressRequestBody)

instance HasCodec RecipientAddressRequestBody where
  codec =
    object "RecipientAddressRequestBody" $
      RecipientAddressRequestBody
        <$> requiredField' "idempotencyKey" .= recipientAddressRequestBodyIdempotencyKey
        <*> requiredField' "address" .= recipientAddressRequestBodyAddress
        <*> optionalField' "addressTag" .= recipientAddressRequestBodyAddressTag
        <*> requiredField' "chain" .= recipientAddressRequestBodyChain
        <*> requiredField' "currency" .= recipientAddressRequestBodyCurrency
        <*> requiredField' "description" .= recipientAddressRequestBodyDescription

---------------------------------------------------------------
-- Deposit Types
---------------------------------------------------------------

data DepositsRequest

type instance CircleRequest DepositsRequest = CircleResponseBody [DepositResponseBody]

instance CircleHasParam DepositsRequest TypeQueryParam

instance CircleHasParam DepositsRequest PaginationQueryParams

instance CircleHasParam DepositsRequest FromQueryParam

instance CircleHasParam DepositsRequest ToQueryParam

instance CircleHasParam DepositsRequest PageSizeQueryParam

-- | Response body for methods that call the "businessAccount\/deposits" endpoint
data DepositResponseBody = DepositResponseBody
  { depositResponseBodyId :: !UUID,
    depositResponseBodySourceWalletId :: !(Maybe WalletId),
    depositResponseBodyDestination :: !DestinationWallet,
    depositResponseBodyAmount :: !MoneyAmount,
    depositResponseBodyFee :: !MoneyAmount,
    depositResponseBodyStatus :: !Status,
    depositResponseBodyRiskEvaluation :: !(Maybe RiskEvaluation),
    depositResponseBodyCreateDate :: !UTCTime,
    depositResponseBodyUpdateDate :: !(Maybe UTCTime)
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec DepositResponseBody)

instance HasCodec DepositResponseBody where
  codec =
    object "DepositResponseBody" $
      DepositResponseBody
        <$> requiredField' "id" .= depositResponseBodyId
        <*> optionalField' "sourceWalletId" .= depositResponseBodySourceWalletId
        <*> requiredField' "destination" .= depositResponseBodyDestination
        <*> requiredField' "amount" .= depositResponseBodyAmount
        <*> requiredField' "fee" .= depositResponseBodyFee
        <*> requiredField' "status" .= depositResponseBodyStatus
        <*> requiredField' "riskEvaluation" .= depositResponseBodyRiskEvaluation
        <*> requiredField' "createDate" .= depositResponseBodyCreateDate
        <*> requiredField' "updateDate" .= depositResponseBodyUpdateDate

---------------------------------------------------------------
-- Mock Payment Types
---------------------------------------------------------------

data MockPaymentRequest

type instance CircleRequest MockPaymentRequest = CircleResponseBody MockPaymentResponseBody

-- | Response body for methods that call the "mock\/payments\/*" endpoint
data MockPaymentResponseBody = MockPaymentResponseBody
  { mockPaymentResponseBodyTrackingRef :: !(Maybe TrackingReference),
    mockPaymentResponseBodyAmount :: !(Maybe MoneyAmount),
    mockPaymentResponseBodyBeneficiaryBank :: !(Maybe BeneficiaryBankDetails),
    mockPaymentResponseBodyStatus :: !(Maybe Status)
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec MockPaymentResponseBody)

instance HasCodec MockPaymentResponseBody where
  codec =
    object "MockPaymentResponseBody" $
      MockPaymentResponseBody
        <$> optionalField' "trackingRef" .= mockPaymentResponseBodyTrackingRef
        <*> optionalField' "amount" .= mockPaymentResponseBodyAmount
        <*> optionalField' "beneficiaryBank" .= mockPaymentResponseBodyBeneficiaryBank
        <*> optionalField' "status" .= mockPaymentResponseBodyStatus

-- | Request body to create a mock SEN or Wire payment (in the sandbox only).
data MockSenOrWirePaymentRequestBody = MockSenOrWirePaymentRequestBody
  { mockSenOrWirePaymentRequestBodyTrackingRef :: !TrackingReference,
    mockSenOrWirePaymentRequestBodyAmount :: !MoneyAmount,
    mockSenOrWirePaymentRequestBodyBeneficiaryBank :: !MockBeneficiaryBankDetails
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec MockSenOrWirePaymentRequestBody)

instance HasCodec MockSenOrWirePaymentRequestBody where
  codec =
    object "MockSenOrWirePaymentRequestBody" $
      MockSenOrWirePaymentRequestBody
        <$> requiredField' "trackingRef" .= mockSenOrWirePaymentRequestBodyTrackingRef
        <*> requiredField' "amount" .= mockSenOrWirePaymentRequestBodyAmount
        <*> requiredField' "beneficiaryBank" .= mockSenOrWirePaymentRequestBodyBeneficiaryBank

-- | Request body to create a mock SEPA payment (in the sandbox only).
data MockSEPAPaymentRequestBody = MockSEPAPaymentRequestBody
  { mockSEPAPaymentRequestBodyTrackingRef :: !TrackingReference,
    mockSEPAPaymentRequestBodyAmount :: !MoneyAmount
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec MockSEPAPaymentRequestBody)

instance HasCodec MockSEPAPaymentRequestBody where
  codec =
    object "MockSEPAPaymentRequestBody" $
      MockSEPAPaymentRequestBody
        <$> requiredField' "trackingRef" .= mockSEPAPaymentRequestBodyTrackingRef
        <*> requiredField' "amount" .= mockSEPAPaymentRequestBodyAmount

newtype MockBeneficiaryBankDetails = MockBeneficiaryBankDetails {mockBeneficiaryBankDetailsAccountNumber :: AccountNumber}
  deriving (Eq, Show, ToJSON, FromJSON)

instance HasCodec MockBeneficiaryBankDetails where
  codec = dimapCodec MockBeneficiaryBankDetails mockBeneficiaryBankDetailsAccountNumber codec

---------------------------------------------------------------
-- Silvergate SEN Types
---------------------------------------------------------------

data SENAccountRequest

type instance CircleRequest SENAccountRequest = CircleResponseBody SENAccountResponseBody

data SENAccountsRequest

type instance CircleRequest SENAccountsRequest = CircleResponseBody [SENAccountResponseBody]

data SENInstructionsRequest

type instance CircleRequest SENInstructionsRequest = CircleResponseBody SENInstructionsResponseData

-- | Request body to create a Silvergate SEN account.
data SENAccountRequestBody = SENAccountRequestBody
  { senAccountRequestBodyIdempotencyKey :: !UUID,
    senAccountRequestBodyAccountNumber :: !AccountNumber,
    senAccountRequestBodyCurrency :: !(Maybe SupportedCurrencies)
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec SENAccountRequestBody)

instance HasCodec SENAccountRequestBody where
  codec =
    object "SENAccountRequestBody" $
      SENAccountRequestBody
        <$> requiredField' "idempotencyKey" .= senAccountRequestBodyIdempotencyKey
        <*> requiredField' "accountNumber" .= senAccountRequestBodyAccountNumber
        <*> optionalField' "currency" .= senAccountRequestBodyCurrency

-- | Response body for methods that call the "businessAccount\/banks\/sen" endpoint
data SENAccountResponseBody = SENAccountResponseBody
  { senAccountResponseBodyId :: !UUID,
    senAccountResponseBodyStatus :: !Status,
    senAccountResponseBodyDescription :: !Text, -- TODO better type: Bank name plus last four digits of the bank account number or IBAN.  Make a custom type for this
    senAccountResponseBodyTrackingRef :: !TrackingReference,
    senAccountResponseBodyCreateDate :: !UTCTime,
    senAccountResponseBodyUpdateDate :: !UTCTime,
    senAccountResponseBodyCurrency :: !(Maybe SupportedCurrencies)
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec SENAccountResponseBody)

instance HasCodec SENAccountResponseBody where
  codec =
    object "SENAccountResponseBody" $
      SENAccountResponseBody
        <$> requiredField' "id" .= senAccountResponseBodyId
        <*> requiredField' "status" .= senAccountResponseBodyStatus
        <*> requiredField' "description" .= senAccountResponseBodyDescription
        <*> requiredField' "trackingRef" .= senAccountResponseBodyTrackingRef
        <*> requiredField' "createDate" .= senAccountResponseBodyCreateDate
        <*> requiredField' "updateDate" .= senAccountResponseBodyUpdateDate
        <*> optionalField' "currency" .= senAccountResponseBodyCurrency

data SENInstructionsResponseData = SENInstructionsResponseData
  { senInstructionsResponseDataTrackingRef :: !TrackingReference,
    senInstructionsResponseDataAccountNumber :: !AccountNumber,
    senInstructionsResponseDataCurrency :: !SupportedCurrencies
  }
  deriving stock (Eq, Show, Generic)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec SENInstructionsResponseData)

instance HasCodec SENInstructionsResponseData where
  codec =
    object "SENInstructionsResponseData" $
      SENInstructionsResponseData
        <$> requiredField' "trackingRef" .= senInstructionsResponseDataTrackingRef
        <*> requiredField' "accountNumber" .= senInstructionsResponseDataAccountNumber
        <*> requiredField' "currency" .= senInstructionsResponseDataCurrency

---------------------------------------------------------------
-- Signet Bank Account Types
---------------------------------------------------------------

data SignetBankAccountRequest

type instance CircleRequest SignetBankAccountRequest = CircleResponseBody SignetBankAccountResponseData

data SignetBankAccountsRequest

type instance CircleRequest SignetBankAccountsRequest = CircleResponseBody [SignetBankAccountResponseData]

data SignetBankInstructionsRequest

type instance CircleRequest SignetBankInstructionsRequest = CircleResponseBody SignetBankInstructionsResponseData

-- | Request body to create Signet Bank bank account.
data SignetBankAccountRequestBody = SignetBankAccountRequestBody
  { signetBankAccountRequestBodyIdempotencyKey :: !UUID,
    signetBankAccountRequestBodyWalletAddress :: !HexString
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec SignetBankAccountRequestBody)

instance HasCodec SignetBankAccountRequestBody where
  codec =
    object "SignetBankAccountRequestBody" $
      SignetBankAccountRequestBody
        <$> requiredField' "idempotencyKey" .= signetBankAccountRequestBodyIdempotencyKey
        <*> requiredField' "walletAddress" .= signetBankAccountRequestBodyWalletAddress

-- | Response body for methods that call the "businessAccount\/banks\/signet" endpoint
data SignetBankAccountResponseData = SignetBankAccountResponseData
  { signetBankAccountId :: !UUID,
    signetBankAccountStatus :: !Status,
    signetBankAccountTrackingRef :: !TrackingReference,
    signetBankAccountWalletAddress :: !HexString,
    signetBankAccountCreateDate :: !UTCTime,
    signetBankAccountUpdateDate :: !UTCTime
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec SignetBankAccountResponseData)

instance HasCodec SignetBankAccountResponseData where
  codec =
    object "SignetBankAccountResponseData" $
      SignetBankAccountResponseData
        <$> requiredField' "id" .= signetBankAccountId
        <*> requiredField' "status" .= signetBankAccountStatus
        <*> requiredField' "trackingRef" .= signetBankAccountTrackingRef
        <*> requiredField' "walletAddress" .= signetBankAccountWalletAddress
        <*> requiredField' "createDate" .= signetBankAccountCreateDate
        <*> requiredField' "updateDate" .= signetBankAccountUpdateDate

data SignetBankInstructionsResponseData = SignetBankInstructionsResponseData
  { signetBankInstructionsTrackingRef :: !(Maybe TrackingReference),
    signetBankInstructionsWalletAddress :: !(Maybe HexString)
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec SignetBankInstructionsResponseData)

instance HasCodec SignetBankInstructionsResponseData where
  codec =
    object "SignetBankInstructionsResponseData" $
      SignetBankInstructionsResponseData
        <$> optionalField' "trackingRef" .= signetBankInstructionsTrackingRef
        <*> optionalField' "walletAddress" .= signetBankInstructionsWalletAddress

---------------------------------------------------------------
-- Wire Account Types
---------------------------------------------------------------

data WireAccountRequest

type instance CircleRequest WireAccountRequest = CircleResponseBody WireAccountResponseBody

data WireAccountsRequest

type instance CircleRequest WireAccountsRequest = CircleResponseBody [WireAccountResponseBody]

data WireInstructionsRequest

type instance CircleRequest WireInstructionsRequest = CircleResponseBody WireInstructionsResponseData

instance CircleHasParam WireInstructionsRequest PaginationQueryParams

-- | Request body to create a wire account.  Sum type because this endpoint supports several
-- different types of wire accounts.
data WireAccountRequestBody
  = USBankAccount !USBankAccountRequestBody
  | IBANBankAccount !IBANBankAccountRequestBody
  | NonIBANBankAccount !NonIBANBankAccountRequestBody
  deriving (Eq, Show)

data USBankAccountRequestBody = USBankAccountRequestBody
  { usBankAccountIdempotencyKey :: !UUID,
    usBankAccountAccountNumber :: !AccountNumber,
    usBankAccountRoutingNumber :: !RoutingNumber,
    usBankAccountBillingDetails :: !BillingDetails,
    usBankAccountBankAddress :: !BankAddress
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec USBankAccountRequestBody)

instance HasCodec USBankAccountRequestBody where
  codec =
    object "USBankAccountRequestBody" $
      USBankAccountRequestBody
        <$> requiredField' "idempotencyKey" .= usBankAccountIdempotencyKey
        <*> requiredField' "accountNumber" .= usBankAccountAccountNumber
        <*> requiredField' "routingNumber" .= usBankAccountRoutingNumber
        <*> requiredField' "billingDetails" .= usBankAccountBillingDetails
        <*> requiredField' "bankAddress" .= usBankAccountBankAddress

data IBANBankAccountRequestBody = IBANBankAccountRequestBody
  { ibanBankAccountIdempotencyKey :: !UUID,
    ibanBankAccountIBAN :: !Iban,
    ibanBankAccountBillingDetails :: !BillingDetails,
    ibanBankAccountBankAddress :: !BankAddress
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec IBANBankAccountRequestBody)

instance HasCodec IBANBankAccountRequestBody where
  codec =
    object "IBANBankAccountRequestBody" $
      IBANBankAccountRequestBody
        <$> requiredField' "idempotencyKey" .= ibanBankAccountIdempotencyKey
        <*> requiredField' "iban" .= ibanBankAccountIBAN
        <*> requiredField' "billingDetails" .= ibanBankAccountBillingDetails
        <*> requiredField' "bankAddress" .= ibanBankAccountBankAddress

data NonIBANBankAccountRequestBody = NonIBANBankAccountRequestBody
  { nonIBANBankAccountIdempotencyKey :: !UUID,
    nonIBANBankAccountAccountNumber :: !AccountNumber,
    nonIBANBankAccountRoutingNumber :: !RoutingNumber,
    nonIBANBankAccountBillingDetails :: !BillingDetails,
    nonIBANBankAccountBankAddress :: !BankAddress
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec NonIBANBankAccountRequestBody)

instance HasCodec NonIBANBankAccountRequestBody where
  codec =
    object "NonIBANBankAccountRequestBody" $
      NonIBANBankAccountRequestBody
        <$> requiredField' "idempotencyKey" .= nonIBANBankAccountIdempotencyKey
        <*> requiredField' "accountNumber" .= nonIBANBankAccountAccountNumber
        <*> requiredField' "routingNumber" .= nonIBANBankAccountRoutingNumber
        <*> requiredField' "billingDetails" .= nonIBANBankAccountBillingDetails
        <*> requiredField' "bankAddress" .= nonIBANBankAccountBankAddress

-- | Response body for `getWireAccountInstructions`
data WireInstructionsResponseData = WireInstructionsResponseData
  { wireInstructionsResponseDataTrackingRef :: !TrackingReference,
    wireInstructionsResponseDataBeneficiaryDetails :: !BeneficiaryDetails,
    wireInstructionsResponseDataBeneficiaryBankDetails :: !BeneficiaryBankDetails
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec WireInstructionsResponseData)

instance HasCodec WireInstructionsResponseData where
  codec =
    object "WireInstructionsResponseData" $
      WireInstructionsResponseData
        <$> requiredField' "trackingRef" .= wireInstructionsResponseDataTrackingRef
        <*> requiredField' "beneficiary" .= wireInstructionsResponseDataBeneficiaryDetails
        <*> requiredField' "beneficiaryBank" .= wireInstructionsResponseDataBeneficiaryBankDetails

-- | Response body for methods that call the "\/wires" or "businessAccount\/wires" endpoints
data WireAccountResponseBody = WireAccountResponseBody
  { wireAccountResponseBodyId :: !UUID,
    wireAccountResponseBodyStatus :: !Status,
    wireAccountResponseBodyDescription :: !Text, -- TODO better type: Bank name plus last four digits of the bank account number or IBAN.  Make a custom type for this
    wireAccountResponseBodyTrackingRef :: !TrackingReference,
    wireAccountResponseBodyFingerprint :: !UUID,
    wireAccountResponseBodyBillingDetails :: !BillingDetails,
    wireAccountResponseBodyBankAddress :: !BankAddress,
    wireAccountResponseBodyCreateDate :: !UTCTime,
    wireAccountResponseBodyUpdateDate :: !UTCTime
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec WireAccountResponseBody)

instance HasCodec WireAccountResponseBody where
  codec =
    object "WireAccountResponseBody" $
      WireAccountResponseBody
        <$> requiredField' "id" .= wireAccountResponseBodyId
        <*> requiredField' "status" .= wireAccountResponseBodyStatus
        <*> requiredField' "description" .= wireAccountResponseBodyDescription
        <*> requiredField' "trackingRef" .= wireAccountResponseBodyTrackingRef
        <*> requiredField' "fingerprint" .= wireAccountResponseBodyFingerprint
        <*> requiredField' "billingDetails" .= wireAccountResponseBodyBillingDetails
        <*> requiredField' "bankAddress" .= wireAccountResponseBodyBankAddress
        <*> requiredField' "createDate" .= wireAccountResponseBodyCreateDate
        <*> requiredField' "updateDate" .= wireAccountResponseBodyUpdateDate

---------------------------------------------------------------
-- Payment Types
---------------------------------------------------------------

data PaymentRequest

type instance CircleRequest PaymentRequest = CircleResponseBody (ThisOrThat FiatOrCryptoPaymentResponseBody FiatCancelOrRefundResponseBody)

data PaymentsRequest

type instance CircleRequest PaymentsRequest = CircleResponseBody [ThisOrThat FiatOrCryptoPaymentResponseBody FiatCancelOrRefundResponseBody]

instance CircleHasParam PaymentsRequest PaginationQueryParams

instance CircleHasParam PaymentsRequest FromQueryParam

instance CircleHasParam PaymentsRequest ToQueryParam

instance CircleHasParam PaymentsRequest PageSizeQueryParam

instance CircleHasParam PaymentsRequest PaymentStatusQueryParams

instance CircleHasParam PaymentsRequest TypeQueryParam

instance CircleHasParam PaymentsRequest DestinationQueryParam

instance CircleHasParam PaymentsRequest SourceQueryParam

instance CircleHasParam PaymentsRequest SettlementIdQueryParam

instance CircleHasParam PaymentsRequest PaymentIntentIdQueryParam

-- | Request body to create any kind of payment.
data CreatePaymentRequestBody = CreatePaymentRequestBody
  { createPaymentIdempotencyKey :: !UUID,
    createPaymentKeyId :: !Text, -- TODO this is actually a UUID, but in Sandbox it has to be `key1`.  Figure out how to reconcile this later.
    requestMetadata :: !RequestMetadata,
    createPaymentAmount :: !MoneyAmount,
    createPaymentAutoCapture :: !(Maybe Bool),
    createPaymentVerification :: !VerificationType,
    -- | The following two fields are only present if VerificationType = ThreeDSecure
    createPaymentVerificationSuccessUrl :: !(Maybe URL),
    createPaymentVerificationFailureUrl :: !(Maybe URL),
    createPaymentSource :: !PaymentSource,
    createPaymentDescription :: !(Maybe Text),
    -- | This encrypted data needs to encrypt the card CVV
    createPaymentEncryptedData :: !(Maybe Text),
    createPaymentChannel :: !(Maybe UUID)
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec CreatePaymentRequestBody)

instance HasCodec CreatePaymentRequestBody where
  codec =
    object "CreatePaymentRequestBody" $
      CreatePaymentRequestBody
        <$> requiredField' "idempotencyKey" .= createPaymentIdempotencyKey
        <*> requiredField' "keyId" .= createPaymentKeyId
        <*> requiredField' "metadata" .= requestMetadata
        <*> requiredField' "amount" .= createPaymentAmount
        <*> optionalField' "autoCapture" .= createPaymentAutoCapture
        <*> requiredField' "verification" .= createPaymentVerification
        <*> optionalField' "verificationSuccessfulUrl" .= createPaymentVerificationSuccessUrl
        <*> optionalField' "verificationFailureUrl" .= createPaymentVerificationFailureUrl
        <*> requiredField' "source" .= createPaymentSource
        <*> optionalField' "description" .= createPaymentDescription
        <*> optionalField' "encryptedData" .= createPaymentEncryptedData
        <*> optionalField' "channel" .= createPaymentChannel

data RequestMetadata = RequestMetadata
  { requestMetadataEmail :: !Email,
    requestMetadataPhoneNumber :: !(Maybe PhoneNumber),
    requestMetadataSessionId :: !SessionId,
    requestMetadataIpAddress :: !IPAddress
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec RequestMetadata)

newtype SessionId = SessionId
  { unSessionId :: Text -- TODO consider validating if necessary
  }
  deriving (Eq, Show, ToJSON, FromJSON)

instance HasCodec SessionId where
  codec = dimapCodec SessionId unSessionId codec

newtype IPAddress = IPAddress
  { unIPAddress :: Text -- TODO consider validating if necessary
  }
  deriving (Eq, Show, ToJSON, FromJSON)

instance HasCodec IPAddress where
  codec = dimapCodec IPAddress unIPAddress codec

newtype PhoneNumber = PhoneNumber
  { unPhoneNumber :: Text -- TODO consider validating if necessary
  }
  deriving (Eq, Show, ToJSON, FromJSON)

instance HasCodec PhoneNumber where
  codec = dimapCodec PhoneNumber unPhoneNumber codec

instance HasCodec RequestMetadata where
  codec =
    object "RequestMetadata" $
      RequestMetadata
        <$> requiredField' "email" .= requestMetadataEmail
        <*> optionalField' "phoneNumber" .= requestMetadataPhoneNumber
        <*> requiredField' "sessionId" .= requestMetadataSessionId
        <*> requiredField' "ipAddress" .= requestMetadataIpAddress

data PaymentErrorCode
  = PaymentFailedErrorCode
  | PaymentFraudDetected
  | PaymentDenied
  | PaymentNotSupportedByIssuer
  | PaymentNotFunded
  | PaymentUnprocessable
  | PaymentStoppedByIssuer
  | PaymentCanceled
  | PaymentReturned
  | PaymentFailedBalanceCheck
  | CardFailed
  | CardInvalid
  | CardAddressMismatch
  | CardZipMismatch
  | CardCvvInvalid
  | CardExpired
  | CardLimitViolated
  | CardNotHonored
  | CardCvvRequired
  | CardRestricted
  | CardAccountIneligible
  | CardNetworkUnsupported
  | ChannelInvalid
  | UnauthorizedTransaction
  | BankAccountIneligible
  | PaymentBankTransactionError
  | InvalidAccountNumber
  | InvalidWireRtn
  | InvalidAchRtn
  | RefIdInvalid
  | AccountNameMismatch
  | AccountNumberMismatch
  | AccountIneligible
  | WalletAddressMismatch
  | CustomerNameMismatch
  | InstitutionNameMismatch
  | PaymentVendorInactive
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec PaymentErrorCode)

instance HasCodec PaymentErrorCode where
  codec =
    stringConstCodec $
      NE.fromList
        [ (PaymentFailedErrorCode, "payment_failed"),
          (PaymentFraudDetected, "payment_fraud_detected"),
          (PaymentDenied, "payment_denied"),
          (PaymentNotSupportedByIssuer, "payment_not_supported_by_issuer"),
          (PaymentNotFunded, "payment_not_funded"),
          (PaymentUnprocessable, "payment_unprocessable"),
          (PaymentStoppedByIssuer, "payment_stopped_by_issuer"),
          (PaymentCanceled, "payment_canceled"),
          (PaymentReturned, "payment_returned"),
          (PaymentFailedBalanceCheck, "payment_failed_balance_check"),
          (CardFailed, "card_failed"),
          (CardInvalid, "card_invalid"),
          (CardAddressMismatch, "card_address_mismatch"),
          (CardZipMismatch, "card_zip_mismatch"),
          (CardCvvInvalid, "card_cvv_invalid"),
          (CardExpired, "card_expired"),
          (CardLimitViolated, "card_limit_violated"),
          (CardNotHonored, "card_not_honored"),
          (CardCvvRequired, "card_cvv_required"),
          (CardRestricted, "card_restricted"),
          (CardAccountIneligible, "card_account_ineligible"),
          (CardNetworkUnsupported, "card_network_unsupported"),
          (ChannelInvalid, "channel_invalid"),
          (UnauthorizedTransaction, "unauthorized_transaction"),
          (BankAccountIneligible, "bank_account_ineligible"),
          (PaymentBankTransactionError, "bank_transaction_error"),
          (InvalidAccountNumber, "invalid_account_number"),
          (InvalidWireRtn, "invalid_wire_rtn"),
          (InvalidAchRtn, "invalid_ach_rtn"),
          (RefIdInvalid, "ref_id_invalid"),
          (AccountNameMismatch, "account_name_mismatch"),
          (AccountNumberMismatch, "account_number_mismatch"),
          (AccountIneligible, "account_ineligible"),
          (WalletAddressMismatch, "wallet_address_mismatch"),
          (CustomerNameMismatch, "customer_name_mismatch"),
          (InstitutionNameMismatch, "institution_name_mismatch"),
          (PaymentVendorInactive, "vendor_inactive")
        ]

-- TODO could likely make a better abstraction here.

-- | A FiatOrCryptoPaymentResponseBody object represents a fiat or crypto payment.  These payments look identical
-- except for the "Description" field, and the fact that a FiatPayment could have response verification data, whereas
-- a crypto payment could have info about the deposit address, transaction hash etc.
-- I'd love to differentiate these fields based on what I can parse from JSON, but there's enough overlap between
-- the two response bodies that I can cheat for now.
data FiatOrCryptoPaymentResponseBody = FiatOrCryptoPaymentResponseBody
  { -- the following fields will be present on every response
    fiatOrCryptoPaymentId :: !UUID,
    fiatOrCryptoPaymentType :: !PaymentType,
    fiatOrCryptoPaymentMerchantId :: !UUID,
    fiatOrCryptoPaymentMerchantWalletId :: !WalletId,
    fiatOrCryptoPaymentAmount :: !MoneyAmount,
    fiatOrCryptoPaymentSource :: !PaymentSource,
    fiatOrCryptoPaymentDescription :: !Text,
    fiatOrCryptoPaymentStatus :: !PaymentStatus,
    -- the following fields will only be present on Crypto payments
    fiatOrCryptoPaymentPaymentIntentId :: !(Maybe UUID),
    fiatOrCryptoPaymentSettlementAmount :: !(Maybe MoneyAmount),
    fiatOrCryptoPaymentDepositAddress :: !(Maybe PaymentDepositAddress),
    fiatOrCryptoPaymentTransactionHash :: !(Maybe HexString),
    -- the following fields will only be present on fiat payments
    fiatOrCryptoPaymentVerification :: !(Maybe VerificationData),
    fiatOrCryptoPaymentCaptured :: !(Maybe Bool),
    fiatOrCryptoPaymentCaptureAmount :: !(Maybe MoneyAmount),
    fiatOrCryptoPaymentCaptureDate :: !(Maybe UTCTime),
    fiatOrCryptoPaymentRequiredAction :: !(Maybe PaymentActionRequired),
    fiatOrCryptoPaymentCancel :: !(Maybe FiatCancelOrRefundResponseBody),
    fiatOrCryptoPaymentRefunds :: !(Maybe [FiatCancelOrRefundResponseBody]),
    fiatOrCryptoPaymentFees :: !(Maybe MoneyAmount),
    fiatOrCryptoPaymentChannel :: !(Maybe UUID),
    fiatOrCryptoPaymentCreateDate :: !(Maybe UTCTime),
    fiatOrCryptoPaymentUpdateDate :: !(Maybe UTCTime),
    fiatOrCryptoPaymentTrackingRef :: !(Maybe TrackingReference),
    fiatOrCryptoPaymentErrorCode :: !(Maybe PaymentErrorCode),
    fiatOrCryptoMetadata :: !(Maybe ResponseMetadata),
    fiatOrCryptoPaymentRiskEvaluation :: !(Maybe RiskEvaluation)
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec FiatOrCryptoPaymentResponseBody)

instance HasCodec FiatOrCryptoPaymentResponseBody where
  codec =
    object "FiatOrCryptoPaymentResponseBody" $
      FiatOrCryptoPaymentResponseBody
        <$> requiredField' "id" .= fiatOrCryptoPaymentId
        <*> requiredField' "type" .= fiatOrCryptoPaymentType
        <*> requiredField' "merchantId" .= fiatOrCryptoPaymentMerchantId
        <*> requiredField' "merchantWalletId" .= fiatOrCryptoPaymentMerchantWalletId
        <*> requiredField' "amount" .= fiatOrCryptoPaymentAmount
        <*> requiredField' "source" .= fiatOrCryptoPaymentSource
        <*> requiredField' "description" .= fiatOrCryptoPaymentDescription
        <*> requiredField' "status" .= fiatOrCryptoPaymentStatus
        <*> optionalField' "paymentIntentId" .= fiatOrCryptoPaymentPaymentIntentId
        <*> optionalField' "settlementAmount" .= fiatOrCryptoPaymentSettlementAmount
        <*> optionalField' "depositAddress" .= fiatOrCryptoPaymentDepositAddress
        <*> optionalField' "transactionHash" .= fiatOrCryptoPaymentTransactionHash
        <*> optionalField' "verification" .= fiatOrCryptoPaymentVerification
        <*> optionalField' "captured" .= fiatOrCryptoPaymentCaptured
        <*> optionalField' "captureAmount" .= fiatOrCryptoPaymentCaptureAmount
        <*> optionalField' "captureDate" .= fiatOrCryptoPaymentCaptureDate
        <*> optionalField' "requiredAction" .= fiatOrCryptoPaymentRequiredAction
        <*> optionalField' "cancel" .= fiatOrCryptoPaymentCancel
        <*> optionalField' "refunds" .= fiatOrCryptoPaymentRefunds
        <*> optionalField' "fees" .= fiatOrCryptoPaymentFees
        <*> optionalField' "channel" .= fiatOrCryptoPaymentChannel
        <*> optionalField' "createDate" .= fiatOrCryptoPaymentCreateDate
        <*> optionalField' "updateDate" .= fiatOrCryptoPaymentUpdateDate
        <*> optionalField' "trackingRef" .= fiatOrCryptoPaymentTrackingRef
        <*> optionalField' "errorCode" .= fiatOrCryptoPaymentErrorCode
        <*> optionalField' "metadata" .= fiatOrCryptoMetadata
        <*> optionalField' "channel" .= fiatOrCryptoPaymentRiskEvaluation

data ResponseMetadata = ResponseMetadata
  { responseMetadataEmail :: !Email,
    responseMetadataPhoneNumber :: !(Maybe PhoneNumber)
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec ResponseMetadata)

instance HasCodec ResponseMetadata where
  codec =
    object "ResponseMetadata" $
      ResponseMetadata
        <$> requiredField' "email" .= responseMetadataEmail
        <*> optionalField' "phoneNumber" .= responseMetadataPhoneNumber

data VerificationData = VerificationData
  { verificationAVS :: !AVS,
    verificationCVV :: !CVV
  }
  deriving (Eq, Show, Generic)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec VerificationData)

instance HasCodec VerificationData where
  codec =
    object "VerificationData" $
      VerificationData
        <$> requiredField "avs" "Represents the raw AVS response, expressed as an upper-case letter." .= verificationAVS
        <*> requiredField "cvv" "Represents the CVV response" .= verificationCVV

data AVS = AVSNotRequested | AVSPending | Y | N
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec AVS)

instance HasCodec AVS where
  codec = stringConstCodec $ NE.fromList [(AVSNotRequested, "not_requested"), (AVSPending, "pending"), (Y, "Y"), (N, "N")]

data CVV = CVVNotRequested | CVVPass | CVVFail | CVVUnavailable | CVVPending
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec CVV)

instance HasCodec CVV where
  codec = stringConstCodec $ NE.fromList [(CVVNotRequested, "not_requested"), (CVVPending, "pending"), (CVVPass, "pass"), (CVVFail, "fail"), (CVVUnavailable, "unavailable")]

data PaymentDepositAddress = PaymentDepositAddress
  { paymentDepositAddressChain :: !Chain,
    paymentDepositAddressAddress :: !HexString
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec PaymentDepositAddress)

instance HasCodec PaymentDepositAddress where
  codec =
    object "PaymentDepositAddress" $
      PaymentDepositAddress
        <$> requiredField' "chain" .= paymentDepositAddressChain
        <*> requiredField' "address" .= paymentDepositAddressAddress

-- | A FiatCancelOrRefundResponseBody object represents an attempt at canceling or refunding a payment.
-- Cancellations apply only to card payments, and its presence doesn't necessarily mean that the cancellation was successful.
-- A successful cancellation has a status of paid, a successful refund has a status of confirmed.
data FiatCancelOrRefundResponseBody = FiatCancelOrRefundResponseBody
  { -- TODO I could likely do some better data modeling here, there's a ton of shared fields between these
    -- types so I kinda cheated and just made one mega type with maybes, but it'll be more ergonomic for devs
    -- to have a specific type that's generated from the parsing.  The tricky part is the differentiator is the
    -- field `type`, so I think I'll need to be clever about this.
    fiatCancelOrRefundResponseBodyId :: !UUID,
    fiatCancelOrRefundResponseBodyType :: !PaymentType,
    fiatCancelOrRefundResponseBodyMerchantId :: !UUID,
    fiatCancelOrRefundResponseBodyMerchantWalletId :: !WalletId,
    fiatCancelOrRefundResponseBodyAmount :: !MoneyAmount,
    fiatCancelOrRefundResponseBodySource :: !PaymentSource,
    fiatCancelOrRefundResponseBodyDescription :: !Text,
    fiatCancelOrRefundResponseBodyStatus :: !PaymentStatus,
    fiatCancelOrRefundResponseBodyOriginalPayment :: !OriginalFiatPayment,
    fiatCancelOrRefundResponseBodyFees :: !(Maybe MoneyAmount),
    fiatCancelOrRefundResponseBodyChannel :: !(Maybe Text),
    fiatCancelOrRefundResponseBodyReason :: !(Maybe CancelPaymentReason),
    fiatCancelOrRefundResponseBodyCreateDate :: !UTCTime,
    fiatCancelOrRefundResponseBodyUpdateDate :: !UTCTime
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec FiatCancelOrRefundResponseBody)

instance HasCodec FiatCancelOrRefundResponseBody where
  codec =
    object "FiatCancelOrRefundResponseBody" $
      FiatCancelOrRefundResponseBody
        <$> requiredField' "id" .= fiatCancelOrRefundResponseBodyId
        <*> requiredField' "type" .= fiatCancelOrRefundResponseBodyType
        <*> requiredField' "merchantId" .= fiatCancelOrRefundResponseBodyMerchantId
        <*> requiredField' "merchantWalletId" .= fiatCancelOrRefundResponseBodyMerchantWalletId
        <*> requiredField' "amount" .= fiatCancelOrRefundResponseBodyAmount
        <*> requiredField' "source" .= fiatCancelOrRefundResponseBodySource
        <*> requiredField' "description" .= fiatCancelOrRefundResponseBodyDescription
        <*> requiredField' "status" .= fiatCancelOrRefundResponseBodyStatus
        <*> requiredField' "originalPayment" .= fiatCancelOrRefundResponseBodyOriginalPayment
        <*> optionalField' "fees" .= fiatCancelOrRefundResponseBodyFees
        <*> optionalField' "channel" .= fiatCancelOrRefundResponseBodyChannel
        <*> optionalField' "reason" .= fiatCancelOrRefundResponseBodyReason
        <*> requiredField' "createDate" .= fiatCancelOrRefundResponseBodyCreateDate
        <*> requiredField' "updateDate" .= fiatCancelOrRefundResponseBodyUpdateDate

data OriginalFiatPayment = OriginalFiatPayment
  { originalFiatPaymentId :: !UUID,
    originalFiatPaymentType :: !PaymentType,
    originalFiatPaymentStatus :: !PaymentStatus,
    originalFiatPaymentCreateDate :: !UTCTime,
    originalFiatPaymentUpdateDate :: !UTCTime,
    originalFiatPaymentDescription :: !(Maybe Text),
    originalFiatPaymentAmount :: !(Maybe MoneyAmount),
    originalFiatPaymentFees :: !(Maybe MoneyAmount),
    originalFiatPaymentMerchantId :: !(Maybe UUID),
    originalFiatPaymentMerchantWalletId :: !(Maybe WalletId),
    originalFiatPaymentSource :: !(Maybe PaymentSource),
    originalFiatPaymentTrackingRef :: !(Maybe TrackingReference)
  }
  deriving (Eq, Show, Generic)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec OriginalFiatPayment)

instance HasCodec OriginalFiatPayment where
  codec =
    object "OriginalFiatPayment" $
      OriginalFiatPayment
        <$> requiredField' "id" .= originalFiatPaymentId
        <*> requiredField' "type" .= originalFiatPaymentType
        <*> requiredField' "status" .= originalFiatPaymentStatus
        <*> requiredField' "createDate" .= originalFiatPaymentCreateDate
        <*> requiredField' "updateDate" .= originalFiatPaymentUpdateDate
        <*> optionalField' "description" .= originalFiatPaymentDescription
        <*> optionalField' "amount" .= originalFiatPaymentAmount
        <*> optionalField' "fees" .= originalFiatPaymentFees
        <*> optionalField' "merchantId" .= originalFiatPaymentMerchantId
        <*> optionalField' "merchantWalletId" .= originalFiatPaymentMerchantWalletId
        <*> optionalField' "source" .= originalFiatPaymentSource
        <*> optionalField' "trackingRef" .= originalFiatPaymentTrackingRef

data PaymentSource = PaymentSource
  { paymentSourceId :: !UUID,
    paymentSourceType :: !PaymentSourceType
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec PaymentSource)

instance HasCodec PaymentSource where
  codec =
    object "PaymentSource" $
      PaymentSource
        <$> requiredField' "id" .= paymentSourceId
        <*> requiredField' "type" .= paymentSourceType

data PaymentActionRequired = PaymentActionRequired
  { paymentActionRequiredType :: !ActionRequiredType,
    paymentActionRequiredRedirectUrl :: !URL
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec PaymentActionRequired)

instance HasCodec PaymentActionRequired where
  codec =
    object "PaymentActionRequired" $
      PaymentActionRequired
        <$> requiredField' "type" .= paymentActionRequiredType
        <*> requiredField' "redirectUrl" .= paymentActionRequiredRedirectUrl

data ActionRequiredType = ThreeDSecureRequired
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec ActionRequiredType)

instance HasCodec ActionRequiredType where
  codec = stringConstCodec $ NE.fromList [(ThreeDSecureRequired, "three_d_secure_required")]

data VerificationType = VerificationThreeDSecure | VerificationCVV
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec VerificationType)

instance HasCodec VerificationType where
  codec = stringConstCodec $ NE.fromList [(VerificationThreeDSecure, "three_d_secure"), (VerificationCVV, "cvv")]

data PaymentType = Payment | Cancel | Refund
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec PaymentType)

instance HasCodec PaymentType where
  codec = stringConstCodec $ NE.fromList [(Payment, "payment"), (Cancel, "cancel"), (Refund, "refund")]

data PaymentSourceType = Card | ACH | WireSource | SEPA
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec PaymentSourceType)

instance HasCodec PaymentSourceType where
  codec = stringConstCodec $ NE.fromList [(Card, "card"), (ACH, "ach"), (WireSource, "wire"), (SEPA, "sepa")]

-- | Request body to cancel a fiat payment.
data CancelPaymentRequestBody = CancelPaymentRequestBody
  { cancelPaymentIdempotencyKey :: !UUID,
    cancelPaymentReason :: !(Maybe CancelPaymentReason)
  }
  deriving (Eq, Show)
  deriving
    ( ToJSON,
      FromJSON
    )
    via (Autodocodec CancelPaymentRequestBody)

instance HasCodec CancelPaymentRequestBody where
  codec =
    object "CancelPaymentRequestBody" $
      CancelPaymentRequestBody
        <$> requiredField' "idempotencyKey" .= cancelPaymentIdempotencyKey
        <*> optionalField' "reason" .= cancelPaymentReason

data CancelPaymentReason
  = CancelPaymentReasonDuplicate
  | CancelPaymentReasonFraudulent
  | CancelPaymentReasonRequestedByCustomer
  | CancelPaymentReasonBankTransactionError
  | CancelPaymentReasonInvalidAccountNumber
  | CancelPaymentReasonInsufficientFunds
  | CancelPaymentReasonPaymentStoppedByIssuer
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec CancelPaymentReason)

instance HasCodec CancelPaymentReason where
  codec =
    stringConstCodec $
      NE.fromList
        [ (CancelPaymentReasonDuplicate, "duplicate"),
          (CancelPaymentReasonFraudulent, "fraudulent"),
          (CancelPaymentReasonRequestedByCustomer, "requested_by_customer"),
          (CancelPaymentReasonBankTransactionError, "bank_transaction_error"),
          (CancelPaymentReasonInvalidAccountNumber, "invalid_account_number"),
          (CancelPaymentReasonInsufficientFunds, "insufficient_funds"),
          (CancelPaymentReasonPaymentStoppedByIssuer, "payment_stopped_by_issuer")
        ]

-- | Request body to refund a fiat payment.
data RefundPaymentRequestBody = RefundPaymentRequestBody
  { refundPaymentIdempotencyKey :: !UUID,
    refundPaymentAmount :: !MoneyAmount,
    refundPaymentReason :: !(Maybe CancelPaymentReason)
  }
  deriving (Eq, Show)
  deriving
    ( ToJSON,
      FromJSON
    )
    via (Autodocodec RefundPaymentRequestBody)

instance HasCodec RefundPaymentRequestBody where
  codec =
    object "RefundPaymentRequestBody" $
      RefundPaymentRequestBody
        <$> requiredField' "idempotencyKey" .= refundPaymentIdempotencyKey
        <*> requiredField' "amount" .= refundPaymentAmount
        <*> optionalField' "reason" .= refundPaymentReason

---------------------------------------------------------------
-- On-chain Payment Types
---------------------------------------------------------------

data OnChainTransferRequest

type instance CircleRequest OnChainTransferRequest = CircleResponseBody TransferResponseBody

instance CircleHasParam OnChainTransferRequest ReturnIdentitiesQueryParam

data OnChainTransfersRequest

type instance CircleRequest OnChainTransfersRequest = CircleResponseBody [TransferResponseBody]

instance CircleHasParam OnChainTransfersRequest PaginationQueryParams

instance CircleHasParam OnChainTransfersRequest FromQueryParam

instance CircleHasParam OnChainTransfersRequest ToQueryParam

instance CircleHasParam OnChainTransfersRequest PageSizeQueryParam

instance CircleHasParam OnChainTransfersRequest WalletIdQueryParam

instance CircleHasParam OnChainTransfersRequest SourceWalletIdQueryParam

instance CircleHasParam OnChainTransfersRequest DestinationWalletIdQueryParam

instance CircleHasParam OnChainTransfersRequest ReturnIdentitiesQueryParam

data OnChainAddressRequest

type instance CircleRequest OnChainAddressRequest = CircleResponseBody DepositAddressResponseBody

-- | Request body to create an on-chain transfer
data OnChainTransferRequestBody = OnChainTransferRequestBody
  { onChainTransferRequestBodyIdempotencyKey :: !UUID,
    onChainTransferRequestBodySource :: !SourceWallet,
    onChainTransferRequestBodyDestination :: !(ThisOrThat DestinationWallet DestinationBlockchain),
    onChainTransferRequestBodyAmount :: !MoneyAmount
  }
  deriving (Eq, Show)

instance ToJSON OnChainTransferRequestBody where
  toJSON :: OnChainTransferRequestBody -> Aeson.Value
  toJSON OnChainTransferRequestBody {..} =
    Aeson.object
      [ "idempotencyKey" Aeson..= onChainTransferRequestBodyIdempotencyKey,
        "source" Aeson..= onChainTransferRequestBodySource,
        "destination" Aeson..= onChainTransferRequestBodyDestination,
        "amount" Aeson..= onChainTransferRequestBodyAmount
      ]

---------------------------------------------------------------
-- Card Types
---------------------------------------------------------------

data CardsRequest

type instance CircleRequest CardsRequest = CircleResponseBody [ListCardResponseBody]

instance CircleHasParam CardsRequest PaginationQueryParams

instance CircleHasParam CardsRequest PageSizeQueryParam

data CardRequest

type instance CircleRequest CardRequest = CircleResponseBody CardResponseBody

-- | Response body for `listAllCards`
data ListCardResponseBody = ListCardResponseBody
  { listCardId :: !UUID,
    listCardStatus :: !Status,
    listCardBillingDetails :: !ListCardBillingDetails,
    listCardExpiryMonth :: !Int,
    listCardExpiryYear :: !Int,
    listCardNetwork :: !CardNetwork,
    listCardBin :: !(Maybe Text), -- first 6 digits of the Card, should be a custom newtype
    listCardIssuerCountry :: !(Maybe ISO3166Alpha2),
    listCardFingerprint :: !UUID,
    listCardVerification :: !VerificationData,
    listCardRiskEvaluation :: !(Maybe RiskEvaluation),
    listCardCreateDate :: !UTCTime,
    listCardUpdateDate :: !UTCTime
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec ListCardResponseBody)

instance HasCodec ListCardResponseBody where
  codec =
    object "ListCardResponseBody" $
      ListCardResponseBody
        <$> requiredField' "id" .= listCardId
        <*> requiredField' "status" .= listCardStatus
        <*> requiredField' "billingDetails" .= listCardBillingDetails
        <*> requiredField' "expMonth" .= listCardExpiryMonth
        <*> requiredField' "expYear" .= listCardExpiryYear
        <*> requiredField' "network" .= listCardNetwork
        <*> optionalField' "bin" .= listCardBin
        <*> optionalField' "issuerCountry" .= listCardIssuerCountry
        <*> requiredField' "fingerprint" .= listCardFingerprint
        <*> requiredField' "verification" .= listCardVerification
        <*> optionalField' "riskEvaluation" .= listCardRiskEvaluation
        <*> requiredField' "createDate" .= listCardCreateDate
        <*> requiredField' "updateDate" .= listCardUpdateDate

-- | Response body for methods that interface with an individual card: `createCard`, `getCard`, and `updateCard`
data CardResponseBody = CardResponseBody
  { cardId :: !UUID,
    cardStatus :: !Status,
    cardBillingDetails :: !BillingDetails,
    cardExpiryMonth :: !Int,
    cardExpiryYear :: !Int,
    cardNetwork :: !CardNetwork,
    cardLast4 :: !Text, -- last 4 digits of card, should be a custom type
    cardBin :: !(Maybe Text), -- first 6 digits of the card, should be a custom newtype
    cardIssuerCountry :: !(Maybe ISO3166Alpha2),
    cardFundingType :: !(Maybe CardFundingType),
    cardFingerprint :: !UUID,
    cardErrorCode :: !(Maybe VerificationErrorCode),
    cardVerification :: !VerificationData,
    cardRiskEvaluation :: !(Maybe RiskEvaluation),
    cardMetadata :: !ResponseMetadata,
    cardCreateDate :: !UTCTime,
    cardUpdateDate :: !UTCTime
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec CardResponseBody)

instance HasCodec CardResponseBody where
  codec =
    object "CardResponseBody" $
      CardResponseBody
        <$> requiredField' "id" .= cardId
        <*> requiredField' "status" .= cardStatus
        <*> requiredField' "billingDetails" .= cardBillingDetails
        <*> requiredField' "expMonth" .= cardExpiryMonth
        <*> requiredField' "expYear" .= cardExpiryYear
        <*> requiredField' "network" .= cardNetwork
        <*> requiredField' "last4" .= cardLast4
        <*> optionalField' "bin" .= cardBin
        <*> optionalField' "issuerCountry" .= cardIssuerCountry
        <*> optionalField' "fundingType" .= cardFundingType
        <*> requiredField' "fingerprint" .= cardFingerprint
        <*> optionalField' "errorCode" .= cardErrorCode
        <*> requiredField' "verification" .= cardVerification
        <*> optionalField' "riskEvaluation" .= cardRiskEvaluation
        <*> requiredField' "metadata" .= cardMetadata
        <*> requiredField' "createDate" .= cardCreateDate
        <*> requiredField' "updateDate" .= cardUpdateDate

-- | Request body to create a debit card.
data CreateCardRequestBody = CreateCardRequestBody
  { createCardIdempotencyKey :: !UUID,
    createCardKeyId :: !(Maybe Text), -- key1 in sandbox
    createCardEncryptedData :: !(Maybe Text), -- NB: this encrypted data contains the CVV AND the card number somehow
    createCardBillingDetails :: !BillingDetails,
    createCardExpiryMonth :: !Int,
    createCardExpiryYear :: !Int,
    createCardMetadata :: !RequestMetadata
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec CreateCardRequestBody)

instance HasCodec CreateCardRequestBody where
  codec =
    object "CreateCardRequestBody" $
      CreateCardRequestBody
        <$> requiredField' "idempotencyKey" .= createCardIdempotencyKey
        <*> optionalField' "keyId" .= createCardKeyId
        <*> requiredField' "encryptedData" .= createCardEncryptedData
        <*> requiredField' "billingDetails" .= createCardBillingDetails
        <*> requiredField' "expMonth" .= createCardExpiryMonth
        <*> requiredField' "expYear" .= createCardExpiryYear
        <*> requiredField' "metadata" .= createCardMetadata

-- | Request body to update a debit card.
data UpdateCardRequestBody = UpdateCardRequestBody
  { updateCardKeyId :: !(Maybe Text), -- key1 in sandbox
    updateCardEncryptedData :: !(Maybe Text), -- NB: this encrypted data contains the CVV AND the card number somehow
    updateCardExpiryMonth :: !Int,
    updateCardExpiryYear :: !Int
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec UpdateCardRequestBody)

instance HasCodec UpdateCardRequestBody where
  codec =
    object "UpdateCardRequestBody" $
      UpdateCardRequestBody
        <$> requiredField' "keyId" .= updateCardKeyId
        <*> requiredField' "encryptedData" .= updateCardEncryptedData
        <*> requiredField' "expMonth" .= updateCardExpiryMonth
        <*> requiredField' "expYear" .= updateCardExpiryYear

data ListCardBillingDetails = ListCardBillingDetails
  { listCardBillingDetailsCountry :: !ISO3166Alpha2,
    listCardBillingDetailsDistrict :: !District
  }
  deriving (Eq, Show, Generic)
  deriving
    ( ToJSON,
      FromJSON
    )
    via (Autodocodec ListCardBillingDetails)

instance HasCodec ListCardBillingDetails where
  codec =
    object "ListCardBillingDetails" $
      ListCardBillingDetails
        <$> requiredField' "country" .= listCardBillingDetailsCountry
        <*> requiredField' "district" .= listCardBillingDetailsDistrict

data CardNetwork
  = VISA
  | MASTERCARD
  | AMEX
  | UNKNOWN
  deriving (Eq, Show, Enum, Bounded)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec CardNetwork)

instance HasCodec CardNetwork where
  codec = shownBoundedEnumCodec

data CardFundingType
  = Credit
  | Debit
  | Prepaid
  | Unknown
  deriving (Eq, Show, Enum, Bounded)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec CardFundingType)

instance HasCodec CardFundingType where
  codec =
    stringConstCodec $
      NE.fromList
        [ (Credit, "credit"),
          (Debit, "debit"),
          (Prepaid, "prepaid"),
          (Unknown, "unknown")
        ]

data VerificationErrorCode
  = VerificationFailed
  | VerificationFraudDetected
  | VerificationDenied
  | VerificationNotSupportedByIssuer
  | VerificationStoppedByIssuer
  | VerificationCardFailed
  | VerificationCardInvalid
  | VerificationCardAddressMismatch
  | VerificationCardZipMismatch
  | VerificationCardCvvInvalid
  | VerificationCardExpired
  | VerificationCardLimitViolated
  | VerificationCardNotHonored
  | VerificationCardCvvRequired
  | VerificationCreditCardNotAllowed
  | VerificationCardAccountIneligible
  | VerificationCardNetworkUnsupported
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec VerificationErrorCode)

instance HasCodec VerificationErrorCode where
  codec =
    stringConstCodec $
      NE.fromList
        [ (VerificationFailed, "verification_failed"),
          (VerificationFraudDetected, "verification_fraud_detected"),
          (VerificationDenied, "verification_denied"),
          (VerificationNotSupportedByIssuer, "verification_not_supported_by_issuer"),
          (VerificationStoppedByIssuer, "verification_stopped_by_issuer"),
          (VerificationCardFailed, "card_failed"),
          (VerificationCardInvalid, "card_invalid"),
          (VerificationCardAddressMismatch, "card_address_mismatch"),
          (VerificationCardZipMismatch, "card_zip_mismatch"),
          (VerificationCardCvvInvalid, "card_cvv_invalid"),
          (VerificationCardExpired, "card_expired"),
          (VerificationCardLimitViolated, "card_limit_violated"),
          (VerificationCardNotHonored, "card_not_honored"),
          (VerificationCardCvvRequired, "card_cvv_required"),
          (VerificationCreditCardNotAllowed, "credit_card_not_allowed"),
          (VerificationCardAccountIneligible, "card_account_ineligible"),
          (VerificationCardNetworkUnsupported, "card_network_unsupported")
        ]

---------------------------------------------------------------
-- ACH Types
---------------------------------------------------------------

data ACHBankAccountRequest

type instance CircleRequest ACHBankAccountRequest = CircleResponseBody ACHBankAccountResponseBody

-- | Response body for `createACHAccount` and `getACHAccount`
data ACHBankAccountResponseBody = ACHBankAccountResponseBody
  { achBankAccountId :: !UUID,
    achBankAccountStatus :: !Status,
    achBankAccountAccountNumber :: !AccountNumber,
    achBankAccountRoutingNumber :: !RoutingNumber,
    achBankAccountBillingDetails :: !BillingDetails,
    achBankAccountType :: !(Maybe ACHBankAccountType),
    achBankAccountBankAddress :: !BankAddress,
    achBankAccountFingerprint :: !UUID,
    achBankAccountErrorCode :: !(Maybe ACHBankAccountErrorCode),
    achBankAccountRiskEvaluation :: !(Maybe RiskEvaluation),
    achBankAccountMetadata :: !ResponseMetadata,
    achBankAccountCreateDate :: !UTCTime,
    achBankAccountUpdateDate :: !UTCTime
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec ACHBankAccountResponseBody)

instance HasCodec ACHBankAccountResponseBody where
  codec =
    object "ACHBankAccountResponseBody" $
      ACHBankAccountResponseBody
        <$> requiredField' "id" .= achBankAccountId
        <*> requiredField' "status" .= achBankAccountStatus
        <*> requiredField' "accountNumber" .= achBankAccountAccountNumber
        <*> requiredField' "routingNumber" .= achBankAccountRoutingNumber
        <*> requiredField' "billingDetails" .= achBankAccountBillingDetails
        <*> optionalField' "bankAccountType" .= achBankAccountType
        <*> requiredField' "bankAddress" .= achBankAccountBankAddress
        <*> requiredField' "fingerprint" .= achBankAccountFingerprint
        <*> optionalField' "errorCode" .= achBankAccountErrorCode
        <*> optionalField' "riskEvaluation" .= achBankAccountRiskEvaluation
        <*> requiredField' "metadata" .= achBankAccountMetadata
        <*> requiredField' "createDate" .= achBankAccountCreateDate
        <*> requiredField' "updateDate" .= achBankAccountUpdateDate

data ACHBankAccountErrorCode
  = ACHBankAccountAuthorizationExpired
  | ACHBankAccountError
  | ACHBankAccountIneligible
  | ACHBankAccountNotFound
  | ACHBankAccountUnauthorized
  | ACHBankAccountUnsupportedRoutingNumber
  | ACHBankAccountVerificationFailed
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec ACHBankAccountErrorCode)

instance HasCodec ACHBankAccountErrorCode where
  codec =
    stringConstCodec $
      NE.fromList
        [ (ACHBankAccountAuthorizationExpired, "bank_account_authorization_expired"),
          (ACHBankAccountError, "bank_account_error"),
          (ACHBankAccountIneligible, "bank_account_ineligible"),
          (ACHBankAccountNotFound, "bank_account_not_found"),
          (ACHBankAccountUnauthorized, "bank_account_unauthorized"),
          (ACHBankAccountUnsupportedRoutingNumber, "unsupported_routing_number"),
          (ACHBankAccountVerificationFailed, "verification_failed")
        ]

-- | Request body to an ACH bank account.
data CreateACHBankAccountRequestBody = CreateACHBankAccountRequestBody
  { achBankAccountBodyIdempotencyKey :: !UUID,
    achBankAccountBodyPlaidProcessorToken :: !ProcessorToken,
    achBankAccountBodyBillingDetails :: !BillingDetails,
    achBankAccountBodyBankAccountType :: !(Maybe ACHBankAccountType),
    achBankAccountBodyMetadata :: !RequestMetadata
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec CreateACHBankAccountRequestBody)

instance HasCodec CreateACHBankAccountRequestBody where
  codec =
    object "CreateACHBankAccountRequestBody" $
      CreateACHBankAccountRequestBody
        <$> requiredField' "idempotencyKey" .= achBankAccountBodyIdempotencyKey
        <*> requiredField' "plaidProcessorToken" .= achBankAccountBodyPlaidProcessorToken
        <*> requiredField' "billingDetails" .= achBankAccountBodyBillingDetails
        <*> optionalField' "bankAccountType" .= achBankAccountBodyBankAccountType
        <*> requiredField' "metadata" .= achBankAccountBodyMetadata

---------------------------------------------------------------
-- Mock Account Types
---------------------------------------------------------------

data MockAccountRequest

type instance CircleRequest MockAccountRequest = CircleResponseBody MockACHBankAccountResponseBody

-- | Request body to create a mock ACH bank account (in the sandbox only).
data CreateMockACHBankAccountRequestBody = CreateMockACHBankAccountRequestBody
  { mockACHBankAccountBodyAccount :: !MockACHBankAccount,
    mockACHBankAccountBodyBalance :: !MoneyAmount
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec CreateMockACHBankAccountRequestBody)

instance HasCodec CreateMockACHBankAccountRequestBody where
  codec =
    object "CreateMockACHBankAccountRequestBody" $
      CreateMockACHBankAccountRequestBody
        <$> requiredField' "account" .= mockACHBankAccountBodyAccount
        <*> requiredField' "balance" .= mockACHBankAccountBodyBalance

-- | Response body for methods that call the "mocks\/ach\/account" endpoint
data MockACHBankAccountResponseBody = MockACHBankAccountResponseBody
  { mockACHBankAccountResponseBodyAccount :: !MockACHBankAccount,
    mockACHBankAccountResponseBodyBalance :: !MoneyAmount,
    -- in sandbox, the value of this token is processor-sandbox-circle-82cf95bb-43f8-4191-8d30-2c9f42853621
    mockACHBankAccountResponseBodyProcessorToken :: !ProcessorToken
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec MockACHBankAccountResponseBody)

instance HasCodec MockACHBankAccountResponseBody where
  codec =
    object "MockACHBankAccountResponseBody" $
      MockACHBankAccountResponseBody
        <$> requiredField' "account" .= mockACHBankAccountResponseBodyAccount
        <*> requiredField' "balance" .= mockACHBankAccountResponseBodyBalance
        <*> requiredField' "processorToken" .= mockACHBankAccountResponseBodyProcessorToken

data MockACHBankAccount = MockACHBankAccount
  { mockACHBankAccountAccountNumber :: !AccountNumber,
    mockACHBankAccountRoutingNumber :: !MockRoutingNumber,
    mockACHBankAccountDescription :: !Text
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec MockACHBankAccount)

instance HasCodec MockACHBankAccount where
  codec =
    object "MockACHBankAccount" $
      MockACHBankAccount
        <$> requiredField' "accountNumber" .= mockACHBankAccountAccountNumber
        <*> requiredField' "routingNumber" .= mockACHBankAccountRoutingNumber
        <*> requiredField' "description" .= mockACHBankAccountDescription

data MockRoutingNumber
  = MockRoutingNumber1
  | MockRoutingNumber2
  | MockRoutingNumber3
  | MockRoutingNumber4
  | MockRoutingNumber5
  | MockRoutingNumber6
  | MockRoutingNumber7
  | MockRoutingNumber8
  | MockRoutingNumber9
  deriving (Eq, Show, Enum, Bounded)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec MockRoutingNumber)

instance HasCodec MockRoutingNumber where
  codec =
    stringConstCodec $
      NE.fromList
        [ (MockRoutingNumber1, "011000028"),
          (MockRoutingNumber2, "011201762"),
          (MockRoutingNumber3, "011500120"),
          (MockRoutingNumber4, "021214862"),
          (MockRoutingNumber5, "121000248"),
          (MockRoutingNumber6, "121140399"),
          (MockRoutingNumber7, "211073473"),
          (MockRoutingNumber8, "221172610"),
          (MockRoutingNumber9, "011000138")
        ]

---------------------------------------------------------------
-- SEPA Types
---------------------------------------------------------------

data SEPAAccountRequest

type instance CircleRequest SEPAAccountRequest = CircleResponseBody SEPAAccountResponseBody

data SEPAInstructionsRequest

type instance CircleRequest SEPAInstructionsRequest = CircleResponseBody WireInstructionsResponseData

-- | Request body to create a SEPA account.
data SEPAAccountRequestBody = SEPAAccountRequestBody
  { sepaAccountRequestBodyIdempotencyKey :: !UUID,
    sepaAccountRequestBodyIBAN :: !Iban,
    sepaAccountRequestBodyBillingDetails :: !BillingDetails
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec SEPAAccountRequestBody)

instance HasCodec SEPAAccountRequestBody where
  codec =
    object "SEPAAccountRequestBody" $
      SEPAAccountRequestBody
        <$> requiredField' "idempotencyKey" .= sepaAccountRequestBodyIdempotencyKey
        <*> requiredField' "iban" .= sepaAccountRequestBodyIBAN
        <*> requiredField' "billingDetails" .= sepaAccountRequestBodyBillingDetails

-- | Response body for methods that call the "banks\/sepa" endpoint
data SEPAAccountResponseBody = SEPAAccountResponseBody
  { sepaAccountResponseBodyId :: !UUID,
    sepaAccountResponseBodyStatus :: !Status,
    sepaAccountResponseBodyDescription :: !Text, -- TODO better type: Bank name plus last four digits of the bank account number or IBAN.  Make a custom type for this
    sepaAccountResponseBodyTrackingRef :: !TrackingReference,
    sepaAccountResponseBodyFingerprint :: !UUID,
    sepaAccountResponseBodyRiskEvaluation :: !(Maybe RiskEvaluation),
    sepaAccountResponseBodyBillingDetails :: !BillingDetails,
    sepaAccountResponseBodyCreateDate :: !UTCTime,
    sepaAccountResponseBodyUpdateDate :: !UTCTime
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec SEPAAccountResponseBody)

instance HasCodec SEPAAccountResponseBody where
  codec =
    object "SEPAAccountResponseBody" $
      SEPAAccountResponseBody
        <$> requiredField' "id" .= sepaAccountResponseBodyId
        <*> requiredField' "status" .= sepaAccountResponseBodyStatus
        <*> requiredField' "description" .= sepaAccountResponseBodyDescription
        <*> requiredField' "trackingRef" .= sepaAccountResponseBodyTrackingRef
        <*> requiredField' "fingerprint" .= sepaAccountResponseBodyFingerprint
        <*> optionalField' "riskEvaluation" .= sepaAccountResponseBodyRiskEvaluation
        <*> requiredField' "billingDetails" .= sepaAccountResponseBodyBillingDetails
        <*> requiredField' "createDate" .= sepaAccountResponseBodyCreateDate
        <*> requiredField' "updateDate" .= sepaAccountResponseBodyUpdateDate

---------------------------------------------------------------
-- Settlements Types
---------------------------------------------------------------

data SettlementRequest

type instance CircleRequest SettlementRequest = CircleResponseBody SettlementResponseBody

data SettlementsRequest

type instance CircleRequest SettlementsRequest = CircleResponseBody [SettlementResponseBody]

instance CircleHasParam SettlementsRequest PaginationQueryParams

instance CircleHasParam SettlementsRequest FromQueryParam

instance CircleHasParam SettlementsRequest ToQueryParam

instance CircleHasParam SettlementsRequest PageSizeQueryParam

-- | Response body for methods that call the "settlements" endpoint
data SettlementResponseBody = SettlementResponseBody
  { settlementResponseBodyId :: !UUID,
    settlementResponseBodyMerchantWalletId :: !UUID,
    settlementResponseBodyWalletId :: !UUID,
    settlementResponseBodyTotalDebits :: !MoneyAmount,
    settlementResponseBodyTotalCredits :: !MoneyAmount,
    settlementResponseBodyPaymentFees :: !MoneyAmount,
    settlementResponseBodyChargebackFees :: !MoneyAmount,
    settlementResponseBodyCreateDate :: !UTCTime,
    settlementResponseBodyUpdateDate :: !UTCTime
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec SettlementResponseBody)

instance HasCodec SettlementResponseBody where
  codec =
    object "SettlementResponseBody" $
      SettlementResponseBody
        <$> requiredField' "id" .= settlementResponseBodyId
        <*> requiredField' "merchantWalletId" .= settlementResponseBodyMerchantWalletId
        <*> requiredField' "walletId" .= settlementResponseBodyWalletId
        <*> requiredField' "totalDebits" .= settlementResponseBodyTotalDebits
        <*> requiredField' "totalCredits" .= settlementResponseBodyTotalCredits
        <*> requiredField' "paymentFees" .= settlementResponseBodyPaymentFees
        <*> requiredField' "chargebackFees" .= settlementResponseBodyChargebackFees
        <*> requiredField' "createDate" .= settlementResponseBodyCreateDate
        <*> requiredField' "updateDate" .= settlementResponseBodyUpdateDate

---------------------------------------------------------------
-- Chargeback Types
---------------------------------------------------------------

data ChargebacksRequest

type instance CircleRequest ChargebacksRequest = CircleResponseBody [ChargebackResponseBody]

instance CircleHasParam ChargebacksRequest PaginationQueryParams

instance CircleHasParam ChargebacksRequest FromQueryParam

instance CircleHasParam ChargebacksRequest ToQueryParam

instance CircleHasParam ChargebacksRequest PageSizeQueryParam

instance CircleHasParam ChargebacksRequest PaymentIdQueryParam

data ChargebackRequest

type instance CircleRequest ChargebackRequest = CircleResponseBody ChargebackResponseBody

data MockChargebackRequest

type instance CircleRequest MockChargebackRequest = CircleResponseBody ChargebackResponseBody

-- | Response body for methods that call the "cards\/chargebacks" and "mocks\/cards\/chargebacks" endpoints
data ChargebackResponseBody = ChargebackResponseBody
  { chargebackResponseBodyId :: !UUID,
    chargebackResponseBodyPaymentId :: !UUID,
    chargebackResponseBodyMerchantId :: !UUID,
    chargebackResponseBodyReasonCode :: !Text, -- it's open-ended, no type will save this
    chargebackResponseBodyCategory :: !(Maybe ChargebackCategory),
    chargebackResponseBodyHistory :: [ChargebackHistory]
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec ChargebackResponseBody)

instance HasCodec ChargebackResponseBody where
  codec =
    object "ChargebackResponseBody" $
      ChargebackResponseBody
        <$> requiredField' "id" .= chargebackResponseBodyId
        <*> requiredField' "paymentId" .= chargebackResponseBodyPaymentId
        <*> requiredField' "merchantId" .= chargebackResponseBodyMerchantId
        <*> requiredField' "reasonCode" .= chargebackResponseBodyReasonCode
        <*> optionalField' "category" .= chargebackResponseBodyCategory
        <*> requiredField' "history" .= chargebackResponseBodyHistory

data ChargebackCategory
  = CanceledRecurringPayment
  | CustomerDispute
  | Fraudulent
  | General
  | ProcessingError
  | NotDefined
  deriving (Eq, Show, Enum, Bounded)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec ChargebackCategory)

instance HasCodec ChargebackCategory where
  codec =
    stringConstCodec $
      NE.fromList
        [ (CanceledRecurringPayment, "Canceled Recurring Payment"),
          (CustomerDispute, "Customer Dispute"),
          (Fraudulent, "Fraudulent"),
          (General, "General"),
          (ProcessingError, "Processing Error"),
          (NotDefined, "Not Defined")
        ]

data ChargebackHistory = ChargebackHistory
  { chargebackHistoryType :: !ChargebackHistoryType,
    chargebackHistoryAmount :: !MoneyAmount,
    chargebackHistoryFee :: !(Maybe MoneyAmount),
    chargebackHistoryDescription :: !Text,
    chargebackHistorySettlementId :: !(Maybe UUID),
    chargebackHistoryCreateDate :: !UTCTime
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec ChargebackHistory)

instance HasCodec ChargebackHistory where
  codec =
    object "ChargebackHistory" $
      ChargebackHistory
        <$> requiredField' "type" .= chargebackHistoryType
        <*> requiredField' "amount" .= chargebackHistoryAmount
        <*> requiredField' "fee" .= chargebackHistoryFee
        <*> requiredField' "description" .= chargebackHistoryDescription
        <*> optionalField' "settlementId" .= chargebackHistorySettlementId
        <*> requiredField' "createDate" .= chargebackHistoryCreateDate

data ChargebackHistoryType
  = FirstChargeback
  | SecondChargeback
  | ChargebackReversal
  | Representment
  | ChargebackSettlement
  deriving (Eq, Show, Enum, Bounded)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec ChargebackHistoryType)

instance HasCodec ChargebackHistoryType where
  codec =
    stringConstCodec $
      NE.fromList
        [ (FirstChargeback, "First Chargeback"),
          (SecondChargeback, "Second Chargeback"),
          (ChargebackReversal, "Chargeback Reversal"),
          (Representment, "Representment"),
          (ChargebackSettlement, "Chargeback Settlement")
        ]

---------------------------------------------------------------
-- Reversal Types
---------------------------------------------------------------

data ReversalsRequest

type instance CircleRequest ReversalsRequest = CircleResponseBody [ReversalResponseBody]

instance CircleHasParam ReversalsRequest PaginationQueryParams

instance CircleHasParam ReversalsRequest FromQueryParam

instance CircleHasParam ReversalsRequest ToQueryParam

instance CircleHasParam ReversalsRequest PageSizeQueryParam

instance CircleHasParam ReversalsRequest PaymentStatusQueryParams

-- | Response body for methods that call the "reversals" endpoint
data ReversalResponseBody = ReversalResponseBody
  { reversalResponseBodyId :: !UUID,
    reversalResponseBodyPaymentId :: !UUID,
    reversalResponseBodyAmount :: !MoneyAmount,
    reversalResponseBodyDescription :: !Text,
    reversalResponseBodyStatus :: !Status,
    reversalResponseBodyReason :: !ReversalReason,
    reversalResponseBodyFees :: !MoneyAmount,
    reversalResponseBodyCreateDate :: !UTCTime,
    reversalResponseBodyUpdateDate :: !UTCTime
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec ReversalResponseBody)

instance HasCodec ReversalResponseBody where
  codec =
    object "ReversalResponseBody" $
      ReversalResponseBody
        <$> requiredField' "id" .= reversalResponseBodyId
        <*> requiredField' "paymentId" .= reversalResponseBodyPaymentId
        <*> requiredField' "amount" .= reversalResponseBodyAmount
        <*> requiredField' "description" .= reversalResponseBodyDescription
        <*> requiredField' "status" .= reversalResponseBodyStatus
        <*> requiredField' "reason" .= reversalResponseBodyReason
        <*> requiredField' "fees" .= reversalResponseBodyFees
        <*> requiredField' "createDate" .= reversalResponseBodyCreateDate
        <*> requiredField' "updateDate" .= reversalResponseBodyUpdateDate

data ReversalReason
  = ReversalDuplicate
  | ReversalFraudulent
  | ReversalRequestedByCustomer
  | ReversalBankTransactionError
  | ReversalInvalidAccountNumber
  | ReversalInsufficientFunds
  | ReversalPaymentStoppedByIssuer
  | ReversalPaymentReturned
  | ReversalBankAccountIneligible
  | ReversalInvalidACHRTN
  | ReversalUnauthorizedTransaction
  | ReversalPaymentFailed
  deriving (Eq, Show, Enum, Bounded)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec ReversalReason)

instance HasCodec ReversalReason where
  codec =
    stringConstCodec $
      NE.fromList
        [ (ReversalDuplicate, "duplicate"),
          (ReversalFraudulent, "fraudulent"),
          (ReversalRequestedByCustomer, "requested_by_customer"),
          (ReversalBankTransactionError, "bank_transaction_error"),
          (ReversalInvalidAccountNumber, "invalid_account_number"),
          (ReversalInsufficientFunds, "insufficient_funds"),
          (ReversalPaymentStoppedByIssuer, "payment_stopped_by_issuer"),
          (ReversalPaymentReturned, "payment_returned"),
          (ReversalBankAccountIneligible, "bank_account_ineligible"),
          (ReversalInvalidACHRTN, "invalid_ach_rtn"),
          (ReversalUnauthorizedTransaction, "unauthorized_transaction"),
          (ReversalPaymentFailed, "payment_failed")
        ]

---------------------------------------------------------------
-- Payment Intent Types
---------------------------------------------------------------

data PaymentIntentRequest

type instance CircleRequest PaymentIntentRequest = CircleResponseBody PaymentIntentResponseBody

data PaymentIntentsRequest

type instance CircleRequest PaymentIntentsRequest = CircleResponseBody [PaymentIntentResponseBody]

instance CircleHasParam PaymentIntentsRequest PaginationQueryParams

instance CircleHasParam PaymentIntentsRequest FromQueryParam

instance CircleHasParam PaymentIntentsRequest ToQueryParam

instance CircleHasParam PaymentIntentsRequest PageSizeQueryParam

instance CircleHasParam PaymentIntentsRequest PaymentStatusQueryParams

instance CircleHasParam PaymentIntentsRequest PaymentIntentContextQueryParams

-- | Request body to create a payment intent for a blockchain payment
data CreatePaymentIntentRequestBody = CreatePaymentIntentRequestBody
  { createPaymentIntentIdempotencyKey :: !UUID,
    createPaymentIntentAmount :: !MoneyAmount,
    createPaymentIntentSettlementCurrency :: !SupportedCurrencies,
    createPaymentIntentPaymentMethods :: [PaymentMethodData]
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec CreatePaymentIntentRequestBody)

instance HasCodec CreatePaymentIntentRequestBody where
  codec =
    object "CreatePaymentIntentRequestBody" $
      CreatePaymentIntentRequestBody
        <$> requiredField' "idempotencyKey" .= createPaymentIntentIdempotencyKey
        <*> requiredField' "amount" .= createPaymentIntentAmount
        <*> requiredField' "settlementCurrency" .= createPaymentIntentSettlementCurrency
        <*> requiredField' "paymentMethods" .= createPaymentIntentPaymentMethods

-- | Response body for methods that call the "paymentIntents" endpoint
data PaymentIntentResponseBody = PaymentIntentResponseBody
  { paymentIntentResponseBodyIdempotencyKey :: !UUID,
    paymentIntentResponseBodyId :: !UUID,
    paymentIntentResponseBodyAmount :: !MoneyAmount,
    paymentIntentResponseBodyAmountPaid :: !MoneyAmount,
    paymentIntentResponseBodySettlementCurrency :: !SupportedCurrencies,
    paymentIntentResponseBodyPaymentMethods :: ![PaymentMethodData],
    paymentIntentResponseBodyFees :: ![BlockchainFeeMoneyAmount],
    paymentIntentResponseBodyPaymentIds :: ![UUID],
    paymentIntentResponseBodyTimeline :: ![TimelineData],
    paymentIntentResponseBodyExpiresOn :: !UTCTime,
    paymentIntentResponseBodyUpdateDate :: !UTCTime,
    paymentIntentResponseBodyCreateDate :: !UTCTime
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec PaymentIntentResponseBody)

instance HasCodec PaymentIntentResponseBody where
  codec =
    object "PaymentIntentResponseBody" $
      PaymentIntentResponseBody
        <$> requiredField' "idempotencyKey" .= paymentIntentResponseBodyIdempotencyKey
        <*> requiredField' "id" .= paymentIntentResponseBodyId
        <*> requiredField' "amount" .= paymentIntentResponseBodyAmount
        <*> requiredField' "amountPaid" .= paymentIntentResponseBodyAmountPaid
        <*> requiredField' "settlementCurrency" .= paymentIntentResponseBodySettlementCurrency
        <*> requiredField' "paymentMethods" .= paymentIntentResponseBodyPaymentMethods
        <*> requiredField' "fees" .= paymentIntentResponseBodyFees
        <*> requiredField' "paymentIds" .= paymentIntentResponseBodyPaymentIds
        <*> requiredField' "timeline" .= paymentIntentResponseBodyTimeline
        <*> requiredField' "expiresOn" .= paymentIntentResponseBodyExpiresOn
        <*> requiredField' "updateDate" .= paymentIntentResponseBodyUpdateDate
        <*> requiredField' "createDate" .= paymentIntentResponseBodyCreateDate

data PaymentMethodData = PaymentMethodData
  { paymentMethodType :: !Text, -- just "blockchain"
    paymentMethodDataChain :: !Chain,
    paymentMethodDataAddress :: !(Maybe HexString)
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec PaymentMethodData)

instance HasCodec PaymentMethodData where
  codec =
    object "PaymentMethodData" $
      PaymentMethodData
        <$> requiredField' "type" .= paymentMethodType
        <*> requiredField' "chain" .= paymentMethodDataChain
        <*> optionalField' "address" .= paymentMethodDataAddress

data TimelineData = TimelineData
  { timelineDataStatus :: !PaymentIntentStatus,
    timelineDataContext :: !PaymentIntentContext,
    timelineDataTime :: !UTCTime
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec TimelineData)

instance HasCodec TimelineData where
  codec =
    object "TimelineData" $
      TimelineData
        <$> requiredField' "status" .= timelineDataStatus
        <*> requiredField' "context" .= timelineDataContext
        <*> requiredField' "time" .= timelineDataTime

---------------------------------------------------------------
-- Payout Return Types
---------------------------------------------------------------

data ReturnsRequest

type instance CircleRequest ReturnsRequest = CircleResponseBody [PayoutReturnResponseBody]

instance CircleHasParam ReturnsRequest PaginationQueryParams

instance CircleHasParam ReturnsRequest FromQueryParam

instance CircleHasParam ReturnsRequest ToQueryParam

instance CircleHasParam ReturnsRequest PageSizeQueryParam

---------------------------------------------------------------
-- Wallet Types
---------------------------------------------------------------

data WalletRequest

type instance CircleRequest WalletRequest = CircleResponseBody WalletResponseBody

data WalletsRequest

type instance CircleRequest WalletsRequest = CircleResponseBody [WalletResponseBody]

instance CircleHasParam WalletsRequest PaginationQueryParams

instance CircleHasParam WalletsRequest FromQueryParam

instance CircleHasParam WalletsRequest ToQueryParam

instance CircleHasParam WalletsRequest PageSizeQueryParam

-- | Response body for methods that call the "wallets" endpoint
data WalletResponseBody = WalletResponseBody
  { walletResponseBodyWalletId :: !WalletId,
    walletResponseBodyEntityId :: !UUID,
    walletResponseBodyType :: !Text, -- This value will always be "end_user_wallet"
    walletResponseBodyDescription :: !(Maybe Text),
    walletResponseBodyBalances :: [MoneyAmount]
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec WalletResponseBody)

instance HasCodec WalletResponseBody where
  codec =
    object "WalletResponseBody" $
      WalletResponseBody
        <$> requiredField' "walletId" .= walletResponseBodyWalletId
        <*> requiredField' "entityId" .= walletResponseBodyEntityId
        <*> requiredField' "type" .= walletResponseBodyType
        <*> optionalField' "description" .= walletResponseBodyDescription
        <*> requiredField' "balances" .= walletResponseBodyBalances

-- | Request body to create a Circle wallet.
data CreateWalletRequestBody = CreateWalletRequestBody
  { createWalletRequestBodyIdempotencyKey :: !UUID,
    createWalletRequestBodyDescription :: !(Maybe Text)
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec CreateWalletRequestBody)

instance HasCodec CreateWalletRequestBody where
  codec =
    object "CreateWalletRequestBody" $
      CreateWalletRequestBody
        <$> requiredField' "idempotencyKey" .= createWalletRequestBodyIdempotencyKey
        <*> optionalField' "description" .= createWalletRequestBodyDescription

---------------------------------------------------------------
-- Utils
---------------------------------------------------------------

utcToCircle :: UTCTime -> Text
utcToCircle ut =
  tshow day <> "T" <> clockTime <> "-00:00"
  where
    day = utctDay ut
    time = utctDayTime ut
    tod = snd $ utcToLocalTimeOfDay utc (timeToTimeOfDay time)
    atLeastTwo :: Text -> Int -> Text
    atLeastTwo t i
      | i < 10 = t <> tshow i
      | otherwise = tshow i
    clockTime =
      atLeastTwo "0" (todHour tod)
        <> ":"
        <> atLeastTwo "0" (todMin tod)
        <> ":"
        <> atLeastTwo "0" (floor $ todSec tod)

tshow :: Show a => a -> Text
tshow = T.pack . show

-- | Similar to 'Either' but with different 'ToJSON' and 'FromJSON' instances.
-- 'ToJSON' will serialize the payload without adding any kind of tag.
-- 'FromJSON' will first attempt to parse JSON as the first type parameter,
-- and if that fails will then attempt to parse as the second type parameter.
--
-- NB: The order of type parameters make a huge difference!
-- In @'ThisOrThat' A B@, if @A@ is structurally a subtype of @B@, then the
-- parser can never produce a 'That' result! For example, with types
--
-- @
--   data A = A {foo :: String, bar :: Scientific}
--   data B = B {foo :: String, bar :: Scientific, baz :: Bool}
-- @
--
-- @'FromJSON' ('ThisOrThat' A B)@ can never parse a @B@. Any JSON object
-- that includes properties @foo: <string>@ and @bar: <number>@ will parse
-- as an @A@, whether or not the property @baz: <boolean>@ is present. You
-- can fix this by instead using @'ThisOrThat' B A@.
data ThisOrThat a b = This a | That b
  deriving stock (Eq, Generic)

catThises :: [ThisOrThat a b] -> [a]
catThises lst = lst >>= toThis
  where
    toThis = \case
      This a -> [a]
      _ -> []

catThats :: [ThisOrThat a b] -> [b]
catThats lst = lst >>= toThat
  where
    toThat = \case
      That b -> [b]
      _ -> []

instance (Show a, Show b) => Show (ThisOrThat a b) where
  show = \case
    This a -> show a
    That b -> show b

instance (ToJSON a, ToJSON b) => ToJSON (ThisOrThat a b) where
  toJSON (This a) = toJSON a
  toJSON (That b) = toJSON b
  toEncoding (This a) = toEncoding a
  toEncoding (That b) = toEncoding b

instance (FromJSON a, FromJSON b) => FromJSON (ThisOrThat a b) where
  parseJSON val = do
    let parsedA = fromJSON val
        parsedB = fromJSON val
    case (parsedA, parsedB) of
      (Success a, _) -> pure $ This a
      (_, Success b) -> pure $ That b
      (Error thisError, Error thatError) ->
        fail $
          fold
            [ "Failed when parsing a ThisOrThat from JSON.\n",
              "Error on the This: " <> thisError <> "\n",
              "Error on the That: " <> thatError
            ]

instance Bifunctor ThisOrThat where
  bimap f _ (This a) = This (f a)
  bimap _ g (That b) = That (g b)

thisOrThatToEither :: ThisOrThat a b -> Either a b
thisOrThatToEither = \case
  This a -> Left a
  That b -> Right b

thisOrThat :: (a -> c) -> (b -> c) -> ThisOrThat a b -> c
thisOrThat f g tot = either f g $ thisOrThatToEither tot

---------------------------------------------------------------
-- General, shared types
---------------------------------------------------------------
data Status = Pending | Complete | Failed
  deriving (Show, Eq)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec Status)

instance HasCodec Status where
  codec = stringConstCodec $ NE.fromList [(Pending, "pending"), (Complete, "complete"), (Failed, "failed")]

data PaymentStatus
  = PaymentPending
  | Confirmed
  | Paid
  | PaymentFailed
  | ActionRequired
  deriving (Show, Eq)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec PaymentStatus)

instance HasCodec PaymentStatus where
  codec =
    stringConstCodec $
      NE.fromList
        [ (Confirmed, "confirmed"),
          (PaymentPending, "pending"),
          (Paid, "paid"),
          (PaymentFailed, "failed"),
          (ActionRequired, "action_required")
        ]

data PaymentIntentStatus
  = PaymentIntentCreated
  | PaymentIntentPending
  | PaymentIntentComplete
  | PaymentIntentExpired
  | PaymentIntentFailed
  deriving (Show, Eq)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec PaymentIntentStatus)

instance HasCodec PaymentIntentStatus where
  codec =
    stringConstCodec $
      NE.fromList
        [ (PaymentIntentCreated, "created"),
          (PaymentIntentPending, "pending"),
          (PaymentIntentComplete, "complete"),
          (PaymentIntentExpired, "expired"),
          (PaymentIntentFailed, "failed")
        ]

data PaymentIntentContext
  = ContextUnderpaid
  | ContextPaid
  | ContextOverpaid
  deriving (Show, Eq)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec PaymentIntentContext)

instance HasCodec PaymentIntentContext where
  codec =
    stringConstCodec $
      NE.fromList
        [ (ContextUnderpaid, "underpaid"),
          (ContextPaid, "paid"),
          (ContextOverpaid, "overpaid")
        ]

data Address = Address
  { addressCity :: !(Maybe City),
    addressCountry :: !(Maybe ISO3166Alpha2),
    addressLine1 :: !(Maybe AddressLine),
    addressLine2 :: !(Maybe AddressLine),
    addressDistrict :: !(Maybe District)
  }
  deriving (Eq, Show)
  deriving
    ( ToJSON,
      FromJSON
    )
    via (Autodocodec Address)

instance HasCodec Address where
  codec =
    object "Address" $
      Address
        <$> optionalField' "city" .= addressCity
        <*> optionalField' "country" .= addressCountry
        <*> optionalField' "line1" .= addressLine1
        <*> optionalField' "line2" .= addressLine2
        <*> optionalField' "district" .= addressDistrict

data BankAccountType = Wire | Sen
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec BankAccountType)

instance HasCodec BankAccountType where
  codec = stringConstCodec $ NE.fromList [(Wire, "wire"), (Sen, "sen")]

data ACHBankAccountType = RetailType | BusinessType
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec ACHBankAccountType)

instance HasCodec ACHBankAccountType where
  codec = stringConstCodec $ NE.fromList [(RetailType, "retail"), (BusinessType, "business")]

data DestinationBankAccount = DestinationBankAccount
  { destinationBankAccountType :: !BankAccountType,
    destinationBankAccountId :: !UUID,
    destinationBankAccountName :: !(Maybe Text)
  }
  deriving (Eq, Show)
  deriving
    ( ToJSON,
      FromJSON
    )
    via (Autodocodec DestinationBankAccount)

instance HasCodec DestinationBankAccount where
  codec =
    object "DestinationBankAccount" $
      DestinationBankAccount
        <$> requiredField' "type" .= destinationBankAccountType
        <*> requiredField' "id" .= destinationBankAccountId
        <*> optionalField' "name" .= destinationBankAccountName

-- TODO can we do type narrowing to have other types that represent subsets of
-- this one without have to write custom constructors?
data SupportedCurrencies = USD | EUR | BTC | ETH
  deriving (Eq, Show, Enum, Bounded)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec SupportedCurrencies)

instance HasCodec SupportedCurrencies where
  codec = shownBoundedEnumCodec

newtype Amount = Amount
  -- TODO consider making this a numeric type, maybe?
  { unAmount :: Text
  }
  deriving (Eq, Show, ToJSON, FromJSON)

instance HasCodec Amount where
  codec = dimapCodec Amount unAmount codec

data BlockchainFeeType = BlockchainLeaseFee | TotalPaymentFees
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec BlockchainFeeType)

instance HasCodec BlockchainFeeType where
  codec = stringConstCodec $ NE.fromList [(BlockchainLeaseFee, "blockChainLeaseFee"), (TotalPaymentFees, "totalPaymentFees")]

data BlockchainFeeMoneyAmount = BlockchainFeeMoneyAmount
  { blockchainFeeMoneyAmountType :: !BlockchainFeeType,
    blockchainFeeMoneyAmountAmount :: !Amount,
    blockchainFeeMoneyAmountCurrency :: !SupportedCurrencies
  }
  deriving (Eq, Show, Generic)
  deriving
    ( ToJSON,
      FromJSON
    )
    via (Autodocodec BlockchainFeeMoneyAmount)

instance HasCodec BlockchainFeeMoneyAmount where
  codec =
    object "BlockchainFeeMoneyAmount" $
      BlockchainFeeMoneyAmount
        <$> requiredField' "type" .= blockchainFeeMoneyAmountType
        <*> requiredField' "amount" .= blockchainFeeMoneyAmountAmount
        <*> requiredField' "currency" .= blockchainFeeMoneyAmountCurrency

data MoneyAmount = MoneyAmount
  { moneyAmountAmount :: !Amount,
    moneyAmountCurrency :: !SupportedCurrencies
  }
  deriving (Eq, Show, Generic)
  deriving
    ( ToJSON,
      FromJSON
    )
    via (Autodocodec MoneyAmount)

instance HasCodec MoneyAmount where
  codec =
    object "MoneyAmount" $
      MoneyAmount
        <$> requiredField' "amount" .= moneyAmountAmount
        <*> requiredField' "currency" .= moneyAmountCurrency

data TransferFeeAmount = TransferFeeAmount
  { transferFeeAmountAmount :: !Amount,
    transferFeeAmountCurrency :: !SupportedCurrencies,
    transferFeeAmountType :: !Text
  }
  deriving (Eq, Show, Generic)
  deriving
    ( ToJSON,
      FromJSON
    )
    via (Autodocodec TransferFeeAmount)

instance HasCodec TransferFeeAmount where
  codec =
    object "TransferFeeAmount" $
      TransferFeeAmount
        <$> requiredField' "amount" .= transferFeeAmountAmount
        <*> requiredField' "currency" .= transferFeeAmountCurrency
        <*> requiredField' "type" .= transferFeeAmountType

data Decision = Approved | Denied | Review
  deriving (Eq, Show, Generic)
  deriving
    ( ToJSON,
      FromJSON
    )
    via (Autodocodec Decision)

instance HasCodec Decision where
  codec = stringConstCodec $ NE.fromList [(Approved, "approved"), (Denied, "denied"), (Review, "review")]

data RiskEvaluation = RiskEvaluation
  { riskEvaluationDecision :: !Decision,
    -- Parsing riskEvaluationReason as a text because it's pretty open-ended: https:\/\/developers.circle.com\/developer\/docs\/cards-banks-and-payments-risk-evaluation
    -- TODO maybe eventually add type constraints around these potential fields (unlikely though, there are a lot)
    riskEvaluationReason :: !Text
  }
  deriving (Eq, Show, Generic)
  deriving
    ( ToJSON,
      FromJSON
    )
    via (Autodocodec RiskEvaluation)

instance HasCodec RiskEvaluation where
  codec =
    object "RiskEvaluation" $
      RiskEvaluation
        <$> requiredField' "decision" .= riskEvaluationDecision
        <*> requiredField' "reason" .= riskEvaluationReason

data Adjustments = Adjustments
  { adjustmentsFXCredit :: !MoneyAmount,
    adjustmentsFXDebit :: !MoneyAmount
  }
  deriving (Eq, Show, Generic)
  deriving
    ( ToJSON,
      FromJSON
    )
    via (Autodocodec Adjustments)

instance HasCodec Adjustments where
  codec =
    object "Adjustments" $
      Adjustments
        <$> requiredField' "fxCredit" .= adjustmentsFXCredit
        <*> requiredField' "fxDebit" .= adjustmentsFXDebit

data BillingDetails = BillingDetails
  { billingDetailsName :: !Text,
    billingDetailsCity :: !City,
    billingDetailsCountry :: !ISO3166Alpha2,
    billingDetailsLine1 :: !AddressLine, -- address type
    billingDetailsLine2 :: !(Maybe AddressLine), -- secondary address type
    billingDetailsDistrict :: !(Maybe District), -- could be a state type
    billingDetailsPostalCode :: !PostalCode -- postal code type
  }
  deriving (Eq, Show, Generic)
  deriving
    ( ToJSON,
      FromJSON
    )
    via (Autodocodec BillingDetails)

instance HasCodec BillingDetails where
  codec =
    object "BillingDetails" $
      BillingDetails
        <$> requiredField' "name" .= billingDetailsName
        <*> requiredField' "city" .= billingDetailsCity
        <*> requiredField' "country" .= billingDetailsCountry
        <*> requiredField' "line1" .= billingDetailsLine1
        <*> optionalField' "line2" .= billingDetailsLine2
        <*> optionalField' "district" .= billingDetailsDistrict
        <*> requiredField' "postalCode" .= billingDetailsPostalCode

-- TODO refactor this type to use address WithName.  Look into the same stuff that we used at Mercury for WithName
data BankAddress = BankAddress
  { bankAddressName :: !(Maybe Text),
    bankAddressCity :: !(Maybe City),
    bankAddressCountry :: !(Maybe ISO3166Alpha2),
    bankAddressLine1 :: !(Maybe AddressLine),
    bankAddressLine2 :: !(Maybe AddressLine),
    bankAddressDistrict :: !(Maybe District)
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec BankAddress)

instance HasCodec BankAddress where
  codec =
    object "BankAddress" $
      BankAddress
        <$> optionalField' "name" .= bankAddressName
        <*> optionalField' "city" .= bankAddressCity
        <*> optionalField' "country" .= bankAddressCountry
        <*> optionalField' "line1" .= bankAddressLine1
        <*> optionalField' "line2" .= bankAddressLine2
        <*> optionalField' "district" .= bankAddressDistrict

data BeneficiaryDetails = BeneficiaryDetails
  { beneficiaryDetailsName :: !Text,
    beneficiaryDetailsAddress1 :: !(Maybe AddressLine),
    beneficiaryDetailsAddress2 :: !(Maybe AddressLine)
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec BeneficiaryDetails)

instance HasCodec BeneficiaryDetails where
  codec =
    object "BeneficiaryDetails" $
      BeneficiaryDetails
        <$> requiredField' "name" .= beneficiaryDetailsName
        <*> optionalField' "address1" .= beneficiaryDetailsAddress1
        <*> optionalField' "address2" .= beneficiaryDetailsAddress2

data BeneficiaryBankDetails = BeneficiaryBankDetails
  { beneficiaryBankDetailsName :: !Text,
    beneficiaryBankDetailsSwiftCode :: !SwiftCode,
    beneficiaryBankDetailsRoutingNumber :: !RoutingNumber,
    beneficiaryBankDetailsAccountNumber :: !AccountNumber,
    beneficiaryBankDetailsCurrency :: !SupportedCurrencies,
    beneficiaryBankDetailsAddress :: !AddressLine,
    beneficiaryBankDetailsCity :: !City,
    beneficiaryBankDetailsPostalCode :: !PostalCode,
    beneficiaryBankDetailsCountry :: !ISO3166Alpha2
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec BeneficiaryBankDetails)

instance HasCodec BeneficiaryBankDetails where
  codec =
    object "BeneficiaryBankDetails" $
      BeneficiaryBankDetails
        <$> requiredField' "name" .= beneficiaryBankDetailsName
        <*> requiredField' "swiftCode" .= beneficiaryBankDetailsSwiftCode
        <*> requiredField' "routingNumber" .= beneficiaryBankDetailsRoutingNumber
        <*> requiredField' "accountNumber" .= beneficiaryBankDetailsAccountNumber
        <*> requiredField' "currency" .= beneficiaryBankDetailsCurrency
        <*> requiredField' "address" .= beneficiaryBankDetailsAddress
        <*> requiredField' "city" .= beneficiaryBankDetailsCity
        <*> requiredField' "postalCode" .= beneficiaryBankDetailsPostalCode
        <*> requiredField' "country" .= beneficiaryBankDetailsCountry

newtype ProcessorToken = ProcessorToken
  { unProcessorToken :: Text
  }
  deriving (Eq, Show, ToJSON, FromJSON)

instance HasCodec ProcessorToken where
  codec = dimapCodec ProcessorToken unProcessorToken codec

newtype AddressLine = AddressLine
  { unAddressLine :: Text
  }
  deriving (Eq, Show, ToJSON, FromJSON)

instance HasCodec AddressLine where
  codec = dimapCodec AddressLine unAddressLine codec

newtype URL = URL {unURL :: Text}
  deriving stock (Eq, Show, Lift)
  deriving newtype (ToJSON)

-- From https://daringfireball.net/2010/07/improved_regex_for_matching_urls
urlRegex :: Regex
urlRegex =
  [re|(?i)\b((?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))|]

mkURL :: Text -> Maybe URL
mkURL t =
  if t =~ urlRegex
    then Just (URL t)
    else Nothing

urlToText :: URL -> Text
urlToText (URL t) = t

urlToByteString :: URL -> BS8.ByteString
urlToByteString url = TE.encodeUtf8 $ urlToText url

instance FromJSON URL where
  parseJSON = withText "URL" $ \t ->
    case mkURL t of
      Nothing -> fail $ "Invalid URL: " ++ T.unpack t
      Just url -> pure url

instance HasCodec URL where
  codec = dimapCodec URL unURL codec

newtype AccountNumber = AccountNumber {unAccountNumber :: Text}
  deriving stock (Eq, Show, Lift)
  deriving newtype (ToJSON)

-- Account numbers can have capital letters or digits
accountNumberRegex :: Regex
accountNumberRegex = [re|^[A-Z0-9]{4,17}$|]

mkAccountNumber :: Text -> Maybe AccountNumber
mkAccountNumber t =
  if t =~ accountNumberRegex
    then Just (AccountNumber t)
    else Nothing

accountNumberToText :: AccountNumber -> Text
accountNumberToText (AccountNumber t) = t

type AccountNumberMask = Refined (SizeEqualTo 4) Text

accountNumberLastFour :: AccountNumber -> AccountNumberMask
accountNumberLastFour (AccountNumber n) =
  reallyUnsafeRefine
    . T.takeEnd 4
    $ n

accountNumberToByteString :: AccountNumber -> BS8.ByteString
accountNumberToByteString accountNumber = TE.encodeUtf8 $ accountNumberToText accountNumber

instance FromJSON AccountNumber where
  parseJSON = withText "AccountNumber" $ \t ->
    case mkAccountNumber t of
      Nothing -> fail $ "Invalid AccountNumber: " ++ T.unpack t
      Just accountNumber -> pure accountNumber

instance HasCodec AccountNumber where
  codec = dimapCodec AccountNumber unAccountNumber codec

compileAccountNumber :: QuasiQuoter
compileAccountNumber =
  QuasiQuoter
    { quoteExp = compileAccountNumber',
      quotePat = error "AccountNumber is not a pattern; use accountNumberToText instead",
      quoteDec = error "accountNumber is not supported at top-level",
      quoteType = error "accountNumber is not supported as a type"
    }
  where
    compileAccountNumber' :: String -> Q Exp
    compileAccountNumber' s = case mkAccountNumber (T.pack s) of
      Nothing -> fail ("Invalid AccountNumber: " ++ s ++ ". Must be 4-17 digits, with no other characters.")
      Just accountNumber -> [|accountNumber|]

newtype RoutingNumber = RoutingNumber {unRoutingNumber :: Text}
  deriving stock (Show, Lift)
  deriving newtype (Eq, ToJSON)

routingNumberRegex :: Regex
routingNumberRegex = [re|^[0-9]{9}$|]

mkRoutingNumber :: Text -> Maybe RoutingNumber
mkRoutingNumber t =
  if t =~ routingNumberRegex
    then Just (RoutingNumber t)
    else Nothing

routingNumberToText :: RoutingNumber -> Text
routingNumberToText (RoutingNumber t) = t

routingNumberToByteString :: RoutingNumber -> BS8.ByteString
routingNumberToByteString routingNumber = TE.encodeUtf8 $ routingNumberToText routingNumber

instance HasCodec RoutingNumber where
  codec = dimapCodec RoutingNumber unRoutingNumber codec

instance FromJSON RoutingNumber where
  parseJSON = withText "RoutingNumber" $ \t ->
    case mkRoutingNumber t of
      Nothing -> fail $ "Invalid RoutingNumber: " ++ T.unpack t
      Just routingNumber -> pure routingNumber

compileRoutingNumber :: QuasiQuoter
compileRoutingNumber =
  QuasiQuoter
    { quoteExp = compileRoutingNumber',
      quotePat = error "RoutingNumber is not a pattern; use routingNumberToText instead",
      quoteDec = error "routingNumber is not supported at top-level",
      quoteType = error "routingNumber is not supported as a type"
    }
  where
    compileRoutingNumber' :: String -> Q Exp
    compileRoutingNumber' s = case mkRoutingNumber (T.pack s) of
      Nothing -> fail ("Invalid RoutingNumber: " ++ s ++ ". Must be nine digits, with no other characters.")
      Just routingNumber -> [|routingNumber|]

newtype Iban = Iban {unIban :: Text}
  deriving stock (Show, Read, Lift)
  deriving newtype (Eq, ToJSON)

mkIban :: Text -> Maybe Iban
mkIban t = if T.length t <= 34 then Just (Iban t) else Nothing

instance FromJSON Iban where
  parseJSON = withText "Iban" $ \t -> case mkIban t of
    Nothing -> fail $ "Invalid Iban: " ++ T.unpack t
    Just iban -> pure iban

instance HasCodec Iban where
  codec = dimapCodec Iban unIban codec

compileIban :: QuasiQuoter
compileIban =
  QuasiQuoter
    { quoteExp = compileIban',
      quotePat = error "Iban is not a pattern - use `ibanToText` instead",
      quoteDec = error "Iban is not supported at top-level",
      quoteType = error "Iban is not supported as a type"
    }
  where
    compileIban' :: String -> Q Exp
    compileIban' s = case mkIban (T.pack s) of
      Nothing -> fail $ "Invalid Iban: " ++ s
      Just txt -> [|txt|]

ibanToText :: Iban -> Text
ibanToText (Iban t) = t

-- | A newtype around email text.
-- This newtype verifies the email is formatted correctly using HTML5's email regexp https:\/\/www.w3.org\/TR\/html5\/forms.html#valid-e-mail-address
newtype Email = Email {getEmailText :: Text}
  deriving stock (Show, Lift)
  deriving newtype (Eq, Ord, ToJSON)

emailRegex :: Regex
emailRegex = [re|^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$|]

-- | Attempt to create an email from text, returning 'Nothing' if it doesn't
-- match our email regular expression.
mkEmail :: Text -> Maybe Email
mkEmail t =
  if t =~ emailRegex
    then Just (Email t)
    else Nothing

-- | Convenience function for APIs that take emails as 'Text'
emailToText :: Email -> Text
emailToText = getEmailText

-- | Convenience function for APIs that take emails as 'BS8.ByteString'
emailToByteString :: Email -> BS8.ByteString
emailToByteString email = TE.encodeUtf8 $ emailToText email

instance HasCodec Email where
  codec = dimapCodec Email getEmailText codec

instance FromJSON Email where
  parseJSON (Aeson.String t) = case mkEmail t of
    Nothing -> fail $ "Invalid email address: " ++ show t
    Just email -> pure email
  parseJSON v = fail $ "When trying to parse an Email, expected String, encountered " ++ show v

-- | Create an email at compile time
-- Usage:
-- > [compileEmail|dmarticus@gmail.com|]
compileEmail :: QuasiQuoter
compileEmail =
  QuasiQuoter
    { quoteExp = compileEmail',
      quotePat = error "Email is not a pattern; use `emailToText` instead",
      quoteDec = error "email is not supported at top-level",
      quoteType = error "email is not supported as a type"
    }
  where
    compileEmail' :: String -> Q Exp
    compileEmail' s = case mkEmail (T.pack s) of
      Nothing -> fail ("Invalid Email: " ++ s ++ ". Make sure you aren't wrapping the email in quotes.")
      Just email -> [|email|]

newtype City = City
  { unCity :: Text
  }
  deriving (Eq, Show, ToJSON, FromJSON)

instance HasCodec City where
  codec = dimapCodec City unCity codec

-- TODO consider adding validation.  Risk here is that I block valid codes.
newtype PostalCode = PostalCode
  { unPostalCode :: Text
  }
  deriving (Eq, Show, ToJSON, FromJSON)

instance HasCodec PostalCode where
  codec = dimapCodec PostalCode unPostalCode codec

newtype SwiftCode = SwiftCode
  { unSwiftCode :: Text
  }
  deriving stock (Show, Lift)
  deriving newtype (Eq, Ord, ToJSON)

swiftCountryCodes :: Set.Set Text
swiftCountryCodes = Set.fromList $ "XK" : (alphaTwoUpper <$> ([minBound .. maxBound] :: [Country]))

swiftCodeRegex :: Regex
swiftCodeRegex = [re|^[A-Z]{6}[A-Z0-9]{2}(?:[A-Z0-9]{3})?$|]

mkSwiftCode :: Text -> Maybe SwiftCode
mkSwiftCode txt = do
  let t = T.toUpper $ T.strip txt
  guard $ t =~ swiftCodeRegex
  guard $ Set.member (getCountryFromSwiftCodeText t) swiftCountryCodes
  pure $ SwiftCode t

instance HasCodec SwiftCode where
  codec = dimapCodec SwiftCode unSwiftCode codec

instance FromJSON SwiftCode where
  parseJSON = withText "SwiftCode" $ \t -> case mkSwiftCode t of
    Nothing -> fail $ "Invalid SwiftCode: " ++ T.unpack t
    Just swift -> pure swift

compileSwiftCode :: QuasiQuoter
compileSwiftCode =
  QuasiQuoter
    { quoteExp = compileSwiftCode',
      quotePat = error "SwiftCode is not a pattern; use swiftCodeToText instead",
      quoteDec = error "SwiftCode is not supported at top-level",
      quoteType = error "SwiftCode is not supported as a type"
    }
  where
    compileSwiftCode' :: String -> Q Exp
    compileSwiftCode' s = case mkSwiftCode (T.pack s) of
      Nothing -> fail $ "Invalid SwiftCode: " ++ s
      Just txt -> [|txt|]

swiftCodeToText :: SwiftCode -> Text
swiftCodeToText (SwiftCode t) = t

-- | Extracts an ISO 3166-1 alpha-2 country code (or XK for Kosovo) from a SWIFT code, e.g. BOFAUS3N -> US
getCountryFromSwiftCodeText :: Text -> Text
getCountryFromSwiftCodeText = T.take 2 . T.drop 4

getCountryFromSwiftCode :: SwiftCode -> Text
getCountryFromSwiftCode = getCountryFromSwiftCodeText . swiftCodeToText

-- | SwiftCodes can be either 11 or 8 digits, this ensures you're always working with the 8 digit variety.
get8DigitSwiftCode :: SwiftCode -> SwiftCode
get8DigitSwiftCode = coerce . T.take 8 . coerce

-- | Gets the branch code from an 11-digit SwiftCode (this is the last 3 digits of an 11-digit SwiftCode), otherwise returns Nothing.
getBranchCodeFromSwiftCode :: SwiftCode -> Maybe Text
getBranchCodeFromSwiftCode swiftCode = do
  let swiftCodeText = swiftCodeToText swiftCode
  guard (T.length swiftCodeText == 11)
  pure (T.drop 8 swiftCodeText)

-- TODO consider adding validation (should district be state?  Can I just use an enum?)
-- District will be a state only if it's US or Canada... will need to be clever here I think.
-- Maybe can have those two fields be modeled with a sum type?
newtype District = District
  { unDistrict :: Text
  }
  deriving (Eq, Show)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec District)

instance HasCodec District where
  codec = dimapCodec District unDistrict codec

-- TODO: Look into making this use Country
newtype ISO3166Alpha2 = ISO3166Alpha2
  { unISO3166Alpha2 :: Text
  }
  deriving newtype (Eq, Show, Ord)
  deriving
    ( FromJSON,
      ToJSON
    )
    via (Autodocodec ISO3166Alpha2)

instance HasCodec ISO3166Alpha2 where
  codec = dimapCodec ISO3166Alpha2 unISO3166Alpha2 codec

newtype TrackingReference = TrackingReference
  { unTrackingReference :: Text
  }
  deriving (Eq, Show, ToJSON, FromJSON)

instance HasCodec TrackingReference where
  codec = dimapCodec TrackingReference unTrackingReference codec

-- TODO consider adding validation to this type if necessary.
-- The string looks like this 0xcac04f0069e4ac9314ac4e608e99278a3bebabcd
newtype HexString = HexString
  { unHexString :: Text
  }
  deriving (Eq, Show, ToJSON, FromJSON)

instance HasCodec HexString where
  codec = dimapCodec HexString unHexString codec

-- TODO consider adding validation if necessary
-- From Circle's docs: "A walletId is a numeric value but should be treated as a string as format may change in the future"
newtype WalletId = WalletId
  { unWalletId :: Text
  }
  deriving (Eq, Show, ToJSON, FromJSON)

instance HasCodec WalletId where
  codec = dimapCodec WalletId unWalletId codec

-- TODO what to do about this orphan instance?
instance HasCodec UUID where
  codec = bimapCodec f UUID.toText codec
    where
      f t =
        case UUID.fromText t of
          Nothing -> Left "Invalid Text when parsing UUID"
          Just u -> Right u