Skip to content

Commit

Permalink
Added OPP client
Browse files Browse the repository at this point in the history
  • Loading branch information
vloothuis committed May 26, 2024
1 parent 404975c commit a47b82c
Show file tree
Hide file tree
Showing 15 changed files with 1,158 additions and 3 deletions.
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@
},
"css.validate": false,
"less.validate": false,
"scss.validate": false
"scss.validate": false,
"cSpell.words": [
"emailaddress"
]
}
8 changes: 8 additions & 0 deletions core/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"cSpell.words": [
"emailaddress",
"idempotency",
"Nestru",
"uuid"
]
}
8 changes: 8 additions & 0 deletions core/config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,11 @@ unless is_nil(bundle) do
end

import_config "#{config_env()}.exs"

if Mix.env() == :dev do
config :mix_test_watch,
clear: true

config :mix_test_watch,
exclude: [~r/\.#/, ~r{priv/repo/migrations}, ~r/assets\/.*/]
end
4 changes: 4 additions & 0 deletions core/config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ if config_env() == :prod do

config :logger, level: System.get_env("LOG_LEVEL", "info") |> String.to_existing_atom()

config :core, opp_client_options: [
auth: {:bearer, System.fetch_env!("OPP_API_KEY")}
]

if sentry_dsn = System.get_env("SENTRY_DSN") do
config :sentry,
dsn: sentry_dsn,
Expand Down
175 changes: 175 additions & 0 deletions core/lib/opp_client.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
defmodule OPPClient do
use OPPClient.Helper

defmodule OPPResponses do
end

@base_url "https://api-sandbox.onlinebetaalplatform.nl/v1"

# @api_key "79eeea74cb5685779ac17f5758ddc5e0"

def new(opts \\ []) do
[
base_url: @base_url
]
|> Keyword.merge(opts)
|> Req.new()
end

def_req(:post, "/merchants",
type: [
type: {:in, ["consumer", "business"]}
],
country: [
# TODO: Download ISO list on build:
# https://raw.githubusercontent.com/lukes/ISO-3166-Countries-with-Regional-Codes/master/all/all.json
type: {:in, ["nld"]},
required: true
],
emailaddress: [
type: :string,
required: true
],
notify_url: [
type: :string,
required: true
]
)

def_req(:get, "/merchants/{{merchant_uuid}}")

def_req(:post, "/transactions",
merchant_uid: [type: :string, required: true],
locale: [type: {:in, ["nl", "en", "fr", "de"]}],
total_price: [type: :pos_integer, required: true],
products: [
type:
{:list,
{:map,
[
name: [type: :string, required: true],
quantity: [type: :pos_integer, required: true],
price: [type: :pos_integer, required: true]
]}},
required: true
],
return_url: [type: :string, required: true],
notify_url: [type: :string, required: true],
metadata: [type: :map]
)

def_req(:post, "/merchants/{{merchant_uid}}/withdrawals",
amount: [type: :pos_integer, required: true],
currency: [type: :string],
partner_fee: [type: :pos_integer],
notify_url: [type: :string, required: true],
description: [type: :string, required: true],
reference: [type: :string],
metadata: [type: :map]
)

def_req(:post, "/charges",
type: [type: {:in, ["balance"]}, required: true],
amount: [type: :pos_integer, required: true],
currency: [type: :string],
description: [type: :string],
payout_description: [type: :string],
to_owner_uid: [type: :string, required: true],
from_owner_uid: [type: :string, required: true],
metadata: [type: :string]
)

# create
# retrieve
# update
# delete

# type string Merchant type. One of:
# consumerbusiness
# country string Country code of the merchant,
# use ISO 3166-1 alpha-3 country code.
# locale string The language in which the text on the verification screens is being displayed and tickets are sent. Default is en
# One of:
# nl en fr de
# name_first string First name of the merchant. ( CONSUMER ONLY! )
# name_last string Last name of the merchant. ( CONSUMER ONLY! )
# is_pep boolean Whether or not the merchant is a PEP. This will mark the contact that is automatically created as a PEP. ( CONSUMER ONLY! )
# coc_nr string Chamber of Commerce number of the merchant.
# up to 45 characters
# nullable
# ( BUSINESS ONLY! )
# vat_nr string Value added tax identification number.
# up to 45 characters
# ( BUSINESS ONLY! )
# legal_name string (Business) Name of the merchant.
# up to 45 characters
# legal_entity string Business entity of the merchant. One of the legal_entity_code from the legal entity list ( BUSINESS ONLY! )
# trading_names array Array with one or more trading names.

# name
# string

# Trading name.
# emailaddress string Email address of the merchant.
# Must be unique for every merchant.
# phone string Phone number of the merchant.
# settlement_interval string The settlement interval of the merchant. Default is set contractually. Can only be provided after agreement with OPP.
# One of:
# daily weekly monthly yearly continuous
# notify_url string URL to which the notifications of the merchant will be sent.
# return_url string URL to which the merchant will be directed when closing the verification screens.
# metadata object with key-value pairs Additional data that belongs to the merchant object.
# addresses array Address array of the merchant with name/value pairs.

# 200 OK Success
# 400 Bad Request Missing parameter(s)
# 401 Unauthorized Invalid or revoked API key
# 404 Not Found Resource doesn't exist
# 409 Conflict Conflict due to concurrent request
# 410 Gone Resource doesn't exist anymore
# 50X Server Errors Temporary problem on our side

# api_key (env var)

# API Status

# Example request - Status check

# curl https://api-sandbox.onlinebetaalplatform.nl/status \
# -H "Authorization: Bearer {{api_key}}"

# Example response

# {
# "status": "online",
# "date": 1611321273
# }

# Idempotency-Key: {key}

# Pagination

# Example request - Retrieve page 2 of the transactions list:

# curl https://api-sandbox.onlinebetaalplatform.nl/v1/transactions?page=2&perpage=10 \
# -H "Authorization: Bearer {{api_key}}"

# Example response:

# {
# "livemode": true,
# "object": "list",
# "url": "/v1/transactions",
# "has_more": true,
# "total_item_count": 259,
# "items_per_page": 10,
# "current_page": 2,
# "last_page": 26,
# "data": []
# }

# When retrieving lists of objects, OPP creates pages to keep the transferred objects small. Use the pagination functionality to navigate when sorting through many results. The pages can be switched by adding the following parameters to the GET call:
# Parameter Description
# page integer The number of the current page.
# perpage integer The limit of objects to be returned. Limit can range between 1 and 100 items.
end
78 changes: 78 additions & 0 deletions core/lib/opp_client/get_merchants_by_merchant_uuid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "https://docs.onlinepaymentplatform.com/#create-merchant",
"type": "object",
"properties": {
"uid": {
"type": "string",
"minLength": 1
},
"object": {
"type": "string",
"minLength": 1
},
"created": {
"type": "number"
},
"updated": {
"type": "number"
},
"status": {
"type": "string",
"minLength": 1
},
"compliance": {
"type": "object",
"properties": {
"level": {
"type": "number"
},
"status": {
"type": "string",
"minLength": 1
},
"overview_url": {
"type": "string",
"minLength": 1
}
},
"required": [
"level",
"status",
"overview_url"
]
},
"type": {
"type": "string",
"minLength": 1
},
"coc_nr": {
"type": "object",
"properties": {}
},
"name": {
"type": "string",
"minLength": 1
},
"phone": {
"type": "string",
"minLength": 1
},
"country": {
"type": "string",
"minLength": 1
}
},
"required": [
"uid",
"object",
"created",
"updated",
"status",
"compliance",
"type",
"name",
"phone",
"country"
]
}
96 changes: 96 additions & 0 deletions core/lib/opp_client/helper.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
defmodule OPPClient.Helper do
@path_param_re ~r/\{\{(.*)\}\}/

