From a3a0db7dcd890f54ec02705f89b74a330ba14026 Mon Sep 17 00:00:00 2001 From: Rbiter Date: Tue, 10 Jun 2025 18:21:58 +0300 Subject: [PATCH] update --- backend/__pycache__/main.cpython-310.pyc | Bin 920 -> 1014 bytes .../v0/__pycache__/feedback.cpython-310.pyc | Bin 3422 -> 3457 bytes .../v0/__pycache__/payments.cpython-310.pyc | Bin 0 -> 6193 bytes .../api/v0/__pycache__/poster.cpython-310.pyc | Bin 6013 -> 6048 bytes .../api/v0/__pycache__/user.cpython-310.pyc | Bin 11464 -> 11499 bytes backend/api/v0/payments.py | 212 +++++++++++++++ .../core/__pycache__/crypt.cpython-310.pyc | Bin 1921 -> 1956 bytes backend/core/__pycache__/db.cpython-310.pyc | Bin 3313 -> 3310 bytes backend/core/__pycache__/file.cpython-310.pyc | Bin 1797 -> 1832 bytes backend/core/__pycache__/pay.cpython-310.pyc | Bin 0 -> 1953 bytes backend/core/db.py | 2 +- backend/core/pay.py | 52 ++++ backend/core/payment/payment_schemas.py | 24 ++ backend/database.db | Bin 45056 -> 45056 bytes backend/main.py | 2 + backend/pay.py | 39 --- backend/requirements.txt | 7 + frontend/src/app/payments/page.tsx | 172 +++++++++++++ frontend/src/components/modal/Poster.tsx | 130 +++++++++- frontend/src/components/modal/Profile.tsx | 241 ++++++++++++++---- tets.txt | 0 21 files changed, 782 insertions(+), 99 deletions(-) create mode 100644 backend/api/v0/__pycache__/payments.cpython-310.pyc create mode 100644 backend/api/v0/payments.py create mode 100644 backend/core/__pycache__/pay.cpython-310.pyc create mode 100644 backend/core/pay.py create mode 100644 backend/core/payment/payment_schemas.py delete mode 100644 backend/pay.py create mode 100644 backend/requirements.txt create mode 100644 frontend/src/app/payments/page.tsx delete mode 100644 tets.txt diff --git a/backend/__pycache__/main.cpython-310.pyc b/backend/__pycache__/main.cpython-310.pyc index 8d072aa6131ec761d6909b94426b182f092baa54..0520daa93689bd47dfc56101f2afb60dd2d31a8c 100644 GIT binary patch delta 289 zcmbQi{*9eCpO=@50SI2$dt^+U$SceEX`*&~ObSB^OO8M;N0cBVLn`9}p@j@Unx&Z` zN;p+`fk-OnLPj7T#Nvgs_~9%;2rEl8g)Nmkg*}C%mk~&E_A-O?WQk3@AkHnG!k5id zRG!L|C9(076eFYLWL~B{lKhDUnR;agdIgD?g#{NjT-bPF-^KO|J1*>0xY&5H>B7DXyJLz= qN>eIhk}`8rOY#d6QzpAI%gdMm&Ea9-VB|r8987F1T#S=zm}LM=_EZi4 delta 244 zcmeyyK7*Y%pO=@50SMmD*UvDY$SccuXQFm|eJbMufrSi=45=*53{irqf(wLFITtbl z`5+cAoW&1eWeKOSrE;gRr*QN#0!hwZW{}D(kyN%6u5`u}?i3y%3BtU+tUwvj6uxYx zqMTHwEU^^+6oC{$pbDWBVUS!3V=xGBmS<#UWE7vQ%(Q0`bC8xg(0xT9;+MX&RZM7c zYEf}aQBr0}YEg_!YH@Z+enE_5QhsTPZ|dav%<>WjK$ScU9E?0jkb{YhW%4a%835qT BIAs6; diff --git a/backend/api/v0/__pycache__/feedback.cpython-310.pyc b/backend/api/v0/__pycache__/feedback.cpython-310.pyc index 9031a0f62ccfa8ce260f677e915bd878c2a8452b..ec3d5d232f8692202adb89eab0186a57fb7f422a 100644 GIT binary patch delta 94 zcmca7)hNxK&&$ij00h4*JvMUpGP`_@0&<+KVnT~ki;82)auQR@V*K+`U5YZxQe!SG xxUk{E#tZu{wqMwBVW+~y#*0lC_FdQ=Q(RJ-QW=w!nUh+QUyzuxc_;HyP5_(qD9r!> delta 59 zcmZpaz9+?<&&$ij00dsk3^#K3GAsVFa<+;IElw>ejwwpYEJ-bjaY-%CF3B&5aZJiD NE%8m=e1~}{Cjc5O6hi<2 diff --git a/backend/api/v0/__pycache__/payments.cpython-310.pyc b/backend/api/v0/__pycache__/payments.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..848d9b57839eac64fdf94a408adb9dfac97f681c GIT binary patch literal 6193 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 o1Da(MoejwwpYEJ-bjaY-%CF3B&5aZJiD NE%8m=oWp7&1ONz267K*2 diff --git a/backend/api/v0/__pycache__/user.cpython-310.pyc b/backend/api/v0/__pycache__/user.cpython-310.pyc index 67667ab172e3023de41f8a6c0a79f68bc2776c66..80cc9fb28603b876885acd2508eb4f0167002280 100644 GIT binary patch delta 94 zcmX>R`8tw2pO=@50SJCsdTiu=%Ifkp49Ib|iU}=FEh>&F%SlWri}BA(bt%d$OO3g( x;KGIr8!zm;*nVNhg`El)8!t9p*mq%fOmRtRN@Yw^W=?8JenDc&W?Y5@KvDSiL| delta 59 zcmaDIc_NZKpO=@50SH1*=xyYF%BuLw%-Jd?v^ce>IHo8mvm~`B#wE2lyClCL#xW_s Nw8S@cvkBXFH2_?86&?Tp diff --git a/backend/api/v0/payments.py b/backend/api/v0/payments.py new file mode 100644 index 0000000..44339b1 --- /dev/null +++ b/backend/api/v0/payments.py @@ -0,0 +1,212 @@ +from fastapi import APIRouter, Depends, HTTPException, Body, Header, Request +from sqlalchemy.orm import Session +from core.db import get_db, Sale, User, Poster +from core.pay import YooKassaPayment +from pydantic import BaseModel, Field +from typing import Optional, List +from datetime import datetime +import json + +class PaymentRequest(BaseModel): + amount: float + currency: str + description: str + return_url: str + user_id: Optional[int] = Field(None) # Делаем необязательным + poster_id: Optional[int] = None + +router = APIRouter() + +# Конфигурация YooKassa +SHOP_ID = "1017909" +API_KEY = "test_udCBy7nXqGavVoj3RrzIRXN9UL02_UnF0FBBykxWH60" +payment_service = YooKassaPayment(SHOP_ID, API_KEY) + +@router.post("/create-payment/") +async def create_payment( + payment_data: dict = Body(...), # Принимаем любой JSON + db: Session = Depends(get_db) +): + try: + # Логирование для отладки + print(f"Received payment data: {json.dumps(payment_data, ensure_ascii=False)}") + + # Создание платежа через YooKassa + payment_response = payment_service.create_payment( + amount=float(payment_data.get("amount", 0)), + currency=payment_data.get("currency", "RUB"), + description=payment_data.get("description", ""), + return_url=payment_data.get("return_url", "http://localhost:3000/payments") + ) + + # Логирование ответа YooKassa + print(f"YooKassa response: {json.dumps(payment_response, ensure_ascii=False)}") + + # Сохранение данных о платеже в БД + new_sale = Sale( + amount=float(payment_data.get("amount", 0)), + status="pending", + description=payment_data.get("description", ""), + poster_id=payment_data.get("poster_id"), + user_id=payment_data.get("user_id", 1), # Используем 1 как значение по умолчанию + user_email=payment_data.get("user_email", "example@example.com"), + payment_id=payment_response["id"], + confirmation_url=payment_response["confirmation"]["confirmation_url"] + ) + db.add(new_sale) + db.commit() + db.refresh(new_sale) + + return {"confirmation_url": new_sale.confirmation_url, "payment_id": payment_response["id"]} + except Exception as e: + print(f"ERROR creating payment: {str(e)}") + raise HTTPException(status_code=400, detail=str(e)) + +@router.get("/payment-status/{payment_id}") +async def get_payment_status(payment_id: str, db: Session = Depends(get_db)): + try: + print(f"Checking payment status for ID: {payment_id}") + + # Получение информации о платеже из БД + sale = db.query(Sale).filter(Sale.payment_id == payment_id).first() + + if not sale: + raise HTTPException(status_code=404, detail=f"Платеж с ID {payment_id} не найден") + + # Обновление статуса из YooKassa + yookassa_status = payment_service.get_payment_status(payment_id) + if yookassa_status and "status" in yookassa_status: + # Маппинг статусов YooKassa на наши статусы + if yookassa_status["status"] == "succeeded" or yookassa_status["paid"] == True: + sale.status = "paid" + elif yookassa_status["status"] == "canceled": + sale.status = "canceled" + elif yookassa_status["status"] == "waiting_for_capture": + sale.status = "waiting_for_capture" + # Сохраняем обновленный статус + db.commit() + print(f"Updated payment status from YooKassa: {yookassa_status['status']} -> {sale.status}") + + return {"payment_id": sale.payment_id, "status": sale.status} + except Exception as e: + print(f"ERROR checking payment status: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/notifications/") +async def process_notification(request: Request, db: Session = Depends(get_db)): + try: + # Получаем тело запроса + notification_data = await request.json() + print(f"Received notification: {json.dumps(notification_data, ensure_ascii=False)}") + + # Проверяем тип события + event = notification_data.get("event") + payment_id = notification_data.get("object", {}).get("id") + + if not payment_id: + raise HTTPException(status_code=400, detail="Missing payment ID") + + # Находим платеж в БД + sale = db.query(Sale).filter(Sale.payment_id == payment_id).first() + if not sale: + raise HTTPException(status_code=404, detail=f"Payment {payment_id} not found") + + # Обновляем статус платежа в зависимости от события + if event == "payment.waiting_for_capture": + sale.status = "waiting_for_capture" + elif event == "payment.succeeded": + sale.status = "paid" + elif event == "payment.canceled": + sale.status = "canceled" + else: + print(f"Unknown event: {event}") + + db.commit() + return {"success": True} + except Exception as e: + print(f"ERROR processing notification: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/update-payment-status/") +async def manual_update_payment_status(payment_id: str, new_status: str, db: Session = Depends(get_db)): + try: + sale = db.query(Sale).filter(Sale.payment_id == payment_id).first() + if not sale: + raise HTTPException(status_code=404, detail="Платеж не найден") + + # Обновляем статус + sale.status = new_status + db.commit() + + return {"message": "Статус платежа обновлен", "payment_id": sale.payment_id, "status": sale.status} + except Exception as e: + print(f"ERROR updating payment status: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/user-tickets/{user_id}") +async def get_user_tickets(user_id: int, db: Session = Depends(get_db)): + """Получение всех оплаченных билетов пользователя""" + try: + # Проверяем существование пользователя + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="Пользователь не найден") + + # Получаем все успешно оплаченные билеты пользователя + sales = db.query(Sale).filter(Sale.user_id == user_id, Sale.status == "paid").all() + + result = [] + for sale in sales: + # Получаем информацию о мероприятии + poster = None + if sale.poster_id: + poster = db.query(Poster).filter(Poster.id == sale.poster_id).first() + + # Формирование даты покупки + purchase_date = sale.date if hasattr(sale, 'date') else datetime.now().isoformat() + + ticket_data = { + "id": sale.id, + "amount": sale.amount, + "description": sale.description, + "payment_id": sale.payment_id, + "purchase_date": purchase_date, + "poster": None + } + + if poster: + # Используем только те поля, которые точно есть в модели Poster + ticket_data["poster"] = { + "id": poster.id, + "title": poster.title, + "date": poster.date + # Убрали поле img, которого нет в модели + } + + result.append(ticket_data) + + return result + except HTTPException as he: + # Пробрасываем дальше HTTPException + raise he + except Exception as e: + print(f"ERROR getting user tickets: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + +# Для тестирования, также добавим временный эндпоинт для обновления статуса билета +@router.post("/update-ticket-status/{sale_id}") +async def update_ticket_status(sale_id: int, status: str, db: Session = Depends(get_db)): + """Временный эндпоинт для обновления статуса билета (для тестирования)""" + try: + sale = db.query(Sale).filter(Sale.id == sale_id).first() + if not sale: + raise HTTPException(status_code=404, detail="Билет не найден") + + sale.status = status + db.commit() + + return {"success": True, "message": f"Статус билета с ID {sale_id} обновлен на {status}"} + except Exception as e: + print(f"ERROR updating ticket status: {str(e)}") + raise HTTPException(status_code=500, detail=str(e)) + diff --git a/backend/core/__pycache__/crypt.cpython-310.pyc b/backend/core/__pycache__/crypt.cpython-310.pyc index db6e4b177e6c94846890caddf2ecd99032cfcee7..eed55b1be7c59e567a09b7c9a4c7444800adc8ca 100644 GIT binary patch delta 94 zcmZqVU&7Cw&&$ij00h4*JvMSTGr4>X1#+CNVnT~ki;82)auQR@V*K+`U5YZxQe!SG xxUk{E#tZu{wqMwBVW+~y#*0lC_FdQ=Q(RJ-QW=w!nUh+QUyzuxc|Fr5765skD2V_7 delta 59 zcmZ3&-^kCM&&$ij00dK>>22h0W>WlR>TDGgTAW%`98;8(S&~{53!`4ZD5762X}6ovo* diff --git a/backend/core/__pycache__/db.cpython-310.pyc b/backend/core/__pycache__/db.cpython-310.pyc index 5bd4b6b12018020c4e73bacee295b079c3d68d6b..7d376507535161de72a9f38a8e011cbe4280d054 100644 GIT binary patch delta 887 zcmaiy&ubGw6o5PVxn`3P*VYC+;cyE4Yzh_geR4Ors z&-$6mw?A#9?z7~icdzLPzEG`JEAkcl%?!C7DzQWIbLivMW0(qrUpEl-1@orI!85x} z%kG-jU49XqE;s$>{!4$u-|VgVul+ZL-lN`Qf75?!dZJ@LFjt%=7p=Bs%Malnr!!QM z5j4U$;@XS`6SO%@m?Rt_gb7hXR&GbmUpb1Qk+dzr8^A5$wA?^#7jl%GM`Uq%R9hK~ zurx~!-ZaJ74%I+VbXqTIOs+8yVQz>KODO_F}(eI#!eM__PFq8wq9c8t?rL1yp zuUL&cOOT?{ClGz(!B{gR$NJ(xsZKqD0>X#^P8CP6pOcOW=Wq?3LK;pr%`yyhc`)%+ zH{y+!>+*)!inhUV#SAkNgD-opN>1ifoz2NbHIJ8_t4($I!<_u0PO+N&t3JF?!Qa6w dAxWTn1w{h&2S^C!2=jy*VOS@Ka`IiW_76A_x8MK( delta 891 zcmaiy&ui0Q7{~YaN17#V+3KoRZSBfd*T{@3bFjk1S(zslWL`=^()6thTa)g|8@vb} zJj|o;{te<$1W(>P_(vGL2qJhZO}lJ}B}tRxSUp9<9NTOARm1}N{ikH&3f zPce3G1ADOF&tEQEun|&1B>jX;tKn)H17} zR#84n9hIy2{tl4fPz`ACSDbD%4p^b&nl#k%)^tNiQT6B*IvsP9N9v`ulT-{WyoQ&b zMW{0gmSs8oXW2p=j7ad0+H1Dd9_L+J`z~ZC`Z8i!u!v|WfLp^A zoaD5M1=sPKIK^@-jZSk6R%KTEY=kM>b6swWFgXCn6*U%W<0t=r3PoAajiq%A+o{D{ zniUAo_WFHCv}WntI|%%}Fwt@FW1iRY@`-+5pBNS8NBtt(l)v?-H?Lv@n1nQ;M5qvG WfI!ATU0{Q-NtonBr=on9UjGX#JiK85 diff --git a/backend/core/__pycache__/file.cpython-310.pyc b/backend/core/__pycache__/file.cpython-310.pyc index 30db6e66c505b8a97094c3d41683a2f9957b58fd..534af502ef8b08754d853bd95565aed78c321c74 100644 GIT binary patch delta 94 zcmZqWTfxVj&&$ij00h4*JvMSDGP!&W0dkzJVnT~ki;82)auQR@V*K+`U5YZxQe!SG xxUk{E#tZu{wqMwBVW+~y#*0lC_FdQ=Q(RJ-QW=w!nUh+QUyzuxxtFPe1psg|C-49O delta 59 zcmZ3%*UHD8&&$ij00h5YYi;CCWK#TP;%pTYTAW%`98;8(S&~{53!c>_}g3jixG6jT5J diff --git a/backend/core/__pycache__/pay.cpython-310.pyc b/backend/core/__pycache__/pay.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..468e018cc6fc8ce049078aa7edfb550767437f22 GIT binary patch literal 1953 zcma)6&yN&E6z;12F+IJrJIjj70kK7*Hp;9~LgL045Qz}XN&+63Ow#G?s%3k3rn{-G z-Y}Ei11yQg$X@{J?$H~>pa^lj8NKyf>*B!^e*l+FMBnS#aS=UONqtrCRn@C{?|rY5 z$w^0p_S@;xOF!2%?RONOY&rh62M0dsw=O*-% z(6bgmK?=>+3JORh-Ovki6C5t+P3=p4!`Rd|$%eV9!K%eHxRB0olR2*@(2g(wl(i6K z{Mb=dUpRJ@$6>;_vc@ChmFxt$T=HeI!edWYM#j5~y9U0|L<8(RIuCGiuJcL8Mb=sE z25hzSam+su(JJqh8|CftPPsMww7ggT;0~`2ua#TngH9%M*6%DtT`rR}V4Z~^ggaO# zOa$+wLBEyumF@dc97*4|;DQXnA{IGF2K%0hTx+~>URgme$sv|@mo$Y1)m#0KJ)A&XRcBdOygb(Dn4~#>pg> zK%^rV`YBhFCm?l+hz8Z(LGujby)=RKkRLhA`vbl8)}lw1xf*nHE~dfLwk+X z4GBiX45%-NX8=7@G+|JUFo_o<(W|VgS{S4dP%if1n%(0vV1Wzf|Ed|5lh86wy_izD(9j;=Bwplf=eFYC-&p<;hgeHy?%_{UHS>;o1f3z2<9V|DmMoctn~ zF2BYa9+x!^4WVfT&I4EZULGDwpQ|2SKlA+9;IztV!E2XP0+L*S2CY5&WJ Tvfmv&<~;)2b}ZRI-x>NBVW}vI literal 0 HcmV?d00001 diff --git a/backend/core/db.py b/backend/core/db.py index 62544ab..5b6da5d 100644 --- a/backend/core/db.py +++ b/backend/core/db.py @@ -3,7 +3,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from datetime import datetime -DATABASE_URL = "sqlite:///c:/Users/rbiter/Desktop/AboutMe/backend/database.db" +DATABASE_URL = "sqlite:///./database.db" engine = create_engine(DATABASE_URL) diff --git a/backend/core/pay.py b/backend/core/pay.py new file mode 100644 index 0000000..f9bb2eb --- /dev/null +++ b/backend/core/pay.py @@ -0,0 +1,52 @@ +import uuid +import requests +import base64 + +class YooKassaPayment: + def __init__(self, shop_id: str, api_key: str): + self.shop_id = shop_id + self.api_key = api_key + self.auth_token = base64.b64encode(f'{shop_id}:{api_key}'.encode()).decode() + + def create_payment(self, amount: float, currency: str, description: str, return_url: str, order_id: str = None): + idempotence_key = str(uuid.uuid4()) + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Basic {self.auth_token}', + 'Idempotence-Key': idempotence_key + } + data = { + "amount": { + "value": f"{amount:.2f}", + "currency": currency + }, + "confirmation": { + "type": "redirect", + "return_url": return_url + }, + "capture": True, + "description": description, + "metadata": { + "order_id": order_id or str(uuid.uuid4()) + } + } + response = requests.post("https://api.yookassa.ru/v3/payments", json=data, headers=headers) + if response.status_code in [200, 201]: + return response.json() + else: + raise Exception(f"Ошибка {response.status_code}: {response.text}") + + def get_payment_status(self, payment_id: str): + """Получение актуального статуса платежа из YooKassa API""" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Basic {self.auth_token}' + } + + response = requests.get(f"https://api.yookassa.ru/v3/payments/{payment_id}", headers=headers) + + if response.status_code == 200: + return response.json() + else: + print(f"Error getting payment status: {response.status_code} {response.text}") + return None diff --git a/backend/core/payment/payment_schemas.py b/backend/core/payment/payment_schemas.py new file mode 100644 index 0000000..6b7a3fc --- /dev/null +++ b/backend/core/payment/payment_schemas.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel, constr, condecimal +from typing import Optional + +class PaymentCreateSchema(BaseModel): + amount: condecimal(gt=0) # Amount must be greater than 0 + currency: constr(min_length=3, max_length=3) # Currency code must be 3 characters + description: str + return_url: str + capture: bool = True + metadata: Optional[dict] = None + +class PaymentResponseSchema(BaseModel): + id: str + status: str + confirmation_url: str + +class PaymentStatusSchema(BaseModel): + id: str + status: str + amount: condecimal(gt=0) + currency: constr(min_length=3, max_length=3) # Currency code must be 3 characters + description: str + created_at: str # ISO format date string + confirmation_url: Optional[str] = None \ No newline at end of file diff --git a/backend/database.db b/backend/database.db index 7ec9e2da471edc77e9791508b21d1cc6d811ce46..90b521a4c33b7148706e67d5d5dd48b5e8476ece 100644 GIT binary patch delta 1556 zcmcK4J#W)M7zc3MX@n98R)>lq3>J}~47TsizVnNOXw(69K%Fu$AmR9hR!w3nbrG48 zcwk@vs(b(>#71b#gGH`;B;CeTGywY{X5+jqis|&u?yd_t^CItoTG)85RA29~Pba{&&&3jZGUO z1p}y?3l~k__FtwP-GR#)J?e$!+ihVMLDfZ51Q1p+N<}ooU6Npu-e!fF_@tCc35w@$5Z+GKz{L>vnf5G-jhNxx*k#3vl^Iq`@1L3|-T7WLy*>6X}D&s2E!e=_TL slKr!Aa_eSXx|NHC^BX@iSom=G?a2YMduPs1xRa^mMhoW~zKb?~1JE%Y2mk;8 delta 50 zcmZp8z|`=7X@WFk>O>i5#?*}oOZ=G_1U3s!c+5X>f;bx&0}#NtN}IV0zS;u-u1OHf diff --git a/backend/main.py b/backend/main.py index 14bc02a..4bef8b5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -6,6 +6,7 @@ from api.v0.user import router as user_router from fastapi.staticfiles import StaticFiles from api.v0.poster import router as poster_router from api.v0.feedback import router as feedback_router +from api.v0.payments import router as payment_router db.initialize_database() @@ -15,6 +16,7 @@ app = FastAPI() app.include_router(user_router) app.include_router(poster_router) app.include_router(feedback_router) +app.include_router(payment_router) app.mount("/res", StaticFiles(directory="res"), name="res") diff --git a/backend/pay.py b/backend/pay.py deleted file mode 100644 index 3df97ce..0000000 --- a/backend/pay.py +++ /dev/null @@ -1,39 +0,0 @@ -import uuid -import requests -import base64 - -shop_id = '1017909' -api_key = 'test_udCBy7nXqGavVoj3RrzIRXN9UL02_UnF0FBBykxWH60' - -auth_token = base64.b64encode(f'{shop_id}:{api_key}'.encode()).decode() - -idempotence_key = str(uuid.uuid4()) - -headers = { - 'Content-Type': 'application/json', - 'Authorization': f'Basic {auth_token}', - 'Idempotence-Key': idempotence_key -} - -data = { - "amount": { - "value": "1500.00", - "currency": "RUB" - }, - "confirmation": { - "type": "redirect", - "return_url": "https://example.com/return" - }, - "capture": True, - "description": "Зеленый слоник 2", - "metadata": { - "order_id": str(uuid.uuid4()) - } -} - -response = requests.post("https://api.yookassa.ru/v3/payments", json=data, headers=headers) - -if response.status_code in [200, 201]: - print(response.json()["confirmation"]["confirmation_url"]) -else: - print(f"Ошибка {response.status_code}: {response.text}") diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..9fb13cc --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn +sqlalchemy +pydantic +python-multipart +PyJWT +requests \ No newline at end of file diff --git a/frontend/src/app/payments/page.tsx b/frontend/src/app/payments/page.tsx new file mode 100644 index 0000000..e79e262 --- /dev/null +++ b/frontend/src/app/payments/page.tsx @@ -0,0 +1,172 @@ +'use client'; +import React, { useEffect, useState, CSSProperties } from 'react'; +import Header from '@/components/Header'; +import Fotter from '@/components/Fotter'; +import { useSearchParams, useRouter } from 'next/navigation'; + +const styles = ` +.page-container { + display: flex; + flex-direction: column; + min-height: 100vh; + background: radial-gradient(circle at top right, rgba(30,30,30,0.8) 0%, rgba(15,15,15,0.8) 100%); + position: relative; + overflow-x: hidden; +} + +.page-container::before { + content: ''; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('/noise-texture.png'), linear-gradient(145deg, rgba(255,127,39,0.1) 0%, rgba(0,0,0,0) 70%); + opacity: 0.4; + pointer-events: none; + z-index: -1; +} + +.content-container { + flex: 1; + animation: fadeInUp 0.6s ease-out forwards; + width: 100%; +} + +.section-title { + color: #ff7f27; + font-size: 2.5rem; + margin-bottom: 1.5rem; + font-weight: bold; + background: linear-gradient(to right, #ff7f27, #ff5500); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: glowPulse 3s infinite; + text-align: center; + letter-spacing: -0.5px; +} + +.payment-status { + text-align: center; + margin: 2rem auto; + padding: 2rem; + border-radius: 1rem; + background: rgba(0,0,0,0.3); + border: 1px solid rgba(255,127,39,0.2); + color: rgba(255,255,255,0.9); + font-size: 1.2rem; +} + +.success-message { + color: #30d158; +} + +.error-message { + color: #ff453a; +} + +.loading-message { + color: rgba(255,255,255,0.7); +} +`; + +const mainContentStyle: CSSProperties = { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + width: '100%', + maxWidth: '90%', + margin: '0 auto', + flex: 1, + paddingTop: '76px', + paddingBottom: '30px', +}; + +export default function PaymentStatusPage() { + const searchParams = useSearchParams(); + const router = useRouter(); + const paymentId = searchParams.get('payment_id'); + const [status, setStatus] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const checkPaymentStatus = async () => { + // Пытаемся получить payment_id из разных источников + const urlPaymentId = searchParams.get('payment_id'); + const returnPaymentId = searchParams.get('return_payment_id'); + const storedPaymentId = typeof window !== 'undefined' ? localStorage.getItem('current_payment_id') : null; + + // Используем первый доступный ID + const finalPaymentId = urlPaymentId || returnPaymentId || storedPaymentId; + + console.log("Available payment IDs:", { urlPaymentId, returnPaymentId, storedPaymentId }); + + if (!finalPaymentId) { + setStatus('error'); + setError('ID платежа не найден'); + setLoading(false); + return; + } + + try { + console.log("Checking payment status for:", finalPaymentId); + const response = await fetch(`http://localhost:8000/payment-status/${finalPaymentId}`); + + if (!response.ok) { + throw new Error(`Ошибка ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + console.log("Payment status response:", data); + setStatus(data.status); + + // Если платеж завершен, перенаправляем через 5 секунд + if (data.status === 'paid') { + setTimeout(() => { + router.push('/'); // или на страницу с билетами пользователя + }, 5000); + } + } catch (error) { + console.error("Error fetching payment status:", error); + setError('Не удалось получить информацию о платеже'); + setStatus('error'); + } finally { + setLoading(false); + } + }; + + checkPaymentStatus(); + + // Проверяем статус каждые 5 секунд, если платеж ожидает оплаты + const intervalId = setInterval(() => { + if (status === 'pending' || status === 'waiting_for_capture') { + checkPaymentStatus(); + } + }, 5000); + + return () => clearInterval(intervalId); + }, [paymentId, router, status]); + + return ( +
+ +
+
+

