circle (empty) → 0.1.0.0
raw patch · 7 files changed
+6153/−0 lines, 7 filesdep +aesondep +autodocodecdep +basesetup-changed
Dependencies added: aeson, autodocodec, base, bytestring, case-insensitive, circle, containers, country, hspec, hspec-expectations, http-client, http-client-tls, http-types, pcre-heavy, random, refined, safe, template-haskell, text, time, unordered-containers, uuid, vector
Files
- LICENSE +22/−0
- Setup.hs +3/−0
- circle.cabal +67/−0
- src/Circle.hs +34/−0
- src/Circle/Client.hs +1000/−0
- src/Circle/Types.hs +4360/−0
- test/Tests.hs +667/−0
+ LICENSE view
@@ -0,0 +1,22 @@++The MIT License (MIT)++Copyright (c) 2022 Dylan Martin++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,3 @@+import Distribution.Simple++main = defaultMain
+ circle.cabal view
@@ -0,0 +1,67 @@+name: circle+version: 0.1.0.0+synopsis: Circle API client for Haskell+description: For usage information please consult README.md+homepage: https://github.com/dmarticus/circle#readme+license: MIT+license-file: LICENSE+author: Dylan Martin+maintainer: dmarticus@gmail.com+copyright: 2022 Dylan Martin+category: Web+build-type: Simple+cabal-version: >=1.10++library+ hs-source-dirs: src+ exposed-modules:+ Circle+ Circle.Client+ Circle.Types++ build-depends:+ aeson >=0.11.1 && <1.6+ , autodocodec >=0.2.0 && <0.3+ , base >=4.7 && <5+ , bytestring >=0.10.0 && <0.11+ , case-insensitive >=1.2.1 && <1.3+ , containers >=0.6.5 && <0.7+ , country >=0.2.1 && <0.3+ , http-client >=0.7.6 && <0.7.9+ , http-client-tls >=0.3 && <0.3.6+ , http-types >=0.8 && <0.13+ , pcre-heavy >=1.0.0 && <1.1+ , refined >=0.6 && <0.9+ , template-haskell >=2.16.0 && <2.17+ , text >=0.11 && <1.3+ , time >=1.4 && <1.11+ , unordered-containers >=0.2.5 && <0.3+ , uuid >=1.3.15 && <1.4+ , vector >=0.10.9 && <0.13++ default-language: Haskell2010+ ghc-options: -Wall -fwarn-unused-binds -fwarn-unused-imports++test-suite test+ ghc-options: -Wall -fwarn-unused-binds -fwarn-unused-imports+ type: exitcode-stdio-1.0+ main-is: Tests.hs+ hs-source-dirs: test+ build-depends:+ base >=4.7 && <5+ , circle+ , hspec >=1.8 && <2.8+ , hspec-expectations >=0.8.2 && <0.9+ , http-client >=0.7.6 && <0.7.9+ , http-client-tls >=0.3 && <0.3.6+ , random >=1.2.0 && <1.3+ , safe >=0.3 && <0.4+ , text >=0.11 && <1.3+ , time >=1.9.3 && <1.10+ , uuid >=1.3.15 && <1.4++ default-language: Haskell2010++source-repository head+ type: git+ location: https://github.com/dmarticus/circle
+ src/Circle.hs view
@@ -0,0 +1,34 @@+-------------------------------------------++-- |+-- Module : Circle+-- Copyright : (c) Dylan Martin, 2022+-- Maintainer : dmarticus@gmail.com+-- Stability : experimental+-- Portability : POSIX+--+-- < https:/\/\developers.circle.com/developer/v1/docs >+--+-- @+-- 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+-- @+module Circle+ ( module Circle.Client,+ module Circle.Types,+ )+where++import Circle.Client+import Circle.Types
+ src/Circle/Client.hs view
@@ -0,0 +1,1000 @@+-------------------------------------------+-------------------------------------------+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- |+-- Module : Circle.Client+-- Copyright : (c) Dylan Martin, 2022+-- Maintainer : dmarticus@gmail.com+-- Stability : experimental+-- Portability : POSIX+--+-- < https:/\/\developers.circle.com/developer/v1/docs >+module Circle.Client where++import Circle.Types+import Data.Aeson (eitherDecode, encode)+import Data.Aeson.Types (FromJSON)+import qualified Data.ByteString.Char8 as BS8+import qualified Data.ByteString.Lazy.Char8 as BSL+import Data.Maybe (fromJust, isNothing)+import qualified Data.Text as T+import Data.UUID+import qualified Data.UUID as UUID+import Network.HTTP.Client+ ( Manager,+ Request (method, queryString, requestBody),+ RequestBody (RequestBodyLBS),+ Response (responseBody),+ applyBearerAuth,+ httpLbs,+ newManager,+ parseRequest,+ requestHeaders,+ )+import Network.HTTP.Client.TLS (tlsManagerSettings)+import Network.HTTP.Types.Header (hAccept, hContentType)+import qualified Network.HTTP.Types.Method as NHTM++---------------------------------------------------------------+-- /businessAccount/banks/wires endpoint+---------------------------------------------------------------++-- | Create a business bank account for a wire+-- https://developers.circle.com/reference/createbusinesswireaccount+createBusinessWireAccount :: WireAccountRequestBody -> CircleAPIRequest WireAccountRequest TupleBS8 BSL.ByteString+createBusinessWireAccount wireAccountBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "businessAccount/banks/wires"+ params = case wireAccountBody of+ USBankAccount usBankAccountBody -> Params (Just $ Body (encode usBankAccountBody)) []+ IBANBankAccount ibanBankAccountBody -> Params (Just $ Body (encode ibanBankAccountBody)) []+ NonIBANBankAccount nonIBANBankAccountBody -> Params (Just $ Body (encode nonIBANBankAccountBody)) []++-- | Get a list of business account wire accounts+-- https://developers.circle.com/reference/listbusinesswireaccounts+listBusinessWireAccounts :: CircleAPIRequest WireAccountsRequest TupleBS8 BSL.ByteString+listBusinessWireAccounts = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/banks/wires"+ params = Params Nothing []++-- | Get a single business account wire account, accepts the wire account Id as a parameter+-- https://developers.circle.com/reference/getbusinesswireaccount+getBusinessWireAccount :: UUID -> CircleAPIRequest WireAccountRequest TupleBS8 BSL.ByteString+getBusinessWireAccount wireAccountId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/banks/wires/" <> UUID.toText wireAccountId+ params = Params Nothing []++-- | Get the wire transfer instructions into the Circle business bank account given your bank account id.+-- https://developers.circle.com/reference/getbusinesswireaccountinstructions+getBusinessWireAccountInstructions :: UUID -> CircleAPIRequest WireInstructionsRequest TupleBS8 BSL.ByteString+getBusinessWireAccountInstructions wireAccountId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/banks/wires/" <> UUID.toText wireAccountId <> "/instructions"+ params = Params Nothing []++---------------------------------------------------------------+-- /businessAccount/balances endpoint+---------------------------------------------------------------++-- | List all business balances+-- https://developers.circle.com/reference/listbusinesspayouts+listAllBusinessBalances :: CircleAPIRequest BalanceRequest TupleBS8 BSL.ByteString+listAllBusinessBalances = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/balances"+ params = Params Nothing []++---------------------------------------------------------------+-- /configuration endpoint+---------------------------------------------------------------++-- | Get configuration info+-- https://developers.circle.com/reference/getaccountconfig+getConfigurationInfo :: CircleAPIRequest ConfigurationRequest TupleBS8 BSL.ByteString+getConfigurationInfo = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "configuration"+ params = Params Nothing []++---------------------------------------------------------------+-- /encryption/public endpoint+---------------------------------------------------------------++-- | Get encryption info+-- https://developers.circle.com/reference/getpublickey+getPublicKey :: CircleAPIRequest EncryptionRequest TupleBS8 BSL.ByteString+getPublicKey = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "encryption/public"+ params = Params Nothing []++---------------------------------------------------------------+-- /channels endpoint+---------------------------------------------------------------++-- | List all channels+-- https://developers.circle.com/reference/listchannels+listAllChannels :: CircleAPIRequest ChannelsRequest TupleBS8 BSL.ByteString+listAllChannels = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "channels"+ params = Params Nothing []++---------------------------------------------------------------+-- /stablecoins endpoint+---------------------------------------------------------------++-- | List all stablecoins+-- https://developers.circle.com/reference/listchannels+listAllStablecoins :: CircleAPIRequest StablecoinsRequest TupleBS8 BSL.ByteString+listAllStablecoins = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "stablecoins"+ params = Params Nothing []++---------------------------------------------------------------+-- /notifications/subscriptions endpoint+---------------------------------------------------------------++-- | List all subscriptions+-- https://developers.circle.com/reference/listsubscriptions+listAllNotificationSubscriptions :: CircleAPIRequest SubscriptionsRequest TupleBS8 BSL.ByteString+listAllNotificationSubscriptions = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "notifications/subscriptions"+ params = Params Nothing []++-- | Create new subscription+-- https://developers.circle.com/reference/createsubscribtion+createSubscription :: SubscriptionRequestBody -> CircleAPIRequest SubscriptionRequest TupleBS8 BSL.ByteString+createSubscription subscriptionBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "notifications/subscriptions"+ params = Params (Just $ Body (encode subscriptionBody)) []++-- | Delete subscription+-- https://developers.circle.com/reference/deletesubscribtion+deleteSubscription :: UUID -> CircleAPIRequest SubscriptionsRequest TupleBS8 BSL.ByteString+deleteSubscription resourceId = do+ mkCircleAPIRequest NHTM.methodDelete url params+ where+ url = "notifications/subscriptions" <> UUID.toText resourceId+ params = Params Nothing []++---------------------------------------------------------------+-- /businessAccount/payouts endpoint+---------------------------------------------------------------++-- | Lists all payouts made from a given business account+-- https://developers.circle.com/reference/listbusinesspayouts+listAllBusinessAccountPayouts :: CircleAPIRequest PayoutsRequest TupleBS8 BSL.ByteString+listAllBusinessAccountPayouts = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/payouts"+ params = Params Nothing []++-- | Gets a specific payout associated with a business account+-- https://developers.circle.com/reference/getbusinesspayout+getBusinessAccountPayout :: UUID -> CircleAPIRequest PayoutRequest TupleBS8 BSL.ByteString+getBusinessAccountPayout payoutId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/payouts" <> UUID.toText payoutId+ params = Params Nothing []++-- | Creates a business account payout+-- https://developers.circle.com/reference/createbusinesspayout+createBusinessAccountPayout :: BusinessPayoutRequestBody -> CircleAPIRequest PayoutRequest TupleBS8 BSL.ByteString+createBusinessAccountPayout payoutBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "businessAccount/payouts"+ params = Params (Just $ Body (encode payoutBody)) []++---------------------------------------------------------------+-- /businessAccount/transfers endpoint+---------------------------------------------------------------++-- | Searches for transfers from your business account.+-- If the date parameters are omitted, returns the most recent transfers.+-- This endpoint returns up to 50 transfers in descending chronological order or pageSize, if provided.+-- https://developers.circle.com/reference/listbusinesstransfers+listAllBusinessAccountTransfers :: CircleAPIRequest TransfersRequest TupleBS8 BSL.ByteString+listAllBusinessAccountTransfers = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/transfers"+ params = Params Nothing []++-- | Get a business account transfer based on a transfer ID+-- https://developers.circle.com/reference/getbusinesstransfer+getBusinessAccountTransfer :: UUID -> CircleAPIRequest TransferRequest TupleBS8 BSL.ByteString+getBusinessAccountTransfer transferId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/transfers/" <> T.pack (show transferId)+ params = Params Nothing []++-- | Create a new transfer+-- https://developers.circle.com/reference/createbusinesstransfer+createBusinessAccountTransfer :: BusinessTransferRequestBody -> CircleAPIRequest TransferRequest TupleBS8 BSL.ByteString+createBusinessAccountTransfer transferBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "businessAccount/transfers"+ params = Params (Just $ Body (encode transferBody)) []++---------------------------------------------------------------+-- /businessAccount/wallets/addresses endpoint+---------------------------------------------------------------++-- | List all deposit addresses+-- https://developers.circle.com/developer/v1/reference/getbusinessdepositaddress+listAllBusinessAccountDepositAddresses :: CircleAPIRequest DepositAddressesRequest TupleBS8 BSL.ByteString+listAllBusinessAccountDepositAddresses = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/wallets/addresses/deposit"+ params = Params Nothing []++-- | Create new business account deposit address+-- Generates a new blockchain address for a wallet for a given currency/chain pair.+-- Circle may reuse addresses on blockchains that support reuse.+-- For example, if you're requesting two addresses for depositing USD and ETH, both on Ethereum,+-- you may see the same Ethereum address returned.+-- Depositing cryptocurrency to a generated address will credit the associated wallet with the value of the deposit.+-- https://developers.circle.com/developer/v1/reference/createbusinessdepositaddress+createBusinessAccountDepositAddress :: DepositAddressRequestBody -> CircleAPIRequest DepositAddressRequest TupleBS8 BSL.ByteString+createBusinessAccountDepositAddress depositAddressBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "businessAccount/wallets/addresses/deposit"+ params = Params (Just $ Body (encode depositAddressBody)) []++-- | List all recipient addresses+-- Returns a list of recipient addresses that have each been verified and are eligible for transfers.+-- Any recipient addresses pending verification are not included in the response.+-- https://developers.circle.com/developer/v1/reference/listbusinessrecipientaddresses+listAllBusinessAccountRecipientAddresses :: CircleAPIRequest RecipientAddressesRequest TupleBS8 BSL.ByteString+listAllBusinessAccountRecipientAddresses = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/wallets/addresses/recipient"+ params = Params Nothing []++-- | Create a new recipient address+-- Stores an external blockchain address. Once added, the recipient address must be verified to ensure that you know and trust each new address.+-- https://developers.circle.com/developer/v1/reference/createbusinessrecipientaddress+createBusinessAccountRecipientAddress :: RecipientAddressRequestBody -> CircleAPIRequest RecipientAddressRequest TupleBS8 BSL.ByteString+createBusinessAccountRecipientAddress recipientAddressBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "businessAccount/wallets/addresses/recipient"+ params = Params (Just $ Body (encode recipientAddressBody)) []++---------------------------------------------------------------+-- /businessAccount/deposits Endpoint+---------------------------------------------------------------++-- | List all deposits+-- Searches for deposits sent to your business account. If the date parameters are omitted, returns the most recent deposits.+-- This endpoint returns up to 50 deposits in descending chronological order or pageSize, if provided.+-- https://developers.circle.com/developer/v1/reference/listbusinessdeposits+listAllBusinessAccountDeposits :: CircleAPIRequest DepositsRequest TupleBS8 BSL.ByteString+listAllBusinessAccountDeposits = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/deposits"+ params = Params Nothing []++---------------------------------------------------------------+-- /businessAccount/banks/signet endpoint (Production-Only)+---------------------------------------------------------------++-- TODO how to test these if no sandbox instance?++-- | Create a signet bank account+-- https://developers.circle.com/developer/v1/reference/createbusinesssignetaccount+createSignetBankAccount :: SignetBankAccountRequestBody -> CircleAPIRequest SignetBankAccountRequest TupleBS8 BSL.ByteString+createSignetBankAccount signetBankAccountBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "businessAccount/banks/signet"+ params = Params (Just $ Body (encode signetBankAccountBody)) []++-- | Get a list of Signet accounts+-- https://developers.circle.com/developer/v1/reference/listbusinesssignetaccounts+listSignetAccounts :: CircleAPIRequest SignetBankAccountsRequest TupleBS8 BSL.ByteString+listSignetAccounts = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/banks/signet"+ params = Params Nothing []++-- | Get a single Signet bank account, accepts the Signet bank account Id as a parameter+-- https://developers.circle.com/developer/v1/reference/getbusinesssignetaccount+getSignetAccount :: UUID -> CircleAPIRequest SignetBankAccountRequestBody TupleBS8 BSL.ByteString+getSignetAccount signetBankAccountId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/banks/signet/" <> UUID.toText signetBankAccountId+ params = Params Nothing []++-- | Get the Signet transfer instructions into the Circle bank account given your bank account id (only available on Production now).+-- https://developers.circle.com/developer/v1/reference/getbusinesssignetaccountinstructions+getSignetAccountInstructions :: UUID -> CircleAPIRequest SignetBankInstructionsResponseData TupleBS8 BSL.ByteString+getSignetAccountInstructions signetBankAccountId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/banks/signet/" <> UUID.toText signetBankAccountId <> "/instructions"+ params = Params Nothing []++---------------------------------------------------------------+-- /businessAccount/banks/sen endpoint (Silvergate SEN operations)+---------------------------------------------------------------++-- | Create a bank account for a SEN+-- https://developers.circle.com/developer/v1/reference/createbusinesssenaccount+createSENAccount :: SENAccountRequestBody -> CircleAPIRequest SENAccountRequest TupleBS8 BSL.ByteString+createSENAccount senAccountBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "businessAccount/banks/sen"+ params = Params (Just $ Body (encode senAccountBody)) []++-- | Get a list of SEN accounts+-- https://developers.circle.com/developer/v1/reference/listbusinesssenaccounts+listSENAccounts :: CircleAPIRequest SENAccountsRequest TupleBS8 BSL.ByteString+listSENAccounts = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/banks/sen"+ params = Params Nothing []++-- | Get a single SEN account, accepts the SEN account Id as a parameter+-- https://developers.circle.com/developer/v1/reference/getbusinesssenaccount+getSENAccount :: UUID -> CircleAPIRequest SENAccountRequest TupleBS8 BSL.ByteString+getSENAccount senAccountId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/banks/sen/" <> UUID.toText senAccountId+ params = Params Nothing []++-- | Get the SEN transfer instructions into the Circle bank account given your bank account id.+-- https://developers.circle.com/developer/v1/reference/getbusinesssenaccountinstructions+getSENAccountInstructions :: UUID -> CircleAPIRequest SENInstructionsRequest TupleBS8 BSL.ByteString+getSENAccountInstructions senAccountId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "businessAccount/banks/sen/" <> UUID.toText senAccountId <> "/instructions"+ params = Params Nothing []++---------------------------------------------------------------+-- /payments endpoint+---------------------------------------------------------------++-- | List all payments+-- https://developers.circle.com/developer/v1/reference/listpayments+listAllPayments :: CircleAPIRequest PaymentsRequest TupleBS8 BSL.ByteString+listAllPayments = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "payments"+ params = Params Nothing []++-- | Create a payment (fiat or Crypto)+-- https://developers.circle.com/developer/v1/reference/payments-payments-create+createPayment :: CreatePaymentRequestBody -> CircleAPIRequest PaymentRequest TupleBS8 BSL.ByteString+createPayment createPaymentBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "payments"+ params = Params (Just $ Body (encode createPaymentBody)) []++-- | Get a payment (fiat or Crypto)+-- https://developers.circle.com/developer/v1/reference/payments-payments-get-id+getPayment :: UUID -> CircleAPIRequest PaymentRequest TupleBS8 BSL.ByteString+getPayment paymentId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "payments/" <> UUID.toText paymentId+ params = Params Nothing []++-- | Cancel a fiat payment+-- https://developers.circle.com/developer/v1/reference/payments-payments-cancel-id+cancelPayment :: UUID -> CancelPaymentRequestBody -> CircleAPIRequest PaymentRequest TupleBS8 BSL.ByteString+cancelPayment paymentId cancelPaymentBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "payments/" <> UUID.toText paymentId <> "/cancel"+ params = Params (Just $ Body (encode cancelPaymentBody)) []++-- | Refund a fiat payment+-- https://developers.circle.com/developer/v1/reference/payments-payments-refund-id+refundPayment :: UUID -> RefundPaymentRequestBody -> CircleAPIRequest PaymentRequest TupleBS8 BSL.ByteString+refundPayment paymentId refundPaymentBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "payments/" <> UUID.toText paymentId <> "/refund"+ params = Params (Just $ Body (encode refundPaymentBody)) []++-- TODO add capture payment method?++---------------------------------------------------------------+-- /mock/payments endpoints+---------------------------------------------------------------++-- TODO constrain these method to run in the sandbox only. Would be cool to do the same thing with the Production only methods++-- | Create mock wire payment SANDBOX ONLY+-- In the sandbox environment, initiate a mock wire payment that mimics the behavior of funds sent through the bank (wire) account linked to master wallet.+-- https://developers.circle.com/developer/v1/reference/createmockwirepayment+createMockWirePayment :: MockSenOrWirePaymentRequestBody -> CircleAPIRequest MockPaymentRequest TupleBS8 BSL.ByteString+createMockWirePayment wireBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "mocks/payments/wire"+ params = Params (Just $ Body (encode wireBody)) []++-- | Create mock SEPA payment SANDBOX ONLY (in Beta)+-- In the sandbox environment, initiate a mock SEPA payment that mimics the behavior of funds sent through the bank (SEPA) account linked to master wallet.+-- https://developers.circle.com/developer/v1/reference/createmocksepapayment+createMockSEPAPayment :: MockSEPAPaymentRequestBody -> CircleAPIRequest MockPaymentRequest TupleBS8 BSL.ByteString+createMockSEPAPayment sepaBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "mocks/payments/sepa"+ params = Params (Just $ Body (encode sepaBody)) []++-- | Create mock Silvergate payment SANDBOX ONLY+-- In the sandbox environment, initiate a mock SEN transfer that mimics the behavior of funds sent through the Silvergate SEN account linked to master wallet.+-- https://developers.circle.com/developer/v1/reference/createmocksenpayment+createMockSilvergatePayment :: MockSenOrWirePaymentRequestBody -> CircleAPIRequest MockPaymentRequest TupleBS8 BSL.ByteString+createMockSilvergatePayment senBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "mocks/payments/sen"+ params = Params (Just $ Body (encode senBody)) []++---------------------------------------------------------------+-- /mock/ach endpoint+---------------------------------------------------------------++-- | Create mock ACH account SANDBOX ONLY+-- In the sandbox environment, create a mock ACH account and retrieve a processor token that can be used to link an ACH account.+-- https://developers.circle.com/developer/v1/reference/createmockachaccount-1+createMockACHBankAccount :: CreateMockACHBankAccountRequestBody -> CircleAPIRequest MockAccountRequest TupleBS8 BSL.ByteString+createMockACHBankAccount achPaymentBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "mocks/ach/account"+ params = Params (Just $ Body (encode achPaymentBody)) []++---------------------------------------------------------------+-- /mock/cards/chargebacks endpoint+---------------------------------------------------------------++-- | Create mock chargeback SANDBOX ONLY+-- In the sandbox environment, initiate a mock chargeback of a specified payment.+-- The entire payment will be charged back for its full value. The payment must be in the paid state+-- (otherwise the endpoint will return a 404), and each payment can only be charged back once+-- (otherwise the endpoint will return a 409). This endpoint is only available in the sandbox environment.+-- https://developers.circle.com/developer/v1/reference/payments-chargebacks-mock-create+createMockChargeback :: UUID -> CircleAPIRequest MockChargebackRequest TupleBS8 BSL.ByteString+createMockChargeback paymentId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "mocks/cards/chargebacks"+ params = Params (Just $ Body (encode paymentId)) []++---------------------------------------------------------------+-- /transfers endpoint (On-chain transfers)+---------------------------------------------------------------++-- | Searches for transfers.+-- Searches for transfers involving the provided wallets. If no wallet ids+-- are provided, searches all wallets associated with your Circle API+-- account. If the date parameters are omitted, returns the most recent+-- transfers. This endpoint returns up to 50 transfers in descending+-- chronological order or pageSize, if provided.+-- https://developers.circle.com/developer/v1/reference/listtransfers-1+listAllOnChainTransfers :: CircleAPIRequest OnChainTransfersRequest TupleBS8 BSL.ByteString+listAllOnChainTransfers = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "transfers"+ params = Params Nothing []++-- | Get a business account transfer based on a transfer ID+-- https://developers.circle.com/developer/v1/reference/gettransfer+getOnChainTransfer :: UUID -> CircleAPIRequest TransferRequest TupleBS8 BSL.ByteString+getOnChainTransfer transferId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "transfers/" <> UUID.toText transferId+ params = Params Nothing []++-- | Create an on-chain transfer (i.e. a crypto payment)+-- https://developers.circle.com/developer/v1/reference/accounts-transfers-create+createOnChainTransfer :: OnChainTransferRequestBody -> CircleAPIRequest TransferRequest TupleBS8 BSL.ByteString+createOnChainTransfer onChainTransferBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "transfers"+ params = Params (Just $ Body (encode onChainTransferBody)) []++---------------------------------------------------------------+-- /cards endpoint+---------------------------------------------------------------++-- | List all cards+-- https://developers.circle.com/developer/v1/reference/listcards+listAllCards :: CircleAPIRequest CardsRequest TupleBS8 BSL.ByteString+listAllCards = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "cards"+ params = Params Nothing []++-- | Get a card+-- https://developers.circle.com/developer/v1/reference/payments-cards-get-id+getCard :: UUID -> CircleAPIRequest CardRequest TupleBS8 BSL.ByteString+getCard cardId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "cards/" <> UUID.toText cardId+ params = Params Nothing []++-- | Create a card+-- https://developers.circle.com/developer/v1/reference/payments-cards-create+createCard :: CreateCardRequestBody -> CircleAPIRequest CardRequest TupleBS8 BSL.ByteString+createCard createCardBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "cards"+ params = Params (Just $ Body (encode createCardBody)) []++-- | Update a card+-- https://developers.circle.com/developer/v1/reference/updatecard+updateCard :: UUID -> UpdateCardRequestBody -> CircleAPIRequest CardRequest TupleBS8 BSL.ByteString+updateCard cardId updateCardBody = do+ mkCircleAPIRequest NHTM.methodPut url params+ where+ url = "cards/" <> UUID.toText cardId+ params = Params (Just $ Body (encode updateCardBody)) []++---------------------------------------------------------------+-- banks/wires endpoint+---------------------------------------------------------------++-- | Create a bank account for a wire+-- https://developers.circle.com/developer/v1/reference/createwireaccount+createWireAccount :: WireAccountRequestBody -> CircleAPIRequest WireAccountRequest TupleBS8 BSL.ByteString+createWireAccount wireAccountBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "banks/wires"+ params = case wireAccountBody of+ USBankAccount usBankAccountBody -> Params (Just $ Body (encode usBankAccountBody)) []+ IBANBankAccount ibanBankAccountBody -> Params (Just $ Body (encode ibanBankAccountBody)) []+ NonIBANBankAccount nonIBANBankAccountBody -> Params (Just $ Body (encode nonIBANBankAccountBody)) []++-- | Get a single wire account, accepts the wire account Id as a parameter+-- https://developers.circle.com/developer/v1/reference/getwireaccount-1+getWireAccount :: UUID -> CircleAPIRequest WireAccountRequest TupleBS8 BSL.ByteString+getWireAccount wireAccountId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "banks/wires/" <> UUID.toText wireAccountId+ params = Params Nothing []++-- | Get the wire transfer instructions into the Circle bank account given your bank account id.+-- https://developers.circle.com/developer/v1/reference/getwireaccountinstructions+getWireAccountInstructions :: UUID -> CircleAPIRequest WireInstructionsRequest TupleBS8 BSL.ByteString+getWireAccountInstructions wireAccountId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "banks/wires/" <> UUID.toText wireAccountId <> "/instructions"+ params = Params Nothing []++---------------------------------------------------------------+-- /banks/ach endpoint+---------------------------------------------------------------++-- NB: Use createMockACHBankAccount in the sandbox environment create a mock ACH account and retrieve a processor token that can be used to link an ACH account.++-- | Create an ACH account+-- https://developers.circle.com/developer/v1/reference/payments-bank-accounts-ach-mock+createACHAccount :: CreateACHBankAccountRequestBody -> CircleAPIRequest ACHBankAccountRequest TupleBS8 BSL.ByteString+createACHAccount achAccountBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "banks/ach"+ params = Params (Just $ Body (encode achAccountBody)) []++-- | Get an ACH account+-- https://developers.circle.com/developer/v1/reference/getachaccount-1+getACHAccount :: UUID -> CircleAPIRequest ACHBankAccountRequest TupleBS8 BSL.ByteString+getACHAccount achAccountId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "banks/ach/" <> UUID.toText achAccountId+ params = Params Nothing []++---------------------------------------------------------------+-- /banks/sepa endpoint+---------------------------------------------------------------++-- TODO some way to mark this stuff as being in beta? Is it even worth it?++-- | Create a SEPA account (in beta)+-- https://developers.circle.com/developer/v1/reference/createsepaaccount-1+createSEPAAccount :: SEPAAccountRequestBody -> CircleAPIRequest SEPAAccountRequest TupleBS8 BSL.ByteString+createSEPAAccount sepaAccountBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "banks/sepa"+ params = Params (Just $ Body (encode sepaAccountBody)) []++-- | Get a SEPA account (in beta)+-- https://developers.circle.com/developer/v1/reference/getsepaaccount-1+getSEPAAccount :: UUID -> CircleAPIRequest SEPAAccountRequest TupleBS8 BSL.ByteString+getSEPAAccount sepaAccountId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "banks/sepa/" <> UUID.toText sepaAccountId+ params = Params Nothing []++-- | Get instructions for a SEPA transfer (in beta)+-- https://developers.circle.com/developer/v1/reference/getsepaaccountinstructions+getSEPAAccountInstructions :: UUID -> CircleAPIRequest SEPAInstructionsRequest TupleBS8 BSL.ByteString+getSEPAAccountInstructions sepaAccountId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "banks/sepa/" <> UUID.toText sepaAccountId <> "/instructions"+ params = Params Nothing []++---------------------------------------------------------------+-- /settlements endpoint+---------------------------------------------------------------++-- | List all settlements+-- https://developers.circle.com/developer/v1/reference/listsettlements+listAllSettlements :: CircleAPIRequest SettlementsRequest TupleBS8 BSL.ByteString+listAllSettlements = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "settlements"+ params = Params Nothing []++-- | Get a settlement+-- https://developers.circle.com/developer/v1/reference/payments-settlements-get-id+getSettlement :: UUID -> CircleAPIRequest SettlementRequest TupleBS8 BSL.ByteString+getSettlement settlementId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "settlements/" <> UUID.toText settlementId+ params = Params Nothing []++---------------------------------------------------------------+-- /chargebacks endpoint+---------------------------------------------------------------++-- | List all chargebacks+-- https://developers.circle.com/developer/v1/reference/listchargebacks+listAllChargebacks :: CircleAPIRequest ChargebacksRequest TupleBS8 BSL.ByteString+listAllChargebacks = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "chargebacks"+ params = Params Nothing []++-- | Get a chargeback+-- https://developers.circle.com/developer/v1/reference/payments-chargebacks-get-id+getChargeback :: UUID -> CircleAPIRequest ChargebackRequest TupleBS8 BSL.ByteString+getChargeback chargebackId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "chargebacks/" <> UUID.toText chargebackId+ params = Params Nothing []++---------------------------------------------------------------+-- /reversals endpoint+---------------------------------------------------------------++-- | Retrieve a list of ACH payment reversals. Results will be sorted by create date descending; more recent reversals will be at the beginning of the list+-- https://developers.circle.com/developer/v1/reference/listreversals+listAllACHReversals :: CircleAPIRequest ReversalsRequest TupleBS8 BSL.ByteString+listAllACHReversals = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "reversals"+ params = Params Nothing []++---------------------------------------------------------------+-- /balances endpoint+---------------------------------------------------------------++-- | Retrieves the balance of merchant funds that have settled and also of funds that have been sent for processing but have not yet settled.+-- https://developers.circle.com/developer/v1/reference/listbalances+listAllBalances :: CircleAPIRequest BalanceRequest TupleBS8 BSL.ByteString+listAllBalances = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "balances"+ params = Params Nothing []++---------------------------------------------------------------+-- /paymentIntents endpoint+---------------------------------------------------------------++-- | List all payment intents+-- https://developers.circle.com/developer/v1/reference/listpaymentintents+listAllPaymentIntents :: CircleAPIRequest PaymentIntentsRequest TupleBS8 BSL.ByteString+listAllPaymentIntents = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "paymentIntents"+ params = Params Nothing []++-- | Create a payment intent+-- https://developers.circle.com/developer/v1/reference/createpaymentintent+createPaymentIntent :: CreatePaymentIntentRequestBody -> CircleAPIRequest PaymentIntentRequest TupleBS8 BSL.ByteString+createPaymentIntent createPaymentBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "paymentIntents"+ params = Params (Just $ Body (encode createPaymentBody)) []++-- | Get a payment intent+-- https://developers.circle.com/developer/v1/reference/getpaymentintent+getPaymentIntent :: UUID -> CircleAPIRequest PaymentIntentRequest TupleBS8 BSL.ByteString+getPaymentIntent paymentIntentId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "paymentIntents/" <> UUID.toText paymentIntentId+ params = Params Nothing []++-- | Expire a payment intent+-- https://developers.circle.com/developer/v1/reference/expirepaymentintent+expirePaymentIntent :: UUID -> CircleAPIRequest PaymentIntentRequest TupleBS8 BSL.ByteString+expirePaymentIntent paymentIntentId = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "paymentIntents/" <> UUID.toText paymentIntentId <> "/expire"+ params = Params Nothing []++---------------------------------------------------------------+-- /payouts endpoint+---------------------------------------------------------------++-- | Lists all payouts made from a given account+-- https://developers.circle.com/developer/v1/reference/listpayouts+listAllPayouts :: CircleAPIRequest PayoutsRequest TupleBS8 BSL.ByteString+listAllPayouts = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "payouts"+ params = Params Nothing []++-- | Gets a specific payout based on an ID+-- https://developers.circle.com/developer/v1/reference/payouts-payouts-get-id+getPayout :: UUID -> CircleAPIRequest PayoutRequest TupleBS8 BSL.ByteString+getPayout payoutId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "payouts" <> UUID.toText payoutId+ params = Params Nothing []++-- | Creates a payout+-- https://developers.circle.com/developer/v1/reference/payouts-payouts-create+createPayout :: PayoutRequestBody -> CircleAPIRequest PayoutRequest TupleBS8 BSL.ByteString+createPayout payoutBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "payouts"+ params = Params (Just $ Body (encode payoutBody)) []++---------------------------------------------------------------+-- /transfers endpoint (On-chain transfers)+---------------------------------------------------------------++-- | Searches for transfers from your account.+-- If the date parameters are omitted, returns the most recent transfers.+-- This endpoint returns up to 50 transfers in descending chronological order or pageSize, if provided.+-- https://developers.circle.com/developer/v1/reference/listtransfers+listAllTransfers :: CircleAPIRequest TransfersRequest TupleBS8 BSL.ByteString+listAllTransfers = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "transfers"+ params = Params Nothing []++-- | Get a transfer based on a transfer ID+-- https://developers.circle.com/reference/getbusinesstransfer+getTransfer :: UUID -> CircleAPIRequest TransferRequest TupleBS8 BSL.ByteString+getTransfer transferId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "transfers/" <> UUID.toText transferId+ params = Params Nothing []++-- | Create a new transfer+-- https://developers.circle.com/developer/v1/reference/payouts-transfers-create+createTransfer :: TransferRequestBody -> CircleAPIRequest TransferRequest TupleBS8 BSL.ByteString+createTransfer transferBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "transfers"+ params = Params (Just $ Body (encode transferBody)) []++---------------------------------------------------------------+-- /returns endpoint+---------------------------------------------------------------++-- | Retrieve a list of Wire and ACH payout returns. Results will be sorted by create date descending;+-- more recent returns will be at the beginning of the list.+-- https://developers.circle.com/developer/v1/reference/listreturns+listAllReturns :: CircleAPIRequest ReturnsRequest TupleBS8 BSL.ByteString+listAllReturns = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "returns"+ params = Params Nothing []++---------------------------------------------------------------+-- /wallets endpoint+---------------------------------------------------------------++-- | Retrieves a list of a user's wallets.+-- https://developers.circle.com/developer/v1/reference/listwallets+listAllWallets :: CircleAPIRequest WalletsRequest TupleBS8 BSL.ByteString+listAllWallets = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "wallets"+ params = Params Nothing []++-- | Get a wallet+-- https://developers.circle.com/developer/v1/reference/accounts-wallets-get-id+getWallet :: UUID -> CircleAPIRequest WalletRequest TupleBS8 BSL.ByteString+getWallet walletId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "wallets/" <> UUID.toText walletId+ params = Params Nothing []++-- | Creates an end user wallet.+-- https://developers.circle.com/developer/v1/reference/accounts-wallets-create+createWallet :: CreateWalletRequestBody -> CircleAPIRequest WalletRequest TupleBS8 BSL.ByteString+createWallet walletBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "wallets"+ params = Params (Just $ Body (encode walletBody)) []++-- | Create new blockchain deposit address+-- Generates a new blockchain address for a wallet for a given currency/chain pair.+-- Circle may reuse addresses on blockchains that support reuse.+-- For example, if you're requesting two addresses for depositing USD and ETH, both on Ethereum,+-- you may see the same Ethereum address returned.+-- Depositing cryptocurrency to a generated address will credit the associated wallet with the value of the deposit.+-- https://developers.circle.com/developer/v1/reference/payments-on-chain-addresses-create+createDepositAddress :: UUID -> DepositAddressRequestBody -> CircleAPIRequest DepositAddressRequest TupleBS8 BSL.ByteString+createDepositAddress walletId depositAddressBody = do+ mkCircleAPIRequest NHTM.methodPost url params+ where+ url = "wallets/" <> UUID.toText walletId <> "/addresses"+ params = Params (Just $ Body (encode depositAddressBody)) []++-- | List all recipient addresses associated with a wallet Id+-- Retrieves a list of addresses associated with a wallet.+-- https://developers.circle.com/developer/v1/reference/listaddresses+listAllAddresses :: UUID -> CircleAPIRequest RecipientAddressesRequest TupleBS8 BSL.ByteString+listAllAddresses walletId = do+ mkCircleAPIRequest NHTM.methodGet url params+ where+ url = "wallets/" <> UUID.toText walletId <> "/addresses"+ params = Params Nothing []++---------------------------------------------------------------+-- Utility methods for calling Circle's API+---------------------------------------------------------------++-- | Create a request to `circle`'s API+circle ::+ (FromJSON (CircleRequest a)) =>+ CircleConfig ->+ CircleAPIRequest a TupleBS8 BSL.ByteString ->+ IO (Either CircleError (CircleRequest a))+circle config request = do+ response <- circle' config request+ let result = eitherDecode $ responseBody response+ case result of+ Left s -> return (Left (CircleError (T.pack s) response))+ Right r -> return (Right r)+ where+ circle' ::+ CircleConfig ->+ CircleAPIRequest a TupleBS8 BSL.ByteString ->+ IO Reply+ circle' CircleConfig {..} CircleAPIRequest {..} = do+ manager <- newManager tlsManagerSettings+ initReq <- parseRequest $ T.unpack $ T.append (hostUri host) endpoint+ let reqBody+ | rMethod == NHTM.methodGet = mempty+ | isNothing (paramsBody params) = mempty+ | otherwise = unBody $ fromJust $ paramsBody params+ req =+ initReq+ { method = rMethod,+ requestBody = RequestBodyLBS reqBody,+ requestHeaders = [(hContentType, "application/json"), (hAccept, "application/json")],+ queryString = paramsToByteString $ paramsQuery params+ }+ circleToken = unApiToken token+ authorizedRequest = applyBearerAuth circleToken req+ httpLbs authorizedRequest manager++-- | This function is only used internally to speed up the test suite.+-- Instead of creating a new Manager we reuse the same one.+circleTest ::+ (FromJSON (CircleRequest a)) =>+ CircleConfig ->+ Manager ->+ CircleAPIRequest a TupleBS8 BSL.ByteString ->+ IO (Either CircleError (CircleRequest a))+circleTest config tlsManager request = do+ response <- circleTest' config request tlsManager+ let result = eitherDecode $ responseBody response+ case result of+ Left s -> return (Left (CircleError (T.pack s) response))+ Right r -> return (Right r)+ where+ circleTest' ::+ CircleConfig ->+ CircleAPIRequest a TupleBS8 BSL.ByteString ->+ Manager ->+ IO Reply+ circleTest' CircleConfig {..} CircleAPIRequest {..} manager = do+ initReq <- parseRequest $ T.unpack $ T.append (hostUri host) endpoint+ let reqBody+ | rMethod == NHTM.methodGet = mempty+ | isNothing (paramsBody params) = mempty+ | otherwise = unBody $ fromJust $ paramsBody params+ req =+ initReq+ { method = rMethod,+ requestBody = RequestBodyLBS reqBody,+ requestHeaders = [(hContentType, "application/json"), (hAccept, "application/json")],+ queryString = paramsToByteString $ paramsQuery params+ }+ circleToken = unApiToken token+ authorizedRequest = applyBearerAuth circleToken req+ httpLbs authorizedRequest manager++-- | Conversion of a key value pair to a query parameterized string+paramsToByteString ::+ [Query] ->+ BS8.ByteString+paramsToByteString [] = mempty+paramsToByteString [x] = fst (unQuery x) <> "=" <> snd (unQuery x)+paramsToByteString (x : xs) =+ mconcat [fst $ unQuery x, "=", snd $ unQuery x, "&"] <> paramsToByteString xs
+ src/Circle/Types.hs view
@@ -0,0 +1,4360 @@+-------------------------------------------+-------------------------------------------+{-# 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
+ test/Tests.hs view
@@ -0,0 +1,667 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}++module Main where++import Circle.Client+import Circle.Types+import Control.Monad+import Control.Monad.IO.Class (liftIO)+import Data.Foldable (for_)+import Data.Maybe (fromJust)+import Data.Text as T+import Data.UUID as UUID+import Data.UUID.V4 as UUID+import Network.HTTP.Client (newManager)+import Network.HTTP.Client.TLS (tlsManagerSettings)+import System.Random+import Test.Hspec+import Test.Hspec.Expectations.Contrib (isRight)++---------------------------------------------------------------+-- Tests+---------------------------------------------------------------++main :: IO ()+main = do+ manager <- newManager tlsManagerSettings+ config <- sandboxEnvConfig Nothing+ hspec $+ parallel $ do+ describe "/configuration endpoint" $ do+ describe "management" $ do+ it "gets configuration info" $ do+ configurationInfo <- circleTest config manager getConfigurationInfo+ configurationInfo `shouldSatisfy` isRight+ describe "/encryption/public" $ do+ it "gets public key info" $ do+ keyInfo <- circleTest config manager getPublicKey+ keyInfo `shouldSatisfy` isRight+ describe "/channels" $ do+ it "lists all channels" $ do+ channels <- circleTest config manager listAllChannels+ channels `shouldSatisfy` isRight+ describe "/stablecoins" $ do+ it "lists all stablecoins" $ do+ stablecoins <- circleTest config manager listAllStablecoins+ stablecoins `shouldSatisfy` isRight+ -- TODO: Skip the subscription tests because I I accidentally hit my subscription limit for the sandbox account and can't add new ones+ -- However, I can't delete the subscriptions either, because they're still in the 'pending' state.+ -- At least this code worked and the tests proved it before it failed!+ xdescribe "/notifications/subscriptions" $ do+ it "creates a new subscription" $ do+ subscription <- circleTest config manager $ createSubscription testSubscriptionBody+ subscription `shouldSatisfy` isRight+ it "deletes a subscription" $ do+ randomSubscriptionId <- liftIO $ UUID.nextRandom+ deletionResponse <- circleTest config manager $ deleteSubscription randomSubscriptionId+ deletionResponse `shouldSatisfy` isRight+ it "lists all subscription" $ do+ subscriptions <- circleTest config manager listAllNotificationSubscriptions+ subscriptions `shouldSatisfy` isRight+ describe "/businessAccount/banks/wires endpoint" $ do+ describe "create business wire account" $ do+ it "creates a new business wire account" $ do+ wireAccountIdempotencyKey <- liftIO $ UUID.nextRandom+ newWireAccount <- circleTest config manager $ createBusinessWireAccount (testUSWireAccountDetails wireAccountIdempotencyKey)+ newWireAccount `shouldSatisfy` isRight+ describe "list business wire accounts" $ do+ it "gets a list of business wire accounts" $ do+ wireAccounts <- circleTest config manager listBusinessWireAccounts+ wireAccounts `shouldSatisfy` isRight+ describe "get business wire account" $ do+ it "gets a single business wire account" $ do+ wireAccountIdempotencyKey <- liftIO $ UUID.nextRandom+ wireAccount1 <- circleTest config manager $ createBusinessWireAccount (testUSWireAccountDetails wireAccountIdempotencyKey)+ wireAccount1 `shouldSatisfy` isRight+ let Right CircleResponseBody {circleResponseData} = wireAccount1+ for_ circleResponseData $ \WireAccountResponseBody {..} -> do+ wireAccount <- circleTest config manager $ getBusinessWireAccount wireAccountResponseBodyId+ wireAccount `shouldSatisfy` isRight+ it "gets wire instructions for a business wire account" $ do+ wireAccountIdempotencyKey <- liftIO $ UUID.nextRandom+ wireAccount2 <- circleTest config manager $ createBusinessWireAccount (testUSWireAccountDetails wireAccountIdempotencyKey)+ wireAccount2 `shouldSatisfy` isRight+ let Right CircleResponseBody {circleResponseData} = wireAccount2+ for_ circleResponseData $ \WireAccountResponseBody {..} -> do+ wireAccountInstructions <- circleTest config manager $ getBusinessWireAccountInstructions wireAccountResponseBodyId+ wireAccountInstructions `shouldSatisfy` isRight+ describe "/businessAccount/balances endpoint" $ do+ -- TODO: could be interesting to actually seed balances, I'll do that when I wrap that API endpoint+ describe "list business account balances balances" $ do+ it "should list all balances for all business wire accounts" $ do+ businessBalances <- circleTest config manager listAllBusinessBalances+ businessBalances `shouldSatisfy` isRight+ describe "/businessAccount/banks/sen endpoint" $ do+ describe "create SEN account" $ do+ it "creates a new SEN account" $ do+ senAccountIdempotencyKey <- liftIO $ UUID.nextRandom+ newSENAccount <- circleTest config manager $ createSENAccount (testSENAccountDetails senAccountIdempotencyKey)+ newSENAccount `shouldSatisfy` isRight+ describe "list SEN accounts" $ do+ it "gets a list of SEN accounts" $ do+ senAccounts <- circleTest config manager listSENAccounts+ senAccounts `shouldSatisfy` isRight+ describe "get SEN account" $ do+ it "gets a single SEN account" $ do+ senAccountIdempotencyKey <- liftIO $ UUID.nextRandom+ senAccount1 <- circleTest config manager $ createSENAccount (testSENAccountDetails senAccountIdempotencyKey)+ senAccount1 `shouldSatisfy` isRight+ let Right CircleResponseBody {circleResponseData} = senAccount1+ for_ circleResponseData $ \SENAccountResponseBody {..} -> do+ senAccount <- circleTest config manager $ getSENAccount senAccountResponseBodyId+ senAccount `shouldSatisfy` isRight+ it "gets instructions for a SEN account" $ do+ senAccountIdempotencyKey <- liftIO $ UUID.nextRandom+ senAccount2 <- circleTest config manager $ createSENAccount (testSENAccountDetails senAccountIdempotencyKey)+ senAccount2 `shouldSatisfy` isRight+ let Right CircleResponseBody {circleResponseData} = senAccount2+ for_ circleResponseData $ \SENAccountResponseBody {..} -> do+ senAccountInstructions <- circleTest config manager $ getSENAccountInstructions senAccountResponseBodyId+ senAccountInstructions `shouldSatisfy` isRight+ describe "/balances endpoint" $ do+ -- TODO: need to actually seed balances, I'll do that when I wrap that API endpoint+ describe "list balances" $ do+ it "should list all balances for the newly-created wire account" $ do+ balances <- circleTest config manager listAllBalances+ balances `shouldSatisfy` isRight+ describe "/businessAccount/transfers endpoint" $ do+ describe "list transfers" $ do+ it "should list all transfers for a given business account" $ do+ transfers <- circleTest config manager listAllBusinessAccountTransfers+ transfers `shouldSatisfy` isRight+ describe "get transfer" $ do+ it "will attempt to return transfer data for a single transfer" $ do+ testTransferId <- liftIO UUID.nextRandom+ transfer <- circleTest config manager (getBusinessAccountTransfer testTransferId)+ transfer `shouldSatisfy` isRight+ describe "create transfer" $ do+ it "will attempt to create a new transfer" $ do+ businessTransferRequestIdempotencyKey <- liftIO $ UUID.nextRandom+ transferDestinationAddress <- liftIO $ UUID.nextRandom+ newTransfer <- circleTest config manager $ createBusinessAccountTransfer (testBusinessTransferRequestBody businessTransferRequestIdempotencyKey transferDestinationAddress)+ newTransfer `shouldSatisfy` isRight+ describe "/businessAccount/wallets/addresses endpoint" $ do+ describe "list business account deposit addresses" $ do+ it "should list all business account deposit addresses for a given business account" $ do+ businessAccountRecipientAddresses <- circleTest config manager listAllBusinessAccountRecipientAddresses+ businessAccountRecipientAddresses `shouldSatisfy` isRight+ describe "create recipient address" $ do+ it "will attempt to create a recipient address" $ do+ recipientAddressIdempotencyKey <- liftIO $ UUID.nextRandom+ -- need to do this because each addressId needs to be unique, even in the sandbox+ randomHexString <- replicateM 20 (randomRIO ('a', 'z'))+ recipientAddress <- circleTest config manager $ createBusinessAccountRecipientAddress (testRecipientAddressRequestBody recipientAddressIdempotencyKey (HexString (T.pack randomHexString)))+ recipientAddress `shouldSatisfy` isRight+ describe "list business account deposit addresses" $ do+ it "should list all deposit addresses for a given business account" $ do+ businessAccountDepositAddresses <- circleTest config manager listAllBusinessAccountDepositAddresses+ businessAccountDepositAddresses `shouldSatisfy` isRight+ describe "create business account deposit address" $ do+ it "will attempt to create a new business account deposit address" $ do+ let depositAddressIdempotencyKey = fromJust $ UUID.fromText "ba943ff1-ca16-49b2-ba55-1057e70ca5c7"+ businessAccountDepositAddress <- circleTest config manager $ createBusinessAccountDepositAddress (testDepositAddressRequestBody depositAddressIdempotencyKey)+ businessAccountDepositAddress `shouldSatisfy` isRight+ describe "/businessAccount/deposits endpoint" $ do+ describe "list deposits" $ do+ it "should list all deposits" $ do+ deposits <- circleTest config manager listAllBusinessAccountDeposits+ deposits `shouldSatisfy` isRight+ describe "/businessAccount/payouts endpoint" $ do+ describe "list payouts" $ do+ -- NB: This request fails without money in the account.+ it " should list a subset of payouts for a given business account given the query params" $ do+ subsetOfPayouts <- circleTest config manager $ listAllBusinessAccountPayouts -&- PaginationQueryParams (PageBefore "a8899b8e-782a-4526-b674-0efe1e04526d")+ subsetOfPayouts `shouldSatisfy` isRight+ it "should list all payouts for a given business account" $ do+ payouts <- circleTest config manager listAllBusinessAccountPayouts+ payouts `shouldSatisfy` isRight+ describe "get payout" $ do+ it "will attempt to return the payout data for the payout Id provided" $ do+ fakePayoutId <- liftIO $ UUID.nextRandom+ failedPayout <- circleTest config manager (getBusinessAccountPayout fakePayoutId)+ failedPayout `shouldSatisfy` isRight+ let Right CircleResponseBody {circleResponseMessage} = failedPayout+ -- NB: this request will fail if there's no payoutId+ circleResponseMessage `shouldBe` Just (ResponseMessage "Resource not found")+ describe "create payout" $ do+ it "fails to create a new payout because no such account exists" $ do+ businessPayoutIdempotencyKey <- liftIO $ UUID.nextRandom+ destinationAccountId <- liftIO $ UUID.nextRandom+ failedPayoutResultsNoAccount <- circleTest config manager $ createBusinessAccountPayout (testBusinessPayoutRequestBody businessPayoutIdempotencyKey destinationAccountId)+ failedPayoutResultsNoAccount `shouldSatisfy` isRight+ let Right CircleResponseBody {circleResponseMessage} = failedPayoutResultsNoAccount+ circleResponseMessage `shouldBe` Just (ResponseMessage "Fiat account not found")+ it "fails to create a new payout because the account has insufficient funds" $ do+ wireAccountIdempotencyKey <- liftIO $ UUID.nextRandom+ createdAccount <- circleTest config manager $ createBusinessWireAccount (testUSWireAccountDetails wireAccountIdempotencyKey)+ createdAccount `shouldSatisfy` isRight+ let Right CircleResponseBody {circleResponseData} = createdAccount+ for_ circleResponseData $ \WireAccountResponseBody {..} -> do+ payoutIdempotencyKey <- liftIO $ UUID.nextRandom+ -- this request will fail because there is no money in the account+ -- but the _method_ still works+ failedPayoutResultInsufficientFunds <- circleTest config manager $ createBusinessAccountPayout (testBusinessPayoutRequestBody payoutIdempotencyKey wireAccountResponseBodyId)+ failedPayoutResultInsufficientFunds `shouldSatisfy` isRight+ let Right CircleResponseBody {circleResponseMessage} = failedPayoutResultInsufficientFunds+ circleResponseMessage `shouldBe` Just (ResponseMessage "Account has insufficient funds")+ describe "/payments endpoint" $ do+ describe "create payment" $ do+ it "should create a payment" $ do+ paymentRequestIdempotencyKey <- liftIO $ UUID.nextRandom+ paymentSourceId <- liftIO $ UUID.nextRandom+ payment <- circleTest config manager $ createPayment (testFiatPayment paymentRequestIdempotencyKey paymentSourceId)+ payment `shouldSatisfy` isRight+ describe "get payment" $ do+ it "should get a newly-created payment" $ do+ paymentRequestIdempotencyKey <- liftIO $ UUID.nextRandom+ paymentSourceId <- liftIO $ UUID.nextRandom+ newlyCreatedPayment <- circleTest config manager $ createPayment (testFiatPayment paymentRequestIdempotencyKey paymentSourceId)+ newlyCreatedPayment `shouldSatisfy` isRight+ let Right CircleResponseBody {circleResponseData} = newlyCreatedPayment+ let Just (This FiatOrCryptoPaymentResponseBody {fiatOrCryptoPaymentId}) = circleResponseData+ thePayment <- circleTest config manager $ getPayment fiatOrCryptoPaymentId+ thePayment `shouldSatisfy` isRight+ -- NB: skipping this failing suite; these test don't pass because the payment will fail immediately if it's in the sandbox+ xdescribe "cancel payment" $ do+ it "should cancel a newly-created payment" $ do+ paymentRequestIdempotencyKey <- liftIO $ UUID.nextRandom+ paymentSourceId <- liftIO $ UUID.nextRandom+ paymentToCancel <- circleTest config manager $ createPayment (testFiatPayment paymentRequestIdempotencyKey paymentSourceId)+ paymentToCancel `shouldSatisfy` isRight+ let Right CircleResponseBody {circleResponseData} = paymentToCancel+ let Just (This FiatOrCryptoPaymentResponseBody {fiatOrCryptoPaymentId}) = circleResponseData+ cancelPaymentIdempotencyKey <- liftIO $ UUID.nextRandom+ cancellablePayment <- circleTest config manager $ cancelPayment fiatOrCryptoPaymentId (testCancelPaymentBody cancelPaymentIdempotencyKey)+ cancellablePayment `shouldSatisfy` isRight+ -- NB: skipping this failing test; the test doesn't pass because the payment will fail immediately if it's in the sandbox+ xdescribe "refund payment" $ do+ it "should refund a newly-created payment" $ do+ paymentRequestIdempotencyKey <- liftIO $ UUID.nextRandom+ paymentSourceId <- liftIO $ UUID.nextRandom+ paymentToRefund <- circleTest config manager $ createPayment (testFiatPayment paymentRequestIdempotencyKey paymentSourceId)+ paymentToRefund `shouldSatisfy` isRight+ let Right CircleResponseBody {circleResponseData} = paymentToRefund+ let Just (This FiatOrCryptoPaymentResponseBody {fiatOrCryptoPaymentId}) = circleResponseData+ refundPaymentIdempotencyKey <- liftIO $ UUID.nextRandom+ refundablePayment <- circleTest config manager $ refundPayment fiatOrCryptoPaymentId (testRefundPaymentBody refundPaymentIdempotencyKey)+ refundablePayment `shouldSatisfy` isRight+ describe "list payments" $ do+ it "should list all payments" $ do+ payments <- circleTest config manager listAllPayments+ payments `shouldSatisfy` isRight+ let Right CircleResponseBody {circleResponseCode, circleResponseMessage} = payments+ circleResponseCode `shouldBe` Nothing+ circleResponseMessage `shouldBe` Nothing+ describe "mock payments" $ do+ it "should create a mock silvergate payment" $ do+ mockSilvergatePayment <- circleTest config manager $ createMockSilvergatePayment mockSilvergatePaymentBody+ mockSilvergatePayment `shouldSatisfy` isRight+ it "should create a mock wire payment" $ do+ mockSilvergatePayment <- circleTest config manager $ createMockWirePayment mockWirePaymentBody+ mockSilvergatePayment `shouldSatisfy` isRight+ it "should create a mock SEPA payment" $ do+ mockSepaPayment <- circleTest config manager $ createMockSEPAPayment mockSEPAPaymentBody+ mockSepaPayment `shouldSatisfy` isRight+ describe "/transfers endpoint" $ do+ describe "create an on-chain transfer" $ do+ it "should create an on-chain transfers" $ do+ onChainTransferIdempotencyKey <- liftIO $ UUID.nextRandom+ newOnChainTransfer <- circleTest config manager $ createOnChainTransfer (testOnChainTransferRequestBody onChainTransferIdempotencyKey testSourceWallet testDestinationWallet)+ newOnChainTransfer `shouldSatisfy` isRight+ describe "list on-chain transfers" $ do+ it "should list all on-chain transfers" $ do+ onChainTransfers <- circleTest config manager listAllOnChainTransfers+ onChainTransfers `shouldSatisfy` isRight+ describe "get an on-chain transfer" $ do+ it "should get an on-chain transfers" $ do+ uuid <- liftIO $ UUID.nextRandom+ onChainTransfer <- circleTest config manager $ getOnChainTransfer uuid+ onChainTransfer `shouldSatisfy` isRight+ describe "/cards endpoint" $ do+ describe "create card" $ do+ it "should create a new card, get it, and then update it" $ do+ cardIdempotencyKeyUUID <- liftIO $ UUID.nextRandom+ newCard <- circleTest config manager $ createCard (testCreateCardRequestBody cardIdempotencyKeyUUID testPaymentMetadata)+ newCard `shouldSatisfy` isRight+ let Right CircleResponseBody {circleResponseCode, circleResponseMessage, circleResponseData} = newCard+ circleResponseCode `shouldBe` Nothing+ circleResponseMessage `shouldBe` Nothing+ for_ circleResponseData $ \CardResponseBody {..} -> do+ card <- circleTest config manager $ getCard cardId+ card `shouldSatisfy` isRight+ updatedCard <- circleTest config manager $ updateCard cardId testUpdateCardBody+ updatedCard `shouldSatisfy` isRight+ describe "list cards" $ do+ it "should list all cards" $ do+ cards <- circleTest config manager listAllCards+ cards `shouldSatisfy` isRight+ describe "banks/wires endpoint" $ do+ describe "create wire account" $ do+ it "creates a new wire account" $ do+ wireAccountIdempotencyKey <- liftIO $ UUID.nextRandom+ newWireAccount <- circleTest config manager $ createWireAccount (testUSWireAccountDetails wireAccountIdempotencyKey)+ newWireAccount `shouldSatisfy` isRight+ describe "get wire account" $ do+ it "gets a single wire account" $ do+ wireAccountIdempotencyKey <- liftIO $ UUID.nextRandom+ wireAccount1 <- circleTest config manager $ createWireAccount (testUSWireAccountDetails wireAccountIdempotencyKey)+ wireAccount1 `shouldSatisfy` isRight+ let Right CircleResponseBody {circleResponseData} = wireAccount1+ for_ circleResponseData $ \WireAccountResponseBody {..} -> do+ wireAccount <- circleTest config manager $ getWireAccount wireAccountResponseBodyId+ wireAccount `shouldSatisfy` isRight+ it "gets wire instructions for a wire account" $ do+ wireAccountIdempotencyKey <- liftIO $ UUID.nextRandom+ wireAccount2 <- circleTest config manager $ createWireAccount (testUSWireAccountDetails wireAccountIdempotencyKey)+ wireAccount2 `shouldSatisfy` isRight+ let Right CircleResponseBody {circleResponseData} = wireAccount2+ for_ circleResponseData $ \WireAccountResponseBody {..} -> do+ wireAccountInstructions <- circleTest config manager $ getWireAccountInstructions wireAccountResponseBodyId+ wireAccountInstructions `shouldSatisfy` isRight+ describe "/settlements endpoint" $ do+ describe "list all settlements" $ do+ it "should list all settlements" $ do+ settlements <- circleTest config manager listAllSettlements+ settlements `shouldSatisfy` isRight+ describe "/reversals endpoint" $ do+ describe "list all reversals" $ do+ it "should list all reversals" $ do+ reversals <- circleTest config manager listAllACHReversals+ reversals `shouldSatisfy` isRight+ describe "/paymentIntents endpoint" $ do+ -- NB: these all 403 in the sandbox. L L L.+ describe "list all paymentIntents" $ do+ it "should list all paymentIntents" $ do+ paymentIntents <- circleTest config manager listAllPaymentIntents+ paymentIntents `shouldSatisfy` isRight+ describe "create new paymentIntent" $ do+ it "should create a new paymentIntent" $ do+ createPaymentIntentIdempotencyKey <- liftIO $ UUID.nextRandom+ paymentIntent <- circleTest config manager $ createPaymentIntent (testPaymentIntentRequestBody createPaymentIntentIdempotencyKey)+ paymentIntent `shouldSatisfy` isRight+ describe "/payouts endpoint" $ do+ describe "list all payouts from a non-business account" $ do+ it "should list all payouts" $ do+ payouts <- circleTest config manager listAllPayouts+ payouts `shouldSatisfy` isRight+ describe "create a new payout from a non-business account" $ do+ it "should create a new payout" $ do+ createPayoutIdempotencyKey <- liftIO $ UUID.nextRandom+ destinationAccountId <- liftIO $ UUID.nextRandom+ payout <- circleTest config manager $ createPayout (testPayoutRequestBody createPayoutIdempotencyKey destinationAccountId)+ payout `shouldSatisfy` isRight+ describe "get an existing payout from a non-business account" $ do+ it "should get a payout" $ do+ testPayoutId <- liftIO $ UUID.nextRandom+ payout <- circleTest config manager $ getPayout testPayoutId+ payout `shouldSatisfy` isRight+ describe "/returns endpoint" $ do+ describe "list all returns" $ do+ it "should list all wire and ACH payout returns" $ do+ payouts <- circleTest config manager listAllReturns+ payouts `shouldSatisfy` isRight+ describe "/wallets endpoint" $ do+ describe "list all wallets" $ do+ it "should retrieve a list of a user's wallets" $ do+ wallets <- circleTest config manager listAllWallets+ wallets `shouldSatisfy` isRight+ describe "get an existing wallet" $ do+ it "should get a wallet" $ do+ testWalletId <- liftIO $ UUID.nextRandom+ wallet <- circleTest config manager $ getWallet testWalletId+ wallet `shouldSatisfy` isRight+ describe "create a new end user wallet" $ do+ it "should create a new wallet" $ do+ testWalletIdempotencyKey <- liftIO $ UUID.nextRandom+ newWallet <- circleTest config manager $ createWallet (testCreateWalletRequestBody testWalletIdempotencyKey)+ newWallet `shouldSatisfy` isRight+ describe "create deposit address" $ do+ -- NB: this tests 404s bc no wallet Id+ it "will attempt to create a new deposit address for a given wallet" $ do+ testRandomWalletId <- liftIO $ UUID.nextRandom+ depositAddressIdempotencyKey <- liftIO $ UUID.nextRandom+ depositAddress <- circleTest config manager $ createDepositAddress testRandomWalletId (testDepositAddressRequestBody depositAddressIdempotencyKey)+ depositAddress `shouldSatisfy` isRight+ describe "list recipient addresses associated with a wallet ID" $ do+ -- TODO: this tests 404s bc no wallet Id+ it "should list all deposit addresses for a given account" $ do+ testRandomWalletId <- liftIO $ UUID.nextRandom+ recipientAddresses <- circleTest config manager $ listAllAddresses testRandomWalletId+ recipientAddresses `shouldSatisfy` isRight++---------------------------------------------------------------+-- Test Data+---------------------------------------------------------------++testUSWireAccountDetails :: UUID -> WireAccountRequestBody+testUSWireAccountDetails wireAccountIdempotencyKey =+ USBankAccount+ ( USBankAccountRequestBody+ wireAccountIdempotencyKey+ [compileAccountNumber|446043103366|]+ [compileRoutingNumber|021000021|]+ ( BillingDetails+ "Test Recipient"+ (City "Snoqualmie")+ (ISO3166Alpha2 "US")+ (AddressLine "6501 Railroad Avenue SE")+ (Just (AddressLine "Room 315"))+ (Just (District "WA"))+ (PostalCode "85283")+ )+ ( BankAddress+ (Just "Test Bank")+ (Just (City "Snoqualmie"))+ (Just (ISO3166Alpha2 "US"))+ (Just (AddressLine "6501 Railroad Avenue SE"))+ (Just (AddressLine "Room 315"))+ (Just (District "WA"))+ )+ )++testBusinessTransferRequestBody :: UUID -> UUID -> BusinessTransferRequestBody+testBusinessTransferRequestBody businessTransferRequestIdempotencyKey transferDestinationAddress =+ BusinessTransferRequestBody+ businessTransferRequestIdempotencyKey+ ( TransferDestination+ VerifiedBlockchain+ transferDestinationAddress+ )+ ( MoneyAmount+ (Amount "100.00")+ USD+ )++testOnChainTransferRequestBody :: UUID -> SourceWallet -> DestinationWallet -> OnChainTransferRequestBody+testOnChainTransferRequestBody onChainTransferIdempotencyKey sourceWallet destinationWallet =+ OnChainTransferRequestBody+ onChainTransferIdempotencyKey+ sourceWallet+ (This destinationWallet)+ ( MoneyAmount+ (Amount "100.00")+ USD+ )++testRecipientAddressRequestBody :: UUID -> HexString -> RecipientAddressRequestBody+testRecipientAddressRequestBody recipientAddressIdempotencyKey blockchainAddress =+ RecipientAddressRequestBody+ recipientAddressIdempotencyKey+ blockchainAddress+ Nothing+ ChainETH+ USD+ "test address"++testDepositAddressRequestBody :: UUID -> DepositAddressRequestBody+testDepositAddressRequestBody depositAddressIdempotencyKey =+ DepositAddressRequestBody+ depositAddressIdempotencyKey+ USD+ ChainETH++testSENAccountDetails :: UUID -> SENAccountRequestBody+testSENAccountDetails senAccountIdempotencyKey =+ SENAccountRequestBody+ senAccountIdempotencyKey+ [compileAccountNumber|446043103367|]+ (Just USD)++testSubscriptionBody :: SubscriptionRequestBody+testSubscriptionBody =+ SubscriptionRequestBody "https://example.org/handler/for/notifications"++testPaymentMetadata :: RequestMetadata+testPaymentMetadata =+ RequestMetadata+ [compileEmail|dylan@test.com|]+ Nothing+ (SessionId "DE6FA86F60BB47B379307F851E238617")+ (IPAddress "244.28.239.130")++testPayoutMetadata :: PayoutMetadata+testPayoutMetadata =+ PayoutMetadata+ [compileEmail|dylan@test.com|]++testCreateCardRequestBody :: UUID -> RequestMetadata -> CreateCardRequestBody+testCreateCardRequestBody cardIdempotencyKeyUUID =+ CreateCardRequestBody+ cardIdempotencyKeyUUID+ (Just "key1")+ (Just "LS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tCgp3Y0JNQTBYV1NGbEZScFZoQVFmL2J2bVVkNG5LZ3dkbExKVTlEdEFEK0p5c0VOTUxuOUlRUWVGWnZJUWEKMGgzQklpRFNRU0RMZmI0NEs2SXZMeTZRbm54bmFLcWx0MjNUSmtPd2hGWFIrdnNSMU5IbnVHN0lUNWJECmZzeVdleXlNK1JLNUVHV0thZ3NmQ2tWamh2NGloY29xUnlTTGtJbWVmRzVaR0tMRkJTTTBsTFNPWFRURQpiMy91eU1zMVJNb3ZiclNvbXkxa3BybzUveWxabWVtV2ZsU1pWQlhNcTc1dGc1YjVSRVIraXM5ckc0cS8KMXl0M0FOYXA3UDhKekFhZVlyTnVNZGhGZFhvK0NFMC9CQnN3L0NIZXdhTDk4SmRVUEV0NjA5WFRHTG9kCjZtamY0YUtMQ01xd0RFMkNVb3dPdE8vMzVIMitnVDZKS3FoMmtjQUQyaXFlb3luNWcralRHaFNyd3NKWgpIdEphQWVZZXpGQUVOaFo3Q01IOGNsdnhZVWNORnJuNXlMRXVGTkwwZkczZy95S3loclhxQ0o3UFo5b3UKMFVxQjkzQURKWDlJZjRBeVQ2bU9MZm9wUytpT2lLall4bG1NLzhlVWc3OGp1OVJ5T1BXelhyTzdLWTNHClFSWm8KPXc1dEYKLS0tLS1FTkQgUEdQIE1FU1NBR0UtLS0tLQo")+ ( BillingDetails+ "Test Recipient"+ (City "Snoqualmie")+ (ISO3166Alpha2 "US")+ (AddressLine "6501 Railroad Avenue SE")+ (Just (AddressLine "Room 315"))+ (Just (District "WA"))+ (PostalCode "85283")+ )+ 12+ 2027++testFiatPayment :: UUID -> UUID -> CreatePaymentRequestBody+testFiatPayment paymentRequestIdempotencyKey paymentSourceId =+ CreatePaymentRequestBody+ paymentRequestIdempotencyKey+ "key1"+ testPaymentMetadata+ ( MoneyAmount+ (Amount "100.00")+ USD+ )+ (Just True)+ VerificationCVV+ Nothing+ Nothing+ ( PaymentSource+ paymentSourceId+ Card+ )+ Nothing+ Nothing+ Nothing++testCancelPaymentBody :: UUID -> CancelPaymentRequestBody+testCancelPaymentBody cancelPaymentIdempotencyKey =+ CancelPaymentRequestBody+ cancelPaymentIdempotencyKey+ (Just CancelPaymentReasonDuplicate)++testRefundPaymentBody :: UUID -> RefundPaymentRequestBody+testRefundPaymentBody refundPaymentIdempotencyKey =+ RefundPaymentRequestBody+ refundPaymentIdempotencyKey+ ( MoneyAmount+ (Amount "100.00")+ USD+ )+ Nothing++testBusinessPayoutRequestBody :: UUID -> UUID -> BusinessPayoutRequestBody+testBusinessPayoutRequestBody payoutIdempotencyKey wireAccountId =+ BusinessPayoutRequestBody+ payoutIdempotencyKey+ ( DestinationBankAccount+ Wire+ wireAccountId+ Nothing+ )+ ( MoneyAmount+ (Amount "100.00")+ USD+ )++testPayoutRequestBody :: UUID -> UUID -> PayoutRequestBody+testPayoutRequestBody payoutIdempotencyKey wireAccountId =+ PayoutRequestBody+ payoutIdempotencyKey+ Nothing+ ( DestinationBankAccount+ Wire+ wireAccountId+ Nothing+ )+ ( MoneyAmount+ (Amount "100.00")+ USD+ )+ testPayoutMetadata++testUpdateCardBody :: UpdateCardRequestBody+testUpdateCardBody =+ UpdateCardRequestBody+ (Just "key1")+ (Just "LS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tCgp3Y0JNQTBYV1NGbEZScFZoQVFmL2J2bVVkNG5LZ3dkbExKVTlEdEFEK0p5c0VOTUxuOUlRUWVGWnZJUWEKMGgzQklpRFNRU0RMZmI0NEs2SXZMeTZRbm54bmFLcWx0MjNUSmtPd2hGWFIrdnNSMU5IbnVHN0lUNWJECmZzeVdleXlNK1JLNUVHV0thZ3NmQ2tWamh2NGloY29xUnlTTGtJbWVmRzVaR0tMRkJTTTBsTFNPWFRURQpiMy91eU1zMVJNb3ZiclNvbXkxa3BybzUveWxabWVtV2ZsU1pWQlhNcTc1dGc1YjVSRVIraXM5ckc0cS8KMXl0M0FOYXA3UDhKekFhZVlyTnVNZGhGZFhvK0NFMC9CQnN3L0NIZXdhTDk4SmRVUEV0NjA5WFRHTG9kCjZtamY0YUtMQ01xd0RFMkNVb3dPdE8vMzVIMitnVDZKS3FoMmtjQUQyaXFlb3luNWcralRHaFNyd3NKWgpIdEphQWVZZXpGQUVOaFo3Q01IOGNsdnhZVWNORnJuNXlMRXVGTkwwZkczZy95S3loclhxQ0o3UFo5b3UKMFVxQjkzQURKWDlJZjRBeVQ2bU9MZm9wUytpT2lLall4bG1NLzhlVWc3OGp1OVJ5T1BXelhyTzdLWTNHClFSWm8KPXc1dEYKLS0tLS1FTkQgUEdQIE1FU1NBR0UtLS0tLQo")+ 1+ 2028++testCreateWalletRequestBody :: UUID -> CreateWalletRequestBody+testCreateWalletRequestBody idempotencyKey =+ CreateWalletRequestBody+ idempotencyKey+ (Just "test wallet")++testPaymentIntentRequestBody :: UUID -> CreatePaymentIntentRequestBody+testPaymentIntentRequestBody createPaymentIntentIdempotencyKey =+ CreatePaymentIntentRequestBody+ createPaymentIntentIdempotencyKey+ ( MoneyAmount+ (Amount "100.00")+ USD+ )+ USD+ [testPaymentMethod]++testPaymentMethod :: PaymentMethodData+testPaymentMethod =+ PaymentMethodData+ "blockchain"+ ChainETH+ Nothing++testIdentity :: Identity+testIdentity =+ Identity+ Individual+ "Mario"+ [ Address+ (Just (City "Snoqualmie"))+ (Just (ISO3166Alpha2 "US"))+ (Just (AddressLine "6501 Railroad Avenue SE"))+ (Just (AddressLine "Room 315"))+ (Just (District "WA"))+ ]++testSourceWallet :: SourceWallet+testSourceWallet =+ SourceWallet+ Wallet+ (WalletId "987654321")+ [testIdentity]++testDestinationWallet :: DestinationWallet+testDestinationWallet =+ DestinationWallet+ Wallet+ (WalletId "123456789")+ Nothing+ Nothing++mockSilvergatePaymentBody :: MockSenOrWirePaymentRequestBody+mockSilvergatePaymentBody =+ MockSenOrWirePaymentRequestBody+ (TrackingReference "CIR13FB13A")+ ( MoneyAmount+ (Amount "100.00")+ USD+ )+ ( MockBeneficiaryBankDetails+ [compileAccountNumber|446043103366|]+ )++mockWirePaymentBody :: MockSenOrWirePaymentRequestBody+mockWirePaymentBody =+ MockSenOrWirePaymentRequestBody+ (TrackingReference "CIR13FB13A")+ ( MoneyAmount+ (Amount "100.00")+ USD+ )+ ( MockBeneficiaryBankDetails+ [compileAccountNumber|446043103366|]+ )++mockSEPAPaymentBody :: MockSEPAPaymentRequestBody+mockSEPAPaymentBody =+ MockSEPAPaymentRequestBody+ (TrackingReference "CIR13FB13A")+ ( MoneyAmount+ (Amount "100.00")+ USD+ )