defmacro __using__(_opts) do
quote do
def get(client, path, params \\ [])
def post(client, path, params \\ [])

import OPPClient.Helper
end
end

defmacro def_req(method, path, schema \\ []) do
response_schema_name = get_response_schema_name(method, path)
positional_args = get_positional_args(path)

schema = get_finalized_schema(schema, method, positional_args)

response_schema_path = Path.join(__DIR__, "#{response_schema_name}.json")

response_schema =
response_schema_path
|> File.read!()
|> Jason.decode!()
|> ExJsonSchema.Schema.resolve()

quote do
# Ensures recompilation on schema file changes
@external_resource unquote(Path.relative_to_cwd(response_schema_path))

def unquote(method)(client, unquote(path), params) do
unquote(__MODULE__).request(
client,
unquote(method),
unquote(path),
unquote(positional_args),
unquote(Macro.escape(schema)),
unquote(Macro.escape(response_schema)),
params
)
end
end
end

def request(client, method, path, positional_args, schema, response_json_schema, params) do
path_with_args =
Regex.replace(@path_param_re, path, fn _, param ->
Keyword.fetch!(params, String.to_atom(param))
end)

{idempotency_key, body_params} =
params
|> Keyword.drop(positional_args)
|> Keyword.pop(:idempotency_key, [])

request =
%{client | method: method}
|> Req.Request.put_header("idempotency-key", idempotency_key)

with {:ok, _} <- NimbleOptions.validate(params, schema),
{:ok, response} <-
Req.request(request,
url: path_with_args,
json: Map.new(body_params)
),
:ok <- ExJsonSchema.Validator.validate(response_json_schema, response.body) do
{:ok, response.body}
end
end

def get_response_schema_name(method, path) do
name =
String.trim(path, "/")
|> String.replace(@path_param_re, "by_\\1", global: true)
|> String.replace("/", "_")

"#{method}_#{name}" |> String.to_atom()
end

def get_positional_args(path) do
Regex.scan(@path_param_re, path)
|> Enum.map(fn [_, param] -> String.to_atom(param) end)
end

def get_finalized_schema(schema, method, positional_args) do
schema =
schema ++ Enum.map(positional_args, fn arg -> {arg, [type: :string, required: true]} end)

# Require idempotency key on mutation
if method == :post do
[{:idempotency_key, [type: :string, required: true]} | schema]
else
schema
end
end
end
Loading

0 comments on commit a47b82c

Please sign in to comment.