Статус платежа

+ {loading ? ( +
Загрузка статуса платежа...
+ ) : status === 'paid' ? ( +
Ваш платеж успешно завершен!
+ ) : status === 'pending' ? ( +
Ваш платеж находится в ожидании подтверждения.
+ ) : status === 'failed' ? ( +
Ваш платеж не удалось завершить.
+ ) : ( +
Произошла ошибка. Платеж не найден.
+ )} +
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/modal/Poster.tsx b/frontend/src/components/modal/Poster.tsx index 05735fc..ab8ee9d 100644 --- a/frontend/src/components/modal/Poster.tsx +++ b/frontend/src/components/modal/Poster.tsx @@ -21,6 +21,7 @@ export default function PosterModal({ data, isOpen, onClose }: PosterModalProps) const [liked, setLiked] = useState(false); const [likesCount, setLikesCount] = useState(data.likes); const [loading, setLoading] = useState(false); + const [paymentLoading, setPaymentLoading] = useState(false); const [message, setMessage] = useState<{text: string, type: 'success' | 'error' | 'info' | null}>({ text: '', type: null @@ -124,6 +125,93 @@ export default function PosterModal({ data, isOpen, onClose }: PosterModalProps) setLoading(false); } }, [data.id, liked, loading]); + + // Функция для обработки платежа + const handlePayment = useCallback(async () => { + if (paymentLoading) return; + + try { + setPaymentLoading(true); + + // Получаем JWT токен из cookie + const token = document.cookie + .split('; ') + .find(row => row.startsWith('JWT_token=')) + ?.split('=')[1]; + + if (!token) { + setMessage({ + text: 'Необходимо авторизоваться для покупки билета', + type: 'error' + }); + setTimeout(() => setMessage({text: '', type: null}), 3000); + return; + } + + try { + // Получаем данные пользователя из JWT + const payload = JSON.parse(atob(token.split('.')[1])); + console.log("JWT payload:", payload); + + // ID пользователя может храниться в разных полях JWT + const userId = payload.sub || payload.user_id || payload.id || 1; + console.log("User ID:", userId); + + // Создание запроса на оплату + const response = await fetch('http://localhost:8000/create-payment/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + amount: data.price, + currency: 'RUB', + description: `Билет на мероприятие: ${data.title}`, + return_url: `${window.location.origin}/payments`, + user_id: userId, + poster_id: data.id + }) + }); + + console.log("Payment response status:", response.status); + const responseData = await response.json(); + console.log("Payment response data:", responseData); + + if (response.ok && responseData.confirmation_url) { + // Сохраняем payment_id в localStorage перед перенаправлением + if (responseData.payment_id) { + localStorage.setItem('current_payment_id', responseData.payment_id); + } + + // Добавляем payment_id в URL вручную + window.location.href = `${responseData.confirmation_url}&return_payment_id=${responseData.payment_id}`; + } else { + setMessage({ + text: responseData.detail || 'Произошла ошибка при создании платежа', + type: 'error' + }); + setTimeout(() => setMessage({text: '', type: null}), 3000); + } + } catch (parseError) { + console.error('Ошибка при обработке JWT:', parseError); + setMessage({ + text: 'Ошибка авторизации. Пожалуйста, войдите заново.', + type: 'error' + }); + setTimeout(() => setMessage({text: '', type: null}), 3000); + } + } catch (error) { + console.error('Ошибка при создании платежа:', error); + setMessage({ + text: 'Не удалось создать платёж. Попробуйте позже.', + type: 'error' + }); + setTimeout(() => setMessage({text: '', type: null}), 3000); + } finally { + setPaymentLoading(false); + } + }, [data.id, data.price, data.title, paymentLoading]); if (!isOpen) return null; @@ -416,22 +504,48 @@ export default function PosterModal({ data, isOpen, onClose }: PosterModalProps) border: 0, borderRadius: 24, fontWeight: 'bold', - cursor: 'pointer', + cursor: paymentLoading ? 'wait' : 'pointer', color: '#000', transition: 'all 0.3s', - boxShadow: '0 2px 10px rgba(255,127,39,0.4)' + boxShadow: '0 2px 10px rgba(255,127,39,0.4)', + display: 'flex', + alignItems: 'center', + gap: 8, + opacity: paymentLoading ? 0.8 : 1 }} - onClick={() => alert('Функция покупки билетов будет доступна в ближайшее время!')} + onClick={handlePayment} + disabled={paymentLoading} onMouseOver={(e) => { - e.currentTarget.style.transform = 'translateY(-2px)'; - e.currentTarget.style.boxShadow = '0 4px 15px rgba(255,127,39,0.5)'; + if (!paymentLoading) { + e.currentTarget.style.transform = 'translateY(-2px)'; + e.currentTarget.style.boxShadow = '0 4px 15px rgba(255,127,39,0.5)'; + } }} onMouseOut={(e) => { - e.currentTarget.style.transform = 'translateY(0)'; - e.currentTarget.style.boxShadow = '0 2px 10px rgba(255,127,39,0.4)'; + if (!paymentLoading) { + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = '0 2px 10px rgba(255,127,39,0.4)'; + } }} > - Купить билет + {paymentLoading ? ( + <> + + + + + Обработка... + + ) : ( + 'Купить билет' + )} diff --git a/frontend/src/components/modal/Profile.tsx b/frontend/src/components/modal/Profile.tsx index 3de8fde..efa6997 100644 --- a/frontend/src/components/modal/Profile.tsx +++ b/frontend/src/components/modal/Profile.tsx @@ -1,7 +1,71 @@ 'use client'; import { CSSProperties, useState, useEffect } from 'react'; -// Улучшенные стили с градиентами и эффектами +// Обновите стили CSS для прокрутки +const styles = ` +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes profileCardAppear { + from { opacity: 0; transform: scale(0.95); } + to { opacity: 1; transform: scale(1); } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Стилизация скроллбара */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.3); + border-radius: 10px; + margin: 5px; +} + +::-webkit-scrollbar-thumb { + background: linear-gradient(145deg, #ff7f27, #e96c00); + border-radius: 10px; + border: 2px solid rgba(0, 0, 0, 0.3); +} + +::-webkit-scrollbar-thumb:hover { + background: linear-gradient(145deg, #ff9540, #ff7f27); +} + +::-webkit-scrollbar-corner { + background: transparent; +} + +.loading-spinner { + animation: spin 1s linear infinite; +} + +.tab-content-scroll { + max-height: calc(65vh - 280px); /* Динамическая высота на основе высоты окна */ + overflow-y: auto; + padding-right: 10px; /* Увеличиваем отступ для скроллбара */ + padding-bottom: 20px; /* Добавляем отступ снизу */ + -webkit-overflow-scrolling: touch; /* Плавная прокрутка на iOS */ +} + +.ticket-card { + animation: fadeIn 0.5s forwards; +} + +.ticket-card:last-child { + margin-bottom: 20px; /* Дополнительный отступ для последнего билета */ +} +`; + +// Существующие стили... const wrapper: CSSProperties = { position: 'fixed', inset: 0, @@ -20,15 +84,18 @@ const modal: CSSProperties = { background: 'linear-gradient(145deg, #0a0a0a, #1a1a1a)', border: '1px solid #ff7f27', borderRadius: 24, - padding: 40, - minWidth: 700, // Увеличено для более широкого профиля + padding: '40px 40px 20px 40px', // Уменьшаем нижний отступ + minWidth: 700, maxWidth: '90vw', - width: '100%', // Добавлено для лучшего контроля ширины + width: '100%', color: '#ff7f27', boxShadow: '0 8px 40px rgba(255,127,39,.25), 0 0 0 1px rgba(255,127,39,0.1)', transformOrigin: 'center', transition: 'transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)', animation: 'profileCardAppear 0.4s forwards', + maxHeight: '85vh', + display: 'flex', + flexDirection: 'column', }; const profileHeader: CSSProperties = { @@ -125,9 +192,14 @@ const tabActive: CSSProperties = { background: 'rgba(255,127,39,0.1)', }; +// Обновленный стиль для контента вкладок const tabContent: CSSProperties = { minHeight: 200, - padding: '20px 0', + padding: '20px 0 0 0', // Убираем нижний padding + flex: 1, + overflow: 'hidden', + display: 'flex', + flexDirection: 'column', }; const settingsButton: CSSProperties = { @@ -254,6 +326,20 @@ interface UserData { full_name?: string; // Для обратной совместимости } +interface Ticket { + id: number; + amount: number; + description: string; + payment_id: string; + purchase_date: string; + poster: { + id: number; + title: string; + date: string; + img: string; + } | null; +} + export default function ProfileModal({ user, onClose, @@ -265,22 +351,23 @@ export default function ProfileModal({ }) { const [activeTab, setActiveTab] = useState('tickets'); const [userData, setUserData] = useState(null); - // Removed unused loading state + const [tickets, setTickets] = useState([]); + const [ticketsLoading, setTicketsLoading] = useState(true); // Получение ID пользователя из cookie const userId = document.cookie .split('; ') .find(row => row.startsWith('id=')) ?.split('=')[1]; - - // Получение токена из cookie (removed unused token variable) - // Загрузка полных данных пользователя при открытии профиля + // Загрузка данных пользователя и билетов useEffect(() => { if (userId) { fetchUserData(userId); + fetchUserTickets(userId); } else { setLoading(false); + setTicketsLoading(false); } }, [userId]); @@ -309,6 +396,28 @@ export default function ProfileModal({ } }; + // Новая функция для загрузки билетов + const fetchUserTickets = async (id: string) => { + try { + const response = await fetch(`http://localhost:8000/user-tickets/${id}`, { + method: 'GET', + credentials: 'include', + }); + + if (response.ok) { + const data = await response.json(); + console.log("Получены билеты пользователя:", data); + setTickets(data); + } else { + console.error("Ошибка при получении билетов"); + } + } catch (error) { + console.error("Ошибка запроса билетов:", error); + } finally { + setTicketsLoading(false); + } + }; + // Функция форматирования полного имени const formatName = () => { if (userData) { @@ -328,6 +437,8 @@ export default function ProfileModal({ style={wrapper} onClick={(e) => e.target === e.currentTarget && onClose()} > + {/* Добавляем тег style с нашими стилями */} +