From 961dfcb29d2fa45ddea54b566b068dec2d631433 Mon Sep 17 00:00:00 2001 From: Pankaj Rawat Date: Sat, 30 Dec 2017 01:37:05 +0530 Subject: [PATCH 01/60] Added project logo --- images/lg.png | Bin 0 -> 8163 bytes mix.exs | 12 +++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 images/lg.png diff --git a/images/lg.png b/images/lg.png new file mode 100644 index 0000000000000000000000000000000000000000..5c15b7a2eb0264ed4cd9aac5b272c7464d3fa40a GIT binary patch literal 8163 zcmdT}WmuF^lpcnW7KTPTrMnvmDH&2gkOs+-ZUzQL8b(U#P?7GG6e$7el7^vMLOS+y z|Lu?6efH-*^W1x9zPUB$yzhI?y%VJaQ+jRyjO9;&M;>jCX1P$qD&fZCo~gav4@ ztTk1YLHGY&xvd3Bz!O|IwHKa1tM;GeX`izk2*kjouKdKnZ+1T`Fo;a0zBeR=5?5V~ z@jVBYA-EdcjPaRc8d<_ax`exW=M6H(m;VWCq>SMw zq?#cvSAv5dEA+?v_t3sD8TLNbv|pLmy-=KI!_S6m0v~T)U!G#?Etq*XHHC~nFMvJA zX9jl)?64>}Vp4@Cgm?D|aJ~BgNtXV3J-jsgXu2_CVxx+yD%-T9h^iD1WaLLhVp?{_+tPolX|E!6tFjB98Js z9!`w6-yPGcInXEnTcXaT?1;(cf=5QMm}|lO53RXEO8Mz$T_r{@_DA}#tn;wFTwMm>%8B3Y5lCX;(!MWFrisyMUYiQsH`dW@) zkS{?^oRaI6-$8Yfh(yennJBSWa)gYEco~xqtgL1~*Aa3Iuuf2TRl^!j8vhRZw+t1g zN8fuJ4A=pYaosbWG`Y1WZ*x40F~+aa<*z_Xy$lvzw0x_HAp3nD5ro zJUG#vVPA=_ip787)2^laY^PCy&m{>wAU8-JDjEqDj*WQ9*NLuuSE0RsEV_dSx_zKH zFDtVKrq@X5nZPojeS8!2{@pg*WS5`l+geG_Nd?)>MhiXS!IKR5YbU{eg~z!&swX^@ z8A^PR?;*`aJVQQGoxm19KTQVO@>c&ki@Npy}CWzB89rJfA13P(!_M?!GvE{@^v2ybsI5grIyo5xo#^Hh*$6UeI1$9i4J44Uo<%yiootOE%?sR6tm5yLHE;GT??vXF#srA5 zmK`dqmU}1?N)wD3wy>O59X)-msy{_t_INpj6ho=ZD8|nHihCj0f8R~PuSMZ-3GXzG z6)O52v2H+TD2MK)F_;8D>N7_KR!iI_JY3^2E}Jfg-> zdV6zOILo2{^m5j>5nR#B9upmRv~({Ht?waZZn>$bnyHg<#z$^wc8v{YF zV*xQYpAfE;Shsd*n-v%Fa+~lnRHhp74)Yg&jxmW1LZpltUXt?+?Fv3BZ^NxPeMsXU zXKD)R0?F!wlpg7v^X8tzAaU*E{@v<5C79>?U>9c*0JibVe!0COxn%@}ze{RG<+L}U z_PtwaK1@wg1|4doP;eM4sP!J?;G2oIFp0e1RRN z8(8kumodXeWplQXFCQ7`d~Pn>37X-Bo$HknkN8RPyms*?a*Sr71*3IfJNdxH>tBN= zJ=e+$pKBk;VZ{3BJNwB+{P&{e|8~e;|4eWJymS zfiFgDu&1HGmrnV-I`n>R%BJ3wnoCU_5)Bdw)1ddj6sLz0QR%U?e{A zQ$^=x26@oHIB4D1gD}Sf}9IYm{>Gq6rIJ6Yp0h5$su<_WCxoO&?hp zxZ^(rtd5aH{DZjs*cX@KcUBXY z2)j@kQzV7^{%9U)$tXh~dBq0xZBTH~&UYRVy~10cJ|Br!{SAihpJa){C}#q>JFkU5nTXfVjl zkYcF>;OmRq{$jcOzLjfind9&S!yHZHy(nXuhUL%IVlq4FJ*ahZ2ag_O5e3JbdSfLi zy*up#jK~8*f=uvy>zK^qK1-uTrkv+?EEi6^7%J{eQ}o`fK1t`AGkcx0(;S;s9R5mR#;aR7b(zD~vecS*lhM(9S|K z3ikiURWf7#LrW?PR$h2$?*LT!%c-{z0Skt>^P{KvfO`Dz)xO)-2$Peuv;Hi>S5ovD zPTtg%p@@jcd-IC+L<(Z!(859vCUI1HI<=FNQ+41=9UbV8&Q29&<15+`1pqMgW{gD?CkDV6jh9j=qRhG zMCCFLE8Clgpt4$to_;1KCnsM!u&?wzTvWtaIl3v+H8+R7d-slmoBOSKh3d0s1Xu)A z$R}^HqL!B}FCP#m448U*i^AvX<6aGCYk^oBe*RR`(A4}~TFNuF`&aBlQ(e6(9X0VG zIa!$@_=4%VpI^N!On)Rf_wI;lFwzj`pdo@4u<-4}uejvblyC8#) zhez8q?9q^!NqhYCZrYVPF8tN6RI8JDtqie*QHSK`n|uqE$HyK42j4bn+Zw~dApdNc z?%K?lIaB|7!Rh?Td_6p)+kdKSdh`RYX}!eld(9|eHa3;BIA&%MadDf4CU4lJrmk*+ ze*MRfAIE>Z!>8lp7ZOSo5)yJT&oRhq)R)N~YG19l=_Q=m)J_+P@{-D0X!30u{suhR zS!fo&yP9s73VN-igqfa^Axq+Z|EH5^b-!WhOl`6K{`RoSbxE#aeW7aG6}8%n(qC#r zY#;S9_!gG}ZjTtAY4Ya&d$2%LVpQRC@iGe*k(^8xOC|VbC|h=Sx`sRW=14W*PY1Rg zL%aPjJpr9mGJq!dR7@wDmAU0~O!KsP=@zyvRn`AVC8iQ_?Sqnm0*mzeE+IB2BjB4u zS;?uXIAw;rDaVmGfYHCE3EMYi8o^+M|6KkSo5*A$ZPej3@^+t4MthS8^f(DOhJx2a z0u>&PWng49C)2}(!XUQlc=Mq4)gbkYUUoJ%CUNm;Uh?BHO@6D1LS2W$d2DR#RlsP* z-#plkPfmE_EgIdJo}%cr7Mgv1XA(SRS9{_*f&HigtN8>jPS^OKyh@i1tS_}$&|L!7 zD=I3gbsT@(VBWhm`uXTEmU5LY5wJsvE>sLJ=AF;`aiIo>#GNfx%_7IkFP-t#hbdg@MOt{nES}XitmN(`pGg7)c`p%{>lbjxXrc( ziUX7|cju|AtIKO@O0TZ2-bzrVuiEWu3=aG|X$1cu8FCjC6F5IM_U2@Bczk0pgOiD= zW8>1_-@kgdw62akDk^F^lYxOjJ1#EnwYzN-%l%-6xTYKpz*(d6aa*C4vW7)w(CP62 z|9}s;UyK>>$cB;!x(oM3N?YLh>w+%F!w%8ijXDc+rE6&f4A|WBMjXJqLY;V}^;^yF}%!uf){0=asbVJx}S-}YoPxgL(eFSX+L`%;_KBKPRLG6e@lE`jl$yRB&-7=HzvSnuA7lvI6B07W@q`mRVlIgi z2kfHz@+Hsw!hFkSc3>CaXnLETI#Z;g5PUOF)%F)9M%N`-*!I7!cq<!Y$iBRzL`JkerB9w}1>#SNz4_N&lLqwwsOG{(3L+<=zPA@L});2cK;WV$^wjK{W zy12VJ8RleX?`1{|Wl9>i2e)>f`;Fv2LH|kW5QNOnd|H8iLQfn&eK0gvvqt)9zBYBz0l;JUm8avj($gJ&RFy+o`s44 z3s|p{83B;Ia$3Zj!uk9HV4-xA_wI&XS`xc9#ra}jh04`tP6(mVuExW#@NnzP#?-E@**?pXnp|gii$sF@8jd^OVvmlDEMMj;sOAcQIqFUr0hIn^Z{E;4c;%Xi0FBM5~O8fXUE28-m+DsdCY}@ zfpM|*8C8ns1?X0)s56DAxVZF(idM{cT1o4K51E;zUI^n#Kn$A}mHg)54U!@>Px;_* zxZmC7riznO@!H+Qgh9~Rj6DqZbgeIm!jY66rtIKQ$X;Bw(CwNKyc#FoR2B=|AcsXn zSR{QN-_|iQ+DvfPdjq%>oun;C3`oMq`J9A=(CzU8X;z(PGyzVfj4gmLAK_qNT6LT! zK)|YL!es)GqxHdPAicM$r6m&(5!HV|^>>5YilX}ULMRkWBjy54daRW=FAp%$*4|#4 zM~5>of{+d`u=8=V&ptB~jkkxYnYVzGb!==tx7_T}F|uVA7jrc~YM{v--*%xl($ixe z5EQTVONK`nAS{}^`VRcD-eMqUNS)$tZi2dcdQ4(F)Ma@H2c?rk3(zko!=d1i>$xRm zWo1a2an%p&5J1RDNJ+PUCa^?CMO9tC{7qgk!SDa%2?h{{;3qPw^M@W-1IPbQLxRXH@=%l{zk!r!KWzrc%?JKN+?Zc43Cky53~pPuW=tf zJwG3-6hZjn%XNw6JE(D6(6_CzeEil6bPUC{DD}gGmJ`_8`udEQ@oejy@4S12iplHO ze0!oQDa98+_CzZiK(>gZ;aXrnoNax$*qVqrW>WT#SNze@(X8hd!@Ldi)(JkxYw99m zVgX$>oMGP=e3y6~8=6OU|9Z8u%ZiB^R!i<{xBU5mj|Rj)alT;G;m?rtVWXIx%XcIO zh2va3TuEI$YA_a?h(7k4=ce`D&1I^5h#Y<3o*+s(apPCo+fSdES=re97m{ch82Il_ za_-d*#vMecqpGPNE2`L zW`=896^=@5WTeim#5ZZj08~ZlJs~xK}BeL~j@5_(&@DJ`X2i z;5$29Iy}8p#QE~&i}4qGdwZMdax+4+%!wdnPHSuHuC6Fj-m#B-X3@g-LyZP`yVDiy zMQx2N+}IbGD7mXbKXeozE9d)j_I3CUMwyB@CBk}-pQR#!MAqNzVP9XLHL!V#?Zeh{ z^J^2nXrWiXs1gzqR!%R+ziQP`)nX$7t~Y2eB)xlCkO>8A%Uym^X@V7W-kvWVdY$+H zVT4t*^nGS#EP%{wL{dG=louHV#j8msu7O+M-LeL}qF4Zf|HAI|^z(QlL%xv90Si_e zF@1e~v%5N9Y~feGpnw{5EZRw>Ba2NBs#Gff)oZm@Yc+DB;lPoSVZaJ&9q%on)`JWi+e185B=Tf z&N5?KxdY^YuS;S95qFuX?*8%h%py>gV)&TsWMhzYO8FJ4NWVbi?gF$yG2l5Yt7=j< z_08MTfgUZsA~Ke7bml6)U?x4hD8TQBJ5sQ1TYWo>RQs| zCVd*`wLeRaO-Kv%_{g{avzH;nMgDAV@@ofh0_KpEoXMN^H!-2kl0(cojju$pXS)@1 zI+khQ0DCvD(aT~PG_i+Q)jQVBa+mD%IZkPadfs33%1aq60Y{?(AP+2X5lega^VhH1 z=C%9@S-HToQq4i_R&!V4@5=|T%9@WG>d=Ug_;q&?879qFZeHxPf$7?Ak`I+J2sr!!hc^DvFkDieu13Hb{Xc)YIl#>V=Vof`Ib~(D?oJEjU(BznCp{jm zwj0QY z1m8#uW=fLw#?z5zn=`#X(lXA()6>^qO&C@YxC1U-tqkx&e6oT1-`oCs%qWdZbEgLj zb-=&oY+fqHUba?Vc2YJTc0dCX5)c&U6%^tXk~9z$krI%U5|rc>5R(!RAo!RO@m~hO cU2Pp-`TzF=5dAFDzyOfC3QW0N(ellI00`@If&c&j literal 0 HcmV?d00001 diff --git a/mix.exs b/mix.exs index 261a72da..7cbefd8f 100644 --- a/mix.exs +++ b/mix.exs @@ -23,7 +23,8 @@ defmodule Gringotts.Mixfile do "coveralls.html": :test, "coveralls.travis": :test ], - deps: deps()] + deps: deps(), + docs: docs()] end # Configuration for the OTP application @@ -68,4 +69,13 @@ defmodule Gringotts.Mixfile do activemerchant ruby gem. """ end + + defp docs do + [ + main: "Gringotts", + logo: "images/lg.png", + source_url: "https://github.com/aviabird/gringotts" + ] + end + end From 90e8b3ecd0bba3a816d09f8e7a5783a1a13984b7 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Tue, 2 Jan 2018 11:50:57 +0530 Subject: [PATCH 02/60] Added version for inch_ex * Also re-ordered the deps list, groups runtime deps on top. --- mix.exs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mix.exs b/mix.exs index 7cbefd8f..14018e3d 100644 --- a/mix.exs +++ b/mix.exs @@ -50,14 +50,14 @@ defmodule Gringotts.Mixfile do [ {:poison, "~> 3.1.0"}, {:httpoison, "~> 0.13"}, + {:xml_builder, "~> 0.1.1"}, + {:elixir_xml_to_map, "~> 0.1"}, {:ex_doc, "~> 0.16", only: :dev, runtime: false}, {:mock, "~> 0.3.0", only: :test}, {:bypass, "~> 0.8", only: :test}, - {:xml_builder, "~> 0.1.1"}, - {:elixir_xml_to_map, "~> 0.1"}, {:excoveralls, "~> 0.7", only: :test}, {:credo, "~> 0.3", only: [:dev, :test]}, - {:inch_ex, only: :docs}, + {:inch_ex, "~> 0.5", only: :docs}, {:dialyxir, "~> 0.3", only: [:dev]} ] end From 907723be385ca343e2178396302066fd8277b3b6 Mon Sep 17 00:00:00 2001 From: JyotiGautam Date: Tue, 2 Jan 2018 18:25:55 +0530 Subject: [PATCH 03/60] test case added for network failure --- lib/gringotts/gateways/trexle.ex | 2 +- test/gateways/trexle_test.exs | 13 ++++++++++++- test/mocks/trexle_mock.exs | 4 ++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/gringotts/gateways/trexle.ex b/lib/gringotts/gateways/trexle.ex index 1bc315fb..4b46a252 100644 --- a/lib/gringotts/gateways/trexle.ex +++ b/lib/gringotts/gateways/trexle.ex @@ -186,7 +186,7 @@ defmodule Gringotts.Gateways.Trexle do @spec store(map, list) :: map def store(payment, opts \\ []) do - params = [email: @email]++card_params(payment) + params = [email: opts[:email]]++card_params(payment) commit(:post, "customers", params, opts) end diff --git a/test/gateways/trexle_test.exs b/test/gateways/trexle_test.exs index 146d5d05..ed821028 100644 --- a/test/gateways/trexle_test.exs +++ b/test/gateways/trexle_test.exs @@ -103,7 +103,7 @@ defmodule Gringotts.Gateways.TrexleTest do test "with valid card" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers, _options) -> MockResponse.test_for_authorize_with_valid_card end] do - {:ok, response} = Trexle.authorize(@amount, @invalid_card, @opts) + {:ok, response} = Trexle.authorize(@amount, @valid_card, @opts) assert response.status_code == 201 assert response.raw["response"]["success"] == true assert response.raw["response"]["captured"] == false @@ -195,4 +195,15 @@ defmodule Gringotts.Gateways.TrexleTest do end end end + + describe "network failure" do + test "with authorization" do + with_mock HTTPoison, + [request: fn(_method, _url, _body, _headers, _options) -> MockResponse.test_for_network_failure end] do + {:error, response} = Trexle.authorize(@amount, @valid_card, @opts) + assert response.success == false + assert response.reason == :network_fail? + end + end + end end diff --git a/test/mocks/trexle_mock.exs b/test/mocks/trexle_mock.exs index e169c078..1a75f19e 100644 --- a/test/mocks/trexle_mock.exs +++ b/test/mocks/trexle_mock.exs @@ -236,4 +236,8 @@ defmodule Gringotts.Gateways.TrexleMock do } } end + + def test_for_network_failure do + {:error, %HTTPoison.Error{id: nil, reason: :nxdomain}} + end end From 609a92c5d5f63d0ed104faca28f399f9177c1186 Mon Sep 17 00:00:00 2001 From: schneems Date: Tue, 2 Jan 2018 15:20:56 -0600 Subject: [PATCH 04/60] [ci skip] Get more Open Source Helpers [CodeTriage](https://www.codetriage.com/) is an app I have maintained for the past 4-5 years with the goal of getting people involved in Open Source projects like this one. The app sends subscribers a random open issue for them to help "triage". For some languages you can also suggested areas to add documentation. The initial approach was inspired by seeing the work of the small core team spending countless hours asking "what version was this in" and "can you give us an example app". The idea is to outsource these small interactions to a huge team of volunteers and let the core team focus on their work. I want to add a badge to the README of this project. The idea is to provide an easy link for people to get started contributing to this project. A badge indicates the number of people currently subscribed to help the repo. The color is based off of open issues in the project. Here are some examples of other projects that have a badge in their README: - https://github.com/crystal-lang/crystal - https://github.com/rails/rails - https://github.com/codetriage/codetriage Thanks for building open source software, I would love to help you find some helpers. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fb508bd4..eb0740a3 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@

Build Status Coverage Status Docs coverage + Help Contribute to Open Source

A simple and unified API to access dozens of different payment From 41f1de506d48fe82e3d2440b1823fe9fb9c9d411 Mon Sep 17 00:00:00 2001 From: sivagollapalli Date: Wed, 3 Jan 2018 13:06:35 +0530 Subject: [PATCH 05/60] Codeclimate fixes (#68) --- lib/gringotts/gateways/authorize_net.ex | 67 +++++++++++++------------ lib/gringotts/gateways/bogus.ex | 2 +- lib/gringotts/gateways/paymill.ex | 17 ++++--- lib/gringotts/gateways/stripe.ex | 6 ++- lib/gringotts/gateways/wire_card.ex | 39 +++++++------- 5 files changed, 70 insertions(+), 61 deletions(-) diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index 2f87243a..5a49644d 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -353,8 +353,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do @spec store(CreditCard.t, Keyword.t) :: tuple def store(card, opts) do request_data = cond do - opts[:customer_profile_id] -> create_customer_payment_profile(card, opts) |> generate - true -> create_customer_profile(card, opts) |> generate + opts[:customer_profile_id] -> card |> create_customer_payment_profile(opts) |> generate + true -> card |> create_customer_profile(opts) |> generate end response_data = commit(:post, request_data, opts) respond(response_data) @@ -374,7 +374,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do @spec unstore(String.t, Keyword.t) :: tuple def unstore(customer_profile_id, opts) do - request_data = delete_customer_profile(customer_profile_id, opts) |> generate + request_data = customer_profile_id |> delete_customer_profile(opts) |> generate response_data = commit(:post, request_data, opts) respond(response_data) end @@ -412,11 +412,11 @@ defmodule Gringotts.Gateways.AuthorizeNet do # Functions to send successful and error responses depending on message received # from gateway. - defp response_check( %{"messages" => %{"resultCode" => "Ok"}}, raw_response) do + defp response_check(%{"messages" => %{"resultCode" => "Ok"}}, raw_response) do {:ok, Response.success(raw: raw_response)} end - defp response_check( %{"messages" => %{"resultCode" => "Error"}}, raw_response) do + defp response_check(%{"messages" => %{"resultCode" => "Error"}}, raw_response) do {:error, Response.error(raw: raw_response)} end @@ -424,52 +424,55 @@ defmodule Gringotts.Gateways.AuthorizeNet do # function for formatting the request as an xml for purchase and authorize method defp add_auth_purchase(amount, payment, opts, transaction_type) do - element(:createTransactionRequest, %{xmlns: @aut_net_namespace}, [ - add_merchant_auth(opts[:config]), - add_order_id(opts), - add_purchase_transaction_request(amount, transaction_type, payment, opts), - ]) + :createTransactionRequest + |> element(%{xmlns: @aut_net_namespace}, [ + add_merchant_auth(opts[:config]), + add_order_id(opts), + add_purchase_transaction_request(amount, transaction_type, payment, opts), + ]) |> generate end # function for formatting the request for normal capture defp normal_capture(amount, id, opts, transaction_type) do - element(:createTransactionRequest, %{xmlns: @aut_net_namespace}, [ - add_merchant_auth(opts[:config]), - add_order_id(opts), - add_capture_transaction_request(amount, id, transaction_type, opts), - ]) + :createTransactionRequest + |> element(%{xmlns: @aut_net_namespace}, [ + add_merchant_auth(opts[:config]), + add_order_id(opts), + add_capture_transaction_request(amount, id, transaction_type, opts), + ]) |> generate end # function to format the request as an xml for the authenticate method defp add_auth_request(opts) do - element(:authenticateTestRequest, %{xmlns: @aut_net_namespace}, [ - add_merchant_auth(opts[:config]) - ]) + :authenticateTestRequest + |> element(%{xmlns: @aut_net_namespace}, [add_merchant_auth(opts[:config])]) |> generate end #function to format the request for normal refund defp normal_refund(amount, id, opts, transaction_type) do - element(:createTransactionRequest, %{xmlns: @aut_net_namespace}, [ - add_merchant_auth(opts[:config]), - add_order_id(opts), - add_refund_transaction_request(amount, id, opts, transaction_type), - ]) + :createTransactionRequest + |> element(%{xmlns: @aut_net_namespace}, [ + add_merchant_auth(opts[:config]), + add_order_id(opts), + add_refund_transaction_request(amount, id, opts, transaction_type), + ]) |> generate end #function to format the request for normal void operation defp normal_void(id, opts, transaction_type) do - element(:createTransactionRequest, %{xmlns: @aut_net_namespace}, [ - add_merchant_auth(opts[:config]), - add_order_id(opts), - element(:transactionRequest, [ - add_transaction_type(transaction_type), - add_ref_trans_id(id) - ]) - ]) + :createTransactionRequest + |> element(%{xmlns: @aut_net_namespace}, [ + add_merchant_auth(opts[:config]), + add_order_id(opts), + element(:transactionRequest, [ + add_transaction_type(transaction_type), + add_ref_trans_id(id) + ]) + ]) |> generate end @@ -501,7 +504,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do end defp delete_customer_profile(id, opts) do - element(:deleteCustomerProfileRequest, %{xmlns: @aut_net_namespace},[ + element(:deleteCustomerProfileRequest, %{xmlns: @aut_net_namespace}, [ add_merchant_auth(opts[:config]), element(:customerProfileId, id) ]) diff --git a/lib/gringotts/gateways/bogus.ex b/lib/gringotts/gateways/bogus.ex index f77178f5..14140f39 100644 --- a/lib/gringotts/gateways/bogus.ex +++ b/lib/gringotts/gateways/bogus.ex @@ -21,7 +21,7 @@ defmodule Gringotts.Gateways.Bogus do def refund(_amount, id, _opts), do: success(id) - def store(_card=%CreditCard{}, _opts), + def store(_card = %CreditCard{}, _opts), do: success() def unstore(customer_id, _opts), diff --git a/lib/gringotts/gateways/paymill.ex b/lib/gringotts/gateways/paymill.ex index b128e061..e70a491e 100644 --- a/lib/gringotts/gateways/paymill.ex +++ b/lib/gringotts/gateways/paymill.ex @@ -27,7 +27,7 @@ defmodule Gringotts.Gateways.Paymill do public_key: "your_public_key" """ use Gringotts.Gateways.Base - alias Gringotts.{ CreditCard, Address, Response} + alias Gringotts.{CreditCard, Address, Response} alias Gringotts.Gateways.Paymill.ResponseHandler, as: ResponseParser use Gringotts.Adapter, required_config: [:private_key, :public_key] @@ -165,7 +165,8 @@ defmodule Gringotts.Gateways.Paymill do end defp get_save_card_params(card, options) do - [ {"transaction.mode" , "CONNECTOR_TEST"}, + [ + {"transaction.mode" , "CONNECTOR_TEST"}, {"channel.id" , get_config(:public_key, options)}, {"jsonPFunction" , "jsonPFunction"}, {"account.number" , card.number}, @@ -194,7 +195,7 @@ defmodule Gringotts.Gateways.Paymill do defp parse_card_response(response) do response - |> String.replace(~r/jsonPFunction\(/,"") + |> String.replace(~r/jsonPFunction\(/, "") |> String.replace(~r/\)/, "") |> Poison.decode end @@ -208,7 +209,8 @@ defmodule Gringotts.Gateways.Paymill do end defp commit(method, action, parameters \\ nil, options) do - HTTPoison.request(method, @live_url <> action, {:form, parameters }, get_headers(options), []) + method + |> HTTPoison.request(@live_url <> action, {:form, parameters}, get_headers(options), []) |> ResponseParser.parse end @@ -216,6 +218,7 @@ defmodule Gringotts.Gateways.Paymill do get_in(options, [:config, key]) end + @moduledoc false defmodule ResponseHandler do alias Gringotts.Response @@ -341,7 +344,7 @@ defmodule Gringotts.Gateways.Paymill do defp set_success(opts, %{"error" => error}) do opts ++ [message: error, success: false] end - defp set_success(opts, %{"transaction" => %{ "response_code" => 20000}}) do + defp set_success(opts, %{"transaction" => %{"response_code" => 20_000}}) do opts ++ [success: true] end @@ -367,7 +370,7 @@ defmodule Gringotts.Gateways.Paymill do response_msg = Map.get(@response_code, response_code, -1) opts ++ [message: response_msg] end - defp parse_status_code(opts, %{ "transaction" => transaction}) do + defp parse_status_code(opts, %{"transaction" => transaction}) do response_code = Map.get(transaction, "response_code", -1) response_msg = Map.get(@response_code, response_code, -1) opts ++ [status_code: response_code, message: response_msg] @@ -381,7 +384,7 @@ defmodule Gringotts.Gateways.Paymill do defp parse_authorization(opts, %{"status" => "failed"}) do opts ++ [success: false] end - defp parse_authorization(opts, %{ "id" => id} = auth) do + defp parse_authorization(opts, %{"id" => id} = auth) do opts ++ [authorization: id] end diff --git a/lib/gringotts/gateways/stripe.ex b/lib/gringotts/gateways/stripe.ex index 3df0beac..b2321b35 100644 --- a/lib/gringotts/gateways/stripe.ex +++ b/lib/gringotts/gateways/stripe.ex @@ -300,7 +300,8 @@ defmodule Gringotts.Gateways.Stripe do defp card_params(%CreditCard{} = card) do card = Map.from_struct(card) - [ "card[name]": card[:name], + [ + "card[name]": card[:name], "card[number]": card[:number], "card[exp_year]": card[:year], "card[exp_month]": card[:month], @@ -313,7 +314,8 @@ defmodule Gringotts.Gateways.Stripe do defp address_params(%Address{} = address) do address = Map.from_struct(address) - [ "card[address_line1]": address[:street1], + [ + "card[address_line1]": address[:street1], "card[address_line2]": address[:street2], "card[address_city]": address[:city], "card[address_state]": address[:region], diff --git a/lib/gringotts/gateways/wire_card.ex b/lib/gringotts/gateways/wire_card.ex index 14134ed2..5aafeb37 100644 --- a/lib/gringotts/gateways/wire_card.ex +++ b/lib/gringotts/gateways/wire_card.ex @@ -77,7 +77,7 @@ defmodule Gringotts.Gateways.WireCard do test: true ] """ - @spec authorize(Integer | Float, CreditCard.t | String.t, Keyword) :: { :ok, Map } + @spec authorize(Integer | Float, CreditCard.t | String.t, Keyword) :: {:ok, Map} def authorize(money, payment_method, options \\ []) def authorize(money, %CreditCard{} = creditcard, options) do @@ -94,7 +94,7 @@ defmodule Gringotts.Gateways.WireCard do Capture - the first paramter here should be a GuWid/authorization. Authorization is obtained by authorizing the creditcard. """ - @spec capture(String.t, Float, Keyword) :: { :ok, Map } + @spec capture(String.t, Float, Keyword) :: {:ok, Map} def capture(authorization, money, options \\ []) when is_binary(authorization) do options = Keyword.put(options, :preauthorization, authorization) commit(:post, :capture, money, options) @@ -106,7 +106,7 @@ defmodule Gringotts.Gateways.WireCard do transaction. If a GuWID is given, rather than a CreditCard, then then the :recurring option will be forced to "Repeated" """ - @spec purchase(Float | Integer, CreditCard| String.t, Keyword) :: { :ok, Map } + @spec purchase(Float | Integer, CreditCard| String.t, Keyword) :: {:ok, Map} def purchase(money, payment_method, options \\ []) def purchase(money, %CreditCard{} = creditcard, options) do @@ -130,7 +130,7 @@ defmodule Gringotts.Gateways.WireCard do identification - The authorization string returned from the initial authorization or purchase. """ - @spec void(String.t, Keyword) :: { :ok, Map } + @spec void(String.t, Keyword) :: {:ok, Map} def void(identification, options \\ []) when is_binary(identification) do options = Keyword.put(options, :preauthorization, identification) commit(:post, :reversal, nil, options) @@ -146,7 +146,7 @@ defmodule Gringotts.Gateways.WireCard do as an Integer value in cents. identification -- GuWID """ - @spec refund(Float, String.t, Keyword) :: { :ok, Map } + @spec refund(Float, String.t, Keyword) :: {:ok, Map} def refund(money, identification, options \\ []) when is_binary(identification) do options = Keyword.put(options, :preauthorization, identification) commit(:post, :bookback, money, options) @@ -183,7 +183,7 @@ defmodule Gringotts.Gateways.WireCard do the returned authorization/GuWID usable in later transactions with +options[:recurring] = 'Repeated'+. """ - @spec store(CreditCard.t, Keyword) :: { :ok, Map } + @spec store(CreditCard.t, Keyword) :: {:ok, Map} def store(%CreditCard{} = creditcard, options \\ []) do options = options |> Keyword.put(:credit_card, creditcard) @@ -201,23 +201,23 @@ defmodule Gringotts.Gateways.WireCard do # Contact WireCard, make the XML request, and parse the # reply into a Response object. defp commit(method, action, money, options) do - #TODO: validate and setup address hash as per AM + # TODO: validate and setup address hash as per AM request = build_request(action, money, options) - headers = %{ "Content-Type" => "text/xml", - "Authorization" => encoded_credentials( - options[:config][:login], options[:config][:password] - ) - } + headers = %{"Content-Type" => "text/xml", + "Authorization" => encoded_credentials( + options[:config][:login], options[:config][:password] + ) + } method |> HTTPoison.request(base_url(options) , request, headers) |> respond end - defp respond({:ok, %{ status_code: 200, body: body}}) do + defp respond({:ok, %{status_code: 200, body: body}}) do response = parse(body) {:ok, response} end defp respond({:ok, %{body: body, status_code: status_code}}) do - { :error, "Some Error Occurred: \n #{ inspect body }" } + {:error, "Some Error Occurred: \n #{ inspect body }"} end # Read the XML message from the gateway and check if it was successful, @@ -298,7 +298,7 @@ defmodule Gringotts.Gateways.WireCard do element(:Zip, address[:zip]), add_state(address), element(:Country, address[:country]), - element(:Phone, (if regex_match(@valid_phone_format ,address[:phone]), do: address[:phone])), + element(:Phone, (if regex_match(@valid_phone_format, address[:phone]), do: address[:phone])), element(:Email, address[:email]) ]) ]) @@ -307,7 +307,7 @@ defmodule Gringotts.Gateways.WireCard do end defp add_state(address) do - if ( regex_match(~r/[A-Za-z]{2}/, address[:state]) && regex_match(~r/^(us|ca)$/i, address[:country]) + if (regex_match(~r/[A-Za-z]{2}/, address[:state]) && regex_match(~r/^(us|ca)$/i, address[:country]) ) do element(:State, (String.upcase(address[:state]))) end @@ -364,9 +364,10 @@ defmodule Gringotts.Gateways.WireCard do # Encode login and password in Base64 to supply as HTTP header # (for http basic authentication) defp encoded_credentials(login, password) do - join_string([login, password], ":") - |> Base.encode64 - |> (&( "Basic "<> &1)).() + [login, password] + |> join_string(":") + |> Base.encode64 + |> (&("Basic "<> &1)).() end defp join_string(list_of_words, joiner), do: Enum.join(list_of_words, joiner) From 2680289706986972ba712a8d9dba1a17065ccb3e Mon Sep 17 00:00:00 2001 From: sivagollapalli Date: Wed, 3 Jan 2018 13:07:00 +0530 Subject: [PATCH 06/60] FIX# code style for large numbers (#67) --- lib/gringotts/gateways/paymill.ex | 198 +++++++++++++++--------------- 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/lib/gringotts/gateways/paymill.ex b/lib/gringotts/gateways/paymill.ex index e70a491e..7a05bca1 100644 --- a/lib/gringotts/gateways/paymill.ex +++ b/lib/gringotts/gateways/paymill.ex @@ -223,105 +223,105 @@ defmodule Gringotts.Gateways.Paymill do alias Gringotts.Response @response_code %{ - 10001 => "Undefined response", - 10002 => "Waiting for something", - 11000 => "Retry request at a later time", - - 20000 => "Operation successful", - 20100 => "Funds held by acquirer", - 20101 => "Funds held by acquirer because merchant is new", - 20200 => "Transaction reversed", - 20201 => "Reversed due to chargeback", - 20202 => "Reversed due to money-back guarantee", - 20203 => "Reversed due to complaint by buyer", - 20204 => "Payment has been refunded", - 20300 => "Reversal has been canceled", - 22000 => "Initiation of transaction successful", - - 30000 => "Transaction still in progress", - 30100 => "Transaction has been accepted", - 31000 => "Transaction pending", - 31100 => "Pending due to address", - 31101 => "Pending due to uncleared eCheck", - 31102 => "Pending due to risk review", - 31103 => "Pending due regulatory review", - 31104 => "Pending due to unregistered/unconfirmed receiver", - 31200 => "Pending due to unverified account", - 31201 => "Pending due to non-captured funds", - 31202 => "Pending due to international account (accept manually)", - 31203 => "Pending due to currency conflict (accept manually)", - 31204 => "Pending due to fraud filters (accept manually)", - - 40000 => "Problem with transaction data", - 40001 => "Problem with payment data", - 40002 => "Invalid checksum", - 40100 => "Problem with credit card data", - 40101 => "Problem with CVV", - 40102 => "Card expired or not yet valid", - 40103 => "Card limit exceeded", - 40104 => "Card is not valid", - 40105 => "Expiry date not valid", - 40106 => "Credit card brand required", - 40200 => "Problem with bank account data", - 40201 => "Bank account data combination mismatch", - 40202 => "User authentication failed", - 40300 => "Problem with 3-D Secure data", - 40301 => "Currency/amount mismatch", - 40400 => "Problem with input data", - 40401 => "Amount too low or zero", - 40402 => "Usage field too long", - 40403 => "Currency not allowed", - 40410 => "Problem with shopping cart data", - 40420 => "Problem with address data", - 40500 => "Permission error with acquirer API", - 40510 => "Rate limit reached for acquirer API", - 42000 => "Initiation of transaction failed", - 42410 => "Initiation of transaction expired", - - 50000 => "Problem with back end", - 50001 => "Country blacklisted", - 50002 => "IP address blacklisted", - 50004 => "Live mode not allowed", - 50005 => "Insufficient permissions (API key)", - 50100 => "Technical error with credit card", - 50101 => "Error limit exceeded", - 50102 => "Card declined", - 50103 => "Manipulation or stolen card", - 50104 => "Card restricted", - 50105 => "Invalid configuration data", - 50200 => "Technical error with bank account", - 50201 => "Account blacklisted", - 50300 => "Technical error with 3-D Secure", - 50400 => "Declined because of risk issues", - 50401 => "Checksum was wrong", - 50402 => "Bank account number was invalid (formal check)", - 50403 => "Technical error with risk check", - 50404 => "Unknown error with risk check", - 50405 => "Unknown bank code", - 50406 => "Open chargeback", - 50407 => "Historical chargeback", - 50408 => "Institution / public bank account (NCA)", - 50409 => "KUNO/Fraud", - 50410 => "Personal Account Protection (PAP)", - 50420 => "Rejected due to acquirer fraud settings", - 50430 => "Rejected due to acquirer risk settings", - 50440 => "Failed due to restrictions with acquirer account", - 50450 => "Failed due to restrictions with user account", - 50500 => "General timeout", - 50501 => "Timeout on side of the acquirer", - 50502 => "Risk management transaction timeout", - 50600 => "Duplicate operation", - 50700 => "Cancelled by user", - 50710 => "Failed due to funding source", - 50711 => "Payment method not usable, use other payment method", - 50712 => "Limit of funding source was exceeded", - 50713 => "Means of payment not reusable (canceled by user)", - 50714 => "Means of payment not reusable (expired)", - 50720 => "Rejected by acquirer", - 50730 => "Transaction denied by merchant", - 50800 => "Preauthorisation failed", - 50810 => "Authorisation has been voided", - 50820 => "Authorisation period expired" + 10_001 => "Undefined response", + 10_002 => "Waiting for something", + 11_000 => "Retry request at a later time", + + 20_000 => "Operation successful", + 20_100 => "Funds held by acquirer", + 20_101 => "Funds held by acquirer because merchant is new", + 20_200 => "Transaction reversed", + 20_201 => "Reversed due to chargeback", + 20_202 => "Reversed due to money-back guarantee", + 20_203 => "Reversed due to complaint by buyer", + 20_204 => "Payment has been refunded", + 20_300 => "Reversal has been canceled", + 22_000 => "Initiation of transaction successful", + + 30_000 => "Transaction still in progress", + 30_100 => "Transaction has been accepted", + 31_000 => "Transaction pending", + 31_100 => "Pending due to address", + 31_101 => "Pending due to uncleared eCheck", + 31_102 => "Pending due to risk review", + 31_103 => "Pending due regulatory review", + 31_104 => "Pending due to unregistered/unconfirmed receiver", + 31_200 => "Pending due to unverified account", + 31_201 => "Pending due to non-captured funds", + 31_202 => "Pending due to international account (accept manually)", + 31_203 => "Pending due to currency conflict (accept manually)", + 31_204 => "Pending due to fraud filters (accept manually)", + + 40_000 => "Problem with transaction data", + 40_001 => "Problem with payment data", + 40_002 => "Invalid checksum", + 40_100 => "Problem with credit card data", + 40_101 => "Problem with CVV", + 40_102 => "Card expired or not yet valid", + 40_103 => "Card limit exceeded", + 40_104 => "Card is not valid", + 40_105 => "Expiry date not valid", + 40_106 => "Credit card brand required", + 40_200 => "Problem with bank account data", + 40_201 => "Bank account data combination mismatch", + 40_202 => "User authentication failed", + 40_300 => "Problem with 3-D Secure data", + 40_301 => "Currency/amount mismatch", + 40_400 => "Problem with input data", + 40_401 => "Amount too low or zero", + 40_402 => "Usage field too long", + 40_403 => "Currency not allowed", + 40_410 => "Problem with shopping cart data", + 40_420 => "Problem with address data", + 40_500 => "Permission error with acquirer API", + 40_510 => "Rate limit reached for acquirer API", + 42_000 => "Initiation of transaction failed", + 42_410 => "Initiation of transaction expired", + + 50_000 => "Problem with back end", + 50_001 => "Country blacklisted", + 50_002 => "IP address blacklisted", + 50_004 => "Live mode not allowed", + 50_005 => "Insufficient permissions (API key)", + 50_100 => "Technical error with credit card", + 50_101 => "Error limit exceeded", + 50_102 => "Card declined", + 50_103 => "Manipulation or stolen card", + 50_104 => "Card restricted", + 50_105 => "Invalid configuration data", + 50_200 => "Technical error with bank account", + 50_201 => "Account blacklisted", + 50_300 => "Technical error with 3-D Secure", + 50_400 => "Declined because of risk issues", + 50_401 => "Checksum was wrong", + 50_402 => "Bank account number was invalid (formal check)", + 50_403 => "Technical error with risk check", + 50_404 => "Unknown error with risk check", + 50_405 => "Unknown bank code", + 50_406 => "Open chargeback", + 50_407 => "Historical chargeback", + 50_408 => "Institution / public bank account (NCA)", + 50_409 => "KUNO/Fraud", + 50_410 => "Personal Account Protection (PAP)", + 50_420 => "Rejected due to acquirer fraud settings", + 50_430 => "Rejected due to acquirer risk settings", + 50_440 => "Failed due to restrictions with acquirer account", + 50_450 => "Failed due to restrictions with user account", + 50_500 => "General timeout", + 50_501 => "Timeout on side of the acquirer", + 50_502 => "Risk management transaction timeout", + 50_600 => "Duplicate operation", + 50_700 => "Cancelled by user", + 50_710 => "Failed due to funding source", + 50_711 => "Payment method not usable, use other payment method", + 50_712 => "Limit of funding source was exceeded", + 50_713 => "Means of payment not reusable (canceled by user)", + 50_714 => "Means of payment not reusable (expired)", + 50_720 => "Rejected by acquirer", + 50_730 => "Transaction denied by merchant", + 50_800 => "Preauthorisation failed", + 50_810 => "Authorisation has been voided", + 50_820 => "Authorisation period expired" } def parse({:ok, %HTTPoison.Response{body: body, status_code: 200}}) do From 6e402e925bb3b73d6e08f3cc6d4573520830020e Mon Sep 17 00:00:00 2001 From: Chandra Shekhar Date: Thu, 4 Jan 2018 16:12:47 +0530 Subject: [PATCH 07/60] Stripe gateway minor fixes and docs update (#75) * separate payment into credit card and address struct * set default currency if currency not passed in options * add guard clause when source params is token or customer * Change stripe docs according to new credit card and address struct * Updated Readme * Fixed specs --- README.md | 4 +- lib/gringotts/credit_card.ex | 6 +- lib/gringotts/gateways/stripe.ex | 127 +++++++++++++++++++------------ 3 files changed, 84 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index eb0740a3..12456ac1 100644 --- a/README.md +++ b/README.md @@ -57,13 +57,13 @@ Copy and paste this code in your module ```elixir alias Gringotts.Gateways.Stripe -alias Gringotts.{CreditCard, Address, Worker, Gateways} +alias Gringotts.{CreditCard, Address} card = %CreditCard{ first_name: "John", last_name: "Smith", number: "4242424242424242", - year: "2017", + year: "2022", month: "12", verification_code: "123" } diff --git a/lib/gringotts/credit_card.ex b/lib/gringotts/credit_card.ex index c2f53c8d..32a2a50c 100644 --- a/lib/gringotts/credit_card.ex +++ b/lib/gringotts/credit_card.ex @@ -43,5 +43,9 @@ defmodule Gringotts.CreditCard do Joins `first_name` and `last_name` with a space in between. """ @spec full_name(t) :: String.t - def full_name(card), do: "#{card.first_name} #{card.last_name}" + def full_name(card) do + name = "#{card.first_name} #{card.last_name}" + String.trim(name) + end + end diff --git a/lib/gringotts/gateways/stripe.ex b/lib/gringotts/gateways/stripe.ex index b2321b35..1a0d2b89 100644 --- a/lib/gringotts/gateways/stripe.ex +++ b/lib/gringotts/gateways/stripe.ex @@ -83,18 +83,29 @@ defmodule Gringotts.Gateways.Stripe do ## Example The following session shows how one would (pre) authorize a payment of $10 on a sample `card`. - iex> payment = %{ - expiration: {2018, 12}, number: "4242424242424242", cvc: "123", name: "John Doe", - street1: "123 Main", street2: "Suite 100", city: "New York", region: "NY", country: "US", + iex> card = %CreditCard{ + first_name: "John", + last_name: "Smith", + number: "4242424242424242", + year: "2017", + month: "12", + verification_code: "123" + } + + address = %Address{ + street1: "123 Main", + city: "New York", + region: "NY", + country: "US", postal_code: "11111" } - iex> opts = [currency: "usd"] + iex> opts = [currency: "usd", address: address] iex> amount = 10 - iex> Gringotts.authorize(Gringotts.Gateways.Stripe, amount, payment, opts) + iex> Gringotts.authorize(Gringotts.Gateways.Stripe, amount, card, opts) """ - @spec authorize(Float, Map, List) :: Map + @spec authorize(number, CreditCard.t() | String.t(), keyword) :: map def authorize(amount, payment, opts \\ []) do params = create_params_for_auth_or_purchase(amount, payment, opts, false) commit(:post, "charges", params, opts) @@ -110,18 +121,29 @@ defmodule Gringotts.Gateways.Stripe do The following session shows how one would process a payment in one-shot, without (pre) authorization. - iex> payemnt = %{ - expiration: {2018, 12}, number: "4242424242424242", cvc: "123", name: "John Doe", - street1: "123 Main", street2: "Suite 100", city: "New York", region: "NY", country: "US", + iex> card = %CreditCard{ + first_name: "John", + last_name: "Smith", + number: "4242424242424242", + year: "2017", + month: "12", + verification_code: "123" + } + + address = %Address{ + street1: "123 Main", + city: "New York", + region: "NY", + country: "US", postal_code: "11111" } - iex> opts = [currency: "usd"] + iex> opts = [currency: "usd", address: address] iex> amount = 5 - iex> Gringotts.purchase(Gringotts.Gateways.Stripe, amount, payment, opts) + iex> Gringotts.purchase(Gringotts.Gateways.Stripe, amount, card, opts) """ - @spec purchase(Float, Map, List) :: Map + @spec purchase(number, CreditCard.t() | String.t(), keyword) :: map def purchase(amount, payment, opts \\ []) do params = create_params_for_auth_or_purchase(amount, payment, opts) commit(:post, "charges", params, opts) @@ -147,7 +169,7 @@ defmodule Gringotts.Gateways.Stripe do iex> Gringotts.capture(Gringotts.Gateways.Stripe, id, amount, opts) """ - @spec capture(String.t, Float, List) :: Map + @spec capture(String.t(), number, keyword) :: map def capture(id, amount, opts \\ []) do params = optional_params(opts) ++ amount_params(amount) commit(:post, "charges/#{id}/capture", params, opts) @@ -180,7 +202,7 @@ defmodule Gringotts.Gateways.Stripe do iex> Gringotts.void(Gringotts.Gateways.Stripe, id, opts) """ - @spec void(String.t, List) :: Map + @spec void(String.t(), keyword) :: map def void(id, opts \\ []) do params = optional_params(opts) commit(:post, "charges/#{id}/refund", params, opts) @@ -202,7 +224,7 @@ defmodule Gringotts.Gateways.Stripe do iex> Gringotts.refund(Gringotts.Gateways.Stripe, amount, id, opts) """ - @spec refund(Float, String.t, List) :: Map + @spec refund(number, String.t(), keyword) :: map def refund(amount, id, opts \\ []) do params = optional_params(opts) ++ amount_params(amount) commit(:post, "charges/#{id}/refund", params, opts) @@ -218,17 +240,28 @@ defmodule Gringotts.Gateways.Stripe do The following session shows how one would store a card (a payment-source) for future use. - iex> payment = %{ - expiration: {2018, 12}, number: "4242424242424242", cvc: "123", name: "John Doe", - street1: "123 Main", street2: "Suite 100", city: "New York", region: "NY", country: "US", + iex> card = %CreditCard{ + first_name: "John", + last_name: "Smith", + number: "4242424242424242", + year: "2017", + month: "12", + verification_code: "123" + } + + address = %Address{ + street1: "123 Main", + city: "New York", + region: "NY", + country: "US", postal_code: "11111" } - iex> opts = [] + iex> opts = [address: address] - iex> Gringotts.store(Gringotts.Gateways.Stripe, payment, opts) + iex> Gringotts.store(Gringotts.Gateways.Stripe, card, opts) """ - @spec store(Map, List) :: Map + @spec store(CreditCard.t() | String.t(), keyword) :: map def store(payment, opts \\ []) do params = optional_params(opts) ++ source_params(payment, opts) commit(:post, "customers", params, opts) @@ -247,7 +280,7 @@ defmodule Gringotts.Gateways.Stripe do iex> Gringotts.unstore(Gringotts.Gateways.Stripe, id, opts) """ - @spec unstore(String.t) :: Map + @spec unstore(String.t()) :: map def unstore(id, opts \\ []), do: commit(:delete, "customers/#{id}", [], opts) # Private methods @@ -263,7 +296,7 @@ defmodule Gringotts.Gateways.Stripe do |> with_currency(params, opts[:config]) end - def with_currency(true, params, config), do: params + def with_currency(true, params, _), do: params def with_currency(false, params, config), do: [{:currency, config[:default_currency]} | params] defp create_card_token(params, opts) do @@ -272,6 +305,14 @@ defmodule Gringotts.Gateways.Stripe do defp amount_params(amount), do: [amount: money_to_cents(amount)] + defp source_params(token_or_customer, _) when is_binary(token_or_customer) do + [head, _] = String.split(token_or_customer, "_") + case head do + "tok" -> [source: token_or_customer] + "cus" -> [customer: token_or_customer] + end + end + defp source_params(%CreditCard{} = card, opts) do params = card_params(card) ++ @@ -283,44 +324,30 @@ defmodule Gringotts.Gateways.Stripe do true -> [] false -> response |> Map.get("id") - |> source_params - end - end - - defp source_params(token_or_customer) do - [head, _] = String.split(token_or_customer, "_") - case head do - "tok" -> [source: token_or_customer] - "cus" -> [customer: token_or_customer] + |> source_params(opts) end end - defp source_params(_, opts), do: [] + defp source_params(_, _), do: [] defp card_params(%CreditCard{} = card) do - card = Map.from_struct(card) - - [ - "card[name]": card[:name], - "card[number]": card[:number], - "card[exp_year]": card[:year], - "card[exp_month]": card[:month], - "card[cvc]": card[:verification_code] + [ "card[name]": CreditCard.full_name(card), + "card[number]": card.number, + "card[exp_year]": card.year, + "card[exp_month]": card.month, + "card[cvc]": card.verification_code ] end defp card_params(_), do: [] defp address_params(%Address{} = address) do - address = Map.from_struct(address) - - [ - "card[address_line1]": address[:street1], - "card[address_line2]": address[:street2], - "card[address_city]": address[:city], - "card[address_state]": address[:region], - "card[address_zip]": address[:postal_code], - "card[address_country]": address[:country] + [ "card[address_line1]": address.street1, + "card[address_line2]": address.street2, + "card[address_city]": address.city, + "card[address_state]": address.region, + "card[address_zip]": address.postal_code, + "card[address_country]": address.country ] end From ea113679cb473eb4b34cdcd6a74848afad914ba5 Mon Sep 17 00:00:00 2001 From: Jyoti Gautam Date: Thu, 4 Jan 2018 16:40:54 +0530 Subject: [PATCH 08/60] Using Address and Credit Card struct (#74) * Address and Credit Card struct usage --- lib/gringotts/gateways/trexle.ex | 142 ++++++++++++++++++++----------- test/gateways/trexle_test.exs | 51 ++++++----- 2 files changed, 123 insertions(+), 70 deletions(-) diff --git a/lib/gringotts/gateways/trexle.ex b/lib/gringotts/gateways/trexle.ex index 4b46a252..05f7d5c8 100644 --- a/lib/gringotts/gateways/trexle.ex +++ b/lib/gringotts/gateways/trexle.ex @@ -39,7 +39,7 @@ defmodule Gringotts.Gateways.Trexle do use Gringotts.Gateways.Base use Gringotts.Adapter, required_config: [:api_key, :default_currency] import Poison, only: [decode: 1] - alias Gringotts.{Response} + alias Gringotts.{Response, CreditCard, Address} @doc """ Performs the authorization of the card to be used for payment. @@ -50,26 +50,33 @@ defmodule Gringotts.Gateways.Trexle do ``` iex> amount = 100 - iex> card = %{ - name: "John Doe", + iex> card = %CreditCard{ number: "5200828282828210", - expiry_month: 1, - expiry_year: 2018, - cvc: "123", - address_line1: "456 My Street", - address_city: "Ottawa", - address_postcode: "K1C2N6", - address_state: "ON", - address_country: "CA" + month: 12, + year: 2018, + first_name: "John", + last_name: "Doe", + verification_code: "123", + brand: "visa" } - iex> options = [email: "john@trexle.com", ip_address: "66.249.79.118" , description: "Store Purchase 1437598192"] + iex> address = %Address{ + street1: "123 Main", + street2: "Suite 100", + city: "New York", + region: "NY", + country: "US", + postal_code: "11111", + phone: "(555)555-5555" + } + + iex> options = [email: "john@trexle.com", ip_address: "66.249.79.118", billing_address: address, description: "Store Purchase 1437598192"] iex> Gringotts.authorize(:payment_worker, Gringotts.Gateways.Trexle, amount, card, options) ``` """ - @spec authorize(float, map, list) :: map + @spec authorize(float, CreditCard.t, list) :: map def authorize(amount, payment, opts \\ []) do params = create_params_for_auth_or_purchase(amount, payment, opts, false) commit(:post, "charges", params, opts) @@ -82,20 +89,39 @@ defmodule Gringotts.Gateways.Trexle do ## Example ``` - iex> card = %{ - name: "John Doe", + iex> card = %CreditCard{ number: "5200828282828210", - expiry_month: 1, - expiry_year: 2018, - cvc: "123", - address_line1: "456 My Street", - address_city: "Ottawa", - address_postcode: "K1C2N6", - address_state: "ON", - address_country: "CA" + month: 12, + year: 2018, + first_name: "John", + last_name: "Doe", + verification_code: "123", + brand: "visa" + } + + iex> address = %Address{ + street1: "123 Main", + street2: "Suite 100", + city: "New York", + region: "NY", + country: "US", + postal_code: "11111", + phone: "(555)555-5555" + } + + iex> options = [email: "john@trexle.com", ip_address: "66.249.79.118" ,billing_address: address, description: "Store Purchase 1437598192"] + + iex> @address %Address{ + street1: "123 Main", + street2: "Suite 100", + city: "New York", + region: "NY", + country: "US", + postal_code: "11111", + phone: "(555)555-5555" } - iex> options = [email: "john@trexle.com", ip_address: "66.249.79.118" ,description: "Store Purchase 1437598192"] + iex> options = [email: "john@trexle.com", ip_address: "66.249.79.118" ,billing_address: @address, description: "Store Purchase 1437598192"] iex> amount = 50 @@ -103,7 +129,7 @@ defmodule Gringotts.Gateways.Trexle do ``` """ - @spec purchase(float, map, list) :: map + @spec purchase(float, CreditCard.t, list) :: map def purchase(amount, payment, opts \\ []) do params = create_params_for_auth_or_purchase(amount, payment, opts) commit(:post, "charges", params, opts) @@ -165,28 +191,37 @@ defmodule Gringotts.Gateways.Trexle do ## Example The following session shows how one would store a card (a payment-source) for future use. ``` - iex> card = %{ - name: "John Doe", + iex> card = %CreditCard{ number: "5200828282828210", - expiry_month: 1, - expiry_year: 2018, - cvc: "123", - address_line1: "456 My Street", - address_city: "Ottawa", - address_postcode: "K1C2N6", - address_state: "ON", - address_country: "CA" + month: 12, + year: 2018, + first_name: "John", + last_name: "Doe", + verification_code: "123", + brand: "visa" } - iex> options = [email: "john@trexle.com", ip_address: "66.249.79.118", description: "Store Purchase 1437598192"] + iex> address = %Address{ + street1: "123 Main", + street2: "Suite 100", + city: "New York", + region: "NY", + country: "US", + postal_code: "11111", + phone: "(555)555-5555" + } + + iex> options = [email: "john@trexle.com", ip_address: "66.249.79.118", billing_address: address, description: "Store Purchase 1437598192"] iex> Gringotts.store(:payment_worker, Gringotts.Gateways.Trexle, card, options) ``` """ - @spec store(map, list) :: map + @spec store(CreditCard.t, list) :: map def store(payment, opts \\ []) do - params = [email: opts[:email]]++card_params(payment) + params = [email: opts[:email]] + ++ card_params(payment) + ++ address_params(opts[:billing_address]) commit(:post, "customers", params, opts) end @@ -199,24 +234,31 @@ defmodule Gringotts.Gateways.Trexle do ip_address: opts[:ip_address], description: opts[:description] ] ++ card_params(payment) + ++ address_params(opts[:billing_address]) + end + + defp card_params(%CreditCard{} = card) do + [ + "card[name]": CreditCard.full_name(card), + "card[number]": card.number, + "card[expiry_year]": card.year, + "card[expiry_month]": card.month, + "card[cvc]": card.verification_code + ] end - defp card_params(%{} = card) do + defp address_params(%Address{} = address) do [ - "card[name]": card[:name], - "card[number]": card[:number], - "card[expiry_year]": card[:expiry_year], - "card[expiry_month]": card[:expiry_month], - "card[cvc]": card[:cvc], - "card[address_line1]": card[:address_line1], - "card[address_city]": card[:address_city], - "card[address_postcode]": card[:address_postcode], - "card[address_state]": card[:address_state], - "card[address_country]": card[:address_country] + "card[address_line1]": address.street1, + "card[address_line2]": address.street2, + "card[address_city]": address.city, + "card[address_postcode]": address.postal_code, + "card[address_state]": address.region, + "card[address_country]": address.country ] end - defp commit(method, path, params \\ [], opts \\ []) do + defp commit(method, path, params, opts) do auth_token = "Basic #{Base.encode64(opts[:config][:api_key])}" headers = [{"Content-Type", "application/x-www-form-urlencoded"}, {"Authorization", auth_token}] data = params_to_string(params) diff --git a/test/gateways/trexle_test.exs b/test/gateways/trexle_test.exs index ed821028..4e2bebeb 100644 --- a/test/gateways/trexle_test.exs +++ b/test/gateways/trexle_test.exs @@ -4,33 +4,41 @@ defmodule Gringotts.Gateways.TrexleTest do use ExUnit.Case, async: false alias Gringotts.Gateways.TrexleMock, as: MockResponse alias Gringotts.Gateways.Trexle + alias Gringotts.{ + CreditCard, + Address + } import Mock - @valid_card %{ - name: "John Doe", + @valid_card %CreditCard{ number: "5200828282828210", - expiry_month: 1, - expiry_year: 2018, - cvc: "123", - address_line1: "456 My Street", - address_city: "Ottawa", - address_postcode: "K1C2N6", - address_state: "ON", - address_country: "CA" + month: 12, + year: 2018, + first_name: "John", + last_name: "Doe", + verification_code: "123", + brand: "visa" } - @invalid_card %{ - name: "John Doe", + @invalid_card %CreditCard{ number: "5200828282828210", - expiry_month: 1, - expiry_year: 2010, - cvc: "123", - address_line1: "456 My Street", - address_city: "Ottawa", - address_postcode: "K1C2N6", - address_state: "ON", - address_country: "CA" + month: 12, + year: 2010, + first_name: "John", + last_name: "Doe", + verification_code: "123", + brand: "visa" + } + + @address %Address{ + street1: "123 Main", + street2: "Suite 100", + city: "New York", + region: "NY", + country: "US", + postal_code: "11111", + phone: "(555)555-5555" } @amount 100 @@ -42,6 +50,7 @@ defmodule Gringotts.Gateways.TrexleTest do @opts [ config: %{api_key: "J5RGMpDlFlTfv9mEFvNWYoqHufyukPP4", default_currency: "USD"}, email: "john@trexle.com", + billing_address: @address, ip_address: "66.249.79.118", description: "Store Purchase 1437598192" ] @@ -49,12 +58,14 @@ defmodule Gringotts.Gateways.TrexleTest do @missingip_opts [ config: %{api_key: "J5RGMpDlFlTfv9mEFvNWYoqHufyukPP4", default_currency: "USD"}, email: "john@trexle.com", + billing_address: @address, description: "Store Purchase 1437598192" ] @invalid_opts [ config: %{api_key: "J5RGMpDlFlTfv9mEFvNWYoqHufyukPP4"}, email: "john@trexle.com", + billing_address: @address, ip_address: "66.249.79.118", description: "Store Purchase 1437598192" ] From df447535a4b9cdb4bc158e55069e4e9e0d30a063 Mon Sep 17 00:00:00 2001 From: gopalshimpi Date: Thu, 4 Jan 2018 22:18:27 +0530 Subject: [PATCH 09/60] Cams : Added error response handling (#65) --- lib/gringotts/gateways/cams.ex | 72 ++++++++++++++++++++++++++++++++-- test/gateways/cams_test.exs | 21 ++++++++-- test/mocks/cams_mock.exs | 28 ++++++++++++- 3 files changed, 112 insertions(+), 9 deletions(-) diff --git a/lib/gringotts/gateways/cams.ex b/lib/gringotts/gateways/cams.ex index eeb70227..b86c2b6b 100644 --- a/lib/gringotts/gateways/cams.ex +++ b/lib/gringotts/gateways/cams.ex @@ -231,6 +231,35 @@ defmodule Gringotts.Gateways.Cams do commit("void", post, options) end + @doc """ + Validates the Account + + This action is used for doing an "Account Verification" on the cardholder's credit card + without actually doing an authorization. + + ## Examples + payment = %{ + number: "4111111111111111", month: 11, year: 2018, + first_name: "Longbob", last_name: "Longsen", + verification_code: "123", brand: "visa" + } + + options = [currency: "USD"] + + + iex> Gringotts.validate(Gringotts.Gateways.Cams, payment, options) + + """ + @spec validate(CreditCard.t, Keyword):: Response + def validate(payment, options) do + post = [] + |> add_invoice(0, options) + |> add_payment(payment) + |> add_address(payment, options) + + commit("verify", post, options) + end + # private methods defp add_invoice(post, money, options) do @@ -295,7 +324,8 @@ defmodule Gringotts.Gateways.Cams do @doc false def parse({:ok, %HTTPoison.Response{body: body, status_code: 200}}) do body = URI.decode_query(body) - [] + + [status_code: 200] |> set_authorization(body) |> set_success(body) |> set_message(body) @@ -304,6 +334,27 @@ defmodule Gringotts.Gateways.Cams do |> handle_opts() end + def parse({:ok, %HTTPoison.Response{body: body, status_code: 400}}) do + body = URI.decode_query(body) + set_params([status_code: 400], body) + end + + def parse({:ok, %HTTPoison.Response{body: body, status_code: 404}}) do + body = URI.decode_query(body) + + [status_code: 404] + |> handle_not_found(body) + |> handle_opts() + end + + def parse({:error, %HTTPoison.Error{} = error}) do + [ + message: "HTTPoison says #{error.reason}", + error_code: error.id, + success: false + ] + end + defp set_authorization(opts, %{"transactionid" => id}) do opts ++ [authorization: id] end @@ -323,10 +374,23 @@ defmodule Gringotts.Gateways.Cams do defp set_success(opts, %{"response_code" => response_code}) do opts ++ [success: response_code == "100"] end - - defp handle_opts(opts) do - {:ok, Response.success(opts)} + + defp handle_not_found(opts, body) do + error = parse_html(body) + opts ++ [success: false, message: error] end + defp parse_html(body) do + error_message = List.to_string(Map.keys(body)) + [html_body | parse_message] = (Regex.run(~r|(.*)|, error_message)) + List.to_string(parse_message) + end + + defp handle_opts(opts) do + case Keyword.fetch(opts, :success) do + {:ok, true} -> {:ok, Response.success(opts)} + {:ok, false} -> {:ok, Response.error(opts)} + end + end end end diff --git a/test/gateways/cams_test.exs b/test/gateways/cams_test.exs index 85024543..694695e8 100644 --- a/test/gateways/cams_test.exs +++ b/test/gateways/cams_test.exs @@ -56,7 +56,6 @@ defmodule Gringotts.Gateways.CamsTest do @bad_authorization "300000000" describe "purchase" do - test "with all good" do with_mock HTTPoison, [post: fn(_url, _body, _headers) -> MockResponse.successful_purchase end] do @@ -106,8 +105,14 @@ defmodule Gringotts.Gateways.CamsTest do assert String.contains?(result, "Invalid Credit Card Number") end end + test "with bad amount" do + with_mock HTTPoison, + [post: fn(_url, _body, _headers) -> MockResponse.failed_purchase_with_bad_money end] do + {:ok, %Response{message: result}} = Gateway.authorize(@bad_money, @payment, @options) + assert String.contains?(result, "Invalid amount") + end + end end - describe "capture" do test "with full amount" do with_mock HTTPoison, @@ -148,7 +153,6 @@ defmodule Gringotts.Gateways.CamsTest do assert String.contains?(result, "A capture requires that") end end - end describe "refund" do @@ -186,5 +190,14 @@ defmodule Gringotts.Gateways.CamsTest do end end end - + + describe "validate" do + test "with all good" do + with_mock HTTPoison, + [post: fn(_url, _body, _headers) -> MockResponse.validate_creditcard end] do + {:ok, %Response{success: result}} = Gateway.validate(@payment, @options) + assert result + end + end + end end diff --git a/test/mocks/cams_mock.exs b/test/mocks/cams_mock.exs index 294959e9..d7ee7808 100644 --- a/test/mocks/cams_mock.exs +++ b/test/mocks/cams_mock.exs @@ -40,6 +40,19 @@ defmodule Gringotts.Gateways.CamsMock do status_code: 200}} end + def failed_purchase_with_bad_credit_card do + {:ok, + %HTTPoison.Response{ + body: "response=3&responsetext=Invalid Credit Card Number REFID:3502947912&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=sale&response_code=300", + headers: [ + {"Date", "Thu, 21 Dec 2017 13:20:08 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "155"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200}} + end def with_invalid_currency do {:ok, %HTTPoison.Response{ @@ -186,7 +199,20 @@ defmodule Gringotts.Gateways.CamsMock do {"Content-Type", "text/html; charset=UTF-8"} ], request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} + status_code: 200}} end + def validate_creditcard do + {:ok, + %HTTPoison.Response{ + body: "response=1&responsetext=&authcode=&transactionid=3933708264&avsresponse=&cvvresponse=&orderid=&type=verify&response_code=100", + headers: [ + {"Date", "Thu, 04 Jan 2018 11:12:20 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "124"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200}} + end end From afdd12eebda94aee60f97b43a7acdc507650a3af Mon Sep 17 00:00:00 2001 From: arjun289 Date: Mon, 25 Dec 2017 22:57:38 +0530 Subject: [PATCH 10/60] test authorize net Added tests for authorize net library for the functions authorize, capture, refund, void, store and unstore. Added mock responses for the authorize net library for functions authorize, capture, refund, void, store and unstore. Added ResponseHandler module to parse gateway responses. Added specs. --- lib/gringotts/gateways/authorize_net.ex | 194 ++++++++++++++++-------- lib/gringotts/response.ex | 2 +- test/gateways/authorize_net_test.exs | 149 +++++++++++------- test/mocks/authorize_net_mock.exs | 23 ++- 4 files changed, 250 insertions(+), 118 deletions(-) diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index 5a49644d..149cd74c 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -86,6 +86,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do use Gringotts.Gateways.Base use Gringotts.Adapter, required_config: [:name, :transaction_key] + alias Gringotts.Gateways.AuthorizeNet.ResponseHandler + @test_url "https://apitest.authorize.net/xml/v1/request.api" @production_url "https://api.authorize.net/xml/v1/request.api" @header [{"Content-Type", "text/xml"}] @@ -98,20 +100,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do void: "voidTransaction" } - @response_type %{ - auth_response: "authenticateTestResponse", - transaction_response: "createTransactionResponse", - error_response: "ErrorResponse", - customer_profile_response: "createCustomerProfileResponse", - customer_payment_profile_response: "createCustomerPaymentProfileResponse", - delete_customer_profile: "deleteCustomerProfileResponse" - } - @aut_net_namespace "AnetApi/xml/v1/schema/AnetApiSchema.xsd" alias Gringotts.{ CreditCard, - Address, Response } @@ -163,9 +155,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> amount = 5 iex> result = Gringotts.purchase(Gringotts.Gateways.AuthorizeNet, amount, card, opts) """ - @spec purchase(Float, CreditCard.t, Keyword.t) :: tuple + @spec purchase(float, CreditCard.t, Keyword.t) :: {:ok | :error, Response.t} def purchase(amount, payment, opts) do - request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:purchase]) + request_data = + add_auth_purchase(amount, payment, opts, @transaction_type[:purchase]) response_data = commit(:post, request_data, opts) respond(response_data) end @@ -216,9 +209,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> amount = 5 iex> result = Gringotts.authorize(Gringotts.Gateways.AuthorizeNet, amount, card, opts) """ - @spec authorize(Float, CreditCard.t, Keyword.t) :: tuple + @spec authorize(float, CreditCard.t, Keyword.t) :: {:ok | :error, Response.t} def authorize(amount, payment, opts) do - request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:authorize]) + request_data = + add_auth_purchase(amount, payment, opts, @transaction_type[:authorize]) response_data = commit(:post, request_data, opts) respond(response_data) end @@ -232,7 +226,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do * `refund/3` a settled transaction. * `void/2` a transaction. - ## Quirks + ## Notes * If a `capture` transaction needs to `void` then it should be done before it is settled. For AuthorieNet all the transactions are settled after 24 hours. @@ -253,7 +247,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> id = "123456" iex> result = Gringotts.capture(Gringotts.Gateways.AuthorizeNet, id, amount, opts) """ - @spec capture(String.t, Float, Keyword.t) :: tuple + @spec capture(String.t, float, Keyword.t) :: {:ok | :error, Response.t} def capture(id, amount, opts) do request_data = normal_capture(amount, id, opts, @transaction_type[:capture]) response_data = commit(:post, request_data, opts) @@ -283,7 +277,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> amount = 5 iex> result = Gringotts.refund(Gringotts.Gateways.AuthorizeNet, amount, id, opts) """ - @spec refund(Float, String.t, Keyword.t) :: tuple + @spec refund(float, String.t, Keyword.t) :: {:ok | :error, Response.t} def refund(amount, id, opts) do request_data = normal_refund(amount, id, opts, @transaction_type[:refund]) response_data = commit(:post, request_data, opts) @@ -307,7 +301,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> id = "123456" iex> result = Gringotts.void(Gringotts.Gateways.AuthorizeNet, id, opts) """ - @spec void(String.t, Keyword.t) :: tuple + @spec void(String.t, Keyword.t) :: {:ok | :error, Response.t} def void(id, opts) do request_data = normal_void(id, opts, @transaction_type[:void]) response_data = commit(:post, request_data, opts) @@ -322,8 +316,9 @@ defmodule Gringotts.Gateways.AuthorizeNet do can create customer payment profile by passing the `customer_profile_id` in the `opts`. The gateway also provide a provision for a `validation mode`, there are two modes `liveMode` and `testMode`, to know more about modes [see](https://developer.authorize.net/api/reference/index.html#customer-profiles-create-customer-profile). + By default `validation mode` is set to `testMode`. - ## Quirks + ## Notes * The current version of this library supports only `credit card` as the payment profile. * If a customer profile is created without the card info, then to create a payment profile `card` info needs to be passed alongwith `cutomer_profile_id` to create it. @@ -335,7 +330,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ## Optional Fields opts = [ validation_mode: String, - billTo: %{ + bill_to: %{ first_name: String, last_name: String, company: String, address: String, city: String, state: String, zip: String, country: String }, @@ -350,11 +345,12 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> card = %CreditCard{number: "5424000000000015", year: 2020, month: 12, verification_code: "999"} iex> result = Gringotts.store(Gringotts.Gateways.AuthorizeNet, card, opts) """ - @spec store(CreditCard.t, Keyword.t) :: tuple + @spec store(CreditCard.t, Keyword.t) :: {:ok | :error, Response.t} def store(card, opts) do - request_data = cond do - opts[:customer_profile_id] -> card |> create_customer_payment_profile(opts) |> generate - true -> card |> create_customer_profile(opts) |> generate + request_data = if opts[:customer_profile_id] do + card |> create_customer_payment_profile(opts) |> generate + else + card |> create_customer_profile(opts) |> generate end response_data = commit(:post, request_data, opts) respond(response_data) @@ -372,7 +368,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> result = Gringotts.store(Gringotts.Gateways.AuthorizeNet, id, opts) """ - @spec unstore(String.t, Keyword.t) :: tuple + @spec unstore(String.t, Keyword.t) :: {:ok | :error, Response.t} def unstore(customer_profile_id, opts) do request_data = customer_profile_id |> delete_customer_profile(opts) |> generate response_data = commit(:post, request_data, opts) @@ -386,38 +382,31 @@ defmodule Gringotts.Gateways.AuthorizeNet do HTTPoison.request(method, path, payload, headers) end - # Function to return a response + # Function to return a response defp respond({:ok, %{body: body, status_code: 200}}) do raw_response = naive_map(body) - cond do - raw_response[@response_type[:auth_response]] -> - response_check(raw_response[@response_type[:auth_response]], raw_response) - raw_response[@response_type[:transaction_response]] -> - response_check(raw_response[@response_type[:transaction_response]], raw_response) - raw_response[@response_type[:error_response]] -> - response_check(raw_response[@response_type[:error_response]], raw_response) - raw_response[@response_type[:customer_profile_response]] -> - response_check(raw_response[@response_type[:customer_profile_response]], raw_response) - raw_response[@response_type[:customer_payment_profile_response]] -> - response_check(raw_response[@response_type[:customer_payment_profile_response]], raw_response) - raw_response[@response_type[:delete_customer_profile]] -> - response_check(raw_response[@response_type[:delete_customer_profile]], raw_response) - end + response_type = ResponseHandler.check_response_type(raw_response) + response_check(raw_response[response_type], raw_response) + end + + defp respond({:ok, %{body: body, status_code: code}}) do + {:error, Response.error(params: body, error_code: code)} end - defp respond({:error, %{body: body, status_code: code}}) do - {:error, Response.error(raw: body, code: code)} + defp respond({:error, %HTTPoison.Error{} = error}) do + IO.inspect error + {:error, Response.error(error_code: error.id, message: "HTTPoison says '#{error.reason}'")} end - + # Functions to send successful and error responses depending on message received # from gateway. defp response_check(%{"messages" => %{"resultCode" => "Ok"}}, raw_response) do - {:ok, Response.success(raw: raw_response)} + {:ok, ResponseHandler.parse_gateway_success(raw_response)} end defp response_check(%{"messages" => %{"resultCode" => "Error"}}, raw_response) do - {:error, Response.error(raw: raw_response)} + {:error, ResponseHandler.parse_gateway_error(raw_response)} end #------------------- Helper functions for the interface functions------------------- @@ -432,7 +421,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ]) |> generate end - + # function for formatting the request for normal capture defp normal_capture(amount, id, opts, transaction_type) do :createTransactionRequest @@ -444,13 +433,6 @@ defmodule Gringotts.Gateways.AuthorizeNet do |> generate end - # function to format the request as an xml for the authenticate method - defp add_auth_request(opts) do - :authenticateTestRequest - |> element(%{xmlns: @aut_net_namespace}, [add_merchant_auth(opts[:config])]) - |> generate - end - #function to format the request for normal refund defp normal_refund(amount, id, opts, transaction_type) do :createTransactionRequest @@ -484,7 +466,9 @@ defmodule Gringotts.Gateways.AuthorizeNet do add_billing_info(opts), add_payment_source(card) ]), - element(:validationMode, opts[:validation_mode]) + element(:validationMode, + (if opts[:validation_mode], do: opts[:validation_mode], else: "testMode") + ) ]) end @@ -496,10 +480,16 @@ defmodule Gringotts.Gateways.AuthorizeNet do element(:description, opts[:profile][:description]), element(:email, opts[:profile][:description]), element(:paymentProfiles, [ - element(:customerType, (if opts[:customer_type], do: opts[:customer_type], else: "individual")), + element(:customerType, + (if opts[:customer_type], do: opts[:customer_type], else: "individual") + ), + add_billing_info(opts), add_payment_source(card) - ]) - ]) + ]), + ]), + element(:validationMode, + (if opts[:validation_mode], do: opts[:validation_mode], else: "testMode") + ) ]) end @@ -553,7 +543,9 @@ defmodule Gringotts.Gateways.AuthorizeNet do element(:payment, [ element(:creditCard, [ element(:cardNumber, opts[:payment][:card][:number]), - element(:expirationDate, join_string([opts[:payment][:card][:year], opts[:payment][:card][:month]], "-")) + element(:expirationDate, + join_string([opts[:payment][:card][:year], opts[:payment][:card][:month]], "-") + ) ]) ]), add_ref_trans_id(id) @@ -688,11 +680,87 @@ defmodule Gringotts.Gateways.AuthorizeNet do end defp base_url(opts) do - cond do - opts[:config][:mode] == :prod -> @production_url - opts[:config][:mode] == :test -> @test_url - true -> @test_url - end + if opts[:config][:mode] == :prod do + @production_url + else + @test_url + end end + defmodule ResponseHandler do + @moduledoc false + alias Gringotts.Response + + @response_type %{ + auth_response: "authenticateTestResponse", + transaction_response: "createTransactionResponse", + error_response: "ErrorResponse", + customer_profile_response: "createCustomerProfileResponse", + customer_payment_profile_response: "createCustomerPaymentProfileResponse", + delete_customer_profile: "deleteCustomerProfileResponse" + } + + def parse_gateway_success(raw_response) do + response_type = check_response_type(raw_response) + message = raw_response[response_type]["messages"]["message"]["text"] + avs_result = raw_response[response_type]["transactionResponse"]["avsResultCode"] + cvc_result = raw_response[response_type]["transactionResponse"]["cavvResultCode"] + + [] + |> status_code(200) + |> set_message(message) + |> set_avs_result(avs_result) + |> set_cvc_result(cvc_result) + |> set_params(raw_response) + |> set_success(true) + |> handle_opts + end + + def parse_gateway_error(raw_response) do + response_type = check_response_type(raw_response) + + {message, error_code} = if raw_response[response_type]["transactionResponse"]["errors"] do + {raw_response[response_type]["messages"]["message"]["text"] <> " " <> + raw_response[response_type]["transactionResponse"]["errors"]["error"]["errorText"], + raw_response[response_type]["transactionResponse"]["errors"]["error"]["errorCode"]} + else + {raw_response[response_type]["messages"]["message"]["text"], + raw_response[response_type]["messages"]["message"]["code"]} + end + + [] + |> status_code(200) + |> set_message(message) + |> set_error_code(error_code) + |> set_params(raw_response) + |> set_success(false) + |> handle_opts + end + + def check_response_type(raw_response) do + cond do + raw_response[@response_type[:transaction_response]] -> "createTransactionResponse" + raw_response[@response_type[:error_response]] -> "ErrorResponse" + raw_response[@response_type[:customer_profile_response]] -> "createCustomerProfileResponse" + raw_response[@response_type[:customer_payment_profile_response]] -> "createCustomerPaymentProfileResponse" + raw_response[@response_type[:delete_customer_profile]] -> "deleteCustomerProfileResponse" + end + end + + defp set_success(opts, value), do: opts ++ [success: value] + defp status_code(opts, code), do: opts ++ [status_code: code] + defp set_message(opts, message), do: opts ++ [message: message] + defp set_avs_result(opts, result), do: opts ++ [avs_result: result] + defp set_cvc_result(opts, result), do: opts ++ [cvc_result: result] + defp set_params(opts, raw_response), do: opts ++ [params: raw_response] + defp set_error_code(opts, code), do: opts ++ [error_code: code] + + defp handle_opts(opts) do + case Keyword.fetch(opts, :success) do + {:ok, true} -> Response.success(opts) + {:ok, false} -> Response.error(opts) + end + end + + end end diff --git a/lib/gringotts/response.ex b/lib/gringotts/response.ex index b9ba7270..f9097490 100644 --- a/lib/gringotts/response.ex +++ b/lib/gringotts/response.ex @@ -17,7 +17,7 @@ defmodule Gringotts.Response do """ defstruct [ - :success, :authorization, :status_code, :error_code, :message, + :success, :authorization, :status_code, :error_code, :message, :avs_result, :cvc_result, :params, :fraud_review ] diff --git a/test/gateways/authorize_net_test.exs b/test/gateways/authorize_net_test.exs index 664ae5cb..0c155a4c 100644 --- a/test/gateways/authorize_net_test.exs +++ b/test/gateways/authorize_net_test.exs @@ -8,6 +8,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do import Mock + @auth %{name: "64jKa6NA", transaction_key: "4vmE338dQmAN6m7B"} @card %CreditCard { number: "5424000000000015", month: 12, @@ -26,29 +27,61 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do @bad_amount "a" @opts [ - config: %{name: "64jKa6NA", transactionKey: "4vmE338dQmAN6m7B"}, - refId: "123456", - order: %{invoiceNumber: "INV-12345", description: "Product Description"}, - lineItem: %{itemId: "1", name: "vase", description: "Cannes logo", quantity: "18", unitPrice: "45.00"} + config: @auth, + ref_id: "123456", + order: %{invoice_number: "INV-12345", description: "Product Description"}, + lineitems: %{ + item_id: "1", + name: "vase", + description: "Cannes logo", + quantity: "18", + unit_price: "45.00" + } ] @opts_refund [ - config: %{name: "64jKa6NA", transaction_key: "4vmE338dQmAN6m7B"}, + config: @auth, ref_id: "123456", payment: %{card: %{number: "5424000000000015", year: 2020, month: 12}} ] + @opts_store [ + config: @auth, + profile: %{ + merchant_customer_id: "123456", + description: "Profile description here", + email: "customer-profile-email@here.com" + }, + customer_type: "individual", + validation_mode: "testMode" + ] + @opts_store_no_profile [ + config: @auth, + ] + @opts_refund [ + config: @auth, + ref_id: "123456", + payment: %{card: %{number: "5424000000000015", year: 2020, month: 12}} + ] @opts_refund_bad_payment [ - config: %{name: "64jKa6NA", transaction_key: "4vmE338dQmAN6m7B"}, - ref_id: "123456", + config: @auth, + ref_id: "123456", payment: %{card: %{number: "123", year: 2020, month: 12}} ] @opts_store [ - config: %{name: "64jKa6NA", transaction_key: "4vmE338dQmAN6m7B"}, - profile: %{merchant_customer_id: "123456", description: "Profile description here", email: "customer-profile-email@here.com"} + config: @auth, + profile: %{merchant_customer_id: "123456", + description: "Profile description here", + email: "customer-profile-email@here.com" + } ] @opts_store_no_profile [ - config: %{name: "64jKa6NA", transaction_key: "4vmE338dQmAN6m7B"}, + config: @auth, ] - + @opts_customer_profile [ + config: @auth, + customer_profile_id: "1814012002", + validation_mode: "testMode" + ] + @refund_id "60036752756" @void_id "60036855217" @void_invalid_id "60036855211" @@ -56,34 +89,32 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do @capture_id "60036752756" @capture_invalid_id "60036855211" + @refund_id "60036752756" + @void_id "60036855217" + @unstore_id "1813991490" + describe "purchase" do test "successful response with right params" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_purchase_response end] do - {:ok, response} = ANet.purchase(@amount, @card, @opts) - assert response.raw["createTransactionResponse"]["messages"]["resultCode"] == "Ok" + assert {:ok, response} = ANet.purchase(@amount, @card, @opts) + assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end test "with bad amount" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.bad_amount_purchase_response end] do - {:error, response} = ANet.purchase(@bad_amount, @card, @opts) - assert response.raw["createTransactionResponse"]["messages"]["resultCode"] == "Error" + assert {:error, response} = ANet.purchase(@bad_amount, @card, @opts) + assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end end test "with bad card" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.bad_card_purchase_response end] do - {:error, response} = ANet.purchase(@amount, @bad_card, @opts) - assert response.raw["ErrorResponse"]["messages"]["resultCode"] == "Error" - end - end - - test "test network error" do - with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.network_error_response end] do - assert {:error, response} = ANet.purchase(@amount, @card, @opts) + assert {:error, response} = ANet.purchase(@amount, @bad_card, @opts) + assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" end end end @@ -91,25 +122,25 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "authorize" do test "successful response with right params" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_authorize_response end] do - {:ok, response} = ANet.authorize(@amount, @card, @opts) - assert response.raw["createTransactionResponse"]["messages"]["resultCode"] == "Ok" - end + [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_authorize_response end] do + assert {:ok, response} = ANet.authorize(@amount, @card, @opts) + assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" + end end test "with bad amount" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.bad_amount_purchase_response end] do - {:error, response} = ANet.authorize(@bad_amount, @card, @opts) - assert response.raw["createTransactionResponse"]["messages"]["resultCode"] == "Error" + assert {:error, response} = ANet.authorize(@bad_amount, @card, @opts) + assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end end test "with bad card" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.bad_card_purchase_response end] do - {:error, response} = ANet.authorize(@amount, @bad_card, @opts) - assert response.raw["ErrorResponse"]["messages"]["resultCode"] == "Error" + assert {:error, response} = ANet.authorize(@amount, @bad_card, @opts) + assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" end end end @@ -118,16 +149,16 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "successful response with right params" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_capture_response end] do - {:ok, response} = ANet.capture(@capture_id, @amount, @opts) - assert response.raw["createTransactionResponse"]["messages"]["resultCode"] == "Ok" + assert {:ok, response} = ANet.capture(@capture_id, @amount, @opts) + assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end test "with bad transaction id" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.bad_id_capture end] do - {:error, response} = ANet.capture(@capture_invalid_id, @amount, @opts) - assert response.raw["createTransactionResponse"]["messages"]["resultCode"] == "Error" + assert {:error, response} = ANet.capture(@capture_invalid_id, @amount, @opts) + assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end end end @@ -136,24 +167,24 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "successful response with right params" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_refund_response end] do - {:ok, response} = ANet.refund(@amount, @refund_id, @opts_refund) - assert response.raw["createTransactionResponse"]["messages"]["resultCode"] == "Ok" + assert {:ok, response} = ANet.refund(@amount, @refund_id, @opts_refund) + assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end test "bad payment params" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.bad_card_refund end] do - {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund_bad_payment) - assert response.raw["ErrorResponse"]["messages"]["resultCode"] == "Error" + assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund_bad_payment) + assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" end end test "debit less than refund amount" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.debit_less_than_refund end] do - {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund) - assert response.raw["createTransactionResponse"]["messages"]["resultCode"] == "Error" + assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund) + assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end end end @@ -162,16 +193,16 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "successful response with right params" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_void end] do - {:ok, response} = ANet.void(@void_id, @opts) - assert response.raw["createTransactionResponse"]["messages"]["resultCode"] == "Ok" + assert {:ok, response} = ANet.void(@void_id, @opts) + assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end test "with bad transaction id" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.void_non_existent_id end] do - {:error, response} = ANet.void(@void_invalid_id, @opts) - assert response.raw["createTransactionResponse"]["messages"]["resultCode"] == "Error" + assert {:error, response} = ANet.void(@void_invalid_id, @opts) + assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end end end @@ -180,28 +211,44 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "successful response with right params" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_store_response end] do - {:ok, response} = ANet.store(@card, @opts_store) - assert response.raw["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" + assert {:ok, response} = ANet.store(@card, @opts_store) + assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" end end test "without any profile" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.store_without_profile_fields end] do - {:error, response} = ANet.store(@card, @opts_store_no_profile) - assert response.raw["createCustomerProfileResponse"]["messages"]["resultCode"] == "Error" + assert {:error, response} = ANet.store(@card, @opts_store_no_profile) + assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Error" end end + + test "with customer profile id" do + with_mock HTTPoison, + [request: fn(_method, _url, _body, _headers) -> MockResponse.customer_payment_profile_success_response end] do + assert {:ok, response} = ANet.store(@card, @opts_customer_profile) + assert response.params["createCustomerPaymentProfileResponse"]["messages"]["resultCode"] == "Ok" + end + end end describe "unstore" do test "successful response with right params" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_unstore_response end] do - {:ok, response} = ANet.unstore(@unstore_id, @opts) - assert response.raw["deleteCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" + assert {:ok, response} = ANet.unstore(@unstore_id, @opts) + assert response.params["deleteCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" end end end + test "network error type non existent domain" do + with_mock HTTPoison, + [request: fn(_method, _url, _body, _headers) -> MockResponse.netwok_error_non_existent_domain end] do + assert {:error, response} = ANet.purchase(@amount, @card, @opts) + assert response.message == "HTTPoison says 'nxdomain'" + end + end + end diff --git a/test/mocks/authorize_net_mock.exs b/test/mocks/authorize_net_mock.exs index c9acc67e..beabfa21 100644 --- a/test/mocks/authorize_net_mock.exs +++ b/test/mocks/authorize_net_mock.exs @@ -297,8 +297,25 @@ status_code: 200}} end - def network_error_response do - body = "no response error" - {:error, %{body: body, status_code: 500}} + def customer_payment_profile_success_response do + {:ok, + %HTTPoison.Response{body: "OkI00001Successful.181401200218086700051,1,1,(TESTMODE) This transaction has been approved.,000000,P,0,none,Test transaction for ValidateCustomerPaymentProfile.,1.00,CC,auth_only,none,,,,,,,,,,,email@example.com,,,,,,,,,0.00,0.00,0.00,FALSE,none,EA9FD49A9501D0415FE26BAEF9FD8B2C,,,,,,,,,,,,,XXXX0015,MasterCard,,,,,,,,,,,,,,,,,", + headers: [{"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", + "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-17537805"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, + {"Content-Length", "828"}, {"Date", "Thu, 28 Dec 2017 13:54:20 GMT"}, + {"Connection", "keep-alive"}], + request_url: "https://apitest.authorize.net/xml/v1/request.api", + status_code: 200}} + end + + def netwok_error_non_existent_domain do + {:error, %HTTPoison.Error{id: nil, reason: :nxdomain}} end end From d5fda24a79be84cf68a6961790c50dfbafa8f241 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Thu, 11 Jan 2018 00:42:50 +0530 Subject: [PATCH 11/60] Introducing the Money protocol (#71) Building on the discussion in #62, this PR introduces a protocol to replace amount argument, and removes the need to specify currency in config or opts. Supporting `ex_money` and `monetized` money libs out of the box. Usage:- Money can now be passed like this while calling the public API methods. money = %{amount: Decimal.new(2017.18), currency: "USD"} Conversation for this happened here on elixir forum and https://elixirforum.com/t/gringotts-a-complete-payment-library-for-elixir-and-phoenix-framework/11054 --- README.md | 78 ++--- lib/gringotts.ex | 416 ++++++++++++----------- lib/gringotts/gateways/monei.ex | 82 ++--- lib/gringotts/money.ex | 55 +++ mix.exs | 21 +- mix.lock | 7 +- test/gateways/monei_test.exs | 35 +- test/integration/gateways/monei_test.exs | 7 +- 8 files changed, 384 insertions(+), 317 deletions(-) create mode 100644 lib/gringotts/money.ex diff --git a/README.md b/README.md index 12456ac1..ebad2c06 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,10 @@ Add gringotts to the list of dependencies. ```elixir def deps do [ - {:gringotts, "~> 1.0"} + {:gringotts, "~> 1.0"}, + # ex_money provides an excellent Money library, and integrates + # out-of-the-box with Gringotts + {:ex_money, "~> 1.1.0"} ] end ``` @@ -46,59 +49,58 @@ This simple example demonstrates how a purchase can be made using a person's cre Add configs in `config/config.exs` file. ```elixir -config :gringotts, Gringotts.Gateways.Stripe, - adapter: Gringotts.Gateways.Stripe, - secret_key: "YOUR_KEY", - default_currency: "USD" - +config :gringotts, Gringotts.Gateways.Monei, + adapter: Gringotts.Gateways.Monei, + userId: "your_secret_user_id", + password: "your_secret_password", + entityId: "your_secret_channel_id" ``` Copy and paste this code in your module ```elixir -alias Gringotts.Gateways.Stripe -alias Gringotts.{CreditCard, Address} +alias Gringotts.Gateways.Monei +alias Gringotts.{CreditCard} card = %CreditCard{ - first_name: "John", - last_name: "Smith", - number: "4242424242424242", - year: "2022", - month: "12", - verification_code: "123" -} - -address = %Address{ - street1: "123 Main", - city: "New York", - region: "NY", - country: "US", - postal_code: "11111" + first_name: "Harry Potter", + last_name: "Doe", + number: "4200000000000000", + year: 2099, month: 12, + verification_code: "123", + brand: "VISA" } -opts = [address: address, currency: "eur"] - -case Gringotts.purchase(Stripe, 10, card, opts) do - {:ok, %{authorization: authorization}} -> - IO.puts("Payment authorized #{authorization}") +amount = Money.new(42, :USD) - {:error, %{code: :declined, reason: reason}} -> - IO.puts("Payment declined #{reason}") +case Gringotts.purchase(Monei, amount, card, opts) do + {:ok, %{id: id}} -> + IO.puts("Payment authorized, reference token: '#{id}'") - {:error, %{code: error}} -> - IO.puts("Payment error #{error}") + {:error, %{status_code: error, raw: raw_response}} -> + IO.puts("Error: #{error}\nRaw:\n#{raw_response}") end ``` ## Supported Gateways -* [Authorize.Net](http://www.authorize.net/) - AD, AT, AU, BE, BG, CA, CH, CY, CZ, DE, DK, ES, FI, FR, GB, GB, GI, GR, HU, IE, IT, LI, LU, MC, MT, NL, NO, PL, PT, RO, SE, SI, SK, SM, TR, US, VA -* [CAMS: Central Account Management System](https://www.centralams.com/) - AU, US -* [MONEI](http://www.monei.net/) - AD, AT, BE, BG, CA, CH, CY, CZ, DE, DK, EE, ES, FI, FO, FR, GB, GI, GR, HU, IE, IL, IS, IT, LI, LT, LU, LV, MT, NL, NO, PL, PT, RO, SE, SI, SK, TR, US, VA -* [PAYMILL](https://paymill.com) - AD, AT, BE, BG, CH, CY, CZ, DE, DK, EE, ES, FI, FO, FR, GB, GI, GR, HU, IE, IL, IS, IT, LI, LT, LU, LV, MT, NL, NO, PL, PT, RO, SE, SI, SK, TR, VA -* [Stripe](https://stripe.com/) - AT, AU, BE, CA, CH, DE, DK, ES, FI, FR, GB, IE, IN, IT, LU, NL, NO, SE, SG, US -* [TREXLE](https://docs.trexle.com/) - AD, AE, AT, AU, BD, BE, BG, BN, CA, CH, CY, CZ, DE, DK, EE, EG, ES, FI, FR, GB, GI, GR, HK, HU, ID, IE, IL, IM, IN, IS, IT, JO, KW, LB, LI, LK, LT, LU, LV, MC, MT, MU, MV, MX, MY, NL, NO, NZ, OM, PH, PL, PT, QA, RO, SA, SE, SG, SI, SK, SM, TR, TT, UM, US, VA, VN, ZA -* [Wirecard](http://www.wirecard.com) - AD, CY, GI, IM, MT, RO, CH, AT, DK, GR, IT, MC, SM, TR, BE, EE, HU, LV, NL, SK, GB, BG, FI, IS, LI, NO, SI, VA, FR, IL, LT, PL, ES, CZ, DE, IE, LU, PT, SE +| Gateway | Supported countries | +| ------ | ----- | +| [Authorize.Net][anet] | AD, AT, AU, BE, BG, CA, CH, CY, CZ, DE, DK, ES, FI, FR, GB, GB, GI, GR, HU, IE, IT, LI, LU, MC, MT, NL, NO, PL, PT, RO, SE, SI, SK, SM, TR, US, VA | +| [CAMS][cams] | AU, US | +| [MONEI][anet] | DE, EE, ES, FR, IT, US | +| [PAYMILL][paymill] | AD, AT, BE, BG, CH, CY, CZ, DE, DK, EE, ES, FI, FO, FR, GB, GI, GR, HU, IE, IL, IS, IT, LI, LT, LU, LV, MT, NL, NO, PL, PT, RO, SE, SI, SK, TR, VA | +| [Stripe][stripe] | AT, AU, BE, CA, CH, DE, DK, ES, FI, FR, GB, IE, IN, IT, LU, NL, NO, SE, SG, US | +| [TREXLE][trexle] | AD, AE, AT, AU, BD, BE, BG, BN, CA, CH, CY, CZ, DE, DK, EE, EG, ES, FI, FR, GB, GI, GR, HK, HU, ID, IE, IL, IM, IN, IS, IT, JO, KW, LB, LI, LK, LT, LU, LV, MC, MT, MU, MV, MX, MY, NL, NO, NZ, OM, PH, PL, PT, QA, RO, SA, SE, SG, SI, SK, SM, TR, TT, UM, US, VA, VN, ZA | +| [Wirecard][wirecard] | AD, AT, BE, BG, CH, CY, CZ, DE, DK, EE, ES, FI, FR, GB, GI, GR, HU, IE, IL, IM, IS, IT, LI, LT, LU, LV, MC, MT, NL, NO, PL, PT, RO, SE, SI, SK, SM, TR, VA | + +[anet]: http://www.authorize.net/ +[cams]: https://www.centralams.com/ +[monei]: http://www.monei.net/ +[paymill]: https://www.paymill.com +[stripe]: https://www.stripe.com/ +[trexle]: https://www.trexle.com/ +[wirecard]: http://www.wirecard.com ## Road Map diff --git a/lib/gringotts.ex b/lib/gringotts.ex index d1440490..0bd368e9 100644 --- a/lib/gringotts.ex +++ b/lib/gringotts.ex @@ -1,133 +1,150 @@ defmodule Gringotts do - @moduledoc ~S""" - Gringotts is a payment gateway integration library supporting many gateway integrations. - - ## Configuration - - The configuration for `Gringotts` must be in your application environment, - usually defined in your `config/config.exs` and is **mandatory**: + @moduledoc """ + Gringotts is a payment gateway integration library for merchants - **Global Configuration** + Gringotts provides a unified interface for multiple Payment Gateways to make it + easy for merchants to use multiple gateways. + All gateways must conform to the API as described in this module, but can also + support more gateway features than those required by Gringotts. - The global configuration sets the library level configurations to interact with the gateway. - If the mode is not set then by 'default' the sandbox account is selected. + ## Standard API arguments - To integrate with the sandbox account set. - config :gringotts, :global_config, - mode: :test - To integrate with the live account set. - config :gringotts, :global_config, - mode: :prod + All requests to Gringotts are served by a supervised worker, this might be + made optional in future releases. + + ### `gateway` (Module) Name + + The `gateway` to which this request is made. This is required in all API calls + because Gringotts supports multiple Gateways. - **Gateway Configuration** + #### Example + If you've configured Gringotts to work with Stripe, you'll do this + to make an `authorization` request: + + Gringotts.authorize(Gingotts.Gateways.Stripe, other args ...) - The gateway level configurations are for fields related to a specific gateway. - config :Gringotts, Gringotts.Gateways.Stripe, - adapter: Gringotts.Gateways.Stripe, - api_key: "sk_test_vIX41hC0sdfBKrPWQerLuOMld", - default_currency: "USD" + ### `amount` _and currency_ - `Key` for the configuration and the adapter value should be the same, we could have - chosen to pick adapter and used it as the key but we have chosen to be explicit rather - than implicit. + This argument represents the "amount", annotated with the currency unit for + the transaction. `amount` is polymorphic thanks to the `Gringotts.Money` + protocol which can be implemented by your custom Money type. - ## Standard Arguments + #### Note + We support [`ex_money`][ex_money] and [`monetized`][monetized] out of the + box, and you can drop their types in this argument and everything will work + as expected. - The public API is designed in such a way that library users end up passing mostly a - standard params for almost all requests. + Otherwise, just wrap your `amount` with the `currency` together in a `Map` like so, + money = %{amount: Decimal.new(100.50), currency: "USD"} - ### Gateway Name - eg: Gringotts.Gateways.Stripe + #### Example - This option specifies which payment gateway this request should be called for. - Since `Gringotts` supports multiple payment gateway integrations at the same time - so this information get's critical. + If you use `ex_money` in your project, and want to make an authorization for + $2.99 to MONEI, you'll do the following: + + amount = Money.new(2.99, :USD) + Gringotts.authorize(Gringotts.Gateways.Monei, amount, some_card, extra_options) - ### Amount - eg: 5000 + ### `card`, a payment source - Amount is the money an application wants to deduct in cents on the card. + Gringotts provides a `Gringotts.CreditCard` type to hold card parameters + which merchants fetch from their clients. The same type can also hold Debit + card details. - ### Card Info - eg: - %CreditCard { - name: "John Doe", + #### Note + Gringotts only supports payment by debit or credit card, even though the + gateways might support payment via other instruments such as e-wallets, + vouchers, bitcoins or banks. Support for these instruments is planned in + future releases. + + %CreditCard { + first_name: "Harry", + last_name: "Potter", number: "4242424242424242", - expiration: {2018, 12}, - cvc: "123", - street1: "123 Main", - street2: "Suite 100", - city: "New York", - region: "NY", - country: "US", - postal_code: "11111" - } + month: 12, + year: 2099, + verification_code: "123", + brand: "VISA"} - This stores all the credit card info of the customer along with some address info etc. + ### `opts` for optional params + + `opts` is a `keyword` list of other options/information accepted by the + gateway. The format, use and structure is gateway specific and documented in + the Gateway's docs. + + [ex_money]: https://hexdocs.pm/ex_money/readme.html + [monetized]: https://hexdocs.pm/monetized/ + + ## Configuration + + Merchants must provide Gateway specific configuration in their application + config in the usual elixir style. The required and optional fields are + documented in every Gateway. + + > The required config keys are validated at runtime, as they include + > authentication information. See `Gringotts.Adapter.validate_config/2`. + + ### Global config + + This is set using the `:global_config` key once in your application. + + #### `:mode` + + Gateways usually provide sandboxed environments to test applications and the + merchant can use the `:mode` switch to choose between the sandbox or live + environment. + + **Available Options:** + + * `:test` -- for sandbox environment, all requests will be routed to the + gateway's sandbox/test API endpoints. Use this in your `:dev` and `:test` + environments. + * `:prod` -- for live environment, all requests will reach the financial and + banking networks. Switch to this in your application's `:prod` environment. + + **Example** + + config :gringotts, :global_config, + # for live environment + mode: :prod + + ### Gateway specific config - ### Other options - eg: [currency: "usd"] + The gateway level config is documented in their docs. They must be of the + following format: - This is a keyword list of all the other options/information which the payment gateway - needs apart from the above mentioned options. + config :gringotts, Gringotts.Gateways.XYZ, + adapter: Gringotts.Gateways.XYZ, + # some_documented_key: associated_value + # some_other_key: another_value - > This is passed as is to the gateway and not modified, usually it comes back in the - response object intact. + > ***Note!*** + > The config key matches the `:adapter`! Both ***must*** be the Gateway module + > name! """ import GenServer, only: [call: 2] @doc """ - This is the bare minimum API for a gateway to support, and consists of a single call: - - @payment %{ - name: "John Doe", - number: "4242424242424242", - expiration: {2018, 12}, - cvc: "123", - street1: "123 Main", - street2: "Suite 100", - city: "New York", - region: "NY", - country: "US", - postal_code: "11111" - } - - @options [currency: "usd"] - - Gringotts.purchase(Gringotts.Gateways.Stripe, 5, @payment, @options) - - This method is expected to authorize payment and transparently trigger eventual - settlement. Preferably it is implemented as a single call to the gateway, - but it can also be implemented as chained `authorize` and `capture` calls. - """ - def purchase(gateway, amount, card, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:purchase, gateway, amount, card, opts}) - end + Performs a (pre) Authorize operation. - @doc """ - Authorize should authorize funds on a payment instrument that will - not be settled without a following call to `capture` within some finite - period of time. When implementing this API, authorize and capture are - both required. - - @payment %{ - name: "John Doe", - number: "4242424242424242", - expiration: {2018, 12}, - cvc: "123", - street1: "123 Main", - street2: "Suite 100", - city: "New York", - region: "NY", - country: "US", - postal_code: "11111" - } - - @options [currency: "usd"] - - Gringotts.authorize(Gringotts.Gateways.Stripe, 5, @payment, @options) + The authorization validates the `card` details with the banking network, + places a hold on the transaction `amount` in the customer’s issuing bank and + may also trigger risk management. Funds are not transferred, until the + authorization is `capture/3`d. + + > `capture/3` must also be implemented alongwith this. + + ## Example + + To (pre) authorize a payment of $4.20 on a sample `card` with the `XYZ` + gateway, + + amount = Money.new(4.2, :USD) + # IF YOU DON'T USE ex_money OR monetized + # amount = %{value: Decimal.new(4.2), currency: "EUR"} + card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} + {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.XYZ, amount, card, opts) """ def authorize(gateway, amount, card, opts \\ []) do validate_config(gateway) @@ -135,33 +152,22 @@ defmodule Gringotts do end @doc """ - Captures deducts an amount from the card, this happens once the card is authorised. + Captures a pre-authorized `amount`. - Partial captures, if supported by the gateway, are achieved by passing an amount. - Not passing an amount to capture should always cause the full amount of the initial - authorization to be captured. + `amount` is transferred to the merchant account. The gateway might support, + * partial captures, + * multiple captures, per authorization - If the gateway does not support partial captures, calling `capture` with an amount - other than nil should raise an error indicating partial capture is not supported. + ## Example - @payment %{ - name: "John Doe", - number: "4242424242424242", - expiration: {2018, 12}, - cvc: "123", - street1: "123 Main", - street2: "Suite 100", - city: "New York", - region: "NY", - country: "US", - postal_code: "11111" - } - - @options [currency: "usd"] - - id = "ch_1BYvGkBImdnrXiZwet3aKkQE" - - Gringotts.capture(Gringotts.Gateways.Stripe, id, 5) + To capture $4.20 on a previously authorized payment worth $4.20 by referencing + the obtained authorization `id` with the `XYZ` gateway, + + amount = Money.new(4.2, :USD) + # IF YOU DON'T USE ex_money OR monetized + # amount = %{value: Decimal.new(4.2), currency: "EUR"} + card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} + Gringotts.capture(Gringotts.Gateways.XYZ, amount, auth_result.id, opts) """ def capture(gateway, id, amount, opts \\ []) do validate_config(gateway) @@ -169,55 +175,47 @@ defmodule Gringotts do end @doc """ - Void is an optional (but highly recommended) supplement to `authorise` & `capture` - API that should immediately cancel an authorized charge, clearing it off of the - underlying payment instrument without waiting for expiration. - - @payment %{ - name: "John Doe", - number: "4242424242424242", - expiration: {2018, 12}, - cvc: "123", - street1: "123 Main", - street2: "Suite 100", - city: "New York", - region: "NY", - country: "US", - postal_code: "11111" - } + Transfers `amount` from the customer to the merchant. - @options [currency: "usd"] + Gateway attempts to process a purchase on behalf of the customer, by debiting + `amount` from the customer's account by charging the customer's `card`. - id = "ch_1BYvGkBImdnrXiZwet3aKkQE" + This method _can_ be implemented as a chained call to `authorize/3` and + `capture/3`. But it must be implemented as a single call to the Gateway if it + provides a specific endpoint or action for this. + + > ***Note!** + > All gateways must implement (atleast) this method. - Gringotts.void(Gringotts.Gateways.Stripe, id) + ## Example + To process a purchase worth $4.2, with the `XYZ` gateway, + + amount = Money.new(4.2, :USD) + # IF YOU DON'T USE ex_money OR monetized + # amount = %{value: Decimal.new(4.2), currency: "EUR"} + card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} + Gringotts.purchase(Gringotts.Gateways.XYZ, amount, card, opts) """ - def void(gateway, id, opts \\ []) do + def purchase(gateway, amount, card, opts \\ []) do validate_config(gateway) - call(:payment_worker, {:void, gateway, id, opts}) + call(:payment_worker, {:purchase, gateway, amount, card, opts}) end @doc """ - Cancels settlement or returns funds as appropriate for a referenced prior - `purchase` or `capture`. - - @payment %{ - name: "John Doe", - number: "4242424242424242", - expiration: {2018, 12}, - cvc: "123", - street1: "123 Main", - street2: "Suite 100", - city: "New York", - region: "NY", - country: "US", - postal_code: "11111" - } - - id = "ch_1BYvGkBImdnrXiZwet3aKkQE" - - Gringotts.refund(Gringotts.Gateways.Stripe, 5, id) + Refunds the `amount` to the customer's account with reference to a prior transfer. + + The end customer will usually see two bookings/records on his statement. + + ## Example + + To refund a previous purchase worth $4.20 referenced by `id`, with the `XYZ` + gateway, + + amount = Money.new(4.2, :USD) + # IF YOU DON'T USE ex_money OR monetized + # amount = %{value: Decimal.new(4.2), currency: "EUR"} + Gringotts.purchase(Gringotts.Gateways.XYZ, amount, id, opts) """ def refund(gateway, amount, id, opts \\ []) do validate_config(gateway) @@ -225,32 +223,20 @@ defmodule Gringotts do end @doc """ - Tokenizes a supported payment method in the gateway's vault. If the gateway - conflates tokenization with customer management, `Gringotts` should hide all - customer management and any customer identifier(s) within the token returned. - It's certainly legitimate to have a library that interacts with all the features - in a gateway's vault, but `Gringotts` is not the right place for it. - - It's critical that `store` returns a token that can be used against `purchase` - and `authorize`. Currently the standard is to return the token in the - `%Response{...}` `authorization` field. - - @payment %{ - name: "John Doe", - number: "4242424242424242", - expiration: {2018, 12}, - cvc: "123", - street1: "123 Main", - street2: "Suite 100", - city: "New York", - region: "NY", - country: "US", - postal_code: "11111" - } - - id = "ch_1BYvGkBImdnrXiZwet3aKkQE" - - Gringotts.store(Gringotts.Gateways.Stripe, @payment) + Stores the payment-source data for later use, returns a `token`. + + > The token must be returned in the `Response.authorization` field. + + ## Note + + This usually enables _One-Click_ and _Recurring_ payments. + + ## Example + + To store a card (a payment-source) for future use, with the `XYZ` gateway, + + card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} + Gringotts.store(Gringotts.Gateways.XYZ, card, opts) """ def store(gateway, card, opts \\ []) do validate_config(gateway) @@ -258,21 +244,45 @@ defmodule Gringotts do end @doc """ - Removes the token from the payment gateway, once `unstore` request is fired the - token which could enable `authorise` & `capture` would not work with this token. + Removes a previously `token` from the gateway + + Once `unstore/3`d, the `token` must becom invalid, though some gateways might + not support this feature. + + ## Example + + To unstore with the `XYZ` gateway, + + token = "some_privileged_customer" + Gringotts.unstore(Gringotts.Gateways.XYZ, token) + """ + def unstore(gateway, token, opts \\ []) do + validate_config(gateway) + call(:payment_worker, {:unstore, gateway, token, opts}) + end + + @doc """ + Voids the referenced payment. + + This method attempts a reversal/immediate cancellation of the a previous + transaction referenced by `id`. - This should be done once the payment capture is done and you don't wish to make any - further deductions for the same card. + As a consequence, the customer usually **won't** see any booking on his + statement. - customer_id = "random_customer" + ## Example - Gringotts.unstore(Gringotts.Gateways.Stripe, customer_id) + To void a previous (pre) authorization with the `XYZ` gateway, + + id = "some_previously_obtained_token" + Gringotts.void(Gringotts.Gateways.XYZ, id, opts) """ - def unstore(gateway, customer_id, opts \\ []) do + def void(gateway, id, opts \\ []) do validate_config(gateway) - call(:payment_worker, {:unstore, gateway, customer_id, opts}) + call(:payment_worker, {:void, gateway, id, opts}) end + # TODO: This is runtime error reporting fix this so that it does compile # time error reporting. defp validate_config(gateway) do diff --git a/lib/gringotts/gateways/monei.ex b/lib/gringotts/gateways/monei.ex index 82549e71..4087bf0c 100644 --- a/lib/gringotts/gateways/monei.ex +++ b/lib/gringotts/gateways/monei.ex @@ -1,5 +1,5 @@ defmodule Gringotts.Gateways.Monei do - @moduledoc ~S""" + @moduledoc """ [MONEI](https://www.monei.net) gateway implementation. For reference see [MONEI's API (v1) documentation](https://docs.monei.net). @@ -30,7 +30,6 @@ defmodule Gringotts.Gateways.Monei do | ---- | --- | ---- | | `billing_address` | | Not implemented | | `cart` | | Not implemented | - | `currency` | | **Implemented** | | `customParameters` | | Not implemented | | `customer` | | Not implemented | | `invoice` | | Not implemented | @@ -39,7 +38,7 @@ defmodule Gringotts.Gateways.Monei do | `shipping_customer` | | Not implemented | > All these keys are being implemented, track progress in - > [issue #36](https://github.com/aviabird/gringotts/issues)! + > [issue #36](https://github.com/aviabird/gringotts/issues/36)! ## Registering your MONEI account at `Gringotts` @@ -107,12 +106,13 @@ defmodule Gringotts.Gateways.Monei do aliases to it (to save some time): ``` iex> alias Gringotts.{Response, CreditCard, Gateways.Monei} - iex> opts = [currency: "EUR"] # The default currency is EUR, and this is just for an example. - iex> card = %CreditCard{first_name: "Jo", - last_name: "Doe", + iex> amount = %{value: Decimal.new(42), currency: "EUR"} + iex> card = %CreditCard{first_name: "Harry", + last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, - verification_code: "123", brand: "VISA"} + verification_code: "123", + brand: "VISA"} ``` We'll be using these in the examples below. @@ -129,11 +129,10 @@ defmodule Gringotts.Gateways.Monei do use Gringotts.Gateways.Base use Gringotts.Adapter, required_config: [:userId, :entityId, :password] import Poison, only: [decode: 1] - alias Gringotts.{CreditCard, Response} + alias Gringotts.{CreditCard, Response, Money} @base_url "https://test.monei-api.net" @default_headers ["Content-Type": "application/x-www-form-urlencoded", charset: "UTF-8"] - @default_currency "EUR" @version "v1" @@ -178,22 +177,18 @@ defmodule Gringotts.Gateways.Monei do The following session shows how one would (pre) authorize a payment of $40 on a sample `card`. - iex> opts = [currency: "EUR"] # The default currency is EUR, and this is just for an example. + iex> amount = %{value: Decimal.new(42), currency: "EUR"} iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> auth_result = Gringotts.authorize(Gringotts.Gateways.Monei, 40, card, opts) + iex> auth_result = Gringotts.authorize(Gringotts.Gateways.Monei, amount, card, opts) iex> auth_result.id # This is the authorization ID """ - @spec authorize(number, CreditCard.t(), keyword) :: {:ok | :error, Response} - def authorize(amount, card = %CreditCard{}, opts) when is_integer(amount) do - authorize(amount / 1, card, opts) - end - - def authorize(amount, card = %CreditCard{}, opts) when is_float(amount) do + @spec authorize(Money.t, CreditCard.t(), keyword) :: {:ok | :error, Response} + def authorize(amount, card = %CreditCard{}, opts) do params = [ paymentType: "PA", - amount: :erlang.float_to_binary(amount, decimals: 2), - currency: currency(opts) + amount: amount |> Money.value |> Decimal.to_float |> :erlang.float_to_binary(decimals: 2), + currency: Money.currency(amount) ] ++ card_params(card) auth_info = Keyword.fetch!(opts, :config) @@ -219,22 +214,18 @@ defmodule Gringotts.Gateways.Monei do The following session shows how one would (partially) capture a previously authorized a payment worth $35 by referencing the obtained authorization `id`. - iex> opts = [currency: "EUR"] # The default currency is EUR, and this is just for an example. + iex> amount = %{value: Decimal.new(42), currency: "EUR"} iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> capture_result = Gringotts.capture(Gringotts.Gateways.Monei, 35, auth_result.id, opts) """ - @spec capture(number, String.t(), keyword) :: {:ok | :error, Response} + @spec capture(Money.t, String.t(), keyword) :: {:ok | :error, Response} def capture(amount, payment_id, opts) - def capture(amount, <>, opts) when is_integer(amount) do - capture(amount / 1, payment_id, opts) - end - - def capture(amount, <>, opts) when is_float(amount) do + def capture(amount, <>, opts) do params = [ paymentType: "CP", - amount: :erlang.float_to_binary(amount, decimals: 2), - currency: currency(opts) + amount: amount |> Money.value |> Decimal.to_float |> :erlang.float_to_binary(decimals: 2), + currency: Money.currency(amount) ] auth_info = Keyword.fetch!(opts, :config) @@ -252,22 +243,18 @@ defmodule Gringotts.Gateways.Monei do The following session shows how one would process a payment in one-shot, without (pre) authorization. - iex> opts = [currency: "EUR"] # The default currency is EUR, and this is just for an example. + iex> amount = %{value: Decimal.new(42), currency: "EUR"} iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> purchase_result = Gringotts.purchase(Gringotts.Gateways.Monei, 40, card, opts) + iex> purchase_result = Gringotts.purchase(Gringotts.Gateways.Monei, amount, card, opts) """ - @spec purchase(number, CreditCard.t(), keyword) :: {:ok | :error, Response} - def purchase(amount, card = %CreditCard{}, opts) when is_integer(amount) do - purchase(amount / 1, card, opts) - end - - def purchase(amount, card = %CreditCard{}, opts) when is_float(amount) do + @spec purchase(Money.t, CreditCard.t(), keyword) :: {:ok | :error, Response} + def purchase(amount, card = %CreditCard{}, opts) do params = card_params(card) ++ [ paymentType: "DB", - amount: :erlang.float_to_binary(amount, decimals: 2), - currency: currency(opts) + amount: amount |> Money.value |> Decimal.to_float |> :erlang.float_to_binary(decimals: 2), + currency: Money.currency(amount) ] auth_info = Keyword.fetch!(opts, :config) @@ -303,7 +290,6 @@ defmodule Gringotts.Gateways.Monei do authorization. Remember that our `capture/3` example only did a partial capture. - iex> opts = [currency: "EUR"] # The default currency is EUR, and this is just for an example. iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> void_result = Gringotts.void(Gringotts.Gateways.Monei, auth_result.id, opts) """ @@ -332,20 +318,16 @@ defmodule Gringotts.Gateways.Monei do The following session shows how one would refund a previous purchase (and similarily for captures). - iex> opts = [currency: "EUR"] # The default currency is EUR, and this is just for an example. + iex> amount = %{value: Decimal.new(42), currency: "EUR"} iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> refund_result = Gringotts.refund(Gringotts.Gateways.Monei, purchase_result.id, opts) + iex> refund_result = Gringotts.refund(Gringotts.Gateways.Monei, purchase_result.id, amount) """ - @spec refund(number, String.t(), keyword) :: {:ok | :error, Response} - def refund(amount, payment_id, opts) when is_integer(amount) do - refund(amount / 1, payment_id, opts) - end - + @spec refund(Money.t, String.t(), keyword) :: {:ok | :error, Response} def refund(amount, <>, opts) do params = [ paymentType: "RF", - amount: :erlang.float_to_binary(amount, decimals: 2), - currency: currency(opts) + amount: amount |> Money.value |> Decimal.to_float |> :erlang.float_to_binary(decimals: 2), + currency: Money.currency(amount) ] auth_info = Keyword.fetch!(opts, :config) @@ -372,9 +354,8 @@ defmodule Gringotts.Gateways.Monei do The following session shows how one would store a card (a payment-source) for future use. - iex> opts = [currency: "EUR"] # The default currency is EUR, and this is just for an example. iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> store_result = Gringotts.store(Gringotts.Gateways.Monei, card, opts) + iex> store_result = Gringotts.store(Gringotts.Gateways.Monei, card, []) """ @spec store(CreditCard.t(), keyword) :: {:ok | :error, Response} def store(%CreditCard{} = card, opts) do @@ -481,6 +462,5 @@ defmodule Gringotts.Gateways.Monei do end defp base_url(opts), do: opts[:test_url] || @base_url - defp currency(opts), do: opts[:currency] || @default_currency defp version(opts), do: opts[:api_version] || @version end diff --git a/lib/gringotts/money.ex b/lib/gringotts/money.ex new file mode 100644 index 00000000..431ce442 --- /dev/null +++ b/lib/gringotts/money.ex @@ -0,0 +1,55 @@ +defprotocol Gringotts.Money do + @moduledoc """ + Money protocol used by the Gringotts API. + + The `amount` argument required for some of Gringotts' API methods must + implement this protocol. + + If your application is already using a supported Money library, just pass in + the Money struct and things will work out of the box. + + Otherwise, just wrap your `amount` with the `currency` together in a `Map` like so, + money = %{amount: Decimal.new(2017.18), currency: "USD"} + + and the API will accept it (as long as the currency is valid [ISO 4217 currency + code](https://www.iso.org/iso-4217-currency-codes.html)). + """ + @fallback_to_any true + @type t :: Gringotts.Money.t + + @spec currency(t) :: String.t + @doc """ + Returns the ISO 4217 compliant currency code associated with this sum of money. + + This must be an UPCASE `string` + """ + def currency(money) + + @spec value(t) :: Decimal.t + @doc """ + Returns a Decimal representing the "worth" of this sum of money in the + associated `currency`. + """ + def value(money) +end + +# this implementation is used for dispatch on ex_money (and will also fire for +# money) +if Code.ensure_compiled?(Money) do + defimpl Gringotts.Money, for: Money do + def currency(money), do: money.currency |> Atom.to_string + def value(money), do: money.amount + end +end + +if Code.ensure_compiled?(Monetized.Money) do + defimpl Gringotts.Money, for: Monetized.Money do + def currency(money), do: money.currency + def value(money), do: money.amount + end +end + +defimpl Gringotts.Money, for: Any do + def currency(money), do: Map.get(money, :currency) + def value(money), do: Map.get(money, :amount) || Map.get(money, :value) +end diff --git a/mix.exs b/mix.exs index 14018e3d..63dcfbe7 100644 --- a/mix.exs +++ b/mix.exs @@ -52,13 +52,22 @@ defmodule Gringotts.Mixfile do {:httpoison, "~> 0.13"}, {:xml_builder, "~> 0.1.1"}, {:elixir_xml_to_map, "~> 0.1"}, + + # Money related + {:decimal, "~> 1.0", optional: true}, + # ex_money is just needed for tests. + {:ex_money, "~> 1.1.0", only: [:dev, :test], optional: true}, + + # docs and tests {:ex_doc, "~> 0.16", only: :dev, runtime: false}, {:mock, "~> 0.3.0", only: :test}, {:bypass, "~> 0.8", only: :test}, {:excoveralls, "~> 0.7", only: :test}, + + # various analyses tools {:credo, "~> 0.3", only: [:dev, :test]}, {:inch_ex, "~> 0.5", only: :docs}, - {:dialyxir, "~> 0.3", only: [:dev]} + {:dialyxir, "~> 0.3", only: :dev} ] end @@ -74,8 +83,14 @@ defmodule Gringotts.Mixfile do [ main: "Gringotts", logo: "images/lg.png", - source_url: "https://github.com/aviabird/gringotts" + source_url: "https://github.com/aviabird/gringotts", + groups_for_modules: groups_for_modules() ] end - + + defp groups_for_modules do + [ + "Gateways": ~r/^Gringotts.Gateways.?/, + ] + end end diff --git a/mix.lock b/mix.lock index 8ac3cafb..df5aabfe 100644 --- a/mix.lock +++ b/mix.lock @@ -1,14 +1,19 @@ -%{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, +%{"abnf2": {:hex, :abnf2, "0.1.2", "6f8792b8ac3288dba5fc889c2bceae9fe78f74e1a7b36bea9726ffaa9d7bef95", [:mix], [], "hexpm"}, + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "bypass": {:hex, :bypass, "0.8.1", "16d409e05530ece4a72fabcf021a3e5c7e15dcc77f911423196a0c551f2a15ca", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, "credo": {:hex, :credo, "0.8.10", "261862bb7363247762e1063713bb85df2bbd84af8d8610d1272cd9c1943bba63", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, + "decimal": {:hex, :decimal, "1.4.1", "ad9e501edf7322f122f7fc151cce7c2a0c9ada96f2b0155b8a09a795c2029770", [:mix], [], "hexpm"}, "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm"}, "elixir_xml_to_map": {:hex, :elixir_xml_to_map, "0.1.1", "57e924cd11731947bfd245ce57d0b8dd8b7168bf8edb20cd156a2982ca96fdfa", [:mix], [{:erlsom, "~>1.4", [hex: :erlsom, repo: "hexpm", optional: false]}], "hexpm"}, "erlsom": {:hex, :erlsom, "1.4.1", "53dbacf35adfea6f0714fd0e4a7b0720d495e88c5e24e12c5dc88c7b62bd3e49", [:rebar3], [], "hexpm"}, + "ex_cldr": {:hex, :ex_cldr, "1.1.0", "26f4a206307770b70139214ab820c5ed1f6241eb3394dd0db216ff95bf7e213a", [:mix], [{:abnf2, "~> 0.1", [hex: :abnf2, repo: "hexpm", optional: false]}, {:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:poison, "~> 2.1 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, + "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "1.1.0", "75904f202ca602eca5f3af572d56ed3d4a51543fecd08c9ab626ae2d876f44da", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 1.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:poison, "~> 2.1 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, + "ex_money": {:hex, :ex_money, "1.1.2", "4336192f1ac263900dfb4f63c1f71bc36a7cdee5d900e81937d3213be3360f9f", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}, {:ex_cldr, "~> 1.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 1.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}], "hexpm"}, "excoveralls": {:hex, :excoveralls, "0.8.0", "99d2691d3edf8612f128be3f9869c4d44b91c67cec92186ce49470ae7a7404cf", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, "hackney": {:hex, :hackney, "1.10.1", "c38d0ca52ea80254936a32c45bb7eb414e7a96a521b4ce76d00a69753b157f21", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/test/gateways/monei_test.exs b/test/gateways/monei_test.exs index 85b35125..3ea51b12 100644 --- a/test/gateways/monei_test.exs +++ b/test/gateways/monei_test.exs @@ -22,10 +22,12 @@ defmodule Gringotts.Gateways.MoneiTest do number: "4200000000000000", year: 2000, month: 12, - verification_code: "123", + verification_code: "123", brand: "VISA" } + @bad_currency Money.new(42, :INR) + @auth_success ~s[ {"id": "8a82944a603b12d001603c1a1c2d5d90", "result": { @@ -66,8 +68,7 @@ defmodule Gringotts.Gateways.MoneiTest do Bypass.expect_once bypass, "POST", "/v1/payments", fn conn -> Plug.Conn.resp(conn, 400, "") end - {:error, response} = Gateway.authorize(52, @card, [config: auth, - currency: "INR"]) + {:error, response} = Gateway.authorize(@bad_currency, @card, [config: auth]) assert response.code == :unsupported_currency end @@ -77,11 +78,11 @@ defmodule Gringotts.Gateways.MoneiTest do Plug.Conn.resp(conn, 200, @auth_success) end Bypass.down bypass - {:error, response} = Gateway.authorize(52.00, @card, [config: auth]) + {:error, response} = Gateway.authorize(Money.new(42, :USD), @card, [config: auth]) assert response.reason == :network_fail? Bypass.up bypass - {:ok, _} = Gateway.authorize(52.00, @card, [config: auth]) + {:ok, _} = Gateway.authorize(Money.new(42, :USD), @card, [config: auth]) end end @@ -90,7 +91,7 @@ defmodule Gringotts.Gateways.MoneiTest do Bypass.expect bypass, "POST", "/v1/payments", fn conn -> Plug.Conn.resp(conn, 200, @auth_success) end - {:ok, response} = Gateway.authorize(52.00, @card, [config: auth]) + {:ok, response} = Gateway.authorize(Money.new(42, :USD), @card, [config: auth]) assert response.code == "000.100.110" end @@ -98,14 +99,14 @@ defmodule Gringotts.Gateways.MoneiTest do Bypass.expect_once bypass, "POST", "/v1/payments", fn conn -> Plug.Conn.resp(conn, 400, "") end - {:error, _} = Gateway.authorize(52.00, @card, [config: auth]) + {:error, _} = Gateway.authorize(Money.new(42, :USD), @card, [config: auth]) end test "when card has expired.", %{bypass: bypass, auth: auth} do Bypass.expect_once bypass, "POST", "/v1/payments", fn conn -> Plug.Conn.resp(conn, 400, "") end - {:error, _response} = Gateway.authorize(52, @bad_card, [config: auth]) + {:error, _response} = Gateway.authorize(Money.new(42, :USD), @bad_card, [config: auth]) end end @@ -114,7 +115,7 @@ defmodule Gringotts.Gateways.MoneiTest do Bypass.expect_once bypass, "POST", "/v1/payments", fn conn -> Plug.Conn.resp(conn, 200, @auth_success) end - {:ok, response} = Gateway.purchase(15, @card, [config: auth]) + {:ok, response} = Gateway.purchase(Money.new(42, :USD), @card, [config: auth]) assert response.code == "000.100.110" end end @@ -135,11 +136,11 @@ defmodule Gringotts.Gateways.MoneiTest do Bypass.expect_once( bypass, "POST", - "/v1/payments/7214344252e11af79c0b9e7b4f3f6234", + "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> Plug.Conn.resp(conn, 200, @auth_success) end) - {:ok, response} = Gateway.capture(4000, "7214344252e11af79c0b9e7b4f3f6234", [config: auth]) + {:ok, response} = Gateway.capture(Money.new(42, :USD), "7214344242e11af79c0b9e7b4f3f6234", [config: auth]) assert response.code == "000.100.110" end end @@ -149,11 +150,11 @@ defmodule Gringotts.Gateways.MoneiTest do Bypass.expect_once( bypass, "POST", - "/v1/payments/7214344252e11af79c0b9e7b4f3f6234", + "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> Plug.Conn.resp(conn, 200, @auth_success) end) - {:ok, response} = Gateway.refund(3, "7214344252e11af79c0b9e7b4f3f6234", [config: auth]) + {:ok, response} = Gateway.refund(Money.new(3, :USD), "7214344242e11af79c0b9e7b4f3f6234", [config: auth]) assert response.code == "000.100.110" end end @@ -163,11 +164,11 @@ defmodule Gringotts.Gateways.MoneiTest do Bypass.expect_once( bypass, "DELETE", - "/v1/registrations/7214344252e11af79c0b9e7b4f3f6234", + "/v1/registrations/7214344242e11af79c0b9e7b4f3f6234", fn conn -> Plug.Conn.resp(conn, 200, "") end) - {:error, response} = Gateway.unstore("7214344252e11af79c0b9e7b4f3f6234", [config: auth]) + {:error, response} = Gateway.unstore("7214344242e11af79c0b9e7b4f3f6234", [config: auth]) assert response.code == :undefined_response_from_monei end end @@ -177,11 +178,11 @@ defmodule Gringotts.Gateways.MoneiTest do Bypass.expect_once( bypass, "POST", - "/v1/payments/7214344252e11af79c0b9e7b4f3f6234", + "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> Plug.Conn.resp(conn, 200, @auth_success) end) - {:ok, response} = Gateway.void("7214344252e11af79c0b9e7b4f3f6234", [config: auth]) + {:ok, response} = Gateway.void("7214344242e11af79c0b9e7b4f3f6234", [config: auth]) assert response.code == "000.100.110" end end diff --git a/test/integration/gateways/monei_test.exs b/test/integration/gateways/monei_test.exs index 516164ea..51f0f31c 100644 --- a/test/integration/gateways/monei_test.exs +++ b/test/integration/gateways/monei_test.exs @@ -26,7 +26,7 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do end test "authorize." do - case Gringotts.authorize(Gateway, 3.1, @card) do + case Gringotts.authorize(Gateway, Money.new(42, :EUR), @card) do {:ok, response} -> assert response.code == "000.100.110" assert response.description == "Request successfully processed in 'Merchant in Integrator Test Mode'" @@ -37,7 +37,7 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do @tag :skip test "capture." do - case Gringotts.capture(Gateway, 32.00, "s") do + case Gringotts.capture(Gateway, Money.new(42, :EUR), "s") do {:ok, response} -> assert response.code == "000.100.110" assert response.description == "Request successfully processed in 'Merchant in Integrator Test Mode'" @@ -48,7 +48,7 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do end test "purchase." do - case Gringotts.purchase(Gateway, 32, @card) do + case Gringotts.purchase(Gateway, Money.new(42, :EUR), @card) do {:ok, response} -> assert response.code == "000.100.110" assert response.description == "Request successfully processed in 'Merchant in Integrator Test Mode'" @@ -61,5 +61,4 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do config = Application.get_env(:gringotts, Gringotts.Gateways.Monei) assert config[:adapter] == Gringotts.Gateways.Monei end - end From 6fafa1ddccbbc3e163e1325d9e03ff0ff9ed21c6 Mon Sep 17 00:00:00 2001 From: Pankaj Kumar Rawat Date: Thu, 11 Jan 2018 00:48:28 +0530 Subject: [PATCH 12/60] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ebad2c06..e89605a0 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

- Gringotts is a payment processing library in Elixir integrating various payment gateways, this draws motivation for shopify's activemerchant gem. + Gringotts is a payment processing library in Elixir integrating various payment gateways, this draws motivation for shopify's activemerchant gem. Checkout the Demo here.

Build Status Coverage Status Docs coverage @@ -101,6 +101,7 @@ end [stripe]: https://www.stripe.com/ [trexle]: https://www.trexle.com/ [wirecard]: http://www.wirecard.com +[demo]: https://gringottspay.herokuapp.com/ ## Road Map From 9a4c89bbf66e3276e8b22bbe087b3a945acd56b4 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Tue, 9 Jan 2018 19:41:10 +0530 Subject: [PATCH 13/60] Adds a super useful mix task: gringotts.new * Generates a barebones implementation * Also generates docs dynamically, - If no required_keys are supplied when the task is run, the docs do not contain an empty table. - Otherwise, the table and config examples include the keys. --- lib/mix/new.ex | 97 +++++++++++++++ templates/gateway.eex | 275 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 372 insertions(+) create mode 100644 lib/mix/new.ex create mode 100644 templates/gateway.eex diff --git a/lib/mix/new.ex b/lib/mix/new.ex new file mode 100644 index 00000000..2165e47b --- /dev/null +++ b/lib/mix/new.ex @@ -0,0 +1,97 @@ +defmodule Mix.Tasks.Gringotts.New do + @shortdoc """ + Generates a barebones implementation for a gateway. + """ + + @moduledoc """ + Generates a barebones implementation for a gateway. + + It expects the (brand) name of the gateway as argument. This will not + necessarily be the module name, but we recommend the name be capitalized. + + mix gringotts.new NAME [-m, --module MODULE] [--url URL] + + A barebones implementation of the gateway will be created along with skeleton + mock and integration tests in `lib/gringotts/gateways/`. The command will + prompt for the module name, and other metadata. + + ## Options + + > ***Tip!*** + > You can supply the extra arguments to `gringotts.new` to skip (some of) the + > prompts. + + * `-m` `--module` - The module name for the Gateway. + * `--url` - The homepage of the gateway. + + ## Examples + + mix gringotts.new foobar + + The prompts for this will be: + MODULE = `Foobar` + URL = `https://www.foobar.com` + REQUIRED_KEYS = [] + """ + + use Mix.Task + import Mix.Generator + + @long_msg ~s{ +Comma separated list of required configuration keys: +(This can be skipped by hitting `Enter`) +> } + + def run(args) do + {key_list, [name], []} = + OptionParser.parse( + args, + switches: [module: :string, url: :string], + aliases: [m: :module] + ) + + Mix.Shell.IO.info("Generating barebones implementation for #{name}.") + Mix.Shell.IO.info("Hit enter to select the suggestion.") + + module_name = + case Keyword.fetch(key_list, :module) do + :error -> prompt_with_suggestion("\nModule name", String.capitalize(name)) + {:ok, mod_name} -> mod_name + end + + url = + case Keyword.fetch(key_list, :url) do + :error -> prompt_with_suggestion("\nHomepage URL", "https://www.#{String.Casing.downcase(name)}.com") + {:ok, url} -> url + end + + required_keys = + case Mix.Shell.IO.prompt(@long_msg) |> String.trim do + "" -> [] + keys -> String.split(keys, ",") |> Enum.map(&(String.trim(&1))) |> Enum.map(&(String.to_atom(&1))) + end + + bindings = [ + gateway: name, + gateway_module: module_name, + gateway_underscore: Macro.underscore(name), + required_config_keys: required_keys, + gateway_url: url + ] + + if (Mix.Shell.IO.yes? "\nDoes this look good?\n#{inspect(bindings, pretty: true)}\n>") do + gateway = EEx.eval_file("templates/gateway.eex", bindings) + # mock = "" + # integration = "" + create_file("lib/gringotts/gateways/#{bindings[:gateway_underscore]}.ex", gateway) + else + Mix.Shell.IO.info("Doing nothing, bye!") + end + end + + defp prompt_with_suggestion(message, suggestion) do + decorated_message = "#{message} [#{suggestion}]" + response = Mix.Shell.IO.prompt(decorated_message) |> String.trim + if response == "", do: suggestion, else: response + end +end diff --git a/templates/gateway.eex b/templates/gateway.eex new file mode 100644 index 00000000..78165a5e --- /dev/null +++ b/templates/gateway.eex @@ -0,0 +1,275 @@ +defmodule Gringotts.Gateways.<%= gateway_module %> do + @moduledoc """ + [<%= gateway %>][home] gateway implementation. + + ## Instructions! + + ***This is an example `moduledoc`, and suggests some items that should be + documented in here.*** + + The quotation boxes like the one below will guide you in writing excellent + documentation for your gateway. All our gateways are documented in this manner + and we aim to keep our docs as consistent with each other as possible. + **Please read them and do as they suggest**. Feel free to add or skip sections + though. + + If you'd like to make edits to the template docs, they exist at + `templates/gateway.eex`. We encourage you to make corrections and open a PR + and tag it with the label `template`. + + ***Actual docs begin below this line!*** + + -------------------------------------------------------------------------------- + + > List features that have been implemented, and what "actions" they map to as + > per the <%= gateway %> gateway docs. + > A table suits really well for this. + + ## Optional or extra parameters + + Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply + optional arguments for transactions with the gateway. + + > List all available (ie, those that will be supported by this module) keys, a + > description of their function/role and whether they have been implemented + > and tested. + > A table suits really well for this. + + ## Registering your <%= gateway %> account at `Gringotts` + + Explain how to make an account with the gateway and show how to put the + `required_keys` (like authentication info) to the configuration. + <%= if required_config_keys != [] do %> + > Here's how the secrets map to the required configuration parameters for <%= gateway %>: + > + > | Config parameter | <%= gateway %> secret | + > | ------- | ---- | + <%= for key <- required_config_keys do %>> | `<%= inspect(key) %>` | **<%= Macro.camelize("#{key}") %>** | + <% end %><% end %> + > Your Application config<%= if required_config_keys != [] do %> **must include the `<%= inspect(required_config_keys) %>` field(s)** and<% end %> would look + > something like this: + > + > config :gringotts, Gringotts.Gateways.<%= gateway_module %>, + > adapter: Gringotts.Gateways.<%= gateway_module %><%= if required_config_keys != [] do %>,<% end %> + <%= for key <- required_config_keys do %>> <%= "#{key}" %>: "your_secret_<%= "#{key}" %>" + <% end %> + + ## Scope of this module + + > It's unlikely that your first iteration will support all features of the + > gateway, so list down those items that are missing. + + ## Supported currencies and countries + + > It's enough if you just add a link to the gateway's docs or FAQ that provide + > info about this. + + ## Following the examples + + 1. First, set up a sample application and configure it to work with MONEI. + - You could do that from scratch by following our [Getting Started][gs] guide. + - To save you time, we recommend [cloning our example + repo][example] that gives you a pre-configured sample app ready-to-go. + + You could use the same config or update it the with your "secrets" + as described [above](#module-registering-your-monei-account-at-<%= + gateway %>). + + 2. Run an `iex` session with `iex -S mix` and add some variable bindings and + aliases to it (to save some time): + ``` + iex> alias Gringotts.{Response, CreditCard, Gateways.<%= gateway_module %>} + iex> card = %CreditCard{first_name: "Jo", + last_name: "Doe", + number: "4200000000000000", + year: 2099, month: 12, + verification_code: "123", brand: "VISA"} + ``` + + > Add any other frequently used bindings up here. + + We'll be using these in the examples below. + + [gs]: https://github.com/aviabird/gringotts/wiki/ + [home]: <%= gateway_url %> + [example]: https://github.com/aviabird/gringotts_example + """ + + # The Base module has the (abstract) public API, and some utility + # implementations. + use Gringotts.Gateways.Base + + # The Adapter module provides the `validate_config/1` + # Add the keys that must be present in the Application config in the + # `required_config` list + use Gringotts.Adapter, required_config: <%= inspect(required_config_keys) %> + + import Poison, only: [decode: 1] + + alias Gringotts.{Money, + CreditCard, + Response} + + @doc """ + Performs a (pre) Authorize operation. + + The authorization validates the `card` details with the banking network, + places a hold on the transaction `amount` in the customer’s issuing bank. + + > ** You could perhaps:** + > 1. describe what are the important fields in the Response struct + > 2. mention what a merchant can do with these important fields (ex: + > `capture/3`, etc.) + + ## Note + + > If there's anything noteworthy about this operation, it comes here. + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + def authorize(amount, card = %CreditCard{}, opts) do + # commit(args, ...) + end + + @doc """ + Captures a pre-authorized `amount`. + + `amount` is transferred to the merchant account by <%= gateway %> used in the + pre-authorization referenced by `payment_id`. + + ## Note + + > If there's anything noteworthy about this operation, it comes here. + > For example, does the gateway support partial, multiple captures? + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec capture(Money.t, String.t(), keyword) :: {:ok | :error, Response} + def capture(amount, payment_id, opts) do + # commit(args, ...) + end + + @doc """ + Transfers `amount` from the customer to the merchant. + + <%= gateway %> attempts to process a purchase on behalf of the customer, by + debiting `amount` from the customer's account by charging the customer's + `card`. + + ## Note + + > If there's anything noteworthy about this operation, it comes here. + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec purchase(Money.t, CreditCard.t(), keyword) :: {:ok | :error, Response} + def purchase(amount, card = %CreditCard{}, opts) do + # commit(args, ...) + end + + @doc """ + Voids the referenced payment. + + This method attempts a reversal of a previous transaction referenced by + `payment_id`. + + > As a consequence, the customer will never see any booking on his statement. + + ## Note + + > Which transactions can be voided? + > Is there a limited time window within which a void can be perfomed? + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec void(String.t(), keyword) :: {:ok | :error, Response} + def void(payment_id, opts) do + # commit(args, ...) + end + + @doc """ + Refunds the `amount` to the customer's account with reference to a prior transfer. + + > Refunds are allowed on which kinds of "prior" transactions? + + ## Note + + > The end customer will usually see two bookings/records on his statement. Is + > that true for <%= gateway %>? + > Is there a limited time window within which a void can be perfomed? + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec refund(Money.t, String.t(), keyword) :: {:ok | :error, Response} + def refund(amount, <>, opts) do + # commit(args, ...) + end + + @doc """ + Stores the payment-source data for later use. + + > This usually enable "One Click" and/or "Recurring Payments" + + ## Note + + > If there's anything noteworthy about this operation, it comes here. + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec store(CreditCard.t(), keyword) :: {:ok | :error, Response} + def store(%CreditCard{} = card, opts) do + # commit(args, ...) + end + + @doc """ + Removes card or payment info that was previously `store/2`d + + Deletes previously stored payment-source data. + + ## Note + + > If there's anything noteworthy about this operation, it comes here. + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec unstore(String.t(), keyword) :: {:ok | :error, Response} + def unstore(<>, opts) do + # commit(args, ...) + end + + ############################################################################### + # PRIVATE METHODS # + ############################################################################### + + # Makes the request to <%= gateway %>'s network. + # For consistency with other gateway implementations, make your (final) + # network request in here, and parse it using another private method called + # `respond`. + @spec commit(_) :: {:ok | :error, Response} + defp commit(_) do + # resp = HTTPoison.request(args, ...) + # respond(resp, ...) + end + + # Parses <%= gateway %>'s response and returns a `Gringotts.Response` struct + # in a `:ok`, `:error` tuple. + @spec respond(term) :: {:ok | :error, Response} + defp respond(<%= gateway_underscore %>_response) + defp respond({:ok, %{status_code: 200, body: body}}), do: "something" + defp respond({:ok, %{status_code: status_code, body: body}}), do: "something" + defp respond({:error, %HTTPoison.Error{} = error}), do: "something" +end From 365c3f12ab06b3618f48786664698afaae1bd7bb Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Fri, 19 Jan 2018 15:31:03 +0530 Subject: [PATCH 14/60] [money-protocol] Expansion: `to_string` and `to_integer` (#86) * Adds `to_string` and `to_integer` to the protocol * Fixes #62 and #85 * Implements all protocol methods for `ex_money` * Adds integration test with ex_money * Adapted Monei with protocol updates * Adds test for `Any` * The default rounding strategy for implementations of Gringotts.Money protocol is HALF-EVEN. * Updated public API docs with "perils of rounding". --- lib/gringotts.ex | 72 +++++++++------ lib/gringotts/gateways/monei.ex | 46 ++++++---- lib/gringotts/money.ex | 155 +++++++++++++++++++++++++++++--- test/integration/money.exs | 61 +++++++++++++ 4 files changed, 274 insertions(+), 60 deletions(-) create mode 100644 test/integration/money.exs diff --git a/lib/gringotts.ex b/lib/gringotts.ex index 0bd368e9..13d0eb07 100644 --- a/lib/gringotts.ex +++ b/lib/gringotts.ex @@ -27,24 +27,45 @@ defmodule Gringotts do This argument represents the "amount", annotated with the currency unit for the transaction. `amount` is polymorphic thanks to the `Gringotts.Money` - protocol which can be implemented by your custom Money type. + protocol which can even be implemented by your very own custom Money type! #### Note - We support [`ex_money`][ex_money] and [`monetized`][monetized] out of the - box, and you can drop their types in this argument and everything will work - as expected. - Otherwise, just wrap your `amount` with the `currency` together in a `Map` like so, - money = %{amount: Decimal.new(100.50), currency: "USD"} + Gringotts supports [`ex_money`][ex_money] out of the box, just drop `ex_money` + types in this argument and everything will work as expected. + + > Support for [`monetized`][monetized] and [`money`][money] is on the + > way, track it [here][iss-money-lib-support]! + Otherwise, just wrap your `amount` with the `currency` together in a `Map` like so, + money = %{value: Decimal.new("100.50"), currency: "USD"} + + > When this highly precise `amount` is serialized into the network request, we + > use a potentially lossy `Gringotts.Money.to_string/1` or + > `Gringotts.Money.to_integer/1` to perform rounding (if required) using the + > [`half-even`][wiki-half-even] strategy. + > + > **Hence, to ensure transparency, protect sanity and save _real_ money, we + > STRONGLY RECOMMEND that merchants perform any required rounding and handle + > remainders in their application logic -- before passing the `amount` to + > Gringotts's API.** + #### Example If you use `ex_money` in your project, and want to make an authorization for - $2.99 to MONEI, you'll do the following: + $2.99 to the `XYZ` Gateway, you'll do the following: + + # the money lib is aliased as "MoneyLib" - amount = Money.new(2.99, :USD) - Gringotts.authorize(Gringotts.Gateways.Monei, amount, some_card, extra_options) + amount = MoneyLib.new("2.99", :USD) + Gringotts.authorize(Gringotts.Gateways.XYZ, amount, some_card, extra_options) + [ex_money]: https://hexdocs.pm/ex_money/readme.html + [monetized]: https://hexdocs.pm/monetized/ + [money]: https://hexdocs.pm/money/Money.html + [iss-money-lib-support]: https://github.com/aviabird/gringotts/projects/3#card-6801146 + [wiki-half-even]: https://en.wikipedia.org/wiki/Rounding#Round_half_to_even + ### `card`, a payment source Gringotts provides a `Gringotts.CreditCard` type to hold card parameters @@ -52,6 +73,7 @@ defmodule Gringotts do card details. #### Note + Gringotts only supports payment by debit or credit card, even though the gateways might support payment via other instruments such as e-wallets, vouchers, bitcoins or banks. Support for these instruments is planned in @@ -71,10 +93,7 @@ defmodule Gringotts do `opts` is a `keyword` list of other options/information accepted by the gateway. The format, use and structure is gateway specific and documented in the Gateway's docs. - - [ex_money]: https://hexdocs.pm/ex_money/readme.html - [monetized]: https://hexdocs.pm/monetized/ - + ## Configuration Merchants must provide Gateway specific configuration in their application @@ -140,9 +159,9 @@ defmodule Gringotts do To (pre) authorize a payment of $4.20 on a sample `card` with the `XYZ` gateway, - amount = Money.new(4.2, :USD) - # IF YOU DON'T USE ex_money OR monetized - # amount = %{value: Decimal.new(4.2), currency: "EUR"} + amount = Money.new("4.2", :USD) + # IF YOU DON'T USE ex_money + # amount = %{value: Decimal.new("4.2"), currency: "EUR"} card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.XYZ, amount, card, opts) """ @@ -163,9 +182,9 @@ defmodule Gringotts do To capture $4.20 on a previously authorized payment worth $4.20 by referencing the obtained authorization `id` with the `XYZ` gateway, - amount = Money.new(4.2, :USD) - # IF YOU DON'T USE ex_money OR monetized - # amount = %{value: Decimal.new(4.2), currency: "EUR"} + amount = Money.new("4.2", :USD) + # IF YOU DON'T USE ex_money + # amount = %{value: Decimal.new("4.2"), currency: "EUR"} card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} Gringotts.capture(Gringotts.Gateways.XYZ, amount, auth_result.id, opts) """ @@ -191,9 +210,9 @@ defmodule Gringotts do To process a purchase worth $4.2, with the `XYZ` gateway, - amount = Money.new(4.2, :USD) - # IF YOU DON'T USE ex_money OR monetized - # amount = %{value: Decimal.new(4.2), currency: "EUR"} + amount = Money.new("4.2", :USD) + # IF YOU DON'T USE ex_money + # amount = %{value: Decimal.new("4.2"), currency: "EUR"} card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} Gringotts.purchase(Gringotts.Gateways.XYZ, amount, card, opts) """ @@ -212,9 +231,9 @@ defmodule Gringotts do To refund a previous purchase worth $4.20 referenced by `id`, with the `XYZ` gateway, - amount = Money.new(4.2, :USD) - # IF YOU DON'T USE ex_money OR monetized - # amount = %{value: Decimal.new(4.2), currency: "EUR"} + amount = Money.new("4.2", :USD) + # IF YOU DON'T USE ex_money + # amount = %{value: Decimal.new("4.2"), currency: "EUR"} Gringotts.purchase(Gringotts.Gateways.XYZ, amount, id, opts) """ def refund(gateway, amount, id, opts \\ []) do @@ -282,9 +301,6 @@ defmodule Gringotts do call(:payment_worker, {:void, gateway, id, opts}) end - - # TODO: This is runtime error reporting fix this so that it does compile - # time error reporting. defp validate_config(gateway) do # Keep the key name and adapter the same in the config in application config = Application.get_env(:gringotts, gateway) diff --git a/lib/gringotts/gateways/monei.ex b/lib/gringotts/gateways/monei.ex index 4087bf0c..bebf8eb1 100644 --- a/lib/gringotts/gateways/monei.ex +++ b/lib/gringotts/gateways/monei.ex @@ -178,17 +178,19 @@ defmodule Gringotts.Gateways.Monei do The following session shows how one would (pre) authorize a payment of $40 on a sample `card`. iex> amount = %{value: Decimal.new(42), currency: "EUR"} - iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} + iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> auth_result = Gringotts.authorize(Gringotts.Gateways.Monei, amount, card, opts) iex> auth_result.id # This is the authorization ID """ - @spec authorize(Money.t, CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} def authorize(amount, card = %CreditCard{}, opts) do + {currency, value} = Money.to_string(amount) + params = [ paymentType: "PA", - amount: amount |> Money.value |> Decimal.to_float |> :erlang.float_to_binary(decimals: 2), - currency: Money.currency(amount) + amount: value, + currency: currency ] ++ card_params(card) auth_info = Keyword.fetch!(opts, :config) @@ -215,17 +217,19 @@ defmodule Gringotts.Gateways.Monei do authorized a payment worth $35 by referencing the obtained authorization `id`. iex> amount = %{value: Decimal.new(42), currency: "EUR"} - iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} + iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> capture_result = Gringotts.capture(Gringotts.Gateways.Monei, 35, auth_result.id, opts) """ - @spec capture(Money.t, String.t(), keyword) :: {:ok | :error, Response} + @spec capture(Money.t(), String.t(), keyword) :: {:ok | :error, Response} def capture(amount, payment_id, opts) def capture(amount, <>, opts) do + {currency, value} = Money.to_string(amount) + params = [ paymentType: "CP", - amount: amount |> Money.value |> Decimal.to_float |> :erlang.float_to_binary(decimals: 2), - currency: Money.currency(amount) + amount: value, + currency: currency ] auth_info = Keyword.fetch!(opts, :config) @@ -243,18 +247,20 @@ defmodule Gringotts.Gateways.Monei do The following session shows how one would process a payment in one-shot, without (pre) authorization. - iex> amount = %{value: Decimal.new(42), currency: "EUR"} - iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} + iex> amount = %{value: Decimal.new(42), currency: "EUR"} + iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> purchase_result = Gringotts.purchase(Gringotts.Gateways.Monei, amount, card, opts) """ - @spec purchase(Money.t, CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} def purchase(amount, card = %CreditCard{}, opts) do + {currency, value} = Money.to_string(amount) + params = card_params(card) ++ [ paymentType: "DB", - amount: amount |> Money.value |> Decimal.to_float |> :erlang.float_to_binary(decimals: 2), - currency: Money.currency(amount) + amount: value, + currency: currency ] auth_info = Keyword.fetch!(opts, :config) @@ -290,7 +296,7 @@ defmodule Gringotts.Gateways.Monei do authorization. Remember that our `capture/3` example only did a partial capture. - iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} + iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> void_result = Gringotts.void(Gringotts.Gateways.Monei, auth_result.id, opts) """ @spec void(String.t(), keyword) :: {:ok | :error, Response} @@ -319,15 +325,17 @@ defmodule Gringotts.Gateways.Monei do similarily for captures). iex> amount = %{value: Decimal.new(42), currency: "EUR"} - iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} + iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> refund_result = Gringotts.refund(Gringotts.Gateways.Monei, purchase_result.id, amount) """ - @spec refund(Money.t, String.t(), keyword) :: {:ok | :error, Response} + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} def refund(amount, <>, opts) do + {currency, value} = Money.to_string(amount) + params = [ paymentType: "RF", - amount: amount |> Money.value |> Decimal.to_float |> :erlang.float_to_binary(decimals: 2), - currency: Money.currency(amount) + amount: value, + currency: currency ] auth_info = Keyword.fetch!(opts, :config) @@ -354,7 +362,7 @@ defmodule Gringotts.Gateways.Monei do The following session shows how one would store a card (a payment-source) for future use. - iex> card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} + iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> store_result = Gringotts.store(Gringotts.Gateways.Monei, card, []) """ @spec store(CreditCard.t(), keyword) :: {:ok | :error, Response} diff --git a/lib/gringotts/money.ex b/lib/gringotts/money.ex index 431ce442..304cc764 100644 --- a/lib/gringotts/money.ex +++ b/lib/gringotts/money.ex @@ -8,38 +8,148 @@ defprotocol Gringotts.Money do If your application is already using a supported Money library, just pass in the Money struct and things will work out of the box. - Otherwise, just wrap your `amount` with the `currency` together in a `Map` like so, - money = %{amount: Decimal.new(2017.18), currency: "USD"} + Otherwise, just wrap your `amount` with the `currency` together in a `Map` + like so, - and the API will accept it (as long as the currency is valid [ISO 4217 currency - code](https://www.iso.org/iso-4217-currency-codes.html)). + price = %{value: Decimal.new("20.18"), currency: "USD"} + + and the API will accept it (as long as the currency is valid [ISO 4217 + currency code](https://www.iso.org/iso-4217-currency-codes.html)). + + ## Note on the `Any` implementation + + Both `to_string/1` and `to_integer/1` assume that the precision for the `currency` + is 2 digits after decimal. """ @fallback_to_any true - @type t :: Gringotts.Money.t - - @spec currency(t) :: String.t + @type t :: Gringotts.Money.t() + @doc """ Returns the ISO 4217 compliant currency code associated with this sum of money. This must be an UPCASE `string` """ + @spec currency(t) :: String.t() def currency(money) - @spec value(t) :: Decimal.t @doc """ - Returns a Decimal representing the "worth" of this sum of money in the + Returns a `Decimal.t` representing the "worth" of this sum of money in the associated `currency`. """ + @spec value(t) :: Decimal.t() def value(money) + + @doc """ + Returns the ISO4217 `currency` code as string and `value` as an integer. + + Useful for gateways that require amount as integer (like cents instead of + dollars). + + ## Note + + Conversion from `Decimal.t` to `integer` is potentially lossy and the rounding + (if required) is performed (automatically) by the Money library defining the + type, or in the implementation of this protocol method. + + If you want to implement this method for your custom type, please ensure that + the rounding strategy (if any rounding is applied) must be + [`half_even`][wiki-half-even]. + + **To keep things predictable and transparent, merchants should round the + `amount` themselves**, perhaps by explicitly calling the relevant method of + the Money library in their application _before_ passing it to `Gringotts`'s + public API. + + ## Examples + + # the money lib is aliased as "MoneyLib" + + iex> usd_price = MoneyLib.new("4.1234", :USD) + #MoneyLib<4.1234, "USD"> + iex> Gringotts.Money.to_integer(usd_price) + {"USD", 412, -2} + + iex> bhd_price = MoneyLib.new("4.1234", :BHD) + #MoneyLib<4.1234, "BHD"> + iex> Gringotts.Money.to_integer(bhd_price) + {"BHD", 4123, -3} + # the Bahraini dinar is divided into 1000 fils unlike the dollar which is + # divided in 100 cents + + [wiki-half-even]: https://en.wikipedia.org/wiki/Rounding#Round_half_to_even + """ + @spec to_integer(Money.t()) :: + {currency :: String.t(), value :: integer, exponent :: neg_integer} + def to_integer(money) + + @doc """ + Returns a tuple of ISO4217 `currency` code and the `value` as strings. + + The stringified `value` must match this regex: `~r/-?\\d+\\.\\d\\d{n}/` where + `n+1` should match the required precision for the `currency`. There should be + no place value separators except the decimal point (like commas). + + > Gringotts will not (and cannot) validate this of course. + + ## Note + + Conversion from `Decimal.t` to `string` is potentially lossy and the rounding + (if required) is performed (automatically) by the Money library defining the + type, or in the implementation of this protocol method. + + If you want to implement this method for your custom type, please ensure that + the rounding strategy (if any rounding is applied) must be + [`half_even`][wiki-half-even]. + + **To keep things predictable and transparent, merchants should round the + `amount` themselves**, perhaps by explicitly calling the relevant method of + the Money library in their application _before_ passing it to `Gringotts`'s + public API. + + ## Examples + + # the money lib is aliased as "MoneyLib" + + iex> usd_price = MoneyLib.new("4.1234", :USD) + #MoneyLib<4.1234, "USD"> + iex> Gringotts.Money.to_string(usd_price) + {"USD", "4.12"} + + iex> bhd_price = MoneyLib.new("4.1234", :BHD) + #MoneyLib<4.1234, "BHD"> + iex> Gringotts.Money.to_string(bhd_price) + {"BHD", "4.123"} + + [wiki-half-even]: https://en.wikipedia.org/wiki/Rounding#Round_half_to_even + """ + @spec to_string(t) :: {currency :: String.t(), value :: String.t()} + def to_string(money) end # this implementation is used for dispatch on ex_money (and will also fire for # money) if Code.ensure_compiled?(Money) do defimpl Gringotts.Money, for: Money do - def currency(money), do: money.currency |> Atom.to_string + def currency(money), do: money.currency |> Atom.to_string() def value(money), do: money.amount - end + + def to_integer(money) do + {_, int_value, exponent, _} = Money.to_integer_exp(money) + {currency(money), int_value, exponent} + end + + def to_string(money) do + {:ok, currency_data} = Cldr.Currency.currency_for_code(currency(money)) + reduced = Money.reduce(money) + + { + currency(reduced), + value(reduced) + |> Decimal.round(currency_data.digits) + |> Decimal.to_string() + } + end + end end if Code.ensure_compiled?(Monetized.Money) do @@ -49,7 +159,26 @@ if Code.ensure_compiled?(Monetized.Money) do end end +# Assumes that the currency is subdivided into 100 units defimpl Gringotts.Money, for: Any do - def currency(money), do: Map.get(money, :currency) - def value(money), do: Map.get(money, :amount) || Map.get(money, :value) + def currency(%{currency: currency}), do: currency + def value(%{value: %Decimal{} = value}), do: value + + def to_integer(%{value: %Decimal{} = value, currency: currency}) do + { + currency, + value + |> Decimal.mult(Decimal.new(100)) + |> Decimal.round(0) + |> Decimal.to_integer(), + -2 + } + end + + def to_string(%{value: %Decimal{} = value, currency: currency}) do + { + currency, + value |> Decimal.round(2) |> Decimal.to_string() + } + end end diff --git a/test/integration/money.exs b/test/integration/money.exs new file mode 100644 index 00000000..ca42febe --- /dev/null +++ b/test/integration/money.exs @@ -0,0 +1,61 @@ +defmodule Gringotts.Integration.Gateways.MoneyTest do + use ExUnit.Case, async: true + + alias Gringotts.Money, as: MoneyProtocol + + @moduletag :integration + + @ex_money Money.new(42, :EUR) + @ex_money_long Money.new("42.126456", :EUR) + @ex_money_bhd Money.new(42, :BHD) + + @any %{value: Decimal.new(42), currency: "EUR"} + @any_long %{value: Decimal.new("42.126456"), currency: "EUR"} + @any_bhd %{value: Decimal.new("42"), currency: "BHD"} + + describe "ex_money" do + test "value is a Decimal.t" do + assert match? %Decimal{}, MoneyProtocol.value(@ex_money) + end + + test "currency is an upcase String.t" do + the_currency = MoneyProtocol.currency(@ex_money) + assert match? currency when is_binary(currency), the_currency + assert the_currency == String.upcase(the_currency) + end + + test "to_integer" do + assert match? {"EUR", 4200, -2}, MoneyProtocol.to_integer(@ex_money) + assert match? {"BHD", 42000, -3}, MoneyProtocol.to_integer(@ex_money_bhd) + end + + test "to_string" do + assert match? {"EUR", "42.00"}, MoneyProtocol.to_string(@ex_money) + assert match? {"EUR", "42.13"}, MoneyProtocol.to_string(@ex_money_long) + assert match? {"BHD", "42.000"}, MoneyProtocol.to_string(@ex_money_bhd) + end + end + + describe "Any" do + test "value is a Decimal.t" do + assert match? %Decimal{}, MoneyProtocol.value(@any) + end + + test "currency is an upcase String.t" do + the_currency = MoneyProtocol.currency(@any) + assert match? currency when is_binary(currency), the_currency + assert the_currency == String.upcase(the_currency) + end + + test "to_integer" do + assert match? {"EUR", 4200, -2}, MoneyProtocol.to_integer(@any) + assert match? {"BHD", 4200, -2}, MoneyProtocol.to_integer(@any_bhd) + end + + test "to_string" do + assert match? {"EUR", "42.00"}, MoneyProtocol.to_string(@any) + assert match? {"EUR", "42.13"}, MoneyProtocol.to_string(@any_long) + assert match? {"BHD", "42.00"}, MoneyProtocol.to_string(@any_bhd) + end + end +end From 6d1828f3b15b06ec7fc7706b32633c06c9cb3b81 Mon Sep 17 00:00:00 2001 From: arjun289 Date: Wed, 17 Jan 2018 23:35:17 +0530 Subject: [PATCH 15/60] add mix task for tests, mocks and integration Added mix task to generate test file for the gateways, add a file to define the mock responses and also to generate integration test. --- lib/mix/new.ex | 13 ++++++++++--- templates/integration.eex | 36 ++++++++++++++++++++++++++++++++++++ templates/mock_response.eex | 9 +++++++++ templates/test.eex | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 templates/integration.eex create mode 100644 templates/mock_response.eex create mode 100644 templates/test.eex diff --git a/lib/mix/new.ex b/lib/mix/new.ex index 2165e47b..4f2bb609 100644 --- a/lib/mix/new.ex +++ b/lib/mix/new.ex @@ -76,14 +76,21 @@ Comma separated list of required configuration keys: gateway_module: module_name, gateway_underscore: Macro.underscore(name), required_config_keys: required_keys, - gateway_url: url + gateway_url: url, + gateway_mock_test: Macro.underscore(name) <> "_test", + gateway_mock_response: Macro.underscore(name) <> "_mock", ] if (Mix.Shell.IO.yes? "\nDoes this look good?\n#{inspect(bindings, pretty: true)}\n>") do gateway = EEx.eval_file("templates/gateway.eex", bindings) - # mock = "" - # integration = "" + mock = EEx.eval_file("templates/test.eex", bindings) + mock_response = EEx.eval_file("templates/mock_response.eex", bindings) + integration = EEx.eval_file("templates/integration.eex", bindings) + create_file("lib/gringotts/gateways/#{bindings[:gateway_underscore]}.ex", gateway) + create_file("test/gateways/#{bindings[:gateway_mock_test]}.exs", mock) + create_file("test/mocks/#{bindings[:gateway_mock_response]}.exs", mock_response) + create_file("test/integration/gateways/#{bindings[:gateway_mock_test]}.exs", integration) else Mix.Shell.IO.info("Doing nothing, bye!") end diff --git a/templates/integration.eex b/templates/integration.eex new file mode 100644 index 00000000..65fc4bff --- /dev/null +++ b/templates/integration.eex @@ -0,0 +1,36 @@ +defmodule Gringotts.Integration.Gateways.<%= gateway_module <> "Test"%> do + # Integration tests for the <%= gateway_module%> + + use ExUnit.Case, async: false + alias Gringotts.Gateways.<%= gateway_module%> + + @moduletag :integration + + setup_all do + Application.put_env(:gringotts, Gringotts.Gateways.<%= gateway_module%>, + [ + adapter: Gringotts.Gateways.<%= gateway_module%><%= if required_config_keys != [] do %><%= for key <- Enum.intersperse(required_config_keys, ",") do %><%= if key === "," do %><%= "#{key}" %><%= else %> + <%= "#{key}" %>: "your_secret_<%= "#{key}" %>"<% end %><% end %><% end %> + ] + ) + end + + # Group the test cases by public api + describe "purchase" do + end + + describe "authorize" do + end + + describe "capture" do + end + + describe "void" do + end + + describe "refund" do + end + + describe "environment setup" do + end +end diff --git a/templates/mock_response.eex b/templates/mock_response.eex new file mode 100644 index 00000000..1b0e5b63 --- /dev/null +++ b/templates/mock_response.eex @@ -0,0 +1,9 @@ +defmodule Gringotts.Gateways.<%= gateway_module <> "Mock"%> do + + # The module should include mock responses for test cases in <%= gateway_mock_test <> ".exs"%>. + # e.g. + # def successful_purchase do + # {:ok, %HTTPoison.Response{body: ~s[{data: "successful_purchase"}]} + # end + +end diff --git a/templates/test.eex b/templates/test.eex new file mode 100644 index 00000000..db4bb72f --- /dev/null +++ b/templates/test.eex @@ -0,0 +1,32 @@ +defmodule Gringotts.Gateways.<%= gateway_module <> "Test" %> do + # The file contains mocked tests for <%= gateway_module%> + + # We recommend using [mock][1] for this, you can place the mock responses from + # the Gateway in `test/mocks/<%= gateway_mock_response%>.exs` file, which has also been + # generated for you. + # + # [1]: https://github.com/jjh42/mock + + # Load the mock response file before running the tests. + Code.require_file "../mocks/<%= gateway_mock_response <> ".exs"%>", __DIR__ + + use ExUnit.Case, async: false + alias Gringotts.Gateways.<%= gateway_module%> + import Mock + + # Group the test cases by public api + describe "purchase" do + end + + describe "authorize" do + end + + describe "capture" do + end + + describe "void" do + end + + describe "refund" do + end +end From a13569227ada1d165960aa8603c8ade48d4552a4 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Sun, 21 Jan 2018 23:50:22 +0530 Subject: [PATCH 16/60] [monei] Implements optional/extra params (#79) * Validates currency, test for unsupported currency * Added EUR to the supported currencies. * Refactored expansion and validation of extras * Removed duplicate code that extracted auth_info from opts * Part of the params to Monei are built in the methods, all extra params are built and validated in commit. * [extra-params] All optional params covered: billing, shipping and merchant, invoice_id, category, transaction_id, custom, register, customer * Added examples of these in docs and integration tests * The inclusion was (partially) implicitly tested in the integration tests, but I moved it to more visible mock tests. --- README.md | 6 +- lib/gringotts/gateways/monei.ex | 383 ++++++++++++++++------- test/gateways/monei_test.exs | 228 ++++++++++---- test/integration/gateways/monei_test.exs | 111 +++++-- 4 files changed, 535 insertions(+), 193 deletions(-) diff --git a/README.md b/README.md index e89605a0..42e56c2f 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,8 @@ alias Gringotts.Gateways.Monei alias Gringotts.{CreditCard} card = %CreditCard{ - first_name: "Harry Potter", - last_name: "Doe", + first_name: "Harry", + last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", @@ -88,7 +88,7 @@ end | ------ | ----- | | [Authorize.Net][anet] | AD, AT, AU, BE, BG, CA, CH, CY, CZ, DE, DK, ES, FI, FR, GB, GB, GI, GR, HU, IE, IT, LI, LU, MC, MT, NL, NO, PL, PT, RO, SE, SI, SK, SM, TR, US, VA | | [CAMS][cams] | AU, US | -| [MONEI][anet] | DE, EE, ES, FR, IT, US | +| [MONEI][monei] | DE, EE, ES, FR, IT, US | | [PAYMILL][paymill] | AD, AT, BE, BG, CH, CY, CZ, DE, DK, EE, ES, FI, FO, FR, GB, GI, GR, HU, IE, IL, IS, IT, LI, LT, LU, LV, MT, NL, NO, PL, PT, RO, SE, SI, SK, TR, VA | | [Stripe][stripe] | AT, AU, BE, CA, CH, DE, DK, ES, FI, FR, GB, IE, IN, IT, LU, NL, NO, SE, SG, US | | [TREXLE][trexle] | AD, AE, AT, AU, BD, BE, BG, BN, CA, CH, CY, CZ, DE, DK, EE, EG, ES, FI, FR, GB, GI, GR, HK, HU, ID, IE, IL, IM, IN, IS, IT, JO, KW, LB, LI, LK, LT, LU, LV, MC, MT, MU, MV, MX, MY, NL, NO, NZ, OM, PH, PL, PT, QA, RO, SA, SE, SG, SI, SK, SM, TR, TT, UM, US, VA, VN, ZA | diff --git a/lib/gringotts/gateways/monei.ex b/lib/gringotts/gateways/monei.ex index bebf8eb1..124f1fbc 100644 --- a/lib/gringotts/gateways/monei.ex +++ b/lib/gringotts/gateways/monei.ex @@ -1,8 +1,8 @@ defmodule Gringotts.Gateways.Monei do @moduledoc """ - [MONEI](https://www.monei.net) gateway implementation. + [MONEI][home] gateway implementation. - For reference see [MONEI's API (v1) documentation](https://docs.monei.net). + For reference see [MONEI's API (v1) documentation][docs]. The following features of MONEI are implemented: @@ -15,35 +15,51 @@ defmodule Gringotts.Gateways.Monei do | Debit | `purchase/3` | `DB` | | Tokenization / Registrations | `store/2` | | - > **What's this last column `type`?**\ + > **What's this last column `type`?** + > > That's the `paymentType` of the request, which you can ignore unless you'd - > like to contribute to this module. Please read the [MONEI - > Guides](https://docs.monei.net). + > like to contribute to this module. Please read the [MONEI Guides][docs]. - ## The `opts` argument - - Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply - optional arguments for transactions with the MONEI gateway. The following keys - are supported: + [home]: https://monei.net + [docs]: https://docs.monei.net - | Key | Remark | Status | - | ---- | --- | ---- | - | `billing_address` | | Not implemented | - | `cart` | | Not implemented | - | `customParameters` | | Not implemented | - | `customer` | | Not implemented | - | `invoice` | | Not implemented | - | `merchant` | | Not implemented | - | `shipping_address` | | Not implemented | - | `shipping_customer` | | Not implemented | + ## The `opts` argument - > All these keys are being implemented, track progress in - > [issue #36](https://github.com/aviabird/gringotts/issues/36)! + Most `Gringotts` API calls accept an optional `keyword` list `opts` to supply + [optional arguments][extra-arg-docs] for transactions with the MONEI + gateway. The following keys are supported: + + | Key | Remark | + | ---- | --- | + | [`billing`][ba] | Address of the customer, which can be used for AVS risk check. | + | [`cart`][cart] | **Not Implemented** | + | [`custom`][custom] | It's a map of "name"-"value" pairs, and all of it is echoed back in the response. | + | [`customer`][c] | Annotate transactions with customer info on your Monei account, and helps in risk management. | + | [`invoice_id`][b] | Merchant provided invoice identifier, must be unique per transaction with Monei. | + | [`transaction_id`][b] | Merchant provided token for a transaction, must be unique per transaction with Monei. | + | [`category`][b] | The category of the transaction. | + | [`merchant`][m] | Information about the merchant, which overrides the cardholder's bank statement. | + | [`register`][t] | Also store payment data included in this request for future use. | + | [`shipping`][sa] | Location of recipient of goods, for logistics. | + | [`shipping_customer`][c] | Recipient details, could be different from `customer`. | + + > These keys are being implemented, track progress in [issue #36][iss36]! + + [extra-arg-docs]: https://docs.monei.net/reference/parameters + [ba]: https://docs.monei.net/reference/parameters#billing-address + [cart]: https://docs.monei.net/reference/parameters#cart + [custom]: https://docs.monei.net/reference/parameters#custom-parameters + [c]: https://docs.monei.net/reference/parameters#customer + [b]: https://docs.monei.net/reference/parameters#basic + [m]: https://docs.monei.net/reference/parameters#merchant + [t]: https://docs.monei.net/reference/parameters#tokenization + [sa]: https://docs.monei.net/reference/parameters#shipping-address + [iss36]: https://github.com/aviabird/gringotts/issues/36 ## Registering your MONEI account at `Gringotts` - After [making an account on MONEI](https://dashboard.monei.net/signin), head - to the dashboard and find your account "secrets" in the `Sub-Accounts > Overview` section. + After [making an account on MONEI][dashboard], head to the dashboard and find + your account "secrets" in the `Sub-Accounts > Overview` section. Here's how the secrets map to the required configuration parameters for MONEI: @@ -62,43 +78,52 @@ defmodule Gringotts.Gateways.Monei do password: "your_secret_password", entityId: "your_secret_channel_id" + [dashboard]: https://dashboard.monei.net/signin + + ## Scope of this module + + * MONEI does not process money in cents, and the `amount` is rounded to 2 + decimal places. + * Although MONEI supports payments from [various][all-card-list] + [cards][card-acc], [banks][bank-acc] and [virtual accounts][virtual-acc] + (like some wallets), this library only accepts payments by [(supported) + cards][all-card-list]. + + [all-card-list]: https://support.monei.net/charges-and-refunds/accepted-credit-cards-payment-methods + [card-acc]: https://docs.monei.net/reference/parameters#card + [bank-acc]: https://docs.monei.net/reference/parameters#bank-account + [virtual-acc]: https://docs.monei.net/reference/parameters#virtual-account - ## Scope of this module, and _quirks_ + ## Supported countries - * MONEI does not process money in cents, and the `amount` is rounded to 2 decimal places. - * Although MONEI supports payments from [various - cards](https://support.monei.net/charges-and-refunds/accepted-credit-cards-payment-methods), - banks and virtual accounts (like some wallets), this library only accepts - payments by (supported) cards. + MONEI supports the countries listed [here][all-country-list] - ## Supported currencies and countries + ## Supported currencies - The following currencies are supported: `USD`, `GBP`, `NAD`, `TWD`, `VUV`, - `NZD`, `NGN`, `NIO`, `NGN`, `NOK`, `PKR`, `PAB`, `PGK`, `PYG`, `PEN`, `NPR`, - `ANG`, `AWG`, `PHP`, `QAR`, `RUB`, `RWF`, `SHP`, `STD`, `SAR`, `SCR`, `SLL`, - `SGD`, `VND`, `SOS`, `ZAR`, `ZWL`, `YER`, `SDG`, `SZL`, `SEK`, `CHF`, `SYP`, - `TJS`, `THB`, `TOP`, `TTD`, `AED`, `TND`, `TRY`, `AZN`, `UGX`, `MKD`, `EGP`, - `GBP`, `TZS`, `UYU`, `UZS`, `WST`, `YER`, `RSD`, `ZMW`, `TWD`, `AZN`, `GHS`, - `RSD`, `MZN`, `AZN`, `MDL`, `TRY`, `XAF`, `XCD`, `XOF`, `XPF`, `MWK`, `SRD`, - `MGA`, `AFN`, `TJS`, `AOA`, `BYN`, `BGN`, `CDF`, `BAM`, `UAH`, `GEL`, `PLN`, - `BRL` and `CUC`. + MONEI supports the currecncies [listed here][all-currency-list], and ***this + module*** supports a subset of those: - > [Here](https://support.monei.net/international/currencies-supported-by-monei) - > is the up-to-date currency list. _Please [raise an - > issue](https://github.com/aviabird/gringotts/issues) if the list above has - > become out-of-date!_ + :AED, :AFN, :ANG, :AOA, :AWG, :AZN, :BAM, :BGN, :BRL, :BYN, :CDF, :CHF, :CUC, + :EGP, :EUR, :GBP, :GEL, :GHS, :MDL, :MGA, :MKD, :MWK, :MZN, :NAD, :NGN, :NIO, + :NOK, :NPR, :NZD, :PAB, :PEN, :PGK, :PHP, :PKR, :PLN, :PYG, :QAR, :RSD, :RUB, + :RWF, :SAR, :SCR, :SDG, :SEK, :SGD, :SHP, :SLL, :SOS, :SRD, :STD, :SYP, :SZL, + :THB, :TJS, :TOP, :TRY, :TTD, :TWD, :TZS, :UAH, :UGX, :USD, :UYU, :UZS, :VND, + :VUV, :WST, :XAF, :XCD, :XOF, :XPF, :YER, :ZAR, :ZMW, :ZWL - MONEI supports the countries listed - [here](https://support.monei.net/international/what-countries-does-monei-support). + > Please [raise an issue][new-issue] if you'd like us to add support for more + > currencies + + [all-currency-list]: https://support.monei.net/international/currencies-supported-by-monei + [new-issue]: https://github.com/aviabird/gringotts/issues + [all-country-list]: https://support.monei.net/international/what-countries-does-monei-support ## Following the examples 1. First, set up a sample application and configure it to work with MONEI. - You could do that from scratch by following our [Getting Started](#) guide. - - To save you time, we recommend [cloning our example - repo](https://github.com/aviabird/gringotts_example) that gives you a - pre-configured sample app ready-to-go. - + You could use the same config or update it the with your "secrets" + - To save you time, we recommend [cloning our example repo][example-repo] + that gives you a pre-configured sample app ready-to-go. + + You could use the same config or update it the with your "secrets" that you see in `Dashboard > Sub-accounts` as described [above](#module-registering-your-monei-account-at-gringotts). @@ -111,12 +136,48 @@ defmodule Gringotts.Gateways.Monei do last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, - verification_code: "123", + verification_code: "123", brand: "VISA"} + iex> customer = %{"givenName": "Harry", + "surname": "Potter", + "merchantCustomerId": "the_boy_who_lived", + "sex": "M", + "birthDate": "1980-07-31", + "mobile": "+15252525252", + "email": "masterofdeath@ministryofmagic.gov", + "ip": "1.1.1", + "status": "NEW"} + iex> merchant = %{"name": "Ollivanders", + "city": "South Side", + "street": "Diagon Alley", + "state": "London", + "country": "GB", + "submerchantId": "Makers of Fine Wands since 382 B.C."} + iex> billing = %{"street1": "301, Gryffindor", + "street2": "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", + "city": "Highlands", + "state": "Scotland", + "country": "GB"} + iex> shipping = %{"street1": "301, Gryffindor", + "street2": "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", + "city": "Highlands", + "state": "Scotland", + "country": "GB", + "method": "SAME_DAY_SERVICE", + "comment": "For our valued customer, Mr. Potter"} + iex> opts = [customer: customer, + merchant: merchant, + billing: billing, + shipping: shipping, + category: "EC", + custom: %{"voldemort": "he who must not be named"}, + register: true] ``` We'll be using these in the examples below. + [example-repo]: https://github.com/aviabird/gringotts_example + ## TODO * [Backoffice operations](https://docs.monei.net/tutorials/manage-payments/backoffice) @@ -134,6 +195,16 @@ defmodule Gringotts.Gateways.Monei do @base_url "https://test.monei-api.net" @default_headers ["Content-Type": "application/x-www-form-urlencoded", charset: "UTF-8"] + @supported_currencies [ + "AED", "AFN", "ANG", "AOA", "AWG", "AZN", "BAM", "BGN", "BRL", "BYN", "CDF", + "CHF", "CUC", "EGP", "EUR", "GBP", "GEL", "GHS", "MDL", "MGA", "MKD", "MWK", + "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "PAB", "PEN", "PGK", "PHP", + "PKR", "PLN", "PYG", "QAR", "RSD", "RUB", "RWF", "SAR", "SCR", "SDG", "SEK", + "SGD", "SHP", "SLL", "SOS", "SRD", "STD", "SYP", "SZL", "THB", "TJS", "TOP", + "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS", "VND", "VUV", + "WST", "XAF", "XCD", "XOF", "XPF", "YER", "ZAR", "ZMW", "ZWL" + ] + @version "v1" @cvc_code_translator %{ @@ -153,8 +224,8 @@ defmodule Gringotts.Gateways.Monei do nil => {nil, nil} } - # MONEI supports payment by card, bank account and even something obscure: virtual account - # opts has the auth keys. + # MONEI supports payment by card, bank account and even something obscure: + # virtual account opts has the auth keys. @doc """ Performs a (pre) Authorize operation. @@ -168,19 +239,24 @@ defmodule Gringotts.Gateways.Monei do * `capture/3` _an_ amount. * `void/2` a pre-authorization. - ## Note + ## Note - A stand-alone pre-authorization [expires in - 72hrs](https://docs.monei.net/tutorials/manage-payments/backoffice). + * The `:register` option when set to `true` will store this card for future + use, and you will recieve a registration `token` in the `:token` field of + the `Response` struct. + * A stand-alone pre-authorization [expires in + 72hrs](https://docs.monei.net/tutorials/manage-payments/backoffice). ## Example - The following session shows how one would (pre) authorize a payment of $40 on a sample `card`. + The following session shows how one would (pre) authorize a payment of $40 on + a sample `card`. iex> amount = %{value: Decimal.new(42), currency: "EUR"} iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> auth_result = Gringotts.authorize(Gringotts.Gateways.Monei, amount, card, opts) + iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Monei, amount, card, opts) iex> auth_result.id # This is the authorization ID + iex> auth_result.token # This is the registration ID/token """ @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} def authorize(amount, card = %CreditCard{}, opts) do @@ -189,12 +265,10 @@ defmodule Gringotts.Gateways.Monei do params = [ paymentType: "PA", - amount: value, - currency: currency + amount: value ] ++ card_params(card) - auth_info = Keyword.fetch!(opts, :config) - commit(:post, "payments", params, auth_info) + commit(:post, "payments", params, [{:currency, currency} | opts]) end @doc """ @@ -217,8 +291,7 @@ defmodule Gringotts.Gateways.Monei do authorized a payment worth $35 by referencing the obtained authorization `id`. iex> amount = %{value: Decimal.new(42), currency: "EUR"} - iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> capture_result = Gringotts.capture(Gringotts.Gateways.Monei, 35, auth_result.id, opts) + iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VIS iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Monei, 35, auth_result.id, opts) """ @spec capture(Money.t(), String.t(), keyword) :: {:ok | :error, Response} def capture(amount, payment_id, opts) @@ -228,12 +301,10 @@ defmodule Gringotts.Gateways.Monei do params = [ paymentType: "CP", - amount: value, - currency: currency + amount: value ] - auth_info = Keyword.fetch!(opts, :config) - commit(:post, "payments/#{payment_id}", params, auth_info) + commit(:post, "payments/#{payment_id}", params, [{:currency, currency} | opts]) end @doc """ @@ -242,6 +313,12 @@ defmodule Gringotts.Gateways.Monei do MONEI attempts to process a purchase on behalf of the customer, by debiting `amount` from the customer's account by charging the customer's `card`. + ## Note + + * The `:register` option when set to `true` will store this card for future + use, and you will recieve a registration `token` in the `:token` field of + the `Response` struct. + ## Example The following session shows how one would process a payment in one-shot, @@ -249,22 +326,20 @@ defmodule Gringotts.Gateways.Monei do iex> amount = %{value: Decimal.new(42), currency: "EUR"} iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> purchase_result = Gringotts.purchase(Gringotts.Gateways.Monei, amount, card, opts) + iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.Monei, amount, card, opts) + iex> purchase_result.token # This is the registration ID/token """ @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} def purchase(amount, card = %CreditCard{}, opts) do {currency, value} = Money.to_string(amount) params = - card_params(card) ++ - [ - paymentType: "DB", - amount: value, - currency: currency - ] - - auth_info = Keyword.fetch!(opts, :config) - commit(:post, "payments", params, auth_info) + [ + paymentType: "DB", + amount: value + ] ++ card_params(card) + + commit(:post, "payments", params, [{:currency, currency} | opts]) end @doc """ @@ -297,15 +372,14 @@ defmodule Gringotts.Gateways.Monei do capture. iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> void_result = Gringotts.void(Gringotts.Gateways.Monei, auth_result.id, opts) + iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.Monei, auth_result.id, opts) """ @spec void(String.t(), keyword) :: {:ok | :error, Response} def void(payment_id, opts) def void(<>, opts) do params = [paymentType: "RV"] - auth_info = Keyword.fetch!(opts, :config) - commit(:post, "payments/#{payment_id}", params, auth_info) + commit(:post, "payments/#{payment_id}", params, opts) end @doc """ @@ -326,7 +400,7 @@ defmodule Gringotts.Gateways.Monei do iex> amount = %{value: Decimal.new(42), currency: "EUR"} iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> refund_result = Gringotts.refund(Gringotts.Gateways.Monei, purchase_result.id, amount) + iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.Monei, purchase_result.id, amount) """ @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} def refund(amount, <>, opts) do @@ -334,12 +408,10 @@ defmodule Gringotts.Gateways.Monei do params = [ paymentType: "RF", - amount: value, - currency: currency + amount: value ] - auth_info = Keyword.fetch!(opts, :config) - commit(:post, "payments/#{payment_id}", params, auth_info) + commit(:post, "payments/#{payment_id}", params, [{:currency, currency} | opts]) end @doc """ @@ -363,13 +435,12 @@ defmodule Gringotts.Gateways.Monei do future use. iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> store_result = Gringotts.store(Gringotts.Gateways.Monei, card, []) + iex> {:ok, store_result} = Gringotts.store(Gringotts.Gateways.Monei, card, []) """ @spec store(CreditCard.t(), keyword) :: {:ok | :error, Response} def store(%CreditCard{} = card, opts) do params = card_params(card) - auth_info = Keyword.fetch!(opts, :config) - commit(:post, "registrations", params, auth_info) + commit(:post, "registrations", params, opts) end @doc """ @@ -380,9 +451,10 @@ defmodule Gringotts.Gateways.Monei do Deletes previously stored payment-source data. """ @spec unstore(String.t(), keyword) :: {:ok | :error, Response} - def unstore(<>, opts) do - auth_info = Keyword.fetch!(opts, :config) - commit(:delete, "registrations/#{registrationId}", [], auth_info) + def unstore(registration_id, opts) + + def unstore(<>, opts) do + commit(:delete, "registrations/#{registration_id}", [], opts) end defp card_params(card) do @@ -397,34 +469,47 @@ defmodule Gringotts.Gateways.Monei do end # Makes the request to MONEI's network. - @spec commit(atom, String.t(), keyword, map) :: {:ok | :error, Response} + @spec commit(atom, String.t(), keyword, keyword) :: {:ok | :error, Response} defp commit(method, endpoint, params, opts) do auth_params = [ - "authentication.userId": opts[:userId], - "authentication.password": opts[:password], - "authentication.entityId": opts[:entityId] + "authentication.userId": opts[:config][:userId], + "authentication.password": opts[:config][:password], + "authentication.entityId": opts[:config][:entityId] ] - body = params ++ auth_params url = "#{base_url(opts)}/#{version(opts)}/#{endpoint}" - network_response = - case method do - :post -> HTTPoison.post(url, {:form, body}, @default_headers) - :delete -> HTTPoison.delete(url <> "?" <> URI.encode_query(auth_params)) - end - - respond(network_response) + case expand_params(opts, params[:paymentType]) do + {:error, reason} -> + {:error, Response.error(description: reason)} + + validated_params -> + network_response = + case method do + :post -> + HTTPoison.post( + url, + {:form, params ++ validated_params ++ auth_params}, + @default_headers + ) + + :delete -> + HTTPoison.delete(url <> "?" <> URI.encode_query(auth_params)) + end + + respond(network_response) + end end - # Parses MONEI's response and returns a `Gringotts.Response` struct in a `:ok`, `:error` tuple. + # Parses MONEI's response and returns a `Gringotts.Response` struct in a + # `:ok`, `:error` tuple. @spec respond(term) :: {:ok | :error, Response} defp respond(monei_response) defp respond({:ok, %{status_code: 200, body: body}}) do case decode(body) do {:ok, decoded_json} -> - case verification_result(decoded_json) do + case parse_response(decoded_json) do {:ok, results} -> {:ok, Response.success([{:id, decoded_json["id"]} | results])} {:error, errors} -> {:ok, Response.error([{:id, decoded_json["id"]} | errors])} end @@ -443,32 +528,100 @@ defmodule Gringotts.Gateways.Monei do :error, Response.error( code: error.id, - reason: :network_fail?, + reason: "network related failure", description: "HTTPoison says '#{error.reason}'" ) } end - defp verification_result(%{"result" => result} = data) do + defp expand_params(params, action_type) do + Enum.reduce_while(params, [], fn {k, v}, acc -> + case k do + :currency -> + if valid_currency?(v), + do: {:cont, [{:currency, v} | acc]}, + else: {:halt, {:error, "Invalid currency"}} + + :customer -> + {:cont, acc ++ make("customer", v)} + + :merchant -> + {:cont, acc ++ make("merchant", v)} + + :billing -> + {:cont, acc ++ make("billing", v)} + + :shipping -> + {:cont, acc ++ make("shipping", v)} + + :invoice_id -> + {:cont, [{"merchantInvoiceId", v} | acc]} + + :transaction_id -> + {:cont, [{"merchantTransactionId", v} | acc]} + + :category -> + {:cont, [{"transactionCategory", v} | acc]} + + :shipping_customer -> + {:cont, acc ++ make("shipping.customer", v)} + + :custom -> + {:cont, acc ++ make_custom(v)} + + :register -> + { + :cont, + if action_type in ["PA", "DB"] do + [{"createRegistration", true} | acc] + else + acc + end + } + + _ -> + {:cont, acc} + end + end) + end + + defp valid_currency?(currency) do + currency in @supported_currencies + end + + defp parse_response(%{"result" => result} = data) do {address, zip_code} = @avs_code_translator[result["avsResponse"]] - code = result["code"] results = [ - code: code, + code: result["code"], description: result["description"], risk: data["risk"]["score"], cvc_result: @cvc_code_translator[result["cvvResponse"]], avs_result: [address: address, zip_code: zip_code], - raw: data + raw: data, + token: data["registrationId"] ] - if String.match?(code, ~r{^(000\.000\.|000\.100\.1|000\.[36])}) do + filtered = Enum.filter(results, fn {_, v} -> v != nil end) + verify(filtered) + end + + defp verify(results) do + if String.match?(results[:code], ~r{^(000\.000\.|000\.100\.1|000\.[36])}) do {:ok, results} else - {:error, [{:reason, result["description"]} | results]} + {:error, [{:reason, results[:description]} | results]} end end - defp base_url(opts), do: opts[:test_url] || @base_url - defp version(opts), do: opts[:api_version] || @version + defp make(prefix, param) do + Enum.into(param, [], fn {k, v} -> {"#{prefix}.#{k}", v} end) + end + + defp make_custom(custom_map) do + Enum.into(custom_map, [], fn {k, v} -> {"customParameters[#{k}]", "#{v}"} end) + end + + defp base_url(opts), do: opts[:config][:test_url] || @base_url + defp version(opts), do: opts[:config][:api_version] || @version end diff --git a/test/gateways/monei_test.exs b/test/gateways/monei_test.exs index 3ea51b12..51823a08 100644 --- a/test/gateways/monei_test.exs +++ b/test/gateways/monei_test.exs @@ -2,13 +2,14 @@ defmodule Gringotts.Gateways.MoneiTest do use ExUnit.Case, async: false alias Gringotts.{ - CreditCard, + CreditCard } + alias Gringotts.Gateways.Monei, as: Gateway @card %CreditCard{ - first_name: "Jo", - last_name: "Doe", + first_name: "Harry", + last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, @@ -17,8 +18,8 @@ defmodule Gringotts.Gateways.MoneiTest do } @bad_card %CreditCard{ - first_name: "Jo", - last_name: "Doe", + first_name: "Harry", + last_name: "Potter", number: "4200000000000000", year: 2000, month: 12, @@ -35,6 +36,14 @@ defmodule Gringotts.Gateways.MoneiTest do "description": "Request successfully processed in 'Merchant in Integrator Test Mode'"} }] + @register_success ~s[ + {"id": "8a82944960e073640160e92da2204743", + "registrationId": "8a82944a60e09c550160e92da144491e", + "result": { + "code": "000.100.110", + "description": "Request successfully processed in 'Merchant in Integrator Test Mode'"} + }] + @store_success ~s[ {"result":{ "code":"000.100.110", @@ -49,83 +58,158 @@ defmodule Gringotts.Gateways.MoneiTest do } }] + @customer %{ + givenName: "Harry", + surname: "Potter", + merchantCustomerId: "the_boy_who_lived", + sex: "M", + birthDate: "1980-07-31", + mobile: "+15252525252", + email: "masterofdeath@ministryofmagic.gov", + ip: "1.1.1", + status: "NEW" + } + @merchant %{ + name: "Ollivanders", + city: "South Side", + street: "Diagon Alley", + state: "London", + country: "GB", + submerchantId: "Makers of Fine Wands since 382 B.C." + } + @billing %{ + street1: "301, Gryffindor", + street2: "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", + city: "Highlands", + state: "Scotland", + country: "GB" + } + @shipping Map.merge( + %{method: "SAME_DAY_SERVICE", comment: "For our valued customer, Mr. Potter"}, + @billing + ) + + @extra_opts [ + customer: @customer, + merchant: @merchant, + billing: @billing, + shipping: @shipping, + shipping_customer: @customer, + category: "EC", + register: true, + custom: %{"voldemort" => "he who must not be named"} + ] + # A new Bypass instance is needed per test, so that we can do parallel tests setup do - bypass = Bypass.open + bypass = Bypass.open() + auth = %{ userId: "8a829417539edb400153c1eae83932ac", password: "6XqRtMGS2N", entityId: "8a829417539edb400153c1eae6de325e", test_url: "http://localhost:#{bypass.port}" } + {:ok, bypass: bypass, auth: auth} end describe "core" do - @tag :skip - test "with unsupported currency.", - %{bypass: bypass, auth: auth} do - Bypass.expect_once bypass, "POST", "/v1/payments", fn conn -> - Plug.Conn.resp(conn, 400, "") - end - {:error, response} = Gateway.authorize(@bad_currency, @card, [config: auth]) - assert response.code == :unsupported_currency + test "with unsupported currency.", %{auth: auth} do + {:error, response} = Gateway.authorize(@bad_currency, @card, config: auth) + assert response.description == "Invalid currency" end - test "when MONEI is down or unreachable.", - %{bypass: bypass, auth: auth} do - Bypass.expect_once bypass, fn conn -> + test "when MONEI is down or unreachable.", %{bypass: bypass, auth: auth} do + Bypass.expect_once(bypass, fn conn -> Plug.Conn.resp(conn, 200, @auth_success) - end - Bypass.down bypass - {:error, response} = Gateway.authorize(Money.new(42, :USD), @card, [config: auth]) - assert response.reason == :network_fail? + end) - Bypass.up bypass - {:ok, _} = Gateway.authorize(Money.new(42, :USD), @card, [config: auth]) + Bypass.down(bypass) + {:error, response} = Gateway.authorize(Money.new(42, :USD), @card, config: auth) + assert response.reason == "network related failure" + + Bypass.up(bypass) + {:ok, _} = Gateway.authorize(Money.new(42, :USD), @card, config: auth) end - end - describe "authorize" do - test "when all is good.", %{bypass: bypass, auth: auth} do - Bypass.expect bypass, "POST", "/v1/payments", fn conn -> - Plug.Conn.resp(conn, 200, @auth_success) - end - {:ok, response} = Gateway.authorize(Money.new(42, :USD), @card, [config: auth]) + test "with all extra_params.", %{bypass: bypass, auth: auth} do + randoms = [ + invoice_id: Base.encode16(:crypto.hash(:md5, :crypto.strong_rand_bytes(32))), + transaction_id: Base.encode16(:crypto.hash(:md5, :crypto.strong_rand_bytes(32))) + ] + + Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> + conn_ = parse(conn) + assert conn_.body_params["createRegistration"] == "true" + assert conn_.body_params["customParameters"] == @extra_opts[:custom] + assert conn_.body_params["merchantInvoiceId"] == randoms[:invoice_id] + assert conn_.body_params["merchantTransactionId"] == randoms[:transaction_id] + assert conn_.body_params["transactionCategory"] == @extra_opts[:category] + assert conn_.body_params["customer.merchantCustomerId"] == @customer[:merchantCustomerId] + assert conn_.body_params["shipping.customer.merchantCustomerId"] == @customer[:merchantCustomerId] + assert conn_.body_params["merchant.submerchantId"] == @merchant[:submerchantId] + assert conn_.body_params["billing.city"] == @billing[:city] + assert conn_.body_params["shipping.method"] == @shipping[:method] + Plug.Conn.resp(conn, 200, @register_success) + end) + + opts = [{:config, auth} | randoms] ++ @extra_opts + {:ok, response} = Gateway.purchase(Money.new(42, :USD), @card, opts) assert response.code == "000.100.110" + assert response.token == "8a82944a60e09c550160e92da144491e" end - test "when we get non-json.", %{bypass: bypass, auth: auth} do - Bypass.expect_once bypass, "POST", "/v1/payments", fn conn -> + test "when card has expired.", %{bypass: bypass, auth: auth} do + Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> Plug.Conn.resp(conn, 400, "") - end - {:error, _} = Gateway.authorize(Money.new(42, :USD), @card, [config: auth]) + end) + + {:error, _} = Gateway.authorize(Money.new(42, :USD), @bad_card, config: auth) end + end - test "when card has expired.", %{bypass: bypass, auth: auth} do - Bypass.expect_once bypass, "POST", "/v1/payments", fn conn -> - Plug.Conn.resp(conn, 400, "") - end - {:error, _response} = Gateway.authorize(Money.new(42, :USD), @bad_card, [config: auth]) + describe "authorize" do + test "when all is good.", %{bypass: bypass, auth: auth} do + Bypass.expect(bypass, "POST", "/v1/payments", fn conn -> + Plug.Conn.resp(conn, 200, @auth_success) + end) + + {:ok, response} = Gateway.authorize(Money.new(42, :USD), @card, config: auth) + assert response.code == "000.100.110" end end describe "purchase" do test "when all is good.", %{bypass: bypass, auth: auth} do - Bypass.expect_once bypass, "POST", "/v1/payments", fn conn -> + Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> Plug.Conn.resp(conn, 200, @auth_success) - end - {:ok, response} = Gateway.purchase(Money.new(42, :USD), @card, [config: auth]) + end) + + {:ok, response} = Gateway.purchase(Money.new(42, :USD), @card, config: auth) + assert response.code == "000.100.110" + end + + test "with createRegistration.", %{bypass: bypass, auth: auth} do + Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> + conn_ = parse(conn) + assert conn_.body_params["createRegistration"] == "true" + Plug.Conn.resp(conn, 200, @register_success) + end) + + {:ok, response} = Gateway.purchase(Money.new(42, :USD), @card, config: auth, register: true) assert response.code == "000.100.110" + assert response.token == "8a82944a60e09c550160e92da144491e" end end describe "store" do test "when all is good.", %{bypass: bypass, auth: auth} do - Bypass.expect_once bypass, "POST", "/v1/registrations", fn conn -> + Bypass.expect_once(bypass, "POST", "/v1/registrations", fn conn -> Plug.Conn.resp(conn, 200, @store_success) - end - {:ok, response} = Gateway.store(@card, [config: auth]) + end) + + {:ok, response} = Gateway.store(@card, config: auth) assert response.code == "000.100.110" assert response.raw["card"]["holder"] == "Jo Doe" end @@ -139,8 +223,29 @@ defmodule Gringotts.Gateways.MoneiTest do "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> Plug.Conn.resp(conn, 200, @auth_success) - end) - {:ok, response} = Gateway.capture(Money.new(42, :USD), "7214344242e11af79c0b9e7b4f3f6234", [config: auth]) + end + ) + + {:ok, response} = + Gateway.capture(Money.new(42, :USD), "7214344242e11af79c0b9e7b4f3f6234", config: auth) + + assert response.code == "000.100.110" + end + + test "with createRegistration that is ignored", %{bypass: bypass, auth: auth} do + Bypass.expect_once( + bypass, + "POST", + "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", + fn conn -> + conn_ = parse(conn) + assert :error == Map.fetch(conn_.body_params, "createRegistration") + Plug.Conn.resp(conn, 200, @auth_success) + end + ) + + {:ok, response} = Gateway.capture(Money.new(42, :USD), "7214344242e11af79c0b9e7b4f3f6234", config: auth, register: true) + assert response.code == "000.100.110" end end @@ -153,12 +258,16 @@ defmodule Gringotts.Gateways.MoneiTest do "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> Plug.Conn.resp(conn, 200, @auth_success) - end) - {:ok, response} = Gateway.refund(Money.new(3, :USD), "7214344242e11af79c0b9e7b4f3f6234", [config: auth]) + end + ) + + {:ok, response} = + Gateway.refund(Money.new(3, :USD), "7214344242e11af79c0b9e7b4f3f6234", config: auth) + assert response.code == "000.100.110" end end - + describe "unstore" do test "when all is good.", %{bypass: bypass, auth: auth} do Bypass.expect_once( @@ -167,8 +276,10 @@ defmodule Gringotts.Gateways.MoneiTest do "/v1/registrations/7214344242e11af79c0b9e7b4f3f6234", fn conn -> Plug.Conn.resp(conn, 200, "") - end) - {:error, response} = Gateway.unstore("7214344242e11af79c0b9e7b4f3f6234", [config: auth]) + end + ) + + {:error, response} = Gateway.unstore("7214344242e11af79c0b9e7b4f3f6234", config: auth) assert response.code == :undefined_response_from_monei end end @@ -181,8 +292,10 @@ defmodule Gringotts.Gateways.MoneiTest do "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> Plug.Conn.resp(conn, 200, @auth_success) - end) - {:ok, response} = Gateway.void("7214344242e11af79c0b9e7b4f3f6234", [config: auth]) + end + ) + + {:ok, response} = Gateway.void("7214344242e11af79c0b9e7b4f3f6234", config: auth) assert response.code == "000.100.110" end end @@ -197,11 +310,16 @@ defmodule Gringotts.Gateways.MoneiTest do then = Enum.map(all, &Gateway.respond({:ok, &1})) assert Keyword.keys(then) == [:ok, :error, :error, :error] end + + def parse(conn, opts \\ []) do + opts = Keyword.put_new(opts, :parsers, [Plug.Parsers.URLENCODED]) + Plug.Parsers.call(conn, Plug.Parsers.init(opts)) + end end defmodule Gringotts.Gateways.MoneiDocTest do use ExUnit.Case, async: true # doctest Gringotts.Gateways.Monei - # doctests will never work. Track progress: https://github.com/aviabird/gringotts/issues/37 + # doctests can work. Track progress: https://github.com/aviabird/gringotts/issues/37 end diff --git a/test/integration/gateways/monei_test.exs b/test/integration/gateways/monei_test.exs index 51f0f31c..da24cc3e 100644 --- a/test/integration/gateways/monei_test.exs +++ b/test/integration/gateways/monei_test.exs @@ -1,59 +1,130 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do - use ExUnit.Case, async: false + use ExUnit.Case, async: true alias Gringotts.{ CreditCard } + alias Gringotts.Gateways.Monei, as: Gateway @moduletag :integration - + + @amount Money.new(42, :EUR) + @card %CreditCard{ first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, - verification_code: "123", + verification_code: "123", brand: "VISA" } + @customer %{ + givenName: "Harry", + surname: "Potter", + merchantCustomerId: "the_boy_who_lived", + sex: "M", + birthDate: "1980-07-31", + mobile: "+15252525252", + email: "masterofdeath@ministryofmagic.gov", + ip: "1.1.1", + status: "NEW" + } + @merchant %{ + name: "Ollivanders", + city: "South Side", + street: "Diagon Alley", + state: "London", + country: "GB", + submerchantId: "Makers of Fine Wands since 382 B.C." + } + @billing %{ + street1: "301, Gryffindor", + street2: "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", + city: "Highlands", + state: "Scotland", + country: "GB" + } + @shipping Map.merge( + %{method: "SAME_DAY_SERVICE", comment: "For our valued customer, Mr. Potter"}, + @billing + ) + + @extra_opts [ + customer: @customer, + merchant: @merchant, + billing: @billing, + shipping: @shipping, + shipping_customer: @customer, + category: "EC", + custom: %{voldemort: "he who must not be named"} + ] + setup_all do - Application.put_env(:gringotts, Gringotts.Gateways.Monei, [adapter: Gringotts.Gateways.Monei, - userId: "8a8294186003c900016010a285582e0a", - password: "hMkqf2qbWf", - entityId: "8a82941760036820016010a28a8337f6"]) + Application.put_env( + :gringotts, + Gringotts.Gateways.Monei, + adapter: Gringotts.Gateways.Monei, + userId: "8a8294186003c900016010a285582e0a", + password: "hMkqf2qbWf", + entityId: "8a82941760036820016010a28a8337f6" + ) + end + + setup do + randoms = [ + invoice_id: Base.encode16(:crypto.hash(:md5, :crypto.strong_rand_bytes(32))), + transaction_id: Base.encode16(:crypto.hash(:md5, :crypto.strong_rand_bytes(32))) + ] + + {:ok, opts: randoms ++ @extra_opts} end - test "authorize." do - case Gringotts.authorize(Gateway, Money.new(42, :EUR), @card) do + test "authorize", %{opts: opts} do + case Gringotts.authorize(Gateway, @amount, @card, opts) do {:ok, response} -> assert response.code == "000.100.110" - assert response.description == "Request successfully processed in 'Merchant in Integrator Test Mode'" + + assert response.description == + "Request successfully processed in 'Merchant in Integrator Test Mode'" + assert String.length(response.id) == 32 - {:error, _err} -> flunk() + + {:error, _err} -> + flunk() end end @tag :skip - test "capture." do - case Gringotts.capture(Gateway, Money.new(42, :EUR), "s") do + test "capture", %{opts: _opts} do + case Gringotts.capture(Gateway, @amount, "s") do {:ok, response} -> assert response.code == "000.100.110" - assert response.description == "Request successfully processed in 'Merchant in Integrator Test Mode'" + + assert response.description == + "Request successfully processed in 'Merchant in Integrator Test Mode'" + assert String.length(response.id) == 32 - - {:error, _err} -> flunk() + + {:error, _err} -> + flunk() end end - test "purchase." do - case Gringotts.purchase(Gateway, Money.new(42, :EUR), @card) do + test "purchase", %{opts: opts} do + case Gringotts.purchase(Gateway, @amount, @card, opts) do {:ok, response} -> assert response.code == "000.100.110" - assert response.description == "Request successfully processed in 'Merchant in Integrator Test Mode'" + + assert response.description == + "Request successfully processed in 'Merchant in Integrator Test Mode'" + assert String.length(response.id) == 32 - {:error, _err} -> flunk() + + {:error, _err} -> + flunk() end end From ce7c6bddfc08f7b85bd941142adbf7a51562767d Mon Sep 17 00:00:00 2001 From: Arjun Singh Date: Sun, 21 Jan 2018 23:52:09 +0530 Subject: [PATCH 17/60] Money integration with ANet and Anet test modification (#82) Used Gringotts.Money for Authorize Net currency support. --- lib/gringotts/gateways/authorize_net.ex | 58 ++++++++++++++----------- test/gateways/authorize_net_test.exs | 56 +++++++++++++++--------- 2 files changed, 67 insertions(+), 47 deletions(-) diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index 149cd74c..42104be7 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -26,7 +26,6 @@ defmodule Gringotts.Gateways.AuthorizeNet do | Key | Remark | Status | | ---- | --- | ---- | - | `currency` | | not Implemented | | `customer` | | implemented | | `invoice` | | implemented | | `bill_to` | | implemented | @@ -46,6 +45,12 @@ defmodule Gringotts.Gateways.AuthorizeNet do To know more about these keywords visit [Request](https://developer.authorize.net/api/reference/index.html#payment-transactions) and [Response](https://developer.authorize.net/api/reference/index.html#payment-transactions) key sections for each function. + ## Notes + Authorize net supports [multiple currencies](https://community.developer.authorize.net/t5/The-Authorize-Net-Developer-Blog/Authorize-Net-UK-Europe-Update/ba-p/35957) + however, multiple currencies in one account are not supported. To support multiple currencies merchant needs + multiple Authorize.Net accounts, one for every currency. Currently, `Gringotts` supports single Authorize.Net + account configuration. + To use this module you need to create an account with the [Authorize.Net gateway](https://www.authorize.net/solutions/merchantsolutions/onlinemerchantaccount/) which will provide you with a `name` and a `transactionKey`. @@ -85,7 +90,6 @@ defmodule Gringotts.Gateways.AuthorizeNet do use Gringotts.Gateways.Base use Gringotts.Adapter, required_config: [:name, :transaction_key] - alias Gringotts.Gateways.AuthorizeNet.ResponseHandler @test_url "https://apitest.authorize.net/xml/v1/request.api" @@ -104,7 +108,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do alias Gringotts.{ CreditCard, - Response + Response, + Money } # ---------------Interface functions to be used by developer for @@ -126,11 +131,11 @@ defmodule Gringotts.Gateways.AuthorizeNet do ref_id: String, lineitems: %{ item_id: String, name: String, description: String, - quantity: Integer, unit_price: Float + quantity: Integer, unit_price: Gringotts.Money.t() }, - tax: %{amount: Float, name: String, description: String}, - duty: %{amount: String, name: String, description: String}, - shipping: %{amount: String, name: String, description: String}, + tax: %{amount: Gringotts.Money.t(), name: String, description: String}, + duty: %{amount: Gringotts.Money.t(), name: String, description: String}, + shipping: %{amount: Gringotts.Money.t(), name: String, description: String}, po_number: String, customer: %{id: String}, bill_to: %{ @@ -152,10 +157,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do lineitems: %{itemId: "1", name: "vase", description: "Cannes logo", quantity: "18", unit_price: "45.00"} ] iex> card = %CreditCard{number: "5424000000000015", year: 2020, month: 12, verification_code: "999"} - iex> amount = 5 + iex> amount = %{amount: Decimal.new(20.0), currency: 'USD'} iex> result = Gringotts.purchase(Gringotts.Gateways.AuthorizeNet, amount, card, opts) """ - @spec purchase(float, CreditCard.t, Keyword.t) :: {:ok | :error, Response.t} + @spec purchase(Money.t, CreditCard.t, Keyword.t) :: {:ok | :error, Response.t} def purchase(amount, payment, opts) do request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:purchase]) @@ -179,11 +184,11 @@ defmodule Gringotts.Gateways.AuthorizeNet do ref_id: String, lineitems: %{ item_id: String, name: String, description: String, - quantity: Integer, unit_price: Float + quantity: Integer, unit_price: Gringotts.Money.t() }, - tax: %{amount: Float, name: String, description: String}, - duty: %{amount: String, name: String, description: String}, - shipping: %{amount: String, name: String, description: String}, + tax: %{amount: Gringotts.Money.t(), name: String, description: String}, + duty: %{amount: Gringotts.Money.t(), name: String, description: String}, + shipping: %{amount: Gringotts.Money.t(), name: String, description: String}, po_number: String, customer: %{id: String}, bill_to: %{ @@ -206,12 +211,12 @@ defmodule Gringotts.Gateways.AuthorizeNet do lineitems: %{itemId: "1", name: "vase", description: "Cannes logo", quantity: "18", unit_price: "45.00"} ] iex> card = %CreditCard{number: "5424000000000015", year: 2020, month: 12, verification_code: "999"} - iex> amount = 5 + iex> amount = %{amount: Decimal.new(20.0), currency: 'USD'} iex> result = Gringotts.authorize(Gringotts.Gateways.AuthorizeNet, amount, card, opts) """ - @spec authorize(float, CreditCard.t, Keyword.t) :: {:ok | :error, Response.t} + @spec authorize(Money.t, CreditCard.t, Keyword.t) :: {:ok | :error, Response.t} def authorize(amount, payment, opts) do - request_data = + request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:authorize]) response_data = commit(:post, request_data, opts) respond(response_data) @@ -243,11 +248,11 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> opts = [ ref_id: "123456" ] - iex> amount = 5 + iex> amount = %{amount: Decimal.new(20.0), currency: 'USD'} iex> id = "123456" iex> result = Gringotts.capture(Gringotts.Gateways.AuthorizeNet, id, amount, opts) """ - @spec capture(String.t, float, Keyword.t) :: {:ok | :error, Response.t} + @spec capture(String.t, Money.t, Keyword.t) :: {:ok | :error, Response.t} def capture(id, amount, opts) do request_data = normal_capture(amount, id, opts, @transaction_type[:capture]) response_data = commit(:post, request_data, opts) @@ -274,10 +279,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do ref_id: "123456" ] iex> id = "123456" - iex> amount = 5 + iex> amount = %{amount: Decimal.new(20.0), currency: 'USD'} iex> result = Gringotts.refund(Gringotts.Gateways.AuthorizeNet, amount, id, opts) """ - @spec refund(float, String.t, Keyword.t) :: {:ok | :error, Response.t} + @spec refund(Money.t, String.t, Keyword.t) :: {:ok | :error, Response.t} def refund(amount, id, opts) do request_data = normal_refund(amount, id, opts, @transaction_type[:refund]) response_data = commit(:post, request_data, opts) @@ -394,7 +399,6 @@ defmodule Gringotts.Gateways.AuthorizeNet do end defp respond({:error, %HTTPoison.Error{} = error}) do - IO.inspect error {:error, Response.error(error_code: error.id, message: "HTTPoison says '#{error.reason}'")} end @@ -561,10 +565,9 @@ defmodule Gringotts.Gateways.AuthorizeNet do end defp add_amount(amount) do - cond do - is_integer(amount) -> element(:amount, amount) - is_float(amount) -> element(:amount, amount) - true -> element(:amount, 0) + if amount do + amount = amount |> Money.value |> Decimal.to_float + element(:amount, amount) end end @@ -596,7 +599,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do element(:name, opts[:lineitems][:name]), element(:description, opts[:lineitems][:description]), element(:quantity, opts[:lineitems][:quantity]), - element(:unitPrice, opts[:lineitems][:unit_price]) + element( + :unitPrice, + opts[:lineitems][:unit_price] |> Money.value |> Decimal.to_float + ) ]) ]) ]) diff --git a/test/gateways/authorize_net_test.exs b/test/gateways/authorize_net_test.exs index 0c155a4c..3c177dc4 100644 --- a/test/gateways/authorize_net_test.exs +++ b/test/gateways/authorize_net_test.exs @@ -23,8 +23,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do verification_code: 123 } - @amount 20 - @bad_amount "a" + @amount %{amount: Decimal.new(20.0), currency: 'USD'} @opts [ config: @auth, @@ -35,7 +34,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do name: "vase", description: "Cannes logo", quantity: "18", - unit_price: "45.00" + unit_price: %{amount: Decimal.new(20.0), currency: 'USD'} } ] @opts_refund [ @@ -43,6 +42,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do ref_id: "123456", payment: %{card: %{number: "5424000000000015", year: 2020, month: 12}} ] + @opts_store [ config: @auth, profile: %{ @@ -53,6 +53,15 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do customer_type: "individual", validation_mode: "testMode" ] + @opts_store_without_validation [ + config: @auth, + profile: %{ + merchant_customer_id: "123456", + description: "Profile description here", + email: "customer-profile-email@here.com" + } + ] + @opts_store_no_profile [ config: @auth, ] @@ -69,7 +78,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do @opts_store [ config: @auth, profile: %{merchant_customer_id: "123456", - description: "Profile description here", + description: "Profile description here", email: "customer-profile-email@here.com" } ] @@ -79,7 +88,12 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do @opts_customer_profile [ config: @auth, customer_profile_id: "1814012002", - validation_mode: "testMode" + validation_mode: "testMode", + customer_type: "individual" + ] + @opts_customer_profile_args[ + config: @auth, + customer_profile_id: "1814012002" ] @refund_id "60036752756" @@ -102,14 +116,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do end end - test "with bad amount" do - with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.bad_amount_purchase_response end] do - assert {:error, response} = ANet.purchase(@bad_amount, @card, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" - end - end - test "with bad card" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.bad_card_purchase_response end] do @@ -128,14 +134,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do end end - test "with bad amount" do - with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.bad_amount_purchase_response end] do - assert {:error, response} = ANet.authorize(@bad_amount, @card, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" - end - end - test "with bad card" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.bad_card_purchase_response end] do @@ -216,6 +214,14 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do end end + test "successful response without validation and customer type" do + with_mock HTTPoison, + [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_store_response end] do + assert {:ok, response} = ANet.store(@card, @opts_store_without_validation) + assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" + end + end + test "without any profile" do with_mock HTTPoison, [request: fn(_method, _url, _body, _headers) -> MockResponse.store_without_profile_fields end] do @@ -231,6 +237,14 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do assert response.params["createCustomerPaymentProfileResponse"]["messages"]["resultCode"] == "Ok" end end + + test "successful response without valiadtion mode and customer type" do + with_mock HTTPoison, + [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_store_response end] do + assert {:ok, response} = ANet.store(@card, @opts_customer_profile_args) + assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" + end + end end describe "unstore" do From db9190b583b238dd75cb0870b76a4c62d1c1e268 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Thu, 25 Jan 2018 17:43:47 +0530 Subject: [PATCH 18/60] [ANet] Corrected use of money protocol and examples (#92) * Corrected use of money protocol and examples Fixes #90 and Fixes #91 Adds a new Response field: authorization. It is set to ["transactionResponse"]["transId"] * Uses Gringotts.Money.to_string instead of converting to lossy Float * doc examples updated (authorize, capture, purchase) * reworded confusing store doc * Improved docs, stripped whitespace * Ran the elixir 1.6 formatter * Used the ~s sigil in mocks * Also removed an invisible unicode codepoint from mock strings --- lib/gringotts/gateways/authorize_net.ex | 490 +++++++++++++----------- test/gateways/authorize_net_test.exs | 190 +++++---- test/mocks/authorize_net_mock.exs | 34 +- 3 files changed, 405 insertions(+), 309 deletions(-) diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index 42104be7..04762db3 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -1,88 +1,104 @@ defmodule Gringotts.Gateways.AuthorizeNet do - @moduledoc """ - A module for working with the Authorize.net payment gateway. - - The module provides a set of functions to perform transactions via this gateway for a merchant. + A module for working with the Authorize.Net payment gateway. - [AuthorizeNet API reference](https://developer.authorize.net/api/reference/index.html) + Refer the official Authorize.Net [API docs][docs]. - The following set of functions for Authorize.Net have been provided: + The following set of functions for Authorize.Net have been implemented: | Action | Method | | ------ | ------ | | Authorize a Credit Card | `authorize/3` | - | Capture a Previously Authorized Amount | `capture/3` | + | Capture a previously authorized amount | `capture/3` | | Charge a Credit Card | `purchase/3` | | Refund a transaction | `refund/3` | - | Void a Transaction | `void/2` | + | Void a transaction | `void/2` | | Create Customer Profile | `store/2` | | Create Customer Payment Profile | `store/2` | | Delete Customer Profile | `unstore/2` | Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply - optional arguments for transactions with the Authorize.Net gateway. The following keys - are supported: - - | Key | Remark | Status | - | ---- | --- | ---- | - | `customer` | | implemented | - | `invoice` | | implemented | - | `bill_to` | | implemented | - | `ship_to` | | implemented | - | `customer_ip` | | implemented | - | `order` | | implemented | - | `lineitems` | | implemented | - | `ref_id` | | implemented | - | `tax` | | implemented | - | `duty` | | implemented | - | `shipping` | | implemented | - | `po_number` | | implemented | - | `customer_type` | | implemented | - | `customer_profile_id`| | implemented | - | `profile` | | implemented | - - To know more about these keywords visit [Request](https://developer.authorize.net/api/reference/index.html#payment-transactions) - and [Response](https://developer.authorize.net/api/reference/index.html#payment-transactions) key sections for each function. + optional arguments for transactions with the Authorize.Net gateway. The + following keys are supported: + + | Key | Remarks | + | ---- | ------- | + | `customer` | | + | `invoice` | | + | `bill_to` | | + | `ship_to` | | + | `customer_ip` | | + | `order` | | + | `lineitems` | | + | `ref_id` | | + | `tax` | | + | `duty` | | + | `shipping` | | + | `po_number` | | + | `customer_type` | | + | `customer_profile_id` | | + | `profile` | | + + To know more about these keywords visit [Request and Response][req-resp] tabs for each + API method. + + [docs]: https://developer.authorize.net/api/reference/index.html + [req-resp]: https://developer.authorize.net/api/reference/index.html#payment-transactions ## Notes - Authorize net supports [multiple currencies](https://community.developer.authorize.net/t5/The-Authorize-Net-Developer-Blog/Authorize-Net-UK-Europe-Update/ba-p/35957) - however, multiple currencies in one account are not supported. To support multiple currencies merchant needs - multiple Authorize.Net accounts, one for every currency. Currently, `Gringotts` supports single Authorize.Net - account configuration. - - To use this module you need to create an account with the [Authorize.Net - gateway](https://www.authorize.net/solutions/merchantsolutions/onlinemerchantaccount/) - which will provide you with a `name` and a `transactionKey`. + + Authorize.Net supports [multiple currencies][currencies] however, multiple + currencies in one account are not supported. A merchant would need multiple + Authorize.Net accounts, one for each chosen currency. + + > Currently, `Gringotts` supports single Authorize.Net account configuration. + + [currencies]: https://community.developer.authorize.net/t5/The-Authorize-Net-Developer-Blog/Authorize-Net-UK-Europe-Update/ba-p/35957 ## Configuring your AuthorizeNet account at `Gringotts` + To use this module you need to [create an account][dashboard] with the + Authorize.Net gateway and obtain your login secrets: `name` and + `transactionKey`. + Your Application config **must include the `name` and `transaction_key` fields** and would look something like this: - + config :gringotts, Gringotts.Gateways.AuthorizeNet, adapter: Gringotts.Gateways.AuthorizeNet, name: "name_provided_by_authorize_net", transaction_key: "transactionKey_provided_by_authorize_net" - - ## Scope of this module, and _quirks_ - * Although Authorize.Net supports payments from [various - sources](https://www.authorize.net/solutions/merchantsolutions/onlinemerchantaccount/), - this library currently accepts payments by (supported) credit cards only. + + ## Scope of this module + + Although Authorize.Net supports payments from various sources (check your + [dashboard][dashboard]), this library currently accepts payments by + (supported) credit cards only. + + [dashboard]: https://www.authorize.net/solutions/merchantsolutions/onlinemerchantaccount/ ## Following the examples + 1. First, set up a sample application and configure it to work with Authorize.Net. - - You could do that from scratch by following our [Getting Started](#) guide. - - To save you time, we recommend [cloning our example - repo](https://github.com/aviabird/gringotts_example) that gives you a - pre-configured sample app ready-to-go. - + You could use the same config or update it the with your "secrets" - [above](#Configuring your AuthorizeNet account at `Gringotts`). + - You could do that from scratch by following our [Getting Started][gs] + guide. + - To save you time, we recommend [cloning our example repo][example-repo] + that gives you a pre-configured sample app ready-to-go. + + You could use the same config or update it the with your "secrets" + [above](#module-configuring-your-authorizenet-account-at-gringotts). 2. Run an `iex` session with `iex -S mix` and add some variable bindings and - aliases to it (to save some time): + aliases to it (to save some time): + ``` - iex> alias Gringotts.{Response, CreditCard, Gateways.AuthorizeNet} + iex> alias Gringotts.{Response, CreditCard, Gateways.AuthorizeNet} + iex> amount = %{value: Decimal.new(20.0), currency: "USD"} + iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} ``` + + We'll be using these in the examples below. + + [example-repo]: https://github.com/aviabird/gringotts_example + [gs]: https://github.com/aviabird/gringotts/wiki """ import XmlBuilder @@ -112,19 +128,20 @@ defmodule Gringotts.Gateways.AuthorizeNet do Money } - # ---------------Interface functions to be used by developer for - #----------------making requests to gateway - @doc """ - Charge a credit card. + Transfers `amount` from the customer to the merchant. + + Charges a credit `card` for the specified `amount`. It performs `authorize` + and `capture` at the [same time][auth-cap-same-time]. + + Authorize.Net returns `transId` (available in the `Response.authorization` + field) which can be used to: - Function to charge a user credit card for the specified `amount`. It performs `authorize` - and `capture` at the [same time](https://developer.authorize.net/api/reference/index.html#payment-transactions-charge-a-credit-card). - For this transaction Authorize.Net returns `transId` which can be used to: - * `refund/3` a settled transaction. * `void/2` a transaction. + [auth-cap-same-time]: https://developer.authorize.net/api/reference/index.html#payment-transactions-charge-a-credit-card + ## Optional Fields opts = [ order: %{invoice_number: String, description: String}, @@ -140,8 +157,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do customer: %{id: String}, bill_to: %{ first_name: String, last_name: String, company: String, - address: String, city: String, state: String, zip: String, - country: String + address: String, city: String, state: String, zip: String, + country: String }, ship_to: %{ first_name: String, last_name: String, company: String, address: String, @@ -151,19 +168,21 @@ defmodule Gringotts.Gateways.AuthorizeNet do ] ## Example + iex> amount = %{value: Decimal.new(20.0), currency: "USD"} iex> opts = [ ref_id: "123456", - order: %{invoice_number: "INV-12345", description: "Product Description"}, - lineitems: %{itemId: "1", name: "vase", description: "Cannes logo", quantity: "18", unit_price: "45.00"} + order: %{invoice_number: "INV-12345", description: "Product Description"}, + lineitems: %{item_id: "1", name: "vase", description: "Cannes logo", quantity: 1, unit_price: amount}, + tax: %{name: "VAT", amount: Money.new("0.1", :EUR), description: "Value Added Tax"}, + shipping: %{name: "SAME-DAY-DELIVERY", amount: Money.new("0.56", :EUR), description: "Zen Logistics"}, + duty: %{name: "import_duty", amount: Money.new("0.25", :EUR), description: "Upon import of goods"} ] - iex> card = %CreditCard{number: "5424000000000015", year: 2020, month: 12, verification_code: "999"} - iex> amount = %{amount: Decimal.new(20.0), currency: 'USD'} + iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} iex> result = Gringotts.purchase(Gringotts.Gateways.AuthorizeNet, amount, card, opts) """ - @spec purchase(Money.t, CreditCard.t, Keyword.t) :: {:ok | :error, Response.t} + @spec purchase(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response} def purchase(amount, payment, opts) do - request_data = - add_auth_purchase(amount, payment, opts, @transaction_type[:purchase]) + request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:purchase]) response_data = commit(:post, request_data, opts) respond(response_data) end @@ -171,10 +190,15 @@ defmodule Gringotts.Gateways.AuthorizeNet do @doc """ Authorize a credit card transaction. - Function to authorize a transaction for the specified amount. It needs to be - followed up with a `capture/3` transaction to transfer the funds to merchant account. - - For this transaction Authorize.Net returns a `transId` which can be use for: + The authorization validates the `card` details with the banking network, + places a hold on the transaction `amount` in the customer’s issuing bank and + also triggers risk management. Funds are not transferred. + + To transfer the funds to merchant's account follow this up with a `capture/3`. + + Authorize.Net returns a `transId` (available in the `Response.authorization` + field) which can be used for: + * `capture/3` an authorized transaction. * `void/2` a transaction. @@ -191,10 +215,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do shipping: %{amount: Gringotts.Money.t(), name: String, description: String}, po_number: String, customer: %{id: String}, - bill_to: %{ + bill_to: %{ first_name: String, last_name: String, company: String, - address: String, city: String, state: String, zip: String, - country: String + address: String, city: String, state: String, zip: String, + country: String }, ship_to: %{ first_name: String, last_name: String, company: String, address: String, @@ -205,54 +229,62 @@ defmodule Gringotts.Gateways.AuthorizeNet do ## Example + iex> amount = %{value: Decimal.new(20.0), currency: "USD"} iex> opts = [ ref_id: "123456", - order: %{invoice_number: "INV-12345", description: "Product Description"}, - lineitems: %{itemId: "1", name: "vase", description: "Cannes logo", quantity: "18", unit_price: "45.00"} + order: %{invoice_number: "INV-12345", description: "Product Description"}, + lineitems: %{itemId: "1", name: "vase", description: "Cannes logo", quantity: 1, unit_price: amount}, + tax: %{name: "VAT", amount: Money.new("0.1", :EUR), description: "Value Added Tax"}, + shipping: %{name: "SAME-DAY-DELIVERY", amount: Money.new("0.56", :EUR), description: "Zen Logistics"}, + duty: %{name: "import_duty", amount: Money.new("0.25", :EUR), description: "Upon import of goods"} ] - iex> card = %CreditCard{number: "5424000000000015", year: 2020, month: 12, verification_code: "999"} - iex> amount = %{amount: Decimal.new(20.0), currency: 'USD'} + iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} iex> result = Gringotts.authorize(Gringotts.Gateways.AuthorizeNet, amount, card, opts) """ - @spec authorize(Money.t, CreditCard.t, Keyword.t) :: {:ok | :error, Response.t} + @spec authorize(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response} def authorize(amount, payment, opts) do - request_data = - add_auth_purchase(amount, payment, opts, @transaction_type[:authorize]) + request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:authorize]) response_data = commit(:post, request_data, opts) respond(response_data) end @doc """ - Capture a transaction. - - Function to capture an `amount` for an authorized transaction. - - For this transaction Authorize.Net returns a `transId` which can be use to: - * `refund/3` a settled transaction. - * `void/2` a transaction. - + Captures a pre-authorized `amount`. + + `amount` is transferred to the merchant account by Authorize.Net when it is smaller or + equal to the amount used in the pre-authorization referenced by `id`. + + Authorize.Net returns a `transId` (available in the `Response.authorization` + field) which can be used to: + + * `refund/3` a settled transaction. + * `void/2` a transaction. + ## Notes - * If a `capture` transaction needs to `void` then it should be done before it is settled. For AuthorieNet - all the transactions are settled after 24 hours. - - * AuthorizeNet supports partical capture of the `authorized amount`. But it is advisable to use one - `authorization code` only [once](https://support.authorize.net/authkb/index?page=content&id=A1720&actp=LIST). + + * Authorize.Net automatically settles authorized transactions after 24 + hours. Hence, unnecessary authorizations must be `void/2`ed within this + period! + * Though Authorize.Net supports partial capture of the authorized `amount`, it + is [advised][sound-advice] not to do so. + + [sound-advice]: https://support.authorize.net/authkb/index?page=content&id=A1720&actp=LIST ## Optional Fields opts = [ order: %{invoice_number: String, description: String}, ref_id: String ] - + ## Example iex> opts = [ ref_id: "123456" ] - iex> amount = %{amount: Decimal.new(20.0), currency: 'USD'} + iex> amount = %{value: Decimal.new(20.0), currency: "USD"} iex> id = "123456" iex> result = Gringotts.capture(Gringotts.Gateways.AuthorizeNet, id, amount, opts) """ - @spec capture(String.t, Money.t, Keyword.t) :: {:ok | :error, Response.t} + @spec capture(String.t(), Money.t(), Keyword.t()) :: {:ok | :error, Response} def capture(id, amount, opts) do request_data = normal_capture(amount, id, opts, @transaction_type[:capture]) response_data = commit(:post, request_data, opts) @@ -262,10 +294,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do @doc """ Refund `amount` for a settled transaction referenced by `id`. - Use this method to refund a customer for a transaction that was already settled, requires - transId of the transaction. The `payment` field in the `opts` is used to set the mode of payment. - The `card` field inside `payment` needs the information of the credit card to be passed in the specified fields - so as to `refund` to that particular card. + The `payment` field in the `opts` is used to set the instrument/mode of + payment, which could be different from the original one. Currently, we + support only refunds to cards, so put the `card` details in the `payment`. + ## Required fields opts = [ payment: %{card: %{number: String, year: Integer, month: Integer}} @@ -275,14 +307,14 @@ defmodule Gringotts.Gateways.AuthorizeNet do ## Example iex> opts = [ - payment: %{card: %{number: "5424000000000015", year: 2020, month: 12}} + payment: %{card: %{number: "5424000000000015", year: 2099, month: 12}} ref_id: "123456" ] iex> id = "123456" - iex> amount = %{amount: Decimal.new(20.0), currency: 'USD'} + iex> amount = %{value: Decimal.new(20.0), currency: "USD"} iex> result = Gringotts.refund(Gringotts.Gateways.AuthorizeNet, amount, id, opts) """ - @spec refund(Money.t, String.t, Keyword.t) :: {:ok | :error, Response.t} + @spec refund(Money.t(), String.t(), Keyword.t()) :: {:ok | :error, Response} def refund(amount, id, opts) do request_data = normal_refund(amount, id, opts, @transaction_type[:refund]) response_data = commit(:post, request_data, opts) @@ -290,11 +322,13 @@ defmodule Gringotts.Gateways.AuthorizeNet do end @doc """ - To void a transaction + Voids the referenced payment. - Use this method to cancel either an original transaction that is not settled or - an entire order composed of more than one transaction. It can be submitted against 'purchase', `authorize` - and `capture`. Requires the `transId` of a transaction. + This method attempts a reversal of the either a previous `purchase/3` or + `authorize/3` referenced by `id`. + + It can cancel either an original transaction that may not be settled or an + entire order composed of more than one transaction. ## Optional fields opts = [ref_id: String] @@ -306,7 +340,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> id = "123456" iex> result = Gringotts.void(Gringotts.Gateways.AuthorizeNet, id, opts) """ - @spec void(String.t, Keyword.t) :: {:ok | :error, Response.t} + @spec void(String.t(), Keyword.t()) :: {:ok | :error, Response} def void(id, opts) do request_data = normal_void(id, opts, @transaction_type[:void]) response_data = commit(:post, request_data, opts) @@ -314,19 +348,29 @@ defmodule Gringotts.Gateways.AuthorizeNet do end @doc """ - Store a customer payment profile. - - Use this function to store the customer card information by creating a [customer profile](https://developer.authorize.net/api/reference/index.html#customer-profiles-create-customer-profile) which also - creates a `payment profile` if `card` inofrmation is provided, and in case the `customer profile` exists without a payment profile, the merchant - can create customer payment profile by passing the `customer_profile_id` in the `opts`. - The gateway also provide a provision for a `validation mode`, there are two modes `liveMode` - and `testMode`, to know more about modes [see](https://developer.authorize.net/api/reference/index.html#customer-profiles-create-customer-profile). - By default `validation mode` is set to `testMode`. - + Store a customer's profile and optionally associate it with a payment profile. + + Authorize.Net separates a [customer's profile][cust-profile] from their payment + profile. Thus a customer can have multiple payment profiles. + + ## Create both profiles + + Add `:customer` details in `opts` and also provide `card` details. The response + will contain a `:customer_profile_id`. + + ## Associate payment profile with existing customer profile + + Simply pass the `:customer_profile_id` in the `opts`. This will add the `card` + details to the profile referenced by the supplied `:customer_profile_id`. + ## Notes - * The current version of this library supports only `credit card` as the payment profile. - * If a customer profile is created without the card info, then to create a payment profile - `card` info needs to be passed alongwith `cutomer_profile_id` to create it. + + * Currently only supports `credit card` in the payment profile. + * The supplied `card` details can be validated by supplying a + [`:validation_mode`][cust-profile], available options are `testMode` and + `liveMode`, the deafult is `testMode`. + + [cust-profile]: https://developer.authorize.net/api/reference/index.html#customer-profiles-create-customer-profile ## Required Fields opts = [ @@ -347,16 +391,18 @@ defmodule Gringotts.Gateways.AuthorizeNet do profile: %{merchant_customer_id: 123456, description: "test store", email: "test@gmail.com"}, validation_mode: "testMode" ] - iex> card = %CreditCard{number: "5424000000000015", year: 2020, month: 12, verification_code: "999"} + iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} iex> result = Gringotts.store(Gringotts.Gateways.AuthorizeNet, card, opts) """ - @spec store(CreditCard.t, Keyword.t) :: {:ok | :error, Response.t} + @spec store(CreditCard.t(), Keyword.t()) :: {:ok | :error, Response} def store(card, opts) do - request_data = if opts[:customer_profile_id] do - card |> create_customer_payment_profile(opts) |> generate - else - card |> create_customer_profile(opts) |> generate - end + request_data = + if opts[:customer_profile_id] do + card |> create_customer_payment_profile(opts) |> generate + else + card |> create_customer_profile(opts) |> generate + end + response_data = commit(:post, request_data, opts) respond(response_data) end @@ -366,14 +412,14 @@ defmodule Gringotts.Gateways.AuthorizeNet do Use this function to unstore the customer card information by deleting the customer profile present. Requires the customer profile id. - + ## Example iex> id = "123456" iex> opts = [] iex> result = Gringotts.store(Gringotts.Gateways.AuthorizeNet, id, opts) """ - - @spec unstore(String.t, Keyword.t) :: {:ok | :error, Response.t} + + @spec unstore(String.t(), Keyword.t()) :: {:ok | :error, Response} def unstore(customer_profile_id, opts) do request_data = customer_profile_id |> delete_customer_profile(opts) |> generate response_data = commit(:post, request_data, opts) @@ -389,7 +435,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do # Function to return a response defp respond({:ok, %{body: body, status_code: 200}}) do - raw_response = naive_map(body) + raw_response = naive_map(body) response_type = ResponseHandler.check_response_type(raw_response) response_check(raw_response[response_type], raw_response) end @@ -413,16 +459,18 @@ defmodule Gringotts.Gateways.AuthorizeNet do {:error, ResponseHandler.parse_gateway_error(raw_response)} end - #------------------- Helper functions for the interface functions------------------- + ############################################################################## + # HELPER METHODS # + ############################################################################## # function for formatting the request as an xml for purchase and authorize method defp add_auth_purchase(amount, payment, opts, transaction_type) do :createTransactionRequest |> element(%{xmlns: @aut_net_namespace}, [ - add_merchant_auth(opts[:config]), - add_order_id(opts), - add_purchase_transaction_request(amount, transaction_type, payment, opts), - ]) + add_merchant_auth(opts[:config]), + add_order_id(opts), + add_purchase_transaction_request(amount, transaction_type, payment, opts) + ]) |> generate end @@ -430,35 +478,35 @@ defmodule Gringotts.Gateways.AuthorizeNet do defp normal_capture(amount, id, opts, transaction_type) do :createTransactionRequest |> element(%{xmlns: @aut_net_namespace}, [ - add_merchant_auth(opts[:config]), - add_order_id(opts), - add_capture_transaction_request(amount, id, transaction_type, opts), - ]) + add_merchant_auth(opts[:config]), + add_order_id(opts), + add_capture_transaction_request(amount, id, transaction_type, opts) + ]) |> generate end - #function to format the request for normal refund + # function to format the request for normal refund defp normal_refund(amount, id, opts, transaction_type) do :createTransactionRequest |> element(%{xmlns: @aut_net_namespace}, [ - add_merchant_auth(opts[:config]), - add_order_id(opts), - add_refund_transaction_request(amount, id, opts, transaction_type), - ]) + add_merchant_auth(opts[:config]), + add_order_id(opts), + add_refund_transaction_request(amount, id, opts, transaction_type) + ]) |> generate end - #function to format the request for normal void operation + # function to format the request for normal void operation defp normal_void(id, opts, transaction_type) do :createTransactionRequest |> element(%{xmlns: @aut_net_namespace}, [ - add_merchant_auth(opts[:config]), - add_order_id(opts), - element(:transactionRequest, [ - add_transaction_type(transaction_type), - add_ref_trans_id(id) - ]) - ]) + add_merchant_auth(opts[:config]), + add_order_id(opts), + element(:transactionRequest, [ + add_transaction_type(transaction_type), + add_ref_trans_id(id) + ]) + ]) |> generate end @@ -470,8 +518,9 @@ defmodule Gringotts.Gateways.AuthorizeNet do add_billing_info(opts), add_payment_source(card) ]), - element(:validationMode, - (if opts[:validation_mode], do: opts[:validation_mode], else: "testMode") + element( + :validationMode, + if(opts[:validation_mode], do: opts[:validation_mode], else: "testMode") ) ]) end @@ -484,16 +533,18 @@ defmodule Gringotts.Gateways.AuthorizeNet do element(:description, opts[:profile][:description]), element(:email, opts[:profile][:description]), element(:paymentProfiles, [ - element(:customerType, - (if opts[:customer_type], do: opts[:customer_type], else: "individual") + element( + :customerType, + if(opts[:customer_type], do: opts[:customer_type], else: "individual") ), add_billing_info(opts), add_payment_source(card) - ]), + ]) ]), - element(:validationMode, - (if opts[:validation_mode], do: opts[:validation_mode], else: "testMode") - ) + element( + :validationMode, + if(opts[:validation_mode], do: opts[:validation_mode], else: "testMode") + ) ]) end @@ -504,8 +555,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do ]) end - #--------------- XMl Builder functions for helper functions to assist - #---------------in attaching different tags for request + ############################################################################## + # HELPERS TO ASSIST IN BUILDING AND # + # COMPOSING DIFFERENT XmlBuilder TAGS # + ############################################################################## defp add_merchant_auth(opts) do element(:merchantAuthentication, [ @@ -547,7 +600,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do element(:payment, [ element(:creditCard, [ element(:cardNumber, opts[:payment][:card][:number]), - element(:expirationDate, + element( + :expirationDate, join_string([opts[:payment][:card][:year], opts[:payment][:card][:month]], "-") ) ]) @@ -566,8 +620,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do defp add_amount(amount) do if amount do - amount = amount |> Money.value |> Decimal.to_float - element(:amount, amount) + {_, value} = amount |> Money.to_string() + element(:amount, value) end end @@ -588,10 +642,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do end defp add_invoice(transactionType, opts) do - element( - [element(:order, [ + element([ + element(:order, [ element(:invoiceNumber, opts[:order][:invoice_number]), - element(:description, opts[:order][:description]), + element(:description, opts[:order][:description]) ]), element(:lineItems, [ element(:lineItem, [ @@ -600,8 +654,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do element(:description, opts[:lineitems][:description]), element(:quantity, opts[:lineitems][:quantity]), element( - :unitPrice, - opts[:lineitems][:unit_price] |> Money.value |> Decimal.to_float + :unitPrice, + opts[:lineitems][:unit_price] |> Money.value() |> Decimal.to_float() ) ]) ]) @@ -612,7 +666,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do element(:tax, [ add_amount(opts[:tax][:amount]), element(:name, opts[:tax][:name]), - element(:description, opts[:tax][:description]), + element(:description, opts[:tax][:description]) ]) end @@ -620,7 +674,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do element(:duty, [ add_amount(opts[:duty][:amount]), element(:name, opts[:duty][:name]), - element(:description, opts[:duty][:description]), + element(:description, opts[:duty][:description]) ]) end @@ -628,7 +682,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do element(:shipping, [ add_amount(opts[:shipping][:amount]), element(:name, opts[:shipping][:name]), - element(:description, opts[:shipping][:description]), + element(:description, opts[:shipping][:description]) ]) end @@ -673,7 +727,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do element(:city, opts[:ship_to][:city]), element(:state, opts[:ship_to][:state]), element(:zip, opts[:ship_to][:zip]), - element(:country, opts[:ship_to][:country]) + element(:country, opts[:ship_to][:country]) ]) end @@ -684,11 +738,11 @@ defmodule Gringotts.Gateways.AuthorizeNet do defp join_string(list, symbol) do Enum.join(list, symbol) end - + defp base_url(opts) do if opts[:config][:mode] == :prod do @production_url - else + else @test_url end end @@ -696,7 +750,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do defmodule ResponseHandler do @moduledoc false alias Gringotts.Response - + @response_type %{ auth_response: "authenticateTestResponse", transaction_response: "createTransactionResponse", @@ -708,39 +762,47 @@ defmodule Gringotts.Gateways.AuthorizeNet do def parse_gateway_success(raw_response) do response_type = check_response_type(raw_response) + token = raw_response[response_type]["transactionResponse"]["transId"] message = raw_response[response_type]["messages"]["message"]["text"] avs_result = raw_response[response_type]["transactionResponse"]["avsResultCode"] cvc_result = raw_response[response_type]["transactionResponse"]["cavvResultCode"] [] - |> status_code(200) - |> set_message(message) - |> set_avs_result(avs_result) - |> set_cvc_result(cvc_result) - |> set_params(raw_response) - |> set_success(true) - |> handle_opts + |> status_code(200) + |> set_token(token) + |> set_message(message) + |> set_avs_result(avs_result) + |> set_cvc_result(cvc_result) + |> set_params(raw_response) + |> set_success(true) + |> handle_opts end def parse_gateway_error(raw_response) do response_type = check_response_type(raw_response) - - {message, error_code} = if raw_response[response_type]["transactionResponse"]["errors"] do - {raw_response[response_type]["messages"]["message"]["text"] <> " " <> - raw_response[response_type]["transactionResponse"]["errors"]["error"]["errorText"], - raw_response[response_type]["transactionResponse"]["errors"]["error"]["errorCode"]} - else - {raw_response[response_type]["messages"]["message"]["text"], - raw_response[response_type]["messages"]["message"]["code"]} - end + + {message, error_code} = + if raw_response[response_type]["transactionResponse"]["errors"] do + { + raw_response[response_type]["messages"]["message"]["text"] <> + " " <> + raw_response[response_type]["transactionResponse"]["errors"]["error"]["errorText"], + raw_response[response_type]["transactionResponse"]["errors"]["error"]["errorCode"] + } + else + { + raw_response[response_type]["messages"]["message"]["text"], + raw_response[response_type]["messages"]["message"]["code"] + } + end [] - |> status_code(200) - |> set_message(message) - |> set_error_code(error_code) - |> set_params(raw_response) - |> set_success(false) - |> handle_opts + |> status_code(200) + |> set_message(message) + |> set_error_code(error_code) + |> set_params(raw_response) + |> set_success(false) + |> handle_opts end def check_response_type(raw_response) do @@ -752,21 +814,21 @@ defmodule Gringotts.Gateways.AuthorizeNet do raw_response[@response_type[:delete_customer_profile]] -> "deleteCustomerProfileResponse" end end - - defp set_success(opts, value), do: opts ++ [success: value] - defp status_code(opts, code), do: opts ++ [status_code: code] - defp set_message(opts, message), do: opts ++ [message: message] - defp set_avs_result(opts, result), do: opts ++ [avs_result: result] - defp set_cvc_result(opts, result), do: opts ++ [cvc_result: result] - defp set_params(opts, raw_response), do: opts ++ [params: raw_response] - defp set_error_code(opts, code), do: opts ++ [error_code: code] - + + defp set_token(opts, token), do: [{:authorization, token} | opts] + defp set_success(opts, value), do: [{:success, value} | opts] + defp status_code(opts, code), do: [{:status, code} | opts] + defp set_message(opts, message), do: [{:message, message} | opts] + defp set_avs_result(opts, result), do: [{:avs, result} | opts] + defp set_cvc_result(opts, result), do: [{:cvc, result} | opts] + defp set_params(opts, raw_response), do: [{:params, raw_response} | opts] + defp set_error_code(opts, code), do: [{:error, code} | opts] + defp handle_opts(opts) do case Keyword.fetch(opts, :success) do {:ok, true} -> Response.success(opts) {:ok, false} -> Response.error(opts) end end - end end diff --git a/test/gateways/authorize_net_test.exs b/test/gateways/authorize_net_test.exs index 3c177dc4..b4906339 100644 --- a/test/gateways/authorize_net_test.exs +++ b/test/gateways/authorize_net_test.exs @@ -1,53 +1,63 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do - - Code.require_file "../mocks/authorize_net_mock.exs", __DIR__ + Code.require_file("../mocks/authorize_net_mock.exs", __DIR__) use ExUnit.Case, async: false alias Gringotts.Gateways.AuthorizeNetMock, as: MockResponse alias Gringotts.CreditCard alias Gringotts.Gateways.AuthorizeNet, as: ANet - + import Mock @auth %{name: "64jKa6NA", transaction_key: "4vmE338dQmAN6m7B"} - @card %CreditCard { + @card %CreditCard{ number: "5424000000000015", month: 12, - year: 2020, + year: 2099, verification_code: 999 } - @bad_card %CreditCard { + @bad_card %CreditCard{ number: "123", month: 10, year: 2010, verification_code: 123 } - @amount %{amount: Decimal.new(20.0), currency: 'USD'} + @amount Money.new("2.99", :USD) @opts [ config: @auth, ref_id: "123456", - order: %{invoice_number: "INV-12345", description: "Product Description"}, + order: %{invoice_number: "INV-12345", description: "Product Description"}, lineitems: %{ item_id: "1", name: "vase", - description: "Cannes logo", - quantity: "18", - unit_price: %{amount: Decimal.new(20.0), currency: 'USD'} + description: "Cannes logo", + quantity: 18, + unit_price: Money.mult!(@amount, 18) + }, + tax: %{name: "VAT", amount: Money.new("0.1", :EUR), description: "Value Added Tax"}, + shipping: %{ + name: "SAME-DAY-DELIVERY", + amount: Money.new("0.56", :EUR), + description: "Zen Logistics" + }, + duty: %{ + name: "import_duty", + amount: Money.new("0.25", :EUR), + description: "Upon import of goods" } ] @opts_refund [ config: @auth, - ref_id: "123456", - payment: %{card: %{number: "5424000000000015", year: 2020, month: 12}} + ref_id: "123456", + payment: %{card: %{number: "5424000000000015", year: 2099, month: 12}} ] @opts_store [ config: @auth, profile: %{ - merchant_customer_id: "123456", - description: "Profile description here", + merchant_customer_id: "123456", + description: "Profile description here", email: "customer-profile-email@here.com" }, customer_type: "individual", @@ -56,34 +66,35 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do @opts_store_without_validation [ config: @auth, profile: %{ - merchant_customer_id: "123456", - description: "Profile description here", + merchant_customer_id: "123456", + description: "Profile description here", email: "customer-profile-email@here.com" } ] @opts_store_no_profile [ - config: @auth, + config: @auth ] @opts_refund [ config: @auth, ref_id: "123456", - payment: %{card: %{number: "5424000000000015", year: 2020, month: 12}} + payment: %{card: %{number: "5424000000000015", year: 2099, month: 12}} ] @opts_refund_bad_payment [ config: @auth, ref_id: "123456", - payment: %{card: %{number: "123", year: 2020, month: 12}} + payment: %{card: %{number: "123", year: 2099, month: 12}} ] @opts_store [ config: @auth, - profile: %{merchant_customer_id: "123456", + profile: %{ + merchant_customer_id: "123456", description: "Profile description here", email: "customer-profile-email@here.com" } ] @opts_store_no_profile [ - config: @auth, + config: @auth ] @opts_customer_profile [ config: @auth, @@ -91,11 +102,11 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do validation_mode: "testMode", customer_type: "individual" ] - @opts_customer_profile_args[ - config: @auth, - customer_profile_id: "1814012002" + @opts_customer_profile_args [ + config: @auth, + customer_profile_id: "1814012002" ] - + @refund_id "60036752756" @void_id "60036855217" @void_invalid_id "60036855211" @@ -110,17 +121,21 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "purchase" do test "successful response with right params" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_purchase_response end] do - assert {:ok, response} = ANet.purchase(@amount, @card, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" + request: fn _method, _url, _body, _headers -> + MockResponse.successful_purchase_response() + end do + assert {:ok, response} = ANet.purchase(@amount, @card, @opts) + assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end test "with bad card" do - with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.bad_card_purchase_response end] do - assert {:error, response} = ANet.purchase(@amount, @bad_card, @opts) - assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" + with_mock HTTPoison, + request: fn _method, _url, _body, _headers -> + MockResponse.bad_card_purchase_response() + end do + assert {:error, response} = ANet.purchase(@amount, @bad_card, @opts) + assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" end end end @@ -128,17 +143,21 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "authorize" do test "successful response with right params" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_authorize_response end] do + request: fn _method, _url, _body, _headers -> + MockResponse.successful_authorize_response() + end do assert {:ok, response} = ANet.authorize(@amount, @card, @opts) assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" - end + end end test "with bad card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.bad_card_purchase_response end] do - assert {:error, response} = ANet.authorize(@amount, @bad_card, @opts) - assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" + request: fn _method, _url, _body, _headers -> + MockResponse.bad_card_purchase_response() + end do + assert {:error, response} = ANet.authorize(@amount, @bad_card, @opts) + assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" end end end @@ -146,15 +165,17 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "capture" do test "successful response with right params" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_capture_response end] do + request: fn _method, _url, _body, _headers -> + MockResponse.successful_capture_response() + end do assert {:ok, response} = ANet.capture(@capture_id, @amount, @opts) assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end - + test "with bad transaction id" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.bad_id_capture end] do + request: fn _method, _url, _body, _headers -> MockResponse.bad_id_capture() end do assert {:error, response} = ANet.capture(@capture_invalid_id, @amount, @opts) assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end @@ -164,7 +185,9 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "refund" do test "successful response with right params" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_refund_response end] do + request: fn _method, _url, _body, _headers -> + MockResponse.successful_refund_response() + end do assert {:ok, response} = ANet.refund(@amount, @refund_id, @opts_refund) assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end @@ -172,17 +195,17 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "bad payment params" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.bad_card_refund end] do - assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund_bad_payment) - assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" + request: fn _method, _url, _body, _headers -> MockResponse.bad_card_refund() end do + assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund_bad_payment) + assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" end end test "debit less than refund amount" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.debit_less_than_refund end] do - assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" + request: fn _method, _url, _body, _headers -> MockResponse.debit_less_than_refund() end do + assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund) + assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end end end @@ -190,17 +213,17 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "void" do test "successful response with right params" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_void end] do - assert {:ok, response} = ANet.void(@void_id, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" + request: fn _method, _url, _body, _headers -> MockResponse.successful_void() end do + assert {:ok, response} = ANet.void(@void_id, @opts) + assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end - + test "with bad transaction id" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.void_non_existent_id end] do - assert {:error, response} = ANet.void(@void_invalid_id, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" + request: fn _method, _url, _body, _headers -> MockResponse.void_non_existent_id() end do + assert {:error, response} = ANet.void(@void_invalid_id, @opts) + assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end end end @@ -208,41 +231,49 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "store" do test "successful response with right params" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_store_response end] do - assert {:ok, response} = ANet.store(@card, @opts_store) - assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" + request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do + assert {:ok, response} = ANet.store(@card, @opts_store) + assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" end end test "successful response without validation and customer type" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_store_response end] do - assert {:ok, response} = ANet.store(@card, @opts_store_without_validation) - assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" + request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do + assert {:ok, response} = ANet.store(@card, @opts_store_without_validation) + assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" end end test "without any profile" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.store_without_profile_fields end] do - assert {:error, response} = ANet.store(@card, @opts_store_no_profile) - assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Error" - end + request: fn _method, _url, _body, _headers -> + MockResponse.store_without_profile_fields() + end do + assert {:error, response} = ANet.store(@card, @opts_store_no_profile) + + assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == + "Error" + end end test "with customer profile id" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.customer_payment_profile_success_response end] do - assert {:ok, response} = ANet.store(@card, @opts_customer_profile) - assert response.params["createCustomerPaymentProfileResponse"]["messages"]["resultCode"] == "Ok" + request: fn _method, _url, _body, _headers -> + MockResponse.customer_payment_profile_success_response() + end do + assert {:ok, response} = ANet.store(@card, @opts_customer_profile) + + assert response.params["createCustomerPaymentProfileResponse"]["messages"]["resultCode"] == + "Ok" end end test "successful response without valiadtion mode and customer type" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_store_response end] do - assert {:ok, response} = ANet.store(@card, @opts_customer_profile_args) - assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" + request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do + assert {:ok, response} = ANet.store(@card, @opts_customer_profile_args) + assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" end end end @@ -250,19 +281,22 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "unstore" do test "successful response with right params" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.successful_unstore_response end] do - assert {:ok, response} = ANet.unstore(@unstore_id, @opts) - assert response.params["deleteCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" + request: fn _method, _url, _body, _headers -> + MockResponse.successful_unstore_response() + end do + assert {:ok, response} = ANet.unstore(@unstore_id, @opts) + assert response.params["deleteCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" end end end test "network error type non existent domain" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.netwok_error_non_existent_domain end] do - assert {:error, response} = ANet.purchase(@amount, @card, @opts) - assert response.message == "HTTPoison says 'nxdomain'" + request: fn _method, _url, _body, _headers -> + MockResponse.netwok_error_non_existent_domain() + end do + assert {:error, response} = ANet.purchase(@amount, @card, @opts) + assert response.message == "HTTPoison says 'nxdomain'" end end - end diff --git a/test/mocks/authorize_net_mock.exs b/test/mocks/authorize_net_mock.exs index beabfa21..cb68df4a 100644 --- a/test/mocks/authorize_net_mock.exs +++ b/test/mocks/authorize_net_mock.exs @@ -3,7 +3,7 @@ # purchase mock response def successful_purchase_response do {:ok, - %HTTPoison.Response{body: "123456OkI00001Successful.1C7HPT1YP2600365530965D6782A03246EE3BAFABE8006E32DE970XXXX0015MasterCard1This transaction has been approved.", + %HTTPoison.Response{body: ~s{123456OkI00001Successful.1C7HPT1YP2600365530965D6782A03246EE3BAFABE8006E32DE970XXXX0015MasterCard1This transaction has been approved.}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", @@ -21,7 +21,7 @@ def bad_card_purchase_response do {:ok, - %HTTPoison.Response{body: "ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XXXXX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.", + %HTTPoison.Response{body: ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XXXXX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", @@ -39,7 +39,7 @@ def bad_amount_purchase_response do {:ok, - %HTTPoison.Response{body: "123456ErrorE00027The transaction was unsuccessful.3P0C7C56F020A2AE2660A87637CD00B4D5C0XXXX0015MasterCard5A valid amount is required.", + %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P0C7C56F020A2AE2660A87637CD00B4D5C0XXXX0015MasterCard5A valid amount is required.}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", @@ -58,7 +58,7 @@ # authorize mock response def successful_authorize_response do {:ok, - %HTTPoison.Response{body: "123456OkI00001Successful.1K6Z0ABYP260036854582A4AD079E22A271D92662CF093CED7A5D0XXXX0015MasterCard1This transaction has been approved.", + %HTTPoison.Response{body: ~s{123456OkI00001Successful.1K6Z0ABYP260036854582A4AD079E22A271D92662CF093CED7A5D0XXXX0015MasterCard1This transaction has been approved.}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", @@ -76,7 +76,7 @@ def bad_card_authorize_response do {:ok, - %HTTPoison.Response{body: "ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XXXXX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.", + %HTTPoison.Response{body: ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XXXXX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", @@ -94,7 +94,7 @@ def bad_amount_authorize_response do {:ok, - %HTTPoison.Response{body: "123456ErrorE00027The transaction was unsuccessful.3P0C7C56F020A2AE2660A87637CD00B4D5C0XXXX0015MasterCard290There is one or more missing or invalid required fields.", + %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P0C7C56F020A2AE2660A87637CD00B4D5C0XXXX0015MasterCard290There is one or more missing or invalid required fields.}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", @@ -114,7 +114,7 @@ def successful_capture_response do {:ok, - %HTTPoison.Response{body: "123456OkI00001Successful.14OKD6YP6003685493160036854931348C4ECD0F764736B012C4655BFA68EF0XXXX0015MasterCard1This transaction has been approved.", + %HTTPoison.Response{body: ~s{123456OkI00001Successful.14OKD6YP6003685493160036854931348C4ECD0F764736B012C4655BFA68EF0XXXX0015MasterCard1This transaction has been approved.}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", @@ -132,7 +132,7 @@ def bad_id_capture do {:ok, - %HTTPoison.Response{body: "123456ErrorE00027The transaction was unsuccessful.3P0A5280E2A6AA1290D451A24286692D1B0033A valid referenced transaction ID is required.", + %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P0A5280E2A6AA1290D451A24286692D1B0033A valid referenced transaction ID is required.}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", @@ -151,7 +151,7 @@ # refund mock response def successful_refund_response do {:ok, - %HTTPoison.Response{body: "123456OkI00001Successful.1P6003685566160036752756169F2381B172A5AA247A01757A3E520A0XXXX0015MasterCard1This transaction has been approved.", + %HTTPoison.Response{body: ~s{123456OkI00001Successful.1P6003685566160036752756169F2381B172A5AA247A01757A3E520A0XXXX0015MasterCard1This transaction has been approved.}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", @@ -169,7 +169,7 @@ def bad_card_refund do {:ok, - %HTTPoison.Response{body: "ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.", + %HTTPoison.Response{body: ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", @@ -187,7 +187,7 @@ def debit_less_than_refund do {:ok, - %HTTPoison.Response{body: "123456ErrorE00027The transaction was unsuccessful.3P060036752756A5280E2A6AA1290D451A24286692D1B00XXXX0015MasterCard55The sum of credits against the referenced transaction would exceed original debit amount.", + %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P060036752756A5280E2A6AA1290D451A24286692D1B00XXXX0015MasterCard55The sum of credits against the referenced transaction would exceed original debit amount.}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", @@ -206,7 +206,7 @@ # void mock response def successful_void do {:ok, - %HTTPoison.Response{body: "123456OkI00001Successful.1ZJPVRXP6003685521760036855217F09A215511891DCEA91B6CC52B9F4E870XXXX0015MasterCard1This transaction has been approved.", + %HTTPoison.Response{body: ~s{123456OkI00001Successful.1ZJPVRXP6003685521760036855217F09A215511891DCEA91B6CC52B9F4E870XXXX0015MasterCard1This transaction has been approved.}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", @@ -224,7 +224,7 @@ def void_non_existent_id do {:ok, - %HTTPoison.Response{body: "123456ErrorE00027The transaction was unsuccessful.3P060036855219C7C56F020A2AE2660A87637CD00B4D5C016The transaction cannot be found.", + %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P060036855219C7C56F020A2AE2660A87637CD00B4D5C016The transaction cannot be found.}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", @@ -244,7 +244,7 @@ def successful_store_response do {:ok, - %HTTPoison.Response{body: "OkI00001Successful.18139914901808649724", + %HTTPoison.Response{body: ~s{OkI00001Successful.18139914901808649724}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", @@ -262,7 +262,7 @@ def store_without_profile_fields do {:ok, - %HTTPoison.Response{body: "ErrorE00041One or more fields in the profile must contain a value.", + %HTTPoison.Response{body: ~s{ErrorE00041One or more fields in the profile must contain a value.}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", @@ -281,7 +281,7 @@ #unstore mock response def successful_unstore_response do {:ok, - %HTTPoison.Response{body: "OkI00001Successful.", + %HTTPoison.Response{body: ~s{OkI00001Successful.}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", @@ -299,7 +299,7 @@ def customer_payment_profile_success_response do {:ok, - %HTTPoison.Response{body: "OkI00001Successful.181401200218086700051,1,1,(TESTMODE) This transaction has been approved.,000000,P,0,none,Test transaction for ValidateCustomerPaymentProfile.,1.00,CC,auth_only,none,,,,,,,,,,,email@example.com,,,,,,,,,0.00,0.00,0.00,FALSE,none,EA9FD49A9501D0415FE26BAEF9FD8B2C,,,,,,,,,,,,,XXXX0015,MasterCard,,,,,,,,,,,,,,,,,", + %HTTPoison.Response{body: ~s{OkI00001Successful.181401200218086700051,1,1,(TESTMODE) This transaction has been approved.,000000,P,0,none,Test transaction for ValidateCustomerPaymentProfile.,1.00,CC,auth_only,none,,,,,,,,,,,email@example.com,,,,,,,,,0.00,0.00,0.00,FALSE,none,EA9FD49A9501D0415FE26BAEF9FD8B2C,,,,,,,,,,,,,XXXX0015,MasterCard,,,,,,,,,,,,,,,,,}, headers: [{"Cache-Control", "private"}, {"Content-Type", "application/xml; charset=utf-8"}, {"X-OPNET-Transaction-Trace", From 5e748e711476f5f6b191099ccc0006758d723ecb Mon Sep 17 00:00:00 2001 From: Jyoti Gautam Date: Thu, 25 Jan 2018 18:39:33 +0530 Subject: [PATCH 19/60] [trexle] Adds Money protocol (#84) * Integrated Money protocol with trexle * token and message added to Response * Used `~s` sigils in mocks --- lib/gringotts/gateways/trexle.ex | 382 +++++++++++++++++++------------ test/gateways/trexle_test.exs | 184 ++++++--------- test/mocks/trexle_mock.exs | 369 +++++++++++++---------------- 3 files changed, 464 insertions(+), 471 deletions(-) diff --git a/lib/gringotts/gateways/trexle.ex b/lib/gringotts/gateways/trexle.ex index 05f7d5c8..d89d7767 100644 --- a/lib/gringotts/gateways/trexle.ex +++ b/lib/gringotts/gateways/trexle.ex @@ -1,9 +1,8 @@ defmodule Gringotts.Gateways.Trexle do - @moduledoc """ - Trexle Payment Gateway Implementation: + [Trexle][home] Payment Gateway implementation. - For further details, please refer [Trexle API documentation](https://docs.trexle.com/). + > For further details, please refer [Trexle API documentation][docs]. Following are the features that have been implemented for the Trexle Gateway: @@ -16,146 +15,213 @@ defmodule Gringotts.Gateways.Trexle do | Store | `store/2` | ## The `opts` argument - A `Keyword` list `opts` passed as an optional argument for transactions with the gateway. Following are the keys + + Most `Gringotts` API calls accept an optional `keyword` list `opts` to supply + optional arguments for transactions with Trexle. The following keys are supported: - * email - * ip_address - * description + * `email` + * `ip_address` + * `description` + + [docs]: https://docs.trexle.com/ + [home]: https://trexle.com/ + + ## Registering your Trexle account at `Gringotts` + + After [creating your account][dashboard] successfully on Trexle, head to the dashboard and find + your account "secrets" in the [`API keys`][keys] section. - ## Trexle account registeration with `Gringotts` - After creating your account successfully on [Trexle](https://docs.trexle.com/) follow the [dashboard link](https://trexle.com/dashboard/api-keys) to fetch the secret api_key. + Here's how the secrets map to the required configuration parameters for MONEI: + + | Config parameter | Trexle secret | + | ------- | ---- | + | `:api_key` | **API key** | Your Application config must look something like this: config :gringotts, Gringotts.Gateways.Trexle, adapter: Gringotts.Gateways.Trexle, - api_key: "Secret API key", - default_currency: "USD" + api_key: "your-secret-API-key" + + [dashboard]: https://trexle.com/dashboard/ + [keys]: https://trexle.com/dashboard/api-keys + + ## Scope of this module + + * Trexle processes money in cents.**citation-needed**. + + ## Supported Gateways + + Find the official [list here][gateways]. + + [gateways]: https://trexle.com/payment-gateway + + ## Following the examples + + 1. First, set up a sample application and configure it to work with Trexle. + - You could do that from scratch by following our [Getting Started][gs] guide. + - To save you time, we recommend [cloning our example repo][example-repo] + that gives you a pre-configured sample app ready-to-go. + + You could use the same config or update it the with your "secrets" + that as described + [above](#module-registering-your-trexle-account-at-gringotts). + + 2. Run an `iex` session with `iex -S mix` and add some variable bindings and + aliases to it (to save some time): + ``` + iex> alias Gringotts.{Response, CreditCard, Gateways.Trexle} + iex> card = %CreditCard{ + first_name: "Harry", + last_name: "Potter", + number: "4200000000000000", + year: 2099, month: 12, + verification_code: "123", + brand: "VISA"} + iex> address = %Address{ + street1: "301, Gryffindor", + street2: "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", + city: "Highlands", + region: "SL", + country: "GB", + postal_code: "11111", + phone: "(555)555-5555"} + iex> options = [email: "masterofdeath@ministryofmagic.gov", + ip_address: "127.0.0.1", + billing_address: address, + description: "For our valued customer, Mr. Potter"] + ``` + + We'll be using these in the examples below. + + [example-repo]: https://github.com/aviabird/gringotts_example + [gs]: # """ @base_url "https://core.trexle.com/api/v1/" use Gringotts.Gateways.Base - use Gringotts.Adapter, required_config: [:api_key, :default_currency] + use Gringotts.Adapter, required_config: [:api_key] import Poison, only: [decode: 1] - alias Gringotts.{Response, CreditCard, Address} + alias Gringotts.{Response, CreditCard, Address, Money} @doc """ - Performs the authorization of the card to be used for payment. + Performs a (pre) Authorize operation. - Authorizes your card with the given amount and returns a charge token and captured status as false in response. + The authorization validates the `card` details with the banking network, + places a hold on the transaction `amount` in the customer’s issuing bank and + also triggers risk management. Funds are not transferred. + + Trexle returns a "charge token", avaliable in the `Response.authorization` + field, which can be used in future to perform a `capture/3`. ### Example - ``` - iex> amount = 100 - iex> card = %CreditCard{ - number: "5200828282828210", - month: 12, - year: 2018, - first_name: "John", - last_name: "Doe", - verification_code: "123", - brand: "visa" - } + The following session shows how one would (pre) authorize a payment of $100 on + a sample `card`. + ``` + iex> amount = %{value: Decimal.new(100),currency: "USD") + iex> card = %CreditCard{ + first_name: "Harry", + last_name: "Potter", + number: "5200828282828210", + year: 2099, month: 12, + verification_code: "123", + brand: "VISA"} iex> address = %Address{ - street1: "123 Main", - street2: "Suite 100", - city: "New York", - region: "NY", - country: "US", - postal_code: "11111", - phone: "(555)555-5555" - } - - iex> options = [email: "john@trexle.com", ip_address: "66.249.79.118", billing_address: address, description: "Store Purchase 1437598192"] - - iex> Gringotts.authorize(:payment_worker, Gringotts.Gateways.Trexle, amount, card, options) + street1: "301, Gryffindor", + street2: "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", + city: "Highlands", + region: "SL", + country: "GB", + postal_code: "11111", + phone: "(555)555-5555"} + iex> options = [email: "masterofdeath@ministryofmagic.gov", + ip_address: "127.0.0.1", + billing_address: address, + description: "For our valued customer, Mr. Potter"] + iex> Gringotts.authorize(Gringotts.Gateways.Trexle, amount, card, options) ``` """ - - @spec authorize(float, CreditCard.t, list) :: map + @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} def authorize(amount, payment, opts \\ []) do params = create_params_for_auth_or_purchase(amount, payment, opts, false) commit(:post, "charges", params, opts) end @doc """ - Performs the amount transfer from the customer to the merchant. + Captures a pre-authorized `amount`. + + `amount` is transferred to the merchant account by MONEI when it is smaller or + equal to the amount used in the pre-authorization referenced by `charge_token`. + + Trexle returns a "charge token", avaliable in the `Response.authorization` + field, which can be used in future to perform a `refund/2`. - The actual amount deduction performed by Trexle using the customer's card info. + ## Note + + Multiple captures cannot be performed on the same "charge token". If the + captured amount is smaller than the (pre) authorized amount, the "un-captured" + amount is released.**citation-needed** ## Example - ``` - iex> card = %CreditCard{ - number: "5200828282828210", - month: 12, - year: 2018, - first_name: "John", - last_name: "Doe", - verification_code: "123", - brand: "visa" - } - iex> address = %Address{ - street1: "123 Main", - street2: "Suite 100", - city: "New York", - region: "NY", - country: "US", - postal_code: "11111", - phone: "(555)555-5555" - } - - iex> options = [email: "john@trexle.com", ip_address: "66.249.79.118" ,billing_address: address, description: "Store Purchase 1437598192"] - - iex> @address %Address{ - street1: "123 Main", - street2: "Suite 100", - city: "New York", - region: "NY", - country: "US", - postal_code: "11111", - phone: "(555)555-5555" - } - - iex> options = [email: "john@trexle.com", ip_address: "66.249.79.118" ,billing_address: @address, description: "Store Purchase 1437598192"] - - iex> amount = 50 - - iex> Gringotts.purchase(:payment_worker, Gringotts.Gateways.Trexle, amount, card, options) + The following example shows how one would (partially) capture a previously + authorized a payment worth $10 by referencing the obtained `charge_token`. + + ``` + iex> amount = %{value: Decimal.new(100),currency: "USD") + iex> token = "some-real-token" + iex> Gringotts.capture(Gringotts.Gateways.Trexle, token, amount) ``` """ - - @spec purchase(float, CreditCard.t, list) :: map - def purchase(amount, payment, opts \\ []) do - params = create_params_for_auth_or_purchase(amount, payment, opts) - commit(:post, "charges", params, opts) + @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} + def capture(charge_token, amount, opts \\ []) do + {_, int_value, _} = Money.to_integer(amount) + params = [amount: int_value] + commit(:put, "charges/#{charge_token}/capture", params, opts) end @doc """ - Captures a particular amount using the charge token of a pre authorized card. + Transfers `amount` from the customer to the merchant. - The amount specified should be less than or equal to the amount given prior to capture while authorizing the card. - If the amount mentioned is less than the amount given in authorization process, the mentioned amount is debited. - Please note that multiple captures can't be performed for a given charge token from the authorization process. + Trexle attempts to process a purchase on behalf of the customer, by debiting + `amount` from the customer's account by charging the customer's `card`. - ### Example - ``` - iex> amount = 100 + ## Example - iex> token = "charge_6a5fcdc6cdbf611ee3448a9abad4348b2afab3ec" + The following session shows how one would process a payment worth $100 in + one-shot, without (pre) authorization. - iex> Gringotts.capture(:payment_worker, Gringotts.Gateways.Trexle, token, amount) + ``` + iex> amount = %{value: Decimal.new(100),currency: "USD") + iex> card = %CreditCard{ + first_name: "Harry", + last_name: "Potter", + number: "5200828282828210", + year: 2099, month: 12, + verification_code: "123", + brand: "VISA"} + iex> address = %Address{ + street1: "301, Gryffindor", + street2: "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", + city: "Highlands", + region: "SL", + country: "GB", + postal_code: "11111", + phone: "(555)555-5555"} + iex> options = [email: "masterofdeath@ministryofmagic.gov", + ip_address: "127.0.0.1", + billing_address: address, + description: "For our valued customer, Mr. Potter"] + iex> Gringotts.purchase(Gringotts.Gateways.Trexle, amount, card, options) ``` """ - - @spec capture(String.t, float, list) :: map - def capture(charge_token, amount, opts \\ []) do - params = [amount: amount] - commit(:put, "charges/#{charge_token}/capture", params, opts) + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + def purchase(amount, payment, opts \\ []) do + params = create_params_for_auth_or_purchase(amount, payment, opts) + commit(:post, "charges", params, opts) end @doc """ @@ -164,24 +230,28 @@ defmodule Gringotts.Gateways.Trexle do Trexle processes a full or partial refund worth `amount`, referencing a previous `purchase/3` or `capture/3`. - Multiple refund can be performed for the same charge token from purchase or capture done before performing refund action unless the cumulative amount is less than the amount given while authorizing. + Trexle returns a "refund token", avaliable in the `Response.authorization` + field. - ## Example - The following session shows how one would refund a previous purchase (and similarily for captures). - ``` - iex> amount = 5 + Multiple, partial refunds can be performed on the same "charge token" + referencing a previous `purchase/3` or `capture/3` till the cumulative refunds + equals the `capture/3`d or `purchase/3`d amount. - iex> token = "charge_668d3e169b27d4938b39246cb8c0890b0bd84c3c" + ## Example - iex> options = [email: "john@trexle.com", ip_address: "66.249.79.118", description: "Store Purchase 1437598192"] + The following session shows how one would refund $100 of a previous + `purchase/3` (and similarily for `capture/3`s). - iex> Gringotts.refund(:payment_worker, Gringotts.Gateways.Trexle, amount, token, options) + ``` + iex> amount = %{value: Decimal.new(100),currency: "USD") + iex> token = "some-real-token" + iex> Gringotts.refund(Gringotts.Gateways.Trexle, amount, token) ``` """ - - @spec refund(float, String.t, list) :: map + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} def refund(amount, charge_token, opts \\ []) do - params = [amount: amount] + {_, int_value, _} = Money.to_integer(amount) + params = [amount: int_value] commit(:post, "charges/#{charge_token}/refunds", params, opts) end @@ -189,52 +259,51 @@ defmodule Gringotts.Gateways.Trexle do Stores the card information for future use. ## Example - The following session shows how one would store a card (a payment-source) for future use. + + The following session shows how one would store a card (a payment-source) for + future use. ``` iex> card = %CreditCard{ - number: "5200828282828210", - month: 12, - year: 2018, - first_name: "John", - last_name: "Doe", - verification_code: "123", - brand: "visa" - } - + first_name: "Harry", + last_name: "Potter", + number: "5200828282828210", + year: 2099, month: 12, + verification_code: "123", + brand: "VISA"} iex> address = %Address{ - street1: "123 Main", - street2: "Suite 100", - city: "New York", - region: "NY", - country: "US", - postal_code: "11111", - phone: "(555)555-5555" - } - - iex> options = [email: "john@trexle.com", ip_address: "66.249.79.118", billing_address: address, description: "Store Purchase 1437598192"] - - iex> Gringotts.store(:payment_worker, Gringotts.Gateways.Trexle, card, options) + street1: "301, Gryffindor", + street2: "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", + city: "Highlands", + region: "SL", + country: "GB", + postal_code: "11111", + phone: "(555)555-5555"} + iex> options = [email: "masterofdeath@ministryofmagic.gov", + ip_address: "127.0.0.1", + billing_address: address, + description: "For our valued customer, Mr. Potter"] + iex> Gringotts.store(Gringotts.Gateways.Trexle, card, options) ``` """ - - @spec store(CreditCard.t, list) :: map + @spec store(CreditCard.t(), keyword) :: {:ok | :error, Response} def store(payment, opts \\ []) do - params = [email: opts[:email]] - ++ card_params(payment) - ++ address_params(opts[:billing_address]) + params = + [email: opts[:email]] ++ card_params(payment) ++ address_params(opts[:billing_address]) + commit(:post, "customers", params, opts) end defp create_params_for_auth_or_purchase(amount, payment, opts, capture \\ true) do + {currency, int_value, _} = Money.to_integer(amount) + [ capture: capture, - amount: amount, - currency: opts[:config][:default_currency], + amount: int_value, + currency: currency, email: opts[:email], ip_address: opts[:ip_address], description: opts[:description] - ] ++ card_params(payment) - ++ address_params(opts[:billing_address]) + ] ++ card_params(payment) ++ address_params(opts[:billing_address]) end defp card_params(%CreditCard{} = card) do @@ -260,30 +329,39 @@ defmodule Gringotts.Gateways.Trexle do defp commit(method, path, params, opts) do auth_token = "Basic #{Base.encode64(opts[:config][:api_key])}" - headers = [{"Content-Type", "application/x-www-form-urlencoded"}, {"Authorization", auth_token}] - data = params_to_string(params) - options = [hackney: [:insecure, basic_auth: {opts[:config][:api_key], "password"}]] + + headers = [ + {"Content-Type", "application/x-www-form-urlencoded"}, + {"Authorization", auth_token} + ] + + options = [basic_auth: {opts[:config][:api_key], "password"}] url = "#{@base_url}#{path}" - response = HTTPoison.request(method, url, data, headers, options) + response = HTTPoison.request(method, url, {:form, params}, headers, options) response |> respond end - @spec respond(term) :: - {:ok, Response} | - {:error, Response} + @spec respond(term) :: {:ok | :error, Response} defp respond(response) defp respond({:ok, %{status_code: code, body: body}}) when code in [200, 201] do - case decode(body) do - {:ok, results} -> {:ok, Response.success(raw: results, status_code: code)} - end + {:ok, results} = decode(body) + token = results["response"]["token"] + message = results["response"]["status_message"] + + { + :ok, + Response.success(authorization: token, message: message, raw: results, status_code: code) + } end defp respond({:ok, %{status_code: status_code, body: body}}) do - {:error, Response.error(status_code: status_code, raw: body)} + {:ok, results} = decode(body) + detail = results["detail"] + {:error, Response.error(status_code: status_code, message: detail, raw: results)} end defp respond({:error, %HTTPoison.Error{} = error}) do - {:error, Response.error(code: error.id, reason: :network_fail?, description: "HTTPoison says '#{error.reason}'")} + {:error, Response.error(code: error.id, message: "HTTPoison says '#{error.reason}'")} end end diff --git a/test/gateways/trexle_test.exs b/test/gateways/trexle_test.exs index 4e2bebeb..f8a50562 100644 --- a/test/gateways/trexle_test.exs +++ b/test/gateways/trexle_test.exs @@ -1,9 +1,9 @@ defmodule Gringotts.Gateways.TrexleTest do - - Code.require_file "../mocks/trexle_mock.exs", __DIR__ + Code.require_file("../mocks/trexle_mock.exs", __DIR__) use ExUnit.Case, async: false alias Gringotts.Gateways.TrexleMock, as: MockResponse alias Gringotts.Gateways.Trexle + alias Gringotts.{ CreditCard, Address @@ -12,100 +12,86 @@ defmodule Gringotts.Gateways.TrexleTest do import Mock @valid_card %CreditCard{ - number: "5200828282828210", + first_name: "Harry", + last_name: "Potter", + number: "4200000000000000", + year: 2099, month: 12, - year: 2018, - first_name: "John", - last_name: "Doe", verification_code: "123", - brand: "visa" + brand: "VISA" } @invalid_card %CreditCard{ - number: "5200828282828210", - month: 12, + first_name: "Harry", + last_name: "Potter", + number: "4200000000000000", year: 2010, - first_name: "John", - last_name: "Doe", + month: 12, verification_code: "123", - brand: "visa" + brand: "VISA" } @address %Address{ - street1: "123 Main", - street2: "Suite 100", - city: "New York", - region: "NY", - country: "US", + street1: "301, Gryffindor", + street2: "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", + city: "Highlands", + region: "SL", + country: "GB", postal_code: "11111", phone: "(555)555-5555" } - @amount 100 - @bad_amount 20 + # $2.99 + @amount Money.new("2.99", :USD) + # 50 US cents, trexle does not work with amount smaller than 50 cents. + @bad_amount Money.new("0.49", :USD) - @valid_token "J5RGMpDlFlTfv9mEFvNWYoqHufyukPP4" - @invalid_token "30" + @valid_token "7214344252e11af79c0b9e7b4f3f6234" + @invalid_token "14a62fff80f24a25f775eeb33624bbb3" + @auth %{api_key: "7214344252e11af79c0b9e7b4f3f6234"} @opts [ - config: %{api_key: "J5RGMpDlFlTfv9mEFvNWYoqHufyukPP4", default_currency: "USD"}, - email: "john@trexle.com", - billing_address: @address, - ip_address: "66.249.79.118", - description: "Store Purchase 1437598192" - ] - - @missingip_opts [ - config: %{api_key: "J5RGMpDlFlTfv9mEFvNWYoqHufyukPP4", default_currency: "USD"}, - email: "john@trexle.com", - billing_address: @address, - description: "Store Purchase 1437598192" - ] - - @invalid_opts [ - config: %{api_key: "J5RGMpDlFlTfv9mEFvNWYoqHufyukPP4"}, - email: "john@trexle.com", + config: @auth, + email: "masterofdeath@ministryofmagic.gov", + ip_address: "127.0.0.1", billing_address: @address, - ip_address: "66.249.79.118", - description: "Store Purchase 1437598192" + description: "For our valued customer, Mr. Potter" ] - describe "validation arguments check" do - test "with no currency passed in config" do - assert_raise ArgumentError, fn -> - Trexle.validate_config(@invalid_opts) - end - end - end - describe "purchase" do test "with valid card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers, _options) -> MockResponse.test_for_purchase_with_valid_card end] do - {:ok, response} = Trexle.purchase(@amount, @valid_card, @opts) - assert response.status_code == 201 - assert response.raw["response"]["success"] == true - assert response.raw["response"]["captured"] == false + request: fn _method, _url, _body, _headers, _options -> + MockResponse.test_for_purchase_with_valid_card() + end do + {:ok, response} = Trexle.purchase(@amount, @valid_card, @opts) + assert response.status_code == 201 + assert response.raw["response"]["success"] == true + assert response.raw["response"]["captured"] == false end end test "with invalid card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers, _options) -> MockResponse.test_for_purchase_with_invalid_card end] do + request: fn _method, _url, _body, _headers, _options -> + MockResponse.test_for_purchase_with_invalid_card() + end do {:error, response} = Trexle.purchase(@amount, @invalid_card, @opts) assert response.status_code == 400 assert response.success == false - assert response.raw == ~s({"error":"Payment failed","detail":"Your card's expiration year is invalid."}) + assert response.message == "Your card's expiration year is invalid." end end test "with invalid amount" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers, _options) -> MockResponse.test_for_purchase_with_invalid_amount end] do + request: fn _method, _url, _body, _headers, _options -> + MockResponse.test_for_purchase_with_invalid_amount() + end do {:error, response} = Trexle.purchase(@bad_amount, @valid_card, @opts) assert response.status_code == 400 assert response.success == false - assert response.raw == ~s({"error":"Payment failed","detail":"Amount must be at least 50 cents"}) + assert response.message == "Amount must be at least 50 cents" end end end @@ -113,94 +99,64 @@ defmodule Gringotts.Gateways.TrexleTest do describe "authorize" do test "with valid card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers, _options) -> MockResponse.test_for_authorize_with_valid_card end] do + request: fn _method, _url, _body, _headers, _options -> + MockResponse.test_for_authorize_with_valid_card() + end do {:ok, response} = Trexle.authorize(@amount, @valid_card, @opts) assert response.status_code == 201 assert response.raw["response"]["success"] == true assert response.raw["response"]["captured"] == false end end - - test "with invalid card" do - with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers, _options) -> MockResponse.test_for_authorize_with_invalid_card end] do - {:error, response} = Trexle.authorize(@amount, @invalid_card, @opts) - assert response.status_code == 400 - assert response.success == false - assert response.raw == ~s({"error":"Payment failed","detail":"Your card's expiration year is invalid."}) - end - end - - test "with invalid amount" do - with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers, _options) -> MockResponse.test_for_authorize_with_invalid_amount end] do - {:error, response} = Trexle.authorize(@amount, @valid_card, @opts) - assert response.status_code == 400 - assert response.success == false - assert response.raw == ~s({"error":"Payment failed","detail":"Amount must be at least 50 cents"}) - end - end - - test "with missing ip address" do - with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers, _options) -> MockResponse.test_for_authorize_with_missing_ip_address end] do - {:error, response} = Trexle.authorize(@amount, @valid_card, @missingip_opts) - assert response.status_code == 500 - assert response.success == false - assert response.raw == ~s({"error":"ip_address is missing"}) - end - end end describe "refund" do test "with valid token" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers, _options) -> MockResponse.test_for_authorize_with_valid_card end] do + request: fn _method, _url, _body, _headers, _options -> + MockResponse.test_for_authorize_with_valid_card() + end do {:ok, response} = Trexle.refund(@amount, @valid_token, @opts) assert response.status_code == 201 assert response.raw["response"]["success"] == true assert response.raw["response"]["captured"] == false end end - - test "with invalid token" do - with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers, _options) -> MockResponse.test_for_authorize_with_invalid_amount end] do - {:error, response} = Trexle.refund(@amount, @invalid_token, @opts) - assert response.status_code == 400 - assert response.success == false - assert response.raw == ~s({"error":"Payment failed","detail":"Amount must be at least 50 cents"}) - end - end end describe "capture" do - test "with valid chargetoken" do + test "with valid charge token" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers, _options) -> MockResponse.test_for_capture_with_valid_chargetoken end] do + request: fn _method, _url, _body, _headers, _options -> + MockResponse.test_for_capture_with_valid_chargetoken() + end do {:ok, response} = Trexle.capture(@valid_token, @amount, @opts) assert response.status_code == 200 assert response.raw["response"]["success"] == true assert response.raw["response"]["captured"] == true - assert response.raw["response"]["status_message"] == "Transaction approved" + assert response.message == "Transaction approved" end end - test "test_for_capture_with_invalid_chargetoken" do - with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers, _options) -> MockResponse.test_for_capture_with_invalid_chargetoken end] do - {:error, response} = Trexle.capture(@invalid_token, @amount, @opts) - assert response.status_code == 400 - assert response.success == false - assert response.raw == ~s({"error":"Capture failed","detail":"invalid token"}) - end + test "with invalid charge token" do + with_mock HTTPoison, + request: fn _method, _url, _body, _headers, _options -> + MockResponse.test_for_capture_with_invalid_chargetoken() + end do + {:error, response} = Trexle.capture(@invalid_token, @amount, @opts) + assert response.status_code == 400 + assert response.success == false + assert response.message == "invalid token" + end end end describe "store" do test "with valid card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers, _options) -> MockResponse.test_for_store_with_valid_card end] do + request: fn _method, _url, _body, _headers, _options -> + MockResponse.test_for_store_with_valid_card() + end do {:ok, response} = Trexle.store(@valid_card, @opts) assert response.status_code == 201 end @@ -210,10 +166,12 @@ defmodule Gringotts.Gateways.TrexleTest do describe "network failure" do test "with authorization" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers, _options) -> MockResponse.test_for_network_failure end] do + request: fn _method, _url, _body, _headers, _options -> + MockResponse.test_for_network_failure() + end do {:error, response} = Trexle.authorize(@amount, @valid_card, @opts) assert response.success == false - assert response.reason == :network_fail? + assert response.message == "HTTPoison says 'some_hackney_error'" end end end diff --git a/test/mocks/trexle_mock.exs b/test/mocks/trexle_mock.exs index 1a75f19e..27c4d1c2 100644 --- a/test/mocks/trexle_mock.exs +++ b/test/mocks/trexle_mock.exs @@ -1,243 +1,200 @@ defmodule Gringotts.Gateways.TrexleMock do - def test_for_purchase_with_valid_card do - {:ok, - %HTTPoison.Response{ - body: "{\"response\":{\"token\":\"charge_3e89c6f073606ac1efe62e76e22dd7885441dc72\",\"success\":true,\"captured\":false}}", - headers: [ - {"Date", "Fri, 22 Dec 2017 11:57:28 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"ETag", "W/\"5a9f44c457a4fdd0478c82ec1af64816\""}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "9b2a1d30-9bca-48f2-862e-4090766689cb"}, - {"X-Runtime", "0.777520"}, - {"Content-Length", "104"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 201 - } - } + {:ok, %HTTPoison.Response{ + body: ~s/{"response":{"token":"charge_3e89c6f073606ac1efe62e76e22dd7885441dc72","success":true,"captured":false}}/, + headers: [ + {"Date", "Fri, 22 Dec 2017 11:57:28 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "9b2a1d30-9bca-48f2-862e-4090766689cb"}, + {"X-Runtime", "0.777520"}, + {"Content-Length", "104"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 201 + }} end def test_for_purchase_with_invalid_card do - {:ok, - %HTTPoison.Response{ - body: "{\"error\":\"Payment failed\",\"detail\":\"Your card's expiration year is invalid.\"}", - headers: [ - {"Date", "Fri, 22 Dec 2017 13:20:50 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "eb8100a1-8ffa-47da-9623-8d3b2af51b84"}, - {"X-Runtime", "0.445244"}, - {"Content-Length", "77"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 400 - } - } + {:ok, %HTTPoison.Response{ + body: ~s/{"error":"Payment failed","detail":"Your card's expiration year is invalid."}/, + headers: [ + {"Date", "Fri, 22 Dec 2017 13:20:50 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "eb8100a1-8ffa-47da-9623-8d3b2af51b84"}, + {"X-Runtime", "0.445244"}, + {"Content-Length", "77"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 400 + }} end def test_for_purchase_with_invalid_amount do - {:ok, - %HTTPoison.Response{ - body: "{\"error\":\"Payment failed\",\"detail\":\"Amount must be at least 50 cents\"}", - headers: [ - {"Date", "Sat, 23 Dec 2017 18:16:33 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "4ce2eea4-3ea9-4345-ac85-9bc45f22f5ac"}, - {"X-Runtime", "0.476058"}, - {"Content-Length", "70"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 400 - } - } + {:ok, %HTTPoison.Response{ + body: ~s/{"error":"Payment failed","detail":"Amount must be at least 50 cents"}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:16:33 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "4ce2eea4-3ea9-4345-ac85-9bc45f22f5ac"}, + {"X-Runtime", "0.476058"}, + {"Content-Length", "70"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 400 + }} end def test_for_authorize_with_valid_card do - {:ok, - %HTTPoison.Response{ - body: "{\"response\":{\"token\":\"charge_8ab2b21a2f02495f5c36b34d129c8a0e836add32\",\"success\":true,\"captured\":false}}", - headers: [ - {"Date", "Sat, 23 Dec 2017 18:33:31 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"ETag", "W/\"ec4f2df0607614f67286ac46eb994150\""}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "51d28d13-81e5-41fd-b711-1b6531fdd3dd"}, - {"X-Runtime", "0.738395"}, - {"Content-Length", "104"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 201 - } - } + {:ok, %HTTPoison.Response{ + body: ~s/{"response":{"token":"charge_8ab2b21a2f02495f5c36b34d129c8a0e836add32","success":true,"captured":false}}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:33:31 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "51d28d13-81e5-41fd-b711-1b6531fdd3dd"}, + {"X-Runtime", "0.738395"}, + {"Content-Length", "104"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 201 + }} end def test_for_authorize_with_invalid_card do - {:ok, - %HTTPoison.Response{ - body: "{\"error\":\"Payment failed\",\"detail\":\"Your card's expiration year is invalid.\"}", - headers: [ - {"Date", "Sat, 23 Dec 2017 18:25:40 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "239e7054-9500-4a87-bf3b-09456d550b6d"}, - {"X-Runtime", "0.466670"}, - {"Content-Length", "77"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 400 - } - } + {:ok, %HTTPoison.Response{ + body: ~s/{"error":"Payment failed","detail":"Your card's expiration year is invalid."}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:25:40 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "239e7054-9500-4a87-bf3b-09456d550b6d"}, + {"X-Runtime", "0.466670"}, + {"Content-Length", "77"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 400 + }} end def test_for_authorize_with_invalid_amount do - {:ok, - %HTTPoison.Response{ - body: "{\"error\":\"Payment failed\",\"detail\":\"Amount must be at least 50 cents\"}", - headers: [ - {"Date", "Sat, 23 Dec 2017 18:40:10 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "d58db806-8016-4a0e-8519-403a969fa1a7"}, - {"X-Runtime", "0.494636"}, - {"Content-Length", "70"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 400 - } - } - end - - def test_for_authorize_with_missing_ip_address do - {:ok, - %HTTPoison.Response{body: "{\"error\":\"ip_address is missing\"}", - headers: [ - {"Date", "Thu, 28 Dec 2017 12:22:43 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "97bae548-a446-42e9-b792-8c505c38f4c1"}, - {"X-Runtime", "0.005652"}, {"Content-Length", "33"}, - {"X-Powered-By", "PleskLin"}, {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1/charges", - status_code: 500 - } - } + {:ok, %HTTPoison.Response{ + body: ~s/{"error":"Payment failed","detail":"Amount must be at least 50 cents"}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:40:10 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "d58db806-8016-4a0e-8519-403a969fa1a7"}, + {"X-Runtime", "0.494636"}, + {"Content-Length", "70"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 400 + }} end def test_for_refund_with_valid_token do - {:ok, - %HTTPoison.Response{ - body: "{\"response\":{\"token\":\"refund_a86a757cc6bdabab50d6ebbfcdcd4db4e04198dd\",\"success\":true,\"amount\":50,\"charge\":\"charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b\",\"status_message\":\"Transaction approved\"}}", - headers: [ - {"Date", "Sat, 23 Dec 2017 18:55:41 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"ETag", "W/\"7410ae0b45094aadada390f5c947a58a\""}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "b1c94703-7fb4-48f2-b1b4-32e3b6a87e57"}, - {"X-Runtime", "1.097186"}, - {"Content-Length", "198"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: "https://core.trexle.com/api/v1//charges/charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b/refunds", - status_code: 201 - } - } + {:ok, %HTTPoison.Response{ + body: ~s/{"response":{"token":"refund_a86a757cc6bdabab50d6ebbfcdcd4db4e04198dd","success":true,"amount":50,"charge":"charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b","status_message":"Transaction approved"}}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:55:41 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "b1c94703-7fb4-48f2-b1b4-32e3b6a87e57"}, + {"X-Runtime", "1.097186"}, + {"Content-Length", "198"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: + "https://core.trexle.com/api/v1//charges/charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b/refunds", + status_code: 201 + }} end def test_for_refund_with_invalid_token do - {:ok, - %HTTPoison.Response{ - body: "{\"error\":\"Refund failed\",\"detail\":\"invalid token\"}", - headers: [ - {"Date", "Sat, 23 Dec 2017 18:53:09 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "276fd8f5-dc21-40be-8add-fa76aabbfc5b"}, - {"X-Runtime", "0.009374"}, - {"Content-Length", "50"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges/34/refunds", - status_code: 400 - } - } + {:ok, %HTTPoison.Response{ + body: ~s/{"error":"Refund failed","detail":"invalid token"}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:53:09 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "276fd8f5-dc21-40be-8add-fa76aabbfc5b"}, + {"X-Runtime", "0.009374"}, + {"Content-Length", "50"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges/34/refunds", + status_code: 400 + }} end def test_for_capture_with_valid_chargetoken do - {:ok, - %HTTPoison.Response{ - body: "{\"response\":{\"token\":\"charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b\",\"success\":true,\"captured\":true,\"amount\":50,\"status_message\":\"Transaction approved\"}}", - headers: [ - {"Date", "Sat, 23 Dec 2017 18:49:50 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"ETag", "W/\"26f05a32c0d0a27b180bbe777488fd5f\""}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "97ca2db6-fd4f-4a5b-ae45-01fae9c13668"}, - {"X-Runtime", "1.092051"}, - {"Content-Length", "155"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: "https://core.trexle.com/api/v1//charges/charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b/capture", - status_code: 200 - } - } + {:ok, %HTTPoison.Response{ + body: ~s/{"response":{"token":"charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b","success":true,"captured":true,"amount":50,"status_message":"Transaction approved"}}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:49:50 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "97ca2db6-fd4f-4a5b-ae45-01fae9c13668"}, + {"X-Runtime", "1.092051"}, + {"Content-Length", "155"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: + "https://core.trexle.com/api/v1//charges/charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b/capture", + status_code: 200 + }} end def test_for_capture_with_invalid_chargetoken do - {:ok, - %HTTPoison.Response{ - body: "{\"error\":\"Capture failed\",\"detail\":\"invalid token\"}", - headers: [ - {"Date", "Sat, 23 Dec 2017 18:47:18 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "b46ecb8d-7df8-4c5f-b87f-c53fae364e79"}, - {"X-Runtime", "0.010255"}, - {"Content-Length", "51"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges/30/capture", - status_code: 400 - } - } + {:ok, %HTTPoison.Response{ + body: ~s/{"error":"Capture failed","detail":"invalid token"}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:47:18 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "b46ecb8d-7df8-4c5f-b87f-c53fae364e79"}, + {"X-Runtime", "0.010255"}, + {"Content-Length", "51"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges/30/capture", + status_code: 400 + }} end def test_for_store_with_valid_card do - {:ok, - %HTTPoison.Response{ - body: "{\"response\":{\"token\":\"token_94e333959850270460e89a86bad2246613528cbb\",\"card\":{\"token\":\"token_2a1ba29522e4a377fafa62e8e204f76ad8ba8f1e\",\"scheme\":\"master\",\"display_number\":\"XXXX-XXXX-XXXX-8210\",\"expiry_year\":2018,\"expiry_month\":1,\"cvc\":123,\"name\":\"John Doe\",\"address_line1\":\"456 My Street\",\"address_line2\":null,\"address_city\":\"Ottawa\",\"address_state\":\"ON\",\"address_postcode\":\"K1C2N6\",\"address_country\":\"CA\",\"primary\":true}}}", - headers: [ - {"Date", "Sat, 23 Dec 2017 19:32:58 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"ETag", "W/\"c4089eabe907fc2327dd565503242b58\""}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "1a334b22-8e01-4e1b-8b58-90dfd0b7c12f"}, - {"X-Runtime", "0.122441"}, - {"Content-Length", "422"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: "https://core.trexle.com/api/v1//customers", - status_code: 201 - } - } + {:ok, %HTTPoison.Response{ + body: ~s/{"response":{"token":"token_94e333959850270460e89a86bad2246613528cbb","card":{"token":"token_2a1ba29522e4a377fafa62e8e204f76ad8ba8f1e","scheme":"master","display_number":"XXXX-XXXX-XXXX-8210","expiry_year":2018,"expiry_month":1,"cvc":123,"name":"John Doe","address_line1":"456 My Street","address_line2":null,"address_city":"Ottawa","address_state":"ON","address_postcode":"K1C2N6","address_country":"CA","primary":true}}}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 19:32:58 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "1a334b22-8e01-4e1b-8b58-90dfd0b7c12f"}, + {"X-Runtime", "0.122441"}, + {"Content-Length", "422"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: "https://core.trexle.com/api/v1//customers", + status_code: 201 + }} end def test_for_network_failure do - {:error, %HTTPoison.Error{id: nil, reason: :nxdomain}} + {:error, %HTTPoison.Error{id: :some_hackney_error_id, reason: :some_hackney_error}} end end From f3c0d582ca5dd477aeceade58ce0c204287972f5 Mon Sep 17 00:00:00 2001 From: gopalshimpi Date: Thu, 25 Jan 2018 18:41:19 +0530 Subject: [PATCH 20/60] [CAMS] Adds money protocol (#89) * Added money protocol for CAMS gateway. * Modified methods according to money protocol. * Modified test data as per money protocol. * Corrected protocol usage, docs and some bugs * Updated docs --- lib/gringotts/gateways/cams.ex | 523 ++++++++++++++++++--------------- test/gateways/cams_test.exs | 195 ++++++------ test/mocks/cams_mock.exs | 271 +++++++++-------- 3 files changed, 528 insertions(+), 461 deletions(-) diff --git a/lib/gringotts/gateways/cams.ex b/lib/gringotts/gateways/cams.ex index b86c2b6b..7e37dd05 100644 --- a/lib/gringotts/gateways/cams.ex +++ b/lib/gringotts/gateways/cams.ex @@ -1,320 +1,383 @@ defmodule Gringotts.Gateways.Cams do - @moduledoc ~S""" - A module for working with the Cams payment gateway. - - You can test gateway operations in [CAMS API TEST MODE](https://secure.centralams.com). - Test it using these crediantials **username:** `testintegrationc`, **password:** `password9`, - as well as you can find api docs in this test account under **integration** link. - - The following features of CAMS are implemented: - - | Action | Method | - | ------ | ------ | - | Authorize | `authorize/3` | - | Capture | `capture/3` | - | Purchase | `purchase/3` | - | Refund | `refund/3` | - | Cancel | `void/2` | + @moduledoc """ + [CAMS][home] gateway implementation. + + CAMS provides a [sandbox account][dashboard] with documentation under the + [`integration` tab][docs]. The login credentials are: + + | Key | Credentials | + | ------ | -------- | + | username | `testintegrationc` | + | password | `password9` | + + The [video tutorials][videos] (on vimeo) are excellent. + + The following features of CAMS are implemented: + + | Action | Method | + | ------ | ------ | + | Authorize | `authorize/3` | + | Capture | `capture/3` | + | Purchase | `purchase/3` | + | Refund | `refund/3` | + | Cancel | `void/2` | ## The `opts` argument - Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply - optional arguments for transactions with the Cams gateway. The following keys - are supported: + Most `Gringotts` API calls accept an optional `keyword` list `opts` to supply + optional arguments for transactions with the CAMS gateway. The following keys + are supported: + + | Key | Type | Remark | + | ---- | ---- | --- | + | `billing_address` | `map` | The address of the customer | + | `order_id` | `String.t` | Merchant provided identifier | + | `description` | `String.t` | Merchant provided description of the transaction | + + > CAMS supports more optional keys and you can raise an [issue][issues] if + this is important to you. + + [issues]: https://github.com/aviabird/gringotts/issues/new - | Key | Remark | Status | - | ---- | --- | ---- | - | `billing_address` | | Not implemented | - | `address` | | Not implemented | - | `currency` | | **Implemented** | - | `order_id` | | Not implemented | - | `description` | | Not implemented | - - All these keys are being implemented, track progress in - [issue #42](https://github.com/aviabird/gringotts/issues/42)! - - ## Configuration parameters for Cams: - - | Config parameter | Cams secret | - | ------- | ---- | - | `:username` | **Username** | - | `:password` | **Password** | + ### Schema + + * `billing_address` is a `map` from `atoms` to `String.t`, and can include any + of the keys from: + `:name, :address1, :address2, :company, :city, :state, :zip, :country, :phone, :fax]` + ## Registering your CAMS account at `Gringotts` + + | Config parameter | CAMS secret | + | ------- | ---- | + | `:username` | **Username** | + | `:password` | **Password** | + > Your Application config **must include the `:username`, `:password` - > fields** and would look something like this: - + > fields** and would look something like this: + config :gringotts, Gringotts.Gateways.Cams, - adapter: Gringotts.Gateways.Cams, - username: "your_secret_user_name", - password: "your_secret_password", - + adapter: Gringotts.Gateways.Cams, + username: "your_secret_user_name", + password: "your_secret_password", + + ## Scope of this module - ## Scope of this module, and _quirks_ + * CAMS **does not** process money in cents. + * Although CAMS supports payments from electronic check & various cards this module only + accepts payments via `VISA`, `MASTER`, `AMERICAN EXPRESS` and `DISCOVER`. - * Cams process money in cents. - * Although Cams supports payments from electronic check & various cards this library only - accepts payments by cards like *visa*, *master*, *american_express* and *discover*. + ## Supported countries + **citation-needed** + + ## Supported currencies + **citation-needed** ## Following the examples - 1. First, set up a sample application and configure it to work with Cams. - - You could do that from scratch by following our [Getting Started](#) guide. - - To save you time, we recommend [cloning our example - repo](https://github.com/aviabird/gringotts_example) that gives you a - pre-configured sample app ready-to-go. - + You could use the same config or update it the with your "secrets" - that you get after registering with Cams. + + 1. First, set up a sample application and configure it to work with CAMS. + - You could do that from scratch by following our [Getting Started][gs] guide. + - To save you time, we recommend [cloning our example][example-repo] that + gives you a pre-configured sample app ready-to-go. + + You could use the same config or update it the with your "secrets" that + you get after [registering with + CAMS](#module-registering-your-cams-account-at-gringotts). 2. Run an `iex` session with `iex -S mix` and add some variable bindings and aliases to it (to save some time): ``` iex> alias Gringotts.{Response, CreditCard, Gateways.Cams} - iex> opts = [currency: "USD"] # The default currency is USD, and this is just for an example. - iex> payment = %CreditCard{number: "4111111111111111", month: 11, year: 2018, - first_name: "Longbob", last_name: "Longsen", - verification_code: "123", brand: "visa"} + iex> card = %CreditCard{first_name: "Harry", + last_name: "Potter", + number: "4111111111111111", + year: 2099, + month: 12, + verification_code: "999", + brand: "VISA"} + iex> money = %{value: Decimal.new(20), currency: "USD"} ``` - We'll be using these in the examples below. + ## Integrating with phoenix + + Refer the [GringottsPay][gpay-heroku-cams] website for an example of how to + integrate CAMS with phoenix. The source is available [here][gpay-repo]. + + [gpay-repo]: https://github.com/aviabird/gringotts_payment + [gpay-heroku-cams]: http://gringottspay.herokuapp.com/cams + ## TODO - * Credit Card Operations + * Operations using Credit Card - Credit - * Electronic Check + * Operations using electronic checks - Sale - Void - Refund + + [home]: http://www.centralams.com/ + [docs]: https://secure.centralams.com/merchants/resources/integration/integration_portal.php?tid=d669ab54bb17e34c5ff2cfe504f033e7 + [dashboard]: https://secure.centralams.com + [videos]: https://secure.centralams.com/merchants/video.php?tid=d669ab54bb17e34c5ff2cfe504f033e7 + [gs]: # + [example-repo]: https://github.com/aviabird/gringotts_example """ - @live_url "https://secure.centralams.com/gw/api/transact.php" - @default_currency "USD" - @headers [{"Content-Type", "application/x-www-form-urlencoded"}] + use Gringotts.Gateways.Base - use Gringotts.Adapter, - required_config: [:username, :password, :default_currency] - alias Gringotts.{CreditCard, Response} + use Gringotts.Adapter, required_config: [:username, :password] + + alias Gringotts.{CreditCard, Response, Money} alias Gringotts.Gateways.Cams.ResponseHandler, as: ResponseParser - import Poison, only: [decode!: 1] + @live_url "https://secure.centralams.com/gw/api/transact.php" + @headers [{"Content-Type", "application/x-www-form-urlencoded"}] + @doc """ - Transfers `amount` from the customer to the merchant. + Performs a (pre) Authorize operation. + + The authorization validates the `card` details with the banking network, + places a hold on the transaction `amount` in the customer’s issuing bank and + also triggers risk management. Funds are not transferred. - Function to charge a user credit card for the specified amount. It performs authorize - and capture at the same time.Purchase transaction are submitted and immediately sent for settlement. - - After successful purchase it returns an `authorization` which can be used later to: - * `refund/3` an amount. - * `void/2` a transaction(*if Not settled*). + When followed up with a `capture/3` transaction, funds will be transferred to + the merchant's account upon settlement. + + CAMS returns a **Transaction ID** (available in the `Response.authorization` + field) which can be used later to: + * `capture/3` an amount. + * `void/2` an authorized transaction. + + ## Optional Fields + options[ + order_id: String, + description: String + ] ## Examples - payment = %CreditCard{ - number: "4111111111111111", month: 11, year: 2018, - first_name: "Longbob", last_name: "Longsen", - verification_code: "123", brand: "visa" - } - - options = [currency: "USD"] - money = 100 - - iex> Gringotts.purchase(Gringotts.Gateways.Cams, money, payment, options) + + The following example shows how one would (pre) authorize a payment of $20 on + a sample `card`. + ``` + iex> card = %CreditCard{first_name: "Harry", + last_name: "Potter", + number: "4111111111111111", + year: 2099, + month: 12, + verification_code: "999", + brand: "VISA"} + iex> money = %{value: Decimal.new(20), currency: "USD"} + iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Cams, money, card) + ``` """ - @spec purchase(number, CreditCard.t, Keyword) :: Response - def purchase(money, payment, options) do - post = [] - |> add_invoice(money, options) - |> add_payment(payment) - |> add_address(payment, options) - commit("sale", post, options) + @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + def authorize(money, %CreditCard{} = card, options) do + params = + [] + |> add_invoice(money) + |> add_payment(card) + |> add_address(card, options) + + commit("auth", params, options) end @doc """ - Authorize a credit card transaction. + Captures a pre-authorized amount. + + Captures can be submitted for an `amount` equal to or less than the originally + authorized `amount` in an `authorize/3`ation referenced by `transaction_id`. - The authorization validates the `card` details with the banking network, places a hold on the - transaction amount in the customer’s issuing bank and also triggers risk management. - Funds are not transferred.It needs to be followed up with a capture transaction to transfer the funds - to merchant account.After successful capture, transaction will be sent for settlement. - - Cams returns an `authorization` which can be used later to: - * `capture/3` an amount. - * `void/2` a authorized transaction. + Partial captures are allowed, and the remaining amount is released back to + the payment source [(video)][auth-and-capture]. + > Multiple, partial captures on the same `authorization` token are **not supported**. + + CAMS returns a **Transaction ID** (available in the `Response.authorization` + field) which can be used later to: + * `refund/3` + * `void/2` *(only before settlements!)* + + [auth-and-capture]: https://vimeo.com/200903640 ## Examples - payment = %{ - number: "4111111111111111", month: 11, year: 2018, - first_name: "Longbob", last_name: "Longsen", - verification_code: "123", brand: "visa" - } - - options = [currency: "USD"] - money = 100 - - iex> Gringotts.authorize(Gringotts.Gateways.Cams, money, payment, options) + + The following example shows how one would (partially) capture a previously + authorized a payment worth $10 by referencing the obtained authorization `id`. + ``` + iex> card = %CreditCard{first_name: "Harry", + last_name: "Potter", + number: "4111111111111111", + year: 2099, + month: 12, + verification_code: "999", + brand: "VISA"} + iex> money = %{value: Decimal.new(10), currency: "USD"} + iex> authorization = auth_result.authorization + # authorization = "some_authorization_transaction_id" + iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Cams, money, authorization) + ``` """ - @spec authorize(number, CreditCard.t, Keyword) :: Response - def authorize(money, payment, options) do - post = [] - |> add_invoice(money, options) - |> add_payment(payment) - |> add_address(payment, options) - commit("auth", post, options) + @spec capture(Money.t(), String.t(), keyword) :: {:ok | :error, Response} + def capture(money, transaction_id, options) do + params = + [transactionid: transaction_id] + |> add_invoice(money) + + commit("capture", params, options) end @doc """ - Captures a pre-authorized amount. + Transfers `amount` from the customer to the merchant. + + CAMS attempts to process a purchase on behalf of the customer, by debiting + `amount` from the customer's account by charging the customer's `card`. - It captures existing authorizations for settlement.Only authorizations can be captured. - Captures can be submitted for an amount equal to or less than the original authorization. - It allows partial captures like many other gateways and release the remaining amount back to - the payment source **[citation-needed]**.Multiple captures can not be done using same `authorization`. + Returns a **Transaction ID** (available in the `Response.authorization` + field) which can be used later to: + * `refund/3` + * `void/2` *(only before settlements!)* ## Examples - authorization = "3904093075" - options = [currency: "USD"] - money = 100 - - iex> Gringotts.capture(Gringotts.Gateways.Cams, money, authorization, options) + The following example shows how one would process a payment worth $20 in + one-shot, without (pre) authorization. + ``` + iex> card = %CreditCard{first_name: "Harry", + last_name: "Potter", + number: "4111111111111111", + year: 2099, + month: 12, + verification_code: "999", + brand: "VISA"} + iex> money = %{value: Decimal.new(20), currency: "USD"} + iex> Gringotts.purchase(Gringotts.Gateways.Cams, money, card) + ``` """ - @spec capture(number, String.t, Keyword) :: Response - def capture(money, authorization, options) do - post = [transactionid: authorization] - add_invoice(post, money, options) - commit("capture", post, options) + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + def purchase(money, %CreditCard{} = card, options) do + params = + [] + |> add_invoice(money) + |> add_payment(card) + |> add_address(card, options) + + commit("sale", params, options) end @doc """ - Refunds the `amount` to the customer's account with reference to a prior transfer. + Refunds the `amount` to the customer's account with reference to a prior transfer. - It will reverse a previously settled or pending settlement transaction. - If the transaction has not been settled, a transaction `void/2` can also reverse it. - It processes a full or partial refund worth `amount`, referencing a previous `purchase/3` or `capture/3`. - Authorized transaction can not be reversed. + It's better to `void/2` a transaction if it has not been settled yet! Refunds + lead to to two entries on the customer's bank statement, one for the original + `purchase/3` or `capture/3` and another for the `refund/3`. - `authorization` can be used to perform multiple refund, till: - * all the pre-authorized amount is captured or, - * the remaining amount is explicitly "reversed" via `void/2`. **[citation-needed]** + Multiple, partial refunds on the same **Transaction ID** are allowed till all + the captured amount is refunded. ## Examples - authorization = "3904093078" - options = [currency: "USD"] - money = 100 - - iex> Gringotts.refund(Gringotts.Gateways.Cams, money, authorization, options) + The following example shows how one would completely refund a previous capture + (and similarily for purchases). + ``` + iex> capture_id = capture_result.authorization + # capture_id = "some_capture_transaction_id" + iex> money = %{value: Decimal.new(20), currency: "USD"} + iex> Gringotts.refund(Gringotts.Gateways.Cams, money, capture_id) + ``` """ - @spec refund(number, String.t, Keyword) :: Response - def refund(money, authorization, options) do - post = [transactionid: authorization] - add_invoice(post, money, options) - commit("refund", post, options) + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} + def refund(money, transaction_id, options) do + params = + [transactionid: transaction_id] + |> add_invoice(money) + + commit("refund", params, options) end @doc """ - Voids the referenced payment. - - Transaction voids will cancel an existing sale or captured authorization. - In addition, non-captured authorizations can be voided to prevent any future capture. - Voids can only occur if the transaction has not been settled. + Voids the referenced payment. + + Cancel a transaction referenced by `transaction_id` that is not settled + yet. This will erase any entries from the customer's bank statement. + + > `authorize/3` can be `void/2`ed to prevent captures. ## Examples - authorization = "3904093075" - options = [] - - iex> Gringotts.void(Gringotts.Gateways.Cams, authorization, options) + The following example shows how one would void a previous (pre) + authorization. + ``` + iex> auth_id = auth_result.id + # auth_id = "aome_authorisation_transaction_id" + iex> Gringotts.void(Gringotts.Gateways.Cams, auth_id) + ``` """ - @spec void(String.t, Keyword) :: Response - def void(authorization , options) do - post = [transactionid: authorization] - commit("void", post, options) + @spec void(String.t(), keyword) :: {:ok | :error, Response} + def void(transaction_id, options) do + params = [transactionid: transaction_id] + commit("void", params, options) end @doc """ - Validates the Account + Validates the `card` - This action is used for doing an "Account Verification" on the cardholder's credit card - without actually doing an authorization. + Verifies the credit `card` without authorizing any amount. ## Examples - payment = %{ - number: "4111111111111111", month: 11, year: 2018, - first_name: "Longbob", last_name: "Longsen", - verification_code: "123", brand: "visa" - } - - options = [currency: "USD"] - - - iex> Gringotts.validate(Gringotts.Gateways.Cams, payment, options) - + ``` + iex> card = %CreditCard{first_name: "Harry", + last_name: "Potter", + number: "4111111111111111", + year: 2099, + month: 12, + verification_code: "999", + brand: "VISA"} + iex> Gringotts.validate(Gringotts.Gateways.Cams, card) + ``` """ - @spec validate(CreditCard.t, Keyword):: Response - def validate(payment, options) do - post = [] - |> add_invoice(0, options) - |> add_payment(payment) - |> add_address(payment, options) - - commit("verify", post, options) + @spec validate(CreditCard.t(), keyword) :: {:ok | :error, Response} + def validate(card, options) do + params = + [] + |> add_invoice(%{value: Decimal.new(0), currency: "USD"}) + |> add_payment(card) + |> add_address(card, options) + + commit("verify", params, options) end # private methods - defp add_invoice(post, money, options) do - post - |> Keyword.put(:amount, money) - |> Keyword.put(:currency, (options[:config][:currency]) || @default_currency) + defp add_invoice(params, money) do + {currency, value} = Money.to_string(money) + [amount: value, currency: currency] ++ params end - defp add_payment(post, payment) do - exp_month = join_month(payment) - exp_year = payment.year - |> to_string() - |> String.slice(-2..-1) - - post - |> Keyword.put(:ccnumber, payment.number) - |> Keyword.put(:ccexp, "#{exp_month}#{exp_year}") - |> Keyword.put(:cvv, payment.verification_code) - end + defp add_payment(params, %CreditCard{} = card) do + exp_month = card.month |> to_string |> String.pad_leading(2, "0") + exp_year = card.year |> to_string |> String.slice(-2..-1) - defp add_address(post, payment, options) do - post = post - |> Keyword.put(:firstname, payment.first_name) - |> Keyword.put(:lastname, payment.last_name) - - if options[:billing_address] do - address = options[:billing_address] - post = post - |> Keyword.put(:address1 , address[:address1]) - |> Keyword.put(:address2, address[:address2]) - |> Keyword.put(:city, address[:city]) - |> Keyword.put(:state, address[:state]) - |> Keyword.put(:zip, address[:zip]) - |> Keyword.put(:country, address[:country]) - |> Keyword.put(:phone, address[:phone]) - end + [ccnumber: card.number, ccexp: "#{exp_month}#{exp_year}", cvv: card.verification_code] ++ + params end - defp join_month(payment) do - payment.month - |> to_string - |> String.pad_leading(2, "0") + defp add_address(params, card, options) do + params ++ + [firstname: card.first_name, lastname: card.last_name] ++ + if options[:billing_address] != nil, do: Enum.into(options[:billing_address], []), else: [] end defp commit(action, params, options) do url = @live_url - params = params - |> Keyword.put(:type, action) - |> Keyword.put(:password, options[:config][:password]) - |> Keyword.put(:username, options[:config][:username]) - |> params_to_string - + + auth = [ + type: action, + password: options[:config][:password], + username: options[:config][:username] + ] + url - |> HTTPoison.post(params, @headers) - |> ResponseParser.parse + |> HTTPoison.post({:form, auth ++ params}, @headers) + |> ResponseParser.parse() end defmodule ResponseHandler do @@ -341,7 +404,7 @@ defmodule Gringotts.Gateways.Cams do def parse({:ok, %HTTPoison.Response{body: body, status_code: 404}}) do body = URI.decode_query(body) - + [status_code: 404] |> handle_not_found(body) |> handle_opts() @@ -382,7 +445,7 @@ defmodule Gringotts.Gateways.Cams do defp parse_html(body) do error_message = List.to_string(Map.keys(body)) - [html_body | parse_message] = (Regex.run(~r|(.*)|, error_message)) + [_ | parse_message] = Regex.run(~r|(.*)|, error_message) List.to_string(parse_message) end diff --git a/test/gateways/cams_test.exs b/test/gateways/cams_test.exs index 694695e8..3c20e545 100644 --- a/test/gateways/cams_test.exs +++ b/test/gateways/cams_test.exs @@ -1,201 +1,212 @@ defmodule Gringotts.Gateways.CamsTest do - - Code.require_file "../mocks/cams_mock.exs", __DIR__ + Code.require_file("../mocks/cams_mock.exs", __DIR__) use ExUnit.Case, async: false + alias Gringotts.{ - CreditCard, Response + CreditCard, + Response } + alias Gringotts.Gateways.CamsMock, as: MockResponse alias Gringotts.Gateways.Cams, as: Gateway import Mock - @payment %CreditCard{ + @card %CreditCard{ number: "4111111111111111", - month: 9, - year: 2018, - first_name: "Gopal", - last_name: "Shimpi", - verification_code: "123", - brand: "visa" + month: 11, + year: 2099, + first_name: "Harry", + last_name: "Potter", + verification_code: "999", + brand: "VISA" } - @bad_payment %CreditCard { - number: "411111111111111", - month: 9, - year: 2018, - first_name: "Gopal", - last_name: "Shimpi", - verification_code: "123", - brand: "visa" + @bad_card %CreditCard{ + number: "42", + month: 11, + year: 2099, + first_name: "Harry", + last_name: "Potter", + verification_code: "999", + brand: "VISA" } @address %{ - name: "Jim Smith", - address1: "456 My Street", - address2: "Apt 1", - company: "Widgets Inc", - city: "Ottawa", - state: "ON", - zip: "K1C2N6", - country: "US", - phone: "(555)555-5555", - fax: "(555)555-6666" + street1: "301, Gryffindor", + street2: "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", + city: "Highlands", + state: "Scotland", + country: "GB", + company: "Ollivanders", + zip: "K1C2N6", + phone: "(555)555-5555", + fax: "(555)555-6666" } + @auth %{username: "some_secret_user_name", password: "some_secret_password"} @options [ - config: %{ - username: "testintegrationc", - password: "password9" - }, order_id: 0001, billing_address: @address, description: "Store Purchase" ] - @money 100 - @bad_money "G" - @authorization "3921111362" - @bad_authorization "300000000" + @money Money.new(:USD, 100) + @money_more Money.new(:USD, 101) + @money_less Money.new(:USD, 99) + @bad_currency Money.new(:INR, 100) + + @authorization "some_transaction_id" + @bad_authorization "some_fake_transaction_id" + + setup_all do + Application.put_env( + :gringotts, + Gateway, + adapter: Gateway, + username: "some_secret_user_name", + password: "some_secret_password" + ) + end describe "purchase" do - test "with all good" do + test "with correct params" do with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.successful_purchase end] do - {:ok, %Response{success: result}} = Gateway.purchase(@money, @payment, @options) + post: fn _url, _body, _headers -> MockResponse.successful_purchase() end do + {:ok, %Response{success: result}} = Gringotts.purchase(Gateway, @money, @card, @options) assert result end end test "with bad card" do with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.failed_purchase_with_bad_credit_card end] do - {:ok, %Response{message: result}} = Gateway.purchase(@money, @bad_payment, @options) - assert String.contains?(result, "Invalid Credit Card Number") - end - end + post: fn _url, _body, _headers -> MockResponse.failed_purchase_with_bad_credit_card() end do + {:ok, %Response{message: result}} = + Gringotts.purchase(Gateway, @money, @bad_card, @options) - test "with bad amount" do - with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.failed_purchase_with_bad_money end] do - {:ok, %Response{message: result}} = Gateway.purchase(@bad_money, @payment, @options) - assert String.contains?(result, "Invalid amount") + assert String.contains?(result, "Invalid Credit Card Number") end end test "with invalid currency" do with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.with_invalid_currency end] do - {:ok, %Response{message: result}} = Gateway.purchase(@money, @payment, @options) + post: fn _url, _body, _headers -> MockResponse.with_invalid_currency() end do + {:ok, %Response{message: result}} = Gringotts.purchase(Gateway, @bad_currency, @card, @options) assert String.contains?(result, "The cc payment type") end end end describe "authorize" do - test "with all good" do + test "with correct params" do with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.successful_authorize end] do - {:ok, %Response{success: result}} = Gateway.authorize(@money, @payment, @options) + post: fn _url, _body, _headers -> MockResponse.successful_authorize() end do + {:ok, %Response{success: result}} = Gringotts.authorize(Gateway, @money, @card, @options) assert result end end test "with bad card" do with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.failed_authorized_with_bad_card end] do - {:ok, %Response{message: result}} = Gateway.authorize(@money, @bad_payment, @options) + post: fn _url, _body, _headers -> MockResponse.failed_authorized_with_bad_card() end do + {:ok, %Response{message: result}} = + Gringotts.authorize(Gateway, @money, @bad_card, @options) + assert String.contains?(result, "Invalid Credit Card Number") end end - test "with bad amount" do - with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.failed_purchase_with_bad_money end] do - {:ok, %Response{message: result}} = Gateway.authorize(@bad_money, @payment, @options) - assert String.contains?(result, "Invalid amount") - end - end end + describe "capture" do test "with full amount" do - with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.successful_capture end] do - {:ok, %Response{success: result}} = Gateway.capture(@money, @authorization, @options) + with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_capture() end do + {:ok, %Response{success: result}} = + Gringotts.capture(Gateway, @money, @authorization, @options) + assert result end end test "with partial amount" do - with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.successful_capture end] do - {:ok, %Response{success: result}} = Gateway.capture(@money - 1, @authorization, @options) + with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_capture() end do + {:ok, %Response{success: result}} = + Gringotts.capture(Gateway, @money_less, @authorization, @options) + assert result end end test "with invalid transaction_id" do with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.invalid_transaction_id end] do - {:ok, %Response{message: result}} = Gateway.capture(@money, @bad_authorization, @options) + post: fn _url, _body, _headers -> MockResponse.invalid_transaction_id() end do + {:ok, %Response{message: result}} = + Gringotts.capture(Gateway, @money, @bad_authorization, @options) + assert String.contains?(result, "Transaction not found") end end test "with more than authorization amount" do with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.more_than_authorization_amount end] do - {:ok, %Response{message: result}} = Gateway.capture(@money + 1, @authorization, @options) + post: fn _url, _body, _headers -> MockResponse.more_than_authorization_amount() end do + {:ok, %Response{message: result}} = + Gringotts.capture(Gateway, @money_more, @authorization, @options) + assert String.contains?(result, "exceeds the authorization amount") end end test "on already captured transaction" do with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.multiple_capture_on_same_transaction end] do - {:ok, %Response{message: result}} = Gateway.capture(@money, @authorization, @options) + post: fn _url, _body, _headers -> MockResponse.multiple_capture_on_same_transaction() end do + {:ok, %Response{message: result}} = + Gringotts.capture(Gateway, @money, @authorization, @options) + assert String.contains?(result, "A capture requires that") end end end describe "refund" do - test "with all good" do - with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.successful_refund end] do - {:ok, %Response{success: result}} = Gateway.refund(@money, @authorization, @options) + test "with correct params" do + with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_refund() end do + {:ok, %Response{success: result}} = + Gringotts.refund(Gateway, @money, @authorization, @options) + assert result end end test "with more than purchased amount" do with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.more_than_purchase_amount end] do - {:ok, %Response{message: result}} = Gateway.refund(@money + 1, @authorization, @options) + post: fn _url, _body, _headers -> MockResponse.more_than_purchase_amount() end do + {:ok, %Response{message: result}} = + Gringotts.refund(Gateway, @money_more, @authorization, @options) + assert String.contains?(result, "Refund amount may not exceed") end end end - - describe "void" do - test "with all good" do - with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.successful_void end] do - {:ok, %Response{message: result}} = Gateway.void(@authorization, @options) + + describe "void" do + test "with correct params" do + with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_void() end do + {:ok, %Response{message: result}} = Gringotts.void(Gateway, @authorization, @options) assert String.contains?(result, "Void Successful") end end test "with invalid transaction_id" do with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.invalid_transaction_id end] do - {:ok, %Response{message: result}} = Gateway.void(@bad_authorization, @options) + post: fn _url, _body, _headers -> MockResponse.invalid_transaction_id() end do + {:ok, %Response{message: result}} = Gringotts.void(Gateway, @bad_authorization, @options) assert String.contains?(result, "Transaction not found") end end end describe "validate" do - test "with all good" do + test "with correct params" do with_mock HTTPoison, - [post: fn(_url, _body, _headers) -> MockResponse.validate_creditcard end] do - {:ok, %Response{success: result}} = Gateway.validate(@payment, @options) + post: fn _url, _body, _headers -> MockResponse.validate_creditcard() end do + {:ok, %Response{success: result}} = Gateway.validate(@card, @options ++ [config: @auth]) assert result end end diff --git a/test/mocks/cams_mock.exs b/test/mocks/cams_mock.exs index d7ee7808..e499b450 100644 --- a/test/mocks/cams_mock.exs +++ b/test/mocks/cams_mock.exs @@ -1,218 +1,211 @@ defmodule Gringotts.Gateways.CamsMock do def successful_purchase do - {:ok, - %HTTPoison.Response{ - body: "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3916017714&avsresponse=N&cvvresponse=N&orderid=&type=sale&response_code=100", + {:ok, %HTTPoison.Response{ + body: + "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3916017714&avsresponse=N&cvvresponse=N&orderid=&type=sale&response_code=100", headers: [ - {"Date", "Thu, 21 Dec 2017 12:45:16 GMT"}, + {"Date", "Thu, 21 Dec 2017 12:45:16 GMT"}, {"Server", "Apache"}, - {"Content-Length", "137"}, + {"Content-Length", "137"}, {"Content-Type", "text/html; charset=UTF-8"} ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end - + def failed_purchase_with_bad_credit_card do - {:ok, - %HTTPoison.Response{ - body: "response=3&responsetext=Invalid Credit Card Number REFID:3502947912&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=sale&response_code=300", + {:ok, %HTTPoison.Response{ + body: + "response=3&responsetext=Invalid Credit Card Number REFID:3502947912&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=sale&response_code=300", headers: [ {"Date", "Thu, 21 Dec 2017 13:20:08 GMT"}, {"Server", "Apache"}, - {"Content-Length", "155"}, + {"Content-Length", "155"}, {"Content-Type", "text/html; charset=UTF-8"} ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end - def failed_purchase_with_bad_money do - {:ok, - %HTTPoison.Response{ - body: "response=3&responsetext=Invalid amount REFID:3502949755&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=sale&response_code=300", - headers: [ - {"Date", "Thu, 21 Dec 2017 13:50:20 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "143"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} - end - - def failed_purchase_with_bad_credit_card do - {:ok, - %HTTPoison.Response{ - body: "response=3&responsetext=Invalid Credit Card Number REFID:3502947912&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=sale&response_code=300", - headers: [ - {"Date", "Thu, 21 Dec 2017 13:20:08 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "155"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} - end + def with_invalid_currency do - {:ok, - %HTTPoison.Response{ - body: "response=3&responsetext=The cc payment type [Visa] and/or currency [INR] is not accepted REFID:3503238709&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=auth&response_code=300", + {:ok, %HTTPoison.Response{ + body: + "response=3&responsetext=The cc payment type [Visa] and/or currency [INR] is not accepted REFID:3503238709&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=auth&response_code=300", headers: [ {"Date", "Tue, 26 Dec 2017 10:37:42 GMT"}, {"Server", "Apache"}, - {"Content-Length", "193"}, + {"Content-Length", "193"}, {"Content-Type", "text/html; charset=UTF-8"} ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end - + def successful_capture do - {:ok, - %HTTPoison.Response{ - body: "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3921111362&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=100", + {:ok, %HTTPoison.Response{ + body: + "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3921111362&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=100", headers: [ {"Date", "Tue, 26 Dec 2017 12:16:55 GMT"}, {"Server", "Apache"}, - {"Content-Length", "138"}, + {"Content-Length", "138"}, {"Content-Type", "text/html; charset=UTF-8"} ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end + def successful_authorize do - {:ok, - %HTTPoison.Response{ - body: "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3921111362&avsresponse=N&cvvresponse=N&orderid=&type=auth&response_code=100", + {:ok, %HTTPoison.Response{ + body: + "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3921111362&avsresponse=N&cvvresponse=N&orderid=&type=auth&response_code=100", headers: [ - {"Date", "Tue, 26 Dec 2017 12:16:11 GMT"}, + {"Date", "Tue, 26 Dec 2017 12:16:11 GMT"}, {"Server", "Apache"}, - {"Content-Length", "137"}, + {"Content-Length", "137"}, {"Content-Type", "text/html; charset=UTF-8"} ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end + def invalid_transaction_id do - {:ok, - %HTTPoison.Response{ - body: "response=3&responsetext=Transaction not found REFID:3503243979&authcode=&transactionid=3921118690&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", + {:ok, %HTTPoison.Response{ + body: + "response=3&responsetext=Transaction not found REFID:3503243979&authcode=&transactionid=3921118690&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", headers: [ - {"Date", "Tue, 26 Dec 2017 12:39:05 GMT"}, + {"Date", "Tue, 26 Dec 2017 12:39:05 GMT"}, {"Server", "Apache"}, - {"Content-Length", "163"}, + {"Content-Length", "163"}, {"Content-Type", "text/html; charset=UTF-8"} ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} - end - def more_than_authorization_amount do - {:ok, - %HTTPoison.Response{ - body: "response=3&responsetext=The specified amount of 1001 exceeds the authorization amount of 1000.00 REFID:3503244462&authcode=&transactionid=3921127126&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} + end + + def more_than_authorization_amount do + {:ok, %HTTPoison.Response{ + body: + "response=3&responsetext=The specified amount of 1001 exceeds the authorization amount of 1000.00 REFID:3503244462&authcode=&transactionid=3921127126&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", headers: [ {"Date", "Tue, 26 Dec 2017 13:00:55 GMT"}, {"Server", "Apache"}, - {"Content-Length", "214"}, + {"Content-Length", "214"}, {"Content-Type", "text/html; charset=UTF-8"} ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end + def successful_refund do - {:ok, - %HTTPoison.Response{ - body: "response=1&responsetext=SUCCESS&authcode=&transactionid=3921158933&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=100", + {:ok, %HTTPoison.Response{ + body: + "response=1&responsetext=SUCCESS&authcode=&transactionid=3921158933&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=100", headers: [ {"Date", "Tue, 26 Dec 2017 14:00:08 GMT"}, {"Server", "Apache"}, - {"Content-Length", "131"}, + {"Content-Length", "131"}, {"Content-Type", "text/html; charset=UTF-8"} ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def more_than_purchase_amount do - {:ok, - %HTTPoison.Response{ - body: "response=3&responsetext=Refund amount may not exceed the transaction balance REFID:3503249728&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300", + {:ok, %HTTPoison.Response{ + body: + "response=3&responsetext=Refund amount may not exceed the transaction balance REFID:3503249728&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300", headers: [ - {"Date", "Tue, 26 Dec 2017 14:05:31 GMT"}, + {"Date", "Tue, 26 Dec 2017 14:05:31 GMT"}, {"Server", "Apache"}, - {"Content-Length", "183"}, + {"Content-Length", "183"}, {"Content-Type", "text/html; charset=UTF-8"} ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} - end + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} + end def successful_void do - {:ok, - %HTTPoison.Response{ - body: "response=1&responsetext=Transaction Void Successful&authcode=123456&transactionid=3921178863&avsresponse=&cvvresponse=&orderid=&type=void&response_code=100", + {:ok, %HTTPoison.Response{ + body: + "response=1&responsetext=Transaction Void Successful&authcode=123456&transactionid=3921178863&avsresponse=&cvvresponse=&orderid=&type=void&response_code=100", headers: [ - {"Date", "Tue, 26 Dec 2017 14:26:05 GMT"}, + {"Date", "Tue, 26 Dec 2017 14:26:05 GMT"}, {"Server", "Apache"}, - {"Content-Length", "155"}, + {"Content-Length", "155"}, {"Content-Type", "text/html; charset=UTF-8"} ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def failed_authorized_with_bad_card do - {:ok, - %HTTPoison.Response{ - body: "response=3&responsetext=Invalid Credit Card Number REFID:3503305883&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=auth&response_code=300", - headers: [ - {"Date", "Wed, 27 Dec 2017 09:51:45 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "155"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} + {:ok, %HTTPoison.Response{ + body: + "response=3&responsetext=Invalid Credit Card Number REFID:3503305883&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=auth&response_code=300", + headers: [ + {"Date", "Wed, 27 Dec 2017 09:51:45 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "155"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def multiple_capture_on_same_transaction do - {:ok, - %HTTPoison.Response{ - body: "response=3&responsetext=A capture requires that the existing transaction be an AUTH REFID:3503316182&authcode=&transactionid=3922433984&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", - headers: [ - {"Date", "Wed, 27 Dec 2017 13:47:12 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "201"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} + {:ok, %HTTPoison.Response{ + body: + "response=3&responsetext=A capture requires that the existing transaction be an AUTH REFID:3503316182&authcode=&transactionid=3922433984&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", + headers: [ + {"Date", "Wed, 27 Dec 2017 13:47:12 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "201"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def refund_the_authorised_transaction do - {:ok, - %HTTPoison.Response{ - body: "response=3&responsetext=Refund amount may not exceed the transaction balance REFID:3503316128&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300", - headers: [{"Date", "Wed, 27 Dec 2017 13:45:19 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "183"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} + {:ok, %HTTPoison.Response{ + body: + "response=3&responsetext=Refund amount may not exceed the transaction balance REFID:3503316128&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300", + headers: [ + {"Date", "Wed, 27 Dec 2017 13:45:19 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "183"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def validate_creditcard do - {:ok, - %HTTPoison.Response{ - body: "response=1&responsetext=&authcode=&transactionid=3933708264&avsresponse=&cvvresponse=&orderid=&type=verify&response_code=100", + {:ok, %HTTPoison.Response{ + body: + "response=1&responsetext=&authcode=&transactionid=3933708264&avsresponse=&cvvresponse=&orderid=&type=verify&response_code=100", headers: [ - {"Date", "Thu, 04 Jan 2018 11:12:20 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "124"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200}} + {"Date", "Thu, 04 Jan 2018 11:12:20 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "124"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end end From f2796b465aba6817687b4fd0af5e3306044aa39a Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Thu, 25 Jan 2018 18:44:38 +0530 Subject: [PATCH 21/60] Fix doc example typos, and mock tests (#93) * Corrected capture args order * Mock tests now use the worker * Corrected capture args order --- lib/gringotts/gateways/monei.ex | 118 +++++++++++------------ test/gateways/monei_test.exs | 106 +++++++++++--------- test/integration/gateways/monei_test.exs | 2 +- 3 files changed, 117 insertions(+), 109 deletions(-) diff --git a/lib/gringotts/gateways/monei.ex b/lib/gringotts/gateways/monei.ex index 124f1fbc..53ebb130 100644 --- a/lib/gringotts/gateways/monei.ex +++ b/lib/gringotts/gateways/monei.ex @@ -131,7 +131,7 @@ defmodule Gringotts.Gateways.Monei do aliases to it (to save some time): ``` iex> alias Gringotts.{Response, CreditCard, Gateways.Monei} - iex> amount = %{value: Decimal.new(42), currency: "EUR"} + iex> amount = %{value: Decimal.new(42), currency: "USD"} iex> card = %CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", @@ -145,7 +145,7 @@ defmodule Gringotts.Gateways.Monei do "birthDate": "1980-07-31", "mobile": "+15252525252", "email": "masterofdeath@ministryofmagic.gov", - "ip": "1.1.1", + "ip": "127.0.0.1", "status": "NEW"} iex> merchant = %{"name": "Ollivanders", "city": "South Side", @@ -249,17 +249,17 @@ defmodule Gringotts.Gateways.Monei do ## Example - The following session shows how one would (pre) authorize a payment of $40 on + The following example shows how one would (pre) authorize a payment of $42 on a sample `card`. - iex> amount = %{value: Decimal.new(42), currency: "EUR"} + iex> amount = %{value: Decimal.new(42), currency: "USD"} iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Monei, amount, card, opts) iex> auth_result.id # This is the authorization ID iex> auth_result.token # This is the registration ID/token """ @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} - def authorize(amount, card = %CreditCard{}, opts) do + def authorize(amount, %CreditCard{} = card, opts) do {currency, value} = Money.to_string(amount) params = @@ -287,16 +287,16 @@ defmodule Gringotts.Gateways.Monei do ## Example - The following session shows how one would (partially) capture a previously + The following example shows how one would (partially) capture a previously authorized a payment worth $35 by referencing the obtained authorization `id`. - iex> amount = %{value: Decimal.new(42), currency: "EUR"} - iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VIS iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Monei, 35, auth_result.id, opts) + iex> amount = %{value: Decimal.new(35), currency: "USD"} + iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Monei, amount, auth_result.id, opts) """ - @spec capture(Money.t(), String.t(), keyword) :: {:ok | :error, Response} - def capture(amount, payment_id, opts) + @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} + def capture(payment_id, amount, opts) - def capture(amount, <>, opts) do + def capture(<>, amount, opts) do {currency, value} = Money.to_string(amount) params = [ @@ -321,16 +321,16 @@ defmodule Gringotts.Gateways.Monei do ## Example - The following session shows how one would process a payment in one-shot, - without (pre) authorization. + The following example shows how one would process a payment worth $42 in + one-shot, without (pre) authorization. - iex> amount = %{value: Decimal.new(42), currency: "EUR"} + iex> amount = %{value: Decimal.new(42), currency: "USD"} iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.Monei, amount, card, opts) iex> purchase_result.token # This is the registration ID/token """ @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} - def purchase(amount, card = %CreditCard{}, opts) do + def purchase(amount, %CreditCard{} = card, opts) do {currency, value} = Money.to_string(amount) params = @@ -342,46 +342,6 @@ defmodule Gringotts.Gateways.Monei do commit(:post, "payments", params, [{:currency, currency} | opts]) end - @doc """ - Voids the referenced payment. - - This method attempts a reversal of the either a previous `purchase/3` or - `authorize/3` referenced by `payment_id`. - - As a consequence, the customer will never see any booking on his - statement. Refer MONEI's [Backoffice - Operations](https://docs.monei.net/tutorials/manage-payments/backoffice) - guide. - - ## Voiding a previous authorization - - MONEI will reverse the authorization by sending a "reversal request" to the - payment source (card issuer) to clear the funds held against the - authorization. If some of the authorized amount was captured, only the - remaining amount is cleared. **[citation-needed]** - - ## Voiding a previous purchase - - MONEI will reverse the payment, by sending all the amount back to the - customer. Note that this is not the same as `refund/3`. - - ## Example - - The following session shows how one would void a previous (pre) - authorization. Remember that our `capture/3` example only did a partial - capture. - - iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.Monei, auth_result.id, opts) - """ - @spec void(String.t(), keyword) :: {:ok | :error, Response} - def void(payment_id, opts) - - def void(<>, opts) do - params = [paymentType: "RV"] - commit(:post, "payments/#{payment_id}", params, opts) - end - @doc """ Refunds the `amount` to the customer's account with reference to a prior transfer. @@ -395,11 +355,10 @@ defmodule Gringotts.Gateways.Monei do ## Example - The following session shows how one would refund a previous purchase (and - similarily for captures). + The following example shows how one would (completely) refund a previous + purchase (and similarily for captures). - iex> amount = %{value: Decimal.new(42), currency: "EUR"} - iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} + iex> amount = %{value: Decimal.new(42), currency: "USD"} iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.Monei, purchase_result.id, amount) """ @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} @@ -431,7 +390,7 @@ defmodule Gringotts.Gateways.Monei do ## Example - The following session shows how one would store a card (a payment-source) for + The following example shows how one would store a card (a payment-source) for future use. iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} @@ -457,6 +416,45 @@ defmodule Gringotts.Gateways.Monei do commit(:delete, "registrations/#{registration_id}", [], opts) end + @doc """ + Voids the referenced payment. + + This method attempts a reversal of the either a previous `purchase/3`, + `capture/3` or `authorize/3` referenced by `payment_id`. + + As a consequence, the customer will never see any booking on his + statement. Refer MONEI's [Backoffice + Operations](https://docs.monei.net/tutorials/manage-payments/backoffice) + guide. + + ## Voiding a previous authorization + + MONEI will reverse the authorization by sending a "reversal request" to the + payment source (card issuer) to clear the funds held against the + authorization. If some of the authorized amount was captured, only the + remaining amount is cleared. **[citation-needed]** + + ## Voiding a previous purchase + + MONEI will reverse the payment, by sending all the amount back to the + customer. Note that this is not the same as `refund/3`. + + ## Example + + The following example shows how one would void a previous (pre) + authorization. Remember that our `capture/3` example only did a partial + capture. + + iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.Monei, auth_result.id, opts) + """ + @spec void(String.t(), keyword) :: {:ok | :error, Response} + def void(payment_id, opts) + + def void(<>, opts) do + params = [paymentType: "RV"] + commit(:post, "payments/#{payment_id}", params, opts) + end + defp card_params(card) do [ "card.number": card.number, diff --git a/test/gateways/monei_test.exs b/test/gateways/monei_test.exs index 51823a08..9fd72895 100644 --- a/test/gateways/monei_test.exs +++ b/test/gateways/monei_test.exs @@ -1,5 +1,5 @@ defmodule Gringotts.Gateways.MoneiTest do - use ExUnit.Case, async: false + use ExUnit.Case, async: true alias Gringotts.{ CreditCard @@ -7,6 +7,10 @@ defmodule Gringotts.Gateways.MoneiTest do alias Gringotts.Gateways.Monei, as: Gateway + @amount42 Money.new(42, :USD) + @amount3 Money.new(3, :USD) + @bad_currency Money.new(42, :INR) + @card %CreditCard{ first_name: "Harry", last_name: "Potter", @@ -27,37 +31,6 @@ defmodule Gringotts.Gateways.MoneiTest do brand: "VISA" } - @bad_currency Money.new(42, :INR) - - @auth_success ~s[ - {"id": "8a82944a603b12d001603c1a1c2d5d90", - "result": { - "code": "000.100.110", - "description": "Request successfully processed in 'Merchant in Integrator Test Mode'"} - }] - - @register_success ~s[ - {"id": "8a82944960e073640160e92da2204743", - "registrationId": "8a82944a60e09c550160e92da144491e", - "result": { - "code": "000.100.110", - "description": "Request successfully processed in 'Merchant in Integrator Test Mode'"} - }] - - @store_success ~s[ - {"result":{ - "code":"000.100.110", - "description":"Request successfully processed in 'Merchant in Integrator Test Mode'" - }, - "card":{ - "bin":"420000", - "last4Digits":"0000", - "holder":"Jo Doe", - "expiryMonth":"12", - "expiryYear":"2099" - } - }] - @customer %{ givenName: "Harry", surname: "Potter", @@ -100,14 +73,43 @@ defmodule Gringotts.Gateways.MoneiTest do custom: %{"voldemort" => "he who must not be named"} ] + @auth_success ~s[ + {"id": "8a82944a603b12d001603c1a1c2d5d90", + "result": { + "code": "000.100.110", + "description": "Request successfully processed in 'Merchant in Integrator Test Mode'"} + }] + + @register_success ~s[ + {"id": "8a82944960e073640160e92da2204743", + "registrationId": "8a82944a60e09c550160e92da144491e", + "result": { + "code": "000.100.110", + "description": "Request successfully processed in 'Merchant in Integrator Test Mode'"} + }] + + @store_success ~s[ + {"result":{ + "code":"000.100.110", + "description":"Request successfully processed in 'Merchant in Integrator Test Mode'" + }, + "card":{ + "bin":"420000", + "last4Digits":"0000", + "holder":"Jo Doe", + "expiryMonth":"12", + "expiryYear":"2099" + } + }] + # A new Bypass instance is needed per test, so that we can do parallel tests setup do bypass = Bypass.open() auth = %{ - userId: "8a829417539edb400153c1eae83932ac", - password: "6XqRtMGS2N", - entityId: "8a829417539edb400153c1eae6de325e", + userId: "some_secret_user_id", + password: "some_secret_password", + entityId: "some_secret_entity_id", test_url: "http://localhost:#{bypass.port}" } @@ -126,11 +128,11 @@ defmodule Gringotts.Gateways.MoneiTest do end) Bypass.down(bypass) - {:error, response} = Gateway.authorize(Money.new(42, :USD), @card, config: auth) + {:error, response} = Gateway.authorize(@amount42, @card, config: auth) assert response.reason == "network related failure" Bypass.up(bypass) - {:ok, _} = Gateway.authorize(Money.new(42, :USD), @card, config: auth) + {:ok, _} = Gateway.authorize(@amount42, @card, config: auth) end test "with all extra_params.", %{bypass: bypass, auth: auth} do @@ -147,15 +149,18 @@ defmodule Gringotts.Gateways.MoneiTest do assert conn_.body_params["merchantTransactionId"] == randoms[:transaction_id] assert conn_.body_params["transactionCategory"] == @extra_opts[:category] assert conn_.body_params["customer.merchantCustomerId"] == @customer[:merchantCustomerId] - assert conn_.body_params["shipping.customer.merchantCustomerId"] == @customer[:merchantCustomerId] + + assert conn_.body_params["shipping.customer.merchantCustomerId"] == + @customer[:merchantCustomerId] + assert conn_.body_params["merchant.submerchantId"] == @merchant[:submerchantId] assert conn_.body_params["billing.city"] == @billing[:city] assert conn_.body_params["shipping.method"] == @shipping[:method] Plug.Conn.resp(conn, 200, @register_success) end) - opts = [{:config, auth} | randoms] ++ @extra_opts - {:ok, response} = Gateway.purchase(Money.new(42, :USD), @card, opts) + opts = randoms ++ @extra_opts ++ [config: auth] + {:ok, response} = Gateway.purchase(@amount42, @card, opts) assert response.code == "000.100.110" assert response.token == "8a82944a60e09c550160e92da144491e" end @@ -165,7 +170,7 @@ defmodule Gringotts.Gateways.MoneiTest do Plug.Conn.resp(conn, 400, "") end) - {:error, _} = Gateway.authorize(Money.new(42, :USD), @bad_card, config: auth) + {:error, _} = Gateway.authorize(@amount42, @bad_card, config: auth) end end @@ -175,7 +180,7 @@ defmodule Gringotts.Gateways.MoneiTest do Plug.Conn.resp(conn, 200, @auth_success) end) - {:ok, response} = Gateway.authorize(Money.new(42, :USD), @card, config: auth) + {:ok, response} = Gateway.authorize(@amount42, @card, config: auth) assert response.code == "000.100.110" end end @@ -186,7 +191,7 @@ defmodule Gringotts.Gateways.MoneiTest do Plug.Conn.resp(conn, 200, @auth_success) end) - {:ok, response} = Gateway.purchase(Money.new(42, :USD), @card, config: auth) + {:ok, response} = Gateway.purchase(@amount42, @card, config: auth) assert response.code == "000.100.110" end @@ -197,7 +202,7 @@ defmodule Gringotts.Gateways.MoneiTest do Plug.Conn.resp(conn, 200, @register_success) end) - {:ok, response} = Gateway.purchase(Money.new(42, :USD), @card, config: auth, register: true) + {:ok, response} = Gateway.purchase(@amount42, @card, register: true, config: auth) assert response.code == "000.100.110" assert response.token == "8a82944a60e09c550160e92da144491e" end @@ -227,7 +232,7 @@ defmodule Gringotts.Gateways.MoneiTest do ) {:ok, response} = - Gateway.capture(Money.new(42, :USD), "7214344242e11af79c0b9e7b4f3f6234", config: auth) + Gateway.capture("7214344242e11af79c0b9e7b4f3f6234", @amount42, config: auth) assert response.code == "000.100.110" end @@ -244,7 +249,13 @@ defmodule Gringotts.Gateways.MoneiTest do end ) - {:ok, response} = Gateway.capture(Money.new(42, :USD), "7214344242e11af79c0b9e7b4f3f6234", config: auth, register: true) + {:ok, response} = + Gateway.capture( + "7214344242e11af79c0b9e7b4f3f6234", + @amount42, + register: true, + config: auth + ) assert response.code == "000.100.110" end @@ -261,8 +272,7 @@ defmodule Gringotts.Gateways.MoneiTest do end ) - {:ok, response} = - Gateway.refund(Money.new(3, :USD), "7214344242e11af79c0b9e7b4f3f6234", config: auth) + {:ok, response} = Gateway.refund(@amount3, "7214344242e11af79c0b9e7b4f3f6234", config: auth) assert response.code == "000.100.110" end diff --git a/test/integration/gateways/monei_test.exs b/test/integration/gateways/monei_test.exs index da24cc3e..6619a43f 100644 --- a/test/integration/gateways/monei_test.exs +++ b/test/integration/gateways/monei_test.exs @@ -99,7 +99,7 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do @tag :skip test "capture", %{opts: _opts} do - case Gringotts.capture(Gateway, @amount, "s") do + case Gringotts.capture(Gateway, "s", @amount) do {:ok, response} -> assert response.code == "000.100.110" From fa1cd11352184520516a63a19e658473676c0a24 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Thu, 25 Jan 2018 18:47:33 +0530 Subject: [PATCH 22/60] [mix-task] Fixed arg order in capture (#94) * Fixed arg order in capture * fixes some patterns in function clauses * Prompts for filename * Added a missing comma in integration template --- lib/mix/new.ex | 57 ++++++++++++++++++++++++------------- templates/gateway.eex | 8 +++--- templates/integration.eex | 2 +- templates/mock_response.eex | 2 +- templates/test.eex | 4 +-- 5 files changed, 45 insertions(+), 28 deletions(-) diff --git a/lib/mix/new.ex b/lib/mix/new.ex index 4f2bb609..c37fd7d0 100644 --- a/lib/mix/new.ex +++ b/lib/mix/new.ex @@ -2,7 +2,7 @@ defmodule Mix.Tasks.Gringotts.New do @shortdoc """ Generates a barebones implementation for a gateway. """ - + @moduledoc """ Generates a barebones implementation for a gateway. @@ -14,24 +14,26 @@ defmodule Mix.Tasks.Gringotts.New do A barebones implementation of the gateway will be created along with skeleton mock and integration tests in `lib/gringotts/gateways/`. The command will prompt for the module name, and other metadata. - + ## Options > ***Tip!*** > You can supply the extra arguments to `gringotts.new` to skip (some of) the > prompts. - + * `-m` `--module` - The module name for the Gateway. * `--url` - The homepage of the gateway. ## Examples - mix gringotts.new foobar + mix gringotts.new FooBar The prompts for this will be: + ``` MODULE = `Foobar` URL = `https://www.foobar.com` - REQUIRED_KEYS = [] + ``` + and the filename will be `foo_bar.ex` """ use Mix.Task @@ -41,7 +43,7 @@ defmodule Mix.Tasks.Gringotts.New do Comma separated list of required configuration keys: (This can be skipped by hitting `Enter`) > } - + def run(args) do {key_list, [name], []} = OptionParser.parse( @@ -58,39 +60,54 @@ Comma separated list of required configuration keys: :error -> prompt_with_suggestion("\nModule name", String.capitalize(name)) {:ok, mod_name} -> mod_name end - + url = case Keyword.fetch(key_list, :url) do - :error -> prompt_with_suggestion("\nHomepage URL", "https://www.#{String.Casing.downcase(name)}.com") - {:ok, url} -> url + :error -> + prompt_with_suggestion( + "\nHomepage URL", + "https://www.#{String.Casing.downcase(name)}.com" + ) + + {:ok, url} -> + url end - + + file_name = prompt_with_suggestion("\nFilename", Macro.underscore(name)) + required_keys = - case Mix.Shell.IO.prompt(@long_msg) |> String.trim do + case Mix.Shell.IO.prompt(@long_msg) |> String.trim() do "" -> [] - keys -> String.split(keys, ",") |> Enum.map(&(String.trim(&1))) |> Enum.map(&(String.to_atom(&1))) + + keys -> + String.split(keys, ",") |> Enum.map(&String.trim(&1)) |> Enum.map(&String.to_atom(&1)) end bindings = [ gateway: name, gateway_module: module_name, - gateway_underscore: Macro.underscore(name), + gateway_underscore: file_name, required_config_keys: required_keys, gateway_url: url, - gateway_mock_test: Macro.underscore(name) <> "_test", - gateway_mock_response: Macro.underscore(name) <> "_mock", + mock_test_filename: file_name <> "_test", + mock_response_filename: file_name <> "_mock" ] - if (Mix.Shell.IO.yes? "\nDoes this look good?\n#{inspect(bindings, pretty: true)}\n>") do + if Mix.Shell.IO.yes?( + "\nDoes this look good?\n#{inspect(bindings, pretty: true, width: 40)}\n>" + ) do gateway = EEx.eval_file("templates/gateway.eex", bindings) mock = EEx.eval_file("templates/test.eex", bindings) mock_response = EEx.eval_file("templates/mock_response.eex", bindings) integration = EEx.eval_file("templates/integration.eex", bindings) create_file("lib/gringotts/gateways/#{bindings[:gateway_underscore]}.ex", gateway) - create_file("test/gateways/#{bindings[:gateway_mock_test]}.exs", mock) - create_file("test/mocks/#{bindings[:gateway_mock_response]}.exs", mock_response) - create_file("test/integration/gateways/#{bindings[:gateway_mock_test]}.exs", integration) + create_file("test/integration/gateways/#{bindings[:mock_test_filename]}.exs", integration) + + if Mix.Shell.IO.yes?("\nAlso create empty mock test suite?\n>") do + create_file("test/gateways/#{bindings[:mock_test_filename]}.exs", mock) + create_file("test/mocks/#{bindings[:mock_response_filename]}.exs", mock_response) + end else Mix.Shell.IO.info("Doing nothing, bye!") end @@ -98,7 +115,7 @@ Comma separated list of required configuration keys: defp prompt_with_suggestion(message, suggestion) do decorated_message = "#{message} [#{suggestion}]" - response = Mix.Shell.IO.prompt(decorated_message) |> String.trim + response = Mix.Shell.IO.prompt(decorated_message) |> String.trim() if response == "", do: suggestion, else: response end end diff --git a/templates/gateway.eex b/templates/gateway.eex index 78165a5e..b68e0a31 100644 --- a/templates/gateway.eex +++ b/templates/gateway.eex @@ -148,8 +148,8 @@ defmodule Gringotts.Gateways.<%= gateway_module %> do > A barebones example using the bindings you've suggested in the `moduledoc`. """ - @spec capture(Money.t, String.t(), keyword) :: {:ok | :error, Response} - def capture(amount, payment_id, opts) do + @spec capture(String.t(), Money.t, keyword) :: {:ok | :error, Response} + def capture(payment_id, amount, opts) do # commit(args, ...) end @@ -211,7 +211,7 @@ defmodule Gringotts.Gateways.<%= gateway_module %> do > A barebones example using the bindings you've suggested in the `moduledoc`. """ @spec refund(Money.t, String.t(), keyword) :: {:ok | :error, Response} - def refund(amount, <>, opts) do + def refund(amount, payment_id, opts) do # commit(args, ...) end @@ -247,7 +247,7 @@ defmodule Gringotts.Gateways.<%= gateway_module %> do > A barebones example using the bindings you've suggested in the `moduledoc`. """ @spec unstore(String.t(), keyword) :: {:ok | :error, Response} - def unstore(<>, opts) do + def unstore(registration_id, opts) do # commit(args, ...) end diff --git a/templates/integration.eex b/templates/integration.eex index 65fc4bff..f57b4b86 100644 --- a/templates/integration.eex +++ b/templates/integration.eex @@ -9,7 +9,7 @@ defmodule Gringotts.Integration.Gateways.<%= gateway_module <> "Test"%> do setup_all do Application.put_env(:gringotts, Gringotts.Gateways.<%= gateway_module%>, [ - adapter: Gringotts.Gateways.<%= gateway_module%><%= if required_config_keys != [] do %><%= for key <- Enum.intersperse(required_config_keys, ",") do %><%= if key === "," do %><%= "#{key}" %><%= else %> + adapter: Gringotts.Gateways.<%= gateway_module%><%= if required_config_keys != [] do %>,<%= for key <- Enum.intersperse(required_config_keys, ",") do %><%= if key === "," do %><%= "#{key}" %><% else %> <%= "#{key}" %>: "your_secret_<%= "#{key}" %>"<% end %><% end %><% end %> ] ) diff --git a/templates/mock_response.eex b/templates/mock_response.eex index 1b0e5b63..d4ad1f5b 100644 --- a/templates/mock_response.eex +++ b/templates/mock_response.eex @@ -1,6 +1,6 @@ defmodule Gringotts.Gateways.<%= gateway_module <> "Mock"%> do - # The module should include mock responses for test cases in <%= gateway_mock_test <> ".exs"%>. + # The module should include mock responses for test cases in <%= mock_test_filename <> ".exs"%>. # e.g. # def successful_purchase do # {:ok, %HTTPoison.Response{body: ~s[{data: "successful_purchase"}]} diff --git a/templates/test.eex b/templates/test.eex index db4bb72f..2fc65a79 100644 --- a/templates/test.eex +++ b/templates/test.eex @@ -2,13 +2,13 @@ defmodule Gringotts.Gateways.<%= gateway_module <> "Test" %> do # The file contains mocked tests for <%= gateway_module%> # We recommend using [mock][1] for this, you can place the mock responses from - # the Gateway in `test/mocks/<%= gateway_mock_response%>.exs` file, which has also been + # the Gateway in `test/mocks/<%= mock_response_filename%>.exs` file, which has also been # generated for you. # # [1]: https://github.com/jjh42/mock # Load the mock response file before running the tests. - Code.require_file "../mocks/<%= gateway_mock_response <> ".exs"%>", __DIR__ + Code.require_file "../mocks/<%= mock_response_filename <> ".exs"%>", __DIR__ use ExUnit.Case, async: false alias Gringotts.Gateways.<%= gateway_module%> From 5b15d30705dcfea8a1bec05433c02e9063e2f88a Mon Sep 17 00:00:00 2001 From: Jyoti Gautam Date: Thu, 25 Jan 2018 18:53:53 +0530 Subject: [PATCH 23/60] Global Collect payment gateway integration. (#95) --- lib/gringotts/gateways/global_collect.ex | 461 +++++++++++++++++++++++ mix.exs | 21 +- test/gateways/global_collect_test.exs | 207 ++++++++++ test/mocks/global_collect_mock.exs | 182 +++++++++ 4 files changed, 861 insertions(+), 10 deletions(-) create mode 100644 lib/gringotts/gateways/global_collect.ex create mode 100644 test/gateways/global_collect_test.exs create mode 100644 test/mocks/global_collect_mock.exs diff --git a/lib/gringotts/gateways/global_collect.ex b/lib/gringotts/gateways/global_collect.ex new file mode 100644 index 00000000..0a2010e6 --- /dev/null +++ b/lib/gringotts/gateways/global_collect.ex @@ -0,0 +1,461 @@ +defmodule Gringotts.Gateways.GlobalCollect do + @moduledoc """ + [GlobalCollect][home] gateway implementation. + + For further details, please refer [GlobalCollect API documentation](https://epayments-api.developer-ingenico.com/s2sapi/v1/en_US/index.html). + + Following are the features that have been implemented for the GlobalCollect Gateway: + + | Action | Method | + | ------ | ------ | + | Authorize | `authorize/3` | + | Purchase | `purchase/3` | + | Capture | `capture/3` | + | Refund | `refund/3` | + | Void | `void/2` | + + ## Optional or extra parameters + + Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply + optional arguments for transactions with the gateway. + + | Key | Status | + | ---- | --- | + | `merchantCustomerId` | implemented | + | `description` | implemented | + | `customer_name` | implemented | + | `dob` | implemented | + | `company` | implemented | + | `email` | implemented | + | `phone` | implemented | + | `order_id` | implemented | + | `invoice` | implemented | + | `billingAddress` | implemented | + | `shippingAddress` | implemented | + | `name` | implemented | + | `skipAuthentication` | implemented | + + ## Registering your GlobalCollect account at `Gringotts` + + After creating your account successfully on [GlobalCollect](http://www.globalcollect.com/) follow the [dashboard link](https://sandbox.account.ingenico.com/#/account/apikey) to fetch the secret_api_key, api_key_id and [here](https://sandbox.account.ingenico.com/#/account/merchantid) for merchant_id. + + Here's how the secrets map to the required configuration parameters for GlobalCollect: + + | Config parameter | GlobalCollect secret | + | ------- | ---- | + | `:secret_api_key`| **SecretApiKey** | + | `:api_key_id` | **ApiKeyId** | + | `:merchant_id` | **MerchantId** | + + Your Application config **must include the `[:secret_api_key, :api_key_id, :merchant_id]` field(s)** and would look + something like this: + + config :gringotts, Gringotts.Gateways.GlobalCollect, + adapter: Gringotts.Gateways.GlobalCollect, + secret_api_key: "your_secret_secret_api_key" + api_key_id: "your_secret_api_key_id" + merchant_id: "your_secret_merchant_id" + + ## Supported currencies and countries + + The GlobalCollect platform is able to support payments in [over 150 currencies][currencies] + + [currencies]: https://epayments.developer-ingenico.com/best-practices/services/currency-conversion + ## Following the examples + + 1. First, set up a sample application and configure it to work with GlobalCollect. + - You could do that from scratch by following our [Getting Started][gs] guide. + - To save you time, we recommend [cloning our example + repo][example] that gives you a pre-configured sample app ready-to-go. + + You could use the same config or update it the with your "secrets" + as described [above](#module-registering-your-globalcollect-account-at-GlobalCollect). + + 2. Run an `iex` session with `iex -S mix` and add some variable bindings and + aliases to it (to save some time): + ``` + iex> alias Gringotts.{Response, CreditCard, Gateways.GlobalCollect} + + iex> shippingAddress = %{ + street: "Desertroad", + houseNumber: "1", + additionalInfo: "Suite II", + zip: "84536", + city: "Monument Valley", + state: "Utah", + countryCode: "US" + } + + iex> billingAddress = %{ + street: "Desertroad", + houseNumber: "13", + additionalInfo: "b", + zip: "84536", + city: "Monument Valley", + state: "Utah", + countryCode: "US" + } + + iex> invoice = %{ + invoiceNumber: "000000123", + invoiceDate: "20140306191500" + } + + iex> name = %{ + title: "Miss", + firstName: "Road", + surname: "Runner" + } + + iex> opts = [ description: "Store Purchase 1437598192", merchantCustomerId: "234", customer_name: "John Doe", dob: "19490917", company: "asma", email: "johndoe@gmail.com", phone: "7765746563", order_id: "2323", invoice: invoice, billingAddress: billingAddress, shippingAddress: shippingAddress, name: name, skipAuthentication: "true" ] + + ``` + + We'll be using these in the examples below. + + [example]: https://github.com/aviabird/gringotts_example + """ + @base_url "https://api-sandbox.globalcollect.com/v1/" + + use Gringotts.Gateways.Base + + # The Adapter module provides the `validate_config/1` + # Add the keys that must be present in the Application config in the + # `required_config` list + use Gringotts.Adapter, required_config: [:secret_api_key, :api_key_id, :merchant_id] + + import Poison, only: [decode: 1] + + alias Gringotts.{Money, + CreditCard, + Response} + + @brand_map %{ + "visa": "1", + "american_express": "2", + "master": "3", + "discover": "128", + "jcb": "125", + "diners_club": "132" + } + + @doc """ + Performs a (pre) Authorize operation. + + The authorization validates the `card` details with the banking network, + places a hold on the transaction `amount` in the customer’s issuing bank and + also triggers risk management. Funds are not transferred. + + GlobalCollect returns a payment id which can be further used to: + * `capture/3` _an_ amount. + * `refund/3` _an_amount + * `void/2` a pre_authorization + + ## Example + + > The following session shows how one would (pre) authorize a payment of $100 on + a sample `card`. + ``` + iex> card = %CreditCard{ + number: "4567350000427977", + month: 12, + year: 18, + first_name: "John", + last_name: "Doe", + verification_code: "123", + brand: "visa" + } + + iex> amount = %{value: Decimal.new(100), currency: "USD"} + + iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.GlobalCollect, amount, card, opts) + ``` + """ + @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + def authorize(amount, card = %CreditCard{}, opts) do + params = create_params_for_auth_or_purchase(amount, card, opts) + commit(:post, "payments", params, opts) + end + + @doc """ + Captures a pre-authorized `amount`. + + `amount` is transferred to the merchant account by GlobalCollect used in the + pre-authorization referenced by `payment_id`. + + ## Note + + > Authorized payment with PENDING_APPROVAL status only allow a single capture whereas the one with PENDING_CAPTURE status is used for payments that allow multiple captures. + > PENDING_APPROVAL is a common status only with card and direct debit transactions. + + ## Example + + The following session shows how one would (partially) capture a previously + authorized a payment worth $100 by referencing the obtained authorization `id`. + + ``` + iex> card = %CreditCard{ + number: "4567350000427977", + month: 12, + year: 18, + first_name: "John", + last_name: "Doe", + verification_code: "123", + brand: "visa" + } + + iex> amount = %{value: Decimal.new(100), currency: "USD"} + + iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.GlobalCollect, amount, card, opts) + + ``` + + """ + @spec capture(String.t(), Money.t, keyword) :: {:ok | :error, Response} + def capture(payment_id, amount, opts) do + params = create_params_for_capture(amount, opts) + commit(:post, "payments/#{payment_id}/approve", params, opts) + end + + @doc """ + Transfers `amount` from the customer to the merchant. + + GlobalCollect attempts to process a purchase on behalf of the customer, by + debiting `amount` from the customer's account by charging the customer's + `card`. + + ## Example + + > The following session shows how one would process a payment in one-shot, + without (pre) authorization. + + ``` + iex> card = %CreditCard{ + number: "4567350000427977", + month: 12, + year: 18, + first_name: "John", + last_name: "Doe", + verification_code: "123", + brand: "visa" + } + + iex> amount = %{value: Decimal.new(100), currency: "USD"} + + iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.GlobalCollect, amount, card, opts) + + ``` + """ + @spec purchase(Money.t, CreditCard.t(), keyword) :: {:ok | :error, Response} + def purchase(amount, card = %CreditCard{}, opts) do + case authorize(amount, card, opts) do + {:ok, results} -> + payment_id = results.raw["payment"]["id"] + capture(payment_id, amount, opts) + + {:error, results} -> + {:error, results} + end + end + + @doc """ + Voids the referenced payment. + + This makes it impossible to process the payment any further and will also try to reverse an authorization on a card. + Reversing an authorization that you will not be utilizing will prevent you from having to pay a fee/penalty for unused authorization requests. + + ## Example + + > The following session shows how one would void a previous (pre) + authorization. Remember that our `capture/3` example only did a complete + capture. + + ``` + iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.GlobalCollect, auth_result.payment.id, opts) + + ``` + """ + @spec void(String.t(), keyword) :: {:ok | :error, Response} + def void(payment_id, opts) do + params = nil + commit(:post, "payments/#{payment_id}/cancel", params, opts) + end + + @doc """ + Refunds the `amount` to the customer's account with reference to a prior transfer. + + > You can refund any transaction by just calling this API + + ## Note + + You always have the option to refund just a portion of the payment amount. + It is also possible to submit multiple refund requests on one payment as long as the total amount to be refunded does not exceed the total amount that was paid. + + ## Example + + > The following session shows how one would refund a previous purchase (and + similarily for captures). + + ``` + iex> amount = %{value: Decimal.new(100), currency: "USD"} + + iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.GlobalCollect, auth_result.payment.id, amount) + ``` + """ + @spec refund(Money.t, String.t(), keyword) :: {:ok | :error, Response} + def refund(amount, payment_id, opts) do + params = create_params_for_refund(amount, opts) + commit(:post, "payments/#{payment_id}/refund", params, opts) + end + + ############################################################################### + # PRIVATE METHODS # + ############################################################################### + + # Makes the request to GlobalCollect's network. + # For consistency with other gateway implementations, make your (final) + # network request in here, and parse it using another private method called + # `respond`. + + defp create_params_for_refund(amount, opts) do + %{ + amountOfMoney: add_money(amount, opts), + customer: add_customer(opts) + } + end + + defp create_params_for_auth_or_purchase(amount, payment, opts) do + %{ + order: add_order(amount, opts), + cardPaymentMethodSpecificInput: add_payment(payment, @brand_map, opts) + } + end + + defp create_params_for_capture(amount, opts) do + %{ + order: add_order(amount, opts) + } + end + + defp add_order(money, options) do + %{ + amountOfMoney: add_money(money, options), + customer: add_customer(options), + references: add_references(options) + } + end + + defp add_money(amount, options) do + {currency, amount, _} = Money.to_integer(amount) + %{ + amount: amount, + currencyCode: currency + } + end + + defp add_customer(options) do + %{ + merchantCustomerId: options[:merchantCustomerId], + personalInformation: personal_info(options), + dateOfBirth: options[:dob], + companyInformation: company_info(options), + billingAddress: options[:billingAddress], + shippingAddress: options[:shippingAddress], + contactDetails: contact(options) + } + end + + defp add_references(options) do + %{ + descriptor: options[:description], + invoiceData: options[:invoice] + } + end + + defp personal_info(options) do + %{ + name: options[:name] + } + end + + defp company_info(options) do + %{ + name: options[:company] + } + end + + defp contact(options) do + %{ + emailAddress: options[:email], + phoneNumber: options[:phone] + } + end + + def add_card(%CreditCard{} = payment) do + %{ + cvv: payment.verification_code, + cardNumber: payment.number, + expiryDate: "#{payment.month}"<>"#{payment.year}", + cardholderName: CreditCard.full_name(payment) + } + end + + defp add_payment(payment, brand_map, opts) do + brand = payment.brand + %{ + paymentProductId: Map.fetch!(brand_map, String.to_atom(brand)), + skipAuthentication: opts[:skipAuthentication], + card: add_card(payment) + } + end + + defp auth_digest(path, secret_api_key, time, opts) do + data = "POST\napplication/json\n#{time}\n/v1/#{opts[:config][:merchant_id]}/#{path}\n" + :crypto.hmac(:sha256, secret_api_key, data) + end + + defp commit(method, path, params, opts) do + headers = create_headers(path, opts) + data = Poison.encode!(params) + url = "#{@base_url}#{opts[:config][:merchant_id]}/#{path}" + response = HTTPoison.request(method, url, data, headers) + response |> respond + end + + defp create_headers(path, opts) do + time = date + sha_signature = auth_digest(path, opts[:config][:secret_api_key], time, opts) |> Base.encode64 + auth_token = "GCS v1HMAC:#{opts[:config][:api_key_id]}:#{sha_signature}" + headers = [{"Content-Type", "application/json"}, {"Authorization", auth_token}, {"Date", time}] + end + + defp date() do + use Timex + datetime = Timex.now |> Timex.local + strftime_str = Timex.format!(datetime, "%a, %d %b %Y %H:%M:%S ", :strftime) + time_zone = Timex.timezone(:local, datetime) + time = strftime_str <>"#{time_zone.abbreviation}" + end + + # Parses GlobalCollect's response and returns a `Gringotts.Response` struct + # in a `:ok`, `:error` tuple. + @spec respond(term) :: {:ok | :error, Response} + defp respond(global_collect_response) + + defp respond({:ok, %{status_code: code, body: body}}) when code in [200, 201] do + case decode(body) do + {:ok, results} -> {:ok, Response.success(raw: results, status_code: code)} + end + end + + defp respond({:ok, %{status_code: status_code, body: body}}) do + {:ok, results} = decode(body) + message = Enum.map(results["errors"],fn (x) -> x["message"] end) + detail = List.to_string(message) + {:error, Response.error(status_code: status_code, message: detail, raw: results)} + end + + defp respond({:error, %HTTPoison.Error{} = error}) do + {:error, Response.error(code: error.id, reason: :network_fail?, description: "HTTPoison says '#{error.reason}'")} + end + +end diff --git a/mix.exs b/mix.exs index 63dcfbe7..7947413c 100644 --- a/mix.exs +++ b/mix.exs @@ -1,6 +1,6 @@ defmodule Gringotts.Mixfile do use Mix.Project - + def project do [ app: :gringotts, @@ -17,9 +17,9 @@ defmodule Gringotts.Mixfile do tool: ExCoveralls ], preferred_cli_env: [ - "coveralls": :test, - "coveralls.detail": :test, - "coveralls.post": :test, + "coveralls": :test, + "coveralls.detail": :test, + "coveralls.post": :test, "coveralls.html": :test, "coveralls.travis": :test ], @@ -32,7 +32,7 @@ defmodule Gringotts.Mixfile do # Type `mix help compile.app` for more information def application do [ - applications: [:httpoison, :hackney, :elixir_xml_to_map], + applications: [:httpoison, :hackney, :elixir_xml_to_map, :timex], mod: {Gringotts.Application, []} ] end @@ -50,7 +50,7 @@ defmodule Gringotts.Mixfile do [ {:poison, "~> 3.1.0"}, {:httpoison, "~> 0.13"}, - {:xml_builder, "~> 0.1.1"}, + {:xml_builder, "~> 0.1.1"}, {:elixir_xml_to_map, "~> 0.1"}, # Money related @@ -67,14 +67,15 @@ defmodule Gringotts.Mixfile do # various analyses tools {:credo, "~> 0.3", only: [:dev, :test]}, {:inch_ex, "~> 0.5", only: :docs}, - {:dialyxir, "~> 0.3", only: :dev} + {:dialyxir, "~> 0.3", only: :dev}, + {:timex, "~> 3.1"} ] end defp description do """ - Gringotts is a payment processing library in Elixir integrating - various payment gateways, this draws motivation for shopify's + Gringotts is a payment processing library in Elixir integrating + various payment gateways, this draws motivation for shopify's activemerchant ruby gem. """ end @@ -92,5 +93,5 @@ defmodule Gringotts.Mixfile do [ "Gateways": ~r/^Gringotts.Gateways.?/, ] - end + end end diff --git a/test/gateways/global_collect_test.exs b/test/gateways/global_collect_test.exs new file mode 100644 index 00000000..87b0d702 --- /dev/null +++ b/test/gateways/global_collect_test.exs @@ -0,0 +1,207 @@ +defmodule Gringotts.Gateways.GlobalCollectTest do + + Code.require_file "../mocks/global_collect_mock.exs", __DIR__ + use ExUnit.Case, async: false + alias Gringotts.Gateways.GlobalCollectMock, as: MockResponse + alias Gringotts.Gateways.GlobalCollect + alias Gringotts.{ + CreditCard + } + + import Mock + + @amount Money.new("500", :USD) + + @bad_amount Money.new("50.3", :USD) + + @shippingAddress %{ + street: "Desertroad", + houseNumber: "1", + additionalInfo: "Suite II", + zip: "84536", + city: "Monument Valley", + state: "Utah", + countryCode: "US" + } + + @valid_card %CreditCard{ + number: "4567350000427977", + month: 12, + year: 18, + first_name: "John", + last_name: "Doe", + verification_code: "123", + brand: "visa" + } + + @invalid_card %CreditCard{ + number: "4567350000427977", + month: 12, + year: 10, + first_name: "John", + last_name: "Doe", + verification_code: "123", + brand: "visa" + } + + @billingAddress %{ + street: "Desertroad", + houseNumber: "13", + additionalInfo: "b", + zip: "84536", + city: "Monument Valley", + state: "Utah", + countryCode: "US" + } + + @invoice %{ + invoiceNumber: "000000123", + invoiceDate: "20140306191500" + } + + @name %{ + title: "Miss", + firstName: "Road", + surname: "Runner" + } + + @valid_token "charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b" + + @invalid_token 30 + + @invalid_config [config: %{secret_api_key: "Qtg9v4Q0G13sLRNcClWhHnvN1kVYWDcy4w9rG8T86XU=", api_key_id: "e5743abfc360ed12"}] + + @options [ + config: %{secret_api_key: "Qtg9v4Q0G13sLRNcClWhHnvN1kVYWDcy4w9rG8T86XU=", api_key_id: "e5743abfc360ed12", merchant_id: "1226"}, + description: "Store Purchase 1437598192", + merchantCustomerId: "234", + customer_name: "John Doe", + dob: "19490917", company: "asma", + email: "johndoe@gmail.com", + phone: "7468474533", + order_id: "2323", + invoice: @invoice, + billingAddress: @billingAddress, + shippingAddress: @shippingAddress, + name: @name, skipAuthentication: "true" + ] + + describe "validation arguments check" do + test "with no merchant id passed in config" do + assert_raise ArgumentError, fn -> + GlobalCollect.validate_config(@invalid_config) + end + end + end + + describe "purchase" do + test "with valid card" do + with_mock HTTPoison, + [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_purchase_with_valid_card end] do + {:ok, response} = GlobalCollect.purchase(@amount, @valid_card, @options) + assert response.status_code == 201 + assert response.success == true + assert response.raw["payment"]["statusOutput"]["isAuthorized"] == true + end + end + + + test "with invalid amount" do + with_mock HTTPoison, + [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_purchase_with_invalid_amount end] do + {:error, response} = GlobalCollect.purchase(@bad_amount, @valid_card, @options) + assert response.status_code == 400 + assert response.success == false + assert response.message == "INVALID_VALUE: '50.3' is not a valid value for field 'amount'" + end + end + end + + describe "authorize" do + test "with valid card" do + with_mock HTTPoison, + [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_authorize_with_valid_card end] do + {:ok, response} = GlobalCollect.authorize(@amount, @valid_card, @options) + assert response.status_code == 201 + assert response.success == true + assert response.raw["payment"]["statusOutput"]["isAuthorized"] == true + end + end + + test "with invalid card" do + with_mock HTTPoison, + [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_authorize_with_invalid_card end] do + {:error, response} = GlobalCollect.authorize(@amount, @invalid_card, @options) + assert response.status_code == 400 + assert response.success == false + assert response.message == "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT" + end + end + + test "with invalid amount" do + with_mock HTTPoison, + [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_authorize_with_invalid_amount end] do + {:error, response} = GlobalCollect.authorize(@bad_amount, @valid_card, @options) + assert response.status_code == 400 + assert response.success == false + assert response.message == "INVALID_VALUE: '50.3' is not a valid value for field 'amount'" + end + end + end + + describe "refund" do + test "with refund not enabled for the respective account" do + with_mock HTTPoison, + [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_refund end] do + {:error, response} = GlobalCollect.refund(@amount, @valid_token, @options) + assert response.status_code == 400 + assert response.success == false + assert response.message == "ORDER WITHOUT REFUNDABLE PAYMENTS" + end + end + end + + describe "capture" do + test "with valid payment id" do + with_mock HTTPoison, + [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_capture_with_valid_paymentid end] do + {:ok, response} = GlobalCollect.capture(@valid_token, @amount, @options) + assert response.status_code == 200 + assert response.success == true + assert response.raw["payment"]["status"] == "CAPTURE_REQUESTED" + end + end + + test "with invalid payment id" do + with_mock HTTPoison, + [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_capture_with_invalid_paymentid end] do + {:error, response} = GlobalCollect.capture(@invalid_token, @amount, @options) + assert response.status_code == 404 + assert response.success == false + assert response.message == "UNKNOWN_PAYMENT_ID" + end + end + end + + describe "void" do + test "with valid card" do + with_mock HTTPoison, + [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_void_with_valid_card end] do + {:ok, response} = GlobalCollect.void(@valid_token, @options) + assert response.status_code == 200 + assert response.raw["payment"]["status"] == "CANCELLED" + end + end + end + + describe "network failure" do + test "with authorization" do + with_mock HTTPoison, + [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_network_failure end] do + {:error, response} = GlobalCollect.authorize(@amount, @valid_card, @options) + assert response.success == false + assert response.reason == :network_fail? + end + end + end +end diff --git a/test/mocks/global_collect_mock.exs b/test/mocks/global_collect_mock.exs new file mode 100644 index 00000000..8bb9d553 --- /dev/null +++ b/test/mocks/global_collect_mock.exs @@ -0,0 +1,182 @@ +defmodule Gringotts.Gateways.GlobalCollectMock do + + def test_for_purchase_with_valid_card do + {:ok, + %HTTPoison.Response{ + body: "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000074\",\n \"externalReference\" : \"000000122600000000740000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000740000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20180118135349\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", + headers: [{"Date", "Thu, 18 Jan 2018 12:53:49 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"Location", + "https://api-sandbox.globalcollect.com:443/v1/1226/payments/000000122600000000740000100001"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 201 + } + } + end + + def test_for_purchase_with_invalid_card do + {:ok, + %HTTPoison.Response{ + body: "{\n \"errorId\" : \"363899bd-acfb-4452-bbb0-741c0df6b4b8\",\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"980825\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000075\",\n \"externalReference\" : \"000000122600000000750000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000750000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"546247\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20180118135651\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n }\n}", + headers: [ + {"Date", "Thu, 18 Jan 2018 12:56:51 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 400 + } + } + end + + def test_for_purchase_with_invalid_amount do + {:ok, + %HTTPoison.Response{ + body: "{\n \"errorId\" : \"8c34dc0b-776c-44e3-8cd4-b36222960153\",\n \"errors\" : [ {\n \"code\" : \"1099\",\n \"id\" : \"INVALID_VALUE\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"message\" : \"INVALID_VALUE: '50.3' is not a valid value for field 'amount'\",\n \"httpStatusCode\" : 400\n } ]\n}", + headers: [ + {"Date", "Wed, 24 Jan 2018 07:16:06 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"}], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 400 + } + } + end + + def test_for_authorize_with_valid_card do + {:ok, + %HTTPoison.Response{ + body: "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000065\",\n \"externalReference\" : \"000000122600000000650000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000650000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20180118110419\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", + headers: [ + {"Date", "Thu, 18 Jan 2018 10:04:19 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"Location", + "https://api-sandbox.globalcollect.com:443/v1/1226/payments/000000122600000000650000100001"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 201 + } + } + end + + def test_for_authorize_with_invalid_card do + {:ok, + %HTTPoison.Response{ + body: "{\n \"errorId\" : \"dcdf5c8d-e475-4fbc-ac57-76123c1640a2\",\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"978754\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000066\",\n \"externalReference\" : \"000000122600000000660000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000660000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"978755\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20180118111508\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n }\n}", + headers: [ + {"Date", "Thu, 18 Jan 2018 10:15:08 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 400 + } + } + end + + def test_for_authorize_with_invalid_amount do + {:ok, + %HTTPoison.Response{body: "{\n \"errorId\" : \"1dbef568-ed86-4c8d-a3c3-74ced258d5a2\",\n \"errors\" : [ {\n \"code\" : \"1099\",\n \"id\" : \"INVALID_VALUE\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"message\" : \"INVALID_VALUE: '50.3' is not a valid value for field 'amount'\",\n \"httpStatusCode\" : 400\n } ]\n}", + headers: [ + {"Date", "Tue, 23 Jan 2018 11:18:11 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 400 + } + } + end + + def test_for_refund do + {:ok, + %HTTPoison.Response{ + body: "{\n \"errorId\" : \"b6ba00d2-8f11-4822-8f32-c6d0a4d8793b\",\n \"errors\" : [ {\n \"code\" : \"300450\",\n \"message\" : \"ORDER WITHOUT REFUNDABLE PAYMENTS\",\n \"httpStatusCode\" : 400\n } ]\n}", + headers: [ + {"Date", "Wed, 24 Jan 2018 05:33:56 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000870000100001/refund", + status_code: 400 + } + } + end + + def test_for_capture_with_valid_paymentid do + {:ok, + %HTTPoison.Response{ + body: "{\n \"payment\" : {\n \"id\" : \"000000122600000000650000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 50,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"CAPTURE_REQUESTED\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_CONNECT_OR_3RD_PARTY\",\n \"statusCode\" : 800,\n \"statusCodeChangeDateTime\" : \"20180123140826\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", + headers: [ + {"Date", "Tue, 23 Jan 2018 13:08:26 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000650000100001/approve", + status_code: 200 + } + } + end + + def test_for_capture_with_invalid_paymentid do + {:ok, + %HTTPoison.Response{ + body: "{\n \"errorId\" : \"ccb99804-0240-45b6-bb28-52aaae59d71b\",\n \"errors\" : [ {\n \"code\" : \"1002\",\n \"id\" : \"UNKNOWN_PAYMENT_ID\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"propertyName\" : \"paymentId\",\n \"message\" : \"UNKNOWN_PAYMENT_ID\",\n \"httpStatusCode\" : 404\n } ]\n}", + headers: [ + {"Date", "Tue, 23 Jan 2018 12:25:59 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/30/approve", + status_code: 404 + } + } + end + + def test_for_void_with_valid_card do + {:ok, + %HTTPoison.Response{ + body: "{\n \"payment\" : {\n \"id\" : \"000000122600000000870000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 50,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"CANCELLED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 99999,\n \"statusCodeChangeDateTime\" : \"20180124064204\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n}", + headers: [ + {"Date", "Wed, 24 Jan 2018 05:42:04 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000870000100001/cancel", + status_code: 200 + } + } + end + + def test_for_network_failure do + {:error, %HTTPoison.Error{id: nil, reason: :nxdomain}} + end +end From 99850ab1172e9b2304feaa522607f908e2fe5ce9 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Tue, 6 Feb 2018 17:27:33 +0530 Subject: [PATCH 24/60] Introduces `Response.t` with docs (#119) * Fixes #1: Introducing `Response.t` with docs * [monei] Adapted for new `Response.t` * Refactored `commit`, `respond` for readability * [monei] Updated test cases * Corrected specs * [bogus] Adapted for Response.t --- lib/gringotts/credit_card.ex | 2 +- lib/gringotts/gateways/bogus.ex | 4 +- lib/gringotts/gateways/monei.ex | 128 ++++++++++++----------- lib/gringotts/response.ex | 83 +++++++++++---- mix.lock | 4 + test/gateways/bogus_test.exs | 16 +-- test/gateways/monei_test.exs | 23 ++-- test/integration/gateways/monei_test.exs | 12 +-- 8 files changed, 163 insertions(+), 109 deletions(-) diff --git a/lib/gringotts/credit_card.ex b/lib/gringotts/credit_card.ex index 32a2a50c..01811e00 100644 --- a/lib/gringotts/credit_card.ex +++ b/lib/gringotts/credit_card.ex @@ -1,6 +1,6 @@ defmodule Gringotts.CreditCard do @moduledoc """ - Defines a `Struct` for (credit) cards and some utilities. + Defines a `struct` for (credit) cards and some utilities. """ defstruct [:number, :month, :year, :first_name, :last_name, :verification_code, :brand] diff --git a/lib/gringotts/gateways/bogus.ex b/lib/gringotts/gateways/bogus.ex index 14140f39..de903744 100644 --- a/lib/gringotts/gateways/bogus.ex +++ b/lib/gringotts/gateways/bogus.ex @@ -28,10 +28,10 @@ defmodule Gringotts.Gateways.Bogus do do: success(customer_id) defp success, - do: {:ok, Response.success(authorization: random_string())} + do: {:ok, Response.success(id: random_string())} defp success(id), - do: {:ok, Response.success(authorization: id)} + do: {:ok, Response.success(id: id)} defp random_string(length \\ 10), do: 1..length |> Enum.map(&random_char/1) |> Enum.join diff --git a/lib/gringotts/gateways/monei.ex b/lib/gringotts/gateways/monei.ex index 53ebb130..7eeca41b 100644 --- a/lib/gringotts/gateways/monei.ex +++ b/lib/gringotts/gateways/monei.ex @@ -258,7 +258,7 @@ defmodule Gringotts.Gateways.Monei do iex> auth_result.id # This is the authorization ID iex> auth_result.token # This is the registration ID/token """ - @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def authorize(amount, %CreditCard{} = card, opts) do {currency, value} = Money.to_string(amount) @@ -293,7 +293,7 @@ defmodule Gringotts.Gateways.Monei do iex> amount = %{value: Decimal.new(35), currency: "USD"} iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Monei, amount, auth_result.id, opts) """ - @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} + @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response.t()} def capture(payment_id, amount, opts) def capture(<>, amount, opts) do @@ -329,7 +329,7 @@ defmodule Gringotts.Gateways.Monei do iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.Monei, amount, card, opts) iex> purchase_result.token # This is the registration ID/token """ - @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def purchase(amount, %CreditCard{} = card, opts) do {currency, value} = Money.to_string(amount) @@ -361,7 +361,7 @@ defmodule Gringotts.Gateways.Monei do iex> amount = %{value: Decimal.new(42), currency: "USD"} iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.Monei, purchase_result.id, amount) """ - @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} def refund(amount, <>, opts) do {currency, value} = Money.to_string(amount) @@ -396,7 +396,7 @@ defmodule Gringotts.Gateways.Monei do iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> {:ok, store_result} = Gringotts.store(Gringotts.Gateways.Monei, card, []) """ - @spec store(CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec store(CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def store(%CreditCard{} = card, opts) do params = card_params(card) commit(:post, "registrations", params, opts) @@ -409,7 +409,7 @@ defmodule Gringotts.Gateways.Monei do Deletes previously stored payment-source data. """ - @spec unstore(String.t(), keyword) :: {:ok | :error, Response} + @spec unstore(String.t(), keyword) :: {:ok | :error, Response.t()} def unstore(registration_id, opts) def unstore(<>, opts) do @@ -447,7 +447,7 @@ defmodule Gringotts.Gateways.Monei do iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.Monei, auth_result.id, opts) """ - @spec void(String.t(), keyword) :: {:ok | :error, Response} + @spec void(String.t(), keyword) :: {:ok | :error, Response.t()} def void(payment_id, opts) def void(<>, opts) do @@ -466,72 +466,101 @@ defmodule Gringotts.Gateways.Monei do ] end - # Makes the request to MONEI's network. - @spec commit(atom, String.t(), keyword, keyword) :: {:ok | :error, Response} - defp commit(method, endpoint, params, opts) do - auth_params = [ + defp auth_params(opts) do + [ "authentication.userId": opts[:config][:userId], "authentication.password": opts[:config][:password], "authentication.entityId": opts[:config][:entityId] ] + end + + # Makes the request to MONEI's network. + @spec commit(atom, String.t(), keyword, keyword) :: {:ok | :error, Response.t()} + defp commit(:post, endpoint, params, opts) do url = "#{base_url(opts)}/#{version(opts)}/#{endpoint}" case expand_params(opts, params[:paymentType]) do {:error, reason} -> - {:error, Response.error(description: reason)} + {:error, Response.error(reason: reason)} validated_params -> - network_response = - case method do - :post -> - HTTPoison.post( - url, - {:form, params ++ validated_params ++ auth_params}, - @default_headers - ) - - :delete -> - HTTPoison.delete(url <> "?" <> URI.encode_query(auth_params)) - end - - respond(network_response) + url + |> HTTPoison.post({:form, params ++ validated_params ++ auth_params(opts)}, @default_headers) + |> respond end end + # This clause is only used by `unstore/2` + defp commit(:delete, endpoint, _params, opts) do + base_url = "#{base_url(opts)}/#{version(opts)}/#{endpoint}" + auth_params = auth_params(opts) + query_string = auth_params |> URI.encode_query() + + base_url <> "?" <> query_string + |> HTTPoison.delete() + |> respond + end + # Parses MONEI's response and returns a `Gringotts.Response` struct in a # `:ok`, `:error` tuple. - @spec respond(term) :: {:ok | :error, Response} + @spec respond(term) :: {:ok | :error, Response.t()} defp respond(monei_response) defp respond({:ok, %{status_code: 200, body: body}}) do - case decode(body) do - {:ok, decoded_json} -> - case parse_response(decoded_json) do - {:ok, results} -> {:ok, Response.success([{:id, decoded_json["id"]} | results])} - {:error, errors} -> {:ok, Response.error([{:id, decoded_json["id"]} | errors])} - end + common = [raw: body, status_code: 200] + + with {:ok, decoded_json} <- decode(body), + {:ok, results} <- parse_response(decoded_json) do + {:ok, Response.success(common ++ results)} + else + {:not_ok, errors} -> + {:ok, Response.error(common ++ errors)} {:error, _} -> - {:error, Response.error(raw: body, code: :undefined_response_from_monei)} + {:error, Response.error([reason: "undefined response from monei"] ++ common)} end end defp respond({:ok, %{status_code: status_code, body: body}}) do - {:error, Response.error(code: status_code, raw: body)} + {:error, Response.error(status_code: status_code, raw: body)} end defp respond({:error, %HTTPoison.Error{} = error}) do { :error, Response.error( - code: error.id, reason: "network related failure", - description: "HTTPoison says '#{error.reason}'" + message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]" ) } end + defp parse_response(%{"result" => result} = data) do + {address, zip_code} = @avs_code_translator[result["avsResponse"]] + + results = [ + id: data["id"], + token: data["registrationId"], + gateway_code: result["code"], + message: result["description"], + fraud_review: data["risk"], + cvc_result: @cvc_code_translator[result["cvvResponse"]], + avs_result: %{address: address, zip_code: zip_code} + ] + + non_nil_params = Enum.filter(results, fn {_, v} -> v != nil end) + verify(non_nil_params) + end + + defp verify(results) do + if String.match?(results[:gateway_code], ~r{^(000\.000\.|000\.100\.1|000\.[36])}) do + {:ok, results} + else + {:not_ok, [{:reason, results[:message]} | results]} + end + end + defp expand_params(params, action_type) do Enum.reduce_while(params, [], fn {k, v}, acc -> case k do @@ -587,31 +616,6 @@ defmodule Gringotts.Gateways.Monei do currency in @supported_currencies end - defp parse_response(%{"result" => result} = data) do - {address, zip_code} = @avs_code_translator[result["avsResponse"]] - - results = [ - code: result["code"], - description: result["description"], - risk: data["risk"]["score"], - cvc_result: @cvc_code_translator[result["cvvResponse"]], - avs_result: [address: address, zip_code: zip_code], - raw: data, - token: data["registrationId"] - ] - - filtered = Enum.filter(results, fn {_, v} -> v != nil end) - verify(filtered) - end - - defp verify(results) do - if String.match?(results[:code], ~r{^(000\.000\.|000\.100\.1|000\.[36])}) do - {:ok, results} - else - {:error, [{:reason, results[:description]} | results]} - end - end - defp make(prefix, param) do Enum.into(param, [], fn {k, v} -> {"#{prefix}.#{k}", v} end) end diff --git a/lib/gringotts/response.ex b/lib/gringotts/response.ex index f9097490..ac369f89 100644 --- a/lib/gringotts/response.ex +++ b/lib/gringotts/response.ex @@ -1,26 +1,73 @@ defmodule Gringotts.Response do - @moduledoc ~S""" - Module which defines the struct for response struct. - - Response struct is a standard response from public API to the application. - - It mostly has such as:- - * `success`: boolean indicating the status of the transaction - * `authorization`: token which is used to issue requests without the card info - * `status_code`: response code - * `error_code`: error code if there is error else nil - * `message`: message related to the status of the response - * `avs_result`: result for address verfication - * `cvc_result`: result for cvc verification - * `params`: original raw response from the gateway - * `fraud_review`: information related to fraudulent transactions + @moduledoc """ + Defines the Response `struct` and some utilities. + + All `Gringotts` public API calls will return a `Response.t` wrapped in an + `:ok` or `:error` `tuple`. It is guaranteed that an `:ok` will be returned + only when the request succeeds at the gateway, ie, no error occurs. """ - + defstruct [ - :success, :authorization, :status_code, :error_code, :message, - :avs_result, :cvc_result, :params, :fraud_review + :success, :id, :token, :status_code, :gateway_code, :reason, :message, + :avs_result, :cvc_result, :raw, :fraud_review ] + @typedoc """ + The standard Response from `Gringotts`. + + | Field | Type | Description | + |----------------|-------------------|---------------------------------------| + | `success` | `boolean` | Indicates the status of the\ + transaction. | + | `id` | `String.t` | Gateway supplied identifier of the\ + transaction. | + | `token` | `String.t` | Gateway supplied `token`. _This is\ + different from `Response.id`_. | + | `status_code` | `non_neg_integer` | `HTTP` response code. | + | `gateway_code` | `String.t` | Gateway's response code "as-is". | + | `message` | `String.t` | String describing the response status.| + | `avs_result` | `map` | Address Verification Result.\ + Schema: `%{street: String.t,\ + zip_code: String.t}` | + | `cvc_result` | `String.t` | Result of the [CVC][cvc] validation. | + | `reason` | `String.t` | Explain the `reason` of error, in\ + case of error. `nil` otherwise. | + | `raw` | `String.t` | Raw response from the gateway. | + | `fraud_review` | `term` | Gateway's risk assessment of the\ + transaction. | + + ## Notes + + 1. It is not guaranteed that all fields will be populated for all calls, and + some gateways might insert non-standard fields. Please refer the Gateways' + docs for that information. + + 2. `success` is deprecated in `v1.1.0` and will be removed in `v1.2.0`. + + 3. For some actions the Gateway returns an additional token, say as reponse to + a customer tokenization/registration. In such cases the `id` is not + useable because it refers to the transaction, the `token` is. + + > On the other hand for authorizations or captures, there's no `token`. + + 4. The schema of `fraud_review` is Gateway specific. + + [cvc]: https://en.wikipedia.org/wiki/Card_security_code + """ + @type t:: %__MODULE__{ + success: boolean, + id: String.t, + token: String.t, + status_code: non_neg_integer, + gateway_code: String.t, + reason: String.t, + message: String.t, + avs_result: %{street: String.t, zip_code: String.t}, + cvc_result: String.t, + raw: String.t, + fraud_review: term + } + def success(opts \\ []) do new(true, opts) end diff --git a/mix.lock b/mix.lock index df5aabfe..e3441847 100644 --- a/mix.lock +++ b/mix.lock @@ -2,6 +2,7 @@ "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "bypass": {:hex, :bypass, "0.8.1", "16d409e05530ece4a72fabcf021a3e5c7e15dcc77f911423196a0c551f2a15ca", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, + "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, "credo": {:hex, :credo, "0.8.10", "261862bb7363247762e1063713bb85df2bbd84af8d8610d1272cd9c1943bba63", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, @@ -16,6 +17,7 @@ "ex_money": {:hex, :ex_money, "1.1.2", "4336192f1ac263900dfb4f63c1f71bc36a7cdee5d900e81937d3213be3360f9f", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}, {:ex_cldr, "~> 1.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 1.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}], "hexpm"}, "excoveralls": {:hex, :excoveralls, "0.8.0", "99d2691d3edf8612f128be3f9869c4d44b91c67cec92186ce49470ae7a7404cf", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, + "gettext": {:hex, :gettext, "0.14.0", "1a019a2e51d5ad3d126efe166dcdf6563768e5d06c32a99ad2281a1fa94b4c72", [:mix], [], "hexpm"}, "hackney": {:hex, :hackney, "1.10.1", "c38d0ca52ea80254936a32c45bb7eb414e7a96a521b4ce76d00a69753b157f21", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "httpoison": {:hex, :httpoison, "0.13.0", "bfaf44d9f133a6599886720f3937a7699466d23bb0cd7a88b6ba011f53c6f562", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, @@ -30,5 +32,7 @@ "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, + "timex": {:hex, :timex, "3.1.25", "6002dae5432f749d1c93e2cd103eb73cecb53e50d2c885349e8e4146fc96bd44", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, + "tzdata": {:hex, :tzdata, "0.5.16", "13424d3afc76c68ff607f2df966c0ab4f3258859bbe3c979c9ed1606135e7352", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, "xml_builder": {:hex, :xml_builder, "0.1.2", "b48ab9ed0a24f43a6061e0c21deda88b966a2121af5c445d4fc550dd822e23dc", [:mix], [], "hexpm"}} diff --git a/test/gateways/bogus_test.exs b/test/gateways/bogus_test.exs index b7c10ebd..5810dfc0 100644 --- a/test/gateways/bogus_test.exs +++ b/test/gateways/bogus_test.exs @@ -5,35 +5,35 @@ defmodule Gringotts.Gateways.BogusTest do alias Gringotts.Gateways.Bogus, as: Gateway test "authorize" do - {:ok, %Response{authorization: authorization, success: success}} = + {:ok, %Response{id: id, success: success}} = Gateway.authorize(10.95, :card, []) assert success - assert authorization != nil + assert id != nil end test "purchase" do - {:ok, %Response{authorization: authorization, success: success}} = + {:ok, %Response{id: id, success: success}} = Gateway.purchase(10.95, :card, []) assert success - assert authorization != nil + assert id != nil end test "capture" do - {:ok, %Response{authorization: authorization, success: success}} = + {:ok, %Response{id: id, success: success}} = Gateway.capture(1234, 5, []) assert success - assert authorization != nil + assert id != nil end test "void" do - {:ok, %Response{authorization: authorization, success: success}} = + {:ok, %Response{id: id, success: success}} = Gateway.void(1234, []) assert success - assert authorization != nil + assert id != nil end test "store" do diff --git a/test/gateways/monei_test.exs b/test/gateways/monei_test.exs index 9fd72895..8d3a11ec 100644 --- a/test/gateways/monei_test.exs +++ b/test/gateways/monei_test.exs @@ -119,7 +119,7 @@ defmodule Gringotts.Gateways.MoneiTest do describe "core" do test "with unsupported currency.", %{auth: auth} do {:error, response} = Gateway.authorize(@bad_currency, @card, config: auth) - assert response.description == "Invalid currency" + assert response.reason == "Invalid currency" end test "when MONEI is down or unreachable.", %{bypass: bypass, auth: auth} do @@ -161,7 +161,7 @@ defmodule Gringotts.Gateways.MoneiTest do opts = randoms ++ @extra_opts ++ [config: auth] {:ok, response} = Gateway.purchase(@amount42, @card, opts) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" assert response.token == "8a82944a60e09c550160e92da144491e" end @@ -181,7 +181,7 @@ defmodule Gringotts.Gateways.MoneiTest do end) {:ok, response} = Gateway.authorize(@amount42, @card, config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end end @@ -192,7 +192,7 @@ defmodule Gringotts.Gateways.MoneiTest do end) {:ok, response} = Gateway.purchase(@amount42, @card, config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end test "with createRegistration.", %{bypass: bypass, auth: auth} do @@ -203,7 +203,7 @@ defmodule Gringotts.Gateways.MoneiTest do end) {:ok, response} = Gateway.purchase(@amount42, @card, register: true, config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" assert response.token == "8a82944a60e09c550160e92da144491e" end end @@ -215,8 +215,7 @@ defmodule Gringotts.Gateways.MoneiTest do end) {:ok, response} = Gateway.store(@card, config: auth) - assert response.code == "000.100.110" - assert response.raw["card"]["holder"] == "Jo Doe" + assert response.gateway_code == "000.100.110" end end @@ -234,7 +233,7 @@ defmodule Gringotts.Gateways.MoneiTest do {:ok, response} = Gateway.capture("7214344242e11af79c0b9e7b4f3f6234", @amount42, config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end test "with createRegistration that is ignored", %{bypass: bypass, auth: auth} do @@ -257,7 +256,7 @@ defmodule Gringotts.Gateways.MoneiTest do config: auth ) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end end @@ -274,7 +273,7 @@ defmodule Gringotts.Gateways.MoneiTest do {:ok, response} = Gateway.refund(@amount3, "7214344242e11af79c0b9e7b4f3f6234", config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end end @@ -290,7 +289,7 @@ defmodule Gringotts.Gateways.MoneiTest do ) {:error, response} = Gateway.unstore("7214344242e11af79c0b9e7b4f3f6234", config: auth) - assert response.code == :undefined_response_from_monei + assert response.reason == "undefined response from monei" end end @@ -306,7 +305,7 @@ defmodule Gringotts.Gateways.MoneiTest do ) {:ok, response} = Gateway.void("7214344242e11af79c0b9e7b4f3f6234", config: auth) - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" end end diff --git a/test/integration/gateways/monei_test.exs b/test/integration/gateways/monei_test.exs index 6619a43f..3066ff62 100644 --- a/test/integration/gateways/monei_test.exs +++ b/test/integration/gateways/monei_test.exs @@ -85,9 +85,9 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do test "authorize", %{opts: opts} do case Gringotts.authorize(Gateway, @amount, @card, opts) do {:ok, response} -> - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" - assert response.description == + assert response.message == "Request successfully processed in 'Merchant in Integrator Test Mode'" assert String.length(response.id) == 32 @@ -101,9 +101,9 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do test "capture", %{opts: _opts} do case Gringotts.capture(Gateway, "s", @amount) do {:ok, response} -> - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" - assert response.description == + assert response.message == "Request successfully processed in 'Merchant in Integrator Test Mode'" assert String.length(response.id) == 32 @@ -116,9 +116,9 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do test "purchase", %{opts: opts} do case Gringotts.purchase(Gateway, @amount, @card, opts) do {:ok, response} -> - assert response.code == "000.100.110" + assert response.gateway_code == "000.100.110" - assert response.description == + assert response.message == "Request successfully processed in 'Merchant in Integrator Test Mode'" assert String.length(response.id) == 32 From 1920ced84bd481f7e7e2ee62d5478c73b8ee814d Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Mon, 5 Feb 2018 14:42:10 +0530 Subject: [PATCH 25/60] Removes payment worker. * Gringotts does not start any process now * Removed `adapter` key from config as it was redundant. * Updated docs and mix task. --- README.md | 10 --- lib/gringotts.ex | 38 +++++----- lib/gringotts/adapter.ex | 2 +- lib/gringotts/application.ex | 33 --------- lib/gringotts/gateways/authorize_net.ex | 1 - lib/gringotts/gateways/cams.ex | 1 - lib/gringotts/gateways/global_collect.ex | 1 - lib/gringotts/gateways/monei.ex | 1 - lib/gringotts/gateways/paymill.ex | 1 - lib/gringotts/gateways/stripe.ex | 1 - lib/gringotts/gateways/trexle.ex | 1 - lib/gringotts/worker.ex | 89 ------------------------ mix.exs | 1 - templates/gateway.eex | 1 - templates/integration.eex | 3 +- test/gateways/cams_test.exs | 5 +- test/gringotts_test.exs | 5 +- test/integration/gateways/monei_test.exs | 1 - 18 files changed, 23 insertions(+), 172 deletions(-) delete mode 100644 lib/gringotts/application.ex delete mode 100644 lib/gringotts/worker.ex diff --git a/README.md b/README.md index 42e56c2f..6b71deb4 100644 --- a/README.md +++ b/README.md @@ -33,15 +33,6 @@ def deps do end ``` -Add gringotts to the list of applications to be started. -```elixir -def application do - [ - extra_applications: [:gringotts] - ] -end -``` - ## Usage This simple example demonstrates how a purchase can be made using a person's credit card details. @@ -50,7 +41,6 @@ Add configs in `config/config.exs` file. ```elixir config :gringotts, Gringotts.Gateways.Monei, - adapter: Gringotts.Gateways.Monei, userId: "your_secret_user_id", password: "your_secret_password", entityId: "your_secret_channel_id" diff --git a/lib/gringotts.ex b/lib/gringotts.ex index 13d0eb07..d4f9e65e 100644 --- a/lib/gringotts.ex +++ b/lib/gringotts.ex @@ -133,13 +133,8 @@ defmodule Gringotts do following format: config :gringotts, Gringotts.Gateways.XYZ, - adapter: Gringotts.Gateways.XYZ, # some_documented_key: associated_value # some_other_key: another_value - - > ***Note!*** - > The config key matches the `:adapter`! Both ***must*** be the Gateway module - > name! """ import GenServer, only: [call: 2] @@ -166,8 +161,8 @@ defmodule Gringotts do {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.XYZ, amount, card, opts) """ def authorize(gateway, amount, card, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:authorize, gateway, amount, card, opts}) + config = get_and_validate_config(gateway) + gateway.authorize(amount, card, [{:config, config} | opts]) end @doc """ @@ -189,8 +184,8 @@ defmodule Gringotts do Gringotts.capture(Gringotts.Gateways.XYZ, amount, auth_result.id, opts) """ def capture(gateway, id, amount, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:capture, gateway, id, amount, opts}) + config = get_and_validate_config(gateway) + gateway.capture(id, amount, [{:config, config} | opts]) end @doc """ @@ -217,8 +212,8 @@ defmodule Gringotts do Gringotts.purchase(Gringotts.Gateways.XYZ, amount, card, opts) """ def purchase(gateway, amount, card, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:purchase, gateway, amount, card, opts}) + config = get_and_validate_config(gateway) + gateway.purchase(amount, card, [{:config, config} | opts]) end @doc """ @@ -237,8 +232,8 @@ defmodule Gringotts do Gringotts.purchase(Gringotts.Gateways.XYZ, amount, id, opts) """ def refund(gateway, amount, id, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:refund, gateway, amount, id, opts}) + config = get_and_validate_config(gateway) + gateway.refund(amount, id, [{:config, config} | opts]) end @doc """ @@ -258,8 +253,8 @@ defmodule Gringotts do Gringotts.store(Gringotts.Gateways.XYZ, card, opts) """ def store(gateway, card, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:store, gateway, card, opts}) + config = get_and_validate_config(gateway) + gateway.store(card, [{:config, config} | opts]) end @doc """ @@ -276,8 +271,8 @@ defmodule Gringotts do Gringotts.unstore(Gringotts.Gateways.XYZ, token) """ def unstore(gateway, token, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:unstore, gateway, token, opts}) + config = get_and_validate_config(gateway) + gateway.unstore(token, [{:config, config} | opts]) end @doc """ @@ -297,13 +292,16 @@ defmodule Gringotts do Gringotts.void(Gringotts.Gateways.XYZ, id, opts) """ def void(gateway, id, opts \\ []) do - validate_config(gateway) - call(:payment_worker, {:void, gateway, id, opts}) + config = get_and_validate_config(gateway) + gateway.void(id, [{:config, config} | opts]) end - defp validate_config(gateway) do + defp get_and_validate_config(gateway) do # Keep the key name and adapter the same in the config in application config = Application.get_env(:gringotts, gateway) + # The following call to validate_config might raise an error gateway.validate_config(config) + global_config = Application.get_env(:gringotts, :global_config) || [mode: :test] + Keyword.merge(global_config, config) end end diff --git a/lib/gringotts/adapter.ex b/lib/gringotts/adapter.ex index d6e250b6..8d2a24f8 100644 --- a/lib/gringotts/adapter.ex +++ b/lib/gringotts/adapter.ex @@ -31,4 +31,4 @@ defmodule Gringotts.Adapter do end end end -end \ No newline at end of file +end diff --git a/lib/gringotts/application.ex b/lib/gringotts/application.ex deleted file mode 100644 index f852de3b..00000000 --- a/lib/gringotts/application.ex +++ /dev/null @@ -1,33 +0,0 @@ -defmodule Gringotts.Application do - @moduledoc ~S""" - Has the supervision tree which monitors all the workers - that are handling the payments. - """ - use Application - - def start(_type, _args) do - import Supervisor.Spec, warn: false - app_config = Application.get_all_env(:gringotts) - adapters = Enum.filter(app_config, fn({_, klist}) -> klist != [] end) - |> Enum.map(fn({_, klist}) -> Keyword.get(klist, :adapter) end) - - children = [ - # Define workers and child supervisors to be supervised - # worker(Gringotts.Worker, [arg1, arg2, arg3]) - worker( - Gringotts.Worker, - [ - adapters, # gateways - app_config, # options(config from application) - # Since we just have one worker handling all the incoming - # requests so this name remains fixed - [name: :payment_worker] - ]) - ] - - # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html - # for other strategies and supported options - opts = [strategy: :one_for_one, name: Gringotts.Supervisor] - Supervisor.start_link(children, opts) - end -end diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index 04762db3..5e2b57fd 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -65,7 +65,6 @@ defmodule Gringotts.Gateways.AuthorizeNet do fields** and would look something like this: config :gringotts, Gringotts.Gateways.AuthorizeNet, - adapter: Gringotts.Gateways.AuthorizeNet, name: "name_provided_by_authorize_net", transaction_key: "transactionKey_provided_by_authorize_net" diff --git a/lib/gringotts/gateways/cams.ex b/lib/gringotts/gateways/cams.ex index 7e37dd05..1787e947 100644 --- a/lib/gringotts/gateways/cams.ex +++ b/lib/gringotts/gateways/cams.ex @@ -56,7 +56,6 @@ defmodule Gringotts.Gateways.Cams do > fields** and would look something like this: config :gringotts, Gringotts.Gateways.Cams, - adapter: Gringotts.Gateways.Cams, username: "your_secret_user_name", password: "your_secret_password", diff --git a/lib/gringotts/gateways/global_collect.ex b/lib/gringotts/gateways/global_collect.ex index 0a2010e6..0c6d143e 100644 --- a/lib/gringotts/gateways/global_collect.ex +++ b/lib/gringotts/gateways/global_collect.ex @@ -51,7 +51,6 @@ defmodule Gringotts.Gateways.GlobalCollect do something like this: config :gringotts, Gringotts.Gateways.GlobalCollect, - adapter: Gringotts.Gateways.GlobalCollect, secret_api_key: "your_secret_secret_api_key" api_key_id: "your_secret_api_key_id" merchant_id: "your_secret_merchant_id" diff --git a/lib/gringotts/gateways/monei.ex b/lib/gringotts/gateways/monei.ex index 7eeca41b..c297a1a3 100644 --- a/lib/gringotts/gateways/monei.ex +++ b/lib/gringotts/gateways/monei.ex @@ -73,7 +73,6 @@ defmodule Gringotts.Gateways.Monei do fields** and would look something like this: config :gringotts, Gringotts.Gateways.Monei, - adapter: Gringotts.Gateways.Monei, userId: "your_secret_user_id", password: "your_secret_password", entityId: "your_secret_channel_id" diff --git a/lib/gringotts/gateways/paymill.ex b/lib/gringotts/gateways/paymill.ex index 7a05bca1..8b00ba3e 100644 --- a/lib/gringotts/gateways/paymill.ex +++ b/lib/gringotts/gateways/paymill.ex @@ -22,7 +22,6 @@ defmodule Gringotts.Gateways.Paymill do Your application config must include 'private_key', 'public_key' config :gringotts, Gringotts.Gateways.Paymill, - adapter: Gringotts.Gateways.Paymill, private_key: "your_privat_key", public_key: "your_public_key" """ diff --git a/lib/gringotts/gateways/stripe.ex b/lib/gringotts/gateways/stripe.ex index 1a0d2b89..7c1befd2 100644 --- a/lib/gringotts/gateways/stripe.ex +++ b/lib/gringotts/gateways/stripe.ex @@ -51,7 +51,6 @@ defmodule Gringotts.Gateways.Stripe do Your Application config must look something like this: config :gringotts, Gringotts.Gateways.Stripe, - adapter: Gringotts.Gateways.Stripe, secret_key: "your_secret_key", default_currency: "usd" """ diff --git a/lib/gringotts/gateways/trexle.ex b/lib/gringotts/gateways/trexle.ex index d89d7767..fba77838 100644 --- a/lib/gringotts/gateways/trexle.ex +++ b/lib/gringotts/gateways/trexle.ex @@ -41,7 +41,6 @@ defmodule Gringotts.Gateways.Trexle do Your Application config must look something like this: config :gringotts, Gringotts.Gateways.Trexle, - adapter: Gringotts.Gateways.Trexle, api_key: "your-secret-API-key" [dashboard]: https://trexle.com/dashboard/ diff --git a/lib/gringotts/worker.ex b/lib/gringotts/worker.ex deleted file mode 100644 index 786ad6ed..00000000 --- a/lib/gringotts/worker.ex +++ /dev/null @@ -1,89 +0,0 @@ -defmodule Gringotts.Worker do - @moduledoc ~S""" - A central supervised worker handling all the calls for different gateways - - It's main task is to re-route the requests to the respective gateway methods. - - State for this worker currently is:- - * `gateways`:- a list of all the gateways configured in the application. - * `all_configs`:- All the configurations for all the gateways that are configured. - """ - use GenServer - - def start_link(gateways, all_config, opts \\ []) do - GenServer.start_link(__MODULE__, [gateways, all_config], opts) - end - - def init([gateways, all_config]) do - {:ok, %{configs: all_config, gateways: gateways}} - end - - @doc """ - Handles call for `authorize` method - """ - def handle_call({:authorize, gateway, amount, card, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.authorize(amount, card, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for `purchase` method - """ - def handle_call({:purchase, gateway, amount, card, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.purchase(amount, card, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for `capture` method - """ - def handle_call({:capture, gateway, id, amount, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.capture(id, amount, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for `void` method - """ - def handle_call({:void, gateway, id, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.void(id, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for 'refund' method - """ - def handle_call({:refund, gateway, amount, id, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.refund(amount, id, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for `store` method - """ - def handle_call({:store, gateway, card, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.store(card, [{:config, config} | opts]) - {:reply, response, state} - end - - @doc """ - Handles call for 'unstore' method - """ - def handle_call({:unstore, gateway, customer_id, opts}, _from, state) do - {gateway, config} = set_gateway_and_config(gateway) - response = gateway.unstore(customer_id, [{:config, config} | opts]) - {:reply, response, state} - end - - defp set_gateway_and_config(request_gateway) do - global_config = Application.get_env(:gringotts, :global_config) || [mode: :test] - gateway_config = Application.get_env(:gringotts, request_gateway) - {request_gateway, Keyword.merge(global_config, gateway_config)} - end -end diff --git a/mix.exs b/mix.exs index 7947413c..36d86864 100644 --- a/mix.exs +++ b/mix.exs @@ -33,7 +33,6 @@ defmodule Gringotts.Mixfile do def application do [ applications: [:httpoison, :hackney, :elixir_xml_to_map, :timex], - mod: {Gringotts.Application, []} ] end diff --git a/templates/gateway.eex b/templates/gateway.eex index b68e0a31..6169bae4 100644 --- a/templates/gateway.eex +++ b/templates/gateway.eex @@ -50,7 +50,6 @@ defmodule Gringotts.Gateways.<%= gateway_module %> do > something like this: > > config :gringotts, Gringotts.Gateways.<%= gateway_module %>, - > adapter: Gringotts.Gateways.<%= gateway_module %><%= if required_config_keys != [] do %>,<% end %> <%= for key <- required_config_keys do %>> <%= "#{key}" %>: "your_secret_<%= "#{key}" %>" <% end %> diff --git a/templates/integration.eex b/templates/integration.eex index f57b4b86..74bc5fc7 100644 --- a/templates/integration.eex +++ b/templates/integration.eex @@ -8,8 +8,7 @@ defmodule Gringotts.Integration.Gateways.<%= gateway_module <> "Test"%> do setup_all do Application.put_env(:gringotts, Gringotts.Gateways.<%= gateway_module%>, - [ - adapter: Gringotts.Gateways.<%= gateway_module%><%= if required_config_keys != [] do %>,<%= for key <- Enum.intersperse(required_config_keys, ",") do %><%= if key === "," do %><%= "#{key}" %><% else %> + [ <%= if required_config_keys == [] do %># some_key: "some_secret_key"<% else %><%= for key <- Enum.intersperse(required_config_keys, ",") do %><%= if key === "," do %><%= "#{key}" %><% else %> <%= "#{key}" %>: "your_secret_<%= "#{key}" %>"<% end %><% end %><% end %> ] ) diff --git a/test/gateways/cams_test.exs b/test/gateways/cams_test.exs index 3c20e545..6faf56b2 100644 --- a/test/gateways/cams_test.exs +++ b/test/gateways/cams_test.exs @@ -57,10 +57,7 @@ defmodule Gringotts.Gateways.CamsTest do @bad_authorization "some_fake_transaction_id" setup_all do - Application.put_env( - :gringotts, - Gateway, - adapter: Gateway, + Application.put_env(:gringotts, Gateway, username: "some_secret_user_name", password: "some_secret_password" ) diff --git a/test/gringotts_test.exs b/test/gringotts_test.exs index d4634e14..7d9f7681 100644 --- a/test/gringotts_test.exs +++ b/test/gringotts_test.exs @@ -4,12 +4,11 @@ defmodule GringottsTest do import Gringotts @test_config [ - adapter: GringottsTest.FakeGateway, some_auth_info: :merchant_secret_key, other_secret: :sun_rises_in_the_east ] - @bad_config [adapter: GringottsTest.FakeGateway, some_auth_info: :merchant_secret_key] + @bad_config [some_auth_info: :merchant_secret_key] defmodule FakeGateway do use Gringotts.Adapter, required_config: [:some_auth_info, :other_secret] @@ -83,7 +82,7 @@ defmodule GringottsTest do assert_raise( ArgumentError, - "expected [:other_secret] to be set, got: [adapter: GringottsTest.FakeGateway, some_auth_info: :merchant_secret_key]\n", + "expected [:other_secret] to be set, got: [some_auth_info: :merchant_secret_key]\n", fn -> authorize(GringottsTest.FakeGateway, 100, :card, []) end ) end diff --git a/test/integration/gateways/monei_test.exs b/test/integration/gateways/monei_test.exs index 3066ff62..8ed929e4 100644 --- a/test/integration/gateways/monei_test.exs +++ b/test/integration/gateways/monei_test.exs @@ -66,7 +66,6 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do Application.put_env( :gringotts, Gringotts.Gateways.Monei, - adapter: Gringotts.Gateways.Monei, userId: "8a8294186003c900016010a285582e0a", password: "hMkqf2qbWf", entityId: "8a82941760036820016010a28a8337f6" From 8622ded4b44a7990c0787939f5fd30d633b83e14 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Mon, 5 Feb 2018 17:38:25 +0530 Subject: [PATCH 26/60] Removed offending (now useless) test case --- test/integration/gateways/monei_test.exs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/integration/gateways/monei_test.exs b/test/integration/gateways/monei_test.exs index 8ed929e4..c9b67aad 100644 --- a/test/integration/gateways/monei_test.exs +++ b/test/integration/gateways/monei_test.exs @@ -126,9 +126,4 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do flunk() end end - - test "Environment setup" do - config = Application.get_env(:gringotts, Gringotts.Gateways.Monei) - assert config[:adapter] == Gringotts.Gateways.Monei - end end From 6aa6a868ecaf2bd8e459bef4a0daf9fdeb49f878 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Thu, 15 Feb 2018 13:00:33 +0530 Subject: [PATCH 27/60] Removed GenServer import. Fixes #8. --- lib/gringotts.ex | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/gringotts.ex b/lib/gringotts.ex index d4f9e65e..11bda944 100644 --- a/lib/gringotts.ex +++ b/lib/gringotts.ex @@ -137,8 +137,6 @@ defmodule Gringotts do # some_other_key: another_value """ - import GenServer, only: [call: 2] - @doc """ Performs a (pre) Authorize operation. From 3768649e58f03f5d395e205bd3e440d821f36907 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Fri, 9 Feb 2018 17:21:41 +0530 Subject: [PATCH 28/60] Refactored ResponseHandler, for new Response.t - Updated dependency `xml_builder`. The new `generate/2` provides a `format: :none | :indented` option. - `:format` is set to `:none` to produce "minified" network requests. * This is almost a complete rewrite to reduce code duplication. - check_response_type() was acting as guard that matched only against some response types. It did not handle the scenario when a non-supported response would be obtained. It really served no purpose - check_response_type -> extract_gateway_response + This guards as well as fetches, previously the fetch was being done multiple times. * Moved all response handling inside the `ResponseHandler`. * Since we now have a struct, and want to deprecate `:success`, `Response.success/1` and `Response.error/1`, helpers now act on structs. * `errorCode` and `errorText` are used to build `:reason`. + Removed pointless asserts from tests. --- lib/gringotts/gateways/authorize_net.ex | 187 ++++++++++++------------ mix.exs | 2 +- mix.lock | 2 +- test/gateways/authorize_net_test.exs | 19 +-- 4 files changed, 96 insertions(+), 114 deletions(-) diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index 5e2b57fd..1504eea2 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -101,7 +101,6 @@ defmodule Gringotts.Gateways.AuthorizeNet do """ import XmlBuilder - import XmlToMap use Gringotts.Gateways.Base use Gringotts.Adapter, required_config: [:name, :transaction_key] @@ -397,9 +396,9 @@ defmodule Gringotts.Gateways.AuthorizeNet do def store(card, opts) do request_data = if opts[:customer_profile_id] do - card |> create_customer_payment_profile(opts) |> generate + card |> create_customer_payment_profile(opts) |> generate(format: :none) else - card |> create_customer_profile(opts) |> generate + card |> create_customer_profile(opts) |> generate(format: :none) end response_data = commit(:post, request_data, opts) @@ -420,42 +419,32 @@ defmodule Gringotts.Gateways.AuthorizeNet do @spec unstore(String.t(), Keyword.t()) :: {:ok | :error, Response} def unstore(customer_profile_id, opts) do - request_data = customer_profile_id |> delete_customer_profile(opts) |> generate + request_data = customer_profile_id |> delete_customer_profile(opts) |> generate(format: :none) response_data = commit(:post, request_data, opts) respond(response_data) end - # method to make the api request with params + # method to make the API request with params defp commit(method, payload, opts) do path = base_url(opts) headers = @header HTTPoison.request(method, path, payload, headers) end - # Function to return a response - defp respond({:ok, %{body: body, status_code: 200}}) do - raw_response = naive_map(body) - response_type = ResponseHandler.check_response_type(raw_response) - response_check(raw_response[response_type], raw_response) - end + defp respond({:ok, %{body: body, status_code: 200}}), do: ResponseHandler.respond(body) defp respond({:ok, %{body: body, status_code: code}}) do - {:error, Response.error(params: body, error_code: code)} + {:error, %Response{raw: body, status_code: code}} end defp respond({:error, %HTTPoison.Error{} = error}) do - {:error, Response.error(error_code: error.id, message: "HTTPoison says '#{error.reason}'")} - end - - # Functions to send successful and error responses depending on message received - # from gateway. - - defp response_check(%{"messages" => %{"resultCode" => "Ok"}}, raw_response) do - {:ok, ResponseHandler.parse_gateway_success(raw_response)} - end - - defp response_check(%{"messages" => %{"resultCode" => "Error"}}, raw_response) do - {:error, ResponseHandler.parse_gateway_error(raw_response)} + { + :error, + %Response{ + reason: "network related failure", + message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]" + } + } end ############################################################################## @@ -470,7 +459,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do add_order_id(opts), add_purchase_transaction_request(amount, transaction_type, payment, opts) ]) - |> generate + |> generate(format: :none) end # function for formatting the request for normal capture @@ -481,7 +470,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do add_order_id(opts), add_capture_transaction_request(amount, id, transaction_type, opts) ]) - |> generate + |> generate(format: :none) end # function to format the request for normal refund @@ -492,7 +481,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do add_order_id(opts), add_refund_transaction_request(amount, id, opts, transaction_type) ]) - |> generate + |> generate(format: :none) end # function to format the request for normal void operation @@ -506,7 +495,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do add_ref_trans_id(id) ]) ]) - |> generate + |> generate(format: :none) end defp create_customer_payment_profile(card, opts) do @@ -746,88 +735,98 @@ defmodule Gringotts.Gateways.AuthorizeNet do end end + ################################################################################## + # RESPONSE_HANDLER MODULE # + # # + ################################################################################## + defmodule ResponseHandler do @moduledoc false alias Gringotts.Response - @response_type %{ - auth_response: "authenticateTestResponse", - transaction_response: "createTransactionResponse", - error_response: "ErrorResponse", - customer_profile_response: "createCustomerProfileResponse", - customer_payment_profile_response: "createCustomerPaymentProfileResponse", - delete_customer_profile: "deleteCustomerProfileResponse" - } + @supported_response_types [ + "authenticateTestResponse", + "createTransactionResponse", + "ErrorResponse", + "createCustomerProfileResponse", + "createCustomerPaymentProfileResponse", + "deleteCustomerProfileResponse" + ] + + def respond(body) do + response_map = XmlToMap.naive_map(body) + case extract_gateway_response(response_map) do + :undefined_response -> + { + :error, + %Response{ + reason: "Undefined response from AunthorizeNet", + raw: body, + message: "You might wish to open an issue with Gringotts." + } + } - def parse_gateway_success(raw_response) do - response_type = check_response_type(raw_response) - token = raw_response[response_type]["transactionResponse"]["transId"] - message = raw_response[response_type]["messages"]["message"]["text"] - avs_result = raw_response[response_type]["transactionResponse"]["avsResultCode"] - cvc_result = raw_response[response_type]["transactionResponse"]["cavvResultCode"] + result -> + build_response(result, %Response{raw: body, status_code: 200}) + end + end + + def extract_gateway_response(response_map) do + # The type of the response should be supported + @supported_response_types + |> Stream.map(&Map.get(response_map, &1, nil)) + # Find the first non-nil from the above, if all are `nil`... + # We are in trouble! + |> Enum.find(:undefined_response, &(&1)) + end - [] - |> status_code(200) - |> set_token(token) + defp build_response(%{"messages" => %{"resultCode" => "Ok"}} = result, base_response) do + {:ok, ResponseHandler.parse_gateway_success(result, base_response)} + end + + defp build_response(%{"messages" => %{"resultCode" => "Error"}} = result, base_response) do + {:error, ResponseHandler.parse_gateway_error(result, base_response)} + end + + def parse_gateway_success(result, base_response) do + id = result["transactionResponse"]["transId"] + message = result["messages"]["message"]["text"] + avs_result = result["transactionResponse"]["avsResultCode"] + cvc_result = result["transactionResponse"]["cavvResultCode"] + gateway_code = result["messages"]["message"]["code"] + + base_response + |> set_id(id) |> set_message(message) + |> set_gateway_code(gateway_code) |> set_avs_result(avs_result) |> set_cvc_result(cvc_result) - |> set_params(raw_response) - |> set_success(true) - |> handle_opts end - def parse_gateway_error(raw_response) do - response_type = check_response_type(raw_response) + def parse_gateway_error(result, base_response) do + message = result["messages"]["message"]["text"] + gateway_code = result["messages"]["message"]["code"] - {message, error_code} = - if raw_response[response_type]["transactionResponse"]["errors"] do - { - raw_response[response_type]["messages"]["message"]["text"] <> - " " <> - raw_response[response_type]["transactionResponse"]["errors"]["error"]["errorText"], - raw_response[response_type]["transactionResponse"]["errors"]["error"]["errorCode"] - } - else - { - raw_response[response_type]["messages"]["message"]["text"], - raw_response[response_type]["messages"]["message"]["code"] - } - end + error_text = result["transactionResponse"]["errors"]["error"]["errorText"] + error_code = result["transactionResponse"]["errors"]["error"]["errorCode"] + reason = "#{error_text} [Error code (#{error_code})]" - [] - |> status_code(200) + base_response |> set_message(message) - |> set_error_code(error_code) - |> set_params(raw_response) - |> set_success(false) - |> handle_opts + |> set_gateway_code(gateway_code) + |> set_reason(reason) end - def check_response_type(raw_response) do - cond do - raw_response[@response_type[:transaction_response]] -> "createTransactionResponse" - raw_response[@response_type[:error_response]] -> "ErrorResponse" - raw_response[@response_type[:customer_profile_response]] -> "createCustomerProfileResponse" - raw_response[@response_type[:customer_payment_profile_response]] -> "createCustomerPaymentProfileResponse" - raw_response[@response_type[:delete_customer_profile]] -> "deleteCustomerProfileResponse" - end - end + ############################################################################ + # HELPERS # + ############################################################################ - defp set_token(opts, token), do: [{:authorization, token} | opts] - defp set_success(opts, value), do: [{:success, value} | opts] - defp status_code(opts, code), do: [{:status, code} | opts] - defp set_message(opts, message), do: [{:message, message} | opts] - defp set_avs_result(opts, result), do: [{:avs, result} | opts] - defp set_cvc_result(opts, result), do: [{:cvc, result} | opts] - defp set_params(opts, raw_response), do: [{:params, raw_response} | opts] - defp set_error_code(opts, code), do: [{:error, code} | opts] - - defp handle_opts(opts) do - case Keyword.fetch(opts, :success) do - {:ok, true} -> Response.success(opts) - {:ok, false} -> Response.error(opts) - end - end + defp set_id(response, id), do: %{response | id: id} + defp set_message(response, message), do: %{response | message: message} + defp set_gateway_code(response, code), do: %{response | gateway_code: code} + defp set_reason(response, body), do: %{response | reason: body} + + defp set_avs_result(response, result), do: %{response | avs_result: result} + defp set_cvc_result(response, result), do: %{response | cvc_result: result} end end diff --git a/mix.exs b/mix.exs index 36d86864..f63edfc6 100644 --- a/mix.exs +++ b/mix.exs @@ -49,7 +49,7 @@ defmodule Gringotts.Mixfile do [ {:poison, "~> 3.1.0"}, {:httpoison, "~> 0.13"}, - {:xml_builder, "~> 0.1.1"}, + {:xml_builder, "~> 2.1"}, {:elixir_xml_to_map, "~> 0.1"}, # Money related diff --git a/mix.lock b/mix.lock index e3441847..da4109e3 100644 --- a/mix.lock +++ b/mix.lock @@ -35,4 +35,4 @@ "timex": {:hex, :timex, "3.1.25", "6002dae5432f749d1c93e2cd103eb73cecb53e50d2c885349e8e4146fc96bd44", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "tzdata": {:hex, :tzdata, "0.5.16", "13424d3afc76c68ff607f2df966c0ab4f3258859bbe3c979c9ed1606135e7352", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, - "xml_builder": {:hex, :xml_builder, "0.1.2", "b48ab9ed0a24f43a6061e0c21deda88b966a2121af5c445d4fc550dd822e23dc", [:mix], [], "hexpm"}} + "xml_builder": {:hex, :xml_builder, "2.1.0", "c249d5339427c13cae11e9d9d0e8b40d25d228b9ecc54029f24017385e60280b", [:mix], [], "hexpm"}} diff --git a/test/gateways/authorize_net_test.exs b/test/gateways/authorize_net_test.exs index b4906339..d57e3522 100644 --- a/test/gateways/authorize_net_test.exs +++ b/test/gateways/authorize_net_test.exs @@ -125,7 +125,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do MockResponse.successful_purchase_response() end do assert {:ok, response} = ANet.purchase(@amount, @card, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end @@ -135,7 +134,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do MockResponse.bad_card_purchase_response() end do assert {:error, response} = ANet.purchase(@amount, @bad_card, @opts) - assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" end end end @@ -147,7 +145,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do MockResponse.successful_authorize_response() end do assert {:ok, response} = ANet.authorize(@amount, @card, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end @@ -157,7 +154,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do MockResponse.bad_card_purchase_response() end do assert {:error, response} = ANet.authorize(@amount, @bad_card, @opts) - assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" end end end @@ -169,7 +165,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do MockResponse.successful_capture_response() end do assert {:ok, response} = ANet.capture(@capture_id, @amount, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end @@ -177,7 +172,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do with_mock HTTPoison, request: fn _method, _url, _body, _headers -> MockResponse.bad_id_capture() end do assert {:error, response} = ANet.capture(@capture_invalid_id, @amount, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end end end @@ -189,7 +183,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do MockResponse.successful_refund_response() end do assert {:ok, response} = ANet.refund(@amount, @refund_id, @opts_refund) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end @@ -197,7 +190,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do with_mock HTTPoison, request: fn _method, _url, _body, _headers -> MockResponse.bad_card_refund() end do assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund_bad_payment) - assert response.params["ErrorResponse"]["messages"]["resultCode"] == "Error" end end @@ -205,7 +197,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do with_mock HTTPoison, request: fn _method, _url, _body, _headers -> MockResponse.debit_less_than_refund() end do assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end end end @@ -215,7 +206,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do with_mock HTTPoison, request: fn _method, _url, _body, _headers -> MockResponse.successful_void() end do assert {:ok, response} = ANet.void(@void_id, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Ok" end end @@ -223,7 +213,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do with_mock HTTPoison, request: fn _method, _url, _body, _headers -> MockResponse.void_non_existent_id() end do assert {:error, response} = ANet.void(@void_invalid_id, @opts) - assert response.params["createTransactionResponse"]["messages"]["resultCode"] == "Error" end end end @@ -233,7 +222,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do with_mock HTTPoison, request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do assert {:ok, response} = ANet.store(@card, @opts_store) - assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" end end @@ -241,7 +229,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do with_mock HTTPoison, request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do assert {:ok, response} = ANet.store(@card, @opts_store_without_validation) - assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" end end @@ -252,7 +239,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do end do assert {:error, response} = ANet.store(@card, @opts_store_no_profile) - assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Error" end end @@ -264,7 +250,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do end do assert {:ok, response} = ANet.store(@card, @opts_customer_profile) - assert response.params["createCustomerPaymentProfileResponse"]["messages"]["resultCode"] == "Ok" end end @@ -273,7 +258,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do with_mock HTTPoison, request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do assert {:ok, response} = ANet.store(@card, @opts_customer_profile_args) - assert response.params["createCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" end end end @@ -285,7 +269,6 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do MockResponse.successful_unstore_response() end do assert {:ok, response} = ANet.unstore(@unstore_id, @opts) - assert response.params["deleteCustomerProfileResponse"]["messages"]["resultCode"] == "Ok" end end end @@ -296,7 +279,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do MockResponse.netwok_error_non_existent_domain() end do assert {:error, response} = ANet.purchase(@amount, @card, @opts) - assert response.message == "HTTPoison says 'nxdomain'" + assert response.message == "HTTPoison says 'nxdomain' [ID: nil]" end end end From e16fe7237d7ec58c83a8fa876cc4b314c10927ae Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Fri, 9 Feb 2018 17:48:04 +0530 Subject: [PATCH 29/60] Refactored `commit` and corrected avs, cvc result Also added CAVV result filed to the response which is nil for MaasterCards (as per ANet docs). --- lib/gringotts/gateways/authorize_net.ex | 109 ++++++++++++++++++------ test/gateways/authorize_net_test.exs | 36 ++++---- 2 files changed, 100 insertions(+), 45 deletions(-) diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index 1504eea2..b036ce52 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -47,11 +47,15 @@ defmodule Gringotts.Gateways.AuthorizeNet do ## Notes - Authorize.Net supports [multiple currencies][currencies] however, multiple - currencies in one account are not supported. A merchant would need multiple - Authorize.Net accounts, one for each chosen currency. - - > Currently, `Gringotts` supports single Authorize.Net account configuration. + 1. Though Authorize.Net supports [multiple currencies][currencies] however, + multiple currencies in one account are not supported in _this_ module. A + merchant would need multiple Authorize.Net accounts, one for each chosen + currency. + 2. The responses of this module include a non-standard field: `:cavv_result`. + - `:cavv_result` is the "cardholder authentication verification response + code". In case of Mastercard transactions, this field will always be + `nil`. Please refer the "Response Format" section in the [docs][docs] for + more details. [currencies]: https://community.developer.authorize.net/t5/The-Authorize-Net-Developer-Blog/Authorize-Net-UK-Europe-Update/ba-p/35957 @@ -108,7 +112,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do @test_url "https://apitest.authorize.net/xml/v1/request.api" @production_url "https://api.authorize.net/xml/v1/request.api" - @header [{"Content-Type", "text/xml"}] + @headers [{"Content-Type", "text/xml"}] @transaction_type %{ purchase: "authCaptureTransaction", @@ -181,8 +185,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do @spec purchase(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response} def purchase(amount, payment, opts) do request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:purchase]) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -242,8 +245,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do @spec authorize(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response} def authorize(amount, payment, opts) do request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:authorize]) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -285,8 +287,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do @spec capture(String.t(), Money.t(), Keyword.t()) :: {:ok | :error, Response} def capture(id, amount, opts) do request_data = normal_capture(amount, id, opts, @transaction_type[:capture]) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -315,8 +316,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do @spec refund(Money.t(), String.t(), Keyword.t()) :: {:ok | :error, Response} def refund(amount, id, opts) do request_data = normal_refund(amount, id, opts, @transaction_type[:refund]) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -341,8 +341,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do @spec void(String.t(), Keyword.t()) :: {:ok | :error, Response} def void(id, opts) do request_data = normal_void(id, opts, @transaction_type[:void]) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -401,8 +400,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do card |> create_customer_profile(opts) |> generate(format: :none) end - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end @doc """ @@ -420,15 +418,15 @@ defmodule Gringotts.Gateways.AuthorizeNet do @spec unstore(String.t(), Keyword.t()) :: {:ok | :error, Response} def unstore(customer_profile_id, opts) do request_data = customer_profile_id |> delete_customer_profile(opts) |> generate(format: :none) - response_data = commit(:post, request_data, opts) - respond(response_data) + commit(request_data, opts) end # method to make the API request with params - defp commit(method, payload, opts) do - path = base_url(opts) - headers = @header - HTTPoison.request(method, path, payload, headers) + defp commit(payload, opts) do + opts + |> base_url() + |> HTTPoison.post(payload, @headers) + |> respond() end defp respond({:ok, %{body: body, status_code: 200}}), do: ResponseHandler.respond(body) @@ -753,6 +751,50 @@ defmodule Gringotts.Gateways.AuthorizeNet do "deleteCustomerProfileResponse" ] + @avs_code_translator %{ + "A" => {"pass", "fail"}, #The street address matched, but the postal code did not. + "B" => {nil, nil}, # No address information was provided. + "E" => {"fail", nil}, # The AVS check returned an error. + "G" => {nil, nil}, # The card was issued by a bank outside the U.S. and does not support AVS. + "N" => {"fail", "fail"}, # Neither the street address nor postal code matched. + "P" => {nil, nil}, # AVS is not applicable for this transaction. + "R" => {nil, nil}, # Retry — AVS was unavailable or timed out. + "S" => {nil, nil}, # AVS is not supported by card issuer. + "U" => {nil, nil}, # Address information is unavailable. + "W" => {"fail", "pass"}, # The US ZIP+4 code matches, but the street address does not. + "X" => {"pass", "pass"}, # Both the street address and the US ZIP+4 code matched. + "Y" => {"pass", "pass"}, # The street address and postal code matched. + "Z" => {"fail", "pass"}, # The postal code matched, but the street address did not. + "" => {nil, nil}, # fallback in-case of absence + nil => {nil, nil} # fallback in-case of absence + } + + @cvc_code_translator %{ + "M" => "CVV matched.", + "N" => "CVV did not match.", + "P" => "CVV was not processed.", + "S" => "CVV should have been present but was not indicated.", + "U" => "The issuer was unable to process the CVV check.", + nil => nil # fallback in-case of absence + } + + @cavv_code_translator %{ + "" => "CAVV not validated.", + "0" => "CAVV was not validated because erroneous data was submitted.", + "1" => "CAVV failed validation.", + "2" => "CAVV passed validation.", + "3" => "CAVV validation could not be performed; issuer attempt incomplete.", + "4" => "CAVV validation could not be performed; issuer system error.", + "5" => "Reserved for future use.", + "6" => "Reserved for future use.", + "7" => "CAVV failed validation, but the issuer is available. Valid for U.S.-issued card submitted to non-U.S acquirer.", + "8" => "CAVV passed validation and the issuer is available. Valid for U.S.-issued card submitted to non-U.S. acquirer.", + "9" => "CAVV failed validation and the issuer is unavailable. Valid for U.S.-issued card submitted to non-U.S acquirer.", + "A" => "CAVV passed validation but the issuer unavailable. Valid for U.S.-issued card submitted to non-U.S acquirer.", + "B" => "CAVV passed validation, information only, no liability shift.", + nil => nil # fallback in-case of absence + } + def respond(body) do response_map = XmlToMap.naive_map(body) case extract_gateway_response(response_map) do @@ -792,7 +834,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do id = result["transactionResponse"]["transId"] message = result["messages"]["message"]["text"] avs_result = result["transactionResponse"]["avsResultCode"] - cvc_result = result["transactionResponse"]["cavvResultCode"] + cvc_result = result["transactionResponse"]["cvvResultCode"] + cavv_result = result["transactionResponse"]["cavvResultCode"] gateway_code = result["messages"]["message"]["code"] base_response @@ -801,6 +844,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do |> set_gateway_code(gateway_code) |> set_avs_result(avs_result) |> set_cvc_result(cvc_result) + |> set_cavv_result(cavv_result) end def parse_gateway_error(result, base_response) do @@ -826,7 +870,18 @@ defmodule Gringotts.Gateways.AuthorizeNet do defp set_gateway_code(response, code), do: %{response | gateway_code: code} defp set_reason(response, body), do: %{response | reason: body} - defp set_avs_result(response, result), do: %{response | avs_result: result} - defp set_cvc_result(response, result), do: %{response | cvc_result: result} + defp set_avs_result(response, avs_code) do + {street, zip_code} = @avs_code_translator[avs_code] + %{response | avs_result: %{street: street, zip_code: zip_code}} + end + + defp set_cvc_result(response, cvv_code) do + %{response | cvc_result: @cvc_code_translator[cvv_code]} + end + + defp set_cavv_result(response, cavv_code) do + Map.put(response, :cavv_result, @cavv_code_translator[cavv_code]) + end + end end diff --git a/test/gateways/authorize_net_test.exs b/test/gateways/authorize_net_test.exs index d57e3522..2ce52278 100644 --- a/test/gateways/authorize_net_test.exs +++ b/test/gateways/authorize_net_test.exs @@ -121,7 +121,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "purchase" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.successful_purchase_response() end do assert {:ok, response} = ANet.purchase(@amount, @card, @opts) @@ -130,7 +130,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "with bad card" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.bad_card_purchase_response() end do assert {:error, response} = ANet.purchase(@amount, @bad_card, @opts) @@ -141,7 +141,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "authorize" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.successful_authorize_response() end do assert {:ok, response} = ANet.authorize(@amount, @card, @opts) @@ -150,7 +150,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "with bad card" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.bad_card_purchase_response() end do assert {:error, response} = ANet.authorize(@amount, @bad_card, @opts) @@ -161,7 +161,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "capture" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.successful_capture_response() end do assert {:ok, response} = ANet.capture(@capture_id, @amount, @opts) @@ -170,7 +170,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "with bad transaction id" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.bad_id_capture() end do + post: fn _url, _body, _headers -> MockResponse.bad_id_capture() end do assert {:error, response} = ANet.capture(@capture_invalid_id, @amount, @opts) end end @@ -179,7 +179,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "refund" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.successful_refund_response() end do assert {:ok, response} = ANet.refund(@amount, @refund_id, @opts_refund) @@ -188,14 +188,14 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "bad payment params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.bad_card_refund() end do + post: fn _url, _body, _headers -> MockResponse.bad_card_refund() end do assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund_bad_payment) end end test "debit less than refund amount" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.debit_less_than_refund() end do + post: fn _url, _body, _headers -> MockResponse.debit_less_than_refund() end do assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund) end end @@ -204,14 +204,14 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "void" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.successful_void() end do + post: fn _url, _body, _headers -> MockResponse.successful_void() end do assert {:ok, response} = ANet.void(@void_id, @opts) end end test "with bad transaction id" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.void_non_existent_id() end do + post: fn _url, _body, _headers -> MockResponse.void_non_existent_id() end do assert {:error, response} = ANet.void(@void_invalid_id, @opts) end end @@ -220,21 +220,21 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "store" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do + post: fn _url, _body, _headers -> MockResponse.successful_store_response() end do assert {:ok, response} = ANet.store(@card, @opts_store) end end test "successful response without validation and customer type" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do + post: fn _url, _body, _headers -> MockResponse.successful_store_response() end do assert {:ok, response} = ANet.store(@card, @opts_store_without_validation) end end test "without any profile" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.store_without_profile_fields() end do assert {:error, response} = ANet.store(@card, @opts_store_no_profile) @@ -245,7 +245,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "with customer profile id" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.customer_payment_profile_success_response() end do assert {:ok, response} = ANet.store(@card, @opts_customer_profile) @@ -256,7 +256,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "successful response without valiadtion mode and customer type" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> MockResponse.successful_store_response() end do + post: fn _url, _body, _headers -> MockResponse.successful_store_response() end do assert {:ok, response} = ANet.store(@card, @opts_customer_profile_args) end end @@ -265,7 +265,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "unstore" do test "successful response with right params" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.successful_unstore_response() end do assert {:ok, response} = ANet.unstore(@unstore_id, @opts) @@ -275,7 +275,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "network error type non existent domain" do with_mock HTTPoison, - request: fn _method, _url, _body, _headers -> + post: fn _url, _body, _headers -> MockResponse.netwok_error_non_existent_domain() end do assert {:error, response} = ANet.purchase(@amount, @card, @opts) From 6eef8ef0bdb10ba78fb6e30f61220da7b889c09b Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Fri, 9 Feb 2018 21:59:32 +0530 Subject: [PATCH 30/60] Correct a few doc examples --- lib/gringotts/gateways/authorize_net.ex | 39 ++++++++++++------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index b036ce52..14950eb9 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -94,7 +94,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ``` iex> alias Gringotts.{Response, CreditCard, Gateways.AuthorizeNet} - iex> amount = %{value: Decimal.new(20.0), currency: "USD"} + iex> amount = Money.new(20, :USD} iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} ``` @@ -136,8 +136,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do Charges a credit `card` for the specified `amount`. It performs `authorize` and `capture` at the [same time][auth-cap-same-time]. - Authorize.Net returns `transId` (available in the `Response.authorization` - field) which can be used to: + Authorize.Net returns `transId` (available in the `Response.id` field) which + can be used to: * `refund/3` a settled transaction. * `void/2` a transaction. @@ -170,7 +170,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ] ## Example - iex> amount = %{value: Decimal.new(20.0), currency: "USD"} + iex> amount = Money.new(20, :USD} iex> opts = [ ref_id: "123456", order: %{invoice_number: "INV-12345", description: "Product Description"}, @@ -182,7 +182,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} iex> result = Gringotts.purchase(Gringotts.Gateways.AuthorizeNet, amount, card, opts) """ - @spec purchase(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response} + @spec purchase(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response.t()} def purchase(amount, payment, opts) do request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:purchase]) commit(request_data, opts) @@ -197,8 +197,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do To transfer the funds to merchant's account follow this up with a `capture/3`. - Authorize.Net returns a `transId` (available in the `Response.authorization` - field) which can be used for: + Authorize.Net returns a `transId` (available in the `Response.id` field) which + can be used for: * `capture/3` an authorized transaction. * `void/2` a transaction. @@ -230,7 +230,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ## Example - iex> amount = %{value: Decimal.new(20.0), currency: "USD"} + iex> amount = Money.new(20, :USD} iex> opts = [ ref_id: "123456", order: %{invoice_number: "INV-12345", description: "Product Description"}, @@ -242,7 +242,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} iex> result = Gringotts.authorize(Gringotts.Gateways.AuthorizeNet, amount, card, opts) """ - @spec authorize(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response} + @spec authorize(Money.t(), CreditCard.t(), Keyword.t()) :: {:ok | :error, Response.t()} def authorize(amount, payment, opts) do request_data = add_auth_purchase(amount, payment, opts, @transaction_type[:authorize]) commit(request_data, opts) @@ -254,8 +254,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do `amount` is transferred to the merchant account by Authorize.Net when it is smaller or equal to the amount used in the pre-authorization referenced by `id`. - Authorize.Net returns a `transId` (available in the `Response.authorization` - field) which can be used to: + Authorize.Net returns a `transId` (available in the `Response.id` field) which + can be used to: * `refund/3` a settled transaction. * `void/2` a transaction. @@ -280,11 +280,11 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> opts = [ ref_id: "123456" ] - iex> amount = %{value: Decimal.new(20.0), currency: "USD"} + iex> amount = Money.new(20, :USD} iex> id = "123456" iex> result = Gringotts.capture(Gringotts.Gateways.AuthorizeNet, id, amount, opts) """ - @spec capture(String.t(), Money.t(), Keyword.t()) :: {:ok | :error, Response} + @spec capture(String.t(), Money.t(), Keyword.t()) :: {:ok | :error, Response.t()} def capture(id, amount, opts) do request_data = normal_capture(amount, id, opts, @transaction_type[:capture]) commit(request_data, opts) @@ -310,10 +310,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do ref_id: "123456" ] iex> id = "123456" - iex> amount = %{value: Decimal.new(20.0), currency: "USD"} + iex> amount = Money.new(20, :USD} iex> result = Gringotts.refund(Gringotts.Gateways.AuthorizeNet, amount, id, opts) """ - @spec refund(Money.t(), String.t(), Keyword.t()) :: {:ok | :error, Response} + @spec refund(Money.t(), String.t(), Keyword.t()) :: {:ok | :error, Response.t()} def refund(amount, id, opts) do request_data = normal_refund(amount, id, opts, @transaction_type[:refund]) commit(request_data, opts) @@ -338,7 +338,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> id = "123456" iex> result = Gringotts.void(Gringotts.Gateways.AuthorizeNet, id, opts) """ - @spec void(String.t(), Keyword.t()) :: {:ok | :error, Response} + @spec void(String.t(), Keyword.t()) :: {:ok | :error, Response.t()} def void(id, opts) do request_data = normal_void(id, opts, @transaction_type[:void]) commit(request_data, opts) @@ -391,7 +391,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} iex> result = Gringotts.store(Gringotts.Gateways.AuthorizeNet, card, opts) """ - @spec store(CreditCard.t(), Keyword.t()) :: {:ok | :error, Response} + @spec store(CreditCard.t(), Keyword.t()) :: {:ok | :error, Response.t()} def store(card, opts) do request_data = if opts[:customer_profile_id] do @@ -411,11 +411,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do ## Example iex> id = "123456" - iex> opts = [] - iex> result = Gringotts.store(Gringotts.Gateways.AuthorizeNet, id, opts) + iex> result = Gringotts.store(Gringotts.Gateways.AuthorizeNet, id) """ - @spec unstore(String.t(), Keyword.t()) :: {:ok | :error, Response} + @spec unstore(String.t(), Keyword.t()) :: {:ok | :error, Response.t()} def unstore(customer_profile_id, opts) do request_data = customer_profile_id |> delete_customer_profile(opts) |> generate(format: :none) commit(request_data, opts) From e3c86cef2e8a34db7635d2466a949771fab5f272 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Thu, 15 Mar 2018 20:58:47 +0530 Subject: [PATCH 31/60] Adds changelog, contributing guide and improves mix task (#117) * Fix Adapter moduledoc and bogus gateway * Fixes #24 * bogus test also uses Money protocol now * Changed validate_config docs * Improved mix task docs (filename) * Better module name suggestion * Now, Filename can be specified on the CLI with the `-f` flag * Added changelog and contributing guide. * Also reworded README slightly * Correct "amount" in examples to ex_money * Replaced example bindings with links to `.iex.exs` * Removed unused params from functions. * Fix call to `downcase` --- CHANGELOG.md | 52 ++++++++++++++ CONTRIBUTING.md | 91 +++++++++++++++++++++++++ README.md | 58 ++++++++++++---- lib/gringotts/adapter.ex | 49 ++++++++++--- lib/gringotts/gateways/authorize_net.ex | 32 ++++----- lib/gringotts/gateways/bogus.ex | 33 ++++----- lib/gringotts/gateways/cams.ex | 37 +++++----- lib/gringotts/gateways/monei.ex | 63 +++-------------- lib/gringotts/gateways/trexle.ex | 41 +++-------- lib/mix/new.ex | 61 +++++++++++------ mix.exs | 6 +- mix.lock | 12 ++-- templates/mock_response.eex | 2 +- templates/test.eex | 4 +- test/gateways/bogus_test.exs | 13 ++-- 15 files changed, 358 insertions(+), 196 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..5a1f16ad --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,52 @@ +# [`v1.1.0-alpha`][tag-1_1_0_alpha] + +## Added + +* [`ISS`][iss#80] [`PR`][pr#78] +Add a `Mix` task that generates a barebones gateway implementation and test suite. + +## Changed + +* [`ISS`][iss#62] [`PR`][pr#71] [`PR`][pr#86] +Deprecate use of `floats` for money amounts, introduce the `Gringotts.Money` protocol. + +[iss#62]: https://github.com/aviabird/gringotts/issues/62 +[iss#80]: https://github.com/aviabird/gringotts/issues/80 + +[pr#71]: https://github.com/aviabird/gringotts/pulls/71 +[pr#78]:https://github.com/aviabird/gringotts/pulls/78 +[pr#86]:https://github.com/aviabird/gringotts/pulls/86 + +# [`v1.0.2`][tag-1_0_2] + +## Added + +* New Gateway: **Trexle** + +## Changed + +* Reduced arity of public API calls by 1 + - No need to pass the name of the `worker` as argument. + +# [`v1.0.1`][tag-1_0_1] + +## Added + +* Improved documentation - made consistent accross gateways +* Improved test coverage + +# [`v1.0.0`][tag-1_0_0] + +* Initial public API release. +* Single worker architecture, config fetched from `config.exs` +* Supported Gateways: + - Stripe + - MONEI + - Paymill + - WireCard + - CAMS + +[tag-1_1_0_alpha]: https://github.com/aviabird/gringotts/releases/tag/v1.1.0-alpha +[tag-1_0_2]: https://github.com/aviabird/gringotts/releases/tag/v1.0.2 +[tag-1_0_1]: https://github.com/aviabird/gringotts/releases/tag/v1.0.1 +[tag-1_0_0]: https://github.com/aviabird/gringotts/releases/tag/v1.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..6eeb7033 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,91 @@ +# Contributing to [`gringotts`][gringotts] + +There are many ways to contribute to `gringotts`, + +1. [Integrate a new Payment Gateway][wiki-new-gateway]. +2. Expanding the feature coverage of (partially) supported gateways. +3. Moving forward on the [roadmap][roadmap] or on tasks being tracked in the + [milestones][milestones]. + +We manage our development using [milestones][milestones] and issues so if you're +a first time contributor, look out for the [`good first issue`][first-issues] +and the [`hotlist: community-help`][ch-issues] labels on the [issues][issues] +page. + +The docs are hosted on [hexdocs.pm][hexdocs] and are updated for each +release. **You must build the docs locally using `mix docs` to get the bleeding +edge developer docs.** + +The article on [Gringott's Architecture][wiki-arch] explains how API calls are +processed. + +:exclamation: ***Please base your work on the `dev` branch.*** + +[roadmap]: https://github.com/aviabird/gringotts/wiki/Roadmap +[wiki-arch]: https://github.com/aviabird/gringotts/wiki/Architecture + +### PR submission checklist + +Each PR should introduce a *focussed set of changes*, and ideally not span over +unrelated modules. + +* [ ] Run the edited files through [credo][credo] and the Elixir + [formatter][hashrocket-formatter] (new in `v1.6`). +* [ ] Check the test coverage by running `mix coveralls`. 100% coverage is not + strictly required. +* [ ] If the PR introduces a new Gateway or just Gateway specific changes, + please format the title like so,\ + `[] ` + +[gringotts]: https://github.com/aviabird/gringotts +[milestones]: https://github.com/aviabird/gringotts/milestones +[issues]: https://github.com/aviabird/gringotts/issues +[first-issues]: https://github.com/aviabird/gringotts/issues?q=is%3Aissue+is%3Aopen+label%3A"good+first+issue" +[ch-issues]: https://github.com/aviabird/gringotts/issues?q=is%3Aissue+is%3Aopen+label%3A"hotfix%3A+community-help" +[hexdocs]: https://hexdocs.pm/gringotts +[credo]: https://github.com/rrrene/credo +[hashrocket-formatter]: https://hashrocket.com/blog/posts/format-your-elixir-code-now + +# Style Guidelines + +We use [`credo`][credo] and the elixir formatter for consistent style, so please +use them! + +## General Rules + +* Keep line length below 120 characters. +* Complex anonymous functions should be extracted into named functions. +* One line functions, should only take up one line! +* Pipes are great, but don't use them, if they are less readable than brackets + then drop the pipe! + +## Writing documentation + +All our docs are inline and built using [`ExDocs`][exdocs]. Please take a look +at how the docs are structured for the [MONEI gateway][src-monei] for +inspiration. + +[exdocs]: https://github.com/elixir-lang/ex_doc +[src-monei]: https://github.com/aviabird/gringotts/blob/dev/lib/gringotts/gateways/monei.ex + +## Writing test cases + +> This is WIP. + +`gringotts` has mock and integration tests. We have currently used +[`bypass`][bypass] and [`mock`][mock] for mock tests, but we don't recommed +using `mock` as it constrains tests to run serially. Use [`mox`][mox] instead.\ +Take a look at [MONEI's mock tests][src-monei-tests] for inspiration. + +-------------------------------------------------------------------------------- + +> **Where to next?** +> Wanna add a new gateway? Head to our [guide][wiki-new-gateway] for that. + +[wiki-new-gateway]: https://github.com/aviabird/gringotts/wiki/Adding-a-new-Gateway +[bypass]: https://github.com/pspdfkit-labs/bypass +[mock]: https://github.com/jjh42/mock +[mox]: https://github.com/plataformatec/mox +[src-monei-tests]: https://github.com/aviabird/gringotts/blob/dev/test/gateways/monei_test.exs +[gringotts]: https://github.com/aviabird/gringotts +[docs]: https://hexdocs.pm/gringotts/Gringotts.html diff --git a/README.md b/README.md index 6b71deb4..ce98c205 100644 --- a/README.md +++ b/README.md @@ -5,24 +5,24 @@

- Gringotts is a payment processing library in Elixir integrating various payment gateways, this draws motivation for shopify's activemerchant gem. Checkout the Demo here. + Gringotts is a payment processing library in Elixir integrating various payment gateways, drawing motivation for Shopify's activemerchant gem. Checkout the Demo here.

Build Status Coverage Status Docs coverage Help Contribute to Open Source

-A simple and unified API to access dozens of different payment -gateways with very different internal APIs is what Gringotts has to offer you. +Gringotts offers a **simple and unified API** to access dozens of different payment +gateways with very different APIs, response schemas, documentation and jargon. ## Installation -### From hex.pm +### From [`hex.pm`][hexpm] -Make the following changes to the `mix.exs` file. - -Add gringotts to the list of dependencies. +Add `gringotts` to the list of dependencies of your application. ```elixir +# your mix.exs + def deps do [ {:gringotts, "~> 1.0"}, @@ -35,23 +35,31 @@ end ## Usage -This simple example demonstrates how a purchase can be made using a person's credit card details. +This simple example demonstrates how a `purchase` can be made using a sample +credit card using the [MONEI][monei] gateway. -Add configs in `config/config.exs` file. +One must "register" their account with `gringotts` ie, put all the +authentication details in the Application config. Usually via +`config/config.exs` ```elixir +# config/config.exs + config :gringotts, Gringotts.Gateways.Monei, userId: "your_secret_user_id", password: "your_secret_password", entityId: "your_secret_channel_id" ``` -Copy and paste this code in your module +Copy and paste this code in a module or an `IEx` session ```elixir alias Gringotts.Gateways.Monei alias Gringotts.{CreditCard} +# a fake sample card that will work now because the Gateway is by default +# in "test" mode. + card = %CreditCard{ first_name: "Harry", last_name: "Potter", @@ -61,9 +69,10 @@ card = %CreditCard{ brand: "VISA" } +# a sum of $42 amount = Money.new(42, :USD) -case Gringotts.purchase(Monei, amount, card, opts) do +case Gringotts.purchase(Monei, amount, card) do {:ok, %{id: id}} -> IO.puts("Payment authorized, reference token: '#{id}'") @@ -72,6 +81,9 @@ case Gringotts.purchase(Monei, amount, card, opts) do end ``` +[hexpm]: https://hex.pm/packages/gringotts +[monei]: http://www.monei.net + ## Supported Gateways | Gateway | Supported countries | @@ -95,9 +107,29 @@ end ## Road Map -- Support more gateways on an on-going basis. -- Each gateway request is hosted in a worker process and supervised. +Apart from supporting more and more gateways, we also keep a somewhat detailed +plan for the future on our [wiki][roadmap]. + +## FAQ + +#### 1. What's with the name? "Gringotts"? + +Gringotts has a nice ring to it. Also [this][reason]. + +#### 2. What is the worker doing in the middle? + +We wanted to "supervise" our payments, and power utilities to process recurring +payments, subscriptions with it. But yes, as of now, it is a bottle neck and +unnecessary. + +It's slated to be removed in [`v2.0.0`][milestone-2_0_0_alpha] and any supervised / async / +parallel work can be explicitly managed via native elixir constructs. + +[milestone-2_0_0_alpha]: https://github.com/aviabird/gringotts/milestone/3 +[reason]: http://harrypotter.wikia.com/wiki/Gringotts ## License MIT + +[roadmap]: https://github.com/aviabird/gringotts/wiki/Roadmap diff --git a/lib/gringotts/adapter.ex b/lib/gringotts/adapter.ex index 8d2a24f8..978cd1d1 100644 --- a/lib/gringotts/adapter.ex +++ b/lib/gringotts/adapter.ex @@ -1,20 +1,53 @@ defmodule Gringotts.Adapter do - @moduledoc ~S""" - Adapter module is currently holding the validation part. + @moduledoc """ + Validates the "required" configuration. - This modules is being `used` by all the payment gateways and raises a run-time - error for the missing configurations which are passed by the gateways to - `validate_config` method. + All gateway modules must `use` this module, which provides a run-time + configuration validator. - Raises an exception `ArgumentError` if the config is not as per the `@required_config` - """ + Gringotts picks up the merchant's Gateway authentication secrets from the + Application config. The configuration validator can be customized by providing + a list of `required_config` keys. The validator will check if these keys are + available at run-time, before each call to the Gateway. + + ## Example + + Say a merchant must provide his `secret_user_name` and `secret_password` to + some Gateway `XYZ`. Then, `Gringotts` expects that the `GatewayXYZ` module + would use `Adapter` in the following manner: + + ``` + defmodule Gringotts.Gateways.GatewayXYZ do + + use Gringotts.Adapter, required_config: [:secret_user_name, :secret_password] + use Gringotts.Gateways.Base + + # the rest of the implentation + end + ``` + And, the merchant woud provide these secrets in the Application config, + possibly via `config/config.exs` like so, + ``` + # config/config.exs + + config :gringotts, Gringotts.Gateways.GatewayXYZ, + adapter: Gringotts.Gateways.GatewayXYZ, + secret_user_name: "some_really_secret_user_name", + secret_password: "some_really_secret_password" + + ``` + """ + defmacro __using__(opts) do quote bind_quoted: [opts: opts] do @required_config opts[:required_config] || [] @doc """ - Validates the config dynamically depending on what is the value of `required_config` + Catches gateway configuration errors. + + Raises a run-time `ArgumentError` if any of the `required_config` values + is not available or missing from the Application config. """ def validate_config(config) do missing_keys = Enum.reduce(@required_config, [], fn(key, missing_keys) -> diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index 14950eb9..59281314 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -89,18 +89,16 @@ defmodule Gringotts.Gateways.AuthorizeNet do that gives you a pre-configured sample app ready-to-go. + You could use the same config or update it the with your "secrets" [above](#module-configuring-your-authorizenet-account-at-gringotts). - 2. Run an `iex` session with `iex -S mix` and add some variable bindings and - aliases to it (to save some time): + + 2. To save a lot of time, create a [`.iex.exs`][iex-docs] file as shown in + [this gist][authorize_net.iex.exs] to introduce a set of handy bindings and + aliases. - ``` - iex> alias Gringotts.{Response, CreditCard, Gateways.AuthorizeNet} - iex> amount = Money.new(20, :USD} - iex> card = %CreditCard{number: "5424000000000015", year: 2099, month: 12, verification_code: "999"} - ``` - - We'll be using these in the examples below. + We'll be using these bindings in the examples below. [example-repo]: https://github.com/aviabird/gringotts_example + [iex-docs]: https://hexdocs.pm/iex/IEx.html#module-the-iex-exs-file + [authorize_net.iex.exs]: https://gist.github.com/oyeb/b1030058bda1fa9a3d81f1cf30723695 [gs]: https://github.com/aviabird/gringotts/wiki """ @@ -170,7 +168,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ] ## Example - iex> amount = Money.new(20, :USD} + iex> amount = Money.new(20, :USD) iex> opts = [ ref_id: "123456", order: %{invoice_number: "INV-12345", description: "Product Description"}, @@ -230,7 +228,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ## Example - iex> amount = Money.new(20, :USD} + iex> amount = Money.new(20, :USD) iex> opts = [ ref_id: "123456", order: %{invoice_number: "INV-12345", description: "Product Description"}, @@ -280,7 +278,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do iex> opts = [ ref_id: "123456" ] - iex> amount = Money.new(20, :USD} + iex> amount = Money.new(20, :USD) iex> id = "123456" iex> result = Gringotts.capture(Gringotts.Gateways.AuthorizeNet, id, amount, opts) """ @@ -310,7 +308,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ref_id: "123456" ] iex> id = "123456" - iex> amount = Money.new(20, :USD} + iex> amount = Money.new(20, :USD) iex> result = Gringotts.refund(Gringotts.Gateways.AuthorizeNet, amount, id, opts) """ @spec refund(Money.t(), String.t(), Keyword.t()) :: {:ok | :error, Response.t()} @@ -465,7 +463,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do |> element(%{xmlns: @aut_net_namespace}, [ add_merchant_auth(opts[:config]), add_order_id(opts), - add_capture_transaction_request(amount, id, transaction_type, opts) + add_capture_transaction_request(amount, id, transaction_type) ]) |> generate(format: :none) end @@ -561,7 +559,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do add_transaction_type(transaction_type), add_amount(amount), add_payment_source(payment), - add_invoice(transaction_type, opts), + add_invoice(opts), add_tax_fields(opts), add_duty_fields(opts), add_shipping_fields(opts), @@ -570,7 +568,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ]) end - defp add_capture_transaction_request(amount, id, transaction_type, opts) do + defp add_capture_transaction_request(amount, id, transaction_type) do element(:transactionRequest, [ add_transaction_type(transaction_type), add_amount(amount), @@ -626,7 +624,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do ]) end - defp add_invoice(transactionType, opts) do + defp add_invoice(opts) do element([ element(:order, [ element(:invoiceNumber, opts[:order][:invoice_number]), diff --git a/lib/gringotts/gateways/bogus.ex b/lib/gringotts/gateways/bogus.ex index de903744..d6bdaa02 100644 --- a/lib/gringotts/gateways/bogus.ex +++ b/lib/gringotts/gateways/bogus.ex @@ -1,4 +1,6 @@ defmodule Gringotts.Gateways.Bogus do + @moduledoc false + use Gringotts.Gateways.Base alias Gringotts.{ @@ -6,36 +8,29 @@ defmodule Gringotts.Gateways.Bogus do Response } + @some_authorization_id "14a62fff80f24a25f775eeb33624bbb3" + def authorize(_amount, _card_or_id, _opts), do: success() def purchase(_amount, _card_or_id, _opts), do: success() - def capture(id, amount, _opts), - do: success(id) + def capture(_id, _amount, _opts), + do: success() - def void(id, _opts), - do: success(id) + def void(_id, _opts), + do: success() - def refund(_amount, id, _opts), - do: success(id) + def refund(_amount, _id, _opts), + do: success() - def store(_card = %CreditCard{}, _opts), + def store(%CreditCard{} = _card, _opts), do: success() - def unstore(customer_id, _opts), - do: success(customer_id) + def unstore(_customer_id, _opts), + do: success() defp success, - do: {:ok, Response.success(id: random_string())} - - defp success(id), - do: {:ok, Response.success(id: id)} - - defp random_string(length \\ 10), - do: 1..length |> Enum.map(&random_char/1) |> Enum.join - - defp random_char(_), - do: to_string(:rand.uniform(9)) + do: {:ok, Response.success(id: @some_authorization_id)} end diff --git a/lib/gringotts/gateways/cams.ex b/lib/gringotts/gateways/cams.ex index 1787e947..0cb0c4e8 100644 --- a/lib/gringotts/gateways/cams.ex +++ b/lib/gringotts/gateways/cams.ex @@ -38,13 +38,13 @@ defmodule Gringotts.Gateways.Cams do this is important to you. [issues]: https://github.com/aviabird/gringotts/issues/new - + ### Schema * `billing_address` is a `map` from `atoms` to `String.t`, and can include any of the keys from: `:name, :address1, :address2, :company, :city, :state, :zip, :country, :phone, :fax]` - + ## Registering your CAMS account at `Gringotts` | Config parameter | CAMS secret | @@ -81,26 +81,21 @@ defmodule Gringotts.Gateways.Cams do you get after [registering with CAMS](#module-registering-your-cams-account-at-gringotts). - 2. Run an `iex` session with `iex -S mix` and add some variable bindings and - aliases to it (to save some time): - ``` - iex> alias Gringotts.{Response, CreditCard, Gateways.Cams} - iex> card = %CreditCard{first_name: "Harry", - last_name: "Potter", - number: "4111111111111111", - year: 2099, - month: 12, - verification_code: "999", - brand: "VISA"} - iex> money = %{value: Decimal.new(20), currency: "USD"} - ``` - We'll be using these in the examples below. + 2. To save a lot of time, create a [`.iex.exs`][iex-docs] file as shown in + [this gist][cams.iex.exs] to introduce a set of handy bindings and + aliases. + + We'll be using these bindings in the examples below. + + [example-repo]: https://github.com/aviabird/gringotts_example + [iex-docs]: https://hexdocs.pm/iex/IEx.html#module-the-iex-exs-file + [cams.iex.exs]: https://gist.github.com/oyeb/9a299df95cc13a87324e321faca5c9b8 ## Integrating with phoenix Refer the [GringottsPay][gpay-heroku-cams] website for an example of how to integrate CAMS with phoenix. The source is available [here][gpay-repo]. - + [gpay-repo]: https://github.com/aviabird/gringotts_payment [gpay-heroku-cams]: http://gringottspay.herokuapp.com/cams @@ -164,7 +159,7 @@ defmodule Gringotts.Gateways.Cams do month: 12, verification_code: "999", brand: "VISA"} - iex> money = %{value: Decimal.new(20), currency: "USD"} + iex> money = Money.new(20, :USD) iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Cams, money, card) ``` """ @@ -209,7 +204,7 @@ defmodule Gringotts.Gateways.Cams do month: 12, verification_code: "999", brand: "VISA"} - iex> money = %{value: Decimal.new(10), currency: "USD"} + iex> money = Money.new(10, :USD) iex> authorization = auth_result.authorization # authorization = "some_authorization_transaction_id" iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Cams, money, authorization) @@ -247,7 +242,7 @@ defmodule Gringotts.Gateways.Cams do month: 12, verification_code: "999", brand: "VISA"} - iex> money = %{value: Decimal.new(20), currency: "USD"} + iex> money = Money.new(20, :USD) iex> Gringotts.purchase(Gringotts.Gateways.Cams, money, card) ``` """ @@ -279,7 +274,7 @@ defmodule Gringotts.Gateways.Cams do ``` iex> capture_id = capture_result.authorization # capture_id = "some_capture_transaction_id" - iex> money = %{value: Decimal.new(20), currency: "USD"} + iex> money = Money.new(20, :USD) iex> Gringotts.refund(Gringotts.Gateways.Cams, money, capture_id) ``` """ diff --git a/lib/gringotts/gateways/monei.ex b/lib/gringotts/gateways/monei.ex index c297a1a3..eeef6ec6 100644 --- a/lib/gringotts/gateways/monei.ex +++ b/lib/gringotts/gateways/monei.ex @@ -126,56 +126,15 @@ defmodule Gringotts.Gateways.Monei do that you see in `Dashboard > Sub-accounts` as described [above](#module-registering-your-monei-account-at-gringotts). - 2. Run an `iex` session with `iex -S mix` and add some variable bindings and - aliases to it (to save some time): - ``` - iex> alias Gringotts.{Response, CreditCard, Gateways.Monei} - iex> amount = %{value: Decimal.new(42), currency: "USD"} - iex> card = %CreditCard{first_name: "Harry", - last_name: "Potter", - number: "4200000000000000", - year: 2099, month: 12, - verification_code: "123", - brand: "VISA"} - iex> customer = %{"givenName": "Harry", - "surname": "Potter", - "merchantCustomerId": "the_boy_who_lived", - "sex": "M", - "birthDate": "1980-07-31", - "mobile": "+15252525252", - "email": "masterofdeath@ministryofmagic.gov", - "ip": "127.0.0.1", - "status": "NEW"} - iex> merchant = %{"name": "Ollivanders", - "city": "South Side", - "street": "Diagon Alley", - "state": "London", - "country": "GB", - "submerchantId": "Makers of Fine Wands since 382 B.C."} - iex> billing = %{"street1": "301, Gryffindor", - "street2": "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", - "city": "Highlands", - "state": "Scotland", - "country": "GB"} - iex> shipping = %{"street1": "301, Gryffindor", - "street2": "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", - "city": "Highlands", - "state": "Scotland", - "country": "GB", - "method": "SAME_DAY_SERVICE", - "comment": "For our valued customer, Mr. Potter"} - iex> opts = [customer: customer, - merchant: merchant, - billing: billing, - shipping: shipping, - category: "EC", - custom: %{"voldemort": "he who must not be named"}, - register: true] - ``` - - We'll be using these in the examples below. + 2. To save a lot of time, create a [`.iex.exs`][iex-docs] file as shown in + [this gist][monei.iex.exs] to introduce a set of handy bindings and + aliases. + + We'll be using these bindings in the examples below. [example-repo]: https://github.com/aviabird/gringotts_example + [iex-docs]: https://hexdocs.pm/iex/IEx.html#module-the-iex-exs-file + [monei.iex.exs]: https://gist.github.com/oyeb/a2e2ac5986cc90a12a6136f6bf1357e5 ## TODO @@ -251,7 +210,7 @@ defmodule Gringotts.Gateways.Monei do The following example shows how one would (pre) authorize a payment of $42 on a sample `card`. - iex> amount = %{value: Decimal.new(42), currency: "USD"} + iex> amount = Money.new(42, :USD) iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Monei, amount, card, opts) iex> auth_result.id # This is the authorization ID @@ -289,7 +248,7 @@ defmodule Gringotts.Gateways.Monei do The following example shows how one would (partially) capture a previously authorized a payment worth $35 by referencing the obtained authorization `id`. - iex> amount = %{value: Decimal.new(35), currency: "USD"} + iex> amount = Money.new(35, :USD) iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Monei, amount, auth_result.id, opts) """ @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response.t()} @@ -323,7 +282,7 @@ defmodule Gringotts.Gateways.Monei do The following example shows how one would process a payment worth $42 in one-shot, without (pre) authorization. - iex> amount = %{value: Decimal.new(42), currency: "USD"} + iex> amount = Money.new(42, :USD) iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.Monei, amount, card, opts) iex> purchase_result.token # This is the registration ID/token @@ -357,7 +316,7 @@ defmodule Gringotts.Gateways.Monei do The following example shows how one would (completely) refund a previous purchase (and similarily for captures). - iex> amount = %{value: Decimal.new(42), currency: "USD"} + iex> amount = Money.new(42, :USD) iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.Monei, purchase_result.id, amount) """ @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} diff --git a/lib/gringotts/gateways/trexle.ex b/lib/gringotts/gateways/trexle.ex index fba77838..523d4c2f 100644 --- a/lib/gringotts/gateways/trexle.ex +++ b/lib/gringotts/gateways/trexle.ex @@ -66,35 +66,16 @@ defmodule Gringotts.Gateways.Trexle do that as described [above](#module-registering-your-trexle-account-at-gringotts). - 2. Run an `iex` session with `iex -S mix` and add some variable bindings and - aliases to it (to save some time): - ``` - iex> alias Gringotts.{Response, CreditCard, Gateways.Trexle} - iex> card = %CreditCard{ - first_name: "Harry", - last_name: "Potter", - number: "4200000000000000", - year: 2099, month: 12, - verification_code: "123", - brand: "VISA"} - iex> address = %Address{ - street1: "301, Gryffindor", - street2: "Hogwarts School of Witchcraft and Wizardry, Hogwarts Castle", - city: "Highlands", - region: "SL", - country: "GB", - postal_code: "11111", - phone: "(555)555-5555"} - iex> options = [email: "masterofdeath@ministryofmagic.gov", - ip_address: "127.0.0.1", - billing_address: address, - description: "For our valued customer, Mr. Potter"] - ``` + 2. To save a lot of time, create a [`.iex.exs`][iex-docs] file as shown in + [this gist][trexle.iex.exs] to introduce a set of handy bindings and + aliases. - We'll be using these in the examples below. + We'll be using these bindings in the examples below. [example-repo]: https://github.com/aviabird/gringotts_example - [gs]: # + [iex-docs]: https://hexdocs.pm/iex/IEx.html#module-the-iex-exs-file + [trexle.iex.exs]: https://gist.github.com/oyeb/055f40e9ad4102f5480febd2cfa00787 + [gs]: https://github.com/aviabird/gringotts/wiki """ @base_url "https://core.trexle.com/api/v1/" @@ -120,7 +101,7 @@ defmodule Gringotts.Gateways.Trexle do a sample `card`. ``` - iex> amount = %{value: Decimal.new(100),currency: "USD") + iex> amount = Money.new(10, :USD) iex> card = %CreditCard{ first_name: "Harry", last_name: "Potter", @@ -170,7 +151,7 @@ defmodule Gringotts.Gateways.Trexle do authorized a payment worth $10 by referencing the obtained `charge_token`. ``` - iex> amount = %{value: Decimal.new(100),currency: "USD") + iex> amount = Money.new(10, :USD) iex> token = "some-real-token" iex> Gringotts.capture(Gringotts.Gateways.Trexle, token, amount) ``` @@ -194,7 +175,7 @@ defmodule Gringotts.Gateways.Trexle do one-shot, without (pre) authorization. ``` - iex> amount = %{value: Decimal.new(100),currency: "USD") + iex> amount = Money.new(10, :USD) iex> card = %CreditCard{ first_name: "Harry", last_name: "Potter", @@ -242,7 +223,7 @@ defmodule Gringotts.Gateways.Trexle do `purchase/3` (and similarily for `capture/3`s). ``` - iex> amount = %{value: Decimal.new(100),currency: "USD") + iex> amount = Money.new(10, :USD) iex> token = "some-real-token" iex> Gringotts.refund(Gringotts.Gateways.Trexle, amount, token) ``` diff --git a/lib/mix/new.ex b/lib/mix/new.ex index c37fd7d0..02f4058b 100644 --- a/lib/mix/new.ex +++ b/lib/mix/new.ex @@ -6,10 +6,12 @@ defmodule Mix.Tasks.Gringotts.New do @moduledoc """ Generates a barebones implementation for a gateway. - It expects the (brand) name of the gateway as argument. This will not - necessarily be the module name, but we recommend the name be capitalized. + It expects the (brand) name of the gateway as argument and we recommend that + it be capitalized. *This will not necessarily be the module name*. - mix gringotts.new NAME [-m, --module MODULE] [--url URL] + ``` + mix gringotts.new NAME [-m, --module MODULE] [-f, --file FILENAME] [--url URL] + ``` A barebones implementation of the gateway will be created along with skeleton mock and integration tests in `lib/gringotts/gateways/`. The command will @@ -22,6 +24,7 @@ defmodule Mix.Tasks.Gringotts.New do > prompts. * `-m` `--module` - The module name for the Gateway. + * `-f` `--file` - The filename. * `--url` - The homepage of the gateway. ## Examples @@ -30,10 +33,10 @@ defmodule Mix.Tasks.Gringotts.New do The prompts for this will be: ``` - MODULE = `Foobar` - URL = `https://www.foobar.com` + MODULE = "Foobar" + URL = "https://www.foobar.com" + FILENAME = "foo_bar.ex" ``` - and the filename will be `foo_bar.ex` """ use Mix.Task @@ -45,20 +48,26 @@ Comma separated list of required configuration keys: > } def run(args) do - {key_list, [name], []} = + {key_list, name, []} = OptionParser.parse( args, - switches: [module: :string, url: :string], - aliases: [m: :module] + switches: [module: :string, url: :string, file: :string], + aliases: [m: :module, f: :file] ) Mix.Shell.IO.info("Generating barebones implementation for #{name}.") Mix.Shell.IO.info("Hit enter to select the suggestion.") + module_suggestion = + name |> String.split() |> Enum.map(&String.capitalize(&1)) |> Enum.join("") + module_name = case Keyword.fetch(key_list, :module) do - :error -> prompt_with_suggestion("\nModule name", String.capitalize(name)) - {:ok, mod_name} -> mod_name + :error -> + prompt_with_suggestion("\nModule name", module_suggestion) + + {:ok, mod_name} -> + mod_name end url = @@ -66,18 +75,28 @@ Comma separated list of required configuration keys: :error -> prompt_with_suggestion( "\nHomepage URL", - "https://www.#{String.Casing.downcase(name)}.com" + "https://www.#{String.downcase(module_suggestion)}.com" ) {:ok, url} -> url end - file_name = prompt_with_suggestion("\nFilename", Macro.underscore(name)) + file_name = + case Keyword.fetch(key_list, :file) do + :error -> + prompt_with_suggestion("\nFilename", Macro.underscore(module_name) <> ".ex") + + {:ok, filename} -> + filename + end + + file_base_name = String.slice(file_name, 0..-4) required_keys = case Mix.Shell.IO.prompt(@long_msg) |> String.trim() do - "" -> [] + "" -> + [] keys -> String.split(keys, ",") |> Enum.map(&String.trim(&1)) |> Enum.map(&String.to_atom(&1)) @@ -87,10 +106,12 @@ Comma separated list of required configuration keys: gateway: name, gateway_module: module_name, gateway_underscore: file_name, + # The key :gateway_filename is not used in any template as of now. + gateway_filename: "#{file_name}", required_config_keys: required_keys, gateway_url: url, - mock_test_filename: file_name <> "_test", - mock_response_filename: file_name <> "_mock" + mock_test_filename: "#{file_base_name}_test.exs", + mock_response_filename: "#{file_base_name}_mock.exs" ] if Mix.Shell.IO.yes?( @@ -101,12 +122,12 @@ Comma separated list of required configuration keys: mock_response = EEx.eval_file("templates/mock_response.eex", bindings) integration = EEx.eval_file("templates/integration.eex", bindings) - create_file("lib/gringotts/gateways/#{bindings[:gateway_underscore]}.ex", gateway) - create_file("test/integration/gateways/#{bindings[:mock_test_filename]}.exs", integration) + create_file("lib/gringotts/gateways/#{bindings[:gateway_filename]}", gateway) + create_file("test/integration/gateways/#{bindings[:mock_test_filename]}", integration) if Mix.Shell.IO.yes?("\nAlso create empty mock test suite?\n>") do - create_file("test/gateways/#{bindings[:mock_test_filename]}.exs", mock) - create_file("test/mocks/#{bindings[:mock_response_filename]}.exs", mock_response) + create_file("test/gateways/#{bindings[:mock_test_filename]}", mock) + create_file("test/mocks/#{bindings[:mock_response_filename]}", mock_response) end else Mix.Shell.IO.info("Doing nothing, bye!") diff --git a/mix.exs b/mix.exs index f63edfc6..ef54cc7f 100644 --- a/mix.exs +++ b/mix.exs @@ -58,16 +58,16 @@ defmodule Gringotts.Mixfile do {:ex_money, "~> 1.1.0", only: [:dev, :test], optional: true}, # docs and tests - {:ex_doc, "~> 0.16", only: :dev, runtime: false}, + {:ex_doc, "~> 0.18", only: :dev, runtime: false}, {:mock, "~> 0.3.0", only: :test}, {:bypass, "~> 0.8", only: :test}, - {:excoveralls, "~> 0.7", only: :test}, + {:excoveralls, "~> 0.8", only: :test}, # various analyses tools {:credo, "~> 0.3", only: [:dev, :test]}, {:inch_ex, "~> 0.5", only: :docs}, {:dialyxir, "~> 0.3", only: :dev}, - {:timex, "~> 3.1"} + {:timex, "~> 3.2"} ] end diff --git a/mix.lock b/mix.lock index da4109e3..4e785b03 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ -%{"abnf2": {:hex, :abnf2, "0.1.2", "6f8792b8ac3288dba5fc889c2bceae9fe78f74e1a7b36bea9726ffaa9d7bef95", [:mix], [], "hexpm"}, +%{ + "abnf2": {:hex, :abnf2, "0.1.2", "6f8792b8ac3288dba5fc889c2bceae9fe78f74e1a7b36bea9726ffaa9d7bef95", [:mix], [], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "bypass": {:hex, :bypass, "0.8.1", "16d409e05530ece4a72fabcf021a3e5c7e15dcc77f911423196a0c551f2a15ca", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, @@ -17,8 +18,8 @@ "ex_money": {:hex, :ex_money, "1.1.2", "4336192f1ac263900dfb4f63c1f71bc36a7cdee5d900e81937d3213be3360f9f", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}, {:ex_cldr, "~> 1.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 1.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}], "hexpm"}, "excoveralls": {:hex, :excoveralls, "0.8.0", "99d2691d3edf8612f128be3f9869c4d44b91c67cec92186ce49470ae7a7404cf", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, - "gettext": {:hex, :gettext, "0.14.0", "1a019a2e51d5ad3d126efe166dcdf6563768e5d06c32a99ad2281a1fa94b4c72", [:mix], [], "hexpm"}, - "hackney": {:hex, :hackney, "1.10.1", "c38d0ca52ea80254936a32c45bb7eb414e7a96a521b4ce76d00a69753b157f21", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, + "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"}, + "hackney": {:hex, :hackney, "1.11.0", "4951ee019df102492dabba66a09e305f61919a8a183a7860236c0fde586134b6", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "httpoison": {:hex, :httpoison, "0.13.0", "bfaf44d9f133a6599886720f3937a7699466d23bb0cd7a88b6ba011f53c6f562", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "inch_ex": {:hex, :inch_ex, "0.5.6", "418357418a553baa6d04eccd1b44171936817db61f4c0840112b420b8e378e67", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, @@ -32,7 +33,8 @@ "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, - "timex": {:hex, :timex, "3.1.25", "6002dae5432f749d1c93e2cd103eb73cecb53e50d2c885349e8e4146fc96bd44", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, + "timex": {:hex, :timex, "3.2.1", "639975eac45c4c08c2dbf7fc53033c313ff1f94fad9282af03619a3826493612", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "tzdata": {:hex, :tzdata, "0.5.16", "13424d3afc76c68ff607f2df966c0ab4f3258859bbe3c979c9ed1606135e7352", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, - "xml_builder": {:hex, :xml_builder, "2.1.0", "c249d5339427c13cae11e9d9d0e8b40d25d228b9ecc54029f24017385e60280b", [:mix], [], "hexpm"}} + "xml_builder": {:hex, :xml_builder, "2.1.0", "c249d5339427c13cae11e9d9d0e8b40d25d228b9ecc54029f24017385e60280b", [:mix], [], "hexpm"}, +} diff --git a/templates/mock_response.eex b/templates/mock_response.eex index d4ad1f5b..b50bc667 100644 --- a/templates/mock_response.eex +++ b/templates/mock_response.eex @@ -1,6 +1,6 @@ defmodule Gringotts.Gateways.<%= gateway_module <> "Mock"%> do - # The module should include mock responses for test cases in <%= mock_test_filename <> ".exs"%>. + # The module should include mock responses for test cases in <%= mock_test_filename %>. # e.g. # def successful_purchase do # {:ok, %HTTPoison.Response{body: ~s[{data: "successful_purchase"}]} diff --git a/templates/test.eex b/templates/test.eex index 2fc65a79..93c0b62d 100644 --- a/templates/test.eex +++ b/templates/test.eex @@ -2,13 +2,13 @@ defmodule Gringotts.Gateways.<%= gateway_module <> "Test" %> do # The file contains mocked tests for <%= gateway_module%> # We recommend using [mock][1] for this, you can place the mock responses from - # the Gateway in `test/mocks/<%= mock_response_filename%>.exs` file, which has also been + # the Gateway in `test/mocks/<%= mock_response_filename %>` file, which has also been # generated for you. # # [1]: https://github.com/jjh42/mock # Load the mock response file before running the tests. - Code.require_file "../mocks/<%= mock_response_filename <> ".exs"%>", __DIR__ + Code.require_file "../mocks/<%= mock_response_filename %>", __DIR__ use ExUnit.Case, async: false alias Gringotts.Gateways.<%= gateway_module%> diff --git a/test/gateways/bogus_test.exs b/test/gateways/bogus_test.exs index 5810dfc0..041c1469 100644 --- a/test/gateways/bogus_test.exs +++ b/test/gateways/bogus_test.exs @@ -4,9 +4,12 @@ defmodule Gringotts.Gateways.BogusTest do alias Gringotts.Response alias Gringotts.Gateways.Bogus, as: Gateway + @some_id "some_arbitrary_id" + @amount Money.new(5, :USD) + test "authorize" do {:ok, %Response{id: id, success: success}} = - Gateway.authorize(10.95, :card, []) + Gateway.authorize(@amount, :card, []) assert success assert id != nil @@ -14,7 +17,7 @@ defmodule Gringotts.Gateways.BogusTest do test "purchase" do {:ok, %Response{id: id, success: success}} = - Gateway.purchase(10.95, :card, []) + Gateway.purchase(@amount, :card, []) assert success assert id != nil @@ -22,7 +25,7 @@ defmodule Gringotts.Gateways.BogusTest do test "capture" do {:ok, %Response{id: id, success: success}} = - Gateway.capture(1234, 5, []) + Gateway.capture(@some_id, @amount, []) assert success assert id != nil @@ -30,7 +33,7 @@ defmodule Gringotts.Gateways.BogusTest do test "void" do {:ok, %Response{id: id, success: success}} = - Gateway.void(1234, []) + Gateway.void(@some_id, []) assert success assert id != nil @@ -45,7 +48,7 @@ defmodule Gringotts.Gateways.BogusTest do test "unstore with customer" do {:ok, %Response{success: success}} = - Gateway.unstore(1234, []) + Gateway.unstore(@some_id, []) assert success end From 71a5bc397fbc8914995bf8dc10e50f5a1b5cbe29 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Thu, 15 Mar 2018 20:59:37 +0530 Subject: [PATCH 32/60] [monei] Test fixes (#116) * Ignore some optional params for RF, RV, CP Some optional params like billing, customer, merchant must not be expanded in case of capture, refund and void. * Improved mock tests, fixes #98 Mock tests now mostly check if the request is correctly built. Since most requests have common parameters, they are not checked everywhere. * Improve integration tests (more cases), fixes #108 * Integration tests no longer use the worker as a workaround for #8 * Added more test cases, can possibly be improved using describe blocks with local setup. * There are almost no assertions and it is expected that errors will bubble up to the pattern matches. --- lib/gringotts/gateways/monei.ex | 67 +++++++++----- mix.lock | 12 +-- test/gateways/monei_test.exs | 110 +++++++++++++++-------- test/integration/gateways/monei_test.exs | 104 +++++++++++++-------- 4 files changed, 195 insertions(+), 98 deletions(-) diff --git a/lib/gringotts/gateways/monei.ex b/lib/gringotts/gateways/monei.ex index eeef6ec6..2d08080c 100644 --- a/lib/gringotts/gateways/monei.ex +++ b/lib/gringotts/gateways/monei.ex @@ -338,6 +338,8 @@ defmodule Gringotts.Gateways.Monei do which can be used to effectively process _One-Click_ and _Recurring_ payments, and return a registration token for reference. + The registration token is available in the `Response.id` field. + It is recommended to associate these details with a "Customer" by passing customer details in the `opts`. @@ -352,7 +354,8 @@ defmodule Gringotts.Gateways.Monei do future use. iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> {:ok, store_result} = Gringotts.store(Gringotts.Gateways.Monei, card, []) + iex> {:ok, store_result} = Gringotts.store(Gringotts.Gateways.Monei, card) + iex> store_result.id # This is the registration token """ @spec store(CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def store(%CreditCard{} = card, opts) do @@ -438,7 +441,7 @@ defmodule Gringotts.Gateways.Monei do defp commit(:post, endpoint, params, opts) do url = "#{base_url(opts)}/#{version(opts)}/#{endpoint}" - case expand_params(opts, params[:paymentType]) do + case expand_params(Keyword.delete(opts, :config), params[:paymentType]) do {:error, reason} -> {:error, Response.error(reason: reason)} @@ -528,16 +531,16 @@ defmodule Gringotts.Gateways.Monei do else: {:halt, {:error, "Invalid currency"}} :customer -> - {:cont, acc ++ make("customer", v)} + {:cont, acc ++ make(action_type, "customer", v)} :merchant -> - {:cont, acc ++ make("merchant", v)} + {:cont, acc ++ make(action_type, "merchant", v)} :billing -> - {:cont, acc ++ make("billing", v)} + {:cont, acc ++ make(action_type, "billing", v)} :shipping -> - {:cont, acc ++ make("shipping", v)} + {:cont, acc ++ make(action_type, "shipping", v)} :invoice_id -> {:cont, [{"merchantInvoiceId", v} | acc]} @@ -549,23 +552,16 @@ defmodule Gringotts.Gateways.Monei do {:cont, [{"transactionCategory", v} | acc]} :shipping_customer -> - {:cont, acc ++ make("shipping.customer", v)} + {:cont, acc ++ make(action_type, "shipping.customer", v)} :custom -> {:cont, acc ++ make_custom(v)} :register -> - { - :cont, - if action_type in ["PA", "DB"] do - [{"createRegistration", true} | acc] - else - acc - end - } - - _ -> - {:cont, acc} + {:cont, acc ++ make(action_type, :register, v)} + + unsupported -> + {:halt, {:error, "Unsupported optional param '#{unsupported}'"}} end end) end @@ -574,8 +570,39 @@ defmodule Gringotts.Gateways.Monei do currency in @supported_currencies end - defp make(prefix, param) do - Enum.into(param, [], fn {k, v} -> {"#{prefix}.#{k}", v} end) + defp parse_response(%{"result" => result} = data) do + {address, zip_code} = @avs_code_translator[result["avsResponse"]] + + results = [ + code: result["code"], + description: result["description"], + risk: data["risk"]["score"], + cvc_result: @cvc_code_translator[result["cvvResponse"]], + avs_result: [address: address, zip_code: zip_code], + raw: data, + token: data["registrationId"] + ] + + filtered = Enum.filter(results, fn {_, v} -> v != nil end) + verify(filtered) + end + + defp verify(results) do + if String.match?(results[:code], ~r{^(000\.000\.|000\.100\.1|000\.[36])}) do + {:ok, results} + else + {:error, [{:reason, results[:description]} | results]} + end + end + + defp make(action_type, _prefix, _param) when action_type in ["CP", "RF", "RV"], do: [] + defp make(action_type, prefix, param) do + case prefix do + :register -> + if action_type in ["PA", "DB"], do: [createRegistration: true], else: [] + + _ -> Enum.into(param, [], fn {k, v} -> {"#{prefix}.#{k}", v} end) + end end defp make_custom(custom_map) do diff --git a/mix.lock b/mix.lock index 4e785b03..aaaa5cc1 100644 --- a/mix.lock +++ b/mix.lock @@ -12,11 +12,11 @@ "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm"}, "elixir_xml_to_map": {:hex, :elixir_xml_to_map, "0.1.1", "57e924cd11731947bfd245ce57d0b8dd8b7168bf8edb20cd156a2982ca96fdfa", [:mix], [{:erlsom, "~>1.4", [hex: :erlsom, repo: "hexpm", optional: false]}], "hexpm"}, "erlsom": {:hex, :erlsom, "1.4.1", "53dbacf35adfea6f0714fd0e4a7b0720d495e88c5e24e12c5dc88c7b62bd3e49", [:rebar3], [], "hexpm"}, - "ex_cldr": {:hex, :ex_cldr, "1.1.0", "26f4a206307770b70139214ab820c5ed1f6241eb3394dd0db216ff95bf7e213a", [:mix], [{:abnf2, "~> 0.1", [hex: :abnf2, repo: "hexpm", optional: false]}, {:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:poison, "~> 2.1 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, - "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "1.1.0", "75904f202ca602eca5f3af572d56ed3d4a51543fecd08c9ab626ae2d876f44da", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 1.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:poison, "~> 2.1 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, - "ex_money": {:hex, :ex_money, "1.1.2", "4336192f1ac263900dfb4f63c1f71bc36a7cdee5d900e81937d3213be3360f9f", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}, {:ex_cldr, "~> 1.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 1.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}], "hexpm"}, - "excoveralls": {:hex, :excoveralls, "0.8.0", "99d2691d3edf8612f128be3f9869c4d44b91c67cec92186ce49470ae7a7404cf", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "ex_cldr": {:hex, :ex_cldr, "1.4.4", "654966e8724d607e5cf9ecd5509ffcf66868b17e479bbd22ab2e9123595f9103", [:mix], [{:abnf2, "~> 0.1", [hex: :abnf2, repo: "hexpm", optional: false]}, {:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 2.1 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"}, + "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "1.3.1", "50a117654dff8f8ee6958e68a65d0c2835a7e2f1aff94c1ea8f582c04fdf0bd4", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 1.4.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.1 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, + "ex_money": {:hex, :ex_money, "1.1.3", "843eed0a5673206de33be47cdc06574401abc3e2d33cbcf6d74e160226791ae4", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}, {:ex_cldr, "~> 1.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 1.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}], "hexpm"}, + "excoveralls": {:hex, :excoveralls, "0.8.1", "0bbf67f22c7dbf7503981d21a5eef5db8bbc3cb86e70d3798e8c802c74fa5e27", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"}, "hackney": {:hex, :hackney, "1.11.0", "4951ee019df102492dabba66a09e305f61919a8a183a7860236c0fde586134b6", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, @@ -29,7 +29,7 @@ "mime": {:hex, :mime, "1.2.0", "78adaa84832b3680de06f88f0997e3ead3b451a440d183d688085be2d709b534", [:mix], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, "mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, - "plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, + "plug": {:hex, :plug, "1.5.0", "224b25b4039bedc1eac149fb52ed456770b9678bbf0349cdd810460e1e09195b", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, diff --git a/test/gateways/monei_test.exs b/test/gateways/monei_test.exs index 8d3a11ec..3aaa88a1 100644 --- a/test/gateways/monei_test.exs +++ b/test/gateways/monei_test.exs @@ -39,7 +39,7 @@ defmodule Gringotts.Gateways.MoneiTest do birthDate: "1980-07-31", mobile: "+15252525252", email: "masterofdeath@ministryofmagic.gov", - ip: "1.1.1", + ip: "127.0.0.1", status: "NEW" } @merchant %{ @@ -96,7 +96,7 @@ defmodule Gringotts.Gateways.MoneiTest do "card":{ "bin":"420000", "last4Digits":"0000", - "holder":"Jo Doe", + "holder":"Harry Potter", "expiryMonth":"12", "expiryYear":"2099" } @@ -123,16 +123,24 @@ defmodule Gringotts.Gateways.MoneiTest do end test "when MONEI is down or unreachable.", %{bypass: bypass, auth: auth} do - Bypass.expect_once(bypass, fn conn -> - Plug.Conn.resp(conn, 200, @auth_success) - end) - Bypass.down(bypass) {:error, response} = Gateway.authorize(@amount42, @card, config: auth) assert response.reason == "network related failure" - Bypass.up(bypass) - {:ok, _} = Gateway.authorize(@amount42, @card, config: auth) + end + + test "that all auth info is picked.", %{bypass: bypass, auth: auth} do + Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["authentication.entityId"] == "some_secret_entity_id" + assert params["authentication.password"] == "some_secret_password" + assert params["authentication.userId"] == "some_secret_user_id" + Plug.Conn.resp(conn, 200, @auth_success) + end) + + {:ok, response} = Gateway.purchase(@amount42, @card, config: auth) + assert response.gateway_code == "000.100.110" end test "with all extra_params.", %{bypass: bypass, auth: auth} do @@ -142,20 +150,21 @@ defmodule Gringotts.Gateways.MoneiTest do ] Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> - conn_ = parse(conn) - assert conn_.body_params["createRegistration"] == "true" - assert conn_.body_params["customParameters"] == @extra_opts[:custom] - assert conn_.body_params["merchantInvoiceId"] == randoms[:invoice_id] - assert conn_.body_params["merchantTransactionId"] == randoms[:transaction_id] - assert conn_.body_params["transactionCategory"] == @extra_opts[:category] - assert conn_.body_params["customer.merchantCustomerId"] == @customer[:merchantCustomerId] - - assert conn_.body_params["shipping.customer.merchantCustomerId"] == + p_conn = parse(conn) + params = p_conn.body_params + assert params["createRegistration"] == "true" + assert params["customParameters"] == @extra_opts[:custom] + assert params["merchantInvoiceId"] == randoms[:invoice_id] + assert params["merchantTransactionId"] == randoms[:transaction_id] + assert params["transactionCategory"] == @extra_opts[:category] + assert params["customer.merchantCustomerId"] == @customer[:merchantCustomerId] + + assert params["shipping.customer.merchantCustomerId"] == @customer[:merchantCustomerId] - assert conn_.body_params["merchant.submerchantId"] == @merchant[:submerchantId] - assert conn_.body_params["billing.city"] == @billing[:city] - assert conn_.body_params["shipping.method"] == @shipping[:method] + assert params["merchant.submerchantId"] == @merchant[:submerchantId] + assert params["billing.city"] == @billing[:city] + assert params["shipping.method"] == @shipping[:method] Plug.Conn.resp(conn, 200, @register_success) end) @@ -165,7 +174,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert response.token == "8a82944a60e09c550160e92da144491e" end - test "when card has expired.", %{bypass: bypass, auth: auth} do + test "when we get non-json.", %{bypass: bypass, auth: auth} do Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> Plug.Conn.resp(conn, 400, "") end) @@ -177,6 +186,11 @@ defmodule Gringotts.Gateways.MoneiTest do describe "authorize" do test "when all is good.", %{bypass: bypass, auth: auth} do Bypass.expect(bypass, "POST", "/v1/payments", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "42.00" + assert params["currency"] == "USD" + assert params["paymentType"] == "PA" Plug.Conn.resp(conn, 200, @auth_success) end) @@ -188,6 +202,11 @@ defmodule Gringotts.Gateways.MoneiTest do describe "purchase" do test "when all is good.", %{bypass: bypass, auth: auth} do Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "42.00" + assert params["currency"] == "USD" + assert params["paymentType"] == "DB" Plug.Conn.resp(conn, 200, @auth_success) end) @@ -197,8 +216,9 @@ defmodule Gringotts.Gateways.MoneiTest do test "with createRegistration.", %{bypass: bypass, auth: auth} do Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> - conn_ = parse(conn) - assert conn_.body_params["createRegistration"] == "true" + p_conn = parse(conn) + params = p_conn.body_params + assert params["createRegistration"] == "true" Plug.Conn.resp(conn, 200, @register_success) end) @@ -211,6 +231,14 @@ defmodule Gringotts.Gateways.MoneiTest do describe "store" do test "when all is good.", %{bypass: bypass, auth: auth} do Bypass.expect_once(bypass, "POST", "/v1/registrations", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + params["card.cvv"] == "123" + params["card.expiryMonth"] == "12" + params["card.expiryYear"] == "2099" + params["card.holder"] == "Harry Potter" + params["card.number"] == "4200000000000000" + params["paymentBrand"] == "VISA" Plug.Conn.resp(conn, 200, @store_success) end) @@ -226,6 +254,11 @@ defmodule Gringotts.Gateways.MoneiTest do "POST", "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "42.00" + assert params["currency"] == "USD" + assert params["paymentType"] == "CP" Plug.Conn.resp(conn, 200, @auth_success) end ) @@ -242,8 +275,9 @@ defmodule Gringotts.Gateways.MoneiTest do "POST", "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> - conn_ = parse(conn) - assert :error == Map.fetch(conn_.body_params, "createRegistration") + p_conn = parse(conn) + params = p_conn.body_params + assert :error == Map.fetch(params, "createRegistration") Plug.Conn.resp(conn, 200, @auth_success) end ) @@ -267,6 +301,11 @@ defmodule Gringotts.Gateways.MoneiTest do "POST", "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert params["amount"] == "3.00" + assert params["currency"] == "USD" + assert params["paymentType"] == "RF" Plug.Conn.resp(conn, 200, @auth_success) end ) @@ -284,6 +323,11 @@ defmodule Gringotts.Gateways.MoneiTest do "DELETE", "/v1/registrations/7214344242e11af79c0b9e7b4f3f6234", fn conn -> + p_conn = parse(conn) + params = p_conn.query_params + assert params["authentication.entityId"] == "some_secret_entity_id" + assert params["authentication.password"] == "some_secret_password" + assert params["authentication.userId"] == "some_secret_user_id" Plug.Conn.resp(conn, 200, "") end ) @@ -300,6 +344,11 @@ defmodule Gringotts.Gateways.MoneiTest do "POST", "/v1/payments/7214344242e11af79c0b9e7b4f3f6234", fn conn -> + p_conn = parse(conn) + params = p_conn.body_params + assert :error == Map.fetch(params, :amount) + assert :error == Map.fetch(params, :currency) + assert params["paymentType"] == "RV" Plug.Conn.resp(conn, 200, @auth_success) end ) @@ -309,17 +358,6 @@ defmodule Gringotts.Gateways.MoneiTest do end end - @tag :skip - test "respond various scenarios, can't test a private function." do - json_200 = %HTTPoison.Response{body: @auth_success, status_code: 200} - json_not_200 = %HTTPoison.Response{body: @auth_success, status_code: 300} - html_200 = %HTTPoison.Response{body: ~s[\n], status_code: 200} - html_not_200 = %HTTPoison.Response{body: ~s[ - assert response.gateway_code == "000.100.110" - - assert response.message == - "Request successfully processed in 'Merchant in Integrator Test Mode'" - - assert String.length(response.id) == 32 - + test "[authorize] without tokenisation", %{opts: opts} do + with {:ok, auth_result} <- Gateway.authorize(@amount, @card, opts), + {:ok, _capture_result} <- Gateway.capture(auth_result.id, @amount, opts) do + "yay!" + else {:error, _err} -> flunk() end end - @tag :skip - test "capture", %{opts: _opts} do - case Gringotts.capture(Gateway, "s", @amount) do - {:ok, response} -> - assert response.gateway_code == "000.100.110" - - assert response.message == - "Request successfully processed in 'Merchant in Integrator Test Mode'" - - assert String.length(response.id) == 32 + test "[authorize -> capture] with tokenisation", %{opts: opts} do + with {:ok, auth_result} <- Gateway.authorize(@amount, @card, opts ++ [register: true]), + {:ok, _registration_token} <- Map.fetch(auth_result, :token), + {:ok, _capture_result} <- Gateway.capture(auth_result.id, @amount, opts) do + "yay!" + else {:error, _err} -> + flunk() + end + end - {:error, _err} -> + test "[authorize -> void]", %{opts: opts} do + with {:ok, auth_result} <- Gateway.authorize(@amount, @card, opts), + {:ok, _void_result} <- Gateway.void(auth_result.id, opts) do + "yay!" + else {:error, _err} -> flunk() end end - test "purchase", %{opts: opts} do - case Gringotts.purchase(Gateway, @amount, @card, opts) do - {:ok, response} -> - assert response.gateway_code == "000.100.110" + test "[purchase/capture -> void]", %{opts: opts} do + with {:ok, purchase_result} <- Gateway.purchase(@amount, @card, opts), + {:ok, _void_result} <- Gateway.void(purchase_result.id, opts) do + "yay!" + else {:error, _err} -> + flunk() + end + end - assert response.message == - "Request successfully processed in 'Merchant in Integrator Test Mode'" + test "[purchase/capture -> refund] (partial)", %{opts: opts} do + with {:ok, purchase_result} <- Gateway.purchase(@amount, @card, opts), + {:ok, _refund_result} <- Gateway.refund(@sub_amount, purchase_result.id, opts) do + "yay!" + else {:error, _err} -> + flunk() + end + end - assert String.length(response.id) == 32 + test "[store]", %{opts: opts} do + assert {:ok, _store_result} = Gateway.store(@card, opts) + end - {:error, _err} -> + @tag :skip + test "[store -> unstore]", %{opts: opts} do + with {:ok, store_result} <- Gateway.store(@card, opts), + {:ok, _unstore_result} <- Gateway.unstore(store_result.id, opts) do + "yay!" + else {:error, _err} -> flunk() end end + + test "[purchase]", %{opts: opts} do + assert {:ok, _response} = Gateway.purchase(@amount, @card, opts) + end + + test "Environment setup" do + config = Application.get_env(:gringotts, Gringotts.Gateways.Monei) + assert config[:adapter] == Gringotts.Gateways.Monei + end end From cf39d54d27e9a13cce97f9787f7bec606b82b83d Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Mon, 19 Mar 2018 11:45:40 +0530 Subject: [PATCH 33/60] Fix gringotts.new Option.parse call. (#125) --- lib/mix/new.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/new.ex b/lib/mix/new.ex index 02f4058b..7b4fcd2e 100644 --- a/lib/mix/new.ex +++ b/lib/mix/new.ex @@ -48,7 +48,7 @@ Comma separated list of required configuration keys: > } def run(args) do - {key_list, name, []} = + {key_list, [name], []} = OptionParser.parse( args, switches: [module: :string, url: :string, file: :string], From a9b76dbfc7c7c46accf00776ff6fbf1def25e122 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Mon, 29 Jan 2018 22:09:12 +0530 Subject: [PATCH 34/60] Adapts Stripe with the money protocol * Fixes #5 and #109 * Moved the stripe_test to integration. * Fixed credo issue in money integration test Changes to MONEI * Removed unnecessary clauses from MONEI * Re-formatted source. --- lib/gringotts/gateways/base.ex | 35 +++---- lib/gringotts/gateways/monei.ex | 53 +++-------- lib/gringotts/gateways/stripe.ex | 107 +++++++++++----------- test/gateways/monei_test.exs | 12 +-- test/gateways/stripe_test.exs | 58 ------------ test/integration/gateways/stripe_test.exs | 44 +++++++++ test/integration/money.exs | 2 +- 7 files changed, 132 insertions(+), 179 deletions(-) delete mode 100644 test/gateways/stripe_test.exs create mode 100644 test/integration/gateways/stripe_test.exs diff --git a/lib/gringotts/gateways/base.ex b/lib/gringotts/gateways/base.ex index 92f8cfd2..be881992 100644 --- a/lib/gringotts/gateways/base.ex +++ b/lib/gringotts/gateways/base.ex @@ -1,4 +1,18 @@ defmodule Gringotts.Gateways.Base do + @moduledoc """ + Dummy implementation of the Gringotts API + + All gateway implementations must `use` this module as it provides (pseudo) + implementations for the all methods of the Gringotts API. + + In case `GatewayXYZ` does not implement `unstore`, the following call would + not raise an error: + ``` + Gringotts.unstore(GatewayXYZ, "some_registration_id") + ``` + because this module provides an implementation. + """ + alias Gringotts.Response defmacro __using__(_) do @@ -38,27 +52,6 @@ defmodule Gringotts.Gateways.Base do not_implemented() end - defp http(method, path, params \\ [], opts \\ []) do - credentials = Keyword.get(opts, :credentials) - headers = [{"Content-Type", "application/x-www-form-urlencoded"}] - data = params_to_string(params) - - HTTPoison.request(method, path, data, headers, [hackney: [basic_auth: credentials]]) - end - - defp money_to_cents(amount) when is_float(amount) do - trunc(amount * 100) - end - - defp money_to_cents(amount) do - amount * 100 - end - - defp params_to_string(params) do - params |> Enum.filter(fn {_k, v} -> v != nil end) - |> URI.encode_query - end - @doc false defp not_implemented do {:error, Response.error(code: :not_implemented)} diff --git a/lib/gringotts/gateways/monei.ex b/lib/gringotts/gateways/monei.ex index 2d08080c..4d659656 100644 --- a/lib/gringotts/gateways/monei.ex +++ b/lib/gringotts/gateways/monei.ex @@ -153,15 +153,11 @@ defmodule Gringotts.Gateways.Monei do @base_url "https://test.monei-api.net" @default_headers ["Content-Type": "application/x-www-form-urlencoded", charset: "UTF-8"] - @supported_currencies [ - "AED", "AFN", "ANG", "AOA", "AWG", "AZN", "BAM", "BGN", "BRL", "BYN", "CDF", - "CHF", "CUC", "EGP", "EUR", "GBP", "GEL", "GHS", "MDL", "MGA", "MKD", "MWK", - "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "PAB", "PEN", "PGK", "PHP", - "PKR", "PLN", "PYG", "QAR", "RSD", "RUB", "RWF", "SAR", "SCR", "SDG", "SEK", - "SGD", "SHP", "SLL", "SOS", "SRD", "STD", "SYP", "SZL", "THB", "TJS", "TOP", - "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS", "VND", "VUV", - "WST", "XAF", "XCD", "XOF", "XPF", "YER", "ZAR", "ZMW", "ZWL" - ] + @supported_currencies ~w(AED AFN ANG AOA AWG AZN BAM BGN BRL BYN CDF CHF CUC + EGP EUR GBP GEL GHS MDL MGA MKD MWK MZN NAD NGN NIO NOK NPR NZD PAB PEN PGK + PHP PKR PLN PYG QAR RSD RUB RWF SAR SCR SDG SEK SGD SHP SLL SOS SRD STD SYP + SZL THB TJS TOP TRY TTD TWD TZS UAH UGX USD UYU UZS VND VUV WST XAF XCD XOF + XPF YER ZAR ZMW ZWL) @version "v1" @@ -435,7 +431,6 @@ defmodule Gringotts.Gateways.Monei do ] end - # Makes the request to MONEI's network. @spec commit(atom, String.t(), keyword, keyword) :: {:ok | :error, Response.t()} defp commit(:post, endpoint, params, opts) do @@ -447,7 +442,10 @@ defmodule Gringotts.Gateways.Monei do validated_params -> url - |> HTTPoison.post({:form, params ++ validated_params ++ auth_params(opts)}, @default_headers) + |> HTTPoison.post( + {:form, params ++ validated_params ++ auth_params(opts)}, + @default_headers + ) |> respond end end @@ -458,7 +456,7 @@ defmodule Gringotts.Gateways.Monei do auth_params = auth_params(opts) query_string = auth_params |> URI.encode_query() - base_url <> "?" <> query_string + (base_url <> "?" <> query_string) |> HTTPoison.delete() |> respond end @@ -472,7 +470,7 @@ defmodule Gringotts.Gateways.Monei do common = [raw: body, status_code: 200] with {:ok, decoded_json} <- decode(body), - {:ok, results} <- parse_response(decoded_json) do + {:ok, results} <- parse_response(decoded_json) do {:ok, Response.success(common ++ results)} else {:not_ok, errors} -> @@ -570,38 +568,15 @@ defmodule Gringotts.Gateways.Monei do currency in @supported_currencies end - defp parse_response(%{"result" => result} = data) do - {address, zip_code} = @avs_code_translator[result["avsResponse"]] - - results = [ - code: result["code"], - description: result["description"], - risk: data["risk"]["score"], - cvc_result: @cvc_code_translator[result["cvvResponse"]], - avs_result: [address: address, zip_code: zip_code], - raw: data, - token: data["registrationId"] - ] - - filtered = Enum.filter(results, fn {_, v} -> v != nil end) - verify(filtered) - end - - defp verify(results) do - if String.match?(results[:code], ~r{^(000\.000\.|000\.100\.1|000\.[36])}) do - {:ok, results} - else - {:error, [{:reason, results[:description]} | results]} - end - end - defp make(action_type, _prefix, _param) when action_type in ["CP", "RF", "RV"], do: [] + defp make(action_type, prefix, param) do case prefix do :register -> if action_type in ["PA", "DB"], do: [createRegistration: true], else: [] - _ -> Enum.into(param, [], fn {k, v} -> {"#{prefix}.#{k}", v} end) + _ -> + Enum.into(param, [], fn {k, v} -> {"#{prefix}.#{k}", v} end) end end diff --git a/lib/gringotts/gateways/stripe.ex b/lib/gringotts/gateways/stripe.ex index 7c1befd2..94a8ce48 100644 --- a/lib/gringotts/gateways/stripe.ex +++ b/lib/gringotts/gateways/stripe.ex @@ -1,9 +1,8 @@ defmodule Gringotts.Gateways.Stripe do - @moduledoc """ Stripe gateway implementation. For reference see [Stripe's API documentation](https://stripe.com/docs/api). The following features of Stripe are implemented: - + | Action | Method | | ------ | ------ | | Pre-authorize | `authorize/3` | @@ -18,7 +17,7 @@ defmodule Gringotts.Gateways.Stripe do Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply optional arguments for transactions with the Stripe gateway. The following keys are supported: - + | Key | Remark | Status | | ---- | --- | ---- | | `currency` | | **Implemented** | @@ -38,18 +37,18 @@ defmodule Gringotts.Gateways.Stripe do | `default_source` | | Not implemented | | `email` | | Not implemented | | `shipping` | | Not implemented | - + ## Registering your Stripe account at `Gringotts` After [making an account on Stripe](https://stripe.com/), head to the dashboard and find your account `secrets` in the `API` section. - + ## Here's how the secrets map to the required configuration parameters for Stripe: | Config parameter | Stripe secret | | ------- | ---- | | `:secret_key` | **Secret key** | - + Your Application config must look something like this: - + config :gringotts, Gringotts.Gateways.Stripe, secret_key: "your_secret_key", default_currency: "usd" @@ -58,11 +57,12 @@ defmodule Gringotts.Gateways.Stripe do @base_url "https://api.stripe.com/v1" use Gringotts.Gateways.Base - use Gringotts.Adapter, required_config: [:secret_key, :default_currency] + use Gringotts.Adapter, required_config: [:secret_key] alias Gringotts.{ CreditCard, - Address + Address, + Money } @doc """ @@ -71,17 +71,17 @@ defmodule Gringotts.Gateways.Stripe do The authorization validates the card details with the banking network, places a hold on the transaction amount in the customer’s issuing bank and also triggers risk management. Funds are not transferred. - + Stripe returns an `charge_id` which should be stored at your side and can be used later to: * `capture/3` an amount. * `void/2` a pre-authorization. - + ## Note Uncaptured charges expire in 7 days. For more information, [see authorizing charges and settling later](https://support.stripe.com/questions/can-i-authorize-a-charge-and-then-wait-to-settle-it-later). ## Example The following session shows how one would (pre) authorize a payment of $10 on a sample `card`. - + iex> card = %CreditCard{ first_name: "John", last_name: "Smith", @@ -104,7 +104,7 @@ defmodule Gringotts.Gateways.Stripe do iex> Gringotts.authorize(Gringotts.Gateways.Stripe, amount, card, opts) """ - @spec authorize(number, CreditCard.t() | String.t(), keyword) :: map + @spec authorize(Money.t(), CreditCard.t() | String.t(), keyword) :: map def authorize(amount, payment, opts \\ []) do params = create_params_for_auth_or_purchase(amount, payment, opts, false) commit(:post, "charges", params, opts) @@ -112,10 +112,10 @@ defmodule Gringotts.Gateways.Stripe do @doc """ Transfers amount from the customer to the merchant. - + Stripe attempts to process a purchase on behalf of the customer, by debiting amount from the customer's account by charging the customer's card. - + ## Example The following session shows how one would process a payment in one-shot, without (pre) authorization. @@ -142,7 +142,7 @@ defmodule Gringotts.Gateways.Stripe do iex> Gringotts.purchase(Gringotts.Gateways.Stripe, amount, card, opts) """ - @spec purchase(number, CreditCard.t() | String.t(), keyword) :: map + @spec purchase(Money.t(), CreditCard.t() | String.t(), keyword) :: map def purchase(amount, payment, opts \\ []) do params = create_params_for_auth_or_purchase(amount, payment, opts) commit(:post, "charges", params, opts) @@ -168,7 +168,7 @@ defmodule Gringotts.Gateways.Stripe do iex> Gringotts.capture(Gringotts.Gateways.Stripe, id, amount, opts) """ - @spec capture(String.t(), number, keyword) :: map + @spec capture(String.t(), Money.t(), keyword) :: map def capture(id, amount, opts \\ []) do params = optional_params(opts) ++ amount_params(amount) commit(:post, "charges/#{id}/capture", params, opts) @@ -176,7 +176,7 @@ defmodule Gringotts.Gateways.Stripe do @doc """ Voids the referenced payment. - + This method attempts a reversal of the either a previous `purchase/3` or `authorize/3` referenced by `charge_id`. As a consequence, the customer will never see any booking on his @@ -190,7 +190,7 @@ defmodule Gringotts.Gateways.Stripe do ## Voiding a previous purchase Stripe will reverse the payment, by sending all the amount back to the customer. Note that this is not the same as `refund/3`. - + ## Example The following session shows how one would void a previous (pre) authorization. Remember that our `capture/3` example only did a partial @@ -223,7 +223,7 @@ defmodule Gringotts.Gateways.Stripe do iex> Gringotts.refund(Gringotts.Gateways.Stripe, amount, id, opts) """ - @spec refund(number, String.t(), keyword) :: map + @spec refund(Money.t(), String.t(), keyword) :: map def refund(amount, id, opts \\ []) do params = optional_params(opts) ++ amount_params(amount) commit(:post, "charges/#{id}/refund", params, opts) @@ -231,7 +231,7 @@ defmodule Gringotts.Gateways.Stripe do @doc """ Stores the payment-source data for later use. - + Stripe can store the payment-source details, for example card which can be used to effectively to process One-Click and Recurring_ payments, and return a `customer_id` for reference. @@ -285,27 +285,22 @@ defmodule Gringotts.Gateways.Stripe do # Private methods defp create_params_for_auth_or_purchase(amount, payment, opts, capture \\ true) do - params = optional_params(opts) - ++ [capture: capture] - ++ amount_params(amount) - ++ source_params(payment, opts) - - params - |> Keyword.has_key?(:currency) - |> with_currency(params, opts[:config]) + [capture: capture] ++ + optional_params(opts) ++ amount_params(amount) ++ source_params(payment, opts) end - def with_currency(true, params, _), do: params - def with_currency(false, params, config), do: [{:currency, config[:default_currency]} | params] - defp create_card_token(params, opts) do commit(:post, "tokens", params, opts) end - defp amount_params(amount), do: [amount: money_to_cents(amount)] + defp amount_params(amount) do + {currency, int_value, _} = Money.to_integer(amount) + [amount: int_value, currency: currency] + end defp source_params(token_or_customer, _) when is_binary(token_or_customer) do [head, _] = String.split(token_or_customer, "_") + case head do "tok" -> [source: token_or_customer] "cus" -> [customer: token_or_customer] @@ -313,64 +308,68 @@ defmodule Gringotts.Gateways.Stripe do end defp source_params(%CreditCard{} = card, opts) do - params = - card_params(card) ++ - address_params(opts[:address]) + params = card_params(card) ++ address_params(opts[:address]) response = create_card_token(params, opts) - case Map.has_key?(response, "error") do - true -> [] - false -> response - |> Map.get("id") - |> source_params(opts) + if Map.has_key?(response, "error") do + [] + else + response + |> Map.get("id") + |> source_params(opts) end end defp source_params(_, _), do: [] defp card_params(%CreditCard{} = card) do - [ "card[name]": CreditCard.full_name(card), + [ + "card[name]": CreditCard.full_name(card), "card[number]": card.number, "card[exp_year]": card.year, "card[exp_month]": card.month, "card[cvc]": card.verification_code - ] + ] end defp card_params(_), do: [] defp address_params(%Address{} = address) do - [ "card[address_line1]": address.street1, + [ + "card[address_line1]": address.street1, "card[address_line2]": address.street2, - "card[address_city]": address.city, + "card[address_city]": address.city, "card[address_state]": address.region, - "card[address_zip]": address.postal_code, + "card[address_zip]": address.postal_code, "card[address_country]": address.country ] end defp address_params(_), do: [] - defp commit(method, path, params \\ [], opts \\ []) do + defp commit(method, path, params, opts) do auth_token = "Bearer " <> opts[:config][:secret_key] - headers = [{"Content-Type", "application/x-www-form-urlencoded"}, {"Authorization", auth_token}] - data = params_to_string(params) - response = HTTPoison.request(method, "#{@base_url}/#{path}", data, headers) + + headers = [ + {"Content-Type", "application/x-www-form-urlencoded"}, + {"Authorization", auth_token} + ] + + response = HTTPoison.request(method, "#{@base_url}/#{path}", {:form, params}, headers) format_response(response) end defp optional_params(opts) do opts - |> Keyword.delete(:config) - |> Keyword.delete(:address) + |> Keyword.delete(:config) + |> Keyword.delete(:address) end defp format_response(response) do case response do - {:ok, %HTTPoison.Response{body: body}} -> body |> Poison.decode! + {:ok, %HTTPoison.Response{body: body}} -> body |> Poison.decode!() _ -> %{"error" => "something went wrong, please try again later"} end end - end diff --git a/test/gateways/monei_test.exs b/test/gateways/monei_test.exs index 3aaa88a1..ae41e9a0 100644 --- a/test/gateways/monei_test.exs +++ b/test/gateways/monei_test.exs @@ -233,12 +233,12 @@ defmodule Gringotts.Gateways.MoneiTest do Bypass.expect_once(bypass, "POST", "/v1/registrations", fn conn -> p_conn = parse(conn) params = p_conn.body_params - params["card.cvv"] == "123" - params["card.expiryMonth"] == "12" - params["card.expiryYear"] == "2099" - params["card.holder"] == "Harry Potter" - params["card.number"] == "4200000000000000" - params["paymentBrand"] == "VISA" + assert params["card.cvv"] == "123" + assert params["card.expiryMonth"] == "12" + assert params["card.expiryYear"] == "2099" + assert params["card.holder"] == "Harry Potter" + assert params["card.number"] == "4200000000000000" + assert params["paymentBrand"] == "VISA" Plug.Conn.resp(conn, 200, @store_success) end) diff --git a/test/gateways/stripe_test.exs b/test/gateways/stripe_test.exs deleted file mode 100644 index 73bc211a..00000000 --- a/test/gateways/stripe_test.exs +++ /dev/null @@ -1,58 +0,0 @@ -defmodule Gringotts.Gateways.StripeTest do - - use ExUnit.Case - - alias Gringotts.Gateways.Stripe - alias Gringotts.{ - CreditCard, - Address - } - - @card %CreditCard{ - first_name: "John", - last_name: "Smith", - number: "4242424242424242", - year: "2017", - month: "12", - verification_code: "123" - } - - @address %Address{ - street1: "123 Main", - street2: "Suite 100", - city: "New York", - region: "NY", - country: "US", - postal_code: "11111" - } - - @required_opts [config: [api_key: "sk_test_vIX41hayC0BKrPWQerLuOMld"], currency: "usd"] - @optional_opts [address: @address] - - describe "authorize/3" do - # test "should authorize wth card and required opts attrs" do - # amount = 5 - # response = Stripe.authorize(amount, @card, @required_opts ++ @optional_opts) - - # assert Map.has_key?(response, "id") - # assert response["amount"] == 500 - # assert response["captured"] == false - # assert response["currency"] == "usd" - # end - - # test "should not authorize if card is not passed" do - # amount = 5 - # response = Stripe.authorize(amount, %{}, @required_opts ++ @optional_opts) - - # assert Map.has_key?(response, "error") - # end - - # test "should not authorize if required opts not present" do - # amount = 5 - # response = Stripe.authorize(amount, @card, @optional_opts) - - # assert Map.has_key?(response, "error") - # end - - end -end diff --git a/test/integration/gateways/stripe_test.exs b/test/integration/gateways/stripe_test.exs new file mode 100644 index 00000000..b390f989 --- /dev/null +++ b/test/integration/gateways/stripe_test.exs @@ -0,0 +1,44 @@ +defmodule Gringotts.Gateways.StripeTest do + + use ExUnit.Case + + alias Gringotts.Gateways.Stripe + alias Gringotts.{ + CreditCard, + Address + } + + @moduletag integration: true + + @amount Money.new(5, :USD) + @card %CreditCard{ + first_name: "John", + last_name: "Smith", + number: "4242424242424242", + year: "2068", # Can't be more than 50 years in the future, Haha. + month: "12", + verification_code: "123" + } + + @address %Address{ + street1: "123 Main", + street2: "Suite 100", + city: "New York", + region: "NY", + country: "US", + postal_code: "11111" + } + + @required_opts [config: [secret_key: "sk_test_vIX41hayC0BKrPWQerLuOMld"]] + @optional_opts [address: @address] + + describe "authorize/3" do + test "with correct params" do + response = Stripe.authorize(@amount, @card, @required_opts ++ @optional_opts) + assert Map.has_key?(response, "id") + assert response["amount"] == 500 + assert response["captured"] == false + assert response["currency"] == "usd" + end + end +end diff --git a/test/integration/money.exs b/test/integration/money.exs index ca42febe..3f5691ba 100644 --- a/test/integration/money.exs +++ b/test/integration/money.exs @@ -26,7 +26,7 @@ defmodule Gringotts.Integration.Gateways.MoneyTest do test "to_integer" do assert match? {"EUR", 4200, -2}, MoneyProtocol.to_integer(@ex_money) - assert match? {"BHD", 42000, -3}, MoneyProtocol.to_integer(@ex_money_bhd) + assert match? {"BHD", 42_000, -3}, MoneyProtocol.to_integer(@ex_money_bhd) end test "to_string" do From f99f2586a3e5a968836474e67e48621b051110b1 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Fri, 9 Feb 2018 14:03:24 +0530 Subject: [PATCH 35/60] Adapted Trexle for new `Response.t` * Trexle does not seem to provide fraud risk, AVS, CVV validation results. There are no docs for this. --- lib/gringotts/gateways/trexle.ex | 12 ++++++-- test/gateways/trexle_test.exs | 52 ++++++++++++-------------------- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/lib/gringotts/gateways/trexle.ex b/lib/gringotts/gateways/trexle.ex index 523d4c2f..a9c3d988 100644 --- a/lib/gringotts/gateways/trexle.ex +++ b/lib/gringotts/gateways/trexle.ex @@ -331,17 +331,23 @@ defmodule Gringotts.Gateways.Trexle do { :ok, - Response.success(authorization: token, message: message, raw: results, status_code: code) + %Response{id: token, message: message, raw: body, status_code: code} } end defp respond({:ok, %{status_code: status_code, body: body}}) do {:ok, results} = decode(body) detail = results["detail"] - {:error, Response.error(status_code: status_code, message: detail, raw: results)} + {:error, %Response{status_code: status_code, message: detail, reason: detail, raw: body}} end defp respond({:error, %HTTPoison.Error{} = error}) do - {:error, Response.error(code: error.id, message: "HTTPoison says '#{error.reason}'")} + { + :error, + %Response{ + reason: "network related failure", + message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]" + } + } end end diff --git a/test/gateways/trexle_test.exs b/test/gateways/trexle_test.exs index f8a50562..7078b9f6 100644 --- a/test/gateways/trexle_test.exs +++ b/test/gateways/trexle_test.exs @@ -14,8 +14,8 @@ defmodule Gringotts.Gateways.TrexleTest do @valid_card %CreditCard{ first_name: "Harry", last_name: "Potter", - number: "4200000000000000", - year: 2099, + number: "4000056655665556", + year: 2068, month: 12, verification_code: "123", brand: "VISA" @@ -24,7 +24,7 @@ defmodule Gringotts.Gateways.TrexleTest do @invalid_card %CreditCard{ first_name: "Harry", last_name: "Potter", - number: "4200000000000000", + number: "4000056655665556", year: 2010, month: 12, verification_code: "123", @@ -46,10 +46,10 @@ defmodule Gringotts.Gateways.TrexleTest do # 50 US cents, trexle does not work with amount smaller than 50 cents. @bad_amount Money.new("0.49", :USD) - @valid_token "7214344252e11af79c0b9e7b4f3f6234" - @invalid_token "14a62fff80f24a25f775eeb33624bbb3" + @valid_token "some_valid_token" + @invalid_token "some_invalid_token" - @auth %{api_key: "7214344252e11af79c0b9e7b4f3f6234"} + @auth %{api_key: "some_api_key"} @opts [ config: @auth, email: "masterofdeath@ministryofmagic.gov", @@ -64,10 +64,7 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_purchase_with_valid_card() end do - {:ok, response} = Trexle.purchase(@amount, @valid_card, @opts) - assert response.status_code == 201 - assert response.raw["response"]["success"] == true - assert response.raw["response"]["captured"] == false + assert {:ok, response} = Trexle.purchase(@amount, @valid_card, @opts) end end @@ -76,10 +73,8 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_purchase_with_invalid_card() end do - {:error, response} = Trexle.purchase(@amount, @invalid_card, @opts) - assert response.status_code == 400 - assert response.success == false - assert response.message == "Your card's expiration year is invalid." + assert {:error, response} = Trexle.purchase(@amount, @invalid_card, @opts) + assert response.reason == "Your card's expiration year is invalid." end end @@ -88,10 +83,9 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_purchase_with_invalid_amount() end do - {:error, response} = Trexle.purchase(@bad_amount, @valid_card, @opts) + assert {:error, response} = Trexle.purchase(@bad_amount, @valid_card, @opts) assert response.status_code == 400 - assert response.success == false - assert response.message == "Amount must be at least 50 cents" + assert response.reason == "Amount must be at least 50 cents" end end end @@ -102,10 +96,8 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_authorize_with_valid_card() end do - {:ok, response} = Trexle.authorize(@amount, @valid_card, @opts) + assert {:ok, response} = Trexle.authorize(@amount, @valid_card, @opts) assert response.status_code == 201 - assert response.raw["response"]["success"] == true - assert response.raw["response"]["captured"] == false end end end @@ -116,10 +108,8 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_authorize_with_valid_card() end do - {:ok, response} = Trexle.refund(@amount, @valid_token, @opts) + assert {:ok, response} = Trexle.refund(@amount, @valid_token, @opts) assert response.status_code == 201 - assert response.raw["response"]["success"] == true - assert response.raw["response"]["captured"] == false end end end @@ -130,11 +120,9 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_capture_with_valid_chargetoken() end do - {:ok, response} = Trexle.capture(@valid_token, @amount, @opts) + assert {:ok, response} = Trexle.capture(@valid_token, @amount, @opts) + # Why 200 here?? It's 201 everywhere lese. Check trexle docs. assert response.status_code == 200 - assert response.raw["response"]["success"] == true - assert response.raw["response"]["captured"] == true - assert response.message == "Transaction approved" end end @@ -143,10 +131,9 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_capture_with_invalid_chargetoken() end do - {:error, response} = Trexle.capture(@invalid_token, @amount, @opts) + assert {:error, response} = Trexle.capture(@invalid_token, @amount, @opts) assert response.status_code == 400 - assert response.success == false - assert response.message == "invalid token" + assert response.reason == "invalid token" end end end @@ -157,7 +144,7 @@ defmodule Gringotts.Gateways.TrexleTest do request: fn _method, _url, _body, _headers, _options -> MockResponse.test_for_store_with_valid_card() end do - {:ok, response} = Trexle.store(@valid_card, @opts) + assert {:ok, response} = Trexle.store(@valid_card, @opts) assert response.status_code == 201 end end @@ -170,8 +157,7 @@ defmodule Gringotts.Gateways.TrexleTest do MockResponse.test_for_network_failure() end do {:error, response} = Trexle.authorize(@amount, @valid_card, @opts) - assert response.success == false - assert response.message == "HTTPoison says 'some_hackney_error'" + assert response.message == "HTTPoison says 'some_hackney_error' [ID: some_hackney_error_id]" end end end From a91deec959e21aa3e61c726faf9c4ead08d46598 Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Wed, 21 Mar 2018 17:25:51 +0530 Subject: [PATCH 36/60] [CAMS] Adapt for new Response.t (#120) * Refactored ResponseHandler, updated Response.t * CAMS now parses AVS and CVV response - that was missing till now. * Removes unnecessary `parse` clause. * Mock tests shouldn't use gringotts.ex --- lib/gringotts/gateways/cams.ex | 151 ++++++++++++++++++--------------- test/gateways/cams_test.exs | 93 +++++++++----------- 2 files changed, 123 insertions(+), 121 deletions(-) diff --git a/lib/gringotts/gateways/cams.ex b/lib/gringotts/gateways/cams.ex index 0cb0c4e8..12687f6b 100644 --- a/lib/gringotts/gateways/cams.ex +++ b/lib/gringotts/gateways/cams.ex @@ -144,7 +144,7 @@ defmodule Gringotts.Gateways.Cams do ## Optional Fields options[ order_id: String, - description: String + description: String ] ## Examples @@ -163,7 +163,7 @@ defmodule Gringotts.Gateways.Cams do iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Cams, money, card) ``` """ - @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def authorize(money, %CreditCard{} = card, options) do params = [] @@ -210,7 +210,7 @@ defmodule Gringotts.Gateways.Cams do iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Cams, money, authorization) ``` """ - @spec capture(Money.t(), String.t(), keyword) :: {:ok | :error, Response} + @spec capture(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} def capture(money, transaction_id, options) do params = [transactionid: transaction_id] @@ -246,7 +246,7 @@ defmodule Gringotts.Gateways.Cams do iex> Gringotts.purchase(Gringotts.Gateways.Cams, money, card) ``` """ - @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def purchase(money, %CreditCard{} = card, options) do params = [] @@ -278,7 +278,7 @@ defmodule Gringotts.Gateways.Cams do iex> Gringotts.refund(Gringotts.Gateways.Cams, money, capture_id) ``` """ - @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response.t()} def refund(money, transaction_id, options) do params = [transactionid: transaction_id] @@ -305,7 +305,7 @@ defmodule Gringotts.Gateways.Cams do iex> Gringotts.void(Gringotts.Gateways.Cams, auth_id) ``` """ - @spec void(String.t(), keyword) :: {:ok | :error, Response} + @spec void(String.t(), keyword) :: {:ok | :error, Response.t()} def void(transaction_id, options) do params = [transactionid: transaction_id] commit("void", params, options) @@ -328,7 +328,7 @@ defmodule Gringotts.Gateways.Cams do iex> Gringotts.validate(Gringotts.Gateways.Cams, card) ``` """ - @spec validate(CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec validate(CreditCard.t(), keyword) :: {:ok | :error, Response.t()} def validate(card, options) do params = [] @@ -378,76 +378,93 @@ defmodule Gringotts.Gateways.Cams do @moduledoc false alias Gringotts.Response + # Fetched from CAMS POST API docs. + @avs_code_translator %{ + "X" => {nil, "pass: 9-character numeric ZIP"}, + "Y" => {nil, "pass: 5-character numeric ZIP"}, + "D" => {nil, "pass: 5-character numeric ZIP"}, + "M" => {nil, "pass: 5-character numeric ZIP"}, + "2" => {"pass: customer name", "pass: 5-character numeric ZIP"}, + "6" => {"pass: customer name", "pass: 5-character numeric ZIP"}, + "A" => {"pass: only address", "fail"}, + "B" => {"pass: only address", "fail"}, + "3" => {"pass: address, customer name", "fail"}, + "7" => {"pass: address, customer name", "fail"}, + "W" => {"fail", "pass: 9-character numeric ZIP match"}, + "Z" => {"fail", "pass: 5-character ZIP match"}, + "P" => {"fail", "pass: 5-character ZIP match"}, + "L" => {"fail", "pass: 5-character ZIP match"}, + "1" => {"pass: only customer name", "pass: 5-character ZIP"}, + "5" => {"pass: only customer name", "pass: 5-character ZIP"}, + "N" => {"fail", "fail"}, + "C" => {"fail", "fail"}, + "4" => {"fail", "fail"}, + "8" => {"fail", "fail"}, + "U" => {nil, nil}, + "G" => {nil, nil}, + "I" => {nil, nil}, + "R" => {nil, nil}, + "E" => {nil, nil}, + "S" => {nil, nil}, + "0" => {nil, nil}, + "O" => {nil, nil}, + "" => {nil, nil} + } + + # Fetched from CAMS POST API docs. + @cvc_code_translator %{ + "M" => "pass", + "N" => "fail", + "P" => "not_processed", + "S" => "Merchant indicated that CVV2/CVC2 is not present on card", + "U" => "Issuer is not certified and/or has not provided Visa encryption key" + } + @doc false def parse({:ok, %HTTPoison.Response{body: body, status_code: 200}}) do - body = URI.decode_query(body) - - [status_code: 200] - |> set_authorization(body) - |> set_success(body) - |> set_message(body) - |> set_params(body) - |> set_error_code(body) - |> handle_opts() - end - - def parse({:ok, %HTTPoison.Response{body: body, status_code: 400}}) do - body = URI.decode_query(body) - set_params([status_code: 400], body) + decoded_body = URI.decode_query(body) + {street, zip_code} = @avs_code_translator[decoded_body["avsresponse"]] + gateway_code = decoded_body["response_code"] + message = decoded_body["responsetext"] + response = %Response{ + status_code: 200, + id: decoded_body["transactionid"], + gateway_code: gateway_code, + avs_result: %{street: street, zip_code: zip_code}, + cvc_result: @cvc_code_translator[decoded_body["cvvresponse"]], + message: decoded_body["responsetext"], + raw: body + } + + if successful?(gateway_code) do + {:ok, response} + else + {:error, %{response | reason: message}} + end end - def parse({:ok, %HTTPoison.Response{body: body, status_code: 404}}) do - body = URI.decode_query(body) + def parse({:ok, %HTTPoison.Response{body: body, status_code: code}}) do + response = %Response{ + status_code: code, + raw: body + } - [status_code: 404] - |> handle_not_found(body) - |> handle_opts() + {:error, response} end def parse({:error, %HTTPoison.Error{} = error}) do - [ - message: "HTTPoison says #{error.reason}", - error_code: error.id, - success: false - ] - end - - defp set_authorization(opts, %{"transactionid" => id}) do - opts ++ [authorization: id] - end - - defp set_message(opts, %{"responsetext" => message}) do - opts ++ [message: message] - end - - defp set_params(opts, body) do - opts ++ [params: body] - end - - defp set_error_code(opts, %{"response_code" => response_code}) do - opts ++ [error_code: response_code] - end - - defp set_success(opts, %{"response_code" => response_code}) do - opts ++ [success: response_code == "100"] + { + :error, + %Response{ + reason: "network related failure", + message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]", + success: false + } + } end - defp handle_not_found(opts, body) do - error = parse_html(body) - opts ++ [success: false, message: error] - end - - defp parse_html(body) do - error_message = List.to_string(Map.keys(body)) - [_ | parse_message] = Regex.run(~r|(.*)|, error_message) - List.to_string(parse_message) - end - - defp handle_opts(opts) do - case Keyword.fetch(opts, :success) do - {:ok, true} -> {:ok, Response.success(opts)} - {:ok, false} -> {:ok, Response.error(opts)} - end + defp successful?(gateway_code) do + gateway_code == "100" end end end diff --git a/test/gateways/cams_test.exs b/test/gateways/cams_test.exs index 6faf56b2..edf0e5f4 100644 --- a/test/gateways/cams_test.exs +++ b/test/gateways/cams_test.exs @@ -43,9 +43,10 @@ defmodule Gringotts.Gateways.CamsTest do } @auth %{username: "some_secret_user_name", password: "some_secret_password"} @options [ - order_id: 0001, + order_id: 1, billing_address: @address, - description: "Store Purchase" + description: "Store Purchase", + config: @auth ] @money Money.new(:USD, 100) @@ -53,40 +54,32 @@ defmodule Gringotts.Gateways.CamsTest do @money_less Money.new(:USD, 99) @bad_currency Money.new(:INR, 100) - @authorization "some_transaction_id" - @bad_authorization "some_fake_transaction_id" - - setup_all do - Application.put_env(:gringotts, Gateway, - username: "some_secret_user_name", - password: "some_secret_password" - ) - end + @id "some_transaction_id" + @bad_id "some_fake_transaction_id" describe "purchase" do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_purchase() end do - {:ok, %Response{success: result}} = Gringotts.purchase(Gateway, @money, @card, @options) - assert result + assert {:ok, %Response{}} = Gateway.purchase(@money, @card, @options) end end test "with bad card" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.failed_purchase_with_bad_credit_card() end do - {:ok, %Response{message: result}} = - Gringotts.purchase(Gateway, @money, @bad_card, @options) + {:error, %Response{reason: reason}} = + Gateway.purchase(@money, @bad_card, @options) - assert String.contains?(result, "Invalid Credit Card Number") + assert String.contains?(reason, "Invalid Credit Card Number") end end test "with invalid currency" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.with_invalid_currency() end do - {:ok, %Response{message: result}} = Gringotts.purchase(Gateway, @bad_currency, @card, @options) - assert String.contains?(result, "The cc payment type") + {:error, %Response{reason: reason}} = Gateway.purchase(@bad_currency, @card, @options) + assert String.contains?(reason, "The cc payment type") end end end @@ -95,18 +88,17 @@ defmodule Gringotts.Gateways.CamsTest do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_authorize() end do - {:ok, %Response{success: result}} = Gringotts.authorize(Gateway, @money, @card, @options) - assert result + assert {:ok, %Response{}} = Gateway.authorize(@money, @card, @options) end end test "with bad card" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.failed_authorized_with_bad_card() end do - {:ok, %Response{message: result}} = - Gringotts.authorize(Gateway, @money, @bad_card, @options) + {:error, %Response{reason: reason}} = + Gateway.authorize(@money, @bad_card, @options) - assert String.contains?(result, "Invalid Credit Card Number") + assert String.contains?(reason, "Invalid Credit Card Number") end end end @@ -114,49 +106,45 @@ defmodule Gringotts.Gateways.CamsTest do describe "capture" do test "with full amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_capture() end do - {:ok, %Response{success: result}} = - Gringotts.capture(Gateway, @money, @authorization, @options) - - assert result + assert {:ok, %Response{}} = + Gateway.capture(@money, @id , @options) end end test "with partial amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_capture() end do - {:ok, %Response{success: result}} = - Gringotts.capture(Gateway, @money_less, @authorization, @options) - - assert result + assert {:ok, %Response{}} = + Gateway.capture(@money_less, @id , @options) end end test "with invalid transaction_id" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.invalid_transaction_id() end do - {:ok, %Response{message: result}} = - Gringotts.capture(Gateway, @money, @bad_authorization, @options) + {:error, %Response{reason: reason}} = + Gateway.capture(@money, @bad_id, @options) - assert String.contains?(result, "Transaction not found") + assert String.contains?(reason, "Transaction not found") end end - test "with more than authorization amount" do + test "with more than authorized amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.more_than_authorization_amount() end do - {:ok, %Response{message: result}} = - Gringotts.capture(Gateway, @money_more, @authorization, @options) + {:error, %Response{reason: reason}} = + Gateway.capture(@money_more, @id , @options) - assert String.contains?(result, "exceeds the authorization amount") + assert String.contains?(reason, "exceeds the authorization amount") end end test "on already captured transaction" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.multiple_capture_on_same_transaction() end do - {:ok, %Response{message: result}} = - Gringotts.capture(Gateway, @money, @authorization, @options) + {:error, %Response{reason: reason}} = + Gateway.capture(@money, @id , @options) - assert String.contains?(result, "A capture requires that") + assert String.contains?(reason, "A capture requires that") end end end @@ -164,20 +152,18 @@ defmodule Gringotts.Gateways.CamsTest do describe "refund" do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_refund() end do - {:ok, %Response{success: result}} = - Gringotts.refund(Gateway, @money, @authorization, @options) - - assert result + assert {:ok, %Response{}} = + Gateway.refund(@money, @id , @options) end end test "with more than purchased amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.more_than_purchase_amount() end do - {:ok, %Response{message: result}} = - Gringotts.refund(Gateway, @money_more, @authorization, @options) + {:error, %Response{reason: reason}} = + Gateway.refund(@money_more, @id , @options) - assert String.contains?(result, "Refund amount may not exceed") + assert String.contains?(reason, "Refund amount may not exceed") end end end @@ -185,16 +171,16 @@ defmodule Gringotts.Gateways.CamsTest do describe "void" do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_void() end do - {:ok, %Response{message: result}} = Gringotts.void(Gateway, @authorization, @options) - assert String.contains?(result, "Void Successful") + {:ok, %Response{message: message}} = Gateway.void(@id , @options) + assert String.contains?(message, "Void Successful") end end test "with invalid transaction_id" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.invalid_transaction_id() end do - {:ok, %Response{message: result}} = Gringotts.void(Gateway, @bad_authorization, @options) - assert String.contains?(result, "Transaction not found") + {:error, %Response{reason: reason}} = Gateway.void(@bad_id, @options) + assert String.contains?(reason, "Transaction not found") end end end @@ -203,8 +189,7 @@ defmodule Gringotts.Gateways.CamsTest do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.validate_creditcard() end do - {:ok, %Response{success: result}} = Gateway.validate(@card, @options ++ [config: @auth]) - assert result + assert {:ok, %Response{}} = Gateway.validate(@card, @options ++ [config: @auth]) end end end From 7bf1857c0cd0c2cc48a76b8a77eb7e6335465b9c Mon Sep 17 00:00:00 2001 From: Ananya Bahadur Date: Thu, 22 Mar 2018 12:42:27 +0530 Subject: [PATCH 37/60] Format project and migrate to CodeCov (#135) * Update travis config, add .formater.exs * Migrate to CodeCov * Travis will run the formatter check * Add git-hooks and update contribution guide * Changed credo lin-length config from 80 to 100 * Ran the formatter on the project * Fix credo warnings --- .credo.exs | 2 +- .formatter.exs | 7 + .scripts/inch_report.sh | 20 + .scripts/post-commit | 12 + .scripts/pre-commit | 50 ++ .travis.yml | 26 +- CONTRIBUTING.md | 83 ++- lib/gringotts.ex | 54 +- lib/gringotts/adapter.ex | 15 +- lib/gringotts/address.ex | 2 + lib/gringotts/credit_card.ex | 22 +- lib/gringotts/gateways/authorize_net.ex | 80 ++- lib/gringotts/gateways/base.ex | 14 +- lib/gringotts/gateways/bogus.ex | 28 +- lib/gringotts/gateways/cams.ex | 3 +- lib/gringotts/gateways/global_collect.ex | 60 +- lib/gringotts/gateways/paymill.ex | 263 ++++----- lib/gringotts/gateways/wire_card.ex | 193 ++++--- lib/gringotts/response.ex | 39 +- mix.exs | 18 +- test/gateways/authorize_net_test.exs | 37 +- test/gateways/bogus_test.exs | 21 +- test/gateways/cams_test.exs | 29 +- test/gateways/global_collect_test.exs | 84 ++- test/gateways/monei_test.exs | 32 +- test/gateways/trexle_test.exs | 4 +- test/gateways/wire_card_test.exs | 12 +- test/gringotts_test.exs | 6 +- test/integration/gateways/monei_test.exs | 17 +- test/integration/gateways/stripe_test.exs | 7 +- test/integration/money.exs | 32 +- test/mocks/authorize_net_mock.exs | 673 +++++++++++++--------- test/mocks/cams_mock.exs | 350 +++++------ test/mocks/global_collect_mock.exs | 255 ++++---- test/mocks/trexle_mock.exs | 340 +++++------ 35 files changed, 1623 insertions(+), 1267 deletions(-) create mode 100644 .formatter.exs create mode 100644 .scripts/inch_report.sh create mode 100755 .scripts/post-commit create mode 100755 .scripts/pre-commit diff --git a/.credo.exs b/.credo.exs index df92ae85..9381d3f7 100644 --- a/.credo.exs +++ b/.credo.exs @@ -77,7 +77,7 @@ {Credo.Check.Readability.FunctionNames}, {Credo.Check.Readability.LargeNumbers}, - {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 80}, + {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 100}, {Credo.Check.Readability.ModuleAttributeNames}, {Credo.Check.Readability.ModuleDoc}, {Credo.Check.Readability.ModuleNames}, diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 00000000..aa758aff --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,7 @@ +[ + inputs: [ + "{lib,config}/**/*.{ex,exs}", # lib and config + "test/**/*.{ex,exs}", # tests + "mix.exs" + ] +] diff --git a/.scripts/inch_report.sh b/.scripts/inch_report.sh new file mode 100644 index 00000000..a352261d --- /dev/null +++ b/.scripts/inch_report.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -e +bold=$(tput bold) +purple='\e[106m' +normal=$(tput sgr0) +allowed_branches="^(master)|(develop)$" + +echo -e "${bold}${purple}" +if [ $TRAVIS_PULL_REQUEST = false ]; then + if [[ $TRAVIS_BRANCH =~ $allowed_branches ]]; then + env MIX_ENV=docs mix deps.get + env MIX_ENV=docs mix inch.report + else + echo "Skipping Inch CI report because this branch does not match on /$allowed_branches/" + fi +else + echo "Skipping Inch CI report because this is a PR build" +fi +echo -e "${normal}" diff --git a/.scripts/post-commit b/.scripts/post-commit new file mode 100755 index 00000000..ada60194 --- /dev/null +++ b/.scripts/post-commit @@ -0,0 +1,12 @@ +#!/bin/sh +# +# Runs credo and the formatter on the staged files, after the commit is made +# This is purely for notification and will not halt/change your commit. + +RED='\033[1;31m' +LGRAY='\033[1;30m' +NC='\033[0m' # No Color + +printf "${RED}Running 'mix credo --strict --format=oneline' on project...${NC}\n" +mix credo --strict --format=oneline +echo diff --git a/.scripts/pre-commit b/.scripts/pre-commit new file mode 100755 index 00000000..1f473c8c --- /dev/null +++ b/.scripts/pre-commit @@ -0,0 +1,50 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# Also run the mix format task, just check though. +exec mix format --check-formatted + diff --git a/.travis.yml b/.travis.yml index 82c3f5a9..a238ae93 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,28 @@ language: elixir -elixir: - - 1.5.2 + otp_release: - - 20.1 + - 20.2 before_install: - mix local.hex --force - mix local.rebar --force - mix deps.get script: - - mix coveralls.travis --include integration + - set -e + - MIX_ENV=test mix format --check-formatted + - set +e + - mix coveralls.json --include=integration after_script: - - MIX_ENV=docs mix deps.get - - MIX_ENV=docs mix inch.report + - bash <(curl -s https://codecov.io/bash) + - bash .scripts/inch_report.sh + +matrix: + include: + - elixir: "1.5.3" + script: + - mix coveralls.json --include=integration + - elixir: "1.6.2" + +notifications: + email: + recipients: + - ananya95+travis@gmail.com diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6eeb7033..f096ca9d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,40 +24,42 @@ processed. [roadmap]: https://github.com/aviabird/gringotts/wiki/Roadmap [wiki-arch]: https://github.com/aviabird/gringotts/wiki/Architecture -### PR submission checklist +# Style Guidelines -Each PR should introduce a *focussed set of changes*, and ideally not span over -unrelated modules. +We follow +[lexmag/elixir-style-guide](https://github.com/lexmag/elixir-style-guide) and +[rrrene/elixir-style-guide](https://github.com/rrrene/elixir-style-guide) (both +overlap a lot), and use the elixir formatter. -* [ ] Run the edited files through [credo][credo] and the Elixir - [formatter][hashrocket-formatter] (new in `v1.6`). -* [ ] Check the test coverage by running `mix coveralls`. 100% coverage is not - strictly required. -* [ ] If the PR introduces a new Gateway or just Gateway specific changes, - please format the title like so,\ - `[] ` +To enforce these, and also to make it easier for new contributors to adhere to +our style, we've provided a collection of handy `git-hooks` under the `.scripts/` +directory. -[gringotts]: https://github.com/aviabird/gringotts -[milestones]: https://github.com/aviabird/gringotts/milestones -[issues]: https://github.com/aviabird/gringotts/issues -[first-issues]: https://github.com/aviabird/gringotts/issues?q=is%3Aissue+is%3Aopen+label%3A"good+first+issue" -[ch-issues]: https://github.com/aviabird/gringotts/issues?q=is%3Aissue+is%3Aopen+label%3A"hotfix%3A+community-help" -[hexdocs]: https://hexdocs.pm/gringotts -[credo]: https://github.com/rrrene/credo -[hashrocket-formatter]: https://hashrocket.com/blog/posts/format-your-elixir-code-now +* `.scripts/pre-commit` Runs the `format --check-formatted` task. +* `.scripts/post-commit` Runs a customised `credo` check. -# Style Guidelines +While we do not force you to use these hooks, you could write your +very own by taking inspiration from ours :smile: -We use [`credo`][credo] and the elixir formatter for consistent style, so please -use them! +To set the `git-hooks` as provided, go to the repo root, +```sh +cd path/to/gringotts/ +``` +and make these symbolic links: +```sh +ln -s .scripts/pre-commit .git/hooks/pre-commit +ln -s .scripts/post-commit .git/hooks/post-commit +``` + +> Note that our CI will fail your PR if you dont run `mix format` in the project +> root. ## General Rules -* Keep line length below 120 characters. +* Keep line length below 100 characters. * Complex anonymous functions should be extracted into named functions. * One line functions, should only take up one line! -* Pipes are great, but don't use them, if they are less readable than brackets - then drop the pipe! +* Pipes are great, but don't use them if they are less readable than brackets! ## Writing documentation @@ -77,6 +79,39 @@ inspiration. using `mock` as it constrains tests to run serially. Use [`mox`][mox] instead.\ Take a look at [MONEI's mock tests][src-monei-tests] for inspiration. +# PR submission checklist + +Each PR should introduce a *focussed set of changes*, and ideally not span over +unrelated modules. + +* [ ] Format the project with the Elixir formatter. + ```sh + cd path/to/gringotts/ + mix format + ``` +* [ ] Run the edited files through [credo][credo] with the `--strict` flag. + ```sh + cd path/to/gringotts/ + mix credo --strict + ``` +* [ ] Check the test coverage by running `mix coveralls`. 100% coverage is not + strictly required. +* [ ] If the PR introduces a new Gateway or just Gateway specific changes, + please format the title like so,\ + `[] ` + +> **Note** +> You can skip the first two steps if you have set up `git-hooks` as we have +> provided! + +[gringotts]: https://github.com/aviabird/gringotts +[milestones]: https://github.com/aviabird/gringotts/milestones +[issues]: https://github.com/aviabird/gringotts/issues +[first-issues]: https://github.com/aviabird/gringotts/issues?q=is%3Aissue+is%3Aopen+label%3A"good+first+issue" +[ch-issues]: https://github.com/aviabird/gringotts/issues?q=is%3Aissue+is%3Aopen+label%3A"hotfix%3A+community-help" +[hexdocs]: https://hexdocs.pm/gringotts +[credo]: https://github.com/rrrene/credo + -------------------------------------------------------------------------------- > **Where to next?** diff --git a/lib/gringotts.ex b/lib/gringotts.ex index 11bda944..003c6aba 100644 --- a/lib/gringotts.ex +++ b/lib/gringotts.ex @@ -6,21 +6,21 @@ defmodule Gringotts do easy for merchants to use multiple gateways. All gateways must conform to the API as described in this module, but can also support more gateway features than those required by Gringotts. - + ## Standard API arguments All requests to Gringotts are served by a supervised worker, this might be made optional in future releases. - + ### `gateway` (Module) Name - + The `gateway` to which this request is made. This is required in all API calls because Gringotts supports multiple Gateways. #### Example If you've configured Gringotts to work with Stripe, you'll do this to make an `authorization` request: - + Gringotts.authorize(Gingotts.Gateways.Stripe, other args ...) ### `amount` _and currency_ @@ -39,7 +39,7 @@ defmodule Gringotts do Otherwise, just wrap your `amount` with the `currency` together in a `Map` like so, money = %{value: Decimal.new("100.50"), currency: "USD"} - + > When this highly precise `amount` is serialized into the network request, we > use a potentially lossy `Gringotts.Money.to_string/1` or > `Gringotts.Money.to_integer/1` to perform rounding (if required) using the @@ -49,14 +49,14 @@ defmodule Gringotts do > STRONGLY RECOMMEND that merchants perform any required rounding and handle > remainders in their application logic -- before passing the `amount` to > Gringotts's API.** - + #### Example If you use `ex_money` in your project, and want to make an authorization for $2.99 to the `XYZ` Gateway, you'll do the following: # the money lib is aliased as "MoneyLib" - + amount = MoneyLib.new("2.99", :USD) Gringotts.authorize(Gringotts.Gateways.XYZ, amount, some_card, extra_options) @@ -65,7 +65,7 @@ defmodule Gringotts do [money]: https://hexdocs.pm/money/Money.html [iss-money-lib-support]: https://github.com/aviabird/gringotts/projects/3#card-6801146 [wiki-half-even]: https://en.wikipedia.org/wiki/Rounding#Round_half_to_even - + ### `card`, a payment source Gringotts provides a `Gringotts.CreditCard` type to hold card parameters @@ -78,7 +78,7 @@ defmodule Gringotts do gateways might support payment via other instruments such as e-wallets, vouchers, bitcoins or banks. Support for these instruments is planned in future releases. - + %CreditCard { first_name: "Harry", last_name: "Potter", @@ -93,18 +93,18 @@ defmodule Gringotts do `opts` is a `keyword` list of other options/information accepted by the gateway. The format, use and structure is gateway specific and documented in the Gateway's docs. - + ## Configuration - + Merchants must provide Gateway specific configuration in their application config in the usual elixir style. The required and optional fields are documented in every Gateway. - + > The required config keys are validated at runtime, as they include > authentication information. See `Gringotts.Adapter.validate_config/2`. - + ### Global config - + This is set using the `:global_config` key once in your application. #### `:mode` @@ -120,9 +120,9 @@ defmodule Gringotts do environments. * `:prod` -- for live environment, all requests will reach the financial and banking networks. Switch to this in your application's `:prod` environment. - + **Example** - + config :gringotts, :global_config, # for live environment mode: :prod @@ -136,7 +136,7 @@ defmodule Gringotts do # some_documented_key: associated_value # some_other_key: another_value """ - + @doc """ Performs a (pre) Authorize operation. @@ -171,7 +171,7 @@ defmodule Gringotts do * multiple captures, per authorization ## Example - + To capture $4.20 on a previously authorized payment worth $4.20 by referencing the obtained authorization `id` with the `XYZ` gateway, @@ -181,7 +181,7 @@ defmodule Gringotts do card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} Gringotts.capture(Gringotts.Gateways.XYZ, amount, auth_result.id, opts) """ - def capture(gateway, id, amount, opts \\ []) do + def capture(gateway, id, amount, opts \\ []) do config = get_and_validate_config(gateway) gateway.capture(id, amount, [{:config, config} | opts]) end @@ -195,14 +195,14 @@ defmodule Gringotts do This method _can_ be implemented as a chained call to `authorize/3` and `capture/3`. But it must be implemented as a single call to the Gateway if it provides a specific endpoint or action for this. - + > ***Note!** > All gateways must implement (atleast) this method. ## Example To process a purchase worth $4.2, with the `XYZ` gateway, - + amount = Money.new("4.2", :USD) # IF YOU DON'T USE ex_money # amount = %{value: Decimal.new("4.2"), currency: "EUR"} @@ -229,7 +229,7 @@ defmodule Gringotts do # amount = %{value: Decimal.new("4.2"), currency: "EUR"} Gringotts.purchase(Gringotts.Gateways.XYZ, amount, id, opts) """ - def refund(gateway, amount, id, opts \\ []) do + def refund(gateway, amount, id, opts \\ []) do config = get_and_validate_config(gateway) gateway.refund(amount, id, [{:config, config} | opts]) end @@ -238,7 +238,7 @@ defmodule Gringotts do Stores the payment-source data for later use, returns a `token`. > The token must be returned in the `Response.authorization` field. - + ## Note This usually enables _One-Click_ and _Recurring_ payments. @@ -250,7 +250,7 @@ defmodule Gringotts do card = %Gringotts.CreditCard{first_name: "Jo", last_name: "Doe", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} Gringotts.store(Gringotts.Gateways.XYZ, card, opts) """ - def store(gateway, card, opts \\ []) do + def store(gateway, card, opts \\ []) do config = get_and_validate_config(gateway) gateway.store(card, [{:config, config} | opts]) end @@ -264,11 +264,11 @@ defmodule Gringotts do ## Example To unstore with the `XYZ` gateway, - + token = "some_privileged_customer" Gringotts.unstore(Gringotts.Gateways.XYZ, token) """ - def unstore(gateway, token, opts \\ []) do + def unstore(gateway, token, opts \\ []) do config = get_and_validate_config(gateway) gateway.unstore(token, [{:config, config} | opts]) end @@ -289,7 +289,7 @@ defmodule Gringotts do id = "some_previously_obtained_token" Gringotts.void(Gringotts.Gateways.XYZ, id, opts) """ - def void(gateway, id, opts \\ []) do + def void(gateway, id, opts \\ []) do config = get_and_validate_config(gateway) gateway.void(id, [{:config, config} | opts]) end diff --git a/lib/gringotts/adapter.ex b/lib/gringotts/adapter.ex index 978cd1d1..5edd06f2 100644 --- a/lib/gringotts/adapter.ex +++ b/lib/gringotts/adapter.ex @@ -15,7 +15,7 @@ defmodule Gringotts.Adapter do Say a merchant must provide his `secret_user_name` and `secret_password` to some Gateway `XYZ`. Then, `Gringotts` expects that the `GatewayXYZ` module would use `Adapter` in the following manner: - + ``` defmodule Gringotts.Gateways.GatewayXYZ do @@ -38,7 +38,7 @@ defmodule Gringotts.Adapter do ``` """ - + defmacro __using__(opts) do quote bind_quoted: [opts: opts] do @required_config opts[:required_config] || [] @@ -50,16 +50,19 @@ defmodule Gringotts.Adapter do is not available or missing from the Application config. """ def validate_config(config) do - missing_keys = Enum.reduce(@required_config, [], fn(key, missing_keys) -> - if config[key] in [nil, ""], do: [key | missing_keys], else: missing_keys - end) + missing_keys = + Enum.reduce(@required_config, [], fn key, missing_keys -> + if config[key] in [nil, ""], do: [key | missing_keys], else: missing_keys + end) + raise_on_missing_config(missing_keys, config) end defp raise_on_missing_config([], _config), do: :ok + defp raise_on_missing_config(key, config) do raise ArgumentError, """ - expected #{inspect key} to be set, got: #{inspect config} + expected #{inspect(key)} to be set, got: #{inspect(config)} """ end end diff --git a/lib/gringotts/address.ex b/lib/gringotts/address.ex index e1c50c95..57c8dd5c 100644 --- a/lib/gringotts/address.ex +++ b/lib/gringotts/address.ex @@ -1,3 +1,5 @@ defmodule Gringotts.Address do + @moduledoc false + defstruct [:street1, :street2, :city, :region, :country, :postal_code, :phone] end diff --git a/lib/gringotts/credit_card.ex b/lib/gringotts/credit_card.ex index 01811e00..e98481a0 100644 --- a/lib/gringotts/credit_card.ex +++ b/lib/gringotts/credit_card.ex @@ -4,6 +4,7 @@ defmodule Gringotts.CreditCard do """ defstruct [:number, :month, :year, :first_name, :last_name, :verification_code, :brand] + @typedoc """ Represents a Credit Card. @@ -29,23 +30,24 @@ defmodule Gringotts.CreditCard do [mo]: http://www.maestrocard.com/gateway/index.html [dc]: http://www.dinersclub.com/ """ - @type t :: %__MODULE__{number: String.t, - month: 1..12, - year: non_neg_integer, - first_name: String.t, - last_name: String.t, - verification_code: String.t, - brand: String.t} + @type t :: %__MODULE__{ + number: String.t(), + month: 1..12, + year: non_neg_integer, + first_name: String.t(), + last_name: String.t(), + verification_code: String.t(), + brand: String.t() + } @doc """ Returns the full name of the card holder. Joins `first_name` and `last_name` with a space in between. """ - @spec full_name(t) :: String.t + @spec full_name(t) :: String.t() def full_name(card) do name = "#{card.first_name} #{card.last_name}" - String.trim(name) + String.trim(name) end - end diff --git a/lib/gringotts/gateways/authorize_net.ex b/lib/gringotts/gateways/authorize_net.ex index 59281314..2612bb7a 100644 --- a/lib/gringotts/gateways/authorize_net.ex +++ b/lib/gringotts/gateways/authorize_net.ex @@ -89,7 +89,7 @@ defmodule Gringotts.Gateways.AuthorizeNet do that gives you a pre-configured sample app ready-to-go. + You could use the same config or update it the with your "secrets" [above](#module-configuring-your-authorizenet-account-at-gringotts). - + 2. To save a lot of time, create a [`.iex.exs`][iex-docs] file as shown in [this gist][authorize_net.iex.exs] to introduce a set of handy bindings and aliases. @@ -226,7 +226,6 @@ defmodule Gringotts.Gateways.AuthorizeNet do customer_ip: String ] - ## Example iex> amount = Money.new(20, :USD) iex> opts = [ @@ -749,21 +748,36 @@ defmodule Gringotts.Gateways.AuthorizeNet do ] @avs_code_translator %{ - "A" => {"pass", "fail"}, #The street address matched, but the postal code did not. - "B" => {nil, nil}, # No address information was provided. - "E" => {"fail", nil}, # The AVS check returned an error. - "G" => {nil, nil}, # The card was issued by a bank outside the U.S. and does not support AVS. - "N" => {"fail", "fail"}, # Neither the street address nor postal code matched. - "P" => {nil, nil}, # AVS is not applicable for this transaction. - "R" => {nil, nil}, # Retry — AVS was unavailable or timed out. - "S" => {nil, nil}, # AVS is not supported by card issuer. - "U" => {nil, nil}, # Address information is unavailable. - "W" => {"fail", "pass"}, # The US ZIP+4 code matches, but the street address does not. - "X" => {"pass", "pass"}, # Both the street address and the US ZIP+4 code matched. - "Y" => {"pass", "pass"}, # The street address and postal code matched. - "Z" => {"fail", "pass"}, # The postal code matched, but the street address did not. - "" => {nil, nil}, # fallback in-case of absence - nil => {nil, nil} # fallback in-case of absence + # The street address matched, but the postal code did not. + "A" => {"pass", "fail"}, + # No address information was provided. + "B" => {nil, nil}, + # The AVS check returned an error. + "E" => {"fail", nil}, + # The card was issued by a bank outside the U.S. and does not support AVS. + "G" => {nil, nil}, + # Neither the street address nor postal code matched. + "N" => {"fail", "fail"}, + # AVS is not applicable for this transaction. + "P" => {nil, nil}, + # Retry — AVS was unavailable or timed out. + "R" => {nil, nil}, + # AVS is not supported by card issuer. + "S" => {nil, nil}, + # Address information is unavailable. + "U" => {nil, nil}, + # The US ZIP+4 code matches, but the street address does not. + "W" => {"fail", "pass"}, + # Both the street address and the US ZIP+4 code matched. + "X" => {"pass", "pass"}, + # The street address and postal code matched. + "Y" => {"pass", "pass"}, + # The postal code matched, but the street address did not. + "Z" => {"fail", "pass"}, + # fallback in-case of absence + "" => {nil, nil}, + # fallback in-case of absence + nil => {nil, nil} } @cvc_code_translator %{ @@ -772,7 +786,8 @@ defmodule Gringotts.Gateways.AuthorizeNet do "P" => "CVV was not processed.", "S" => "CVV should have been present but was not indicated.", "U" => "The issuer was unable to process the CVV check.", - nil => nil # fallback in-case of absence + # fallback in-case of absence + nil => nil } @cavv_code_translator %{ @@ -784,16 +799,22 @@ defmodule Gringotts.Gateways.AuthorizeNet do "4" => "CAVV validation could not be performed; issuer system error.", "5" => "Reserved for future use.", "6" => "Reserved for future use.", - "7" => "CAVV failed validation, but the issuer is available. Valid for U.S.-issued card submitted to non-U.S acquirer.", - "8" => "CAVV passed validation and the issuer is available. Valid for U.S.-issued card submitted to non-U.S. acquirer.", - "9" => "CAVV failed validation and the issuer is unavailable. Valid for U.S.-issued card submitted to non-U.S acquirer.", - "A" => "CAVV passed validation but the issuer unavailable. Valid for U.S.-issued card submitted to non-U.S acquirer.", + "7" => + "CAVV failed validation, but the issuer is available. Valid for U.S.-issued card submitted to non-U.S acquirer.", + "8" => + "CAVV passed validation and the issuer is available. Valid for U.S.-issued card submitted to non-U.S. acquirer.", + "9" => + "CAVV failed validation and the issuer is unavailable. Valid for U.S.-issued card submitted to non-U.S acquirer.", + "A" => + "CAVV passed validation but the issuer unavailable. Valid for U.S.-issued card submitted to non-U.S acquirer.", "B" => "CAVV passed validation, information only, no liability shift.", - nil => nil # fallback in-case of absence + # fallback in-case of absence + nil => nil } def respond(body) do response_map = XmlToMap.naive_map(body) + case extract_gateway_response(response_map) do :undefined_response -> { @@ -812,11 +833,11 @@ defmodule Gringotts.Gateways.AuthorizeNet do def extract_gateway_response(response_map) do # The type of the response should be supported - @supported_response_types - |> Stream.map(&Map.get(response_map, &1, nil)) # Find the first non-nil from the above, if all are `nil`... # We are in trouble! - |> Enum.find(:undefined_response, &(&1)) + @supported_response_types + |> Stream.map(&Map.get(response_map, &1, nil)) + |> Enum.find(:undefined_response, & &1) end defp build_response(%{"messages" => %{"resultCode" => "Ok"}} = result, base_response) do @@ -862,10 +883,10 @@ defmodule Gringotts.Gateways.AuthorizeNet do # HELPERS # ############################################################################ - defp set_id(response, id), do: %{response | id: id} - defp set_message(response, message), do: %{response | message: message} + defp set_id(response, id), do: %{response | id: id} + defp set_message(response, message), do: %{response | message: message} defp set_gateway_code(response, code), do: %{response | gateway_code: code} - defp set_reason(response, body), do: %{response | reason: body} + defp set_reason(response, body), do: %{response | reason: body} defp set_avs_result(response, avs_code) do {street, zip_code} = @avs_code_translator[avs_code] @@ -879,6 +900,5 @@ defmodule Gringotts.Gateways.AuthorizeNet do defp set_cavv_result(response, cavv_code) do Map.put(response, :cavv_result, @cavv_code_translator[cavv_code]) end - end end diff --git a/lib/gringotts/gateways/base.ex b/lib/gringotts/gateways/base.ex index be881992..145b4b7a 100644 --- a/lib/gringotts/gateways/base.ex +++ b/lib/gringotts/gateways/base.ex @@ -12,18 +12,18 @@ defmodule Gringotts.Gateways.Base do ``` because this module provides an implementation. """ - + alias Gringotts.Response defmacro __using__(_) do quote location: :keep do @doc false - def purchase(_amount, _card_or_id, _opts) do + def purchase(_amount, _card_or_id, _opts) do not_implemented() end @doc false - def authorize(_amount, _card_or_id, _opts) do + def authorize(_amount, _card_or_id, _opts) do not_implemented() end @@ -57,7 +57,13 @@ defmodule Gringotts.Gateways.Base do {:error, Response.error(code: :not_implemented)} end - defoverridable [purchase: 3, authorize: 3, capture: 3, void: 2, refund: 3, store: 2, unstore: 2] + defoverridable purchase: 3, + authorize: 3, + capture: 3, + void: 2, + refund: 3, + store: 2, + unstore: 2 end end end diff --git a/lib/gringotts/gateways/bogus.ex b/lib/gringotts/gateways/bogus.ex index d6bdaa02..ada7575d 100644 --- a/lib/gringotts/gateways/bogus.ex +++ b/lib/gringotts/gateways/bogus.ex @@ -1,6 +1,6 @@ defmodule Gringotts.Gateways.Bogus do @moduledoc false - + use Gringotts.Gateways.Base alias Gringotts.{ @@ -9,28 +9,20 @@ defmodule Gringotts.Gateways.Bogus do } @some_authorization_id "14a62fff80f24a25f775eeb33624bbb3" - - def authorize(_amount, _card_or_id, _opts), - do: success() - def purchase(_amount, _card_or_id, _opts), - do: success() + def authorize(_amount, _card_or_id, _opts), do: success() + + def purchase(_amount, _card_or_id, _opts), do: success() - def capture(_id, _amount, _opts), - do: success() + def capture(_id, _amount, _opts), do: success() - def void(_id, _opts), - do: success() + def void(_id, _opts), do: success() - def refund(_amount, _id, _opts), - do: success() + def refund(_amount, _id, _opts), do: success() - def store(%CreditCard{} = _card, _opts), - do: success() + def store(%CreditCard{} = _card, _opts), do: success() - def unstore(_customer_id, _opts), - do: success() + def unstore(_customer_id, _opts), do: success() - defp success, - do: {:ok, Response.success(id: @some_authorization_id)} + defp success, do: {:ok, Response.success(id: @some_authorization_id)} end diff --git a/lib/gringotts/gateways/cams.ex b/lib/gringotts/gateways/cams.ex index 12687f6b..2f1d5805 100644 --- a/lib/gringotts/gateways/cams.ex +++ b/lib/gringotts/gateways/cams.ex @@ -408,7 +408,7 @@ defmodule Gringotts.Gateways.Cams do "S" => {nil, nil}, "0" => {nil, nil}, "O" => {nil, nil}, - "" => {nil, nil} + "" => {nil, nil} } # Fetched from CAMS POST API docs. @@ -426,6 +426,7 @@ defmodule Gringotts.Gateways.Cams do {street, zip_code} = @avs_code_translator[decoded_body["avsresponse"]] gateway_code = decoded_body["response_code"] message = decoded_body["responsetext"] + response = %Response{ status_code: 200, id: decoded_body["transactionid"], diff --git a/lib/gringotts/gateways/global_collect.ex b/lib/gringotts/gateways/global_collect.ex index 0c6d143e..624a8950 100644 --- a/lib/gringotts/gateways/global_collect.ex +++ b/lib/gringotts/gateways/global_collect.ex @@ -124,17 +124,15 @@ defmodule Gringotts.Gateways.GlobalCollect do import Poison, only: [decode: 1] - alias Gringotts.{Money, - CreditCard, - Response} - - @brand_map %{ - "visa": "1", - "american_express": "2", - "master": "3", - "discover": "128", - "jcb": "125", - "diners_club": "132" + alias Gringotts.{Money, CreditCard, Response} + + @brand_map %{ + visa: "1", + american_express: "2", + master: "3", + discover: "128", + jcb: "125", + diners_club: "132" } @doc """ @@ -209,7 +207,7 @@ defmodule Gringotts.Gateways.GlobalCollect do ``` """ - @spec capture(String.t(), Money.t, keyword) :: {:ok | :error, Response} + @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} def capture(payment_id, amount, opts) do params = create_params_for_capture(amount, opts) commit(:post, "payments/#{payment_id}/approve", params, opts) @@ -244,7 +242,7 @@ defmodule Gringotts.Gateways.GlobalCollect do ``` """ - @spec purchase(Money.t, CreditCard.t(), keyword) :: {:ok | :error, Response} + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} def purchase(amount, card = %CreditCard{}, opts) do case authorize(amount, card, opts) do {:ok, results} -> @@ -300,7 +298,7 @@ defmodule Gringotts.Gateways.GlobalCollect do iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.GlobalCollect, auth_result.payment.id, amount) ``` """ - @spec refund(Money.t, String.t(), keyword) :: {:ok | :error, Response} + @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} def refund(amount, payment_id, opts) do params = create_params_for_refund(amount, opts) commit(:post, "payments/#{payment_id}/refund", params, opts) @@ -329,7 +327,7 @@ defmodule Gringotts.Gateways.GlobalCollect do } end - defp create_params_for_capture(amount, opts) do + defp create_params_for_capture(amount, opts) do %{ order: add_order(amount, opts) } @@ -345,6 +343,7 @@ defmodule Gringotts.Gateways.GlobalCollect do defp add_money(amount, options) do {currency, amount, _} = Money.to_integer(amount) + %{ amount: amount, currencyCode: currency @@ -393,15 +392,16 @@ defmodule Gringotts.Gateways.GlobalCollect do %{ cvv: payment.verification_code, cardNumber: payment.number, - expiryDate: "#{payment.month}"<>"#{payment.year}", + expiryDate: "#{payment.month}" <> "#{payment.year}", cardholderName: CreditCard.full_name(payment) } end defp add_payment(payment, brand_map, opts) do brand = payment.brand + %{ - paymentProductId: Map.fetch!(brand_map, String.to_atom(brand)), + paymentProductId: Map.fetch!(brand_map, String.to_atom(brand)), skipAuthentication: opts[:skipAuthentication], card: add_card(payment) } @@ -422,17 +422,25 @@ defmodule Gringotts.Gateways.GlobalCollect do defp create_headers(path, opts) do time = date - sha_signature = auth_digest(path, opts[:config][:secret_api_key], time, opts) |> Base.encode64 + + sha_signature = + auth_digest(path, opts[:config][:secret_api_key], time, opts) |> Base.encode64() + auth_token = "GCS v1HMAC:#{opts[:config][:api_key_id]}:#{sha_signature}" - headers = [{"Content-Type", "application/json"}, {"Authorization", auth_token}, {"Date", time}] + + headers = [ + {"Content-Type", "application/json"}, + {"Authorization", auth_token}, + {"Date", time} + ] end defp date() do use Timex - datetime = Timex.now |> Timex.local + datetime = Timex.now() |> Timex.local() strftime_str = Timex.format!(datetime, "%a, %d %b %Y %H:%M:%S ", :strftime) time_zone = Timex.timezone(:local, datetime) - time = strftime_str <>"#{time_zone.abbreviation}" + time = strftime_str <> "#{time_zone.abbreviation}" end # Parses GlobalCollect's response and returns a `Gringotts.Response` struct @@ -448,13 +456,17 @@ defmodule Gringotts.Gateways.GlobalCollect do defp respond({:ok, %{status_code: status_code, body: body}}) do {:ok, results} = decode(body) - message = Enum.map(results["errors"],fn (x) -> x["message"] end) + message = Enum.map(results["errors"], fn x -> x["message"] end) detail = List.to_string(message) {:error, Response.error(status_code: status_code, message: detail, raw: results)} end defp respond({:error, %HTTPoison.Error{} = error}) do - {:error, Response.error(code: error.id, reason: :network_fail?, description: "HTTPoison says '#{error.reason}'")} + {:error, + Response.error( + code: error.id, + reason: :network_fail?, + description: "HTTPoison says '#{error.reason}'" + )} end - end diff --git a/lib/gringotts/gateways/paymill.ex b/lib/gringotts/gateways/paymill.ex index 8b00ba3e..5cf177e2 100644 --- a/lib/gringotts/gateways/paymill.ex +++ b/lib/gringotts/gateways/paymill.ex @@ -56,7 +56,7 @@ defmodule Gringotts.Gateways.Paymill do iex> Gringotts.authorize(Gringotts.Gateways.Paymill, amount, card, options) """ - @spec authorize(number, String.t | CreditCard.t, Keyword) :: {:ok | :error, Response} + @spec authorize(number, String.t() | CreditCard.t(), Keyword) :: {:ok | :error, Response} def authorize(amount, card_or_token, options) do Keyword.put(options, :money, amount) action_with_token(:authorize, amount, card_or_token, options) @@ -81,7 +81,7 @@ defmodule Gringotts.Gateways.Paymill do iex> Gringotts.purchase(Gringotts.Gateways.Paymill, amount, card, options) """ - @spec purchase(number, CreditCard.t, Keyword) :: {:ok | :error, Response} + @spec purchase(number, CreditCard.t(), Keyword) :: {:ok | :error, Response} def purchase(amount, card, options) do Keyword.put(options, :money, amount) action_with_token(:purchase, amount, card, options) @@ -99,7 +99,7 @@ defmodule Gringotts.Gateways.Paymill do iex> Gringotts.capture(Gringotts.Gateways.Paymill, token, amount, options) """ - @spec capture(String.t, number, Keyword) :: {:ok | :error, Response} + @spec capture(String.t(), number, Keyword) :: {:ok | :error, Response} def capture(authorization, amount, options) do post = add_amount([], amount, options) ++ [{"preauthorization", authorization}] @@ -116,13 +116,13 @@ defmodule Gringotts.Gateways.Paymill do iex> Gringotts.void(Gringotts.Gateways.Paymill, token, options) """ - @spec void(String.t, Keyword) :: {:ok | :error, Response} + @spec void(String.t(), Keyword) :: {:ok | :error, Response} def void(authorization, options) do commit(:delete, "preauthorizations/#{authorization}", [], options) end @doc false - @spec authorize_with_token(number, String.t, Keyword) :: term + @spec authorize_with_token(number, String.t(), Keyword) :: term def authorize_with_token(money, card_token, options) do post = add_amount([], money, options) ++ [{"token", card_token}] @@ -130,51 +130,53 @@ defmodule Gringotts.Gateways.Paymill do end @doc false - @spec purchase_with_token(number, String.t, Keyword) :: term + @spec purchase_with_token(number, String.t(), Keyword) :: term def purchase_with_token(money, card_token, options) do post = add_amount([], money, options) ++ [{"token", card_token}] commit(:post, "transactions", post, options) end - @spec save_card(CreditCard.t, Keyword) :: Response + @spec save_card(CreditCard.t(), Keyword) :: Response defp save_card(card, options) do - {:ok, %HTTPoison.Response{body: response}} = HTTPoison.get( + {:ok, %HTTPoison.Response{body: response}} = + HTTPoison.get( get_save_card_url(), get_headers(options), - params: get_save_card_params(card, options)) + params: get_save_card_params(card, options) + ) - parse_card_response(response) + parse_card_response(response) end - @spec save(CreditCard.t, Keyword) :: Response + @spec save(CreditCard.t(), Keyword) :: Response defp save(card, options) do save_card(card, options) end defp action_with_token(action, amount, "tok_" <> id = card_token, options) do - apply(__MODULE__, String.to_atom("#{action}_with_token"), [amount, card_token , options]) + apply(__MODULE__, String.to_atom("#{action}_with_token"), [amount, card_token, options]) end defp action_with_token(action, amount, %CreditCard{} = card, options) do {:ok, response} = save_card(card, options) card_token = get_token(response) - apply(__MODULE__, String.to_atom("#{action}_with_token"), [amount, card_token , options]) + apply(__MODULE__, String.to_atom("#{action}_with_token"), [amount, card_token, options]) end defp get_save_card_params(card, options) do - [ - {"transaction.mode" , "CONNECTOR_TEST"}, - {"channel.id" , get_config(:public_key, options)}, - {"jsonPFunction" , "jsonPFunction"}, - {"account.number" , card.number}, - {"account.expiry.month" , card.month}, - {"account.expiry.year" , card.year}, - {"account.verification" , card.verification_code}, - {"account.holder" , "#{card.first_name} #{card.last_name}"}, - {"presentation.amount3D" , get_amount(options)}, - {"presentation.currency3D" , get_currency(options)} + [ + {"transaction.mode", "CONNECTOR_TEST"}, + {"channel.id", get_config(:public_key, options)}, + {"jsonPFunction", "jsonPFunction"}, + {"account.number", card.number}, + {"account.expiry.month", card.month}, + {"account.expiry.year", card.year}, + {"account.verification", card.verification_code}, + {"account.holder", CreditCard.full_name(card)}, + {"presentation.amount3D", get_amount(options)}, + {"presentation.currency3D", get_currency(options)} ] end @@ -196,7 +198,7 @@ defmodule Gringotts.Gateways.Paymill do response |> String.replace(~r/jsonPFunction\(/, "") |> String.replace(~r/\)/, "") - |> Poison.decode + |> Poison.decode() end defp get_currency(options), do: options[:currency] || @default_currency @@ -210,7 +212,7 @@ defmodule Gringotts.Gateways.Paymill do defp commit(method, action, parameters \\ nil, options) do method |> HTTPoison.request(@live_url <> action, {:form, parameters}, get_headers(options), []) - |> ResponseParser.parse + |> ResponseParser.parse() end defp get_config(key, options) do @@ -222,118 +224,118 @@ defmodule Gringotts.Gateways.Paymill do alias Gringotts.Response @response_code %{ - 10_001 => "Undefined response", - 10_002 => "Waiting for something", - 11_000 => "Retry request at a later time", - - 20_000 => "Operation successful", - 20_100 => "Funds held by acquirer", - 20_101 => "Funds held by acquirer because merchant is new", - 20_200 => "Transaction reversed", - 20_201 => "Reversed due to chargeback", - 20_202 => "Reversed due to money-back guarantee", - 20_203 => "Reversed due to complaint by buyer", - 20_204 => "Payment has been refunded", - 20_300 => "Reversal has been canceled", - 22_000 => "Initiation of transaction successful", - - 30_000 => "Transaction still in progress", - 30_100 => "Transaction has been accepted", - 31_000 => "Transaction pending", - 31_100 => "Pending due to address", - 31_101 => "Pending due to uncleared eCheck", - 31_102 => "Pending due to risk review", - 31_103 => "Pending due regulatory review", - 31_104 => "Pending due to unregistered/unconfirmed receiver", - 31_200 => "Pending due to unverified account", - 31_201 => "Pending due to non-captured funds", - 31_202 => "Pending due to international account (accept manually)", - 31_203 => "Pending due to currency conflict (accept manually)", - 31_204 => "Pending due to fraud filters (accept manually)", - - 40_000 => "Problem with transaction data", - 40_001 => "Problem with payment data", - 40_002 => "Invalid checksum", - 40_100 => "Problem with credit card data", - 40_101 => "Problem with CVV", - 40_102 => "Card expired or not yet valid", - 40_103 => "Card limit exceeded", - 40_104 => "Card is not valid", - 40_105 => "Expiry date not valid", - 40_106 => "Credit card brand required", - 40_200 => "Problem with bank account data", - 40_201 => "Bank account data combination mismatch", - 40_202 => "User authentication failed", - 40_300 => "Problem with 3-D Secure data", - 40_301 => "Currency/amount mismatch", - 40_400 => "Problem with input data", - 40_401 => "Amount too low or zero", - 40_402 => "Usage field too long", - 40_403 => "Currency not allowed", - 40_410 => "Problem with shopping cart data", - 40_420 => "Problem with address data", - 40_500 => "Permission error with acquirer API", - 40_510 => "Rate limit reached for acquirer API", - 42_000 => "Initiation of transaction failed", - 42_410 => "Initiation of transaction expired", - - 50_000 => "Problem with back end", - 50_001 => "Country blacklisted", - 50_002 => "IP address blacklisted", - 50_004 => "Live mode not allowed", - 50_005 => "Insufficient permissions (API key)", - 50_100 => "Technical error with credit card", - 50_101 => "Error limit exceeded", - 50_102 => "Card declined", - 50_103 => "Manipulation or stolen card", - 50_104 => "Card restricted", - 50_105 => "Invalid configuration data", - 50_200 => "Technical error with bank account", - 50_201 => "Account blacklisted", - 50_300 => "Technical error with 3-D Secure", - 50_400 => "Declined because of risk issues", - 50_401 => "Checksum was wrong", - 50_402 => "Bank account number was invalid (formal check)", - 50_403 => "Technical error with risk check", - 50_404 => "Unknown error with risk check", - 50_405 => "Unknown bank code", - 50_406 => "Open chargeback", - 50_407 => "Historical chargeback", - 50_408 => "Institution / public bank account (NCA)", - 50_409 => "KUNO/Fraud", - 50_410 => "Personal Account Protection (PAP)", - 50_420 => "Rejected due to acquirer fraud settings", - 50_430 => "Rejected due to acquirer risk settings", - 50_440 => "Failed due to restrictions with acquirer account", - 50_450 => "Failed due to restrictions with user account", - 50_500 => "General timeout", - 50_501 => "Timeout on side of the acquirer", - 50_502 => "Risk management transaction timeout", - 50_600 => "Duplicate operation", - 50_700 => "Cancelled by user", - 50_710 => "Failed due to funding source", - 50_711 => "Payment method not usable, use other payment method", - 50_712 => "Limit of funding source was exceeded", - 50_713 => "Means of payment not reusable (canceled by user)", - 50_714 => "Means of payment not reusable (expired)", - 50_720 => "Rejected by acquirer", - 50_730 => "Transaction denied by merchant", - 50_800 => "Preauthorisation failed", - 50_810 => "Authorisation has been voided", - 50_820 => "Authorisation period expired" - } + 10_001 => "Undefined response", + 10_002 => "Waiting for something", + 11_000 => "Retry request at a later time", + 20_000 => "Operation successful", + 20_100 => "Funds held by acquirer", + 20_101 => "Funds held by acquirer because merchant is new", + 20_200 => "Transaction reversed", + 20_201 => "Reversed due to chargeback", + 20_202 => "Reversed due to money-back guarantee", + 20_203 => "Reversed due to complaint by buyer", + 20_204 => "Payment has been refunded", + 20_300 => "Reversal has been canceled", + 22_000 => "Initiation of transaction successful", + 30_000 => "Transaction still in progress", + 30_100 => "Transaction has been accepted", + 31_000 => "Transaction pending", + 31_100 => "Pending due to address", + 31_101 => "Pending due to uncleared eCheck", + 31_102 => "Pending due to risk review", + 31_103 => "Pending due regulatory review", + 31_104 => "Pending due to unregistered/unconfirmed receiver", + 31_200 => "Pending due to unverified account", + 31_201 => "Pending due to non-captured funds", + 31_202 => "Pending due to international account (accept manually)", + 31_203 => "Pending due to currency conflict (accept manually)", + 31_204 => "Pending due to fraud filters (accept manually)", + 40_000 => "Problem with transaction data", + 40_001 => "Problem with payment data", + 40_002 => "Invalid checksum", + 40_100 => "Problem with credit card data", + 40_101 => "Problem with CVV", + 40_102 => "Card expired or not yet valid", + 40_103 => "Card limit exceeded", + 40_104 => "Card is not valid", + 40_105 => "Expiry date not valid", + 40_106 => "Credit card brand required", + 40_200 => "Problem with bank account data", + 40_201 => "Bank account data combination mismatch", + 40_202 => "User authentication failed", + 40_300 => "Problem with 3-D Secure data", + 40_301 => "Currency/amount mismatch", + 40_400 => "Problem with input data", + 40_401 => "Amount too low or zero", + 40_402 => "Usage field too long", + 40_403 => "Currency not allowed", + 40_410 => "Problem with shopping cart data", + 40_420 => "Problem with address data", + 40_500 => "Permission error with acquirer API", + 40_510 => "Rate limit reached for acquirer API", + 42_000 => "Initiation of transaction failed", + 42_410 => "Initiation of transaction expired", + 50_000 => "Problem with back end", + 50_001 => "Country blacklisted", + 50_002 => "IP address blacklisted", + 50_004 => "Live mode not allowed", + 50_005 => "Insufficient permissions (API key)", + 50_100 => "Technical error with credit card", + 50_101 => "Error limit exceeded", + 50_102 => "Card declined", + 50_103 => "Manipulation or stolen card", + 50_104 => "Card restricted", + 50_105 => "Invalid configuration data", + 50_200 => "Technical error with bank account", + 50_201 => "Account blacklisted", + 50_300 => "Technical error with 3-D Secure", + 50_400 => "Declined because of risk issues", + 50_401 => "Checksum was wrong", + 50_402 => "Bank account number was invalid (formal check)", + 50_403 => "Technical error with risk check", + 50_404 => "Unknown error with risk check", + 50_405 => "Unknown bank code", + 50_406 => "Open chargeback", + 50_407 => "Historical chargeback", + 50_408 => "Institution / public bank account (NCA)", + 50_409 => "KUNO/Fraud", + 50_410 => "Personal Account Protection (PAP)", + 50_420 => "Rejected due to acquirer fraud settings", + 50_430 => "Rejected due to acquirer risk settings", + 50_440 => "Failed due to restrictions with acquirer account", + 50_450 => "Failed due to restrictions with user account", + 50_500 => "General timeout", + 50_501 => "Timeout on side of the acquirer", + 50_502 => "Risk management transaction timeout", + 50_600 => "Duplicate operation", + 50_700 => "Cancelled by user", + 50_710 => "Failed due to funding source", + 50_711 => "Payment method not usable, use other payment method", + 50_712 => "Limit of funding source was exceeded", + 50_713 => "Means of payment not reusable (canceled by user)", + 50_714 => "Means of payment not reusable (expired)", + 50_720 => "Rejected by acquirer", + 50_730 => "Transaction denied by merchant", + 50_800 => "Preauthorisation failed", + 50_810 => "Authorisation has been voided", + 50_820 => "Authorisation period expired" + } def parse({:ok, %HTTPoison.Response{body: body, status_code: 200}}) do body = Poison.decode!(body) parse_body(body) end + def parse({:ok, %HTTPoison.Response{body: body, status_code: 400}}) do body = Poison.decode!(body) + [] |> set_params(body) end + def parse({:ok, %HTTPoison.Response{body: body, status_code: 404}}) do body = Poison.decode!(body) + [] |> set_success(body) |> set_params(body) @@ -343,6 +345,7 @@ defmodule Gringotts.Gateways.Paymill do defp set_success(opts, %{"error" => error}) do opts ++ [message: error, success: false] end + defp set_success(opts, %{"transaction" => %{"response_code" => 20_000}}) do opts ++ [success: true] end @@ -363,31 +366,33 @@ defmodule Gringotts.Gateways.Paymill do end end - #Status code + # Status code defp parse_status_code(opts, %{"status" => "failed"} = body) do response_code = get_in(body, ["transaction", "response_code"]) response_msg = Map.get(@response_code, response_code, -1) opts ++ [message: response_msg] end + defp parse_status_code(opts, %{"transaction" => transaction}) do response_code = Map.get(transaction, "response_code", -1) response_msg = Map.get(@response_code, response_code, -1) opts ++ [status_code: response_code, message: response_msg] end + defp parse_status_code(opts, %{"response_code" => code}) do response_msg = Map.get(@response_code, code, -1) opts ++ [status_code: code, message: response_msg] end - #Authorization + # Authorization defp parse_authorization(opts, %{"status" => "failed"}) do opts ++ [success: false] end + defp parse_authorization(opts, %{"id" => id} = auth) do opts ++ [authorization: id] end defp set_params(opts, body), do: opts ++ [params: body] end - end diff --git a/lib/gringotts/gateways/wire_card.ex b/lib/gringotts/gateways/wire_card.ex index 5aafeb37..939a0fef 100644 --- a/lib/gringotts/gateways/wire_card.ex +++ b/lib/gringotts/gateways/wire_card.ex @@ -1,5 +1,6 @@ # call => Gringotts.Gateways.WireCard.authorize(100, creditcard, options) import XmlBuilder + defmodule Gringotts.Gateways.WireCard do @moduledoc """ WireCard System Plugins @@ -7,7 +8,7 @@ defmodule Gringotts.Gateways.WireCard do @test_url "https://c3-test.wirecard.com/secure/ssl-gateway" @live_url "https://c3.wirecard.com/secure/ssl-gateway" @homepage_url "http://www.wirecard.com" - + @doc """ Wirecard only allows phone numbers with a format like this: +xxx(yyy)zzz-zzzz-ppp, where: xxx = Country code @@ -18,8 +19,8 @@ defmodule Gringotts.Gateways.WireCard do number 5551234 within area code 202 (country code 1). """ @valid_phone_format ~r/\+\d{1,3}(\(?\d{3}\)?)?\d{3}-\d{4}-\d{3}/ - @default_currency "EUR" - @default_amount 100 + @default_currency "EUR" + @default_amount 100 use Gringotts.Gateways.Base use Gringotts.Adapter, required_config: [:login, :password, :signature] @@ -38,10 +39,10 @@ defmodule Gringotts.Gateways.WireCard do then then the :recurring option will be forced to "Repeated" =========================================================== TODO: Mandatorily check for :login,:password, :signature in options - Note: payment_menthod for now is only credit_card and + Note: payment_menthod for now is only credit_card and TODO: change it so it can also have GuWID ================================================ - E.g: => + E.g: => creditcard = %CreditCard{ number: "4200000000000000", month: 12, @@ -65,19 +66,19 @@ defmodule Gringotts.Gateways.WireCard do } options = [ config: %{ - login: "00000031629CA9FA", + login: "00000031629CA9FA", password: "TestXAPTER", signature: "00000031629CAFD5", - }, + }, order_id: 1, billing_address: address, description: 'Wirecard remote test purchase', email: "soleone@example.com", ip: "127.0.0.1", test: true - ] + ] """ - @spec authorize(Integer | Float, CreditCard.t | String.t, Keyword) :: {:ok, Map} + @spec authorize(Integer | Float, CreditCard.t() | String.t(), Keyword) :: {:ok, Map} def authorize(money, payment_method, options \\ []) def authorize(money, %CreditCard{} = creditcard, options) do @@ -92,9 +93,9 @@ defmodule Gringotts.Gateways.WireCard do @doc """ Capture - the first paramter here should be a GuWid/authorization. - Authorization is obtained by authorizing the creditcard. + Authorization is obtained by authorizing the creditcard. """ - @spec capture(String.t, Float, Keyword) :: {:ok, Map} + @spec capture(String.t(), Float, Keyword) :: {:ok, Map} def capture(authorization, money, options \\ []) when is_binary(authorization) do options = Keyword.put(options, :preauthorization, authorization) commit(:post, :capture, money, options) @@ -106,7 +107,7 @@ defmodule Gringotts.Gateways.WireCard do transaction. If a GuWID is given, rather than a CreditCard, then then the :recurring option will be forced to "Repeated" """ - @spec purchase(Float | Integer, CreditCard| String.t, Keyword) :: {:ok, Map} + @spec purchase(Float | Integer, CreditCard | String.t(), Keyword) :: {:ok, Map} def purchase(money, payment_method, options \\ []) def purchase(money, %CreditCard{} = creditcard, options) do @@ -120,33 +121,33 @@ defmodule Gringotts.Gateways.WireCard do end @doc """ - Void - A credit card purchase that a seller cancels after it has - been authorized but before it has been settled. - A void transaction does not appear on the customer's + Void - A credit card purchase that a seller cancels after it has + been authorized but before it has been settled. + A void transaction does not appear on the customer's credit card statement, though it might appear in a list - of pending transactions when the customer checks their + of pending transactions when the customer checks their account online. ==== Parameters ====== identification - The authorization string returned from the initial authorization or purchase. """ - @spec void(String.t, Keyword) :: {:ok, Map} + @spec void(String.t(), Keyword) :: {:ok, Map} def void(identification, options \\ []) when is_binary(identification) do options = Keyword.put(options, :preauthorization, identification) commit(:post, :reversal, nil, options) end - + @doc """ Performs a credit. - - This transaction indicates that money - should flow from the merchant to the customer. + + This transaction indicates that money + should flow from the merchant to the customer. ==== Parameters ==== - money -- The amount to be credited to the customer + money -- The amount to be credited to the customer as an Integer value in cents. identification -- GuWID """ - @spec refund(Float, String.t, Keyword) :: {:ok, Map} + @spec refund(Float, String.t(), Keyword) :: {:ok, Map} def refund(money, identification, options \\ []) when is_binary(identification) do options = Keyword.put(options, :preauthorization, identification) commit(:post, :bookback, money, options) @@ -161,54 +162,61 @@ defmodule Gringotts.Gateways.WireCard do "RECURRING_TRANSACTION/Type" set to "Initial". Subsequent transactions can then use the GuWID in place of a credit card by setting "RECURRING_TRANSACTION/Type" to "Repeated". - + This implementation of card store utilizes a Wirecard "Authorization Check" (a Preauthorization that is automatically reversed). It defaults to a check amount of "100" (i.e. $1.00) but this can be overriden (see below). - + IMPORTANT: In order to reuse the stored reference, the +authorization+ from the response should be saved by your application code. - + ==== Options specific to +store+ - + * :amount -- The amount, in cents, that should be "validated" by the Authorization Check. This amount will be reserved and then reversed. Default is 100. - + Note: This is not the only way to achieve a card store operation at Wirecard. Any +purchase+ or +authorize+ can be sent with +options[:recurring] = 'Initial'+ to make the returned authorization/GuWID usable in later transactions with +options[:recurring] = 'Repeated'+. """ - @spec store(CreditCard.t, Keyword) :: {:ok, Map} + @spec store(CreditCard.t(), Keyword) :: {:ok, Map} def store(%CreditCard{} = creditcard, options \\ []) do - options = options - |> Keyword.put(:credit_card, creditcard) - |> Keyword.put(:recurring, "Initial") + options = + options + |> Keyword.put(:credit_card, creditcard) + |> Keyword.put(:recurring, "Initial") + money = options[:amount] || @default_amount # Amex does not support authorization_check case creditcard.brand do "american_express" -> commit(:post, :preauthorization, money, options) - _ -> commit(:post, :authorization_check, money, options) + _ -> commit(:post, :authorization_check, money, options) end end - - # =================== Private Methods =================== - + + # =================== Private Methods =================== + # Contact WireCard, make the XML request, and parse the # reply into a Response object. defp commit(method, action, money, options) do # TODO: validate and setup address hash as per AM request = build_request(action, money, options) - headers = %{"Content-Type" => "text/xml", - "Authorization" => encoded_credentials( - options[:config][:login], options[:config][:password] - ) - } - method |> HTTPoison.request(base_url(options) , request, headers) |> respond + + headers = %{ + "Content-Type" => "text/xml", + "Authorization" => + encoded_credentials( + options[:config][:login], + options[:config][:password] + ) + } + + method |> HTTPoison.request(base_url(options), request, headers) |> respond end defp respond({:ok, %{status_code: 200, body: body}}) do @@ -217,13 +225,13 @@ defmodule Gringotts.Gateways.WireCard do end defp respond({:ok, %{body: body, status_code: status_code}}) do - {:error, "Some Error Occurred: \n #{ inspect body }"} + {:error, "Some Error Occurred: \n #{inspect(body)}"} end # Read the XML message from the gateway and check if it was successful, # and also extract required return values from the response # TODO: parse XML Response - defp parse(data) do + defp parse(data) do XmlToMap.naive_map(data) end @@ -231,15 +239,18 @@ defmodule Gringotts.Gateways.WireCard do defp build_request(action, money, options) do options = Keyword.put(options, :action, action) - request = doc(element(:WIRECARD_BXML, [ - element(:W_REQUEST, [ - element(:W_JOB, [ - element(:JobID, ""), - element(:BusinessCaseSignature, options[:config][:signature]), - add_transaction_data(action, money, options) + request = + doc( + element(:WIRECARD_BXML, [ + element(:W_REQUEST, [ + element(:W_JOB, [ + element(:JobID, ""), + element(:BusinessCaseSignature, options[:config][:signature]), + add_transaction_data(action, money, options) + ]) ]) ]) - ])) + ) request end @@ -250,11 +261,14 @@ defmodule Gringotts.Gateways.WireCard do defp add_transaction_data(action, money, options) do element("FNC_CC_#{atom_to_upcase_string(options[:action])}", [ element(:FunctionID, "dummy_description"), - element(:CC_TRANSACTION, [ - element(:TransactionID, options[:order_id]), - element(:CommerceType, (if options[:commerce_type], do: options[:commerce_type])) - ] ++ add_action_data(action, money, options) ++ add_customer_data(options) - )]) + element( + :CC_TRANSACTION, + [ + element(:TransactionID, options[:order_id]), + element(:CommerceType, if(options[:commerce_type], do: options[:commerce_type])) + ] ++ add_action_data(action, money, options) ++ add_customer_data(options) + ) + ]) end # Includes the IP address of the customer to the transaction-xml @@ -269,9 +283,14 @@ defmodule Gringotts.Gateways.WireCard do def add_action_data(action, money, options) do case options[:action] do # returns array of elements - action when(action in [:preauthorization, :purchase, :authorization_check]) -> create_elems_for_preauth_or_purchase_or_auth_check(money, options) - action when(action in [:capture, :bookback]) -> create_elems_for_capture_or_bookback(money, options) - action when(action == :reversal) -> add_guwid(options[:preauthorization]) + action when action in [:preauthorization, :purchase, :authorization_check] -> + create_elems_for_preauth_or_purchase_or_auth_check(money, options) + + action when action in [:capture, :bookback] -> + create_elems_for_capture_or_bookback(money, options) + + action when action == :reversal -> + add_guwid(options[:preauthorization]) end end @@ -280,25 +299,29 @@ defmodule Gringotts.Gateways.WireCard do add_guwid(options[:preauthorization]) ++ [add_amount(money, options)] end - # Creates xml request elements if action is preauth, purchase ir auth_check + # Creates xml request elements if action is preauth, purchase ir auth_check # TODO: handle nil values if array not generated defp create_elems_for_preauth_or_purchase_or_auth_check(money, options) do # TODO: setup_recurring_flag - add_invoice(money, options) ++ element_for_credit_card_or_guwid(options) ++ add_address(options[:billing_address]) + add_invoice(money, options) ++ + element_for_credit_card_or_guwid(options) ++ add_address(options[:billing_address]) end - + defp add_address(address) do if address do [ element(:CORPTRUSTCENTER_DATA, [ element(:ADDRESS, [ element(:Address1, address[:address1]), - element(:Address2, (if address[:address2], do: address[:address2])), + element(:Address2, if(address[:address2], do: address[:address2])), element(:City, address[:city]), - element(:Zip, address[:zip]), + element(:Zip, address[:zip]), add_state(address), element(:Country, address[:country]), - element(:Phone, (if regex_match(@valid_phone_format, address[:phone]), do: address[:phone])), + element( + :Phone, + if(regex_match(@valid_phone_format, address[:phone]), do: address[:phone]) + ), element(:Email, address[:email]) ]) ]) @@ -307,9 +330,9 @@ defmodule Gringotts.Gateways.WireCard do end defp add_state(address) do - if (regex_match(~r/[A-Za-z]{2}/, address[:state]) && regex_match(~r/^(us|ca)$/i, address[:country]) - ) do - element(:State, (String.upcase(address[:state]))) + if regex_match(~r/[A-Za-z]{2}/, address[:state]) && + regex_match(~r/^(us|ca)$/i, address[:country]) do + element(:State, String.upcase(address[:state])) end end @@ -320,7 +343,7 @@ defmodule Gringotts.Gateways.WireCard do add_guwid(options[:preauthorization]) end end - + # Includes Guwid data to transaction-xml defp add_guwid(preauth) do [element(:GuWID, preauth)] @@ -329,13 +352,15 @@ defmodule Gringotts.Gateways.WireCard do # Includes the credit-card data to the transaction-xml # TODO: Format Credit Card month, ref AM defp add_creditcard(creditcard) do - [element(:CREDIT_CARD_DATA, [ - element(:CreditCardNumber, creditcard.number), - element(:CVC2, creditcard.verification_code), - element(:ExpirationYear, creditcard.year), - element(:ExpirationMonth, creditcard.month), - element(:CardHolderName, join_string([creditcard.first_name, creditcard.last_name], " ")) - ])] + [ + element(:CREDIT_CARD_DATA, [ + element(:CreditCardNumber, creditcard.number), + element(:CVC2, creditcard.verification_code), + element(:ExpirationYear, creditcard.year), + element(:ExpirationMonth, creditcard.month), + element(:CardHolderName, join_string([creditcard.first_name, creditcard.last_name], " ")) + ]) + ] end # Includes the payment (amount, currency, country) to the transaction-xml @@ -345,11 +370,11 @@ defmodule Gringotts.Gateways.WireCard do element(:Currency, currency(options)), element(:CountryCode, options[:billing_address][:country]), element(:RECURRING_TRANSACTION, [ - element(:Type, (options[:recurring] || "Single")) + element(:Type, options[:recurring] || "Single") ]) ] end - + # Include the amount in the transaction-xml # TODO: check for localized currency or currency # localized_amount(money, options[:currency] || currency(money)) @@ -357,24 +382,24 @@ defmodule Gringotts.Gateways.WireCard do defp atom_to_upcase_string(atom) do atom - |> to_string - |> String.upcase + |> to_string + |> String.upcase() end # Encode login and password in Base64 to supply as HTTP header # (for http basic authentication) defp encoded_credentials(login, password) do [login, password] - |> join_string(":") - |> Base.encode64 - |> (&("Basic "<> &1)).() + |> join_string(":") + |> Base.encode64() + |> (&("Basic " <> &1)).() end defp join_string(list_of_words, joiner), do: Enum.join(list_of_words, joiner) defp regex_match(regex, string), do: Regex.match?(regex, string) - defp base_url(opts), do: if opts[:test], do: @test_url, else: @live_url + defp base_url(opts), do: if(opts[:test], do: @test_url, else: @live_url) defp currency(opts), do: opts[:currency] || @default_currency end diff --git a/lib/gringotts/response.ex b/lib/gringotts/response.ex index ac369f89..c64ec0a2 100644 --- a/lib/gringotts/response.ex +++ b/lib/gringotts/response.ex @@ -8,8 +8,17 @@ defmodule Gringotts.Response do """ defstruct [ - :success, :id, :token, :status_code, :gateway_code, :reason, :message, - :avs_result, :cvc_result, :raw, :fraud_review + :success, + :id, + :token, + :status_code, + :gateway_code, + :reason, + :message, + :avs_result, + :cvc_result, + :raw, + :fraud_review ] @typedoc """ @@ -54,19 +63,19 @@ defmodule Gringotts.Response do [cvc]: https://en.wikipedia.org/wiki/Card_security_code """ - @type t:: %__MODULE__{ - success: boolean, - id: String.t, - token: String.t, - status_code: non_neg_integer, - gateway_code: String.t, - reason: String.t, - message: String.t, - avs_result: %{street: String.t, zip_code: String.t}, - cvc_result: String.t, - raw: String.t, - fraud_review: term - } + @type t :: %__MODULE__{ + success: boolean, + id: String.t(), + token: String.t(), + status_code: non_neg_integer, + gateway_code: String.t(), + reason: String.t(), + message: String.t(), + avs_result: %{street: String.t(), zip_code: String.t()}, + cvc_result: String.t(), + raw: String.t(), + fraud_review: term + } def success(opts \\ []) do new(true, opts) diff --git a/mix.exs b/mix.exs index ef54cc7f..244a6405 100644 --- a/mix.exs +++ b/mix.exs @@ -17,14 +17,14 @@ defmodule Gringotts.Mixfile do tool: ExCoveralls ], preferred_cli_env: [ - "coveralls": :test, + coveralls: :test, "coveralls.detail": :test, - "coveralls.post": :test, - "coveralls.html": :test, - "coveralls.travis": :test + "coveralls.json": :test, + "coveralls.html": :test ], deps: deps(), - docs: docs()] + docs: docs() + ] end # Configuration for the OTP application @@ -32,7 +32,7 @@ defmodule Gringotts.Mixfile do # Type `mix help compile.app` for more information def application do [ - applications: [:httpoison, :hackney, :elixir_xml_to_map, :timex], + applications: [:httpoison, :hackney, :elixir_xml_to_map, :timex] ] end @@ -89,8 +89,8 @@ defmodule Gringotts.Mixfile do end defp groups_for_modules do - [ - "Gateways": ~r/^Gringotts.Gateways.?/, - ] + [ + Gateways: ~r/^Gringotts.Gateways.?/ + ] end end diff --git a/test/gateways/authorize_net_test.exs b/test/gateways/authorize_net_test.exs index 2ce52278..cec0b0cd 100644 --- a/test/gateways/authorize_net_test.exs +++ b/test/gateways/authorize_net_test.exs @@ -103,8 +103,8 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do customer_type: "individual" ] @opts_customer_profile_args [ - config: @auth, - customer_profile_id: "1814012002" + config: @auth, + customer_profile_id: "1814012002" ] @refund_id "60036752756" @@ -124,7 +124,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do post: fn _url, _body, _headers -> MockResponse.successful_purchase_response() end do - assert {:ok, response} = ANet.purchase(@amount, @card, @opts) + assert {:ok, _response} = ANet.purchase(@amount, @card, @opts) end end @@ -144,7 +144,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do post: fn _url, _body, _headers -> MockResponse.successful_authorize_response() end do - assert {:ok, response} = ANet.authorize(@amount, @card, @opts) + assert {:ok, _response} = ANet.authorize(@amount, @card, @opts) end end @@ -164,13 +164,12 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do post: fn _url, _body, _headers -> MockResponse.successful_capture_response() end do - assert {:ok, response} = ANet.capture(@capture_id, @amount, @opts) + assert {:ok, _response} = ANet.capture(@capture_id, @amount, @opts) end end test "with bad transaction id" do - with_mock HTTPoison, - post: fn _url, _body, _headers -> MockResponse.bad_id_capture() end do + with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.bad_id_capture() end do assert {:error, response} = ANet.capture(@capture_invalid_id, @amount, @opts) end end @@ -182,13 +181,12 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do post: fn _url, _body, _headers -> MockResponse.successful_refund_response() end do - assert {:ok, response} = ANet.refund(@amount, @refund_id, @opts_refund) + assert {:ok, _response} = ANet.refund(@amount, @refund_id, @opts_refund) end end test "bad payment params" do - with_mock HTTPoison, - post: fn _url, _body, _headers -> MockResponse.bad_card_refund() end do + with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.bad_card_refund() end do assert {:error, response} = ANet.refund(@amount, @refund_id, @opts_refund_bad_payment) end end @@ -203,9 +201,8 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do describe "void" do test "successful response with right params" do - with_mock HTTPoison, - post: fn _url, _body, _headers -> MockResponse.successful_void() end do - assert {:ok, response} = ANet.void(@void_id, @opts) + with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_void() end do + assert {:ok, _response} = ANet.void(@void_id, @opts) end end @@ -221,14 +218,14 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do test "successful response with right params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_store_response() end do - assert {:ok, response} = ANet.store(@card, @opts_store) + assert {:ok, _response} = ANet.store(@card, @opts_store) end end test "successful response without validation and customer type" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_store_response() end do - assert {:ok, response} = ANet.store(@card, @opts_store_without_validation) + assert {:ok, _response} = ANet.store(@card, @opts_store_without_validation) end end @@ -239,7 +236,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do end do assert {:error, response} = ANet.store(@card, @opts_store_no_profile) - "Error" + "Error" end end @@ -248,16 +245,16 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do post: fn _url, _body, _headers -> MockResponse.customer_payment_profile_success_response() end do - assert {:ok, response} = ANet.store(@card, @opts_customer_profile) + assert {:ok, _response} = ANet.store(@card, @opts_customer_profile) - "Ok" + "Ok" end end test "successful response without valiadtion mode and customer type" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_store_response() end do - assert {:ok, response} = ANet.store(@card, @opts_customer_profile_args) + assert {:ok, _response} = ANet.store(@card, @opts_customer_profile_args) end end end @@ -268,7 +265,7 @@ defmodule Gringotts.Gateways.AuthorizeNetTest do post: fn _url, _body, _headers -> MockResponse.successful_unstore_response() end do - assert {:ok, response} = ANet.unstore(@unstore_id, @opts) + assert {:ok, _response} = ANet.unstore(@unstore_id, @opts) end end end diff --git a/test/gateways/bogus_test.exs b/test/gateways/bogus_test.exs index 041c1469..9235bcf8 100644 --- a/test/gateways/bogus_test.exs +++ b/test/gateways/bogus_test.exs @@ -6,51 +6,44 @@ defmodule Gringotts.Gateways.BogusTest do @some_id "some_arbitrary_id" @amount Money.new(5, :USD) - + test "authorize" do - {:ok, %Response{id: id, success: success}} = - Gateway.authorize(@amount, :card, []) + {:ok, %Response{id: id, success: success}} = Gateway.authorize(@amount, :card, []) assert success assert id != nil end test "purchase" do - {:ok, %Response{id: id, success: success}} = - Gateway.purchase(@amount, :card, []) + {:ok, %Response{id: id, success: success}} = Gateway.purchase(@amount, :card, []) assert success assert id != nil end test "capture" do - {:ok, %Response{id: id, success: success}} = - Gateway.capture(@some_id, @amount, []) + {:ok, %Response{id: id, success: success}} = Gateway.capture(@some_id, @amount, []) assert success assert id != nil end test "void" do - {:ok, %Response{id: id, success: success}} = - Gateway.void(@some_id, []) + {:ok, %Response{id: id, success: success}} = Gateway.void(@some_id, []) assert success assert id != nil end test "store" do - {:ok, %Response{success: success}} = - Gateway.store(%Gringotts.CreditCard{}, []) + {:ok, %Response{success: success}} = Gateway.store(%Gringotts.CreditCard{}, []) assert success end test "unstore with customer" do - {:ok, %Response{success: success}} = - Gateway.unstore(@some_id, []) + {:ok, %Response{success: success}} = Gateway.unstore(@some_id, []) assert success end - end diff --git a/test/gateways/cams_test.exs b/test/gateways/cams_test.exs index edf0e5f4..a6117a78 100644 --- a/test/gateways/cams_test.exs +++ b/test/gateways/cams_test.exs @@ -68,8 +68,7 @@ defmodule Gringotts.Gateways.CamsTest do test "with bad card" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.failed_purchase_with_bad_credit_card() end do - {:error, %Response{reason: reason}} = - Gateway.purchase(@money, @bad_card, @options) + {:error, %Response{reason: reason}} = Gateway.purchase(@money, @bad_card, @options) assert String.contains?(reason, "Invalid Credit Card Number") end @@ -95,8 +94,7 @@ defmodule Gringotts.Gateways.CamsTest do test "with bad card" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.failed_authorized_with_bad_card() end do - {:error, %Response{reason: reason}} = - Gateway.authorize(@money, @bad_card, @options) + {:error, %Response{reason: reason}} = Gateway.authorize(@money, @bad_card, @options) assert String.contains?(reason, "Invalid Credit Card Number") end @@ -106,23 +104,20 @@ defmodule Gringotts.Gateways.CamsTest do describe "capture" do test "with full amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_capture() end do - assert {:ok, %Response{}} = - Gateway.capture(@money, @id , @options) + assert {:ok, %Response{}} = Gateway.capture(@money, @id, @options) end end test "with partial amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_capture() end do - assert {:ok, %Response{}} = - Gateway.capture(@money_less, @id , @options) + assert {:ok, %Response{}} = Gateway.capture(@money_less, @id, @options) end end test "with invalid transaction_id" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.invalid_transaction_id() end do - {:error, %Response{reason: reason}} = - Gateway.capture(@money, @bad_id, @options) + {:error, %Response{reason: reason}} = Gateway.capture(@money, @bad_id, @options) assert String.contains?(reason, "Transaction not found") end @@ -131,8 +126,7 @@ defmodule Gringotts.Gateways.CamsTest do test "with more than authorized amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.more_than_authorization_amount() end do - {:error, %Response{reason: reason}} = - Gateway.capture(@money_more, @id , @options) + {:error, %Response{reason: reason}} = Gateway.capture(@money_more, @id, @options) assert String.contains?(reason, "exceeds the authorization amount") end @@ -141,8 +135,7 @@ defmodule Gringotts.Gateways.CamsTest do test "on already captured transaction" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.multiple_capture_on_same_transaction() end do - {:error, %Response{reason: reason}} = - Gateway.capture(@money, @id , @options) + {:error, %Response{reason: reason}} = Gateway.capture(@money, @id, @options) assert String.contains?(reason, "A capture requires that") end @@ -152,16 +145,14 @@ defmodule Gringotts.Gateways.CamsTest do describe "refund" do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_refund() end do - assert {:ok, %Response{}} = - Gateway.refund(@money, @id , @options) + assert {:ok, %Response{}} = Gateway.refund(@money, @id, @options) end end test "with more than purchased amount" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.more_than_purchase_amount() end do - {:error, %Response{reason: reason}} = - Gateway.refund(@money_more, @id , @options) + {:error, %Response{reason: reason}} = Gateway.refund(@money_more, @id, @options) assert String.contains?(reason, "Refund amount may not exceed") end @@ -171,7 +162,7 @@ defmodule Gringotts.Gateways.CamsTest do describe "void" do test "with correct params" do with_mock HTTPoison, post: fn _url, _body, _headers -> MockResponse.successful_void() end do - {:ok, %Response{message: message}} = Gateway.void(@id , @options) + {:ok, %Response{message: message}} = Gateway.void(@id, @options) assert String.contains?(message, "Void Successful") end end diff --git a/test/gateways/global_collect_test.exs b/test/gateways/global_collect_test.exs index 87b0d702..d64489c2 100644 --- a/test/gateways/global_collect_test.exs +++ b/test/gateways/global_collect_test.exs @@ -1,9 +1,9 @@ defmodule Gringotts.Gateways.GlobalCollectTest do - - Code.require_file "../mocks/global_collect_mock.exs", __DIR__ + Code.require_file("../mocks/global_collect_mock.exs", __DIR__) use ExUnit.Case, async: false alias Gringotts.Gateways.GlobalCollectMock, as: MockResponse alias Gringotts.Gateways.GlobalCollect + alias Gringotts.{ CreditCard } @@ -69,21 +69,32 @@ defmodule Gringotts.Gateways.GlobalCollectTest do @invalid_token 30 - @invalid_config [config: %{secret_api_key: "Qtg9v4Q0G13sLRNcClWhHnvN1kVYWDcy4w9rG8T86XU=", api_key_id: "e5743abfc360ed12"}] + @invalid_config [ + config: %{ + secret_api_key: "Qtg9v4Q0G13sLRNcClWhHnvN1kVYWDcy4w9rG8T86XU=", + api_key_id: "e5743abfc360ed12" + } + ] @options [ - config: %{secret_api_key: "Qtg9v4Q0G13sLRNcClWhHnvN1kVYWDcy4w9rG8T86XU=", api_key_id: "e5743abfc360ed12", merchant_id: "1226"}, + config: %{ + secret_api_key: "Qtg9v4Q0G13sLRNcClWhHnvN1kVYWDcy4w9rG8T86XU=", + api_key_id: "e5743abfc360ed12", + merchant_id: "1226" + }, description: "Store Purchase 1437598192", merchantCustomerId: "234", customer_name: "John Doe", - dob: "19490917", company: "asma", + dob: "19490917", + company: "asma", email: "johndoe@gmail.com", phone: "7468474533", order_id: "2323", invoice: @invoice, billingAddress: @billingAddress, shippingAddress: @shippingAddress, - name: @name, skipAuthentication: "true" + name: @name, + skipAuthentication: "true" ] describe "validation arguments check" do @@ -97,18 +108,21 @@ defmodule Gringotts.Gateways.GlobalCollectTest do describe "purchase" do test "with valid card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_purchase_with_valid_card end] do - {:ok, response} = GlobalCollect.purchase(@amount, @valid_card, @options) - assert response.status_code == 201 - assert response.success == true - assert response.raw["payment"]["statusOutput"]["isAuthorized"] == true + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_purchase_with_valid_card() + end do + {:ok, response} = GlobalCollect.purchase(@amount, @valid_card, @options) + assert response.status_code == 201 + assert response.success == true + assert response.raw["payment"]["statusOutput"]["isAuthorized"] == true end end - test "with invalid amount" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_purchase_with_invalid_amount end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_purchase_with_invalid_amount() + end do {:error, response} = GlobalCollect.purchase(@bad_amount, @valid_card, @options) assert response.status_code == 400 assert response.success == false @@ -120,7 +134,9 @@ defmodule Gringotts.Gateways.GlobalCollectTest do describe "authorize" do test "with valid card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_authorize_with_valid_card end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_authorize_with_valid_card() + end do {:ok, response} = GlobalCollect.authorize(@amount, @valid_card, @options) assert response.status_code == 201 assert response.success == true @@ -130,17 +146,23 @@ defmodule Gringotts.Gateways.GlobalCollectTest do test "with invalid card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_authorize_with_invalid_card end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_authorize_with_invalid_card() + end do {:error, response} = GlobalCollect.authorize(@amount, @invalid_card, @options) assert response.status_code == 400 assert response.success == false - assert response.message == "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT" + + assert response.message == + "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT" end end test "with invalid amount" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_authorize_with_invalid_amount end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_authorize_with_invalid_amount() + end do {:error, response} = GlobalCollect.authorize(@bad_amount, @valid_card, @options) assert response.status_code == 400 assert response.success == false @@ -152,7 +174,7 @@ defmodule Gringotts.Gateways.GlobalCollectTest do describe "refund" do test "with refund not enabled for the respective account" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_refund end] do + request: fn _method, _url, _body, _headers -> MockResponse.test_for_refund() end do {:error, response} = GlobalCollect.refund(@amount, @valid_token, @options) assert response.status_code == 400 assert response.success == false @@ -164,7 +186,9 @@ defmodule Gringotts.Gateways.GlobalCollectTest do describe "capture" do test "with valid payment id" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_capture_with_valid_paymentid end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_capture_with_valid_paymentid() + end do {:ok, response} = GlobalCollect.capture(@valid_token, @amount, @options) assert response.status_code == 200 assert response.success == true @@ -173,20 +197,24 @@ defmodule Gringotts.Gateways.GlobalCollectTest do end test "with invalid payment id" do - with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_capture_with_invalid_paymentid end] do - {:error, response} = GlobalCollect.capture(@invalid_token, @amount, @options) - assert response.status_code == 404 - assert response.success == false - assert response.message == "UNKNOWN_PAYMENT_ID" - end + with_mock HTTPoison, + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_capture_with_invalid_paymentid() + end do + {:error, response} = GlobalCollect.capture(@invalid_token, @amount, @options) + assert response.status_code == 404 + assert response.success == false + assert response.message == "UNKNOWN_PAYMENT_ID" + end end end describe "void" do test "with valid card" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_void_with_valid_card end] do + request: fn _method, _url, _body, _headers -> + MockResponse.test_for_void_with_valid_card() + end do {:ok, response} = GlobalCollect.void(@valid_token, @options) assert response.status_code == 200 assert response.raw["payment"]["status"] == "CANCELLED" @@ -197,7 +225,7 @@ defmodule Gringotts.Gateways.GlobalCollectTest do describe "network failure" do test "with authorization" do with_mock HTTPoison, - [request: fn(_method, _url, _body, _headers) -> MockResponse.test_for_network_failure end] do + request: fn _method, _url, _body, _headers -> MockResponse.test_for_network_failure() end do {:error, response} = GlobalCollect.authorize(@amount, @valid_card, @options) assert response.success == false assert response.reason == :network_fail? diff --git a/test/gateways/monei_test.exs b/test/gateways/monei_test.exs index ae41e9a0..e641d91b 100644 --- a/test/gateways/monei_test.exs +++ b/test/gateways/monei_test.exs @@ -6,6 +6,7 @@ defmodule Gringotts.Gateways.MoneiTest do } alias Gringotts.Gateways.Monei, as: Gateway + alias Plug.{Conn, Parsers} @amount42 Money.new(42, :USD) @amount3 Money.new(3, :USD) @@ -136,7 +137,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert params["authentication.entityId"] == "some_secret_entity_id" assert params["authentication.password"] == "some_secret_password" assert params["authentication.userId"] == "some_secret_user_id" - Plug.Conn.resp(conn, 200, @auth_success) + Conn.resp(conn, 200, @auth_success) end) {:ok, response} = Gateway.purchase(@amount42, @card, config: auth) @@ -159,13 +160,12 @@ defmodule Gringotts.Gateways.MoneiTest do assert params["transactionCategory"] == @extra_opts[:category] assert params["customer.merchantCustomerId"] == @customer[:merchantCustomerId] - assert params["shipping.customer.merchantCustomerId"] == - @customer[:merchantCustomerId] + assert params["shipping.customer.merchantCustomerId"] == @customer[:merchantCustomerId] assert params["merchant.submerchantId"] == @merchant[:submerchantId] assert params["billing.city"] == @billing[:city] assert params["shipping.method"] == @shipping[:method] - Plug.Conn.resp(conn, 200, @register_success) + Conn.resp(conn, 200, @register_success) end) opts = randoms ++ @extra_opts ++ [config: auth] @@ -176,7 +176,7 @@ defmodule Gringotts.Gateways.MoneiTest do test "when we get non-json.", %{bypass: bypass, auth: auth} do Bypass.expect_once(bypass, "POST", "/v1/payments", fn conn -> - Plug.Conn.resp(conn, 400, "") + Conn.resp(conn, 400, "") end) {:error, _} = Gateway.authorize(@amount42, @bad_card, config: auth) @@ -191,7 +191,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert params["amount"] == "42.00" assert params["currency"] == "USD" assert params["paymentType"] == "PA" - Plug.Conn.resp(conn, 200, @auth_success) + Conn.resp(conn, 200, @auth_success) end) {:ok, response} = Gateway.authorize(@amount42, @card, config: auth) @@ -207,7 +207,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert params["amount"] == "42.00" assert params["currency"] == "USD" assert params["paymentType"] == "DB" - Plug.Conn.resp(conn, 200, @auth_success) + Conn.resp(conn, 200, @auth_success) end) {:ok, response} = Gateway.purchase(@amount42, @card, config: auth) @@ -219,7 +219,7 @@ defmodule Gringotts.Gateways.MoneiTest do p_conn = parse(conn) params = p_conn.body_params assert params["createRegistration"] == "true" - Plug.Conn.resp(conn, 200, @register_success) + Conn.resp(conn, 200, @register_success) end) {:ok, response} = Gateway.purchase(@amount42, @card, register: true, config: auth) @@ -239,7 +239,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert params["card.holder"] == "Harry Potter" assert params["card.number"] == "4200000000000000" assert params["paymentBrand"] == "VISA" - Plug.Conn.resp(conn, 200, @store_success) + Conn.resp(conn, 200, @store_success) end) {:ok, response} = Gateway.store(@card, config: auth) @@ -259,7 +259,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert params["amount"] == "42.00" assert params["currency"] == "USD" assert params["paymentType"] == "CP" - Plug.Conn.resp(conn, 200, @auth_success) + Conn.resp(conn, 200, @auth_success) end ) @@ -278,7 +278,7 @@ defmodule Gringotts.Gateways.MoneiTest do p_conn = parse(conn) params = p_conn.body_params assert :error == Map.fetch(params, "createRegistration") - Plug.Conn.resp(conn, 200, @auth_success) + Conn.resp(conn, 200, @auth_success) end ) @@ -306,7 +306,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert params["amount"] == "3.00" assert params["currency"] == "USD" assert params["paymentType"] == "RF" - Plug.Conn.resp(conn, 200, @auth_success) + Conn.resp(conn, 200, @auth_success) end ) @@ -328,7 +328,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert params["authentication.entityId"] == "some_secret_entity_id" assert params["authentication.password"] == "some_secret_password" assert params["authentication.userId"] == "some_secret_user_id" - Plug.Conn.resp(conn, 200, "") + Conn.resp(conn, 200, "") end ) @@ -349,7 +349,7 @@ defmodule Gringotts.Gateways.MoneiTest do assert :error == Map.fetch(params, :amount) assert :error == Map.fetch(params, :currency) assert params["paymentType"] == "RV" - Plug.Conn.resp(conn, 200, @auth_success) + Conn.resp(conn, 200, @auth_success) end ) @@ -359,8 +359,8 @@ defmodule Gringotts.Gateways.MoneiTest do end def parse(conn, opts \\ []) do - opts = Keyword.put_new(opts, :parsers, [Plug.Parsers.URLENCODED]) - Plug.Parsers.call(conn, Plug.Parsers.init(opts)) + opts = Keyword.put_new(opts, :parsers, [Parsers.URLENCODED]) + Parsers.call(conn, Parsers.init(opts)) end end diff --git a/test/gateways/trexle_test.exs b/test/gateways/trexle_test.exs index 7078b9f6..00da54ca 100644 --- a/test/gateways/trexle_test.exs +++ b/test/gateways/trexle_test.exs @@ -157,7 +157,9 @@ defmodule Gringotts.Gateways.TrexleTest do MockResponse.test_for_network_failure() end do {:error, response} = Trexle.authorize(@amount, @valid_card, @opts) - assert response.message == "HTTPoison says 'some_hackney_error' [ID: some_hackney_error_id]" + + assert response.message == + "HTTPoison says 'some_hackney_error' [ID: some_hackney_error_id]" end end end diff --git a/test/gateways/wire_card_test.exs b/test/gateways/wire_card_test.exs index d23d9090..59de0f03 100644 --- a/test/gateways/wire_card_test.exs +++ b/test/gateways/wire_card_test.exs @@ -3,18 +3,11 @@ defmodule Gringotts.Gateways.WireCardTest do import Mock - alias Gringotts.{ - CreditCard, - Address, - Response - } - alias Gringotts.Gateways.WireCard, as: Gateway - setup do # TEST_AUTHORIZATION_GUWID = 'C822580121385121429927' # TEST_PURCHASE_GUWID = 'C865402121385575982910' # TEST_CAPTURE_GUWID = 'C833707121385268439116' - + # credit_card = %CreditCard{name: "Longbob", number: "4200000000000000", cvc: "123", expiration: {2015, 11}} # config = %{credentails: {'user', 'pass'}, default_currency: "EUR"} @@ -24,7 +17,4 @@ defmodule Gringotts.Gateways.WireCardTest do test "test_successful_authorization" do assert 1 + 1 == 2 end - - - end diff --git a/test/gringotts_test.exs b/test/gringotts_test.exs index 7d9f7681..01d01824 100644 --- a/test/gringotts_test.exs +++ b/test/gringotts_test.exs @@ -48,13 +48,11 @@ defmodule GringottsTest do end test "authorization" do - assert authorize(GringottsTest.FakeGateway, 100, :card, []) == - :authorization_response + assert authorize(GringottsTest.FakeGateway, 100, :card, []) == :authorization_response end test "purchase" do - assert purchase(GringottsTest.FakeGateway, 100, :card, []) == - :purchase_response + assert purchase(GringottsTest.FakeGateway, 100, :card, []) == :purchase_response end test "capture" do diff --git a/test/integration/gateways/monei_test.exs b/test/integration/gateways/monei_test.exs index 71f48bd7..59bc9b88 100644 --- a/test/integration/gateways/monei_test.exs +++ b/test/integration/gateways/monei_test.exs @@ -68,7 +68,7 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do password: "hMkqf2qbWf", entityId: "8a82941760036820016010a28a8337f6" } - + setup_all do Application.put_env( :gringotts, @@ -104,7 +104,8 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do {:ok, _registration_token} <- Map.fetch(auth_result, :token), {:ok, _capture_result} <- Gateway.capture(auth_result.id, @amount, opts) do "yay!" - else {:error, _err} -> + else + {:error, _err} -> flunk() end end @@ -113,7 +114,8 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do with {:ok, auth_result} <- Gateway.authorize(@amount, @card, opts), {:ok, _void_result} <- Gateway.void(auth_result.id, opts) do "yay!" - else {:error, _err} -> + else + {:error, _err} -> flunk() end end @@ -122,7 +124,8 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do with {:ok, purchase_result} <- Gateway.purchase(@amount, @card, opts), {:ok, _void_result} <- Gateway.void(purchase_result.id, opts) do "yay!" - else {:error, _err} -> + else + {:error, _err} -> flunk() end end @@ -131,7 +134,8 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do with {:ok, purchase_result} <- Gateway.purchase(@amount, @card, opts), {:ok, _refund_result} <- Gateway.refund(@sub_amount, purchase_result.id, opts) do "yay!" - else {:error, _err} -> + else + {:error, _err} -> flunk() end end @@ -145,7 +149,8 @@ defmodule Gringotts.Integration.Gateways.MoneiTest do with {:ok, store_result} <- Gateway.store(@card, opts), {:ok, _unstore_result} <- Gateway.unstore(store_result.id, opts) do "yay!" - else {:error, _err} -> + else + {:error, _err} -> flunk() end end diff --git a/test/integration/gateways/stripe_test.exs b/test/integration/gateways/stripe_test.exs index b390f989..e4d3e070 100644 --- a/test/integration/gateways/stripe_test.exs +++ b/test/integration/gateways/stripe_test.exs @@ -1,21 +1,22 @@ defmodule Gringotts.Gateways.StripeTest do - use ExUnit.Case alias Gringotts.Gateways.Stripe + alias Gringotts.{ CreditCard, Address } @moduletag integration: true - + @amount Money.new(5, :USD) @card %CreditCard{ first_name: "John", last_name: "Smith", number: "4242424242424242", - year: "2068", # Can't be more than 50 years in the future, Haha. + # Can't be more than 50 years in the future, Haha. + year: "2068", month: "12", verification_code: "123" } diff --git a/test/integration/money.exs b/test/integration/money.exs index 3f5691ba..e4aaa331 100644 --- a/test/integration/money.exs +++ b/test/integration/money.exs @@ -4,7 +4,7 @@ defmodule Gringotts.Integration.Gateways.MoneyTest do alias Gringotts.Money, as: MoneyProtocol @moduletag :integration - + @ex_money Money.new(42, :EUR) @ex_money_long Money.new("42.126456", :EUR) @ex_money_bhd Money.new(42, :BHD) @@ -12,50 +12,50 @@ defmodule Gringotts.Integration.Gateways.MoneyTest do @any %{value: Decimal.new(42), currency: "EUR"} @any_long %{value: Decimal.new("42.126456"), currency: "EUR"} @any_bhd %{value: Decimal.new("42"), currency: "BHD"} - + describe "ex_money" do test "value is a Decimal.t" do - assert match? %Decimal{}, MoneyProtocol.value(@ex_money) + assert match?(%Decimal{}, MoneyProtocol.value(@ex_money)) end test "currency is an upcase String.t" do the_currency = MoneyProtocol.currency(@ex_money) - assert match? currency when is_binary(currency), the_currency + assert match?(currency when is_binary(currency), the_currency) assert the_currency == String.upcase(the_currency) end test "to_integer" do - assert match? {"EUR", 4200, -2}, MoneyProtocol.to_integer(@ex_money) - assert match? {"BHD", 42_000, -3}, MoneyProtocol.to_integer(@ex_money_bhd) + assert match?({"EUR", 4200, -2}, MoneyProtocol.to_integer(@ex_money)) + assert match?({"BHD", 42_000, -3}, MoneyProtocol.to_integer(@ex_money_bhd)) end test "to_string" do - assert match? {"EUR", "42.00"}, MoneyProtocol.to_string(@ex_money) - assert match? {"EUR", "42.13"}, MoneyProtocol.to_string(@ex_money_long) - assert match? {"BHD", "42.000"}, MoneyProtocol.to_string(@ex_money_bhd) + assert match?({"EUR", "42.00"}, MoneyProtocol.to_string(@ex_money)) + assert match?({"EUR", "42.13"}, MoneyProtocol.to_string(@ex_money_long)) + assert match?({"BHD", "42.000"}, MoneyProtocol.to_string(@ex_money_bhd)) end end describe "Any" do test "value is a Decimal.t" do - assert match? %Decimal{}, MoneyProtocol.value(@any) + assert match?(%Decimal{}, MoneyProtocol.value(@any)) end test "currency is an upcase String.t" do the_currency = MoneyProtocol.currency(@any) - assert match? currency when is_binary(currency), the_currency + assert match?(currency when is_binary(currency), the_currency) assert the_currency == String.upcase(the_currency) end test "to_integer" do - assert match? {"EUR", 4200, -2}, MoneyProtocol.to_integer(@any) - assert match? {"BHD", 4200, -2}, MoneyProtocol.to_integer(@any_bhd) + assert match?({"EUR", 4200, -2}, MoneyProtocol.to_integer(@any)) + assert match?({"BHD", 4200, -2}, MoneyProtocol.to_integer(@any_bhd)) end test "to_string" do - assert match? {"EUR", "42.00"}, MoneyProtocol.to_string(@any) - assert match? {"EUR", "42.13"}, MoneyProtocol.to_string(@any_long) - assert match? {"BHD", "42.00"}, MoneyProtocol.to_string(@any_bhd) + assert match?({"EUR", "42.00"}, MoneyProtocol.to_string(@any)) + assert match?({"EUR", "42.13"}, MoneyProtocol.to_string(@any_long)) + assert match?({"BHD", "42.00"}, MoneyProtocol.to_string(@any_bhd)) end end end diff --git a/test/mocks/authorize_net_mock.exs b/test/mocks/authorize_net_mock.exs index cb68df4a..712602cd 100644 --- a/test/mocks/authorize_net_mock.exs +++ b/test/mocks/authorize_net_mock.exs @@ -1,321 +1,422 @@ - defmodule Gringotts.Gateways.AuthorizeNetMock do - - # purchase mock response - def successful_purchase_response do - {:ok, - %HTTPoison.Response{body: ~s{123456OkI00001Successful.1C7HPT1YP2600365530965D6782A03246EE3BAFABE8006E32DE970XXXX0015MasterCard1This transaction has been approved.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-13182173"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "908"}, {"Date", "Thu, 21 Dec 2017 09:29:12 GMT"}, - {"Connection", "keep-alive"}], +defmodule Gringotts.Gateways.AuthorizeNetMock do + # purchase mock response + def successful_purchase_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456OkI00001Successful.1C7HPT1YP2600365530965D6782A03246EE3BAFABE8006E32DE970XXXX0015MasterCard1This transaction has been approved.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-13182173"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "908"}, + {"Date", "Thu, 21 Dec 2017 09:29:12 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def bad_card_purchase_response do - {:ok, - %HTTPoison.Response{body: ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XXXXX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-10066531"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "514"}, {"Date", "Thu, 21 Dec 2017 09:35:45 GMT"}, - {"Connection", "keep-alive"}], + def bad_card_purchase_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XXXXX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-10066531"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "514"}, + {"Date", "Thu, 21 Dec 2017 09:35:45 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def bad_amount_purchase_response do - {:ok, - %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P0C7C56F020A2AE2660A87637CD00B4D5C0XXXX0015MasterCard5A valid amount is required.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-13187900"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "867"}, {"Date", "Thu, 21 Dec 2017 09:44:33 GMT"}, - {"Connection", "keep-alive"}], + def bad_amount_purchase_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456ErrorE00027The transaction was unsuccessful.3P0C7C56F020A2AE2660A87637CD00B4D5C0XXXX0015MasterCard5A valid amount is required.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-13187900"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "867"}, + {"Date", "Thu, 21 Dec 2017 09:44:33 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - # authorize mock response - def successful_authorize_response do - {:ok, - %HTTPoison.Response{body: ~s{123456OkI00001Successful.1K6Z0ABYP260036854582A4AD079E22A271D92662CF093CED7A5D0XXXX0015MasterCard1This transaction has been approved.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15778237"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "908"}, {"Date", "Mon, 25 Dec 2017 14:17:56 GMT"}, - {"Connection", "keep-alive"}], + # authorize mock response + def successful_authorize_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456OkI00001Successful.1K6Z0ABYP260036854582A4AD079E22A271D92662CF093CED7A5D0XXXX0015MasterCard1This transaction has been approved.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15778237"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "908"}, + {"Date", "Mon, 25 Dec 2017 14:17:56 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def bad_card_authorize_response do - {:ok, - %HTTPoison.Response{body: ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XXXXX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12660528"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "514"}, {"Date", "Mon, 25 Dec 2017 14:19:29 GMT"}, - {"Connection", "keep-alive"}], + def bad_card_authorize_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XXXXX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12660528"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "514"}, + {"Date", "Mon, 25 Dec 2017 14:19:29 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def bad_amount_authorize_response do - {:ok, - %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P0C7C56F020A2AE2660A87637CD00B4D5C0XXXX0015MasterCard290There is one or more missing or invalid required fields.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15779095"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "898"}, {"Date", "Mon, 25 Dec 2017 14:22:02 GMT"}, - {"Connection", "keep-alive"}], + def bad_amount_authorize_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456ErrorE00027The transaction was unsuccessful.3P0C7C56F020A2AE2660A87637CD00B4D5C0XXXX0015MasterCard290There is one or more missing or invalid required fields.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15779095"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "898"}, + {"Date", "Mon, 25 Dec 2017 14:22:02 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - # capture mock response + # capture mock response - def successful_capture_response do - {:ok, - %HTTPoison.Response{body: ~s{123456OkI00001Successful.14OKD6YP6003685493160036854931348C4ECD0F764736B012C4655BFA68EF0XXXX0015MasterCard1This transaction has been approved.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15783402"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "899"}, {"Date", "Mon, 25 Dec 2017 14:39:28 GMT"}, - {"Connection", "keep-alive"}], + def successful_capture_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456OkI00001Successful.14OKD6YP6003685493160036854931348C4ECD0F764736B012C4655BFA68EF0XXXX0015MasterCard1This transaction has been approved.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15783402"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "899"}, + {"Date", "Mon, 25 Dec 2017 14:39:28 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def bad_id_capture do - {:ok, - %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P0A5280E2A6AA1290D451A24286692D1B0033A valid referenced transaction ID is required.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15784805"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "843"}, {"Date", "Mon, 25 Dec 2017 14:45:32 GMT"}, - {"Connection", "keep-alive"}], + def bad_id_capture do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456ErrorE00027The transaction was unsuccessful.3P0A5280E2A6AA1290D451A24286692D1B0033A valid referenced transaction ID is required.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15784805"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "843"}, + {"Date", "Mon, 25 Dec 2017 14:45:32 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - # refund mock response - def successful_refund_response do - {:ok, - %HTTPoison.Response{body: ~s{123456OkI00001Successful.1P6003685566160036752756169F2381B172A5AA247A01757A3E520A0XXXX0015MasterCard1This transaction has been approved.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12678232"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "884"}, {"Date", "Mon, 25 Dec 2017 15:22:19 GMT"}, - {"Connection", "keep-alive"}], + # refund mock response + def successful_refund_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456OkI00001Successful.1P6003685566160036752756169F2381B172A5AA247A01757A3E520A0XXXX0015MasterCard1This transaction has been approved.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12678232"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "884"}, + {"Date", "Mon, 25 Dec 2017 15:22:19 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def bad_card_refund do - {:ok, - %HTTPoison.Response{body: ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15795999"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "511"}, {"Date", "Mon, 25 Dec 2017 15:21:20 GMT"}, - {"Connection", "keep-alive"}], + def bad_card_refund do + {:ok, + %HTTPoison.Response{ + body: + ~s{ErrorE00003The 'AnetApi/xml/v1/schema/AnetApiSchema.xsd:cardNumber' element is invalid - The value XX is invalid according to its datatype 'String' - The actual length is less than the MinLength value.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15795999"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "511"}, + {"Date", "Mon, 25 Dec 2017 15:21:20 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def debit_less_than_refund do - {:ok, - %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P060036752756A5280E2A6AA1290D451A24286692D1B00XXXX0015MasterCard55The sum of credits against the referenced transaction would exceed original debit amount.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12681460"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "952"}, {"Date", "Mon, 25 Dec 2017 15:39:25 GMT"}, - {"Connection", "keep-alive"}], + def debit_less_than_refund do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456ErrorE00027The transaction was unsuccessful.3P060036752756A5280E2A6AA1290D451A24286692D1B00XXXX0015MasterCard55The sum of credits against the referenced transaction would exceed original debit amount.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12681460"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "952"}, + {"Date", "Mon, 25 Dec 2017 15:39:25 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - # void mock response - def successful_void do - {:ok, - %HTTPoison.Response{body: ~s{123456OkI00001Successful.1ZJPVRXP6003685521760036855217F09A215511891DCEA91B6CC52B9F4E870XXXX0015MasterCard1This transaction has been approved.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12682366"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "899"}, {"Date", "Mon, 25 Dec 2017 15:43:56 GMT"}, - {"Connection", "keep-alive"}], + # void mock response + def successful_void do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456OkI00001Successful.1ZJPVRXP6003685521760036855217F09A215511891DCEA91B6CC52B9F4E870XXXX0015MasterCard1This transaction has been approved.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_f2f80544-1a98-4ad7-989b-8d267ebf5043-56152-12682366"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "899"}, + {"Date", "Mon, 25 Dec 2017 15:43:56 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def void_non_existent_id do - {:ok, - %HTTPoison.Response{body: ~s{123456ErrorE00027The transaction was unsuccessful.3P060036855219C7C56F020A2AE2660A87637CD00B4D5C016The transaction cannot be found.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15801470"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "861"}, {"Date", "Mon, 25 Dec 2017 15:49:38 GMT"}, - {"Connection", "keep-alive"}], + def void_non_existent_id do + {:ok, + %HTTPoison.Response{ + body: + ~s{123456ErrorE00027The transaction was unsuccessful.3P060036855219C7C56F020A2AE2660A87637CD00B4D5C016The transaction cannot be found.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15801470"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "861"}, + {"Date", "Mon, 25 Dec 2017 15:49:38 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - # store mock response + # store mock response - def successful_store_response do - {:ok, - %HTTPoison.Response{body: ~s{OkI00001Successful.18139914901808649724}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15829721"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "577"}, {"Date", "Mon, 25 Dec 2017 17:08:12 GMT"}, - {"Connection", "keep-alive"}], + def successful_store_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{OkI00001Successful.18139914901808649724}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15829721"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "577"}, + {"Date", "Mon, 25 Dec 2017 17:08:12 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - def store_without_profile_fields do - {:ok, - %HTTPoison.Response{body: ~s{ErrorE00041One or more fields in the profile must contain a value.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15831457"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "408"}, {"Date", "Mon, 25 Dec 2017 17:12:30 GMT"}, - {"Connection", "keep-alive"}], + status_code: 200 + }} + end + + def store_without_profile_fields do + {:ok, + %HTTPoison.Response{ + body: + ~s{ErrorE00041One or more fields in the profile must contain a value.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15831457"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "408"}, + {"Date", "Mon, 25 Dec 2017 17:12:30 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - #unstore mock response - def successful_unstore_response do - {:ok, - %HTTPoison.Response{body: ~s{OkI00001Successful.}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15833786"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "361"}, {"Date", "Mon, 25 Dec 2017 17:21:20 GMT"}, - {"Connection", "keep-alive"}], + # unstore mock response + def successful_unstore_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{OkI00001Successful.}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-15833786"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "361"}, + {"Date", "Mon, 25 Dec 2017 17:21:20 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end + status_code: 200 + }} + end - def customer_payment_profile_success_response do - {:ok, - %HTTPoison.Response{body: ~s{OkI00001Successful.181401200218086700051,1,1,(TESTMODE) This transaction has been approved.,000000,P,0,none,Test transaction for ValidateCustomerPaymentProfile.,1.00,CC,auth_only,none,,,,,,,,,,,email@example.com,,,,,,,,,0.00,0.00,0.00,FALSE,none,EA9FD49A9501D0415FE26BAEF9FD8B2C,,,,,,,,,,,,,XXXX0015,MasterCard,,,,,,,,,,,,,,,,,}, - headers: [{"Cache-Control", "private"}, - {"Content-Type", "application/xml; charset=utf-8"}, - {"X-OPNET-Transaction-Trace", - "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-17537805"}, - {"Access-Control-Allow-Origin", "*"}, - {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, - {"Access-Control-Allow-Headers", - "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, - {"Access-Control-Allow-Credentials", "true"}, {"X-Cnection", "close"}, - {"Content-Length", "828"}, {"Date", "Thu, 28 Dec 2017 13:54:20 GMT"}, - {"Connection", "keep-alive"}], + def customer_payment_profile_success_response do + {:ok, + %HTTPoison.Response{ + body: + ~s{OkI00001Successful.181401200218086700051,1,1,(TESTMODE) This transaction has been approved.,000000,P,0,none,Test transaction for ValidateCustomerPaymentProfile.,1.00,CC,auth_only,none,,,,,,,,,,,email@example.com,,,,,,,,,0.00,0.00,0.00,FALSE,none,EA9FD49A9501D0415FE26BAEF9FD8B2C,,,,,,,,,,,,,XXXX0015,MasterCard,,,,,,,,,,,,,,,,,}, + headers: [ + {"Cache-Control", "private"}, + {"Content-Type", "application/xml; charset=utf-8"}, + {"X-OPNET-Transaction-Trace", "a2_b6b84b43-d399-4dde-bc12-fb1f8ccf4b27-51156-17537805"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "PUT,OPTIONS,POST,GET"}, + {"Access-Control-Allow-Headers", + "x-requested-with,cache-control,content-type,origin,method,SOAPAction"}, + {"Access-Control-Allow-Credentials", "true"}, + {"X-Cnection", "close"}, + {"Content-Length", "828"}, + {"Date", "Thu, 28 Dec 2017 13:54:20 GMT"}, + {"Connection", "keep-alive"} + ], request_url: "https://apitest.authorize.net/xml/v1/request.api", - status_code: 200}} - end - - def netwok_error_non_existent_domain do - {:error, %HTTPoison.Error{id: nil, reason: :nxdomain}} - end + status_code: 200 + }} + end + + def netwok_error_non_existent_domain do + {:error, %HTTPoison.Error{id: nil, reason: :nxdomain}} end +end diff --git a/test/mocks/cams_mock.exs b/test/mocks/cams_mock.exs index e499b450..1eb50faf 100644 --- a/test/mocks/cams_mock.exs +++ b/test/mocks/cams_mock.exs @@ -1,211 +1,225 @@ defmodule Gringotts.Gateways.CamsMock do def successful_purchase do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3916017714&avsresponse=N&cvvresponse=N&orderid=&type=sale&response_code=100", - headers: [ - {"Date", "Thu, 21 Dec 2017 12:45:16 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "137"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3916017714&avsresponse=N&cvvresponse=N&orderid=&type=sale&response_code=100", + headers: [ + {"Date", "Thu, 21 Dec 2017 12:45:16 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "137"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def failed_purchase_with_bad_credit_card do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=Invalid Credit Card Number REFID:3502947912&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=sale&response_code=300", - headers: [ - {"Date", "Thu, 21 Dec 2017 13:20:08 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "155"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=Invalid Credit Card Number REFID:3502947912&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=sale&response_code=300", + headers: [ + {"Date", "Thu, 21 Dec 2017 13:20:08 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "155"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def with_invalid_currency do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=The cc payment type [Visa] and/or currency [INR] is not accepted REFID:3503238709&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=auth&response_code=300", - headers: [ - {"Date", "Tue, 26 Dec 2017 10:37:42 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "193"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=The cc payment type [Visa] and/or currency [INR] is not accepted REFID:3503238709&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=auth&response_code=300", + headers: [ + {"Date", "Tue, 26 Dec 2017 10:37:42 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "193"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def successful_capture do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3921111362&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=100", - headers: [ - {"Date", "Tue, 26 Dec 2017 12:16:55 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "138"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3921111362&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=100", + headers: [ + {"Date", "Tue, 26 Dec 2017 12:16:55 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "138"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def successful_authorize do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3921111362&avsresponse=N&cvvresponse=N&orderid=&type=auth&response_code=100", - headers: [ - {"Date", "Tue, 26 Dec 2017 12:16:11 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "137"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=SUCCESS&authcode=123456&transactionid=3921111362&avsresponse=N&cvvresponse=N&orderid=&type=auth&response_code=100", + headers: [ + {"Date", "Tue, 26 Dec 2017 12:16:11 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "137"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def invalid_transaction_id do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=Transaction not found REFID:3503243979&authcode=&transactionid=3921118690&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", - headers: [ - {"Date", "Tue, 26 Dec 2017 12:39:05 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "163"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=Transaction not found REFID:3503243979&authcode=&transactionid=3921118690&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", + headers: [ + {"Date", "Tue, 26 Dec 2017 12:39:05 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "163"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def more_than_authorization_amount do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=The specified amount of 1001 exceeds the authorization amount of 1000.00 REFID:3503244462&authcode=&transactionid=3921127126&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", - headers: [ - {"Date", "Tue, 26 Dec 2017 13:00:55 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "214"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=The specified amount of 1001 exceeds the authorization amount of 1000.00 REFID:3503244462&authcode=&transactionid=3921127126&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", + headers: [ + {"Date", "Tue, 26 Dec 2017 13:00:55 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "214"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def successful_refund do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=SUCCESS&authcode=&transactionid=3921158933&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=100", - headers: [ - {"Date", "Tue, 26 Dec 2017 14:00:08 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "131"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=SUCCESS&authcode=&transactionid=3921158933&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=100", + headers: [ + {"Date", "Tue, 26 Dec 2017 14:00:08 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "131"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def more_than_purchase_amount do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=Refund amount may not exceed the transaction balance REFID:3503249728&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300", - headers: [ - {"Date", "Tue, 26 Dec 2017 14:05:31 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "183"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=Refund amount may not exceed the transaction balance REFID:3503249728&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300", + headers: [ + {"Date", "Tue, 26 Dec 2017 14:05:31 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "183"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def successful_void do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=Transaction Void Successful&authcode=123456&transactionid=3921178863&avsresponse=&cvvresponse=&orderid=&type=void&response_code=100", - headers: [ - {"Date", "Tue, 26 Dec 2017 14:26:05 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "155"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=Transaction Void Successful&authcode=123456&transactionid=3921178863&avsresponse=&cvvresponse=&orderid=&type=void&response_code=100", + headers: [ + {"Date", "Tue, 26 Dec 2017 14:26:05 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "155"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def failed_authorized_with_bad_card do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=Invalid Credit Card Number REFID:3503305883&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=auth&response_code=300", - headers: [ - {"Date", "Wed, 27 Dec 2017 09:51:45 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "155"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=Invalid Credit Card Number REFID:3503305883&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=auth&response_code=300", + headers: [ + {"Date", "Wed, 27 Dec 2017 09:51:45 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "155"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def multiple_capture_on_same_transaction do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=A capture requires that the existing transaction be an AUTH REFID:3503316182&authcode=&transactionid=3922433984&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", - headers: [ - {"Date", "Wed, 27 Dec 2017 13:47:12 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "201"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=A capture requires that the existing transaction be an AUTH REFID:3503316182&authcode=&transactionid=3922433984&avsresponse=&cvvresponse=&orderid=&type=capture&response_code=300", + headers: [ + {"Date", "Wed, 27 Dec 2017 13:47:12 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "201"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def refund_the_authorised_transaction do - {:ok, %HTTPoison.Response{ - body: - "response=3&responsetext=Refund amount may not exceed the transaction balance REFID:3503316128&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300", - headers: [ - {"Date", "Wed, 27 Dec 2017 13:45:19 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "183"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=3&responsetext=Refund amount may not exceed the transaction balance REFID:3503316128&authcode=&transactionid=&avsresponse=&cvvresponse=&orderid=&type=refund&response_code=300", + headers: [ + {"Date", "Wed, 27 Dec 2017 13:45:19 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "183"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end def validate_creditcard do - {:ok, %HTTPoison.Response{ - body: - "response=1&responsetext=&authcode=&transactionid=3933708264&avsresponse=&cvvresponse=&orderid=&type=verify&response_code=100", - headers: [ - {"Date", "Thu, 04 Jan 2018 11:12:20 GMT"}, - {"Server", "Apache"}, - {"Content-Length", "124"}, - {"Content-Type", "text/html; charset=UTF-8"} - ], - request_url: "https://secure.centralams.com/gw/api/transact.php", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + "response=1&responsetext=&authcode=&transactionid=3933708264&avsresponse=&cvvresponse=&orderid=&type=verify&response_code=100", + headers: [ + {"Date", "Thu, 04 Jan 2018 11:12:20 GMT"}, + {"Server", "Apache"}, + {"Content-Length", "124"}, + {"Content-Type", "text/html; charset=UTF-8"} + ], + request_url: "https://secure.centralams.com/gw/api/transact.php", + status_code: 200 + }} end end diff --git a/test/mocks/global_collect_mock.exs b/test/mocks/global_collect_mock.exs index 8bb9d553..219b9551 100644 --- a/test/mocks/global_collect_mock.exs +++ b/test/mocks/global_collect_mock.exs @@ -1,179 +1,184 @@ defmodule Gringotts.Gateways.GlobalCollectMock do - def test_for_purchase_with_valid_card do {:ok, - %HTTPoison.Response{ - body: "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000074\",\n \"externalReference\" : \"000000122600000000740000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000740000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20180118135349\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", - headers: [{"Date", "Thu, 18 Jan 2018 12:53:49 GMT"}, + %HTTPoison.Response{ + body: + "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000074\",\n \"externalReference\" : \"000000122600000000740000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000740000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20180118135349\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", + headers: [ + {"Date", "Thu, 18 Jan 2018 12:53:49 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, {"Location", "https://api-sandbox.globalcollect.com:443/v1/1226/payments/000000122600000000740000100001"}, {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, {"Transfer-Encoding", "chunked"}, {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 201 - } - } + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 201 + }} end def test_for_purchase_with_invalid_card do {:ok, - %HTTPoison.Response{ - body: "{\n \"errorId\" : \"363899bd-acfb-4452-bbb0-741c0df6b4b8\",\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"980825\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000075\",\n \"externalReference\" : \"000000122600000000750000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000750000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"546247\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20180118135651\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n }\n}", - headers: [ - {"Date", "Thu, 18 Jan 2018 12:56:51 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Connection", "close"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 400 - } - } + %HTTPoison.Response{ + body: + "{\n \"errorId\" : \"363899bd-acfb-4452-bbb0-741c0df6b4b8\",\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"980825\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000075\",\n \"externalReference\" : \"000000122600000000750000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000750000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"546247\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20180118135651\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n }\n}", + headers: [ + {"Date", "Thu, 18 Jan 2018 12:56:51 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 400 + }} end def test_for_purchase_with_invalid_amount do {:ok, %HTTPoison.Response{ - body: "{\n \"errorId\" : \"8c34dc0b-776c-44e3-8cd4-b36222960153\",\n \"errors\" : [ {\n \"code\" : \"1099\",\n \"id\" : \"INVALID_VALUE\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"message\" : \"INVALID_VALUE: '50.3' is not a valid value for field 'amount'\",\n \"httpStatusCode\" : 400\n } ]\n}", - headers: [ - {"Date", "Wed, 24 Jan 2018 07:16:06 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Connection", "close"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"}], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 400 - } - } + body: + "{\n \"errorId\" : \"8c34dc0b-776c-44e3-8cd4-b36222960153\",\n \"errors\" : [ {\n \"code\" : \"1099\",\n \"id\" : \"INVALID_VALUE\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"message\" : \"INVALID_VALUE: '50.3' is not a valid value for field 'amount'\",\n \"httpStatusCode\" : 400\n } ]\n}", + headers: [ + {"Date", "Wed, 24 Jan 2018 07:16:06 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 400 + }} end def test_for_authorize_with_valid_card do {:ok, - %HTTPoison.Response{ - body: "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000065\",\n \"externalReference\" : \"000000122600000000650000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000650000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20180118110419\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", - headers: [ - {"Date", "Thu, 18 Jan 2018 10:04:19 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"Location", + %HTTPoison.Response{ + body: + "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000065\",\n \"externalReference\" : \"000000122600000000650000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000650000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20180118110419\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", + headers: [ + {"Date", "Thu, 18 Jan 2018 10:04:19 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"Location", "https://api-sandbox.globalcollect.com:443/v1/1226/payments/000000122600000000650000100001"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 201 - } - } + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 201 + }} end def test_for_authorize_with_invalid_card do {:ok, - %HTTPoison.Response{ - body: "{\n \"errorId\" : \"dcdf5c8d-e475-4fbc-ac57-76123c1640a2\",\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"978754\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000066\",\n \"externalReference\" : \"000000122600000000660000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000660000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"978755\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20180118111508\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n }\n}", - headers: [ - {"Date", "Thu, 18 Jan 2018 10:15:08 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Connection", "close"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 400 - } - } + %HTTPoison.Response{ + body: + "{\n \"errorId\" : \"dcdf5c8d-e475-4fbc-ac57-76123c1640a2\",\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"978754\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000066\",\n \"externalReference\" : \"000000122600000000660000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000660000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"978755\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20180118111508\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n }\n}", + headers: [ + {"Date", "Thu, 18 Jan 2018 10:15:08 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 400 + }} end def test_for_authorize_with_invalid_amount do {:ok, - %HTTPoison.Response{body: "{\n \"errorId\" : \"1dbef568-ed86-4c8d-a3c3-74ced258d5a2\",\n \"errors\" : [ {\n \"code\" : \"1099\",\n \"id\" : \"INVALID_VALUE\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"message\" : \"INVALID_VALUE: '50.3' is not a valid value for field 'amount'\",\n \"httpStatusCode\" : 400\n } ]\n}", - headers: [ - {"Date", "Tue, 23 Jan 2018 11:18:11 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Connection", "close"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", - status_code: 400 - } - } + %HTTPoison.Response{ + body: + "{\n \"errorId\" : \"1dbef568-ed86-4c8d-a3c3-74ced258d5a2\",\n \"errors\" : [ {\n \"code\" : \"1099\",\n \"id\" : \"INVALID_VALUE\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"message\" : \"INVALID_VALUE: '50.3' is not a valid value for field 'amount'\",\n \"httpStatusCode\" : 400\n } ]\n}", + headers: [ + {"Date", "Tue, 23 Jan 2018 11:18:11 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments", + status_code: 400 + }} end def test_for_refund do {:ok, %HTTPoison.Response{ - body: "{\n \"errorId\" : \"b6ba00d2-8f11-4822-8f32-c6d0a4d8793b\",\n \"errors\" : [ {\n \"code\" : \"300450\",\n \"message\" : \"ORDER WITHOUT REFUNDABLE PAYMENTS\",\n \"httpStatusCode\" : 400\n } ]\n}", - headers: [ - {"Date", "Wed, 24 Jan 2018 05:33:56 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Connection", "close"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000870000100001/refund", - status_code: 400 - } - } + body: + "{\n \"errorId\" : \"b6ba00d2-8f11-4822-8f32-c6d0a4d8793b\",\n \"errors\" : [ {\n \"code\" : \"300450\",\n \"message\" : \"ORDER WITHOUT REFUNDABLE PAYMENTS\",\n \"httpStatusCode\" : 400\n } ]\n}", + headers: [ + {"Date", "Wed, 24 Jan 2018 05:33:56 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Connection", "close"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: + "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000870000100001/refund", + status_code: 400 + }} end def test_for_capture_with_valid_paymentid do {:ok, %HTTPoison.Response{ - body: "{\n \"payment\" : {\n \"id\" : \"000000122600000000650000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 50,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"CAPTURE_REQUESTED\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_CONNECT_OR_3RD_PARTY\",\n \"statusCode\" : 800,\n \"statusCodeChangeDateTime\" : \"20180123140826\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", - headers: [ - {"Date", "Tue, 23 Jan 2018 13:08:26 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000650000100001/approve", - status_code: 200 - } - } + body: + "{\n \"payment\" : {\n \"id\" : \"000000122600000000650000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 50,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"CAPTURE_REQUESTED\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_CONNECT_OR_3RD_PARTY\",\n \"statusCode\" : 800,\n \"statusCodeChangeDateTime\" : \"20180123140826\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", + headers: [ + {"Date", "Tue, 23 Jan 2018 13:08:26 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: + "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000650000100001/approve", + status_code: 200 + }} end def test_for_capture_with_invalid_paymentid do {:ok, %HTTPoison.Response{ - body: "{\n \"errorId\" : \"ccb99804-0240-45b6-bb28-52aaae59d71b\",\n \"errors\" : [ {\n \"code\" : \"1002\",\n \"id\" : \"UNKNOWN_PAYMENT_ID\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"propertyName\" : \"paymentId\",\n \"message\" : \"UNKNOWN_PAYMENT_ID\",\n \"httpStatusCode\" : 404\n } ]\n}", - headers: [ - {"Date", "Tue, 23 Jan 2018 12:25:59 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/30/approve", - status_code: 404 - } - } + body: + "{\n \"errorId\" : \"ccb99804-0240-45b6-bb28-52aaae59d71b\",\n \"errors\" : [ {\n \"code\" : \"1002\",\n \"id\" : \"UNKNOWN_PAYMENT_ID\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"propertyName\" : \"paymentId\",\n \"message\" : \"UNKNOWN_PAYMENT_ID\",\n \"httpStatusCode\" : 404\n } ]\n}", + headers: [ + {"Date", "Tue, 23 Jan 2018 12:25:59 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/30/approve", + status_code: 404 + }} end def test_for_void_with_valid_card do {:ok, %HTTPoison.Response{ - body: "{\n \"payment\" : {\n \"id\" : \"000000122600000000870000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 50,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"CANCELLED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 99999,\n \"statusCodeChangeDateTime\" : \"20180124064204\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n}", - headers: [ - {"Date", "Wed, 24 Jan 2018 05:42:04 GMT"}, - {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, - {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, - {"Transfer-Encoding", "chunked"}, - {"Content-Type", "application/json"} - ], - request_url: "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000870000100001/cancel", - status_code: 200 - } - } + body: + "{\n \"payment\" : {\n \"id\" : \"000000122600000000870000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 50,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"CANCELLED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 99999,\n \"statusCodeChangeDateTime\" : \"20180124064204\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n}", + headers: [ + {"Date", "Wed, 24 Jan 2018 05:42:04 GMT"}, + {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, + {"X-Powered-By", "Servlet/3.0 JSP/2.2"}, + {"Transfer-Encoding", "chunked"}, + {"Content-Type", "application/json"} + ], + request_url: + "https://api-sandbox.globalcollect.com/v1/1226/payments/000000122600000000870000100001/cancel", + status_code: 200 + }} end def test_for_network_failure do diff --git a/test/mocks/trexle_mock.exs b/test/mocks/trexle_mock.exs index 27c4d1c2..73f5aa9e 100644 --- a/test/mocks/trexle_mock.exs +++ b/test/mocks/trexle_mock.exs @@ -1,197 +1,213 @@ defmodule Gringotts.Gateways.TrexleMock do def test_for_purchase_with_valid_card do - {:ok, %HTTPoison.Response{ - body: ~s/{"response":{"token":"charge_3e89c6f073606ac1efe62e76e22dd7885441dc72","success":true,"captured":false}}/, - headers: [ - {"Date", "Fri, 22 Dec 2017 11:57:28 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "9b2a1d30-9bca-48f2-862e-4090766689cb"}, - {"X-Runtime", "0.777520"}, - {"Content-Length", "104"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 201 - }} + {:ok, + %HTTPoison.Response{ + body: + ~s/{"response":{"token":"charge_3e89c6f073606ac1efe62e76e22dd7885441dc72","success":true,"captured":false}}/, + headers: [ + {"Date", "Fri, 22 Dec 2017 11:57:28 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "9b2a1d30-9bca-48f2-862e-4090766689cb"}, + {"X-Runtime", "0.777520"}, + {"Content-Length", "104"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 201 + }} end def test_for_purchase_with_invalid_card do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Payment failed","detail":"Your card's expiration year is invalid."}/, - headers: [ - {"Date", "Fri, 22 Dec 2017 13:20:50 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "eb8100a1-8ffa-47da-9623-8d3b2af51b84"}, - {"X-Runtime", "0.445244"}, - {"Content-Length", "77"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 400 - }} + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Payment failed","detail":"Your card's expiration year is invalid."}/, + headers: [ + {"Date", "Fri, 22 Dec 2017 13:20:50 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "eb8100a1-8ffa-47da-9623-8d3b2af51b84"}, + {"X-Runtime", "0.445244"}, + {"Content-Length", "77"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 400 + }} end def test_for_purchase_with_invalid_amount do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Payment failed","detail":"Amount must be at least 50 cents"}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:16:33 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "4ce2eea4-3ea9-4345-ac85-9bc45f22f5ac"}, - {"X-Runtime", "0.476058"}, - {"Content-Length", "70"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 400 - }} + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Payment failed","detail":"Amount must be at least 50 cents"}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:16:33 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "4ce2eea4-3ea9-4345-ac85-9bc45f22f5ac"}, + {"X-Runtime", "0.476058"}, + {"Content-Length", "70"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 400 + }} end def test_for_authorize_with_valid_card do - {:ok, %HTTPoison.Response{ - body: ~s/{"response":{"token":"charge_8ab2b21a2f02495f5c36b34d129c8a0e836add32","success":true,"captured":false}}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:33:31 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "51d28d13-81e5-41fd-b711-1b6531fdd3dd"}, - {"X-Runtime", "0.738395"}, - {"Content-Length", "104"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 201 - }} + {:ok, + %HTTPoison.Response{ + body: + ~s/{"response":{"token":"charge_8ab2b21a2f02495f5c36b34d129c8a0e836add32","success":true,"captured":false}}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:33:31 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "51d28d13-81e5-41fd-b711-1b6531fdd3dd"}, + {"X-Runtime", "0.738395"}, + {"Content-Length", "104"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 201 + }} end def test_for_authorize_with_invalid_card do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Payment failed","detail":"Your card's expiration year is invalid."}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:25:40 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "239e7054-9500-4a87-bf3b-09456d550b6d"}, - {"X-Runtime", "0.466670"}, - {"Content-Length", "77"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 400 - }} + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Payment failed","detail":"Your card's expiration year is invalid."}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:25:40 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "239e7054-9500-4a87-bf3b-09456d550b6d"}, + {"X-Runtime", "0.466670"}, + {"Content-Length", "77"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 400 + }} end def test_for_authorize_with_invalid_amount do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Payment failed","detail":"Amount must be at least 50 cents"}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:40:10 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "d58db806-8016-4a0e-8519-403a969fa1a7"}, - {"X-Runtime", "0.494636"}, - {"Content-Length", "70"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges", - status_code: 400 - }} + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Payment failed","detail":"Amount must be at least 50 cents"}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:40:10 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "d58db806-8016-4a0e-8519-403a969fa1a7"}, + {"X-Runtime", "0.494636"}, + {"Content-Length", "70"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges", + status_code: 400 + }} end def test_for_refund_with_valid_token do - {:ok, %HTTPoison.Response{ - body: ~s/{"response":{"token":"refund_a86a757cc6bdabab50d6ebbfcdcd4db4e04198dd","success":true,"amount":50,"charge":"charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b","status_message":"Transaction approved"}}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:55:41 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "b1c94703-7fb4-48f2-b1b4-32e3b6a87e57"}, - {"X-Runtime", "1.097186"}, - {"Content-Length", "198"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: - "https://core.trexle.com/api/v1//charges/charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b/refunds", - status_code: 201 - }} + {:ok, + %HTTPoison.Response{ + body: + ~s/{"response":{"token":"refund_a86a757cc6bdabab50d6ebbfcdcd4db4e04198dd","success":true,"amount":50,"charge":"charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b","status_message":"Transaction approved"}}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:55:41 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "b1c94703-7fb4-48f2-b1b4-32e3b6a87e57"}, + {"X-Runtime", "1.097186"}, + {"Content-Length", "198"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: + "https://core.trexle.com/api/v1//charges/charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b/refunds", + status_code: 201 + }} end def test_for_refund_with_invalid_token do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Refund failed","detail":"invalid token"}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:53:09 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "276fd8f5-dc21-40be-8add-fa76aabbfc5b"}, - {"X-Runtime", "0.009374"}, - {"Content-Length", "50"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges/34/refunds", - status_code: 400 - }} + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Refund failed","detail":"invalid token"}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:53:09 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "276fd8f5-dc21-40be-8add-fa76aabbfc5b"}, + {"X-Runtime", "0.009374"}, + {"Content-Length", "50"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges/34/refunds", + status_code: 400 + }} end def test_for_capture_with_valid_chargetoken do - {:ok, %HTTPoison.Response{ - body: ~s/{"response":{"token":"charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b","success":true,"captured":true,"amount":50,"status_message":"Transaction approved"}}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:49:50 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "97ca2db6-fd4f-4a5b-ae45-01fae9c13668"}, - {"X-Runtime", "1.092051"}, - {"Content-Length", "155"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: - "https://core.trexle.com/api/v1//charges/charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b/capture", - status_code: 200 - }} + {:ok, + %HTTPoison.Response{ + body: + ~s/{"response":{"token":"charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b","success":true,"captured":true,"amount":50,"status_message":"Transaction approved"}}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:49:50 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "97ca2db6-fd4f-4a5b-ae45-01fae9c13668"}, + {"X-Runtime", "1.092051"}, + {"Content-Length", "155"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: + "https://core.trexle.com/api/v1//charges/charge_cb17a0c34e870a479dfa13bd873e7ce7e090ec9b/capture", + status_code: 200 + }} end def test_for_capture_with_invalid_chargetoken do - {:ok, %HTTPoison.Response{ - body: ~s/{"error":"Capture failed","detail":"invalid token"}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 18:47:18 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "no-cache"}, - {"X-Request-Id", "b46ecb8d-7df8-4c5f-b87f-c53fae364e79"}, - {"X-Runtime", "0.010255"}, - {"Content-Length", "51"}, - {"X-Powered-By", "PleskLin"}, - {"Connection", "close"} - ], - request_url: "https://core.trexle.com/api/v1//charges/30/capture", - status_code: 400 - }} + {:ok, + %HTTPoison.Response{ + body: ~s/{"error":"Capture failed","detail":"invalid token"}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 18:47:18 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "no-cache"}, + {"X-Request-Id", "b46ecb8d-7df8-4c5f-b87f-c53fae364e79"}, + {"X-Runtime", "0.010255"}, + {"Content-Length", "51"}, + {"X-Powered-By", "PleskLin"}, + {"Connection", "close"} + ], + request_url: "https://core.trexle.com/api/v1//charges/30/capture", + status_code: 400 + }} end def test_for_store_with_valid_card do - {:ok, %HTTPoison.Response{ - body: ~s/{"response":{"token":"token_94e333959850270460e89a86bad2246613528cbb","card":{"token":"token_2a1ba29522e4a377fafa62e8e204f76ad8ba8f1e","scheme":"master","display_number":"XXXX-XXXX-XXXX-8210","expiry_year":2018,"expiry_month":1,"cvc":123,"name":"John Doe","address_line1":"456 My Street","address_line2":null,"address_city":"Ottawa","address_state":"ON","address_postcode":"K1C2N6","address_country":"CA","primary":true}}}/, - headers: [ - {"Date", "Sat, 23 Dec 2017 19:32:58 GMT"}, - {"Content-Type", "application/json; charset=UTF-8"}, - {"Cache-Control", "max-age=0, private, must-revalidate"}, - {"X-Request-Id", "1a334b22-8e01-4e1b-8b58-90dfd0b7c12f"}, - {"X-Runtime", "0.122441"}, - {"Content-Length", "422"}, - {"X-Powered-By", "PleskLin"} - ], - request_url: "https://core.trexle.com/api/v1//customers", - status_code: 201 - }} + {:ok, + %HTTPoison.Response{ + body: + ~s/{"response":{"token":"token_94e333959850270460e89a86bad2246613528cbb","card":{"token":"token_2a1ba29522e4a377fafa62e8e204f76ad8ba8f1e","scheme":"master","display_number":"XXXX-XXXX-XXXX-8210","expiry_year":2018,"expiry_month":1,"cvc":123,"name":"John Doe","address_line1":"456 My Street","address_line2":null,"address_city":"Ottawa","address_state":"ON","address_postcode":"K1C2N6","address_country":"CA","primary":true}}}/, + headers: [ + {"Date", "Sat, 23 Dec 2017 19:32:58 GMT"}, + {"Content-Type", "application/json; charset=UTF-8"}, + {"Cache-Control", "max-age=0, private, must-revalidate"}, + {"X-Request-Id", "1a334b22-8e01-4e1b-8b58-90dfd0b7c12f"}, + {"X-Runtime", "0.122441"}, + {"Content-Length", "422"}, + {"X-Powered-By", "PleskLin"} + ], + request_url: "https://core.trexle.com/api/v1//customers", + status_code: 201 + }} end def test_for_network_failure do From a4082cbd810aaa2a5b71b5d6421880831f50dadf Mon Sep 17 00:00:00 2001 From: Jyoti Gautam Date: Fri, 30 Mar 2018 12:05:03 +0530 Subject: [PATCH 38/60] [global-collect] Layout, docs improvements and code refactors (#111) [global-collect] Layout, docs and code refactor =============================================== New features ------------ Risk, AVS, CVS fields added in `Response` struct! Layout, docs ----------- * `credo` issues resolved. * Corrected `amount` in examples * Ran the elixir 1.6 code formatter - Used sigils in mocks Code refactors -------------- * Removed unnecessary functions - Reduced arity of `add_money` * Refactored Timex usage * Removed a test on `validate_config` as it is already tested. --- lib/gringotts/gateways/global_collect.ex | 411 ++++++++++++----------- test/gateways/global_collect_test.exs | 30 +- test/mocks/global_collect_mock.exs | 88 ++++- 3 files changed, 294 insertions(+), 235 deletions(-) diff --git a/lib/gringotts/gateways/global_collect.ex b/lib/gringotts/gateways/global_collect.ex index 624a8950..6075451d 100644 --- a/lib/gringotts/gateways/global_collect.ex +++ b/lib/gringotts/gateways/global_collect.ex @@ -2,9 +2,9 @@ defmodule Gringotts.Gateways.GlobalCollect do @moduledoc """ [GlobalCollect][home] gateway implementation. - For further details, please refer [GlobalCollect API documentation](https://epayments-api.developer-ingenico.com/s2sapi/v1/en_US/index.html). + For further details, please refer [GlobalCollect API documentation][docs]. - Following are the features that have been implemented for the GlobalCollect Gateway: + Following are the features that have been implemented for GlobalCollect: | Action | Method | | ------ | ------ | @@ -14,32 +14,34 @@ defmodule Gringotts.Gateways.GlobalCollect do | Refund | `refund/3` | | Void | `void/2` | - ## Optional or extra parameters + ## Optional parameters Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply optional arguments for transactions with the gateway. - | Key | Status | + | Key | Remark | | ---- | --- | - | `merchantCustomerId` | implemented | - | `description` | implemented | - | `customer_name` | implemented | - | `dob` | implemented | - | `company` | implemented | - | `email` | implemented | - | `phone` | implemented | - | `order_id` | implemented | - | `invoice` | implemented | - | `billingAddress` | implemented | - | `shippingAddress` | implemented | - | `name` | implemented | - | `skipAuthentication` | implemented | - + | `merchantCustomerId` | Identifier for the consumer that can be used as a search criteria in the Global Collect Payment Console | + | `description` | Descriptive text that is used towards to consumer, either during an online checkout at a third party and/or on the statement of the consumer | + | `dob` | The date of birth of the consumer Format: YYYYMMDD | + | `company` | Name of company, as a consumer | + | `email` | Email address of the consumer | + | `phone` | Phone number of the consumer | + | `invoice` | Object containing additional invoice data | + | `billingAddress` | Object containing billing address details | + | `shippingAddress` | Object containing shipping address details | + | `name` | Object containing the name details of the consumer | + | `skipAuthentication` | 3D Secure Authentication will be skipped for this transaction if set to true | + + For more details of the required keys refer [this.][options] ## Registering your GlobalCollect account at `Gringotts` - After creating your account successfully on [GlobalCollect](http://www.globalcollect.com/) follow the [dashboard link](https://sandbox.account.ingenico.com/#/account/apikey) to fetch the secret_api_key, api_key_id and [here](https://sandbox.account.ingenico.com/#/account/merchantid) for merchant_id. + After creating your account successfully on [GlobalCollect][home] open the + [dashboard][dashboard] to fetch the `secret_api_key`, `api_key_id` and + `merchant_id` from the menu. - Here's how the secrets map to the required configuration parameters for GlobalCollect: + Here's how the secrets map to the required configuration parameters for + GlobalCollect: | Config parameter | GlobalCollect secret | | ------- | ---- | @@ -47,19 +49,22 @@ defmodule Gringotts.Gateways.GlobalCollect do | `:api_key_id` | **ApiKeyId** | | `:merchant_id` | **MerchantId** | - Your Application config **must include the `[:secret_api_key, :api_key_id, :merchant_id]` field(s)** and would look - something like this: + Your Application config **must include the `:secret_api_key`, `:api_key_id`, + `:merchant_id` field(s)** and would look something like this: config :gringotts, Gringotts.Gateways.GlobalCollect, secret_api_key: "your_secret_secret_api_key" api_key_id: "your_secret_api_key_id" merchant_id: "your_secret_merchant_id" + ## Scope of this module + + * [All amount fields in globalCollect are in cents with each amount having 2 decimals.][amountReference] + ## Supported currencies and countries - The GlobalCollect platform is able to support payments in [over 150 currencies][currencies] + The GlobalCollect platform supports payments in [over 150 currencies][currencies]. - [currencies]: https://epayments.developer-ingenico.com/best-practices/services/currency-conversion ## Following the examples 1. First, set up a sample application and configure it to work with GlobalCollect. @@ -67,51 +72,74 @@ defmodule Gringotts.Gateways.GlobalCollect do - To save you time, we recommend [cloning our example repo][example] that gives you a pre-configured sample app ready-to-go. + You could use the same config or update it the with your "secrets" - as described [above](#module-registering-your-globalcollect-account-at-GlobalCollect). + as described [above](#module-registering-your-globalcollect-account-at-gringotts). 2. Run an `iex` session with `iex -S mix` and add some variable bindings and - aliases to it (to save some time): + aliases to it (to save some time): ``` iex> alias Gringotts.{Response, CreditCard, Gateways.GlobalCollect} - iex> shippingAddress = %{ - street: "Desertroad", - houseNumber: "1", - additionalInfo: "Suite II", - zip: "84536", - city: "Monument Valley", - state: "Utah", - countryCode: "US" - } + street: "Desertroad", + houseNumber: "1", + additionalInfo: "Suite II", + zip: "84536", + city: "Monument Valley", + state: "Utah", + countryCode: "US" + } iex> billingAddress = %{ - street: "Desertroad", - houseNumber: "13", - additionalInfo: "b", - zip: "84536", - city: "Monument Valley", - state: "Utah", - countryCode: "US" - } + street: "Desertroad", + houseNumber: "13", + additionalInfo: "b", + zip: "84536", + city: "Monument Valley", + state: "Utah", + countryCode: "US" + } iex> invoice = %{ - invoiceNumber: "000000123", - invoiceDate: "20140306191500" - } + invoiceNumber: "000000123", + invoiceDate: "20140306191500" + } iex> name = %{ - title: "Miss", - firstName: "Road", - surname: "Runner" - } + title: "Miss", + firstName: "Road", + surname: "Runner" + } - iex> opts = [ description: "Store Purchase 1437598192", merchantCustomerId: "234", customer_name: "John Doe", dob: "19490917", company: "asma", email: "johndoe@gmail.com", phone: "7765746563", order_id: "2323", invoice: invoice, billingAddress: billingAddress, shippingAddress: shippingAddress, name: name, skipAuthentication: "true" ] + iex> card = %CreditCard{ + number: "4567350000427977", + month: 12, + year: 43, + first_name: "John", + last_name: "Doe", + verification_code: "123", + brand: "VISA" + } + iex> opts = [ + description: "Store Purchase 1437598192", + merchantCustomerId: "234", dob: "19490917", + company: "asma", email: "johndoe@gmail.com", + phone: "7765746563", invoice: invoice, + billingAddress: billingAddress, + shippingAddress: shippingAddress, + name: name, skipAuthentication: "true" + ] ``` We'll be using these in the examples below. + [home]: http://www.globalcollect.com/ + [docs]: https://epayments-api.developer-ingenico.com/s2sapi/v1/en_US/index.html + [dashboard]: https://sandbox.account.ingenico.com/#/dashboard + [gs]: # + [options]: https://epayments-api.developer-ingenico.com/s2sapi/v1/en_US/java/payments/create.html#payments-create-payload + [currencies]: https://epayments.developer-ingenico.com/best-practices/services/currency-conversion [example]: https://github.com/aviabird/gringotts_example + [amountReference]: https://epayments-api.developer-ingenico.com/c2sapi/v1/en_US/swift/services/convertAmount.html """ @base_url "https://api-sandbox.globalcollect.com/v1/" @@ -127,12 +155,12 @@ defmodule Gringotts.Gateways.GlobalCollect do alias Gringotts.{Money, CreditCard, Response} @brand_map %{ - visa: "1", - american_express: "2", - master: "3", - discover: "128", - jcb: "125", - diners_club: "132" + VISA: "1", + AMERICAN_EXPRESS: "2", + MASTER: "3", + DISCOVER: "128", + JCB: "125", + DINERS_CLUB: "132" } @doc """ @@ -143,73 +171,70 @@ defmodule Gringotts.Gateways.GlobalCollect do also triggers risk management. Funds are not transferred. GlobalCollect returns a payment id which can be further used to: - * `capture/3` _an_ amount. - * `refund/3` _an_amount + * `capture/3` an amount. + * `refund/3` an amount * `void/2` a pre_authorization ## Example - > The following session shows how one would (pre) authorize a payment of $100 on + The following example shows how one would (pre) authorize a payment of $100 on a sample `card`. ``` iex> card = %CreditCard{ number: "4567350000427977", month: 12, - year: 18, + year: 43, first_name: "John", last_name: "Doe", verification_code: "123", - brand: "visa" + brand: "VISA" } - iex> amount = %{value: Decimal.new(100), currency: "USD"} + iex> amount = Money.new(100, :USD) iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.GlobalCollect, amount, card, opts) ``` """ @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} - def authorize(amount, card = %CreditCard{}, opts) do - params = create_params_for_auth_or_purchase(amount, card, opts) + def authorize(amount, %CreditCard{} = card, opts) do + params = %{ + order: add_order(amount, opts), + cardPaymentMethodSpecificInput: add_card(card, opts) + } + commit(:post, "payments", params, opts) end @doc """ Captures a pre-authorized `amount`. - `amount` is transferred to the merchant account by GlobalCollect used in the - pre-authorization referenced by `payment_id`. + `amount` used in the pre-authorization referenced by `payment_id` is + transferred to the merchant account by GlobalCollect. ## Note - > Authorized payment with PENDING_APPROVAL status only allow a single capture whereas the one with PENDING_CAPTURE status is used for payments that allow multiple captures. - > PENDING_APPROVAL is a common status only with card and direct debit transactions. + Authorized payment with PENDING_APPROVAL status only allow a single capture whereas + the one with PENDING_CAPTURE status is used for payments that allow multiple captures. ## Example - The following session shows how one would (partially) capture a previously + The following example shows how one would (partially) capture a previously authorized a payment worth $100 by referencing the obtained authorization `id`. ``` - iex> card = %CreditCard{ - number: "4567350000427977", - month: 12, - year: 18, - first_name: "John", - last_name: "Doe", - verification_code: "123", - brand: "visa" - } - - iex> amount = %{value: Decimal.new(100), currency: "USD"} + iex> amount = Money.new(100, :USD) - iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.GlobalCollect, amount, card, opts) + iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.GlobalCollect, auth_result.authorization, amount, opts) ``` """ @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} def capture(payment_id, amount, opts) do - params = create_params_for_capture(amount, opts) + params = %{ + order: add_order(amount, opts) + } + commit(:post, "payments/#{payment_id}/approve", params, opts) end @@ -222,28 +247,28 @@ defmodule Gringotts.Gateways.GlobalCollect do ## Example - > The following session shows how one would process a payment in one-shot, + The following example shows how one would process a payment in one-shot, without (pre) authorization. ``` iex> card = %CreditCard{ number: "4567350000427977", month: 12, - year: 18, + year: 43, first_name: "John", last_name: "Doe", verification_code: "123", - brand: "visa" + brand: "VISA" } - iex> amount = %{value: Decimal.new(100), currency: "USD"} + iex> amount = Money.new(100, :USD) iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.GlobalCollect, amount, card, opts) ``` """ @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} - def purchase(amount, card = %CreditCard{}, opts) do + def purchase(amount, %CreditCard{} = card, opts) do case authorize(amount, card, opts) do {:ok, results} -> payment_id = results.raw["payment"]["id"] @@ -257,50 +282,53 @@ defmodule Gringotts.Gateways.GlobalCollect do @doc """ Voids the referenced payment. - This makes it impossible to process the payment any further and will also try to reverse an authorization on a card. - Reversing an authorization that you will not be utilizing will prevent you from having to pay a fee/penalty for unused authorization requests. + This makes it impossible to process the payment any further and will also try + to reverse an authorization on a card. + Reversing an authorization that you will not be utilizing will prevent you + from having to [pay a fee/penalty][void] for unused authorization requests. + [void]: https://epayments-api.developer-ingenico.com/s2sapi/v1/en_US/java/payments/cancel.html#payments-cancel-request ## Example - > The following session shows how one would void a previous (pre) + The following example shows how one would void a previous (pre) authorization. Remember that our `capture/3` example only did a complete capture. ``` - iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.GlobalCollect, auth_result.payment.id, opts) + iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.GlobalCollect, auth_result.authorization, opts) ``` """ @spec void(String.t(), keyword) :: {:ok | :error, Response} def void(payment_id, opts) do - params = nil - commit(:post, "payments/#{payment_id}/cancel", params, opts) + commit(:post, "payments/#{payment_id}/cancel", [], opts) end @doc """ Refunds the `amount` to the customer's account with reference to a prior transfer. - > You can refund any transaction by just calling this API - - ## Note - You always have the option to refund just a portion of the payment amount. - It is also possible to submit multiple refund requests on one payment as long as the total amount to be refunded does not exceed the total amount that was paid. + It is also possible to submit multiple refund requests on one payment as long + as the total amount to be refunded does not exceed the total amount that was paid. ## Example - > The following session shows how one would refund a previous purchase (and + The following example shows how one would refund a previous purchase (and similarily for captures). ``` - iex> amount = %{value: Decimal.new(100), currency: "USD"} + iex> amount = Money.new(100, :USD) - iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.GlobalCollect, auth_result.payment.id, amount) + iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.GlobalCollect, auth_result.authorization, amount) ``` """ @spec refund(Money.t(), String.t(), keyword) :: {:ok | :error, Response} def refund(amount, payment_id, opts) do - params = create_params_for_refund(amount, opts) + params = %{ + amountOfMoney: add_money(amount), + customer: add_customer(opts) + } + commit(:post, "payments/#{payment_id}/refund", params, opts) end @@ -308,40 +336,18 @@ defmodule Gringotts.Gateways.GlobalCollect do # PRIVATE METHODS # ############################################################################### - # Makes the request to GlobalCollect's network. - # For consistency with other gateway implementations, make your (final) - # network request in here, and parse it using another private method called - # `respond`. - - defp create_params_for_refund(amount, opts) do - %{ - amountOfMoney: add_money(amount, opts), - customer: add_customer(opts) - } - end - - defp create_params_for_auth_or_purchase(amount, payment, opts) do - %{ - order: add_order(amount, opts), - cardPaymentMethodSpecificInput: add_payment(payment, @brand_map, opts) - } - end - - defp create_params_for_capture(amount, opts) do - %{ - order: add_order(amount, opts) - } - end - defp add_order(money, options) do %{ - amountOfMoney: add_money(money, options), + amountOfMoney: add_money(money), customer: add_customer(options), - references: add_references(options) + references: %{ + descriptor: options[:description], + invoiceData: options[:invoice] + } } end - defp add_money(amount, options) do + defp add_money(amount) do {currency, amount, _} = Money.to_integer(amount) %{ @@ -353,94 +359,65 @@ defmodule Gringotts.Gateways.GlobalCollect do defp add_customer(options) do %{ merchantCustomerId: options[:merchantCustomerId], - personalInformation: personal_info(options), + personalInformation: %{ + name: options[:name] + }, dateOfBirth: options[:dob], - companyInformation: company_info(options), + companyInformation: %{ + name: options[:company] + }, billingAddress: options[:billingAddress], shippingAddress: options[:shippingAddress], - contactDetails: contact(options) - } - end - - defp add_references(options) do - %{ - descriptor: options[:description], - invoiceData: options[:invoice] - } - end - - defp personal_info(options) do - %{ - name: options[:name] - } - end - - defp company_info(options) do - %{ - name: options[:company] - } - end - - defp contact(options) do - %{ - emailAddress: options[:email], - phoneNumber: options[:phone] + contactDetails: %{ + emailAddress: options[:email], + phoneNumber: options[:phone] + } } end - def add_card(%CreditCard{} = payment) do + defp add_card(card, opts) do %{ - cvv: payment.verification_code, - cardNumber: payment.number, - expiryDate: "#{payment.month}" <> "#{payment.year}", - cardholderName: CreditCard.full_name(payment) - } - end - - defp add_payment(payment, brand_map, opts) do - brand = payment.brand - - %{ - paymentProductId: Map.fetch!(brand_map, String.to_atom(brand)), + paymentProductId: Map.fetch!(@brand_map, String.to_atom(card.brand)), skipAuthentication: opts[:skipAuthentication], - card: add_card(payment) + card: %{ + cvv: card.verification_code, + cardNumber: card.number, + expiryDate: "#{card.month}#{card.year}", + cardholderName: CreditCard.full_name(card) + } } end - defp auth_digest(path, secret_api_key, time, opts) do - data = "POST\napplication/json\n#{time}\n/v1/#{opts[:config][:merchant_id]}/#{path}\n" - :crypto.hmac(:sha256, secret_api_key, data) - end - defp commit(method, path, params, opts) do headers = create_headers(path, opts) data = Poison.encode!(params) - url = "#{@base_url}#{opts[:config][:merchant_id]}/#{path}" - response = HTTPoison.request(method, url, data, headers) - response |> respond + merchant_id = opts[:config][:merchant_id] + url = "#{@base_url}#{merchant_id}/#{path}" + + gateway_response = HTTPoison.request(method, url, data, headers) + gateway_response |> respond end defp create_headers(path, opts) do - time = date + datetime = Timex.now() |> Timex.local() + + date_string = + "#{Timex.format!(datetime, "%a, %d %b %Y %H:%M:%S", :strftime)} #{datetime.zone_abbr}" - sha_signature = - auth_digest(path, opts[:config][:secret_api_key], time, opts) |> Base.encode64() + api_key_id = opts[:config][:api_key_id] - auth_token = "GCS v1HMAC:#{opts[:config][:api_key_id]}:#{sha_signature}" + sha_signature = auth_digest(path, date_string, opts) - headers = [ - {"Content-Type", "application/json"}, - {"Authorization", auth_token}, - {"Date", time} - ] + auth_token = "GCS v1HMAC:#{api_key_id}:#{Base.encode64(sha_signature)}" + [{"Content-Type", "application/json"}, {"Authorization", auth_token}, {"Date", date_string}] end - defp date() do - use Timex - datetime = Timex.now() |> Timex.local() - strftime_str = Timex.format!(datetime, "%a, %d %b %Y %H:%M:%S ", :strftime) - time_zone = Timex.timezone(:local, datetime) - time = strftime_str <> "#{time_zone.abbreviation}" + defp auth_digest(path, date_string, opts) do + secret_api_key = opts[:config][:secret_api_key] + merchant_id = opts[:config][:merchant_id] + + data = "POST\napplication/json\n#{date_string}\n/v1/#{merchant_id}/#{path}\n" + :crypto.hmac(:sha256, secret_api_key, data) end # Parses GlobalCollect's response and returns a `Gringotts.Response` struct @@ -450,7 +427,31 @@ defmodule Gringotts.Gateways.GlobalCollect do defp respond({:ok, %{status_code: code, body: body}}) when code in [200, 201] do case decode(body) do - {:ok, results} -> {:ok, Response.success(raw: results, status_code: code)} + {:ok, results} -> + { + :ok, + Response.success( + authorization: results["payment"]["id"], + raw: results, + status_code: code, + avs_result: + results["payment"]["paymentOutput"]["cardPaymentMethodSpecificOutput"][ + "fraudResults" + ]["avsResult"], + cvc_result: + results["payment"]["paymentOutput"]["cardPaymentMethodSpecificOutput"][ + "fraudResults" + ]["cvcResult"], + message: results["payment"]["status"], + fraud_review: + results["payment"]["paymentOutput"]["cardPaymentMethodSpecificOutput"][ + "fraudResults" + ]["fraudServiceResult"] + ) + } + + {:error, _} -> + {:error, Response.error(raw: body, message: "undefined response from GlobalCollect")} end end @@ -462,11 +463,13 @@ defmodule Gringotts.Gateways.GlobalCollect do end defp respond({:error, %HTTPoison.Error{} = error}) do - {:error, - Response.error( - code: error.id, - reason: :network_fail?, - description: "HTTPoison says '#{error.reason}'" - )} + { + :error, + Response.error( + code: error.id, + reason: :network_fail?, + description: "HTTPoison says '#{error.reason}'" + ) + } end end diff --git a/test/gateways/global_collect_test.exs b/test/gateways/global_collect_test.exs index d64489c2..8d09449c 100644 --- a/test/gateways/global_collect_test.exs +++ b/test/gateways/global_collect_test.exs @@ -14,7 +14,7 @@ defmodule Gringotts.Gateways.GlobalCollectTest do @bad_amount Money.new("50.3", :USD) - @shippingAddress %{ + @shipping_address %{ street: "Desertroad", houseNumber: "1", additionalInfo: "Suite II", @@ -31,7 +31,7 @@ defmodule Gringotts.Gateways.GlobalCollectTest do first_name: "John", last_name: "Doe", verification_code: "123", - brand: "visa" + brand: "VISA" } @invalid_card %CreditCard{ @@ -41,10 +41,10 @@ defmodule Gringotts.Gateways.GlobalCollectTest do first_name: "John", last_name: "Doe", verification_code: "123", - brand: "visa" + brand: "VISA" } - @billingAddress %{ + @billing_address %{ street: "Desertroad", houseNumber: "13", additionalInfo: "b", @@ -71,16 +71,16 @@ defmodule Gringotts.Gateways.GlobalCollectTest do @invalid_config [ config: %{ - secret_api_key: "Qtg9v4Q0G13sLRNcClWhHnvN1kVYWDcy4w9rG8T86XU=", - api_key_id: "e5743abfc360ed12" + secret_api_key: "some_secret_api_key", + api_key_id: "some_api_key_id" } ] @options [ config: %{ - secret_api_key: "Qtg9v4Q0G13sLRNcClWhHnvN1kVYWDcy4w9rG8T86XU=", - api_key_id: "e5743abfc360ed12", - merchant_id: "1226" + secret_api_key: "some_secret_api_key", + api_key_id: "some_api_key_id", + merchant_id: "some_merchant_id" }, description: "Store Purchase 1437598192", merchantCustomerId: "234", @@ -91,20 +91,12 @@ defmodule Gringotts.Gateways.GlobalCollectTest do phone: "7468474533", order_id: "2323", invoice: @invoice, - billingAddress: @billingAddress, - shippingAddress: @shippingAddress, + billingAddress: @billing_address, + shippingAddress: @shipping_address, name: @name, skipAuthentication: "true" ] - describe "validation arguments check" do - test "with no merchant id passed in config" do - assert_raise ArgumentError, fn -> - GlobalCollect.validate_config(@invalid_config) - end - end - end - describe "purchase" do test "with valid card" do with_mock HTTPoison, diff --git a/test/mocks/global_collect_mock.exs b/test/mocks/global_collect_mock.exs index 219b9551..ebedf90a 100644 --- a/test/mocks/global_collect_mock.exs +++ b/test/mocks/global_collect_mock.exs @@ -3,7 +3,15 @@ defmodule Gringotts.Gateways.GlobalCollectMock do {:ok, %HTTPoison.Response{ body: - "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000074\",\n \"externalReference\" : \"000000122600000000740000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000740000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20180118135349\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", + ~s/{"creationOutput":{"additionalReference":"00000012260000000074","externalReference": + "000000122600000000740000100001"},"payment":{"id":"000000122600000000740000100001", + "paymentOutput":{"amountOfMoney":{"amount":500,"currencyCode":"USD"},"references": + {"paymentReference":"0"},"paymentMethod":"card","cardPaymentMethodSpecificOutput": + {"paymentProductId":1,"authorisationCode":"OK1131","fraudResults":{"fraudServiceResult": + "no-advice","avsResult":"0","cvvResult":"0"},"card":{"cardNumber":"************7977", + "expiryDate":"1218"}}},"status":"PENDING_APPROVAL","statusOutput":{"isCancellable":true, + "statusCategory":"PENDING_MERCHANT","statusCode":600,"statusCodeChangeDateTime": + "20180118135349","isAuthorized":true,"isRefundable":false}}}/, headers: [ {"Date", "Thu, 18 Jan 2018 12:53:49 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -22,7 +30,18 @@ defmodule Gringotts.Gateways.GlobalCollectMock do {:ok, %HTTPoison.Response{ body: - "{\n \"errorId\" : \"363899bd-acfb-4452-bbb0-741c0df6b4b8\",\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"980825\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000075\",\n \"externalReference\" : \"000000122600000000750000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000750000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"546247\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20180118135651\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n }\n}", + ~s/{"errorId" : "363899bd-acfb-4452-bbb0-741c0df6b4b8","errors" : [ {"code" : "21000120", + "requestId" : "980825","propertyName" : "cardPaymentMethodSpecificInput.card.expiryDate", + "message" : "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT", + "httpStatusCode" : 400} ],"paymentResult" : {"creationOutput" : { " additionalReference" : "00000012260000000075", + "externalReference" : "000000122600000000750000100001"},"payment" : {"id" : "000000122600000000750000100001", + "paymentOutput" : {"amountOfMoney" : {"amount" : 500,"currencyCode" : "USD"},"references" : {"paymentReference" : "0"}, + "paymentMethod" : "card","cardPaymentMethodSpecificOutput" : {"paymentProductId" : 1}}, + "status" : "REJECTED","statusOutput" : {"errors" : [ {"code" : "21000120", + "requestId" : "546247","propertyName" : "cardPaymentMethodSpecificInput.card.expiryDate", + "message" : "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT", + "httpStatusCode" : 400} ],"isCancellable" : false,"statusCategory" : "UNSUCCESSFUL","statusCode" : 100, + "statusCodeChangeDateTime" : "20180118135651","isAuthorized" : false,"isRefundable" : false}}}}/, headers: [ {"Date", "Thu, 18 Jan 2018 12:56:51 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -40,7 +59,10 @@ defmodule Gringotts.Gateways.GlobalCollectMock do {:ok, %HTTPoison.Response{ body: - "{\n \"errorId\" : \"8c34dc0b-776c-44e3-8cd4-b36222960153\",\n \"errors\" : [ {\n \"code\" : \"1099\",\n \"id\" : \"INVALID_VALUE\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"message\" : \"INVALID_VALUE: '50.3' is not a valid value for field 'amount'\",\n \"httpStatusCode\" : 400\n } ]\n}", + ~s/{ "errorId" : "8c34dc0b-776c-44e3-8cd4-b36222960153","errors" : [ {"code" : "1099","id" : + "INVALID_VALUE","category" : "CONNECT_PLATFORM_ERROR","message" : + "INVALID_VALUE: '50.3' is not a valid value for field 'amount'", + "httpStatusCode" : 400 } ]}/, headers: [ {"Date", "Wed, 24 Jan 2018 07:16:06 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -58,7 +80,16 @@ defmodule Gringotts.Gateways.GlobalCollectMock do {:ok, %HTTPoison.Response{ body: - "{\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000065\",\n \"externalReference\" : \"000000122600000000650000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000650000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"PENDING_APPROVAL\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_MERCHANT\",\n \"statusCode\" : 600,\n \"statusCodeChangeDateTime\" : \"20180118110419\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", + ~s/{"creationOutput" : {"additionalReference" : "00000012260000000065","externalReference" : + "000000122600000000650000100001"},"payment" : {"id" : "000000122600000000650000100001", + "paymentOutput" :{"amountOfMoney" : {"amount" : 500,"currencyCode" : "USD"},"references" : + {"paymentReference" : "0" },"paymentMethod" : "card","cardPaymentMethodSpecificOutput" : + {"paymentProductId" : 1,"authorisationCode" : "OK1131","fraudResults" : + {"fraudServiceResult" : "no-advice","avsResult" : "0","cvvResult" : "0"},"card" : + {"cardNumber" : "************7977","expiryDate" : "1218"}}},"status" : "PENDING_APPROVAL", + "statusOutput" : {"isCancellable" : true,"statusCategory" : "PENDING_MERCHANT","statusCode" + : 600,"statusCodeChangeDateTime" : "20180118110419","isAuthorized" : true, + "isRefundable" : false}}}/, headers: [ {"Date", "Thu, 18 Jan 2018 10:04:19 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -77,7 +108,21 @@ defmodule Gringotts.Gateways.GlobalCollectMock do {:ok, %HTTPoison.Response{ body: - "{\n \"errorId\" : \"dcdf5c8d-e475-4fbc-ac57-76123c1640a2\",\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"978754\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"paymentResult\" : {\n \"creationOutput\" : {\n \"additionalReference\" : \"00000012260000000066\",\n \"externalReference\" : \"000000122600000000660000100001\"\n },\n \"payment\" : {\n \"id\" : \"000000122600000000660000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 500,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1\n }\n },\n \"status\" : \"REJECTED\",\n \"statusOutput\" : {\n \"errors\" : [ {\n \"code\" : \"21000120\",\n \"requestId\" : \"978755\",\n \"propertyName\" : \"cardPaymentMethodSpecificInput.card.expiryDate\",\n \"message\" : \"cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT\",\n \"httpStatusCode\" : 400\n } ],\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 100,\n \"statusCodeChangeDateTime\" : \"20180118111508\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n }\n}", + ~s/{"errorId" : "dcdf5c8d-e475-4fbc-ac57-76123c1640a2","errors" : [ {"code" : "21000120", + "requestId" : "978754","propertyName" : "cardPaymentMethodSpecificInput.card.expiryDate","message": + "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT", + "httpStatusCode" : 400} ],"paymentResult" : {"creationOutput" : + {"additionalReference" :"00000012260000000066","externalReference" : + "000000122600000000660000100001"},"payment" :{"id" : "000000122600000000660000100001", + "paymentOutput" : {"amountOfMoney" : {"amount" : 500,"currencyCode" : "USD"}, + "references" : {"paymentReference" : "0"},"paymentMethod" : "card", + "cardPaymentMethodSpecificOutput" : {"paymentProductId" : 1}},"status" : "REJECTED", + "statusOutput":{"errors" : [ {"code" : "21000120","requestId" : "978755","propertyName" : + "cardPaymentMethodSpecificInput.card.expiryDate","message" : + "cardPaymentMethodSpecificInput.card.expiryDate (1210) IS IN THE PAST OR NOT IN CORRECT MMYY FORMAT", + "httpStatusCode" : 400} ],"isCancellable" : false,"statusCategory" : + "UNSUCCESSFUL","statusCode" : 100,"statusCodeChangeDateTime" : "20180118111508", + "isAuthorized" : false,"isRefundable" : false}}}}/, headers: [ {"Date", "Thu, 18 Jan 2018 10:15:08 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -95,7 +140,9 @@ defmodule Gringotts.Gateways.GlobalCollectMock do {:ok, %HTTPoison.Response{ body: - "{\n \"errorId\" : \"1dbef568-ed86-4c8d-a3c3-74ced258d5a2\",\n \"errors\" : [ {\n \"code\" : \"1099\",\n \"id\" : \"INVALID_VALUE\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"message\" : \"INVALID_VALUE: '50.3' is not a valid value for field 'amount'\",\n \"httpStatusCode\" : 400\n } ]\n}", + ~s/{"errorId" : "1dbef568-ed86-4c8d-a3c3-74ced258d5a2","errors" : [ {"code" : "1099","id" : + "INVALID_VALUE", "category" : "CONNECT_PLATFORM_ERROR","message" : + "INVALID_VALUE: '50.3' is not a valid value for field 'amount'","httpStatusCode" : 400} ]}/, headers: [ {"Date", "Tue, 23 Jan 2018 11:18:11 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -113,7 +160,8 @@ defmodule Gringotts.Gateways.GlobalCollectMock do {:ok, %HTTPoison.Response{ body: - "{\n \"errorId\" : \"b6ba00d2-8f11-4822-8f32-c6d0a4d8793b\",\n \"errors\" : [ {\n \"code\" : \"300450\",\n \"message\" : \"ORDER WITHOUT REFUNDABLE PAYMENTS\",\n \"httpStatusCode\" : 400\n } ]\n}", + ~s/{ "errorId" : "b6ba00d2-8f11-4822-8f32-c6d0a4d8793b", "errors" : [ {"code" : "300450", + "message" : "ORDER WITHOUT REFUNDABLE PAYMENTS", "httpStatusCode" : 400 } ]}/, headers: [ {"Date", "Wed, 24 Jan 2018 05:33:56 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -131,8 +179,15 @@ defmodule Gringotts.Gateways.GlobalCollectMock do def test_for_capture_with_valid_paymentid do {:ok, %HTTPoison.Response{ - body: - "{\n \"payment\" : {\n \"id\" : \"000000122600000000650000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 50,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"CAPTURE_REQUESTED\",\n \"statusOutput\" : {\n \"isCancellable\" : true,\n \"statusCategory\" : \"PENDING_CONNECT_OR_3RD_PARTY\",\n \"statusCode\" : 800,\n \"statusCodeChangeDateTime\" : \"20180123140826\",\n \"isAuthorized\" : true,\n \"isRefundable\" : false\n }\n }\n}", + body: ~s/{ "payment" : {"id" : "000000122600000000650000100001", "paymentOutput" : { + "amountOfMoney" :{"amount" : 50,"currencyCode" : "USD"},"references" : {"paymentReference" + : "0"},"paymentMethod" : "card","cardPaymentMethodSpecificOutput" : {"paymentProductId" : + 1,"authorisationCode" : "OK1131","fraudResults" : {"fraudServiceResult" : "no-advice", + "avsResult" : "0","cvvResult" : "0"},"card" :{"cardNumber" : "************7977", + "expiryDate" : "1218"}}},"status" : "CAPTURE_REQUESTED","statusOutput" : + {"isCancellable" : true,"statusCategory" : "PENDING_CONNECT_OR_3RD_PARTY", + "statusCode" : 800,"statusCodeChangeDateTime" : "20180123140826","isAuthorized" : true, + "isRefundable" : false} }}/, headers: [ {"Date", "Tue, 23 Jan 2018 13:08:26 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -149,8 +204,9 @@ defmodule Gringotts.Gateways.GlobalCollectMock do def test_for_capture_with_invalid_paymentid do {:ok, %HTTPoison.Response{ - body: - "{\n \"errorId\" : \"ccb99804-0240-45b6-bb28-52aaae59d71b\",\n \"errors\" : [ {\n \"code\" : \"1002\",\n \"id\" : \"UNKNOWN_PAYMENT_ID\",\n \"category\" : \"CONNECT_PLATFORM_ERROR\",\n \"propertyName\" : \"paymentId\",\n \"message\" : \"UNKNOWN_PAYMENT_ID\",\n \"httpStatusCode\" : 404\n } ]\n}", + body: ~s/{ "errorId" : "ccb99804-0240-45b6-bb28-52aaae59d71b", "errors" : [ + {"code" : "1002","id" :"UNKNOWN_PAYMENT_ID","category" : "CONNECT_PLATFORM_ERROR", + "propertyName" : "paymentId","message": "UNKNOWN_PAYMENT_ID","httpStatusCode" :404}]}/, headers: [ {"Date", "Tue, 23 Jan 2018 12:25:59 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, @@ -167,7 +223,15 @@ defmodule Gringotts.Gateways.GlobalCollectMock do {:ok, %HTTPoison.Response{ body: - "{\n \"payment\" : {\n \"id\" : \"000000122600000000870000100001\",\n \"paymentOutput\" : {\n \"amountOfMoney\" : {\n \"amount\" : 50,\n \"currencyCode\" : \"USD\"\n },\n \"references\" : {\n \"paymentReference\" : \"0\"\n },\n \"paymentMethod\" : \"card\",\n \"cardPaymentMethodSpecificOutput\" : {\n \"paymentProductId\" : 1,\n \"authorisationCode\" : \"OK1131\",\n \"fraudResults\" : {\n \"fraudServiceResult\" : \"no-advice\",\n \"avsResult\" : \"0\",\n \"cvvResult\" : \"0\"\n },\n \"card\" : {\n \"cardNumber\" : \"************7977\",\n \"expiryDate\" : \"1218\"\n }\n }\n },\n \"status\" : \"CANCELLED\",\n \"statusOutput\" : {\n \"isCancellable\" : false,\n \"statusCategory\" : \"UNSUCCESSFUL\",\n \"statusCode\" : 99999,\n \"statusCodeChangeDateTime\" : \"20180124064204\",\n \"isAuthorized\" : false,\n \"isRefundable\" : false\n }\n }\n}", + ~s/{ "payment" : {"id" : "000000122600000000870000100001","paymentOutput" : {"amountOfMoney" + :{"amount" : 50,"currencyCode" : "USD"},"references" : {"paymentReference" : "0"}, + "paymentMethod" : "card","cardPaymentMethodSpecificOutput" : {"paymentProductId" : 1, + "authorisationCode" : "OK1131","fraudResults" : {"fraudServiceResult" : "no-advice", + "avsResult" : "0","cvvResult" : "0"},"card" :{"cardNumber" : "************7977", + "expiryDate" : "1218"}}},"status" : "CANCELLED","statusOutput":{"isCancellable" : + false,"statusCategory" : "UNSUCCESSFUL","statusCode" : 99999, + "statusCodeChangeDateTime" : "20180124064204","isAuthorized" : false,"isRefundable" : + false}}}/, headers: [ {"Date", "Wed, 24 Jan 2018 05:42:04 GMT"}, {"Server", "Apache/2.4.27 (Unix) OpenSSL/1.0.2l"}, From a4441797de0abf09a2a9c3d49b8ef4472c98b349 Mon Sep 17 00:00:00 2001 From: siriusdark Date: Tue, 20 Mar 2018 16:58:36 +0530 Subject: [PATCH 39/60] Initial Commit \n Implemented Authorize --- lib/gringotts/gateways/mercadopago.ex | 370 ++++++++++++++++++ test/gateways/mercadopago_test.exs | 32 ++ .../integration/gateways/mercadopago_test.exs | 36 ++ test/mocks/mercadopago_mock.exs | 9 + 4 files changed, 447 insertions(+) create mode 100644 lib/gringotts/gateways/mercadopago.ex create mode 100644 test/gateways/mercadopago_test.exs create mode 100644 test/integration/gateways/mercadopago_test.exs create mode 100644 test/mocks/mercadopago_mock.exs diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex new file mode 100644 index 00000000..06ae5dde --- /dev/null +++ b/lib/gringotts/gateways/mercadopago.ex @@ -0,0 +1,370 @@ +defmodule Gringotts.Gateways.Mercadopago do + @moduledoc """ + [mercadopago][home] gateway implementation. + + ## Instructions! + + ***This is an example `moduledoc`, and suggests some items that should be + documented in here.*** + + The quotation boxes like the one below will guide you in writing excellent + documentation for your gateway. All our gateways are documented in this manner + and we aim to keep our docs as consistent with each other as possible. + **Please read them and do as they suggest**. Feel free to add or skip sections + though. + + If you'd like to make edits to the template docs, they exist at + `templates/gateway.eex`. We encourage you to make corrections and open a PR + and tag it with the label `template`. + + ***Actual docs begin below this line!*** + + -------------------------------------------------------------------------------- + + > List features that have been implemented, and what "actions" they map to as + > per the mercadopago gateway docs. + > A table suits really well for this. + + ## Optional or extra parameters + + Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply + optional arguments for transactions with the gateway. + + > List all available (ie, those that will be supported by this module) keys, a + > description of their function/role and whether they have been implemented + > and tested. + > A table suits really well for this. + + ## Registering your mercadopago account at `Gringotts` + + Explain how to make an account with the gateway and show how to put the + `required_keys` (like authentication info) to the configuration. + + > Here's how the secrets map to the required configuration parameters for mercadopago: + > + > | Config parameter | mercadopago secret | + > | ------- | ---- | + > | `:public_key` | **PublicKey** | + > | `:access_token` | **AccessToken** | + + > Your Application config **must include the `[:public_key, :access_token]` field(s)** and would look + > something like this: + > + > config :gringotts, Gringotts.Gateways.Mercadopago, + > public_key: "your_secret_public_key" + > access_token: "your_secret_access_token" + + + ## Scope of this module + + > It's unlikely that your first iteration will support all features of the + > gateway, so list down those items that are missing. + + ## Supported currencies and countries + + > It's enough if you just add a link to the gateway's docs or FAQ that provide + > info about this. + + ## Following the examples + + 1. First, set up a sample application and configure it to work with MONEI. + - You could do that from scratch by following our [Getting Started][gs] guide. + - To save you time, we recommend [cloning our example + repo][example] that gives you a pre-configured sample app ready-to-go. + + You could use the same config or update it the with your "secrets" + as described [above](#module-registering-your-monei-account-at-mercadopago). + + 2. Run an `iex` session with `iex -S mix` and add some variable bindings and + aliases to it (to save some time): + ``` + iex> alias Gringotts.{Response, CreditCard, Gateways.Mercadopago} + iex> card = %CreditCard{first_name: "Jo", + last_name: "Doe", + number: "4200000000000000", + year: 2099, month: 12, + verification_code: "123", brand: "VISA"} + ``` + + > Add any other frequently used bindings up here. + + We'll be using these in the examples below. + + [gs]: https://github.com/aviabird/gringotts/wiki/ + [home]: https://www.mercadopago.com + [example]: https://github.com/aviabird/gringotts_example + """ + + # The Base module has the (abstract) public API, and some utility + # implementations. + @base_url "https://api.mercadopago.com" + use Gringotts.Gateways.Base + + # The Adapter module provides the `validate_config/1` + # Add the keys that must be present in the Application config in the + # `required_config` list + use Gringotts.Adapter, required_config: [:public_key, :access_token] + + import Poison, only: [decode: 1] + + alias Gringotts.{Money, + CreditCard, + Response} + + @doc """ + Performs a (pre) Authorize operation. + + The authorization validates the `card` details with the banking network, + places a hold on the transaction `amount` in the customer’s issuing bank. + + > ** You could perhaps:** + > 1. describe what are the important fields in the Response struct + > 2. mention what a merchant can do with these important fields (ex: + > `capture/3`, etc.) + + ## Note + + > If there's anything noteworthy about this operation, it comes here. + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + + + + @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + def authorize(amount, card, opts) do + # commit(args, ...) + if(is_nil(opts[:customer_id])) do + customer_id = get_customer_id(opts) + opts = opts ++ [customer_id: customer_id] + end + token_id = get_token_id(card, opts) + opts = opts ++ [token_id: token_id] + + body = get_authorize_body(amount, card, opts, opts[:token_id], opts[:customer_id]) |> Poison.encode!() + headers = [{"content-type", "application/json"}, {"accept", "application/json"}] + + response = HTTPoison.post!("#{@base_url}/v1/payments?access_token=#{opts[:config][:access_token]}", body, headers, []) + %HTTPoison.Response{body: body, status_code: status_code} = response + body = body |> Poison.decode!() + format_response(body, status_code, opts) + end + + @doc """ + Captures a pre-authorized `amount`. + + `amount` is transferred to the merchant account by mercadopago used in the + pre-authorization referenced by `payment_id`. + + ## Note + + > If there's anything noteworthy about this operation, it comes here. + > For example, does the gateway support partial, multiple captures? + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec capture(String.t(), Money.t, keyword) :: {:ok | :error, Response} + def capture(payment_id, amount, opts) do + # commit(args, ...) + end + + @doc """ + Transfers `amount` from the customer to the merchant. + + mercadopago attempts to process a purchase on behalf of the customer, by + debiting `amount` from the customer's account by charging the customer's + `card`. + + ## Note + + > If there's anything noteworthy about this operation, it comes here. + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec purchase(Money.t, CreditCard.t(), keyword) :: {:ok | :error, Response} + def purchase(amount, card = %CreditCard{}, opts) do + # commit(args, ...) + end + + @doc """ + Voids the referenced payment. + + This method attempts a reversal of a previous transaction referenced by + `payment_id`. + + > As a consequence, the customer will never see any booking on his statement. + + ## Note + + > Which transactions can be voided? + > Is there a limited time window within which a void can be perfomed? + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec void(String.t(), keyword) :: {:ok | :error, Response} + def void(payment_id, opts) do + # commit(args, ...) + end + + @doc """ + Refunds the `amount` to the customer's account with reference to a prior transfer. + + > Refunds are allowed on which kinds of "prior" transactions? + + ## Note + + > The end customer will usually see two bookings/records on his statement. Is + > that true for mercadopago? + > Is there a limited time window within which a void can be perfomed? + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec refund(Money.t, String.t(), keyword) :: {:ok | :error, Response} + def refund(amount, payment_id, opts) do + # commit(args, ...) + end + + @doc """ + Stores the payment-source data for later use. + + > This usually enable "One Click" and/or "Recurring Payments" + + ## Note + + > If there's anything noteworthy about this operation, it comes here. + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec store(CreditCard.t(), keyword) :: {:ok | :error, Response} + def store(%CreditCard{} = card, opts) do + # commit(args, ...) + end + + @doc """ + Removes card or payment info that was previously `store/2`d + + Deletes previously stored payment-source data. + + ## Note + + > If there's anything noteworthy about this operation, it comes here. + + ## Example + + > A barebones example using the bindings you've suggested in the `moduledoc`. + """ + @spec unstore(String.t(), keyword) :: {:ok | :error, Response} + def unstore(registration_id, opts) do + # commit(args, ...) + end + + ############################################################################### + # PRIVATE METHODS # + ############################################################################### + + # Makes the request to mercadopago's network. + # For consistency with other gateway implementations, make your (final) + # network request in here, and parse it using another private method called + # `respond`. + @spec commit(_) :: {:ok | :error, Response} + defp commit(_) do + # resp = HTTPoison.request(args, ...) + # respond(resp, ...) + end + + # Parses mercadopago's response and returns a `Gringotts.Response` struct + # in a `:ok`, `:error` tuple. + @spec respond(term) :: {:ok | :error, Response} + defp respond(mercadopago_response) + defp respond({:ok, %{status_code: 200, body: body}}), do: "something" + defp respond({:ok, %{status_code: status_code, body: body}}), do: "something" + defp respond({:error, %HTTPoison.Error{} = error}), do: "something" + + defp get_customer_id(opts) do + body = %{"email": opts[:email]} |> Poison.encode! + headers = [{"content-type", "application/json"}, {"accept", "application/json"}] + response = HTTPoison.post!("#{@base_url}/v1/customers?access_token=#{opts[:config][:access_token]}", body, headers, []) + %HTTPoison.Response{body: body} = response + body = body |> Poison.decode!() + body["id"] + end + + defp get_token_body(card) do + %{ + "expirationYear": card[:year], + "expirationMonth": card[:month], + "cardNumber": card[:number], + "securityCode": card[:verification_code], + "cardholder": %{ + "name": card[:first_name] <> card[:last_name] + } + } + end + + defp get_token_id(card, opts) do + body = get_token_body(card) |> Poison.encode!() + headers = [{"content-type", "application/json"}, {"accept", "application/json"}] + token = HTTPoison.post!("#{@base_url}/v1/card_tokens/#{opts[:customer_id]}?public_key=#{opts[:config][:public_key]}", body, headers, []) + %HTTPoison.Response{body: body} = token + body = body |> Poison.decode!() + body["id"] + end + + defp get_authorize_body(amount, card, opts, token_id, customer_id) do + %{ + "payer": %{ + "type": "customer", + "id": customer_id, + "first_name": card[:first_name], + "last_name": card[:last_name] + }, + + "order": %{ + "type": "mercadopago", + "id": opts[:order_id] + }, + "installments": 1, + "transaction_amount": amount, + "payment_method_id": opts[:payment_method_id], #visa + "token": token_id, + capture: false + } + end + + defp get_success_body(body, status_code, opts) do + %Response{ + success: true, + id: body["id"], + token: opts[:customer_id], + status_code: status_code, + message: body["status"] + } + end + defp get_error_body(body, status_code, opts) do + %Response{ + success: false, + token: opts[:customer_id], + status_code: status_code, + message: body["message"] + } + end + + defp format_response(body, status_code, opts) do + case body["cause"] do + nil -> {:ok, get_success_body(body, status_code, opts)} + _ -> {:error, get_error_body(body, status_code, opts)} + end + end + +end diff --git a/test/gateways/mercadopago_test.exs b/test/gateways/mercadopago_test.exs new file mode 100644 index 00000000..62bf1ae7 --- /dev/null +++ b/test/gateways/mercadopago_test.exs @@ -0,0 +1,32 @@ +defmodule Gringotts.Gateways.MercadopagoTest do + # The file contains mocked tests for Mercadopago + + # We recommend using [mock][1] for this, you can place the mock responses from + # the Gateway in `test/mocks/mercadopago_mock.exs` file, which has also been + # generated for you. + # + # [1]: https://github.com/jjh42/mock + + # Load the mock response file before running the tests. + Code.require_file "../mocks/mercadopago_mock.exs", __DIR__ + + use ExUnit.Case, async: false + alias Gringotts.Gateways.Mercadopago + import Mock + + # Group the test cases by public api + describe "purchase" do + end + + describe "authorize" do + end + + describe "capture" do + end + + describe "void" do + end + + describe "refund" do + end +end diff --git a/test/integration/gateways/mercadopago_test.exs b/test/integration/gateways/mercadopago_test.exs new file mode 100644 index 00000000..8d60dd1a --- /dev/null +++ b/test/integration/gateways/mercadopago_test.exs @@ -0,0 +1,36 @@ +defmodule Gringotts.Integration.Gateways.MercadopagoTest do + # Integration tests for the Mercadopago + + use ExUnit.Case, async: false + alias Gringotts.Gateways.Mercadopago + + @moduletag :integration + + setup_all do + Application.put_env(:gringotts, Gringotts.Gateways.Mercadopago, + [ + public_key: "your_secret_public_key", + access_token: "your_secret_access_token" + ] + ) + end + + # Group the test cases by public api + describe "purchase" do + end + + describe "authorize" do + end + + describe "capture" do + end + + describe "void" do + end + + describe "refund" do + end + + describe "environment setup" do + end +end diff --git a/test/mocks/mercadopago_mock.exs b/test/mocks/mercadopago_mock.exs new file mode 100644 index 00000000..f7a73709 --- /dev/null +++ b/test/mocks/mercadopago_mock.exs @@ -0,0 +1,9 @@ +defmodule Gringotts.Gateways.MercadopagoMock do + + # The module should include mock responses for test cases in mercadopago_test.exs. + # e.g. + # def successful_purchase do + # {:ok, %HTTPoison.Response{body: ~s[{data: "successful_purchase"}]} + # end + +end From ea9c91234f4f5d46a16d898e4ed6d780d7401412 Mon Sep 17 00:00:00 2001 From: siriusdark Date: Tue, 20 Mar 2018 20:27:54 +0530 Subject: [PATCH 40/60] Implemented authorize Created all the helper functions, body for HTTPoison requests, response methods. --- lib/gringotts/gateways/mercadopago.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex index 06ae5dde..0c50911d 100644 --- a/lib/gringotts/gateways/mercadopago.ex +++ b/lib/gringotts/gateways/mercadopago.ex @@ -329,7 +329,6 @@ defmodule Gringotts.Gateways.Mercadopago do "first_name": card[:first_name], "last_name": card[:last_name] }, - "order": %{ "type": "mercadopago", "id": opts[:order_id] From 2127fe9dfaba3c6d9ad6cd8db7c5a73c9cafe801 Mon Sep 17 00:00:00 2001 From: siriusdark Date: Wed, 21 Mar 2018 15:19:46 +0530 Subject: [PATCH 41/60] Refined authorize function 1. Completed the docs 2. Used predefined CreditCard and Money data type 3. Added brand in CreditCard --- lib/gringotts/gateways/mercadopago.ex | 165 ++++++++++++++------------ 1 file changed, 86 insertions(+), 79 deletions(-) diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex index 0c50911d..56042f4d 100644 --- a/lib/gringotts/gateways/mercadopago.ex +++ b/lib/gringotts/gateways/mercadopago.ex @@ -1,51 +1,40 @@ defmodule Gringotts.Gateways.Mercadopago do @moduledoc """ - [mercadopago][home] gateway implementation. + [MERCADOPAGO][home] gateway implementation. - ## Instructions! - - ***This is an example `moduledoc`, and suggests some items that should be - documented in here.*** + For reference see [MERCADOPAGO API (v1) documentation][docs]. - The quotation boxes like the one below will guide you in writing excellent - documentation for your gateway. All our gateways are documented in this manner - and we aim to keep our docs as consistent with each other as possible. - **Please read them and do as they suggest**. Feel free to add or skip sections - though. + The following features of MERCADOPAGO are implemented: - If you'd like to make edits to the template docs, they exist at - `templates/gateway.eex`. We encourage you to make corrections and open a PR - and tag it with the label `template`. + | Action | Method | + | ------ | ------ | + | Pre-authorize | `authorize/3` | - ***Actual docs begin below this line!*** - - -------------------------------------------------------------------------------- + [home]: https://www.mercadopago.com/ + [docs]: https://www.mercadopago.com.ar/developers/en/api-docs/ - > List features that have been implemented, and what "actions" they map to as - > per the mercadopago gateway docs. - > A table suits really well for this. + ## The `opts` argument - ## Optional or extra parameters + Most `Gringotts` API calls accept an optional `keyword` list `opts` to supply + optional arguments for transactions with the MERCADOAPGO + gateway. The following keys are supported: - Most `Gringotts` API calls accept an optional `Keyword` list `opts` to supply - optional arguments for transactions with the gateway. - - > List all available (ie, those that will be supported by this module) keys, a - > description of their function/role and whether they have been implemented - > and tested. - > A table suits really well for this. + | Key | Remark | + | ---- | --- | + | `email` | Email of the customer. Type - string . | + | `order_id` | Order id issued by the merchant. Type- integer. | + | `payment_method_id` | Payment network operators, eg. `visa`, `mastercard`. Type- string. | + | `customer_id` | Unique customer id issued by the gateway. For new customer it must be nil. Type- string | - ## Registering your mercadopago account at `Gringotts` + ## Registering your MERCADOPAGO account at `Gringotts` - Explain how to make an account with the gateway and show how to put the - `required_keys` (like authentication info) to the configuration. - - > Here's how the secrets map to the required configuration parameters for mercadopago: - > - > | Config parameter | mercadopago secret | - > | ------- | ---- | - > | `:public_key` | **PublicKey** | - > | `:access_token` | **AccessToken** | + After [making an account on MERCADOPAGO][credentials], head to the credentials and find + your account "secrets" in the `Checkout Transparent`. + + | Config parameter | MERCADOPAGO secret | + | ------- | ---- | + | `:access_token` | **Access Token** | + | `:public_key` | **Public Key** | > Your Application config **must include the `[:public_key, :access_token]` field(s)** and would look > something like this: @@ -53,40 +42,42 @@ defmodule Gringotts.Gateways.Mercadopago do > config :gringotts, Gringotts.Gateways.Mercadopago, > public_key: "your_secret_public_key" > access_token: "your_secret_access_token" - - - ## Scope of this module - > It's unlikely that your first iteration will support all features of the - > gateway, so list down those items that are missing. + [credentials]: https://www.mercadopago.com/mlb/account/credentials?type=basic + + * MERCADOPAGO does not process money in cents, and the `amount` is rounded to 2 + decimal places. + + > Please [raise an issue][new-issue] if you'd like us to add support for more + > currencies + [new-issue]: https://github.com/aviabird/gringotts/issues ## Supported currencies and countries - > It's enough if you just add a link to the gateway's docs or FAQ that provide - > info about this. + > MERCADOPAGO supports the currencies listed [here][all-currency-list] + [all-currency-list]: https://www.mercadopago.com.br/developers/en/api-docs/account/payments/search + :ARS, :BRL, :VEF,:CLP, :MXN, :COP, :PEN, :UYU ## Following the examples - 1. First, set up a sample application and configure it to work with MONEI. + 1. First, set up a sample application and configure it to work with MERCADOPAGO. - You could do that from scratch by following our [Getting Started][gs] guide. - To save you time, we recommend [cloning our example repo][example] that gives you a pre-configured sample app ready-to-go. + You could use the same config or update it the with your "secrets" as described [above](#module-registering-your-monei-account-at-mercadopago). - 2. Run an `iex` session with `iex -S mix` and add some variable bindings and - aliases to it (to save some time): + 2. Run an `iex` session with `iex -S mix` and add some variable bindings : ``` - iex> alias Gringotts.{Response, CreditCard, Gateways.Mercadopago} - iex> card = %CreditCard{first_name: "Jo", - last_name: "Doe", - number: "4200000000000000", - year: 2099, month: 12, - verification_code: "123", brand: "VISA"} + iex> card = %{first_name: "John", + last_name: "Doe", + number: "4509953566233704", + year: 2099, + month: 12, + verification_code: "123", + brand: "VISA"} ``` - > Add any other frequently used bindings up here. - We'll be using these in the examples below. [gs]: https://github.com/aviabird/gringotts/wiki/ @@ -98,7 +89,7 @@ defmodule Gringotts.Gateways.Mercadopago do # implementations. @base_url "https://api.mercadopago.com" use Gringotts.Gateways.Base - + alias Gringotts.CreditCard # The Adapter module provides the `validate_config/1` # Add the keys that must be present in the Application config in the # `required_config` list @@ -106,8 +97,7 @@ defmodule Gringotts.Gateways.Mercadopago do import Poison, only: [decode: 1] - alias Gringotts.{Money, - CreditCard, + alias Gringotts.{CreditCard, Response} @doc """ @@ -116,25 +106,42 @@ defmodule Gringotts.Gateways.Mercadopago do The authorization validates the `card` details with the banking network, places a hold on the transaction `amount` in the customer’s issuing bank. - > ** You could perhaps:** - > 1. describe what are the important fields in the Response struct - > 2. mention what a merchant can do with these important fields (ex: - > `capture/3`, etc.) + MERCADOPAGO's `authorize` returns a map containing authorization ID string which can be used to: + + * `capture/3` _an_ amount. + * `void/2` a pre-authorization. ## Note > If there's anything noteworthy about this operation, it comes here. ## Example + iex> amount = Money.new(42, :BRL) + iex> card = %Gringotts.CreditCard{ + first_name: "John", + last_name: "Doe", + number: "4509953566233704", + year: 2099, + month: 12, + verification_code: "123", + brand: "VISA" + } + iex> opts = [email: "xyz@test.com", + order_id: 123125, + customer_id: "308537342-HStv9cJCgK0dWU", + payment_method_id: "visa", + config: %{access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", + public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"}] + iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Mercadopago, amount, card, opts) + iex> auth_result.id # This is the authorization ID + iex> auth_result.token # This is the customer ID/token - > A barebones example using the bindings you've suggested in the `moduledoc`. """ - - @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} - def authorize(amount, card, opts) do + def authorize(amount , %CreditCard{} = card, opts) do # commit(args, ...) + {_, value, _, _} = Money.to_integer_exp(amount) if(is_nil(opts[:customer_id])) do customer_id = get_customer_id(opts) opts = opts ++ [customer_id: customer_id] @@ -142,7 +149,7 @@ defmodule Gringotts.Gateways.Mercadopago do token_id = get_token_id(card, opts) opts = opts ++ [token_id: token_id] - body = get_authorize_body(amount, card, opts, opts[:token_id], opts[:customer_id]) |> Poison.encode!() + body = get_authorize_body(value, card, opts, opts[:token_id], opts[:customer_id]) |> Poison.encode!() headers = [{"content-type", "application/json"}, {"accept", "application/json"}] response = HTTPoison.post!("#{@base_url}/v1/payments?access_token=#{opts[:config][:access_token]}", body, headers, []) @@ -187,7 +194,7 @@ defmodule Gringotts.Gateways.Mercadopago do > A barebones example using the bindings you've suggested in the `moduledoc`. """ @spec purchase(Money.t, CreditCard.t(), keyword) :: {:ok | :error, Response} - def purchase(amount, card = %CreditCard{}, opts) do + def purchase(amount, %CreditCard{} = card, opts) do # commit(args, ...) end @@ -300,19 +307,19 @@ defmodule Gringotts.Gateways.Mercadopago do body["id"] end - defp get_token_body(card) do + defp get_token_body(%CreditCard{} = card) do %{ - "expirationYear": card[:year], - "expirationMonth": card[:month], - "cardNumber": card[:number], - "securityCode": card[:verification_code], + "expirationYear": card.year, + "expirationMonth": card.month, + "cardNumber": card.number, + "securityCode": card.verification_code, "cardholder": %{ - "name": card[:first_name] <> card[:last_name] + "name": card.first_name <> card.last_name } } end - defp get_token_id(card, opts) do + defp get_token_id(%CreditCard{} = card, opts) do body = get_token_body(card) |> Poison.encode!() headers = [{"content-type", "application/json"}, {"accept", "application/json"}] token = HTTPoison.post!("#{@base_url}/v1/card_tokens/#{opts[:customer_id]}?public_key=#{opts[:config][:public_key]}", body, headers, []) @@ -321,20 +328,20 @@ defmodule Gringotts.Gateways.Mercadopago do body["id"] end - defp get_authorize_body(amount, card, opts, token_id, customer_id) do + defp get_authorize_body(value, %CreditCard{} = card, opts, token_id, customer_id) do %{ "payer": %{ "type": "customer", "id": customer_id, - "first_name": card[:first_name], - "last_name": card[:last_name] + "first_name": card.first_name, + "last_name": card.last_name }, "order": %{ "type": "mercadopago", "id": opts[:order_id] }, "installments": 1, - "transaction_amount": amount, + "transaction_amount": value, "payment_method_id": opts[:payment_method_id], #visa "token": token_id, capture: false From d7d74b994ad6000071d6bd68bb72e51399b92a9f Mon Sep 17 00:00:00 2001 From: siriusdark Date: Wed, 21 Mar 2018 20:58:37 +0530 Subject: [PATCH 42/60] Implemented authorize 1. Implemented authorize 2. Completed docs 3. Used CreditCard and Money datatype --- lib/gringotts/gateways/mercadopago.ex | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex index 56042f4d..535d858a 100644 --- a/lib/gringotts/gateways/mercadopago.ex +++ b/lib/gringotts/gateways/mercadopago.ex @@ -140,7 +140,6 @@ defmodule Gringotts.Gateways.Mercadopago do @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} def authorize(amount , %CreditCard{} = card, opts) do - # commit(args, ...) {_, value, _, _} = Money.to_integer_exp(amount) if(is_nil(opts[:customer_id])) do customer_id = get_customer_id(opts) @@ -175,7 +174,12 @@ defmodule Gringotts.Gateways.Mercadopago do """ @spec capture(String.t(), Money.t, keyword) :: {:ok | :error, Response} def capture(payment_id, amount, opts) do - # commit(args, ...) + body = %{"capture": true} |> Poison.encode! + headers = [{"content-type", "application/json"}, {"accept", "application/json"}] + response = HTTPoison.put!("#{@base_url}/v1/payments/#{payment_id}?access_token=#{opts[:config][:access_token]}", body, headers, []) + %HTTPoison.Response{body: body, status_code: status_code} = response + body = body |> Poison.decode!() + format_response(body, status_code, opts) end @doc """ @@ -237,7 +241,13 @@ defmodule Gringotts.Gateways.Mercadopago do """ @spec refund(Money.t, String.t(), keyword) :: {:ok | :error, Response} def refund(amount, payment_id, opts) do - # commit(args, ...) + {_, value, _, _} = Money.to_integer_exp(amount) + body = %{"amount": value} |> Poison.encode! + headers = [{"content-type", "application/json"}] + response = HTTPoison.post!("#{@base_url}/v1/payments/#{payment_id}/refunds?access_token=#{opts[:config][:access_token]}", body, headers, []) + %HTTPoison.Response{body: body, status_code: status_code} = response + body = body |> Poison.decode! + format_response(body, status_code, opts) end @doc """ @@ -344,7 +354,7 @@ defmodule Gringotts.Gateways.Mercadopago do "transaction_amount": value, "payment_method_id": opts[:payment_method_id], #visa "token": token_id, - capture: false + "capture": false } end From b776dc9527293357414a21ec936ed08b645af22d Mon Sep 17 00:00:00 2001 From: siriusdark Date: Mon, 26 Mar 2018 21:03:24 +0530 Subject: [PATCH 43/60] jgkg --- lib/gringotts/gateways/mercadopago.ex | 324 +++++++++++++------------- 1 file changed, 168 insertions(+), 156 deletions(-) diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex index 535d858a..0fb936a9 100644 --- a/lib/gringotts/gateways/mercadopago.ex +++ b/lib/gringotts/gateways/mercadopago.ex @@ -1,14 +1,18 @@ defmodule Gringotts.Gateways.Mercadopago do @moduledoc """ - [MERCADOPAGO][home] gateway implementation. + [mercadopago][home] gateway implementation. - For reference see [MERCADOPAGO API (v1) documentation][docs]. + For reference see [mercadopago documentation][docs]. - The following features of MERCADOPAGO are implemented: + The following features of mercadopago are implemented: | Action | Method | | ------ | ------ | | Pre-authorize | `authorize/3` | + | Capture | `capture/3` | + | Purchase | `purchase/3` | + | Reversal | `void/2` | + | Refund | `refund/3` | [home]: https://www.mercadopago.com/ [docs]: https://www.mercadopago.com.ar/developers/en/api-docs/ @@ -16,7 +20,7 @@ defmodule Gringotts.Gateways.Mercadopago do ## The `opts` argument Most `Gringotts` API calls accept an optional `keyword` list `opts` to supply - optional arguments for transactions with the MERCADOAPGO + optional arguments for transactions with the mercadopago gateway. The following keys are supported: | Key | Remark | @@ -26,9 +30,9 @@ defmodule Gringotts.Gateways.Mercadopago do | `payment_method_id` | Payment network operators, eg. `visa`, `mastercard`. Type- string. | | `customer_id` | Unique customer id issued by the gateway. For new customer it must be nil. Type- string | - ## Registering your MERCADOPAGO account at `Gringotts` + ## Registering your mercadopago account at `Gringotts` - After [making an account on MERCADOPAGO][credentials], head to the credentials and find + After [making an account on mercadopago][credentials], head to the credentials and find your account "secrets" in the `Checkout Transparent`. | Config parameter | MERCADOPAGO secret | @@ -45,18 +49,13 @@ defmodule Gringotts.Gateways.Mercadopago do [credentials]: https://www.mercadopago.com/mlb/account/credentials?type=basic - * MERCADOPAGO does not process money in cents, and the `amount` is rounded to 2 + * mercadopago does not process money in cents, and the `amount` is rounded to 2 decimal places. - > Please [raise an issue][new-issue] if you'd like us to add support for more - > currencies - [new-issue]: https://github.com/aviabird/gringotts/issues - ## Supported currencies and countries - > MERCADOPAGO supports the currencies listed [here][all-currency-list] - [all-currency-list]: https://www.mercadopago.com.br/developers/en/api-docs/account/payments/search - :ARS, :BRL, :VEF,:CLP, :MXN, :COP, :PEN, :UYU + > mercadoapgo supports the currencies listed here : + :ARS, :BRL, :VEF, :CLP, :MXN, :COP, :PEN, :UYU ## Following the examples @@ -69,13 +68,7 @@ defmodule Gringotts.Gateways.Mercadopago do 2. Run an `iex` session with `iex -S mix` and add some variable bindings : ``` - iex> card = %{first_name: "John", - last_name: "Doe", - number: "4509953566233704", - year: 2099, - month: 12, - verification_code: "123", - brand: "VISA"} + iex> card = %CreditCard{first_name: "John", last_name: "Doe", number: "4509953566233704", year: 2099, month: 12, verification_code: "123", brand: "VISA"} ``` We'll be using these in the examples below. @@ -97,8 +90,7 @@ defmodule Gringotts.Gateways.Mercadopago do import Poison, only: [decode: 1] - alias Gringotts.{CreditCard, - Response} + alias Gringotts.{CreditCard, Response} @doc """ Performs a (pre) Authorize operation. @@ -106,32 +98,30 @@ defmodule Gringotts.Gateways.Mercadopago do The authorization validates the `card` details with the banking network, places a hold on the transaction `amount` in the customer’s issuing bank. - MERCADOPAGO's `authorize` returns a map containing authorization ID string which can be used to: + mercadoapgo's `authorize` returns a map containing authorization ID string which can be used to: * `capture/3` _an_ amount. * `void/2` a pre-authorization. - ## Note - > If there's anything noteworthy about this operation, it comes here. + > For a new customer, `customer_id` field should be ignored. Otherwise it should be provided. ## Example + + ### Authorization using `email`, for new customer. (Ignore `customer_id`) + The following example shows how one would (pre) authorize a payment of 42 BRL on a sample `card`. iex> amount = Money.new(42, :BRL) - iex> card = %Gringotts.CreditCard{ - first_name: "John", - last_name: "Doe", - number: "4509953566233704", - year: 2099, - month: 12, - verification_code: "123", - brand: "VISA" - } - iex> opts = [email: "xyz@test.com", - order_id: 123125, - customer_id: "308537342-HStv9cJCgK0dWU", - payment_method_id: "visa", - config: %{access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", - public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"}] + iex> card = %Gringotts.CreditCard{first_name: "Lord", last_name: "Voldemort", number: "4509953566233704", year: 2099, month: 12, verification_code: "123", brand: "VISA"} + iex> opts = [email: "tommarvolo@riddle.com", order_id: 123123, payment_method_id: "visa", config: %{access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"}] + iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Mercadopago, amount, card, opts) + iex> auth_result.id # This is the authorization ID + iex> auth_result.token # This is the customer ID/token + + ### Authorization using `customer_id`, for old customer. (Mention `customer_id`) + The following example shows how one would (pre) authorize a payment of 42 BRL on a sample `card`. + iex> amount = Money.new(42, :BRL) + iex> card = %Gringotts.CreditCard{first_name: "Hermione", last_name: "Granger", number: "4509953566233704", year: 2099, month: 12, verification_code: "123", brand: "VISA"} + iex> opts = [email: "hermione@granger.com", order_id: 123125, customer_id: "308537342-HStv9cJCgK0dWU", payment_method_id: "visa", config: %{access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"}] iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Mercadopago, amount, card, opts) iex> auth_result.id # This is the authorization ID iex> auth_result.token # This is the customer ID/token @@ -140,22 +130,18 @@ defmodule Gringotts.Gateways.Mercadopago do @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} def authorize(amount , %CreditCard{} = card, opts) do - {_, value, _, _} = Money.to_integer_exp(amount) - if(is_nil(opts[:customer_id])) do - customer_id = get_customer_id(opts) - opts = opts ++ [customer_id: customer_id] + if Keyword.has_key?(opts, :customer_id) do + auth_token(amount, card, opts, opts[:customer_id], false) + else + {state, res} = get_customer_id(opts) + if state == :error do + {state, res} + else + auth_token(amount, card, opts, res, false) + end end - token_id = get_token_id(card, opts) - opts = opts ++ [token_id: token_id] - - body = get_authorize_body(value, card, opts, opts[:token_id], opts[:customer_id]) |> Poison.encode!() - headers = [{"content-type", "application/json"}, {"accept", "application/json"}] - - response = HTTPoison.post!("#{@base_url}/v1/payments?access_token=#{opts[:config][:access_token]}", body, headers, []) - %HTTPoison.Response{body: body, status_code: status_code} = response - body = body |> Poison.decode!() - format_response(body, status_code, opts) end + @doc """ Captures a pre-authorized `amount`. @@ -164,22 +150,31 @@ defmodule Gringotts.Gateways.Mercadopago do pre-authorization referenced by `payment_id`. ## Note + mercadopago allows partial captures also. However, you can make a partial capture to a payment only **once**. - > If there's anything noteworthy about this operation, it comes here. - > For example, does the gateway support partial, multiple captures? + > The authorization will be valid for 7 days. If you do not capture it by that time, it will be cancelled. + + > The specified amount can not exceed the originally reserved. + + > If you do not specify the amount, all the reserved money is captured. + + > In Argentina only available for Visa and American Express cards. ## Example - > A barebones example using the bindings you've suggested in the `moduledoc`. + The following example shows how one would (partially) capture a previously + authorized a payment worth 35 BRL by referencing the obtained authorization `id`. + + iex> amount = Money.new(35, :BRL) + iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Mercadopago, auth_result.id, amount, opts) """ @spec capture(String.t(), Money.t, keyword) :: {:ok | :error, Response} def capture(payment_id, amount, opts) do - body = %{"capture": true} |> Poison.encode! + {_, value, _, _} = Money.to_integer_exp(amount) + url = "#{@base_url}/v1/payments/#{payment_id}?access_token=#{opts[:config][:access_token]}" + body = %{"capture": true, "transaction_amount": value} |> Poison.encode! headers = [{"content-type", "application/json"}, {"accept", "application/json"}] - response = HTTPoison.put!("#{@base_url}/v1/payments/#{payment_id}?access_token=#{opts[:config][:access_token]}", body, headers, []) - %HTTPoison.Response{body: body, status_code: status_code} = response - body = body |> Poison.decode!() - format_response(body, status_code, opts) + commit(:put, url, body, headers) |> respond(opts) end @doc """ @@ -189,17 +184,29 @@ defmodule Gringotts.Gateways.Mercadopago do debiting `amount` from the customer's account by charging the customer's `card`. - ## Note + ## Example - > If there's anything noteworthy about this operation, it comes here. + The following example shows how one would process a payment worth 42 BRL in + one-shot, without (pre) authorization. - ## Example + iex> amount = Money.new(42, :BRL) + iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} + iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.Mercadopago, amount, card, opts) + iex> purchase_result.token # This is the customer ID/token - > A barebones example using the bindings you've suggested in the `moduledoc`. """ @spec purchase(Money.t, CreditCard.t(), keyword) :: {:ok | :error, Response} def purchase(amount, %CreditCard{} = card, opts) do - # commit(args, ...) + if Keyword.has_key?(opts, :customer_id) do + auth_token(amount, card, opts, opts[:customer_id], true) + else + {state, res} = get_customer_id(opts) + if state == :error do + {state, res} + else + auth_token(amount, card, opts, res, true) + end + end end @doc """ @@ -212,79 +219,60 @@ defmodule Gringotts.Gateways.Mercadopago do ## Note - > Which transactions can be voided? - > Is there a limited time window within which a void can be perfomed? + > Only pending or in_process payments can be cancelled. + + > Cancelled coupon payments, deposits and/or transfers will be deposited in the buyer’s Mercadopago account. ## Example - > A barebones example using the bindings you've suggested in the `moduledoc`. + The following example shows how one would void a previous (pre) + authorization. Remember that our `capture/3` example only did a partial + capture. + + iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.Mercadopago, auth_result.id, opts) + """ @spec void(String.t(), keyword) :: {:ok | :error, Response} def void(payment_id, opts) do - # commit(args, ...) + url = "#{@base_url}/v1/payments/#{payment_id}?access_token=#{opts[:config][:access_token]}" + headers = [{"content-type", "application/json"}] + body = %{"status": "cancelled"} |> Poison.encode! + commit(:put, url, body, headers) |> respond(opts) end @doc """ Refunds the `amount` to the customer's account with reference to a prior transfer. - > Refunds are allowed on which kinds of "prior" transactions? + mercadopago processes a full or partial refund worth `amount`, referencing a + previous `purchase/3` or `capture/3`. ## Note - > The end customer will usually see two bookings/records on his statement. Is - > that true for mercadopago? - > Is there a limited time window within which a void can be perfomed? + > You must have enough available money in your account so you can refund the payment amount successfully. Otherwise, you'll get a 400 Bad Request error. + + > You can refund a payment within 90 days after it was accredited. + + > You can only refund approved payments. + + > You can perform up to 20 partial refunds in one payment. ## Example - > A barebones example using the bindings you've suggested in the `moduledoc`. + The following example shows how one would (completely) refund a previous + purchase (and similarily for captures). + + iex> amount = Money.new(35, :BRL) + iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.Mercadopago, purchase_result.id, amount) """ @spec refund(Money.t, String.t(), keyword) :: {:ok | :error, Response} - def refund(amount, payment_id, opts) do + def refund(payment_id, amount, opts) do {_, value, _, _} = Money.to_integer_exp(amount) + url = "#{@base_url}/v1/payments/#{payment_id}/refunds?access_token=#{opts[:config][:access_token]}" body = %{"amount": value} |> Poison.encode! headers = [{"content-type", "application/json"}] - response = HTTPoison.post!("#{@base_url}/v1/payments/#{payment_id}/refunds?access_token=#{opts[:config][:access_token]}", body, headers, []) - %HTTPoison.Response{body: body, status_code: status_code} = response - body = body |> Poison.decode! - format_response(body, status_code, opts) + commit(:post, url, body, headers) |> respond(opts) end - @doc """ - Stores the payment-source data for later use. - - > This usually enable "One Click" and/or "Recurring Payments" - - ## Note - - > If there's anything noteworthy about this operation, it comes here. - - ## Example - - > A barebones example using the bindings you've suggested in the `moduledoc`. - """ - @spec store(CreditCard.t(), keyword) :: {:ok | :error, Response} - def store(%CreditCard{} = card, opts) do - # commit(args, ...) - end - - @doc """ - Removes card or payment info that was previously `store/2`d - - Deletes previously stored payment-source data. - - ## Note - - > If there's anything noteworthy about this operation, it comes here. - - ## Example - - > A barebones example using the bindings you've suggested in the `moduledoc`. - """ - @spec unstore(String.t(), keyword) :: {:ok | :error, Response} - def unstore(registration_id, opts) do - # commit(args, ...) - end ############################################################################### # PRIVATE METHODS # @@ -294,27 +282,44 @@ defmodule Gringotts.Gateways.Mercadopago do # For consistency with other gateway implementations, make your (final) # network request in here, and parse it using another private method called # `respond`. - @spec commit(_) :: {:ok | :error, Response} - defp commit(_) do - # resp = HTTPoison.request(args, ...) - # respond(resp, ...) + #@spec commit(_, _, _, _) :: {:ok | :error, Response} + defp commit(method, path, body, headers) do + HTTPoison.request(method, path, body, headers, []) end # Parses mercadopago's response and returns a `Gringotts.Response` struct # in a `:ok`, `:error` tuple. - @spec respond(term) :: {:ok | :error, Response} - defp respond(mercadopago_response) - defp respond({:ok, %{status_code: 200, body: body}}), do: "something" - defp respond({:ok, %{status_code: status_code, body: body}}), do: "something" - defp respond({:error, %HTTPoison.Error{} = error}), do: "something" + defp auth_token(amount, %CreditCard{} = card, opts, customer_id, capture) do + opts = opts ++ [customer_id: customer_id] + {state, res} = get_token_id(card, opts) + if state == :error do + {state, res} + else + auth(amount, card, opts, res, capture) + end + end + + defp auth(amount, %CreditCard{} = card, opts, token_id, capture) do + {_, value, _, _} = Money.to_integer_exp(amount) + opts = opts ++ [token_id: token_id] + url = "#{@base_url}/v1/payments?access_token=#{opts[:config][:access_token]}" + body = get_authorize_body(value, card, opts, opts[:token_id], opts[:customer_id], capture) |> Poison.encode! + headers = [{"content-type", "application/json"}, {"accept", "application/json"}] + commit(:post, url, body, headers) + |> respond(opts) + end + defp get_customer_id(opts) do + url = "#{@base_url}/v1/customers?access_token=#{opts[:config][:access_token]}" body = %{"email": opts[:email]} |> Poison.encode! headers = [{"content-type", "application/json"}, {"accept", "application/json"}] - response = HTTPoison.post!("#{@base_url}/v1/customers?access_token=#{opts[:config][:access_token]}", body, headers, []) - %HTTPoison.Response{body: body} = response - body = body |> Poison.decode!() - body["id"] + {state, res} = commit(:post, url, body, headers) |> respond(opts) + if state == :error do + {state, res} + else + {state, res.id} + end end defp get_token_body(%CreditCard{} = card) do @@ -323,64 +328,71 @@ defmodule Gringotts.Gateways.Mercadopago do "expirationMonth": card.month, "cardNumber": card.number, "securityCode": card.verification_code, - "cardholder": %{ - "name": card.first_name <> card.last_name - } + "cardholder": %{"name": CreditCard.full_name(card)} } - end + end defp get_token_id(%CreditCard{} = card, opts) do + url = "#{@base_url}/v1/card_tokens/#{opts[:customer_id]}?public_key=#{opts[:config][:public_key]}" body = get_token_body(card) |> Poison.encode!() headers = [{"content-type", "application/json"}, {"accept", "application/json"}] - token = HTTPoison.post!("#{@base_url}/v1/card_tokens/#{opts[:customer_id]}?public_key=#{opts[:config][:public_key]}", body, headers, []) - %HTTPoison.Response{body: body} = token - body = body |> Poison.decode!() - body["id"] + {state, res} = commit(:post, url, body, headers) |> respond(opts) + if state == :error do + {state, res} + else + {state, res.id} + end end - defp get_authorize_body(value, %CreditCard{} = card, opts, token_id, customer_id) do + defp get_authorize_body(value, %CreditCard{} = card, opts, token_id, customer_id, capture) do %{ "payer": %{ "type": "customer", "id": customer_id, "first_name": card.first_name, "last_name": card.last_name - }, + }, "order": %{ "type": "mercadopago", "id": opts[:order_id] - }, + }, "installments": 1, "transaction_amount": value, - "payment_method_id": opts[:payment_method_id], #visa + "payment_method_id": opts[:payment_method_id], "token": token_id, - "capture": false - } + "capture": capture + } end defp get_success_body(body, status_code, opts) do %Response{ - success: true, - id: body["id"], - token: opts[:customer_id], - status_code: status_code, - message: body["status"] - } + success: true, + id: body["id"], + token: opts[:customer_id], + status_code: status_code, + message: body["status"] + } end + defp get_error_body(body, status_code, opts) do %Response{ - success: false, - token: opts[:customer_id], - status_code: status_code, - message: body["message"] + success: false, + token: opts[:customer_id], + status_code: status_code, + message: body["message"] } end - defp format_response(body, status_code, opts) do + defp respond({:ok, %HTTPoison.Response{body: body, status_code: status_code}}, opts) do + body = body |> Poison.decode! case body["cause"] do nil -> {:ok, get_success_body(body, status_code, opts)} _ -> {:error, get_error_body(body, status_code, opts)} end end -end + defp respond({:error, %HTTPoison.Error{id: _, reason: _}}, opts) do + {:error, "Network Error"} + end + +end \ No newline at end of file From 4a99128e411288b64b4a59f353095dfb63e0c478 Mon Sep 17 00:00:00 2001 From: siriusdark Date: Mon, 26 Mar 2018 21:10:42 +0530 Subject: [PATCH 44/60] Modification in authorize/3, and implemented capture 1. Used commit for HTTPoison requests. 2. Handled network and arguement errors. 3. Updated docs --- lib/gringotts/gateways/mercadopago.ex | 94 --------------------------- 1 file changed, 94 deletions(-) diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex index 0fb936a9..cb58b120 100644 --- a/lib/gringotts/gateways/mercadopago.ex +++ b/lib/gringotts/gateways/mercadopago.ex @@ -177,101 +177,7 @@ defmodule Gringotts.Gateways.Mercadopago do commit(:put, url, body, headers) |> respond(opts) end - @doc """ - Transfers `amount` from the customer to the merchant. - - mercadopago attempts to process a purchase on behalf of the customer, by - debiting `amount` from the customer's account by charging the customer's - `card`. - - ## Example - - The following example shows how one would process a payment worth 42 BRL in - one-shot, without (pre) authorization. - - iex> amount = Money.new(42, :BRL) - iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.Mercadopago, amount, card, opts) - iex> purchase_result.token # This is the customer ID/token - - """ - @spec purchase(Money.t, CreditCard.t(), keyword) :: {:ok | :error, Response} - def purchase(amount, %CreditCard{} = card, opts) do - if Keyword.has_key?(opts, :customer_id) do - auth_token(amount, card, opts, opts[:customer_id], true) - else - {state, res} = get_customer_id(opts) - if state == :error do - {state, res} - else - auth_token(amount, card, opts, res, true) - end - end - end - - @doc """ - Voids the referenced payment. - - This method attempts a reversal of a previous transaction referenced by - `payment_id`. - - > As a consequence, the customer will never see any booking on his statement. - - ## Note - - > Only pending or in_process payments can be cancelled. - - > Cancelled coupon payments, deposits and/or transfers will be deposited in the buyer’s Mercadopago account. - - ## Example - - The following example shows how one would void a previous (pre) - authorization. Remember that our `capture/3` example only did a partial - capture. - - iex> {:ok, void_result} = Gringotts.void(Gringotts.Gateways.Mercadopago, auth_result.id, opts) - """ - @spec void(String.t(), keyword) :: {:ok | :error, Response} - def void(payment_id, opts) do - url = "#{@base_url}/v1/payments/#{payment_id}?access_token=#{opts[:config][:access_token]}" - headers = [{"content-type", "application/json"}] - body = %{"status": "cancelled"} |> Poison.encode! - commit(:put, url, body, headers) |> respond(opts) - end - - @doc """ - Refunds the `amount` to the customer's account with reference to a prior transfer. - - mercadopago processes a full or partial refund worth `amount`, referencing a - previous `purchase/3` or `capture/3`. - - ## Note - - > You must have enough available money in your account so you can refund the payment amount successfully. Otherwise, you'll get a 400 Bad Request error. - - > You can refund a payment within 90 days after it was accredited. - - > You can only refund approved payments. - - > You can perform up to 20 partial refunds in one payment. - - ## Example - - The following example shows how one would (completely) refund a previous - purchase (and similarily for captures). - - iex> amount = Money.new(35, :BRL) - iex> {:ok, refund_result} = Gringotts.refund(Gringotts.Gateways.Mercadopago, purchase_result.id, amount) - """ - @spec refund(Money.t, String.t(), keyword) :: {:ok | :error, Response} - def refund(payment_id, amount, opts) do - {_, value, _, _} = Money.to_integer_exp(amount) - url = "#{@base_url}/v1/payments/#{payment_id}/refunds?access_token=#{opts[:config][:access_token]}" - body = %{"amount": value} |> Poison.encode! - headers = [{"content-type", "application/json"}] - commit(:post, url, body, headers) |> respond(opts) - end ############################################################################### From a6b42982c8346668bea9e8a4a2fbdff56dd5bf8e Mon Sep 17 00:00:00 2001 From: siriusdark Date: Mon, 26 Mar 2018 21:28:08 +0530 Subject: [PATCH 45/60] Modification in authorize/3, and implemented capture 1. Used commit for HTTPoison requests. 2. Handled network and arguement errors. 3. Updated docs --- lib/gringotts/gateways/mercadopago.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex index cb58b120..e9e6e231 100644 --- a/lib/gringotts/gateways/mercadopago.ex +++ b/lib/gringotts/gateways/mercadopago.ex @@ -180,6 +180,9 @@ defmodule Gringotts.Gateways.Mercadopago do + + + ############################################################################### # PRIVATE METHODS # ############################################################################### From 94f5b1c4b2a8cc7a50da7649c459f56d201093c5 Mon Sep 17 00:00:00 2001 From: siriusdark Date: Mon, 26 Mar 2018 21:32:07 +0530 Subject: [PATCH 46/60] Modification in authorize/3, and implemented capture 1. Used commit for HTTPoison requests. 2. Handled network and arguement errors. 3. Updated docs --- lib/gringotts/gateways/mercadopago.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex index e9e6e231..38b2267f 100644 --- a/lib/gringotts/gateways/mercadopago.ex +++ b/lib/gringotts/gateways/mercadopago.ex @@ -64,7 +64,7 @@ defmodule Gringotts.Gateways.Mercadopago do - To save you time, we recommend [cloning our example repo][example] that gives you a pre-configured sample app ready-to-go. + You could use the same config or update it the with your "secrets" - as described [above](#module-registering-your-monei-account-at-mercadopago). + as described [above](#module-registering-your-mercadopago-account-at-mercadopago). 2. Run an `iex` session with `iex -S mix` and add some variable bindings : ``` From 1d46184c6332734c1f92ea3a42c42fa9f16360ee Mon Sep 17 00:00:00 2001 From: siriusdark Date: Mon, 26 Mar 2018 21:38:51 +0530 Subject: [PATCH 47/60] Modification in authorize/3, and implemented capture 1. Used commit for HTTPoison requests. 2. Handled network and arguement errors. 3. Updated docs --- lib/gringotts/gateways/mercadopago.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex index 38b2267f..50c0170a 100644 --- a/lib/gringotts/gateways/mercadopago.ex +++ b/lib/gringotts/gateways/mercadopago.ex @@ -70,6 +70,7 @@ defmodule Gringotts.Gateways.Mercadopago do ``` iex> card = %CreditCard{first_name: "John", last_name: "Doe", number: "4509953566233704", year: 2099, month: 12, verification_code: "123", brand: "VISA"} ``` + We'll be using these in the examples below. From 5956d5b7edf9bd5fa9c18764875960b9f141e9cc Mon Sep 17 00:00:00 2001 From: siriusdark Date: Wed, 28 Mar 2018 00:02:30 +0530 Subject: [PATCH 48/60] Updated authorize/3 1. functions are renamed 2. if else replaced with case 3. docs updated --- lib/gringotts/gateways/mercadopago.ex | 94 ++++++++------------------- 1 file changed, 26 insertions(+), 68 deletions(-) diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex index 50c0170a..dddfe791 100644 --- a/lib/gringotts/gateways/mercadopago.ex +++ b/lib/gringotts/gateways/mercadopago.ex @@ -49,13 +49,11 @@ defmodule Gringotts.Gateways.Mercadopago do [credentials]: https://www.mercadopago.com/mlb/account/credentials?type=basic - * mercadopago does not process money in cents, and the `amount` is rounded to 2 - decimal places. - ## Supported currencies and countries - > mercadoapgo supports the currencies listed here : - :ARS, :BRL, :VEF, :CLP, :MXN, :COP, :PEN, :UYU + mercadopago supports the currencies listed [here][currencies]. + + [currencies]: https://api.mercadopago.com/currencies ## Following the examples @@ -64,7 +62,7 @@ defmodule Gringotts.Gateways.Mercadopago do - To save you time, we recommend [cloning our example repo][example] that gives you a pre-configured sample app ready-to-go. + You could use the same config or update it the with your "secrets" - as described [above](#module-registering-your-mercadopago-account-at-mercadopago). + as described [above](#module-registering-your-mercadopago-account-at-gringotts). 2. Run an `iex` session with `iex -S mix` and add some variable bindings : ``` @@ -99,30 +97,30 @@ defmodule Gringotts.Gateways.Mercadopago do The authorization validates the `card` details with the banking network, places a hold on the transaction `amount` in the customer’s issuing bank. - mercadoapgo's `authorize` returns a map containing authorization ID string which can be used to: + mercadoapgo's `authorize` returns authorization ID : * `capture/3` _an_ amount. * `void/2` a pre-authorization. ## Note - > For a new customer, `customer_id` field should be ignored. Otherwise it should be provided. + For a new customer, `customer_id` field should be ignored. Otherwise it should be provided. ## Example - ### Authorization using `email`, for new customer. (Ignore `customer_id`) + ### Authorization for new customer. (Ignore `customer_id`) The following example shows how one would (pre) authorize a payment of 42 BRL on a sample `card`. iex> amount = Money.new(42, :BRL) iex> card = %Gringotts.CreditCard{first_name: "Lord", last_name: "Voldemort", number: "4509953566233704", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> opts = [email: "tommarvolo@riddle.com", order_id: 123123, payment_method_id: "visa", config: %{access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"}] + iex> opts = [email: "tommarvolo@riddle.com", order_id: 123123, payment_method_id: "visa", config: %{access_token: YOUR_ACCESS_TOKEN, public_key: YOUR_PUBLIC_KEY}] iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Mercadopago, amount, card, opts) iex> auth_result.id # This is the authorization ID iex> auth_result.token # This is the customer ID/token - ### Authorization using `customer_id`, for old customer. (Mention `customer_id`) + ### Authorization for old customer. (Mention `customer_id`) The following example shows how one would (pre) authorize a payment of 42 BRL on a sample `card`. iex> amount = Money.new(42, :BRL) iex> card = %Gringotts.CreditCard{first_name: "Hermione", last_name: "Granger", number: "4509953566233704", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> opts = [email: "hermione@granger.com", order_id: 123125, customer_id: "308537342-HStv9cJCgK0dWU", payment_method_id: "visa", config: %{access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"}] + iex> opts = [email: "hermione@granger.com", order_id: 123125, customer_id: "308537342-HStv9cJCgK0dWU", payment_method_id: "visa", config: %{access_token: YOUR_ACCESS_TOKEN, public_key: YOUR_PUBLIC_KEY}] iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Mercadopago, amount, card, opts) iex> auth_result.id # This is the authorization ID iex> auth_result.token # This is the customer ID/token @@ -134,56 +132,16 @@ defmodule Gringotts.Gateways.Mercadopago do if Keyword.has_key?(opts, :customer_id) do auth_token(amount, card, opts, opts[:customer_id], false) else - {state, res} = get_customer_id(opts) - if state == :error do - {state, res} - else - auth_token(amount, card, opts, res, false) + case create_customer(opts) do + {:error, res} -> {:error, res} + {:ok, customer_id} -> auth_token(amount, card, opts, customer_id, false) end end end - @doc """ - Captures a pre-authorized `amount`. - - `amount` is transferred to the merchant account by mercadopago used in the - pre-authorization referenced by `payment_id`. - - ## Note - mercadopago allows partial captures also. However, you can make a partial capture to a payment only **once**. - - > The authorization will be valid for 7 days. If you do not capture it by that time, it will be cancelled. - - > The specified amount can not exceed the originally reserved. - - > If you do not specify the amount, all the reserved money is captured. - - > In Argentina only available for Visa and American Express cards. - - ## Example - - The following example shows how one would (partially) capture a previously - authorized a payment worth 35 BRL by referencing the obtained authorization `id`. - - iex> amount = Money.new(35, :BRL) - iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Mercadopago, auth_result.id, amount, opts) - """ - @spec capture(String.t(), Money.t, keyword) :: {:ok | :error, Response} - def capture(payment_id, amount, opts) do - {_, value, _, _} = Money.to_integer_exp(amount) - url = "#{@base_url}/v1/payments/#{payment_id}?access_token=#{opts[:config][:access_token]}" - body = %{"capture": true, "transaction_amount": value} |> Poison.encode! - headers = [{"content-type", "application/json"}, {"accept", "application/json"}] - commit(:put, url, body, headers) |> respond(opts) - end - - - - - ############################################################################### # PRIVATE METHODS # ############################################################################### @@ -202,7 +160,7 @@ defmodule Gringotts.Gateways.Mercadopago do defp auth_token(amount, %CreditCard{} = card, opts, customer_id, capture) do opts = opts ++ [customer_id: customer_id] - {state, res} = get_token_id(card, opts) + {state, res} = create_token(card, opts) if state == :error do {state, res} else @@ -214,13 +172,13 @@ defmodule Gringotts.Gateways.Mercadopago do {_, value, _, _} = Money.to_integer_exp(amount) opts = opts ++ [token_id: token_id] url = "#{@base_url}/v1/payments?access_token=#{opts[:config][:access_token]}" - body = get_authorize_body(value, card, opts, opts[:token_id], opts[:customer_id], capture) |> Poison.encode! + body = authorize_params(value, card, opts, opts[:token_id], opts[:customer_id], capture) |> Poison.encode! headers = [{"content-type", "application/json"}, {"accept", "application/json"}] commit(:post, url, body, headers) |> respond(opts) end - defp get_customer_id(opts) do + defp create_customer(opts) do url = "#{@base_url}/v1/customers?access_token=#{opts[:config][:access_token]}" body = %{"email": opts[:email]} |> Poison.encode! headers = [{"content-type", "application/json"}, {"accept", "application/json"}] @@ -232,7 +190,7 @@ defmodule Gringotts.Gateways.Mercadopago do end end - defp get_token_body(%CreditCard{} = card) do + defp token_params(%CreditCard{} = card) do %{ "expirationYear": card.year, "expirationMonth": card.month, @@ -242,19 +200,18 @@ defmodule Gringotts.Gateways.Mercadopago do } end - defp get_token_id(%CreditCard{} = card, opts) do + defp create_token(%CreditCard{} = card, opts) do url = "#{@base_url}/v1/card_tokens/#{opts[:customer_id]}?public_key=#{opts[:config][:public_key]}" - body = get_token_body(card) |> Poison.encode!() + body = token_params(card) |> Poison.encode!() headers = [{"content-type", "application/json"}, {"accept", "application/json"}] {state, res} = commit(:post, url, body, headers) |> respond(opts) - if state == :error do - {state, res} - else - {state, res.id} + case state do + :error -> {state, res} + _ -> {state, res.id} end end - defp get_authorize_body(value, %CreditCard{} = card, opts, token_id, customer_id, capture) do + defp authorize_params(value, %CreditCard{} = card, opts, token_id, customer_id, capture) do %{ "payer": %{ "type": "customer", @@ -266,7 +223,7 @@ defmodule Gringotts.Gateways.Mercadopago do "type": "mercadopago", "id": opts[:order_id] }, - "installments": 1, + "installments": opts[:installments], "transaction_amount": value, "payment_method_id": opts[:payment_method_id], "token": token_id, @@ -305,4 +262,5 @@ defmodule Gringotts.Gateways.Mercadopago do {:error, "Network Error"} end -end \ No newline at end of file +end + From fd135db986c13df96eac272d00aac86c6e443b1e Mon Sep 17 00:00:00 2001 From: siriusdark Date: Wed, 28 Mar 2018 00:32:21 +0530 Subject: [PATCH 49/60] Updated authorize/3 1. changed if else to case 2. changed function name 3. updated docs --- lib/gringotts/gateways/mercadopago.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex index dddfe791..3f64a3e5 100644 --- a/lib/gringotts/gateways/mercadopago.ex +++ b/lib/gringotts/gateways/mercadopago.ex @@ -231,7 +231,7 @@ defmodule Gringotts.Gateways.Mercadopago do } end - defp get_success_body(body, status_code, opts) do + defp success_body(body, status_code, opts) do %Response{ success: true, id: body["id"], @@ -241,7 +241,7 @@ defmodule Gringotts.Gateways.Mercadopago do } end - defp get_error_body(body, status_code, opts) do + defp error_body(body, status_code, opts) do %Response{ success: false, token: opts[:customer_id], @@ -253,8 +253,8 @@ defmodule Gringotts.Gateways.Mercadopago do defp respond({:ok, %HTTPoison.Response{body: body, status_code: status_code}}, opts) do body = body |> Poison.decode! case body["cause"] do - nil -> {:ok, get_success_body(body, status_code, opts)} - _ -> {:error, get_error_body(body, status_code, opts)} + nil -> {:ok, success_body(body, status_code, opts)} + _ -> {:error, error_body(body, status_code, opts)} end end From e7349da379cd6da1b9d5da20d3c99e69d88c5257 Mon Sep 17 00:00:00 2001 From: siriusdark Date: Thu, 29 Mar 2018 03:06:19 +0530 Subject: [PATCH 50/60] Updated authorize/3 1. Added default value of `installments` in function `authorize_params/6` as 1. 2. Added `headers` and `respond`function call in commit function itself. 3. Used `reason` in `respond` function, in case of network error. 4. Removed block quotes '>'. 5. Added money processing note. --- lib/gringotts/gateways/mercadopago.ex | 47 +++++++++++++++------------ 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex index 3f64a3e5..1b2bf198 100644 --- a/lib/gringotts/gateways/mercadopago.ex +++ b/lib/gringotts/gateways/mercadopago.ex @@ -49,6 +49,10 @@ defmodule Gringotts.Gateways.Mercadopago do [credentials]: https://www.mercadopago.com/mlb/account/credentials?type=basic + ## Note + + mercadopago processes money with upto two decimal places. + ## Supported currencies and countries mercadopago supports the currencies listed [here][currencies]. @@ -68,7 +72,6 @@ defmodule Gringotts.Gateways.Mercadopago do ``` iex> card = %CreditCard{first_name: "John", last_name: "Doe", number: "4509953566233704", year: 2099, month: 12, verification_code: "123", brand: "VISA"} ``` - We'll be using these in the examples below. @@ -97,7 +100,7 @@ defmodule Gringotts.Gateways.Mercadopago do The authorization validates the `card` details with the banking network, places a hold on the transaction `amount` in the customer’s issuing bank. - mercadoapgo's `authorize` returns authorization ID : + mercadoapgo's `authorize` returns authorization ID(available in the `Response.id` field) : * `capture/3` _an_ amount. * `void/2` a pre-authorization. @@ -107,8 +110,9 @@ defmodule Gringotts.Gateways.Mercadopago do ## Example - ### Authorization for new customer. (Ignore `customer_id`) + ### Authorization for new customer. The following example shows how one would (pre) authorize a payment of 42 BRL on a sample `card`. + Ignore `customer_id`. iex> amount = Money.new(42, :BRL) iex> card = %Gringotts.CreditCard{first_name: "Lord", last_name: "Voldemort", number: "4509953566233704", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> opts = [email: "tommarvolo@riddle.com", order_id: 123123, payment_method_id: "visa", config: %{access_token: YOUR_ACCESS_TOKEN, public_key: YOUR_PUBLIC_KEY}] @@ -116,8 +120,9 @@ defmodule Gringotts.Gateways.Mercadopago do iex> auth_result.id # This is the authorization ID iex> auth_result.token # This is the customer ID/token - ### Authorization for old customer. (Mention `customer_id`) + ### Authorization for old customer. The following example shows how one would (pre) authorize a payment of 42 BRL on a sample `card`. + Mention `customer_id`. iex> amount = Money.new(42, :BRL) iex> card = %Gringotts.CreditCard{first_name: "Hermione", last_name: "Granger", number: "4509953566233704", year: 2099, month: 12, verification_code: "123", brand: "VISA"} iex> opts = [email: "hermione@granger.com", order_id: 123125, customer_id: "308537342-HStv9cJCgK0dWU", payment_method_id: "visa", config: %{access_token: YOUR_ACCESS_TOKEN, public_key: YOUR_PUBLIC_KEY}] @@ -138,9 +143,6 @@ defmodule Gringotts.Gateways.Mercadopago do end end end - - - ############################################################################### # PRIVATE METHODS # @@ -151,8 +153,9 @@ defmodule Gringotts.Gateways.Mercadopago do # network request in here, and parse it using another private method called # `respond`. #@spec commit(_, _, _, _) :: {:ok | :error, Response} - defp commit(method, path, body, headers) do - HTTPoison.request(method, path, body, headers, []) + defp commit(method, path, body, opts) do + headers = [{"content-type", "application/json"}, {"accept", "application/json"}] + HTTPoison.request(method, path, body, headers, []) |> respond(opts) end # Parses mercadopago's response and returns a `Gringotts.Response` struct @@ -173,16 +176,13 @@ defmodule Gringotts.Gateways.Mercadopago do opts = opts ++ [token_id: token_id] url = "#{@base_url}/v1/payments?access_token=#{opts[:config][:access_token]}" body = authorize_params(value, card, opts, opts[:token_id], opts[:customer_id], capture) |> Poison.encode! - headers = [{"content-type", "application/json"}, {"accept", "application/json"}] - commit(:post, url, body, headers) - |> respond(opts) + commit(:post, url, body, opts) end defp create_customer(opts) do url = "#{@base_url}/v1/customers?access_token=#{opts[:config][:access_token]}" body = %{"email": opts[:email]} |> Poison.encode! - headers = [{"content-type", "application/json"}, {"accept", "application/json"}] - {state, res} = commit(:post, url, body, headers) |> respond(opts) + {state, res} = commit(:post, url, body, opts) if state == :error do {state, res} else @@ -202,9 +202,8 @@ defmodule Gringotts.Gateways.Mercadopago do defp create_token(%CreditCard{} = card, opts) do url = "#{@base_url}/v1/card_tokens/#{opts[:customer_id]}?public_key=#{opts[:config][:public_key]}" - body = token_params(card) |> Poison.encode!() - headers = [{"content-type", "application/json"}, {"accept", "application/json"}] - {state, res} = commit(:post, url, body, headers) |> respond(opts) + body = token_params(card) |> Poison.encode! + {state, res} = commit(:post, url, body, opts) case state do :error -> {state, res} _ -> {state, res.id} @@ -223,9 +222,9 @@ defmodule Gringotts.Gateways.Mercadopago do "type": "mercadopago", "id": opts[:order_id] }, - "installments": opts[:installments], + "installments": opts[:installments] || 1, "transaction_amount": value, - "payment_method_id": opts[:payment_method_id], + "payment_method_id": String.downcase(card.brand), "token": token_id, "capture": capture } @@ -258,8 +257,14 @@ defmodule Gringotts.Gateways.Mercadopago do end end - defp respond({:error, %HTTPoison.Error{id: _, reason: _}}, opts) do - {:error, "Network Error"} + defp respond({:error, %HTTPoison.Error{} = error}, opts) do + { + :error, + Response.error( + reason: "network related failure", + message: "HTTPoison says '#{error.reason}' [ID: #{error.id || "nil"}]" + ) + } end end From 5eabe3438bec35d55ad0e743dc0c297a2b35a024 Mon Sep 17 00:00:00 2001 From: siriusdark Date: Sat, 31 Mar 2018 14:01:41 +0530 Subject: [PATCH 51/60] Updated authorize/3 1. Removed unnecessary `opts` from arguements of error `respond` function. 2. Used fifth arguement in `HTTPoison.Request` to send part of url as keyword list. 3. Added @spec for `commit` function. --- lib/gringotts/gateways/mercadopago.ex | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex index 1b2bf198..aa55c50a 100644 --- a/lib/gringotts/gateways/mercadopago.ex +++ b/lib/gringotts/gateways/mercadopago.ex @@ -152,10 +152,10 @@ defmodule Gringotts.Gateways.Mercadopago do # For consistency with other gateway implementations, make your (final) # network request in here, and parse it using another private method called # `respond`. - #@spec commit(_, _, _, _) :: {:ok | :error, Response} - defp commit(method, path, body, opts) do + @spec commit(atom, String.t(), String.t(), keyword, keyword) :: {:ok | :error, Response.t()} + defp commit(method, path, body, opts, url_params) do headers = [{"content-type", "application/json"}, {"accept", "application/json"}] - HTTPoison.request(method, path, body, headers, []) |> respond(opts) + HTTPoison.request(method, "#{@base_url}#{path}", body, headers, url_params) |> respond(opts) end # Parses mercadopago's response and returns a `Gringotts.Response` struct @@ -174,15 +174,16 @@ defmodule Gringotts.Gateways.Mercadopago do defp auth(amount, %CreditCard{} = card, opts, token_id, capture) do {_, value, _, _} = Money.to_integer_exp(amount) opts = opts ++ [token_id: token_id] - url = "#{@base_url}/v1/payments?access_token=#{opts[:config][:access_token]}" + url_params = [access_token: opts[:config][:access_token]] body = authorize_params(value, card, opts, opts[:token_id], opts[:customer_id], capture) |> Poison.encode! - commit(:post, url, body, opts) + commit(:post, "/v1/payments", body, opts, params: url_params ) end defp create_customer(opts) do - url = "#{@base_url}/v1/customers?access_token=#{opts[:config][:access_token]}" + url_params = [access_token: opts[:config][:access_token]] body = %{"email": opts[:email]} |> Poison.encode! - {state, res} = commit(:post, url, body, opts) + + {state, res} = commit(:post, "/v1/customers", body, opts, params: url_params) if state == :error do {state, res} else @@ -201,9 +202,9 @@ defmodule Gringotts.Gateways.Mercadopago do end defp create_token(%CreditCard{} = card, opts) do - url = "#{@base_url}/v1/card_tokens/#{opts[:customer_id]}?public_key=#{opts[:config][:public_key]}" + url_params = [public_key: opts[:config][:public_key]] body = token_params(card) |> Poison.encode! - {state, res} = commit(:post, url, body, opts) + {state, res} = commit(:post, "/v1/card_tokens/#{opts[:customer_id]}", body, opts, params: url_params) case state do :error -> {state, res} _ -> {state, res.id} @@ -257,7 +258,7 @@ defmodule Gringotts.Gateways.Mercadopago do end end - defp respond({:error, %HTTPoison.Error{} = error}, opts) do + defp respond({:error, %HTTPoison.Error{} = error}, _) do { :error, Response.error( From b51e7f16de9371f3802d833458eddf4cd69fd0ab Mon Sep 17 00:00:00 2001 From: siriusdark Date: Sat, 31 Mar 2018 20:58:59 +0530 Subject: [PATCH 52/60] Authorize integration test cases created --- .../integration/gateways/mercadopago_test.exs | 137 +++++++++++++++--- 1 file changed, 118 insertions(+), 19 deletions(-) diff --git a/test/integration/gateways/mercadopago_test.exs b/test/integration/gateways/mercadopago_test.exs index 8d60dd1a..7cb658a9 100644 --- a/test/integration/gateways/mercadopago_test.exs +++ b/test/integration/gateways/mercadopago_test.exs @@ -2,35 +2,134 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do # Integration tests for the Mercadopago use ExUnit.Case, async: false - alias Gringotts.Gateways.Mercadopago + use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney + alias Gringotts.Gateways.Mercadopago, as: Gateway @moduletag :integration - setup_all do - Application.put_env(:gringotts, Gringotts.Gateways.Mercadopago, - [ - public_key: "your_secret_public_key", - access_token: "your_secret_access_token" - ] - ) - end + @amount Money.new(45, :BRL) + @sub_amount Money.new(30, :BRL) + @good_card %Gringotts.CreditCard{ + first_name: "Hermoine", + last_name: "Granger", + number: "4509953566233704", + year: 2030, + month: 07, + verification_code: "123", + brand: "VISA" + } + + @bad_card %Gringotts.CreditCard{ + first_name: "Hermoine", + last_name: "Granger", + number: "4509953566233704", + year: 2000, + month: 07, + verification_code: "123", + brand: "VISA" + } + + @good_opts [email: "hermoine@granger.com", + order_id: 123126, + customer_id: "311211654-YrXF6J0QikpIWX", + config: [access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", + public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"], + installments: 1 + ] + @new_cutomer_good_opts [order_id: 123126, + config: [access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", + public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"], + installments: 1 + ] + @new_cutomer_bad_opts [order_id: 123126, + config: [public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"], + installments: 1 + ] + @bad_opts [email: "hermoine@granger.com", + order_id: 123126, + customer_id: "311211654-YrXF6J0QikpIWX", + config: [public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"], + installments: 1 + ] + # Group the test cases by public api - describe "purchase" do - end - describe "authorize" do + def new_email_opts(good) do + no1 = :rand.uniform(1_000_00) |> to_string + no2 = :rand.uniform(1_000_00) |> to_string + no3 = :rand.uniform(1_000_00) |> to_string + email = "hp" <> no1 <> no2 <> no3 <> "@potter.com" + case good do + true -> @new_cutomer_good_opts ++ [email: email] + _ -> @new_cutomer_bad_opts ++ [email: email] + end end - describe "capture" do - end + describe "[authorize]" do + test "old customer with good_opts and good_card" do + use_cassette "mercadopago/authorize_old customer with good_opts and good_card" do + assert {:ok, response} = Gateway.authorize(@amount, @good_card, @good_opts) + assert response.success == true + assert response.status_code == 201 + end + end + test "old customer with good_opts and bad_card" do + use_cassette "mercadopago/authorize_old customer with good_opts and bad_card" do + assert {:error, response} = Gateway.authorize(@amount, @bad_card, @good_opts) + assert response.success == false + assert response.status_code == 400 + end + end - describe "void" do - end + test "old customer with bad_opts and good_card" do + use_cassette "mercadopago/authorize_old customer with bad_opts and good_card" do + assert {:error, response} = Gateway.authorize(@amount, @good_card, @bad_opts) + assert response.success == false + assert response.status_code == 401 + end + end + test "old customer with bad_opts and bad_opts" do + use_cassette "mercadopago/authorize_old customer with bad_opts and bad_opts" do + assert {:error, response} = Gateway.authorize(@amount, @bad_card, @bad_opts) + assert response.success == false + assert response.status_code == 400 + end + end - describe "refund" do - end + test "new cutomer with good_opts and good_card" do + use_cassette "mercadopago/authorize_new cutomer with good_opts and good_card" do + opts = new_email_opts(true) + assert {:ok, response} = Gateway.authorize(@amount, @good_card, opts) + assert response.success == true + assert response.status_code == 201 + end + end + + test "new customer with good_opts and bad_card" do + use_cassette "mercadopago/authorize_new customer with good_opts and bad_card" do + opts = new_email_opts(true) + assert {:error, response} = Gateway.authorize(@amount, @bad_card, opts) + assert response.success == false + assert response.status_code == 400 + end + end - describe "environment setup" do + test "new customer with bad_opts and good_card" do + use_cassette "mercadopago/authorize_new customer with bad_opts and good_card" do + opts = new_email_opts(false) + assert {:error, response} = Gateway.authorize(@amount, @good_card, opts) + assert response.success == false + assert response.status_code == 401 + end + end + test "new customer with bad_opts and bad_card" do + use_cassette "mercadopago/authorize_new customer with bad_opts and bad_card" do + opts = new_email_opts(false) + assert {:error, response} = Gateway.authorize(@amount, @bad_card, opts) + assert response.success == false + assert response.status_code == 401 + end + end end end From d0277b29c1cb931c18067b709c30e104d82937cd Mon Sep 17 00:00:00 2001 From: siriusdark Date: Sat, 31 Mar 2018 21:10:55 +0530 Subject: [PATCH 53/60] Authorize integration test cases created --- test/integration/gateways/mercadopago_test.exs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/integration/gateways/mercadopago_test.exs b/test/integration/gateways/mercadopago_test.exs index 7cb658a9..f33a48c5 100644 --- a/test/integration/gateways/mercadopago_test.exs +++ b/test/integration/gateways/mercadopago_test.exs @@ -67,13 +67,14 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do end describe "[authorize]" do - test "old customer with good_opts and good_card" do - use_cassette "mercadopago/authorize_old customer with good_opts and good_card" do - assert {:ok, response} = Gateway.authorize(@amount, @good_card, @good_opts) - assert response.success == true - assert response.status_code == 201 - end + test "old customer with good_opts and good_card" do + use_cassette "mercadopago/authorize_old customer with good_opts and good_card" do + assert {:ok, response} = Gateway.authorize(@amount, @good_card, @good_opts) + assert response.success == true + assert response.status_code == 201 end + end + test "old customer with good_opts and bad_card" do use_cassette "mercadopago/authorize_old customer with good_opts and bad_card" do assert {:error, response} = Gateway.authorize(@amount, @bad_card, @good_opts) @@ -89,6 +90,7 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do assert response.status_code == 401 end end + test "old customer with bad_opts and bad_opts" do use_cassette "mercadopago/authorize_old customer with bad_opts and bad_opts" do assert {:error, response} = Gateway.authorize(@amount, @bad_card, @bad_opts) @@ -123,6 +125,7 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do assert response.status_code == 401 end end + test "new customer with bad_opts and bad_card" do use_cassette "mercadopago/authorize_new customer with bad_opts and bad_card" do opts = new_email_opts(false) @@ -132,4 +135,6 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do end end end + end + From 0e159abc93fb5739a80df0ae3118c1648c9c7164 Mon Sep 17 00:00:00 2001 From: siriusdark Date: Sat, 31 Mar 2018 21:28:17 +0530 Subject: [PATCH 54/60] Authorize integration test cases created --- mix.exs | 1 + mix.lock | 2 ++ test/integration/gateways/mercadopago_test.exs | 3 --- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mix.exs b/mix.exs index 244a6405..c3074816 100644 --- a/mix.exs +++ b/mix.exs @@ -62,6 +62,7 @@ defmodule Gringotts.Mixfile do {:mock, "~> 0.3.0", only: :test}, {:bypass, "~> 0.8", only: :test}, {:excoveralls, "~> 0.8", only: :test}, + {:exvcr, "~> 0.10", only: :test}, # various analyses tools {:credo, "~> 0.3", only: [:dev, :test]}, diff --git a/mix.lock b/mix.lock index aaaa5cc1..83a82360 100644 --- a/mix.lock +++ b/mix.lock @@ -16,8 +16,10 @@ "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "1.3.1", "50a117654dff8f8ee6958e68a65d0c2835a7e2f1aff94c1ea8f582c04fdf0bd4", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 1.4.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.1 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, "ex_money": {:hex, :ex_money, "1.1.3", "843eed0a5673206de33be47cdc06574401abc3e2d33cbcf6d74e160226791ae4", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}, {:ex_cldr, "~> 1.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 1.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}], "hexpm"}, + "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm"}, "excoveralls": {:hex, :excoveralls, "0.8.1", "0bbf67f22c7dbf7503981d21a5eef5db8bbc3cb86e70d3798e8c802c74fa5e27", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, + "exvcr": {:hex, :exvcr, "0.10.0", "5150808404d9f48dbda636f70f7f8fefd93e2433cd39f695f810e73b3a9d1736", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 0.13", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.0", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"}, "hackney": {:hex, :hackney, "1.11.0", "4951ee019df102492dabba66a09e305f61919a8a183a7860236c0fde586134b6", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "httpoison": {:hex, :httpoison, "0.13.0", "bfaf44d9f133a6599886720f3937a7699466d23bb0cd7a88b6ba011f53c6f562", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/test/integration/gateways/mercadopago_test.exs b/test/integration/gateways/mercadopago_test.exs index f33a48c5..0a1cdbd3 100644 --- a/test/integration/gateways/mercadopago_test.exs +++ b/test/integration/gateways/mercadopago_test.exs @@ -52,9 +52,6 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do installments: 1 ] - - # Group the test cases by public api - def new_email_opts(good) do no1 = :rand.uniform(1_000_00) |> to_string no2 = :rand.uniform(1_000_00) |> to_string From d0bda62c3acc59ee3469382211c2a3bd2c2223be Mon Sep 17 00:00:00 2001 From: siriusdark Date: Tue, 17 Apr 2018 23:23:45 +0530 Subject: [PATCH 55/60] Applied elixer formatter. --- lib/gringotts/gateways/mercadopago.ex | 80 +++++++++------- .../integration/gateways/mercadopago_test.exs | 95 ++++++++++--------- 2 files changed, 95 insertions(+), 80 deletions(-) diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex index aa55c50a..6aa7804f 100644 --- a/lib/gringotts/gateways/mercadopago.ex +++ b/lib/gringotts/gateways/mercadopago.ex @@ -39,7 +39,7 @@ defmodule Gringotts.Gateways.Mercadopago do | ------- | ---- | | `:access_token` | **Access Token** | | `:public_key` | **Public Key** | - + > Your Application config **must include the `[:public_key, :access_token]` field(s)** and would look > something like this: > @@ -89,7 +89,7 @@ defmodule Gringotts.Gateways.Mercadopago do # Add the keys that must be present in the Application config in the # `required_config` list use Gringotts.Adapter, required_config: [:public_key, :access_token] - + import Poison, only: [decode: 1] alias Gringotts.{CreditCard, Response} @@ -133,13 +133,13 @@ defmodule Gringotts.Gateways.Mercadopago do """ @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} - def authorize(amount , %CreditCard{} = card, opts) do + def authorize(amount, %CreditCard{} = card, opts) do if Keyword.has_key?(opts, :customer_id) do auth_token(amount, card, opts, opts[:customer_id], false) else case create_customer(opts) do {:error, res} -> {:error, res} - {:ok, customer_id} -> auth_token(amount, card, opts, customer_id, false) + {:ok, customer_id} -> auth_token(amount, card, opts, customer_id, false) end end end @@ -147,7 +147,7 @@ defmodule Gringotts.Gateways.Mercadopago do ############################################################################### # PRIVATE METHODS # ############################################################################### - + # Makes the request to mercadopago's network. # For consistency with other gateway implementations, make your (final) # network request in here, and parse it using another private method called @@ -164,6 +164,7 @@ defmodule Gringotts.Gateways.Mercadopago do defp auth_token(amount, %CreditCard{} = card, opts, customer_id, capture) do opts = opts ++ [customer_id: customer_id] {state, res} = create_token(card, opts) + if state == :error do {state, res} else @@ -175,15 +176,20 @@ defmodule Gringotts.Gateways.Mercadopago do {_, value, _, _} = Money.to_integer_exp(amount) opts = opts ++ [token_id: token_id] url_params = [access_token: opts[:config][:access_token]] - body = authorize_params(value, card, opts, opts[:token_id], opts[:customer_id], capture) |> Poison.encode! - commit(:post, "/v1/payments", body, opts, params: url_params ) + + body = + authorize_params(value, card, opts, opts[:token_id], opts[:customer_id], capture) + |> Poison.encode!() + + commit(:post, "/v1/payments", body, opts, params: url_params) end - + defp create_customer(opts) do url_params = [access_token: opts[:config][:access_token]] - body = %{"email": opts[:email]} |> Poison.encode! + body = %{email: opts[:email]} |> Poison.encode!() {state, res} = commit(:post, "/v1/customers", body, opts, params: url_params) + if state == :error do {state, res} else @@ -193,41 +199,44 @@ defmodule Gringotts.Gateways.Mercadopago do defp token_params(%CreditCard{} = card) do %{ - "expirationYear": card.year, - "expirationMonth": card.month, - "cardNumber": card.number, - "securityCode": card.verification_code, - "cardholder": %{"name": CreditCard.full_name(card)} + expirationYear: card.year, + expirationMonth: card.month, + cardNumber: card.number, + securityCode: card.verification_code, + cardholder: %{name: CreditCard.full_name(card)} } end defp create_token(%CreditCard{} = card, opts) do url_params = [public_key: opts[:config][:public_key]] - body = token_params(card) |> Poison.encode! - {state, res} = commit(:post, "/v1/card_tokens/#{opts[:customer_id]}", body, opts, params: url_params) + body = token_params(card) |> Poison.encode!() + + {state, res} = + commit(:post, "/v1/card_tokens/#{opts[:customer_id]}", body, opts, params: url_params) + case state do - :error -> {state, res} - _ -> {state, res.id} + :error -> {state, res} + _ -> {state, res.id} end end defp authorize_params(value, %CreditCard{} = card, opts, token_id, customer_id, capture) do %{ - "payer": %{ - "type": "customer", - "id": customer_id, - "first_name": card.first_name, - "last_name": card.last_name - }, - "order": %{ - "type": "mercadopago", - "id": opts[:order_id] - }, - "installments": opts[:installments] || 1, - "transaction_amount": value, - "payment_method_id": String.downcase(card.brand), - "token": token_id, - "capture": capture + payer: %{ + type: "customer", + id: customer_id, + first_name: card.first_name, + last_name: card.last_name + }, + order: %{ + type: "mercadopago", + id: opts[:order_id] + }, + installments: opts[:installments] || 1, + transaction_amount: value, + payment_method_id: String.downcase(card.brand), + token: token_id, + capture: capture } end @@ -251,7 +260,8 @@ defmodule Gringotts.Gateways.Mercadopago do end defp respond({:ok, %HTTPoison.Response{body: body, status_code: status_code}}, opts) do - body = body |> Poison.decode! + body = body |> Poison.decode!() + case body["cause"] do nil -> {:ok, success_body(body, status_code, opts)} _ -> {:error, error_body(body, status_code, opts)} @@ -267,6 +277,4 @@ defmodule Gringotts.Gateways.Mercadopago do ) } end - end - diff --git a/test/integration/gateways/mercadopago_test.exs b/test/integration/gateways/mercadopago_test.exs index 0a1cdbd3..fbfa5bd7 100644 --- a/test/integration/gateways/mercadopago_test.exs +++ b/test/integration/gateways/mercadopago_test.exs @@ -5,61 +5,70 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney alias Gringotts.Gateways.Mercadopago, as: Gateway - @moduletag :integration + @moduletag integration: true @amount Money.new(45, :BRL) @sub_amount Money.new(30, :BRL) @good_card %Gringotts.CreditCard{ - first_name: "Hermoine", - last_name: "Granger", - number: "4509953566233704", - year: 2030, - month: 07, - verification_code: "123", - brand: "VISA" - } + first_name: "Hermoine", + last_name: "Granger", + number: "4509953566233704", + year: 2030, + month: 07, + verification_code: "123", + brand: "VISA" + } @bad_card %Gringotts.CreditCard{ - first_name: "Hermoine", - last_name: "Granger", - number: "4509953566233704", - year: 2000, - month: 07, - verification_code: "123", - brand: "VISA" - } - - @good_opts [email: "hermoine@granger.com", - order_id: 123126, - customer_id: "311211654-YrXF6J0QikpIWX", - config: [access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", - public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"], - installments: 1 - ] - @new_cutomer_good_opts [order_id: 123126, - config: [access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", - public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"], - installments: 1 - ] - @new_cutomer_bad_opts [order_id: 123126, - config: [public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"], - installments: 1 - ] - @bad_opts [email: "hermoine@granger.com", - order_id: 123126, - customer_id: "311211654-YrXF6J0QikpIWX", - config: [public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"], - installments: 1 - ] + first_name: "Hermoine", + last_name: "Granger", + number: "4509953566233704", + year: 2000, + month: 07, + verification_code: "123", + brand: "VISA" + } + + @good_opts [ + email: "hermoine@granger.com", + order_id: 123_126, + customer_id: "311211654-YrXF6J0QikpIWX", + config: [ + access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", + public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a" + ], + installments: 1 + ] + @new_cutomer_good_opts [ + order_id: 123_126, + config: [ + access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", + public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a" + ], + installments: 1 + ] + @new_cutomer_bad_opts [ + order_id: 123_126, + config: [public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"], + installments: 1 + ] + @bad_opts [ + email: "hermoine@granger.com", + order_id: 123_126, + customer_id: "311211654-YrXF6J0QikpIWX", + config: [public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"], + installments: 1 + ] def new_email_opts(good) do no1 = :rand.uniform(1_000_00) |> to_string no2 = :rand.uniform(1_000_00) |> to_string no3 = :rand.uniform(1_000_00) |> to_string email = "hp" <> no1 <> no2 <> no3 <> "@potter.com" + case good do - true -> @new_cutomer_good_opts ++ [email: email] - _ -> @new_cutomer_bad_opts ++ [email: email] + true -> @new_cutomer_good_opts ++ [email: email] + _ -> @new_cutomer_bad_opts ++ [email: email] end end @@ -132,6 +141,4 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do end end end - end - From 1357aa8ea4f536c752674b1d1c0e774e71c24a4c Mon Sep 17 00:00:00 2001 From: siriusdark Date: Wed, 18 Apr 2018 01:58:01 +0530 Subject: [PATCH 56/60] Capture and test cases 1. Implemented capture 2. Added test cases for capture --- lib/gringotts/gateways/mercadopago.ex | 33 +++++ .../integration/gateways/mercadopago_test.exs | 114 +++--------------- 2 files changed, 49 insertions(+), 98 deletions(-) diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex index 6aa7804f..df4189ef 100644 --- a/lib/gringotts/gateways/mercadopago.ex +++ b/lib/gringotts/gateways/mercadopago.ex @@ -144,6 +144,39 @@ defmodule Gringotts.Gateways.Mercadopago do end end + @doc """ + Captures a pre-authorized `amount`. + + `amount` is transferred to the merchant account by mercadopago used in the + pre-authorization referenced by `payment_id`. + + ## Note + mercadopago allows partial captures also. However, you can make a partial capture to a payment only **once**. + + > The authorization will be valid for 7 days. If you do not capture it by that time, it will be cancelled. + + > The specified amount can not exceed the originally reserved. + + > If you do not specify the amount, all the reserved money is captured. + + > In Argentina only available for Visa and American Express cards. + + ## Example + + The following example shows how one would (partially) capture a previously + authorized a payment worth 35 BRL by referencing the obtained authorization `id`. + + iex> amount = Money.new(35, :BRL) + iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Mercadopago, auth_result.id, amount, opts) + """ + @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} + def capture(payment_id, amount, opts) do + {_, value, _, _} = Money.to_integer_exp(amount) + url_params = [access_token: opts[:config][:access_token]] + body = %{capture: true, transaction_amount: value} |> Poison.encode!() + commit(:put, "/v1/payments/#{payment_id}", body, opts, params: url_params) + end + ############################################################################### # PRIVATE METHODS # ############################################################################### diff --git a/test/integration/gateways/mercadopago_test.exs b/test/integration/gateways/mercadopago_test.exs index fbfa5bd7..afff1e87 100644 --- a/test/integration/gateways/mercadopago_test.exs +++ b/test/integration/gateways/mercadopago_test.exs @@ -19,16 +19,6 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do brand: "VISA" } - @bad_card %Gringotts.CreditCard{ - first_name: "Hermoine", - last_name: "Granger", - number: "4509953566233704", - year: 2000, - month: 07, - verification_code: "123", - brand: "VISA" - } - @good_opts [ email: "hermoine@granger.com", order_id: 123_126, @@ -39,106 +29,34 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do ], installments: 1 ] - @new_cutomer_good_opts [ - order_id: 123_126, - config: [ - access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", - public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a" - ], - installments: 1 - ] - @new_cutomer_bad_opts [ - order_id: 123_126, - config: [public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"], - installments: 1 - ] - @bad_opts [ - email: "hermoine@granger.com", - order_id: 123_126, - customer_id: "311211654-YrXF6J0QikpIWX", - config: [public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"], - installments: 1 - ] - - def new_email_opts(good) do - no1 = :rand.uniform(1_000_00) |> to_string - no2 = :rand.uniform(1_000_00) |> to_string - no3 = :rand.uniform(1_000_00) |> to_string - email = "hp" <> no1 <> no2 <> no3 <> "@potter.com" - case good do - true -> @new_cutomer_good_opts ++ [email: email] - _ -> @new_cutomer_bad_opts ++ [email: email] - end - end - - describe "[authorize]" do - test "old customer with good_opts and good_card" do - use_cassette "mercadopago/authorize_old customer with good_opts and good_card" do - assert {:ok, response} = Gateway.authorize(@amount, @good_card, @good_opts) + describe "[capture]" do + test "capture success" do + use_cassette "mercadopago/capture_success" do + {:ok, response} = Gateway.authorize(@sub_amount, @good_card, @good_opts) + {:ok, response} = Gateway.capture(response.id, @sub_amount, @good_opts) assert response.success == true - assert response.status_code == 201 - end - end - - test "old customer with good_opts and bad_card" do - use_cassette "mercadopago/authorize_old customer with good_opts and bad_card" do - assert {:error, response} = Gateway.authorize(@amount, @bad_card, @good_opts) - assert response.success == false - assert response.status_code == 400 - end - end - - test "old customer with bad_opts and good_card" do - use_cassette "mercadopago/authorize_old customer with bad_opts and good_card" do - assert {:error, response} = Gateway.authorize(@amount, @good_card, @bad_opts) - assert response.success == false - assert response.status_code == 401 + assert response.status_code == 200 end end - test "old customer with bad_opts and bad_opts" do - use_cassette "mercadopago/authorize_old customer with bad_opts and bad_opts" do - assert {:error, response} = Gateway.authorize(@amount, @bad_card, @bad_opts) + test "invalid payment_id" do + use_cassette "mercadopago/capture_invalid_payment_id" do + {:ok, response} = Gateway.authorize(@sub_amount, @good_card, @good_opts) + id = response.id + 1 + {:error, response} = Gateway.capture(id, @sub_amount, @good_opts) assert response.success == false - assert response.status_code == 400 + assert response.status_code == 404 end end - test "new cutomer with good_opts and good_card" do - use_cassette "mercadopago/authorize_new cutomer with good_opts and good_card" do - opts = new_email_opts(true) - assert {:ok, response} = Gateway.authorize(@amount, @good_card, opts) - assert response.success == true - assert response.status_code == 201 - end - end - - test "new customer with good_opts and bad_card" do - use_cassette "mercadopago/authorize_new customer with good_opts and bad_card" do - opts = new_email_opts(true) - assert {:error, response} = Gateway.authorize(@amount, @bad_card, opts) + test "accessing amount" do + use_cassette "mercadopago/access_amount" do + {:ok, response} = Gateway.authorize(@sub_amount, @good_card, @good_opts) + {:error, response} = Gateway.capture(response.id, @amount, @good_opts) assert response.success == false assert response.status_code == 400 end end - - test "new customer with bad_opts and good_card" do - use_cassette "mercadopago/authorize_new customer with bad_opts and good_card" do - opts = new_email_opts(false) - assert {:error, response} = Gateway.authorize(@amount, @good_card, opts) - assert response.success == false - assert response.status_code == 401 - end - end - - test "new customer with bad_opts and bad_card" do - use_cassette "mercadopago/authorize_new customer with bad_opts and bad_card" do - opts = new_email_opts(false) - assert {:error, response} = Gateway.authorize(@amount, @bad_card, opts) - assert response.success == false - assert response.status_code == 401 - end - end end end From 699876ff30effe200d23023fa32858f3c0d963ea Mon Sep 17 00:00:00 2001 From: siriusdark Date: Wed, 18 Apr 2018 02:05:06 +0530 Subject: [PATCH 57/60] Added test cases for capture --- test/integration/gateways/mercadopago_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/gateways/mercadopago_test.exs b/test/integration/gateways/mercadopago_test.exs index afff1e87..d8078cbe 100644 --- a/test/integration/gateways/mercadopago_test.exs +++ b/test/integration/gateways/mercadopago_test.exs @@ -50,7 +50,7 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do end end - test "accessing amount" do + test "extra amount capture" do use_cassette "mercadopago/access_amount" do {:ok, response} = Gateway.authorize(@sub_amount, @good_card, @good_opts) {:error, response} = Gateway.capture(response.id, @amount, @good_opts) From 3254c5408910d2ac2fd185ece3e3c84a4fe20983 Mon Sep 17 00:00:00 2001 From: siriusdark Date: Wed, 18 Apr 2018 02:22:31 +0530 Subject: [PATCH 58/60] Implemented purchase 1. added purchase 2. added test cases --- lib/gringotts/gateways/mercadopago.ex | 88 +++----------- .../integration/gateways/mercadopago_test.exs | 114 +++++++++++++++--- 2 files changed, 117 insertions(+), 85 deletions(-) diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex index df4189ef..da3a5f4e 100644 --- a/lib/gringotts/gateways/mercadopago.ex +++ b/lib/gringotts/gateways/mercadopago.ex @@ -95,88 +95,38 @@ defmodule Gringotts.Gateways.Mercadopago do alias Gringotts.{CreditCard, Response} @doc """ - Performs a (pre) Authorize operation. + Transfers `amount` from the customer to the merchant. - The authorization validates the `card` details with the banking network, - places a hold on the transaction `amount` in the customer’s issuing bank. - - mercadoapgo's `authorize` returns authorization ID(available in the `Response.id` field) : - - * `capture/3` _an_ amount. - * `void/2` a pre-authorization. - ## Note - - For a new customer, `customer_id` field should be ignored. Otherwise it should be provided. + mercadopago attempts to process a purchase on behalf of the customer, by + debiting `amount` from the customer's account by charging the customer's + `card`. ## Example - ### Authorization for new customer. - The following example shows how one would (pre) authorize a payment of 42 BRL on a sample `card`. - Ignore `customer_id`. - iex> amount = Money.new(42, :BRL) - iex> card = %Gringotts.CreditCard{first_name: "Lord", last_name: "Voldemort", number: "4509953566233704", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> opts = [email: "tommarvolo@riddle.com", order_id: 123123, payment_method_id: "visa", config: %{access_token: YOUR_ACCESS_TOKEN, public_key: YOUR_PUBLIC_KEY}] - iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Mercadopago, amount, card, opts) - iex> auth_result.id # This is the authorization ID - iex> auth_result.token # This is the customer ID/token - - ### Authorization for old customer. - The following example shows how one would (pre) authorize a payment of 42 BRL on a sample `card`. - Mention `customer_id`. + The following example shows how one would process a payment worth 42 BRL in + one-shot, without (pre) authorization. + iex> amount = Money.new(42, :BRL) - iex> card = %Gringotts.CreditCard{first_name: "Hermione", last_name: "Granger", number: "4509953566233704", year: 2099, month: 12, verification_code: "123", brand: "VISA"} - iex> opts = [email: "hermione@granger.com", order_id: 123125, customer_id: "308537342-HStv9cJCgK0dWU", payment_method_id: "visa", config: %{access_token: YOUR_ACCESS_TOKEN, public_key: YOUR_PUBLIC_KEY}] - iex> {:ok, auth_result} = Gringotts.authorize(Gringotts.Gateways.Mercadopago, amount, card, opts) - iex> auth_result.id # This is the authorization ID - iex> auth_result.token # This is the customer ID/token + iex> card = %Gringotts.CreditCard{first_name: "Harry", last_name: "Potter", number: "4200000000000000", year: 2099, month: 12, verification_code: "123", brand: "VISA"} + iex> {:ok, purchase_result} = Gringotts.purchase(Gringotts.Gateways.Mercadopago, amount, card, opts) + iex> purchase_result.token # This is the customer ID/token """ - - @spec authorize(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} - def authorize(amount, %CreditCard{} = card, opts) do + @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} + def purchase(amount, %CreditCard{} = card, opts) do if Keyword.has_key?(opts, :customer_id) do - auth_token(amount, card, opts, opts[:customer_id], false) + auth_token(amount, card, opts, opts[:customer_id], true) else - case create_customer(opts) do - {:error, res} -> {:error, res} - {:ok, customer_id} -> auth_token(amount, card, opts, customer_id, false) + {state, res} = create_customer(opts) + + if state == :error do + {state, res} + else + auth_token(amount, card, opts, res, true) end end end - @doc """ - Captures a pre-authorized `amount`. - - `amount` is transferred to the merchant account by mercadopago used in the - pre-authorization referenced by `payment_id`. - - ## Note - mercadopago allows partial captures also. However, you can make a partial capture to a payment only **once**. - - > The authorization will be valid for 7 days. If you do not capture it by that time, it will be cancelled. - - > The specified amount can not exceed the originally reserved. - - > If you do not specify the amount, all the reserved money is captured. - - > In Argentina only available for Visa and American Express cards. - - ## Example - - The following example shows how one would (partially) capture a previously - authorized a payment worth 35 BRL by referencing the obtained authorization `id`. - - iex> amount = Money.new(35, :BRL) - iex> {:ok, capture_result} = Gringotts.capture(Gringotts.Gateways.Mercadopago, auth_result.id, amount, opts) - """ - @spec capture(String.t(), Money.t(), keyword) :: {:ok | :error, Response} - def capture(payment_id, amount, opts) do - {_, value, _, _} = Money.to_integer_exp(amount) - url_params = [access_token: opts[:config][:access_token]] - body = %{capture: true, transaction_amount: value} |> Poison.encode!() - commit(:put, "/v1/payments/#{payment_id}", body, opts, params: url_params) - end - ############################################################################### # PRIVATE METHODS # ############################################################################### diff --git a/test/integration/gateways/mercadopago_test.exs b/test/integration/gateways/mercadopago_test.exs index d8078cbe..9d396546 100644 --- a/test/integration/gateways/mercadopago_test.exs +++ b/test/integration/gateways/mercadopago_test.exs @@ -19,6 +19,16 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do brand: "VISA" } + @bad_card %Gringotts.CreditCard{ + first_name: "Hermoine", + last_name: "Granger", + number: "4509953566233704", + year: 2000, + month: 07, + verification_code: "123", + brand: "VISA" + } + @good_opts [ email: "hermoine@granger.com", order_id: 123_126, @@ -29,34 +39,106 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do ], installments: 1 ] + @new_cutomer_good_opts [ + order_id: 123_126, + config: [ + access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", + public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a" + ], + installments: 1 + ] + @new_cutomer_bad_opts [ + order_id: 123_126, + config: [public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"], + installments: 1 + ] + @bad_opts [ + email: "hermoine@granger.com", + order_id: 123_126, + customer_id: "311211654-YrXF6J0QikpIWX", + config: [public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"], + installments: 1 + ] + + def new_email_opts(good) do + no1 = :rand.uniform(1_000_00) |> to_string + no2 = :rand.uniform(1_000_00) |> to_string + no3 = :rand.uniform(1_000_00) |> to_string + email = "hp" <> no1 <> no2 <> no3 <> "@potter.com" - describe "[capture]" do - test "capture success" do - use_cassette "mercadopago/capture_success" do - {:ok, response} = Gateway.authorize(@sub_amount, @good_card, @good_opts) - {:ok, response} = Gateway.capture(response.id, @sub_amount, @good_opts) + case good do + true -> @new_cutomer_good_opts ++ [email: email] + _ -> @new_cutomer_bad_opts ++ [email: email] + end + end + + describe "[purchase]" do + test "old customer with good_opts and good_card" do + use_cassette "mercadopago/purchase_old customer with good_opts and good_card" do + assert {:ok, response} = Gateway.purchase(@amount, @good_card, @good_opts) assert response.success == true - assert response.status_code == 200 + assert response.status_code == 201 + end + end + + test "old customer with good_opts and bad_card" do + use_cassette "mercadopago/purchase_old customer with good_opts and bad_card" do + assert {:error, response} = Gateway.purchase(@amount, @bad_card, @good_opts) + assert response.success == false + assert response.status_code == 400 + end + end + + test "old customer with bad_opts and good_card" do + use_cassette "mercadopago/purchase_old customer with bad_opts and good_card" do + assert {:error, response} = Gateway.purchase(@amount, @good_card, @bad_opts) + assert response.success == false + assert response.status_code == 401 end end - test "invalid payment_id" do - use_cassette "mercadopago/capture_invalid_payment_id" do - {:ok, response} = Gateway.authorize(@sub_amount, @good_card, @good_opts) - id = response.id + 1 - {:error, response} = Gateway.capture(id, @sub_amount, @good_opts) + test "old customer with bad_opts and bad_opts" do + use_cassette "mercadopago/purchase_old customer with bad_opts and bad_opts" do + assert {:error, response} = Gateway.purchase(@amount, @bad_card, @bad_opts) assert response.success == false - assert response.status_code == 404 + assert response.status_code == 400 end end - test "extra amount capture" do - use_cassette "mercadopago/access_amount" do - {:ok, response} = Gateway.authorize(@sub_amount, @good_card, @good_opts) - {:error, response} = Gateway.capture(response.id, @amount, @good_opts) + test "new cutomer with good_opts and good_card" do + use_cassette "mercadopago/purchase_new cutomer with good_opts and good_card" do + opts = new_email_opts(true) + assert {:ok, response} = Gateway.purchase(@amount, @good_card, opts) + assert response.success == true + assert response.status_code == 201 + end + end + + test "new customer with good_opts and bad_card" do + use_cassette "mercadopago/purchase_new customer with good_opts and bad_card" do + opts = new_email_opts(true) + assert {:error, response} = Gateway.purchase(@amount, @bad_card, opts) assert response.success == false assert response.status_code == 400 end end + + test "new customer with bad_opts and good_card" do + use_cassette "mercadopago/purchase_new customer with bad_opts and good_card" do + opts = new_email_opts(false) + assert {:error, response} = Gateway.purchase(@amount, @good_card, opts) + assert response.success == false + assert response.status_code == 401 + end + end + + test "new customer with bad_opts and bad_card" do + use_cassette "mercadopago/purchase_new customer with bad_opts and bad_card" do + opts = new_email_opts(false) + assert {:error, response} = Gateway.purchase(@amount, @bad_card, opts) + assert response.success == false + assert response.status_code == 401 + end + end end end From 8b752610bc8a64ff72f3b4c39c54d3f5733afb57 Mon Sep 17 00:00:00 2001 From: siriusdark Date: Wed, 18 Apr 2018 23:38:12 +0530 Subject: [PATCH 59/60] Applied mix formatter. --- test/gateways/mercadopago_test.exs | 10 +++++----- test/mocks/mercadopago_mock.exs | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/test/gateways/mercadopago_test.exs b/test/gateways/mercadopago_test.exs index 62bf1ae7..74e9c0e4 100644 --- a/test/gateways/mercadopago_test.exs +++ b/test/gateways/mercadopago_test.exs @@ -1,15 +1,15 @@ defmodule Gringotts.Gateways.MercadopagoTest do # The file contains mocked tests for Mercadopago - + # We recommend using [mock][1] for this, you can place the mock responses from # the Gateway in `test/mocks/mercadopago_mock.exs` file, which has also been # generated for you. # # [1]: https://github.com/jjh42/mock - + # Load the mock response file before running the tests. - Code.require_file "../mocks/mercadopago_mock.exs", __DIR__ - + Code.require_file("../mocks/mercadopago_mock.exs", __DIR__) + use ExUnit.Case, async: false alias Gringotts.Gateways.Mercadopago import Mock @@ -21,7 +21,7 @@ defmodule Gringotts.Gateways.MercadopagoTest do describe "authorize" do end - describe "capture" do + describe "capture" do end describe "void" do diff --git a/test/mocks/mercadopago_mock.exs b/test/mocks/mercadopago_mock.exs index f7a73709..14ed9338 100644 --- a/test/mocks/mercadopago_mock.exs +++ b/test/mocks/mercadopago_mock.exs @@ -1,9 +1,7 @@ defmodule Gringotts.Gateways.MercadopagoMock do - # The module should include mock responses for test cases in mercadopago_test.exs. # e.g. # def successful_purchase do # {:ok, %HTTPoison.Response{body: ~s[{data: "successful_purchase"}]} # end - end From 7b2b36a51f7e1ed3e51d334b12d5c3136759b613 Mon Sep 17 00:00:00 2001 From: siriusdark Date: Thu, 19 Apr 2018 00:10:07 +0530 Subject: [PATCH 60/60] 1. Updated purchase function. 2. Updated test cases. --- lib/gringotts/gateways/mercadopago.ex | 59 ++++++------------ .../integration/gateways/mercadopago_test.exs | 60 ++----------------- 2 files changed, 25 insertions(+), 94 deletions(-) diff --git a/lib/gringotts/gateways/mercadopago.ex b/lib/gringotts/gateways/mercadopago.ex index da3a5f4e..8b9ea2f2 100644 --- a/lib/gringotts/gateways/mercadopago.ex +++ b/lib/gringotts/gateways/mercadopago.ex @@ -114,16 +114,15 @@ defmodule Gringotts.Gateways.Mercadopago do """ @spec purchase(Money.t(), CreditCard.t(), keyword) :: {:ok | :error, Response} def purchase(amount, %CreditCard{} = card, opts) do - if Keyword.has_key?(opts, :customer_id) do - auth_token(amount, card, opts, opts[:customer_id], true) - else - {state, res} = create_customer(opts) + with {:ok, customer_id} <- create_customer(opts), + {:ok, card_token} <- create_token(card, opts) do + {_, value, _, _} = Money.to_integer_exp(amount) + url_params = [access_token: opts[:config][:access_token]] - if state == :error do - {state, res} - else - auth_token(amount, card, opts, res, true) - end + body = + authorize_params(value, card, opts, card_token, customer_id, false) |> Poison.encode!() + + commit(:post, "/v1/payments", body, opts, params: url_params) end end @@ -144,39 +143,19 @@ defmodule Gringotts.Gateways.Mercadopago do # Parses mercadopago's response and returns a `Gringotts.Response` struct # in a `:ok`, `:error` tuple. - defp auth_token(amount, %CreditCard{} = card, opts, customer_id, capture) do - opts = opts ++ [customer_id: customer_id] - {state, res} = create_token(card, opts) - - if state == :error do - {state, res} - else - auth(amount, card, opts, res, capture) - end - end - - defp auth(amount, %CreditCard{} = card, opts, token_id, capture) do - {_, value, _, _} = Money.to_integer_exp(amount) - opts = opts ++ [token_id: token_id] - url_params = [access_token: opts[:config][:access_token]] - - body = - authorize_params(value, card, opts, opts[:token_id], opts[:customer_id], capture) - |> Poison.encode!() - - commit(:post, "/v1/payments", body, opts, params: url_params) - end - defp create_customer(opts) do - url_params = [access_token: opts[:config][:access_token]] - body = %{email: opts[:email]} |> Poison.encode!() - - {state, res} = commit(:post, "/v1/customers", body, opts, params: url_params) - - if state == :error do - {state, res} + if Keyword.has_key?(opts, :customer_id) do + {:ok, opts[:customer_id]} else - {state, res.id} + url_params = [access_token: opts[:config][:access_token]] + body = %{email: opts[:email]} |> Poison.encode!() + {state, res} = commit(:post, "/v1/customers", body, opts, params: url_params) + + if state == :error do + {state, res} + else + {state, res.id} + end end end diff --git a/test/integration/gateways/mercadopago_test.exs b/test/integration/gateways/mercadopago_test.exs index 9d396546..3a961610 100644 --- a/test/integration/gateways/mercadopago_test.exs +++ b/test/integration/gateways/mercadopago_test.exs @@ -9,6 +9,10 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do @amount Money.new(45, :BRL) @sub_amount Money.new(30, :BRL) + @config [ + access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", + public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a" + ] @good_card %Gringotts.CreditCard{ first_name: "Hermoine", last_name: "Granger", @@ -33,30 +37,12 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do email: "hermoine@granger.com", order_id: 123_126, customer_id: "311211654-YrXF6J0QikpIWX", - config: [ - access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", - public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a" - ], + config: @config, installments: 1 ] @new_cutomer_good_opts [ order_id: 123_126, - config: [ - access_token: "TEST-2774702803649645-031303-1b9d3d63acb57cdad3458d386eee62bd-307592510", - public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a" - ], - installments: 1 - ] - @new_cutomer_bad_opts [ - order_id: 123_126, - config: [public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"], - installments: 1 - ] - @bad_opts [ - email: "hermoine@granger.com", - order_id: 123_126, - customer_id: "311211654-YrXF6J0QikpIWX", - config: [public_key: "TEST-911f45a1-0560-4c16-915e-a8833830b29a"], + config: @config, installments: 1 ] @@ -89,22 +75,6 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do end end - test "old customer with bad_opts and good_card" do - use_cassette "mercadopago/purchase_old customer with bad_opts and good_card" do - assert {:error, response} = Gateway.purchase(@amount, @good_card, @bad_opts) - assert response.success == false - assert response.status_code == 401 - end - end - - test "old customer with bad_opts and bad_opts" do - use_cassette "mercadopago/purchase_old customer with bad_opts and bad_opts" do - assert {:error, response} = Gateway.purchase(@amount, @bad_card, @bad_opts) - assert response.success == false - assert response.status_code == 400 - end - end - test "new cutomer with good_opts and good_card" do use_cassette "mercadopago/purchase_new cutomer with good_opts and good_card" do opts = new_email_opts(true) @@ -122,23 +92,5 @@ defmodule Gringotts.Integration.Gateways.MercadopagoTest do assert response.status_code == 400 end end - - test "new customer with bad_opts and good_card" do - use_cassette "mercadopago/purchase_new customer with bad_opts and good_card" do - opts = new_email_opts(false) - assert {:error, response} = Gateway.purchase(@amount, @good_card, opts) - assert response.success == false - assert response.status_code == 401 - end - end - - test "new customer with bad_opts and bad_card" do - use_cassette "mercadopago/purchase_new customer with bad_opts and bad_card" do - opts = new_email_opts(false) - assert {:error, response} = Gateway.purchase(@amount, @bad_card, opts) - assert response.success == false - assert response.status_code == 401 - end - end end end