11# payment/services.py
22
3+ from __future__ import annotations
4+
35import requests
46from dataclasses import dataclass
5- from typing import Optional , List , Dict
7+ from typing import Optional , List , Dict , Any
68from django .conf import settings
79from django .db import transaction
8-
910from django .contrib .auth import get_user_model
1011
1112from acm .exceptions import CustomAPIException
@@ -38,7 +39,7 @@ def _callback_url() -> str:
3839
3940def _unverified_list () -> List [Dict ]:
4041 """
41- Fetches the list of unverified authorities from Zarinpal.
42+ Fetch the list of unverified authorities from Zarinpal.
4243 Returns [] on non-100 or network error.
4344 """
4445 url = f"{ Z_BASE } /unVerified.json"
@@ -65,8 +66,6 @@ def _request_payment(
6566 "metadata" : {"email" : email , ** ({"mobile" : mobile } if mobile else {})},
6667 }
6768 r = requests .post (url , json = payload , headers = HEADERS , timeout = 20 )
68- # optional debug:
69- # print(f"status code: {r.status_code}, message: {r.text}")
7069 r .raise_for_status ()
7170 return r .json ()
7271
@@ -85,15 +84,18 @@ def initiate_payment_for_target(
8584 * ,
8685 user : User ,
8786 target_type : str ,
88- target_id : int ,
87+ target_id : str , # e.g. "PARENT_ID,CHILD_ID_1,CHILD_ID_2"
8988 amount : int ,
9089 description : str = "" ,
90+ extra_metadata : Optional [Dict [str , Any ]] = None , # attach {"reg_id": ..., "parent_course_id": ..., "child_course_ids":[...], ...}
9191) -> StartPayResult :
9292 """
9393 1) Check existing PENDING payments for the user; if any authority is still unverified at the gateway,
9494 attempt to verify it. If verification succeeds for SAME target, raise conflict.
9595 2) Create a new Zarinpal request and persist a PENDING Payment with the returned authority.
9696 3) Return StartPay URL.
97+
98+ NOTE: target_id is a string so we can store composite IDs (e.g., "42,101,102").
9799 """
98100 if not user or not user .is_authenticated :
99101 raise CustomAPIException (
@@ -128,7 +130,7 @@ def initiate_payment_for_target(
128130 p .card_hash = str (d .get ("card_hash" , "" ) or "" )
129131 p .save ()
130132 # conflict if same purchase already paid
131- if p .target_type == target_type and p .target_id == target_id :
133+ if p .target_type == target_type and str ( p .target_id ) == str ( target_id ) :
132134 raise CustomAPIException (
133135 code = EC .PAY_EXISTING_SUCCESS ,
134136 message = "Existing successful payment found for this purchase" ,
@@ -151,12 +153,12 @@ def initiate_payment_for_target(
151153 Payment .objects .create (
152154 user = user ,
153155 target_type = target_type ,
154- target_id = target_id ,
156+ target_id = str ( target_id ) ,
155157 amount = amount ,
156158 status = Payment .Status .PG_INITIATE_ERROR ,
157159 zarinpal_message = str (e ),
158160 description = description or "" ,
159- metadata = {"stage" : "request" , "exc" : str (e )},
161+ metadata = {"stage" : "request" , "exc" : str (e ), ** ( extra_metadata or {}) },
160162 )
161163 raise CustomAPIException (
162164 code = EC .PAY_INIT_FAILED ,
@@ -170,13 +172,13 @@ def initiate_payment_for_target(
170172 Payment .objects .create (
171173 user = user ,
172174 target_type = target_type ,
173- target_id = target_id ,
175+ target_id = str ( target_id ) ,
174176 amount = amount ,
175177 status = Payment .Status .PG_INITIATE_ERROR ,
176178 zarinpal_code = str (code ),
177179 zarinpal_message = d .get ("message" , "" ) or "" ,
178180 description = description or "" ,
179- metadata = {"stage" : "request" , "resp" : d },
181+ metadata = {"stage" : "request" , "resp" : d , ** ( extra_metadata or {}) },
180182 )
181183 raise CustomAPIException (
182184 code = EC .PAY_GATEWAY_REFUSED ,
@@ -188,12 +190,12 @@ def initiate_payment_for_target(
188190 pay = Payment .objects .create (
189191 user = user ,
190192 target_type = target_type ,
191- target_id = target_id ,
193+ target_id = str ( target_id ), # store as string
192194 amount = amount ,
193195 status = Payment .Status .PENDING ,
194196 authority = authority ,
195197 description = description or "" ,
196- metadata = {"fee_type" : d .get ("fee_type" ), "fee" : d .get ("fee" )},
198+ metadata = {"fee_type" : d .get ("fee_type" ), "fee" : d .get ("fee" ), ** ( extra_metadata or {}) },
197199 )
198200 startpay_url = f"https://payment.zarinpal.com/pg/StartPay/{ authority } "
199201 return StartPayResult (url = startpay_url , payment = pay )
@@ -204,6 +206,8 @@ def verify_by_authority(*, user: User, authority: str) -> Payment:
204206 """
205207 Verify a payment by authority for the given user (frontend passes authority after redirect).
206208 On success/failure, updates Payment and triggers domain hooks.
209+
210+ If metadata includes 'reg_id', finalize that registration (parent+children) immediately.
207211 """
208212 if not user or not user .is_authenticated :
209213 raise CustomAPIException (
@@ -222,10 +226,11 @@ def verify_by_authority(*, user: User, authority: str) -> Payment:
222226 )
223227
224228 if p .status != Payment .Status .PENDING :
225- # Already processed; return as-is (frontend can branch on status)
229+ # Already processed; return as-is
226230 return p
227231
228232 merchant = settings .ZARINPAL_MERCHANT_ID
233+
229234 try :
230235 res = _verify_payment (merchant_id = merchant , amount = p .amount , authority = authority )
231236 except requests .RequestException as e :
@@ -251,5 +256,22 @@ def verify_by_authority(*, user: User, authority: str) -> Payment:
251256 p .status = Payment .Status .FAILED
252257 p .save ()
253258 on_payment_failure (p )
259+ return p
260+
261+ # If we know the registration that initiated this payment, finalize it now.
262+ try :
263+ reg_id = (p .metadata or {}).get ("reg_id" )
264+ if reg_id :
265+ from presentations .services import set_status_final # local import to avoid circulars
266+ from presentations .models import Registration
267+ reg = (
268+ Registration .objects
269+ .select_for_update ()
270+ .select_related ("course" , "user" )
271+ .get (id = int (reg_id ), user = user )
272+ )
273+ set_status_final ([reg ])
274+ except Exception :
275+ pass
254276
255277 return p
0 commit comments