From 8c37b6aeab34f7d97b69f0d4c83967557062afe6 Mon Sep 17 00:00:00 2001 From: Edwin Eames Date: Mon, 8 Sep 2025 14:30:40 -0400 Subject: [PATCH] Add all project files --- Dockerfile.dev | 14 ++ Dockerfile.local | 14 ++ Dockerfile.prod | 16 ++ __pycache__/config.cpython-39.pyc | Bin 0 -> 560 bytes __pycache__/settings_dev.cpython-39.pyc | Bin 0 -> 910 bytes app/__init__.py | 0 app/__pycache__/__init__.cpython-39.pyc | Bin 0 -> 111 bytes app/__pycache__/crud.cpython-39.pyc | Bin 0 -> 2221 bytes app/__pycache__/database.cpython-39.pyc | Bin 0 -> 898 bytes app/__pycache__/main.cpython-39.pyc | Bin 0 -> 836 bytes app/__pycache__/models.cpython-39.pyc | Bin 0 -> 1716 bytes app/__pycache__/schemas.cpython-39.pyc | Bin 0 -> 2933 bytes app/crud.py | 61 +++++ app/database.py | 34 +++ app/main.py | 34 +++ app/models.py | 45 ++++ app/routers/__init__.py | 0 .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 119 bytes .../__pycache__/payment.cpython-39.pyc | Bin 0 -> 4177 bytes app/routers/payment.py | 133 ++++++++++ app/schemas.py | 64 +++++ .../payment_service.cpython-39.pyc | Bin 0 -> 5426 bytes app/services/payment_service.py | 235 ++++++++++++++++++ config.py | 23 ++ requirements.txt | 7 + settings_dev.py | 34 +++ settings_local.py | 33 +++ settings_prod.py | 26 ++ 28 files changed, 773 insertions(+) create mode 100644 Dockerfile.dev create mode 100644 Dockerfile.local create mode 100644 Dockerfile.prod create mode 100644 __pycache__/config.cpython-39.pyc create mode 100644 __pycache__/settings_dev.cpython-39.pyc create mode 100644 app/__init__.py create mode 100644 app/__pycache__/__init__.cpython-39.pyc create mode 100644 app/__pycache__/crud.cpython-39.pyc create mode 100644 app/__pycache__/database.cpython-39.pyc create mode 100644 app/__pycache__/main.cpython-39.pyc create mode 100644 app/__pycache__/models.cpython-39.pyc create mode 100644 app/__pycache__/schemas.cpython-39.pyc create mode 100644 app/crud.py create mode 100644 app/database.py create mode 100644 app/main.py create mode 100644 app/models.py create mode 100644 app/routers/__init__.py create mode 100644 app/routers/__pycache__/__init__.cpython-39.pyc create mode 100644 app/routers/__pycache__/payment.cpython-39.pyc create mode 100644 app/routers/payment.py create mode 100644 app/schemas.py create mode 100644 app/services/__pycache__/payment_service.cpython-39.pyc create mode 100644 app/services/payment_service.py create mode 100644 config.py create mode 100644 requirements.txt create mode 100644 settings_dev.py create mode 100644 settings_local.py create mode 100644 settings_prod.py diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..508ae4b --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,14 @@ +FROM python:3.9 +ENV PYTHONFAULTHANDLER=1 + +ENV PYTHONUNBUFFERED=1 + +ENV MODE="DEVELOPMENT" +WORKDIR /app + +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt + +COPY . . + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/Dockerfile.local b/Dockerfile.local new file mode 100644 index 0000000..a9e93e0 --- /dev/null +++ b/Dockerfile.local @@ -0,0 +1,14 @@ +FROM python:3.9 +ENV PYTHONFAULTHANDLER=1 + +ENV PYTHONUNBUFFERED=1 + +ENV MODE="LOCAL" +WORKDIR /app + +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt + +COPY . . + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..92320fc --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,16 @@ +FROM python:3.9 + +ENV PYTHONFAULTHANDLER=1 + +ENV PYTHONUNBUFFERED=1 + +ENV MODE="PRODUCTION" + +WORKDIR /app + +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt + +COPY . . + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/__pycache__/config.cpython-39.pyc b/__pycache__/config.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3614ff2ad5ee30eed9e1044a4c3eb01c2832c7b7 GIT binary patch literal 560 zcmZ8e%}T>S5T4omwDb>r0V&?%!TJCqN}GdF8}S$SpoVU&(KH)2QBd@v9()H8^rkQ4 z#oJyz`3jz#jUv@q=7Vp(o!Ob$u-U8u+3VZ$(G>%DH)PT*B1`0Xn?QiT1UN)+jE_vP zCp<)93HuhgC2#{g=Ywv+fZMA<&mW>8KSs5oO5P>U?+7jq3aWMq_wc}O5aAL8J7H?p z)Jt&8u1pMfV8bX5DZNs${wXdgd!=GeDm#$Q*ibm<*9hkP&2?dy>dwzhx5)71S-$OmMoMlNo42xK%U6~%nBkuHr?sA{kd;YfH57t)w!KUYG zCsT1+@MazrMVyZE{Y*)r%;3EB8%yLcOnBp$i|9<(SH_uCg|C!U9@Dnzrp$W*ax4K4)Xr))rf~e|LJv_$|)y4!ztJb8@S5MU8$T(7UI}yWx#9o96%^X7B z1rb`7n!9qnx%0B#tjkgx@SS!`23hRP++g|arvH5UJS3NH5PI@nqj7b;cXhpgbxo6+ zuzWm;O?=;(+vAh{KH{ee5#eZh+fI{ccXf1X- zZ;!(ZP-(Sy4nRv@w$6<9#2(0ct&)fk&Yc;CFv&wW3(#Vk=0yn47tZvrB0z`&7eY|! zjZKqYC$v=C*7SjuY#SrX9+;{H$Chg9%8{BBZjtdODc=@UK2go2bPJ4;Y2Vr3@20yY zDap|nI&)(1)H)t2!`^{dUzSJR8Rj0Ql&v;`3%qT p?9IxtT)KYqS&nnYo&iqN8`2&=B_vios literal 0 HcmV?d00001 diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/__pycache__/__init__.cpython-39.pyc b/app/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78a8abf52847dd8e4818b8cb7b9adfb11dad2c30 GIT binary patch literal 111 zcmYe~<>g`kf(sW{WPs?$AOaaM0yz#qT+9L_QW%06G#UL?G8BP?5yUSM{ltO-FpiJU c%*!l^kJl@xyv1RYo1apelWGT2{TYZE0LN(+Jpcdz literal 0 HcmV?d00001 diff --git a/app/__pycache__/crud.cpython-39.pyc b/app/__pycache__/crud.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3cfbd432be4cf74ab3f95528a86ba322b22ea87c GIT binary patch literal 2221 zcmbtV&2HO95a#kvvM9-l9mkEExM`cDWq+I^=b|W*yHUVB6uJ;JcV$zdelkm?P@&#( z?Hl-DUkdmk`XYNO(32mamlj3)%}TOZ*+35^wWC>bcKOZBH?wK0)o^hA`sXjF&!PRs zlj>E&}2X7*~+ZI-D%ez?Jw7czKeoyXV-jWab ziE7(*?_k}1xrcR|vVG*l`&Yo~#KP2$l-BuZcqMr5ns6|Zs?esUvr{!l^`&cEd2FI= zqRY`hQIgBd<+Y28bsys;hTeymIp&%`U91sBedb14qv&;+k1il)Tt zO#S6zXA}1FqEuwuT$-SllP)QQ#ic`=`1_;tEFF*S$Y>%D##7TgQDw3O#+zx2Owb)( z<`O5y1n?p+{tsr_z&>$E%mRy^kDNqc`EG*VC$``&e$g zv?yp7+B)i16}9F7EYb335945tG%6__>NG1sB&zd{p)S*MqRnnPDNmCj@>DTl4h67U zUYz9?hV|zgD5cXEGjIpP!+hR8h{n1Myf|jpMsrCC3?t$6R<+uZOh|o@Yhqkl>t<#swSKZ z%_tnDN*6h6Wd%W+3E;TrD0 z=m5`_?s5KaxW{eBy7%9`?`9>gT;RRu&EpwsYrywz0d5N`m!H5~bBrzMzv&BTi?XzY zom0xjh3Rvn?ym8Fhqt$-R?e^Ze~p;fL;93gFGz09t>wzd5`Qgf`;)O;V|LOTQIe(OazctDrSCyF zVxO7qhJtTjgsx>a^EGxY?}*J}99{TzOk2M$(t?l2>A{Ew#D|VqoDAI7jF%5fdj=T~ zXBcr~C+?SKvr_OcQgjlo9?LPCH=KIKC@qP2FL63g3iTBigXTv~(Q!Lo N$Ghw8ZFd@-@E?=R*h>Ha literal 0 HcmV?d00001 diff --git a/app/__pycache__/database.cpython-39.pyc b/app/__pycache__/database.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5caf837785addf7446b18b0cd18440bb0d33787a GIT binary patch literal 898 zcmYjPOLNmO5SDC5ahyk+wBr+$fj8K?@alsJ$?RcW=v6&b1;tzjf;mxxB9dRFH|L}v|b;&s=j5t|lSoGFzQ zwC3=soSKOZMoG$JSM%av0B+unG*3Yl=n{QJUpg1)7-4jRF3>f;0hC4yv<-WJ-tk`7 zv(>?@C0mJQRyOJJ(J!Ri@Tc4LEtcn{EDGGr&n+2?V#r62T|$=`VS+szdU_4;UiTr5 zpm!$YiBRX-1CBoWPi%_M#pcpzHQXOWTT2d^RyuHGdx@N_MOT2gWHxC{oYCINbD6}j zrYwCq+W?uZlC}@_2ZwL^y}|Hk(ChE*yz9~W9d&R*SMCh>eBA3V1-#mI?%Uh6dFOMm z-#@heR7*?d#k5@h>ikOBVAt(0RH9&RST>qpC?;h&m~x^^s^g-=XDZ`UDW1D6nkS$d Yc>egr4`8gp;<+WY&}(7h5!~?q0bF_RdH?_b literal 0 HcmV?d00001 diff --git a/app/__pycache__/main.cpython-39.pyc b/app/__pycache__/main.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..013728c148bdac9e028bbb0e315c8b9d8f6b7bd8 GIT binary patch literal 836 zcmYjP&2H2%5Vn(SlKpMAE%eA9xGabE1u8^Y3BiFpOU&B~4=Csk>0s9g6Q z;8xy;Q?8u)3gyB$p+AmhGBdWv-}l*aIP5VR-+o^&*B)bkT=IBCl)NGbK`?ubqkcT}h&5mC$_RI%=gZWBkUr~9JM8TPN37+_~LT#Z&6=tI1w zj>xH+V6qxx7YD0N=t6J78|$yBGtF#awAzLN45>a^)5)0pcmf+gIV3Rt&aSw43Y*7l zx^+Wn*_7AO8(CHdNAGTUn}?V$vK(n9)CN$MZcQOCH0BkBquGa#$L})$h3C?`WqoBN z%u|z}W(!J#lPrf%HGeS;#84__&gaHv)Up)gHcc%8HOr(b?^bB6mIm(AB}zbBUIu%z z$ZY6DLYb literal 0 HcmV?d00001 diff --git a/app/__pycache__/models.cpython-39.pyc b/app/__pycache__/models.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fde198b85965071b0a8895e8437e0043efdee745 GIT binary patch literal 1716 zcmah}J9FGN5GHwi-BYK>NpfD3Dl^w4e;||5*qLP9sIiI+8Vp3hcT@n70zoV1Rw<(& z;)CsLwKWA&+Ac^7x0IB{me8!UGhyNkW$_km5PqX3@d?Ys5+EC~KaqyNNUoWW{- z0l@8SAQm-3)lj5AoxUm!$4LLQKahh!jPkwCxD@!f!=U}ZeM4R1q|dp+^` zu&0l_o_hZnbci4m@7v~0L7#zd&-*eTH{ZqW_u>wAzC-WZ);scg?sFEq{bTRj_HokF zPeFeI=X}P8Fu9S-pS6vxlrUsi?6l_MP2|S4DHSst`m5Nuq|{vecK-y1aQ^|tA0Wzq zV0PvEK@glTKX*B$nkhl48&Rq%-b&OblwP+??sle>T6QVL)-(|mF_0d(sN@ErLT0Qe zs#aU7TXhLN9X#|zC2PP7Cy>L=_p&q(&S~eg)r}^z&TK4vax&@c*JW)|JnG+N3MOSQ zPbgN(-ubMBDKT3XWYN{C*OeBZwz`IKQoE`9$Fkazd6&pIH==3CsHjxU^oEw4?035o zj8{Y5z#N0$iw_6ivAXsPK7)|WxAn##OLJJwH>iYh zGz%xYUk>m7+FFy)=X^p49Niff`V++e5F+C7U9=;_R!2f0@R1DoMj}i+jUt}h#RN%! z{u#c(kNHT52i+55=SNb&)xa}|K~i8Hj7MS|iN)38>e%m{@EorQH@+}TH>|K_rOCuT z8qI|n!=HgmAlI#w2+-xgrL8WnTVbZKWf1rV=LbO<7cW^W?fLT5?4$nx#UY9#6dkI~ z0!_R@$YE^+0BBvA^0lB$A;ihFSTnQg%z3XRj~6zy3pwPZ<_GdMs9940X&gqed67`$K;#DcKtO`l3v*=|lxRJRXz7G&@=G+^Y$O7_q&Gi7m@X!19FH` z{SM#;H&Y`sL(`;hE44B^v@<7kG`CYXb3@lOzBRbR-QNuE3iGKQdf*=KfOmuk-U0V{ z7rd*v58mSec%a*N!TWpwKG60a_zoX}54Al2AMr8xSOgfa&v$9>xF7a)->$X|VB67sd&J>UpoR}6%PSVs z;>oL-nv_zod3{>Q4oEEHB!zf~TUet^{siH{wWVDmKmXv=oAm|h95DWpzu4;K<$Qx8a z@b}~%D@$Ff_*7)9x?7%04;B}t8UcFd2#>FEWcE#M-5%$==(38lK$N+Oc)EBa&iYO9 zB~I~m5fe|1*J5&nU8zOmOrWqFmI`de$fWKNk5iCM0Jn%23)-Nj2LS z_yE=o3#4?RG@&=rggc=tJkh}shWIV&aqJ;h9qvP{{HIpva|iBpRj}rcvJx(#y@v8C z9FTc6lXr*(@w}>wOh}y1D~qG^dWfRaez`bODsEu{`^ox#-90MuO;iSD=UNutegCCk3%!`9x&_6WS` zuVp(NxAs7{-)-%E%_psWp!r_QcQn7&wjXMKy|s@tztQrs<~PCj_4;>-Blr{5KU!o? zb7$g`kg8vs+WPs?$AOaaM0yz#qT+9L_QW%06G#UL?G8BP?5yUSU{ltO-FfPh3 kElDjZ){l?R%*!l^kJl@xyv1RYo1apelWGUj^%;m60I$s$w*UYD literal 0 HcmV?d00001 diff --git a/app/routers/__pycache__/payment.cpython-39.pyc b/app/routers/__pycache__/payment.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1886df49f6a09d2be33603e136fd9de9bd321ea7 GIT binary patch literal 4177 zcmaJ^UvC@75x>1V9*?9%Nt9$;wkw|_#|_g&bkim+gd(*f3AWKd7G$|W4+Yp0cO{V~ zkM!QsjTo{hBERabZ%qN`(VrsUW1kB2$zPxkNrB`yd!$Ioc2C;5z1!Kj*_q$`VKhCR zw{ZP?`MV>zXj%WEhsn#s!(EI-G0Wl>XH6@lyWM0hJG5I)=(OC>W%SNzW?Ek8nYr7{ zw*1g<<-(kKX3cz<=Naxb3$3Yesx=)>w~Ar0H51NYJ;Srj+16Y*$1G6_=Y`GvL&kHb zEL^x}@jNd)w|GJ18;d7ScnNQ&_%z;358qrCIWfOv313{o>3N5bj>xOp)ytg=g^L0Huz6{PAJTRQ^a`wn7-+V>>*e*Ldx4!dW zx7}4j>g=ZIh?pmO`u^VD&eoST(NXnwTxL4^NF)hOZJpWccA7%_56xQCd}TvW+0|}M zcDeRiZ7!OL&L*`Z(TWo7^GHSeQ6eB^rV|~vM69Zbkk9Hh5l-#NC{CgpeI)^dtD?iC zMw(4tz(|Ug1x$n$GH!)7XQ9LG(B%$J+w&2gld-osiJ_xtM?8f@e-s9b^@fp(DW!gjvT*XKz7)fEMOz+@gUy7sy zj|lBap}I1zkr_u99guX`z)1QaeM{a|pq!o9@^0U1u!ha;Czvaz;SQKtbFlV_%3#%N zWKURZb9X?)i9Z6DKVbB1I5Xht zJCRI85~w2)jI<99WV>bFtasH>Th@DGbz7)lh`v>`@4daFdJ^EDb|!#Ga3q?rce;6B z9JHnQAgHUKCqWb(w%a_Y9Yyh>2vj?GB1ETbq-ei|0ErGo650=+=y^59_3qG?+j_1l zMMD^{V-_XtxcB~cd#tV$HBo;ic>3ufwt~Y*iN8k2tG(+xY4ikK)SC4eeEY{|tL41T z9z{u{l$6WxNtlNfx=po#a5PAi6g=8VuzrKqvqr@Sobt0J1CX*Suj8h@#Ke!j9KmYU zSg7j6X{vRuXD=RO0$&6zeYx5pZ&XPD?LTt}78AmlmAT41hGBbvU7k%bA9DckHr9HD z3T-t0svs#8N82fGMH4BjQ0peD-4c?tDNJamZN6V4Y>qA;6B-7qpJSq|7p%{|1+Ekj zbi)4Z4BSCx;1N>$tdUVbC+L@GpbAi4xmz}cjy%YLeW zQH7M!bM$2ch>=Ie+8OQ7ADa^YMPw8wl!w+ILu~R6h-K$l5dawf+24CPm)WDgA0+Yi z7AuH8c_=DhO+a99&gEnS>OPFi|A&b}V(MG}wEktEFh)#uUahAmSQ`pyTLgN#017DmG>&K;xZe20p%@?fa8l zeg6yVk>y%y_V~Zd8lbc3t%1u|bA2Shku09$Rrd#E2D!iG z;(U6)W8J|UK(w&kyR*6V+2ea_!R{!%q$Wh(dD<0;3XtJ~32Uwey`|xfy>S)`$eU|{ zc6q&~fcoiF$Pn-H1K9Z@un5n+mA8nZ^qFFAEs%GJ^ZJl;!yu6d6A=WJZVp$^nv$R% zQzix$4E`a7YE-W@{$5S&N3|zwL3u%EI4Ha+O-t zL9rK=vYqB13i=v#ccUwd2@MlkJD4ccdemYI^`4#D1G{gdj-oB&4no5Inkn}MD!vIN z?_4NZlx1ljhL4Qu@r=o+-gcRpZ1?IPaA$92eVAAy>JTR(5yWjmZa3yHZCyw!R<(xu zpgk^BRBx7tnUqpz>PBN`0%bX8l3sZA@Ir0dKQ<+clCjxFolXjs<)LlNx~WWjGFO`H z8!mS{h>~+jv(~F zVMeu0PKE1#kelG>tyCtyR~Z$e%2%Tq4nF_@OnrC_01hOOQ49bG=pz7}xqn%q^&7dr zZ&NQzE;ZtQPv8F-9S4SpLjlJ7`E@SYtZ zwfF@?Ui&{n{tf{NwGEbCBvUfEg?ah5`7t9@y(M(^AOh$*bxH9d$u8!zVKI5yjGFY@ z=Xe!yFEd!t9y)+}d?=~YmJ~qR6>+zv)8Z?sTA9) Tuple[TransactionStatus, Optional[str], Optional[str]]: + """ + Parses the response from the Authorize.Net service. + (This is the same helper from before, it's a good change to keep) + """ + if response and hasattr(response, 'messages') and response.messages.resultCode == "Ok": + status = TransactionStatus.APPROVED + auth_net_transaction_id = str(response.transactionResponse.transId) if hasattr(response, 'transactionResponse') else None + rejection_reason = None + else: + status = TransactionStatus.DECLINED + auth_net_transaction_id = None + if hasattr(response, '_rejection_reason'): + rejection_reason = str(response._rejection_reason) + elif response is None: + rejection_reason = "No response received from payment gateway." + else: + rejection_reason = "Payment declined by gateway." + return status, auth_net_transaction_id, rejection_reason + + +@router.post("/authorize/", response_model=schemas.Transaction) +def authorize_card(customer_id: int, transaction: schemas.TransactionAuthorize, db: Session = Depends(database.get_db)): + auth_net_response = payment_service.authorize_credit_card(transaction) + status, auth_net_transaction_id, rejection_reason = _parse_authnet_response(auth_net_response) + + transaction_data = schemas.TransactionBase( + preauthorize_amount=transaction.preauthorize_amount, + transaction_type=TransactionType.AUTHORIZE, + service_id=transaction.service_id, + delivery_id=transaction.delivery_id, + card_id=transaction.card_id, + rejection_reason=rejection_reason + ) + + return crud.create_transaction( + db=db, + transaction=transaction_data, + customer_id=customer_id, + status=status, + auth_net_transaction_id=auth_net_transaction_id + ) + +@router.post("/charge/{customer_id}", response_model=schemas.Transaction) +def charge_card(customer_id: int, transaction: schemas.TransactionCreate, db: Session = Depends(database.get_db)): + # Add debug logging + print(f"DEBUG: Received charge request for customer_id: {customer_id}") + print(f"DEBUG: Transaction data: {transaction.dict() if hasattr(transaction, 'dict') else transaction}") + + try: + auth_net_response = payment_service.charge_credit_card(transaction) + status, auth_net_transaction_id, rejection_reason = _parse_authnet_response(auth_net_response) + + transaction_data = schemas.TransactionBase( + charge_amount=transaction.charge_amount, + transaction_type=TransactionType.CHARGE, + service_id=transaction.service_id, + delivery_id=transaction.delivery_id, + card_id=transaction.card_id, + rejection_reason=rejection_reason + ) + print(f"DEBUG: Transaction data to create: {transaction_data.dict()}") + + result = crud.create_transaction( + db=db, + transaction=transaction_data, + customer_id=customer_id, + status=status, + auth_net_transaction_id=auth_net_transaction_id + ) + print(f"DEBUG: Created transaction: {result.dict()}") + return result + except Exception as e: + print(f"DEBUG: Exception in charge_card: {str(e)}") + import traceback + print(f"DEBUG: Traceback: {traceback.format_exc()}") + raise + + +@router.post("/capture/", response_model=schemas.Transaction) +def capture_authorized_amount(transaction: schemas.TransactionCapture, db: Session = Depends(database.get_db)): + auth_transaction = crud.get_transaction_by_auth_id(db, auth_net_transaction_id=transaction.auth_net_transaction_id) + if not auth_transaction: + raise HTTPException(status_code=404, detail="Authorization transaction not found") + + auth_net_response = payment_service.capture_authorized_transaction(transaction) + status, _, rejection_reason = _parse_authnet_response(auth_net_response) + + return crud.update_transaction_for_capture( + db=db, + auth_net_transaction_id=transaction.auth_net_transaction_id, + charge_amount=transaction.charge_amount, + status=status, + rejection_reason=rejection_reason + ) + +@router.get("/transaction/delivery/{delivery_id}", response_model=schemas.Transaction) +def get_transaction_by_delivery(delivery_id: int, db: Session = Depends(database.get_db)): + transaction = crud.get_transaction_by_delivery_id(db, delivery_id=delivery_id) + if not transaction: + raise HTTPException(status_code=404, detail="No pre-authorized transaction found for this delivery") + return transaction diff --git a/app/schemas.py b/app/schemas.py new file mode 100644 index 0000000..a5710da --- /dev/null +++ b/app/schemas.py @@ -0,0 +1,64 @@ +from pydantic import BaseModel +from typing import List, Optional +from datetime import datetime + +class TransactionBase(BaseModel): + preauthorize_amount: Optional[float] = None # Amount preauthorized (for auth transactions) + charge_amount: Optional[float] = None # Final charge amount (for charge/capture transactions) + transaction_type: int # 0 = charge, 1 = auth, 3 = capture - Required for database + service_id: Optional[int] = None # Reference to Service_Service.id + delivery_id: Optional[int] = None # Reference to Delivery_Delivery.id + card_id: Optional[int] = None # Reference to credit card used for payment + payment_gateway: int = 1 # 1 = Authorize.Net, 0 = Other + rejection_reason: Optional[str] = None # Detailed error message when payment is declined + +class TransactionCreate(TransactionBase): + charge_amount: float # Final charge amount + card_number: str + expiration_date: str # MM/YY + cvv: str + +class TransactionAuthorize(TransactionBase): + preauthorize_amount: float # Amount to preauthorize + card_number: str + expiration_date: str + cvv: str + +class TransactionCapture(BaseModel): + charge_amount: float # Amount to capture + auth_net_transaction_id: str + +class Transaction(TransactionBase): + id: int + transaction_type: int # 0 = charge, 1 = auth, 3 = capture + status: int # 0 = approved, 1 = declined + auth_net_transaction_id: Optional[str] = None + customer_id: int + + class Config: + orm_mode = True + +class CustomerBase(BaseModel): + account_number: Optional[str] = None + customer_last_name: Optional[str] = None + customer_first_name: Optional[str] = None + customer_town: Optional[str] = None + customer_state: Optional[int] = None + customer_zip: Optional[str] = None + customer_first_call: Optional[datetime] = None + customer_email: Optional[str] = None + customer_automatic: Optional[int] = None + customer_phone_number: Optional[str] = None + customer_home_type: Optional[int] = None + customer_apt: Optional[str] = None + customer_address: Optional[str] = None + company_id: Optional[int] = None + customer_latitude: Optional[str] = None + customer_longitude: Optional[str] = None + correct_address: Optional[bool] = None + +class Customer(CustomerBase): + id: int + + class Config: + orm_mode = True diff --git a/app/services/__pycache__/payment_service.cpython-39.pyc b/app/services/__pycache__/payment_service.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..466d3e2be8a6d19311a1de5b90b1eda56900d79a GIT binary patch literal 5426 zcmeHLO>7&-72erhE|*J+q)1A(C0pLaj%=m=OY_rIOlH)S|^t_0}|uYjRwoQ!uW>xFN@7T7mHlCHIx3>NjAO(p(~m1wj>4@s zfHU9b%#GLs?)o7xJAuE7^MU6v{*4N1Ad?T#jzSIJ7}3U{Lr%Dci&xS-nI?vZZRI&uD@+>x!!=; zQDBD=@Aw;+>`m9F9`tPQNzb!`hg}AHu=_mdGwy9)k?S;t99%$ovN|!)NyTS8?gp-2IgjA z4%Uuy&IzgkJsImjPepTaZikGF*w~wYst^U*(;%6r%E*j?4#=*`uf_T9nQ>p*t|O=0@`(t-DRGQO$&`uC#(NEc{9%C5`4 zmGt96_f?R67u)VRU!}#71^sityN#{nl}5$b`b?oyaS`m%M({fAlE-`gwaiMTRO`1{ zEn1cu>R1D`O1@4jBlDMv|CX*q=Xc5WKZr6a5oI_>tI=z5X@}sRQQ~8Gu}7s(O1_n} z*OY4tXip#7yUMR0**S0~J70vI<4veZ>r`$}tgDYbyQ@An{4z)_(gxlWI+HnC$4Q8(g+;)4xz>k*g)~q{-Hdoz#G~n5nUKpIMI~_Wl zUQLuZ3;Tf|vSnN3ZgC#);oJ&n2D~$5SDGxcQ#m;&@;!K@yTLey0im}8%90l^{|o<- zANfqr@3d`( zRy@1LacYM}+rhx2uvBC}WOm=>A*1lLOUFJf(M)%fF|_buvO%!70y`NCV6Yi30AuVf zdz}Y8`@mB^i}o*M=Q(S$&K7L$h#7p>aU<14;^p3wCh^e+b3nw`|j?q}m^%w`X zAK^0~RCU@e2JHvieI`mPch{WTcivxXI%_wD?z=rEDi7|hH1DshK3KccbUwKCu_$K( z)CX)^lpz7oPPFQBDiSZi->2@^F| z;rvB04W@Ec>)d~hN ztru>}jAKuS2yU74|IA_*z?vC$mKRBrEcm=NQaQ^O5#qn#oY;&v_~$(*De@C&=t(s6&1{N0 zlGz{yN_QUhF+T;GOBeYZt}+iq)H5P1oAvk^lvzM>7Kx4I6(p}BIfn!>2Y(I8B9aS8 zE+Sb%B4g|ium|u>;g^xfkb4z-*O0t{1S7D_4E#;(y@lj$B<~=>48VVcfFfc02V=cbu^I&WKbR%DXusTxZ)3hD=djCD$$wPN^u29NL*ok zVo6*9ZHg4?BXlH6^*?GT9>$@F*y;HTQQuD zb2iQau9#`I=KH)8a13V8f`=p)*3yYdEj$i^^M^y=SnMT3;aKfuL*X%cFBA%o(er~8 zgpAAo36RNz!q;CQ6t)xmbvSH(3o?-LGgVr$B@lRiNIZygSjovyCr)78aUc>qnX(e# zUJL5mb(z)HD1XsVI0@J<9~760vH<4qp=C1G{uF!Hk^BtF4J0c_evSk`mhhWMjs(($ z)EmwupwjHt{t7DLM?e&)Ka(oez#3Louisv^*3zogl2xnqysC8#)?S(wc?4{LwOgf$ zA>d_sLaq5M>&p1coBXmCCg?-qIao#*$@esF;Gh6NOT3?EDiih49zT6*Q#s(2b)8;%m4rY literal 0 HcmV?d00001 diff --git a/app/services/payment_service.py b/app/services/payment_service.py new file mode 100644 index 0000000..b67aa80 --- /dev/null +++ b/app/services/payment_service.py @@ -0,0 +1,235 @@ +import logging +from authorizenet import apicontractsv1 +from authorizenet.apicontrollers import createTransactionController +from .. import schemas +from config import load_config + +# Set up logging +logger = logging.getLogger(__name__) + +# Load Authorize.net credentials from config +ApplicationConfig = load_config() +API_LOGIN_ID = ApplicationConfig.API_LOGIN_ID +TRANSACTION_KEY = ApplicationConfig.TRANSACTION_KEY +# For sandbox, endpoint is https://apitest.authorize.net/xml/v1/request.api + +def safe_string_convert(value): + """ + Safely convert any value to string, handling lxml objects properly. + """ + if value is None: + return None + # Check if it's an lxml object with text attribute + if hasattr(value, 'text'): + return value.text + # Otherwise use standard string conversion + return str(value) + +def charge_credit_card(transaction: schemas.TransactionCreate): + logger.info(f"Processing charge for amount: {transaction.charge_amount}") + + merchantAuth = apicontractsv1.merchantAuthenticationType() + merchantAuth.name = API_LOGIN_ID + merchantAuth.transactionKey = TRANSACTION_KEY + + creditCard = apicontractsv1.creditCardType() + creditCard.cardNumber = transaction.card_number + creditCard.expirationDate = transaction.expiration_date + creditCard.cardCode = transaction.cvv + + payment = apicontractsv1.paymentType() + payment.creditCard = creditCard + + transactionRequest = apicontractsv1.transactionRequestType() + transactionRequest.transactionType = "authCaptureTransaction" + transactionRequest.amount = transaction.charge_amount # ✅ Fixed: Use charge_amount + transactionRequest.payment = payment + + createtransactionrequest = apicontractsv1.createTransactionRequest() + createtransactionrequest.merchantAuthentication = merchantAuth + createtransactionrequest.refId = "ref_id" # Optional reference ID + createtransactionrequest.transactionRequest = transactionRequest + + controller = createTransactionController(createtransactionrequest) + controller.execute() + + response = controller.getresponse() + + # Extract rejection reason if payment failed + rejection_reason = None + if response is not None and response.messages is not None: + logger.info(f"Charge response: {response.messages.resultCode}") + + # If payment was declined (resultCode is "Error"), extract error details + if response.messages.resultCode == "Error": + rejection_reason = "Authorize.Net Charge Error" + if hasattr(response.messages, 'message'): + try: + if len(response.messages.message) > 0: + for msg in response.messages.message: + if hasattr(msg, 'code') and hasattr(msg, 'text'): + # Convert lxml StringElement objects to Python strings + code_str = msg.code.text if msg.code else "Unknown" + text_str = msg.text.text if msg.text else "No details available" + rejection_reason = f"{code_str}: {text_str}" + break # Use the first error message + elif hasattr(msg, 'text'): + # Convert lxml StringElement to Python string + text_str = msg.text.text if msg.text else "No details available" + rejection_reason = f"Error: {text_str}" + break + else: + rejection_reason = "Charge declined - no specific error details available" + except Exception as e: + rejection_reason = f"Charge declined - error details could not be parsed: {str(e)}" + else: + rejection_reason = "Charge declined - no error message available" + + if hasattr(response.messages, 'message') and len(response.messages.message) > 0: + for msg in response.messages.message: + logger.info(f"Message: {msg.text.text if msg.text else 'No message text'}") + else: + logger.error("No response from Authorize.net") + rejection_reason = "No response received from Authorize.Net" + + # Attach rejection reason to response for the router to use + if response is not None: + response._rejection_reason = rejection_reason + + return response + +def authorize_credit_card(transaction: schemas.TransactionAuthorize): + logger.info(f"Processing preauthorization for amount: {transaction.preauthorize_amount}") + + merchantAuth = apicontractsv1.merchantAuthenticationType() + merchantAuth.name = API_LOGIN_ID + merchantAuth.transactionKey = TRANSACTION_KEY + + creditCard = apicontractsv1.creditCardType() + creditCard.cardNumber = transaction.card_number + creditCard.expirationDate = transaction.expiration_date + creditCard.cardCode = transaction.cvv + + payment = apicontractsv1.paymentType() + payment.creditCard = creditCard + + transactionRequest = apicontractsv1.transactionRequestType() + transactionRequest.transactionType = "authOnlyTransaction" + transactionRequest.amount = transaction.preauthorize_amount # ✅ Fixed: Use preauthorize_amount + transactionRequest.payment = payment + + createtransactionrequest = apicontractsv1.createTransactionRequest() + createtransactionrequest.merchantAuthentication = merchantAuth + createtransactionrequest.refId = "ref_id" + createtransactionrequest.transactionRequest = transactionRequest + + controller = createTransactionController(createtransactionrequest) + controller.execute() + + response = controller.getresponse() + + # Extract rejection reason if payment failed + rejection_reason = None + if response is not None and response.messages is not None: + logger.info(f"Preauthorization response: {response.messages.resultCode}") + + # If payment was declined (resultCode is "Error"), extract error details + if response.messages.resultCode == "Error": + rejection_reason = "Authorize.Net Error" + if hasattr(response.messages, 'message'): + try: + if len(response.messages.message) > 0: + for msg in response.messages.message: + if hasattr(msg, 'code') and hasattr(msg, 'text'): + # Convert lxml StringElement objects to Python strings + code_str = msg.code.text if msg.code else "Unknown" + text_str = msg.text.text if msg.text else "No details available" + rejection_reason = f"{code_str}: {text_str}" + break # Use the first error message + elif hasattr(msg, 'text'): + # Convert lxml StringElement to Python string + text_str = msg.text.text if msg.text else "No details available" + rejection_reason = f"Error: {text_str}" + break + else: + rejection_reason = "Payment declined - no specific error details available" + except Exception as e: + rejection_reason = f"Payment declined - error details could not be parsed: {str(e)}" + else: + rejection_reason = "Payment declined - no error message available" + + if hasattr(response.messages, 'message') and len(response.messages.message) > 0: + for msg in response.messages.message: + logger.info(f"Message: {msg.text.text if msg.text else 'No message text'}") + else: + logger.error("No response from Authorize.net for preauthorization") + rejection_reason = "No response received from Authorize.Net" + + # Attach rejection reason to response for the router to use + if response is not None: + response._rejection_reason = rejection_reason + + return response + +def capture_authorized_transaction(transaction: schemas.TransactionCapture): + merchantAuth = apicontractsv1.merchantAuthenticationType() + merchantAuth.name = API_LOGIN_ID + merchantAuth.transactionKey = TRANSACTION_KEY + + transactionRequest = apicontractsv1.transactionRequestType() + transactionRequest.transactionType = "priorAuthCaptureTransaction" + transactionRequest.amount = transaction.charge_amount # ✅ Fixed: Use charge_amount + transactionRequest.refTransId = transaction.auth_net_transaction_id + + createtransactionrequest = apicontractsv1.createTransactionRequest() + createtransactionrequest.merchantAuthentication = merchantAuth + createtransactionrequest.refId = "ref_id" + createtransactionrequest.transactionRequest = transactionRequest + + controller = createTransactionController(createtransactionrequest) + controller.execute() + + response = controller.getresponse() + + # Extract rejection reason if capture failed + rejection_reason = None + if response is not None and response.messages is not None: + logger.info(f"Capture response: {response.messages.resultCode}") + + # If capture was declined (resultCode is "Error"), extract error details + if response.messages.resultCode == "Error": + rejection_reason = "Authorize.Net Capture Error" + if hasattr(response.messages, 'message'): + try: + if len(response.messages.message) > 0: + for msg in response.messages.message: + if hasattr(msg, 'code') and hasattr(msg, 'text'): + # Convert lxml StringElement objects to Python strings + code_str = msg.code.text if msg.code else "Unknown" + text_str = msg.text.text if msg.text else "No details available" + rejection_reason = f"{code_str}: {text_str}" + break # Use the first error message + elif hasattr(msg, 'text'): + # Convert lxml StringElement to Python string + text_str = msg.text.text if msg.text else "No details available" + rejection_reason = f"Error: {text_str}" + break + else: + rejection_reason = "Capture declined - no specific error details available" + except Exception as e: + rejection_reason = f"Capture declined - error details could not be parsed: {str(e)}" + else: + rejection_reason = "Capture declined - no error message available" + + if hasattr(response.messages, 'message') and len(response.messages.message) > 0: + for msg in response.messages.message: + logger.info(f"Message: {msg.text.text if msg.text else 'No message text'}") + else: + logger.error("No response from Authorize.net for capture") + rejection_reason = "No response received from Authorize.Net for capture" + + # Attach rejection reason to response for the router to use + if response is not None: + response._rejection_reason = rejection_reason + + return response diff --git a/config.py b/config.py new file mode 100644 index 0000000..2b79c46 --- /dev/null +++ b/config.py @@ -0,0 +1,23 @@ +import os + +def load_config(mode=os.environ.get('MODE')): + + try: + print(f"mode is {mode}") + if mode == 'PRODUCTION': + from settings_prod import ApplicationConfig + return ApplicationConfig + + elif mode == 'LOCAL': + from settings_local import ApplicationConfig + return ApplicationConfig + + elif mode == 'DEVELOPMENT': + from settings_dev import ApplicationConfig + return ApplicationConfig + else: + pass + + except ImportError: + from settings_local import ApplicationConfig + return ApplicationConfig \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0bdfdfb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn +sqlalchemy +psycopg2-binary +pydantic +python-dotenv +authorizenet \ No newline at end of file diff --git a/settings_dev.py b/settings_dev.py new file mode 100644 index 0000000..d2edf9a --- /dev/null +++ b/settings_dev.py @@ -0,0 +1,34 @@ + + +class ApplicationConfig: + """ + Basic Configuration for a generic User + """ + CURRENT_SETTINGS = 'DEVELOPMENT' + # databases info + POSTGRES_USERNAME = 'postgres' + POSTGRES_PW = 'password' + POSTGRES_SERVER = '192.168.1.204' + POSTGRES_PORT = '5432' + POSTGRES_DBNAME00 = 'eamco' + SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME, + POSTGRES_PW, + POSTGRES_SERVER, + POSTGRES_DBNAME00 + ) + SQLALCHEMY_BINDS = {'eamco': SQLALCHEMY_DATABASE_URI} + + origins = [ + "http://localhost:9000", + "https://localhost:9513", + "http://localhost:9514", + "http://localhost:9512", + "http://localhost:9511", + "http://localhost:5173", # Frontend port + "http://localhost:9516", # Authorize service port + +] + + # Authorize.net credentials (Sandbox Test Credentials) + API_LOGIN_ID = '5KP3u95bQpv' + TRANSACTION_KEY = '346HZ32z3fP4hTG2' diff --git a/settings_local.py b/settings_local.py new file mode 100644 index 0000000..572bc99 --- /dev/null +++ b/settings_local.py @@ -0,0 +1,33 @@ + + +class ApplicationConfig: + """ + Basic Configuration for a generic User + """ + CURRENT_SETTINGS = 'LOCAL' + # databases info + POSTGRES_USERNAME = 'postgres' + POSTGRES_PW = 'password' + POSTGRES_SERVER = '192.168.1.204' + POSTGRES_PORT = '5432' + POSTGRES_DBNAME00 = 'auburnoil' + SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME, + POSTGRES_PW, + POSTGRES_SERVER, + POSTGRES_DBNAME00 + ) + + SQLALCHEMY_BINDS = {'auburnoil': SQLALCHEMY_DATABASE_URI} + + + origins = [ + "http://192.168.1.204:9000", + "http://192.168.1.204:9613", + "http://192.168.1.204:9614", + "http://192.168.1.204:9612", + "http://192.168.1.204:9611", +] + + # Authorize.net credentials + API_LOGIN_ID = 'bizdev05' + TRANSACTION_KEY = '4kJd237rZu59qAZd' diff --git a/settings_prod.py b/settings_prod.py new file mode 100644 index 0000000..18bf840 --- /dev/null +++ b/settings_prod.py @@ -0,0 +1,26 @@ +class ApplicationConfig: + """ + Basic Configuration for a generic User + """ + CURRENT_SETTINGS = 'PRODUCTION' + # databases info + POSTGRES_USERNAME = 'postgres' + POSTGRES_PW = 'password' + POSTGRES_SERVER = '192.168.1.204' + POSTGRES_PORT = '5432' + POSTGRES_DBNAME00 = 'auburnoil' + SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME, + POSTGRES_PW, + POSTGRES_SERVER, + POSTGRES_DBNAME00 + ) + SQLALCHEMY_BINDS = {'auburnoil': SQLALCHEMY_DATABASE_URI} + + origins = [ + "https://oil.edwineames.com", + "https://apiauto.edwineames.com", +] + + # Authorize.net credentials + API_LOGIN_ID = 'bizdev05' + TRANSACTION_KEY = '4kJd237rZu59qAZd'