gocardless-clj0.4.1Clojure client library for the GoCardless API dependencies
| (this space intentionally left almost blank) | |||||||||||||||
HTTP client wrapper and other utilities | ||||||||||||||||
(ns gocardless-clj.client (:require [clojure.walk :refer [stringify-keys]] [clj-http.client :as client] [cheshire.core :as json] [clj-time.core :as time] [gocardless-clj.signature :refer [sign-params normalise-params]])) | ||||||||||||||||
(defn- ua-string [] (format "gocardless-clj/v%s" (System/getProperty "gocardless-clj.version"))) | ||||||||||||||||
Recursively replace all dashes with underscores in all the keys of m. From clojure.walk/stringify-keys. | (defn- underscorize-keys [m] (let [f (fn [[k v]] [(clojure.string/replace k "-" "_") v])] (clojure.walk/postwalk (fn [x] (if (map? x) (into {} (map f x)) x)) m))) | |||||||||||||||
Join parts with | (defn path [& parts] (clojure.string/join "/" parts)) | |||||||||||||||
Generate a nonce for a Connect request. | (def ^:private secure-random (java.security.SecureRandom.)) (defn- generate-nonce [] (-> (java.math.BigInteger. 256 secure-random) (.toString 32))) | |||||||||||||||
Produce a correct URL for retrieving resource(s). | (defn api-url [account uri] (if (.startsWith uri "http") uri (condp = (:environment account) :live (format "https://gocardless.com/api/v1/%s" uri) :sandbox (format "https://sandbox.gocardless.com/api/v1/%s" uri)))) | |||||||||||||||
Produce a correct connect base URL. | (defn connect-url [account resource-type] (condp = (:environment account) :live (format "https://gocardless.com/connect/%ss/new" resource-type) :sandbox (format "https://sandbox.gocardless.com/connect/%ss/new" resource-type))) | |||||||||||||||
Produce a correct connect URL for creating a limit. | (defn new-limit-url [account resource-type limit-params] (let [url (connect-url account resource-type) limit-params (-> limit-params stringify-keys underscorize-keys) limit-params (assoc limit-params "merchant_id" (:merchant-id account)) meta-params (select-keys limit-params ["redirect_uri" "cancel_uri" "state"]) limit-params (dissoc limit-params "redirect_uri" "cancel_uri" "state") base-params {"nonce" (generate-nonce) "timestamp" (str (time/now)) "client_id" (:app-id account) resource-type limit-params} params (merge base-params meta-params) signature (-> params (sign-params (:app-secret account))) params (merge {"signature" signature} params)] (str url "?" (-> params normalise-params)))) | |||||||||||||||
Worker function for | (defn do-request [method url params] (let [headers {"Accept" "application/json" "User-Agent" (ua-string)} final-params (merge params {:method method :url url :headers headers :as :json})] (:body (client/request final-params)))) | |||||||||||||||
Do a GET request to the API. | (defn api-get ([account uri params] (api-get account uri params {:oauth-token (:access-token account)})) ([account uri params auth-params] (do-request :get (api-url account uri) (merge auth-params {:query-params params})))) | |||||||||||||||
Do a POST request to the API. | (defn api-post ([account uri params] (api-post account uri params {:oauth-token (:access-token account)})) ([account uri params auth-params] (do-request :post (api-url account uri) (merge auth-params {:form-params params :content-type :json})))) | |||||||||||||||
Do a PUT request to the API. | (defn api-put ([account uri params] (api-put account uri params {:oauth-token (:access-token account)})) ([account uri params auth-params] (do-request :put (api-url account uri) (merge auth-params {:form-params params :content-type :json})))) | |||||||||||||||
Core API functionsThis namespace contains the public API functions for this client library. At a high level, you can:
| ||||||||||||||||
(ns gocardless-clj.core (:require [gocardless-clj.client :as c] [gocardless-clj.resources :refer :all] [gocardless-clj.signature :refer [sign-params]])) | ||||||||||||||||
(def cancel #'gocardless-clj.protocols/cancel) (def cancelable? #'gocardless-clj.protocols/cancelable?) (def retry #'gocardless-clj.protocols/retry) (def retriable? #'gocardless-clj.protocols/retriable?) | ||||||||||||||||
(declare customers) (declare payouts) (declare bills) (declare subscriptions) (declare pre-authorizations) | ||||||||||||||||
Resource lookup functionsEvery resource has a singular and a plural version. For example the customer
function comes in two variants - The plural version takes a params map as the second argument. The map can contain any keys outlined in Filtering and Pagination. The map can be omitted or left empty, in which case the defaults are used. | ||||||||||||||||
Retrieve a single customer by their ID. Example:
| (defn customer [account id] (customers account {:id id})) | |||||||||||||||
Retrieve merchant's customers or a single customer. Takes as arguments either the account-map and no other params, or the account-map and a map of params. Examples:
| (defn customers ([account] (customers account {})) ([account {:keys [id] :as params}] (if id (c/api-get account (c/path "users" id) {}) (c/api-get account (c/path "merchants" (:merchant-id account) "users") params)))) | |||||||||||||||
Retrieve a single payout by the payout ID. Example:
| (defn payout [account id] (payouts account {:id id})) | |||||||||||||||
Retrieve merchant's payouts or a single payout. Takes as arguments either the account-map and no other params, or the account-map and a map of params. Examples:
| (defn payouts ([account] (payouts account {})) ([account {:keys [id] :as params}] (if id (c/api-get account (c/path "payouts" id) {}) (c/api-get account (c/path "merchants" (:merchant-id account) "payouts") params)))) | |||||||||||||||
Retrieve a single bill by the bill ID. Example:
| (defn bill [account id] (bills account {:id id})) | |||||||||||||||
Retrieve merchant's bills or a single bill. Takes as arguments either the account-map and an ID of a bill, or the account-map and params. Examples:
| (defn bills ([account] (bills account {})) ([account {:keys [id] :as params}] (if id (-> (c/api-get account (c/path "bills" id) {}) map->Bill) (let [path (c/path "merchants" (:merchant-id account) "bills") bills (c/api-get account path params)] (map map->Bill bills))))) | |||||||||||||||
Retrieve a single subscription by the subscription ID. Example:
| (defn subscription [account id] (subscriptions account {:id id})) | |||||||||||||||
Retrieve merchant's subscriptions or a single subscription. Takes as arguments either the account-map and no other arguments, or the account-map and a map of params. Examples:
| (defn subscriptions ([account] (subscriptions account {})) ([account {:keys [id] :as params}] (if id (-> (c/api-get account (c/path "subscriptions" id) {}) (map->Subscription)) (let [path (c/path "merchants" (:merchant-id account) "subscriptions") subs (c/api-get account path params)] (map map->Subscription subs))))) | |||||||||||||||
Retrieve a single pre-authorization by the pre-authorization ID. Example:
| (defn pre-authorization [account id] (pre-authorizations account {:id id})) | |||||||||||||||
Retrieve merchant's pre-authorizations or a single pre-authorization. Takes as arguments either the account-map and no other arguments, or the account-map and a map of params. Examples:
| (defn pre-authorizations ([account] (pre-authorizations account {})) ([account {:keys [id] :as params}] (if id (-> (c/api-get account (c/path "pre_authorizations" id) {}) map->PreAuthorization) (let [path (c/path "merchants" (:merchant-id account) "pre_authorizations") preauths (c/api-get account path params)] (map map->PreAuthorization preauths))))) | |||||||||||||||
Creates a new bill under an existing pre-authorization. Takes as arguments the account map and a params map containing the amount and the pre-authorization ID. Example:
Resource creation functions | (defn create-bill [account {:keys [amount pre_authorization_id] :as opts}] {:pre [(number? amount) (> amount 1.0) (string? pre_authorization_id) (not (empty? pre_authorization_id))]} (let [params (assoc opts :amount (bigdec amount))] (-> (c/api-post account "bills" {"bill" params}) map->Bill))) | |||||||||||||||
Returns the Connect URL for a new Bill. Required map keys: Example:
| (defn new-bill [account {:keys [amount] :as opts}] {:pre [(number? amount) (> amount 1.0)]} (let [params (assoc opts :amount (bigdec amount))] (c/new-limit-url account "bill" params))) | |||||||||||||||
Returns the Connect URL for a new Subscription. Required map keys: Example:
| (defn new-subscription [account {:keys [amount interval_length interval_unit] :as opts}] {:pre [(number? amount) (number? interval_length) (pos? interval_length) (contains? #{"day" "week" "month"} interval_unit)]} (let [params (assoc opts :amount (bigdec amount))] (c/new-limit-url account "subscription" params))) | |||||||||||||||
Returns the Connect URL for a new PreAuthorization. Required keys: Example:
| (defn new-pre-authorization [account {:keys [max_amount interval_length interval_unit] :as opts}] {:pre [(number? max_amount) (number? interval_length) (pos? interval_length) (contains? #{"day" "week" "month"} interval_unit)]} (let [params (assoc opts :max_amount (bigdec max_amount))] (c/new-limit-url account "pre_authorization" params))) | |||||||||||||||
Authenticating merchant's details | ||||||||||||||||
Get the account details. Example:
| (defn details [account] (c/api-get account (c/path "merchants" (:merchant-id account)) {})) | |||||||||||||||
Resource confirmation | ||||||||||||||||
Confirm a created limit (bill/subscription/preauthorization). Signature will be checked. If signatures don't match, the resource will not be
confirmed and Example:
| (defn confirm-resource [account params] {:pre [(every? (set (keys params)) ["resource_id" "resource_type" "resource_uri" "signature"])]} (let [ks [:resource_id :resource_type :resource_uri :state :signature] params (clojure.walk/keywordize-keys params) params (select-keys params ks) to-sign (dissoc params :signature) data (select-keys params [:resource_id :resource_type])] (if (= (:signature params) (sign-params to-sign (:app-secret account))) (c/api-post account "confirm" data {:basic-auth [(:app-id account) (:app-secret account)]}) false))) | |||||||||||||||
(ns gocardless-clj.protocols) | ||||||||||||||||
Flatten params according to the signature guide. | (defprotocol PFlattenable (flatten-params [coll ns])) | |||||||||||||||
(defprotocol PCancellable (cancelable? [resource] "Check if the resource can be cancelled.") (cancel [resource account] "Cancel a resource.")) | ||||||||||||||||
(defprotocol PRetriable (retriable? [resource] "Check if the resource can be retried.") (retry [resource account] "Retry a resource.")) | ||||||||||||||||
(ns gocardless-clj.resources (:require [gocardless-clj.protocols :refer :all] [gocardless-clj.client :as client])) | ||||||||||||||||
(defrecord Bill [id] gocardless-clj.protocols/PCancellable (cancelable? [resource] (true? (:can_be_cancelled resource))) (cancel [resource account] (when (cancelable? resource) (do (client/api-put account (client/path (:uri resource) "cancel") {}) true))) gocardless-clj.protocols/PRetriable (retriable? [resource] (true? (:can_be_retried resource))) (retry [resource account] (when (retriable? resource) (do (client/api-post account (client/path (:uri resource) "retry") {}) true)))) | ||||||||||||||||
(defrecord Subscription [id] gocardless-clj.protocols/PCancellable (cancelable? [resource] (not (= "cancelled" (:status resource)))) (cancel [resource account] (when (cancelable? resource) (do (client/api-put account (client/path (:uri resource) "cancel") {}) true)))) | ||||||||||||||||
(defrecord PreAuthorization [id] gocardless-clj.protocols/PCancellable (cancelable? [resource] (not (= "cancelled" (:status resource)))) (cancel [resource account] (when (cancelable? resource) (do (client/api-put account (client/path (:uri resource) "cancel") {}) true)))) | ||||||||||||||||
Signature calculation functionsImplements signature calculation according to the signature guide. | (ns gocardless-clj.signature (:require [pandect.core :refer [sha256-hmac]] [gocardless-clj.protocols :refer [flatten-params]]) (:import java.net.URLEncoder)) | |||||||||||||||
(defn new-ns ([ns] (str ns "[]")) ([ns k] (if ns (str ns "[" k "]") k))) | ||||||||||||||||
(extend-protocol gocardless-clj.protocols/PFlattenable clojure.lang.PersistentArrayMap (flatten-params [coll ns] (let [pairs (map #(flatten-params %2 (new-ns ns %1)) (keys coll) (vals coll))] (if (empty? pairs) [] (apply concat pairs)))) clojure.lang.PersistentHashMap (flatten-params [coll ns] (let [pairs (map #(flatten-params %2 (new-ns ns %1)) (keys coll) (vals coll))] (if (empty? pairs) [] (apply concat pairs)))) clojure.lang.PersistentVector (flatten-params [coll ns] (let [pairs (map #(flatten-params %1 (new-ns ns)) coll)] (if (empty? pairs) [] (apply concat pairs)))) java.lang.String (flatten-params [string ns] [[ns string]]) clojure.lang.Keyword (flatten-params [kw ns] [[ns (name kw)]]) java.lang.Long (flatten-params [number ns] [[ns (str number)]]) java.math.BigDecimal (flatten-params [number ns] [[ns (str number)]])) | ||||||||||||||||
Percent-encode a string. | (defn percent-encode [s] (URLEncoder/encode (name s) "UTF-8")) | |||||||||||||||
Normalises a key-value pair. Percent-encodes both key and value and joins them with | (defn normalise-keyval [keyval-pair] (clojure.string/join "=" (map percent-encode keyval-pair))) | |||||||||||||||
Genrates a percent-encoded query string. Individual keys and values are joined with | (defn normalise-params [params] (let [flattened-sorted (-> params (flatten-params nil) sort)] (clojure.string/join "&" (map normalise-keyval flattened-sorted)))) | |||||||||||||||
Sign the normalised params with SHA256 using a key. The key in this case is the app secret. | (defn sign-params [params key] (-> params normalise-params (sha256-hmac key))) | |||||||||||||||