
    i<                       d dl mZmZmZmZmZ d dlmZmZ d dl	m
Z
mZ d dlmZmZmZmZmZmZmZ d dlmZmZ d dlmZmZ d dlZd dlZd dlZd dlZd dlZd dlZd dlZd dl Z d dl!Z! ede"      Z# ejH                  e"      Z%e!jL                  jO                  d	      d
ddddZ(e#jS                  ddg      d        Z*e#jS                  ddg      d        Z+d Z,e#jS                  ddg      d        Z-e#jS                  ddg      d        Z.e#jS                  ddg      d        Z/d Z0de1fdZ2dGde1fd Z3e#jS                  d!dg      d"        Z4d# Z5dGd$e6d%ed&e1dz  fd'Z7e#jS                  d(dg      d)        Z8d* Z9d+ Z:d$e6d,e6fd-Z;e#jS                  d.dg      d/        Z<e#jS                  d0dg      d1        Z=e#jS                  d0d2g      d3        Z>e#jS                  d4dg      d5        Z?e#jS                  d6dg      d7        Z@deAfd8ZBd9 ZCd:eDde6fd;ZEde1fd<ZFde6fd=ZGde1fd>ZHd?eDdz  deAfd@ZIdA ZJdB ZKe#jS                  dC      dD        ZLe#j                  dE      dF        ZNy)H    )	Blueprintrequestjsonifysessioncurrent_app)login_requiredcurrent_user)dbUser)ProductSubscriptionPaymentSubscriptionStatusPaymentStatusPaymentMethodUserProfilePaymentHistoryr   )datetime	timedeltaNsubscriptionTOSS_API_KEYz#https://pay.toss.im/api/v2/paymentsz"https://pay.toss.im/api/v2/refundsz!https://pay.toss.im/api/v2/statusz"https://pay.toss.im/api/v2/execute)api_keybase_url
refund_url
status_urlexecute_urlz/api/subscription/productsGET)methodsc                     	 t         j                  j                  dd      } | dk(  r0t        j                  j                  dd      j                         }nc| dk(  r0t        j                  j                  dd      j                         }n.t        j                  j                  d      j                         }d }t        |D cg c]c  }|j                  |j                  |j                  |j                  |j                  |j                  |j                   ||j                        de c}      d	fS c c}w # t         $ r7}t"        j%                  d
       t        dt'        |      i      dfcY d}~S d}~ww xY w)u   상품 목록 조회billing_cyclemonthlyT)	is_activer!   yearly)r#   c                     | sg S 	 t        j                  |       }t        |t              r|S g S # t         j                  t
        f$ r'}t        j                  d| d|         g cY d}~S d}~ww xY w)u:   features JSON 파싱 (파싱 실패 시 빈 배열 반환)u   features JSON 파싱 실패: u   , 원본 데이터: N)jsonloads
isinstancelistJSONDecodeError	TypeErrorloggerwarning)features_stringparsedes      L/home/kdj-ubuntu1/mlink_AI_Server/mlink-backend/./src/routes/subscription.pysafe_parse_featuresz)get_products.<locals>.safe_parse_features+   sn    "	O4!+FD!9vArA(()4 !>qcAUVeUfgh	s   &/ / A/A*$A/*A/)idnamedescriptionpricecurrencyr!   
trial_daysfeatures   zget_products failederror  N)r   argsgetr   query	filter_byallr   r3   r4   r5   r6   r7   r!   r8   r9   	Exceptionr,   	exceptionstr)r!   productsr2   pr0   s        r1   get_productsrG      sP   "/(()D I%}}..Y.W[[]Hh&}}..X.VZZ\H}}...>BBDH		  	 a $$FF==WW

__,,+AJJ7	
 	 	  #	# 		# 	  /./Q()3../s1   CD< A(D7.D< 7D< <	E<,E71E<7E<z/api/subscription/subscribePOSTc            
         	 t        j                  d      } | st        ddi      dfS t        j                  dd      xs i }|j                  d      }|st        ddi      d	fS t
        j                  j                  |      }t        j                         }|j                  xs d
d
kD  r,|t        |j                        z   }|}t        j                  }n2d}|t        |j                  dk(  rdnd      z   }t        j                  }t!        | |||||dd      }t"        j                   j%                  |       t"        j                   j'                          t        |j(                  |j*                  j,                  |j.                  j1                         d      dfS # t2        $ rU}	t"        j                   j5                          t6        j9                  d       t        dt;        |	      i      dfcY d}	~	S d}	~	ww xY w)uK   구독 생성 (결제 없이 선 생성 가능) — auto_renew 비활성화user_idr;      로그인이 필요합니다.  Tforcesilent
product_idu   상품 ID가 필요합니다.  r   daysNr"      m  F)rJ   rP   status
start_dateend_datetrial_end_date
auto_renewnext_billing_date)subscription_idrV   rX      zcreate_subscription failedr<   )r   r>   r   r   get_jsonr   r?   
get_or_404r   nowr8   r   r   TRIALr!   ACTIVEr   r
   addcommitr3   rV   valuerX   	isoformatrB   rollbackr,   rC   rD   )
rJ   datarP   productrW   rY   rX   rV   r   r0   s
             r1   create_subscriptionrj   G   s   ,/++i(G%DEFKKd48>BXXl+
G%DEFKK--**:6\\^
#!q(')9K9K*LLN%H'--F!N!I9N9NR[9[2ad$eeH'..F#!!)"	
 	

|$


+"))//$--779
  	 	  /


56Q()3../s*   %F  <F  %D:F   	G>)A
G93G>9G>c                 0   	 |dk(  rbt         j                  | _        d| _        d| _        | j
                  }|j                  t        j                  k(  rt        j                  |_        n|dk(  r^t         j                  | _        d| _        d| _        | j
                  }t        j                  |_        t        j                         |_        nI|dk(  rDt         j                  | _        d| _        d	| _        | j
                  }t        j                  |_        t        j                         | _        t         j"                  j%                          y# t&        $ r?}t         j"                  j)                          t+        d
t-        |              Y d}~yd}~ww xY w)u   결제 상태 변경 처리successSUCCESSu   결제 완료	cancelled	CANCELLEDu   결제 취소failedFAILEDu   결제 실패u$   결제 상태 변경 처리 실패: N)r   	COMPLETEDrV   pg_response_codepg_response_messager   r   ra   rb   ro   r   r`   cancelled_atrq   EXPIRED
updated_atr
   r   rd   rB   rg   printrD   )paymentrV   webhook_datar   r0   s        r1   handle_payment_status_changer{   y   s?   #?Y*44GN'0G$*9G' #//L""&8&>&>>&8&?&?#{"*44GN'2G$*9G' #//L"4">">L(0L%x*11GN'/G$*9G' #//L"4"<"<L%\\^


 ?


4SVH=>>?s   E
E 	F5FFz!/api/subscription/my-subscriptionc                  (   	 t        j                  d      } | sd} t        j                  j	                  | t
        j                        j                  t        j                  j                               j                         }|st        ddi      dfS t        d|j                  |j                  j                  |j                  r-|j                  j!                  d      j#                         dz   nd|j$                  r-|j$                  j!                  d      j#                         dz   nd|j&                  r-|j&                  j!                  d      j#                         dz   nd|j(                  |j*                  r-|j*                  j!                  d      j#                         dz   nd|j,                  r|j,                  j                  nd|j.                  |j0                  |j2                  |j4                  j                  |j4                  j6                  |j4                  j8                  |j4                  j2                  d	d
i      dfS # t:        $ r7}t<        j?                  d       t        dtA        |      i      dfcY d}~S d}~ww xY w)u   내 구독 정보 조회rJ      rJ   rV   r   Nr:   )tzinfoZ)r3   r4   r6   r!   )r3   rV   rW   rX   rY   rZ   r[   payment_method
payment_idamountr!   ri   zget_my_subscription failedr;   r<   )!r   r>   r   r?   r@   r   rb   order_by
created_atdescfirstr   r3   rV   re   rW   replacerf   rX   rY   rZ   r[   r   r   r   r!   ri   r4   r6   rB   r,   rC   rD   rJ   r   r0   s      r1   get_my_subscriptionr      sT   %/++i(G#))33GL^LeLe3foo##((*

%' 	 ND12C77"oo&--33`l`w`wl55==T=JTTVY\\  ~B\h\q\qL11999FPPRUXXw{ht  iD  iD,"="="E"ET"E"R"\"\"^ad"d  JN*55nz  oM  oM\%C%C%K%KSW%K%X%b%b%dgj%j  SWGSGbGb,"="="C"Chl*55&--!-!;!;&..11(0055)1177%1%9%9%G%G	
 ( ) 	,  /56Q()3../s%   BI F<I 	J,JJJz'/api/subscription/fix-next-billing-datec                     	 t        j                  d      } | sd} t        j                  j	                  | t
        j                        j                         }|st        ddi      dfS |j                  |_
        t        j                   j                          t        d|j                  |j                  j                         |j                  j                         d      d	fS # t        $ rU}t        j                   j!                          t"        j%                  d
       t        dt'        |      i      dfcY d}~S d}~ww xY w)u<   기존 구독의 next_billing_date 수정 (임시 수정용)rJ   r}   r~   r;   u)   활성 구독을 찾을 수 없습니다.  T)rl   r\   r[   rX   r:   zfix_next_billing_date failedr<   N)r   r>   r   r?   r@   r   rb   r   r   rX   r[   r
   rd   r3   rf   rB   rg   r,   rC   rD   r   s      r1   fix_next_billing_dater      s    /++i(G#))33%,, 4 
 %' 	
 G%PQRTWWW *6)>)>&


+!-!?!?!I!I!K$--779	
 
  	  /


78Q()3../s&   A&C$ )A:C$ $	E-A
D=7E=Ez/api/subscription/cancelc                  6   	 t        j                  d      } | st        ddi      dfS t        j                  j                  | t        j                        j                         }|st        ddi      dfS g }t        j                  d|j                   d	|j                          d
dd}d}	 ddlm} |j                  j                  |j                  t        j                         j#                  |j$                  j'                               j                         }|r=t        j                  d|j(                   d|j*                   d|j$                          n#t        j-                  d|j                   d       |j                  t4        j6                  k(  r|r	 ddlm}m} |j                  j                  |j                  |j                         j#                  |j$                  j'                               j9                         }|s t        j                  d       ddddd}	d}
nt;        |      }	|	d   }t        j                  d|	        d}g }|D ]  }t=        |j*                  xs d      }|dk  r#ddl}ddl }d |jB                         jD                  dd  }t=        |dz        }||z
  }tF        jH                  j                  d      |j(                  |||d|dd d!	} |jJ                  tL        d"   d#d$i|d%&      }|jN                  d'k(  r_||z  }|jP                  |_)        d(| |_*        tV        j                   jY                          |j[                  |j                  ||d)       t        j1                  d*|j                   d+|jN                   d,|j\                          t        d
d-|j                  |j\                  d.      d/fc S  d}
|	d   dkD  rt_        |j`                  ||d   |	d   0      }
t        jb                  |_)        d|_2        tg        jh                         |_5        tg        jh                         |_6        tV        j                   jY                          	 dd1l7m8} |j                  j                  |j`                        }|r%d2|_9        tV        j                   jY                          t        d4d5d6|rtu        d7 |D              ndd8|	j                  dd      |
xs i j                  d9      |
xs i j                  d:      |
xs i j                  d;      |	j                  d<      |	j                  d=      |	j                  d>      d?d@dA      d'fS t        d4dD|dE      d'fS # t.        $ r,}t        j1                  dt3        |              Y d}~6d}~ww xY w# t.        $ r,}t        j-                  d3t3        |              Y d}~"d}~ww xY w# t.        $ r7}t        jw                  dB       t        dt3        |      i      dCfcY d}~S d}~ww xY w# t.        $ rU}tV        j                   jy                          t        jw                  dF       t        dt3        |      i      dCfcY d}~S d}~ww xY w)Gu&   구독 취소 — 환불 처리 포함rJ   r;   rK   rL   r~      활성 구독이 없습니다.r   '   구독 정보 확인 - payment_method: , subscription_id: Fu   환불 처리 로직 미실행rl   r;   Nr   r   r\   rV   "   최근 결제 발견: payment_key=	, amount=, created_at=   구독 )   의 완료된 결제를 찾을 수 없음!   최근 결제 조회 중 오류: r   uA   완료된 결제 없음 — 사용분 정산만 0원으로 처리
total_paid
total_days	used_daysusage_charger   [CancelChain] usage_meta=refund_   皙?r   u   반품 취소(전체 환불)	apiKeypayTokenrefundNor   amountTaxableamountTaxFree	amountVatamountServiceFeereasonr   Content-Typeapplication/jsonrT   headersr&   timeoutr:      환불 완료 - )r   	refund_nor   u#   [CancelChain] 환불 실패: ph_id=z, resp= u   일부 환불 실패(중단).rl   r;   failed_payment_idrawrQ   rJ   r   latest_paymentr   )r   	role_freeu   권한 회수 실패(무시): Tu[   구독이 취소되었고, 전체 환불 후 사용분 정산 결제를 준비했습니다.
full_chainc              3   N   K   | ]  }t        |j                  xs d         ywr   Nintr   .0phs     r1   	<genexpr>z&cancel_subscription.<locals>.<genexpr>}  s     +Y.BC		Q,?.   #%)modetotal_refundorderIdpayUrl
simulationr   r   r   r   r   r   r   r   r   r   meta)rl   messagerefundusage_settlementu+   [CancelChain] 전체 환불 + 정산 실패r<   u0   구독이 성공적으로 취소되었습니다.rl   r   r   u%   기간 연장형 구독 취소 실패)=r   r>   r   r   r?   r@   r   rb   r   r,   infor   r3   src.models.paymentr   r   rr   r   r   r   payment_keyr   r-   rB   r;   rD   r   TOSS_PAYMENTSrA   _calc_usage_charge_for_chainr   uuidrequestsuuid4hexosenvironpostTOSS_CONFIGstatus_codeREFUNDEDrV   r5   r
   rd   appendtext!_prepare_usage_settlement_paymentrJ   ro   r[   r   r`   rX   rw   src.models.userr   rolesumrC   rg   )rJ   r   refund_resultsrefund_resultr   r   r0   PHStatuscompleted_list
usage_metausage_paymentr   refund_totalrefund_detailsr   refund_amountr   r   r   amount_taxable
amount_vatrefund_datarr   us                            r1   cancel_subscriptionr      s_   k/++i(G%DEFKK#))33%,, 4 
 %' 	
 G%EFGLL =l>Y>Y=ZZmnzn}n}m~  	A 5
 	G9+11;; ,$.. <  h~005578 
 @A[A[@\\eftf{f{e|  }J  KY  Kd  Kd  Je  f  g(99bcd &&-*E*EE.t7X #1"6"6#,9\__U]UgUg9#h#+8N,E,E,J,J,L#M#&35 
 &KK cd01QRde!fJ$(M ">n!MJ#-n#=LKK";J< HI $%L%'N,(+BIIN(;(A-$-&-jdjjl.>.>s.C-D$E	),]S-@)A%2^%C
 ')jjnn^&D(*(1&3-;-.)301&D
' *HMM+l*C3ACU2V/:BH ==C/(M9L(0(9(9BI/?	{-KBNJJ--/*11.0ee-6*73  #LL+NruugU\]^]j]j\kklmnmsmslt)uv#*+0)H57UU'(vv	, $
 !$$$ $K -Z %)M!.1A5(I$0$8$8)5+9!+<)3N)C	) '9&B&B#15.(0%*2,,.'

!!#N4

|';';<A!,

))+
 #| ,]k+Y.+Y(Yqr
 #-.."C$1$7R#<#<Y#G#0#6B";";H"E'4':&?&?&M*4..*F*4..*F)3)D!
)  $ % 4 I#
  	 	{  	GLL<SVHEFF	G~ ! NNN%CCF8#LMMN0  7  !NOQ013667  /


@AQ()3../s   %X: AX: 68X: /CV !X: #I W7 $BW7 ;AV? B(W7 6X: 	V<!V71X: 7V<<X: ?	W4!W/)W7 /W44W7 7	X7 ,X2,X7-X: 2X77X: :	ZA
ZZZc                 `   	 | j                   r| j                  syt        j                         }| j                   }| j                  }||k  r<t        j                  d| j                   d| j                   d       | j                  S ||k\  r$t        j                  d| j                   d       y||z
  j                  }||z
  j                  }|dk  ry| j                  }t        j                  d| j                   d       t        j                  d| j                   d       t        j                  d| d	|        t        j                  d
| d| d       t        j                  d| d       |S # t        $ r+}t        j                  dt        |              Y d}~yd}~ww xY w)u?   기간 연장형 모델 - 특정 결제의 환불 금액 계산r   u   결제 u)    - 기간 미시작으로 전액 환불: u   원u!    - 기간 만료로 환불 불가u2    환불 계산 (토스페이먼츠 전체 환불):u     - 결제 금액: u     - 구독 기간: z ~ u     - 남은 일수: u
   일 / 총 u   일u     - 환불 금액: u   원 (전체 금액 환불)u   환불 금액 계산 실패: N)subscription_start_datesubscription_end_dater   r`   r,   r   r3   r   rS   rB   r;   rD   )payment_historyr`   rW   rX   remaining_daysr   r   r0   s           r1   #calculate_refund_amount_for_paymentr     s   (66o>c>clln$<<
"88 KK'/"4"4!55^_n_u_u^vvyz{"))) (?KK'/"4"4!55VWX #S...+11
? (..go0011cde)/*@*@)AEF)*S
CD).)9J<sST)-8RST 4SVH=>s0   E9 A,E9 (E9 1#E9 B#E9 9	F-!F((F-returnc           	      V   t        | j                  xs d      }t        | j                  xs d      xs | j                  dk(  rdnd}| j
                  xs" | j                  xs t        j                         }t        j                         }t        dt        ||j                         |j                         z
  j                  dz               }t        t        | dd      xs d      }||z   |dkD  r||z  ndz  }t        dt        t        ||z
                    }||t        |      |dS )	ug  
    최근 결제 1건을 기준으로 '전체 환불 후 사용분 재결제'에 필요한 금액을 계산.
    - 전체 환불: latest_payment.amount
    - 사용분 청구: (amount + carryover_credit_amount) * (경과일수 / 총일수) - carryover_credit_amount
      (carryover 크레딧은 먼저 소진된 가치이므로 사용분에서 차감)
    r   r"   rT   rU   r}   carryover_credit_amountg      ?)r   elapsed_daysr   r   )floatr   r   subscription_daysr!   r   r   r   r`   maxmindaterS   getattrround)	r   r   r   start_dttodayr   	co_amount
used_valuer   s	            r1   _calc_usage_charger
    s    >((-A.F^55:;ynFbFbfoFouxJ 55d9R9RdV^VbVbVdHLLNEq#j5::<(--/+I*O*ORS*STUL gn.GKPqQI 9$jSTn
)BZ]^J q#eJ$:;<=L !$#&y>$	     c           
         ddl m }m}m} | sdddddS  |j                         |xs }|j                         }ddlm fd}| D cg c]  } ||      s| }}|sdddddS fdd }	t        fd|D              }
|
j                         }d}d}d}d	}|D ]  }t        t        |d
d      xs d      }||z  } |      } |	|      }||z  }t        |dd      }|s| ||      z   }|j                         }|j                         }t        ||      }t        ||      }||z
  j                  dz   }|dk  rd}|dkD  r||z  n|}||t        dt        ||            z  z  }|t        dt        ||            z  } t        t        |            }t        |      t        |      t        |      t        |      |
dS c c}w )u{  
    체인 전체 환불 모드 — 구간별 일일단가 반영.
    - 구간별 per-day = (금액 + 이월금액?) / 구간일수
      * 기본은 금액/일수. carryover_credit_amount가 '그 구간의 추가 일수 가치'로 포함된 경우
        (subscription_days에 carryover가 더해진 구조라면) per-day를 (amount + carryover_credit_amount)/subscription_days 로도 선택 가능.
        아래 기본값은 금액/일수이며, 주석의 옵션을 사용하려면 해당 줄을 교체하세요.
    - overlap_days: [구간시작, 구간끝] ∩ [체인시작, as_of] 의 일 수(일 단위, 오늘 포함)
    r   )r   r   r  r   r   c                 :   t        | dd       j                  k(  }t        t        | dd      xs d      }t        t        | dd      xs d      j                         }t        t        | dd      xs d      }|j	                  d      xs |dk(  xs d|v }|xr | S )	NrV   order_id r!   product_namemlink_usage_onetime   정산)r  rr   rD   lower
startswith)r   	status_okoidcycpnameis_settlementr   s         r1   _is_effectivez3_calc_usage_charge_for_chain.<locals>._is_effective  s    R40H4F4FF	'"j"-34'"or28b9??AGB39r:~6a#:Jax[`O`...r  c                 @    | j                   xs | j                  xs S N)r   r   )r   r`   s    r1   	_start_ofz/_calc_usage_charge_for_chain.<locals>._start_of  s    **BbmmBsCr  c                     t        t        | dd      xs d      }|dk  r-t        t        | dd            j                         }|dk(  rdnd}|S )Nr   r   r!   r"   rT   rU   )r   r  rD   r  )r   dbcs      r1   _days_ofz._calc_usage_charge_for_chain.<locals>._days_of  sP    /38q96WR)<=CCEBIo3Ar  c              3   .   K   | ]  } |        y wr   )r   r   r  s     r1   r   z/_calc_usage_charge_for_chain.<locals>.<genexpr>  s     522s           r   r   NrR   r}   r   r   r   r   chain_start)r   r   r  r`   r   r   r  r   r  r  rS   r   r  )ph_listas_ofr   r   r  
as_of_dater  r   effr#  chain_start_dtchain_start_dater   r   used_days_totalr	  r   r  rS   end_dtstart_dend_dleftrightoverlap_daysper_dayr   r   r  r`   s                              @@@r1   r   r     s    32qqRSTT (,,.CLSEJ =/  
5"=#42C
5qqRSTTD 555N%**,JJOJwr8Q/415f
 R=|d
4d;	t 44F--/++- $g.J&**Q.!L $(!8&4- 	gAs<'> ???
3q#lD"9::C F uZ()L *o*o)L)% y 6s   GGz!/api/subscription/payment-historyc                     	 t        j                  d      } | st        ddi      dfS t        j                  j                  |       j                  t        j                  t        j                  k7        j                  t        j                  j                               j                         }g }|D ]#  }|j                         }|j                  |       % t         j#                  d|  dt%        |       d       t        d	|d
      dfS # t&        $ r:}t         j)                  dt+        |              t        ddi      dfcY d}~S d}~ww xY w)u!   사용자의 결제 이력 조회rJ   r;   rK   rL   rJ   u
   사용자 u   의 결제 이력 조회: u   건T)rl   paymentsr:   u   결제 이력 조회 실패: u+   결제 이력 조회에 실패했습니다.r<   N)r   r>   r   r   r?   r@   filterrV   r   PENDINGr   r   r   rA   to_dictr   r,   r   lenrB   r;   rD   )rJ   r9  payments_datary   payment_dictr0   s         r1   get_payment_historyr@  N  s@   V++i(G%DEFKK "''11'1BII!!]%:%::

(>,,113
4SSU 	
 G"??,L  .   	j	)CCDVCWWZ[\%
   	
  V4SVH=>!NOPRUUUVs#   %D C%D 	E/EEEc                    ddl m} ddlm}m} ddl}ddl}ddl}t        j                         }g }d}	|j                  j                  | j                  |j                        j                  |j                  j!                               j#                         }
|
r| j$                  |k  rT|j&                  | _        d| _        t        j                         | _        t.        j0                  j3                          dg dS |
D ]  }|j4                  r|j6                  s|j6                  |k  r-|j8                  }d |j:                         j<                  dd  }t?        |d	z        }||z
  }|j@                  jC                  d
      |jD                  |||d|ddd	}	  |jF                  tH        d   ddi|d      }|jJ                  dk(  r|jL                  |_        d| |_'        t.        j0                  j3                          |jP                  r_| j$                  tS        |jP                        z
  | _        t        j                         | _        t.        j0                  j3                          |	|z  }	|jU                  |j                  ||ddd       | j$                  |k  r- n.|jU                  |j                  ||d|jV                  d        | j$                  |k  r|j&                  | _        d| _        t.        j0                  j3                          	 t\        j                  jC                  | j^                        }|r%d|_0        t.        j0                  j3                          |	|dS # tX        $ r5}|jU                  |j                  ||dt[        |      d       Y d}~d}~ww xY w# tX        $ r+}tb        je                  dt[        |              Y d}~ud}~ww xY w)u  
    현재 활성 구독의 '지금 이후 구간'에 해당하는 결제들을 최근 순서로 전액 환불하고,
    각 환불마다 subscription.end_date에서 subscription_days를 차감(회수)한다.
    end_date <= now가 되면 구독을 CANCELLED 처리한다.

    반환:
      {
        "total_refund": int,
        "items": [
          {"payment_history_id": int, "amount": int, "refund_no": str, "success": bool, "error": str|None}
        ]
      }
    r   r   r   r   Nr   r   itemsr   r   r   r   u   반품 취소 환불r   r   r   r   rT   r   r:   u   플랜 변경 환불 완료 - rR   Tpayment_history_idr   r   rl   r;   Fr   u   권한 회수 실패: )3r   r   src.models.subscriptionr   r   r   r   r   r   r`   r?   r@   r3   rr   r   r   r   rA   rX   ro   rV   r[   rw   r
   r   rd   r   r   r   r   r   r   r   r>   r   r   r   r   r   r5   r   r   r   r   rB   rD   r   rJ   r   r,   r;   )r   r   r   r   r   r   r   r`   detailsr   	historiesr   r   r   taxablevatr   r   r0   users                       r1   %_refund_all_future_periods_and_cancelrM  n  s    2I
,,.CGL $$..$&& /  h~((--/0  --40::)-&"*,,.


 !B//))1I1I##s* jdjjl..s345	 fsl#w jjnn^4!$ !,

	GL)');< 	A }}#)22	#A)!M

!!# '',8,A,AISUSgSgDh,hL).6llnL+JJ%%'&beev\erv  BF   G  H  ((C/ beev\erw  CD  CI  CI   J  Km v #0::)-&


	<::>>,"6"67D'	

!!# )7;;%  	GNN"%%6Xans  B  CD  E  F  G  G	G  	<LL1#a&:;;	<s8   DM/&+M/AN0 /	N-8*N((N-0	O$9!OO$rJ   ri   	carryoverc                    ddl }ddl}ddlm} t        |j                        }|j
                  }|j                  }d|  d |j                         j                  dd  d| d| dt         |j                         j                                
}	t        |dz        }
||
z
  }|	|d|
||t        d   d	d
dd|	 dd|	 d} |j                  t        d   ddi|d      }|j                  dk(  r|j                         }|j                  d      }t!        | d||	|t"        j$                  t&        j(                  |j*                  ||||xs i j                  dd      |xs i j                  dd       |j                               }t,        j.                  j1                  |       t,        j.                  j3                          ||	|||j                  dd      ddS d|	 |	||dd	dS )u   
    새 상품 결제를 위한 Toss 결제 세션 생성 유틸.
    - PaymentHistory(PENDING) 임시 레코드 생성
    - payUrl/checkoutPage 반환
    r   N)r   mlink__   r   r   T*https://pay.toss.im/payfront/demo/callbackV2;http://localhost:8010/api/v2/payments/toss/success?orderno=&status=PAY_COMPLETEm   http://localhost:3001/payment/fail?code=PAY_PROCESS_CANCELED&message=결제가 취소되었습니다&orderId=orderNor   r   r   r   productDescr   autoExecuteresultCallbackcallbackVersionretUrlretCancelUrlr   r   r   
   r   r:   r   credit_amountcredit_days)rJ   r\   r   r  r   rV   r   rP   r  product_pricer!   r   carryover_credit_daysr   checkoutPager  F)
paymentKeyr   r   	orderNamer   r   sim_payment_key_)r   r   r   r   r6   r4   r!   r   r   r`   	timestampr   r   r   r&   r>   r   r   r;  r   r   r3   r
   r   rc   rd   )rJ   ri   rN  r   r   r   r   r  r!   r  r   r   payment_datar   result	pay_tokentemp_phs                    r1   !_prepare_toss_payment_for_productrn    s$    !F<<L))M y*$**,"2"22A"6!7q-PQRUVbV^VbVbVdVnVnVpRqQrsH #&N.(J '#i(FOPXzYmn H  IQ  HR  SL 	J!34		A 	}}JJz*	 ! ! (((66zz% '%._"$9$9/1$M#,?"7"7q"I#x||~
" 	

w


 $%jj4
 	
 )
3! r  z/api/subscription/change-planc                     	 t        j                  d      } | st        ddi      dfS t        j                  dd      xs i }|j                  d      }|j                  d      xs d	j                         }|st        dd
i      dfS t        j                  j                  |      }t        j                  j                  | t        j                        j                         }|s&t        | |d      }t        ddg dddd|d      dfS |dk(  r0t        |      }ddd}t        | |d      }t        d|||d      dfS t!        |      }t#        ||      }t        | ||      }t        d|||d      dfS # t$        $ rU}	t&        j)                  d       t*        j                   j-                          t        dt/        |	      i      dfcY d}	~	S d}	~	ww xY w)zN
    req: { "new_product_id": int, "strategy": "carryover"|"refund_all" }
    rJ   r;   rK   rL   TrM   new_product_idstrategyrN  u"   new_product_id가 필요합니다.rQ   r~   N)rN  r   rC  ra  rb  )rl   r   rN  ry   r:   
refund_allzchange_plan failedr<   )r   r>   r   r   r^   r  r   r?   r_   r   r@   r   rb   r   rn  rM  _refund_pure_future_periods*_compute_partial_credit_for_current_periodrB   r,   rC   r
   rg   rD   )
rJ   rh   rp  rq  ri   r   payment_prepr   rN  r0   s
             r1   change_planrw  3  s	   
@/++i(G%DEFKKd48>B"23HHZ(7K>>@G%IJKSPP--**>: $))33$6$=$= 4 

%' 	
 <WgY]^L+,r:/0C'	 
   |#A,OM*+A>I<WgY]^L'&'	 
   4LA ?|WU	 9'U^_#"#	
 
  	  /-.


Q()3../s7   %E8 AE8 BE8 4E8 6E8 8	GA
GGGc                 d   ddl m} ddlm} ddlm} t        j                         j                         }	 |j                  j                  |j                  | j                  k(  |j                  |j                  k(  |j                  |j                         k  |j                   |j                         k\        j#                  |j$                  j'                               j)                         }|r|j*                  r|j*                  dk  rdddS t-        |j*                        }t/        d|j                   j                         |z
  j0                        }|dk  rdddS t-        |j2                  ||z  z        }	t5        |dd      dk(  rd	nd
}
|j6                  |
z  }t-        |	|z        }t8        j:                  j=                  d| d| d|j2                   d|	 d| 
       |	|d}t8        j:                  j=                  d|        |S # t>        $ r}t8        j:                  jA                  dtC        |              t8        j:                  jA                  d| r| j                  nd        t8        j:                  jA                  d|r|j                  nd        dddcY d}~S d}~ww xY w)u   
    현재(오늘 포함) 활성 구간 1건의 남은 가치를 새 상품 기준 '일수'로 환산
    - 현재 코드베이스 정책: 토스 부분환불 불가 → 금액은 환불하지 않고 '일수'로 업사이클
    r   r   r  )funcrr  r!   r"   r$   rU   rT   z'[ChangePlan] carryover compute: remain=/r   z -> credit_amount=z, credit_days=z@[ChangePlan] _compute_partial_credit_for_current_period return: z@[ChangePlan] _compute_partial_credit_for_current_period failed: z[ChangePlan] subscription_id: Nonez[ChangePlan] new_product_id: N)"r   r   rG  r   
sqlalchemyry  r   r`   r  r?   r:  r\   r3   rV   rr   r   r   r   r   r   r   r   r   r  rS   r   r  r6   r   r,   r   rB   r;   rD   )r   new_productr   r   ry  r  curr   r   ra  new_cycle_daysprice_per_day_newrb  rk  r0   s                  r1   ru  ru  {  st   
 25LLN!E%6""))**loo=!!]%<%<<22dhhj@00DHHJ>	

 (>,,113
4UUW 	 #//33H3HA3M%&q99../
Q!:!:!?!?!AE!I O OPQ%&q99 CJJ.:*EFG ")oy!QU]!]df'-->-*;;<5n5EQzl Sjj\!3M?.Q\P]_	

 $1M"bcibj kl 6  #cdghidjck!lm  #AUa,//gmAn!op  #@S^dj@k!lm!"1556s-   CH AH B,H 	J/BJ*$J/*J/c                 F   ddl m} ddlm}m} t        j                         }g }d}|j                  j                  | j                  |j                        j                  |j                  j                               j                         }|D ]  }|j                  r|j                   s|j                  |kD  s-|j"                  }	dt%        j&                         j(                  dd  }
t+        |	dz        }|	|z
  }t,        j.                  j1                  d	      |j2                  |
|	|d|dd
d	}	 t5        j6                  t8        d   ddi|d      }|j:                  dk(  r|j<                  |_        d|
 |_         tB        jD                  jG                          |jH                  r_| jJ                  tM        |jH                        z
  | _%        t        j                         | _'        tB        jD                  jG                          ||	z  }|jQ                  |j                  |	|
ddd       n+|jQ                  |j                  |	|
d|jR                  d        ||dS # tT        $ r5}|jQ                  |j                  |	|
dtW        |      d       Y d}~d}~ww xY w)u   
    '지금 이후'에 완전히 위치하는 결제 기간(아직 시작도 안 한 구간)만 전액 환불.
    현재 진행 중인 구간(now ∈ [start, end])은 환불하지 않음.
    r   r   rB  r   r   Nr   r   r   u#   플랜 변경(미래 구간 환불)r   r   r   r   rT   r   r:   u   미래구간 환불 완료 - rR   TrE  FrC  ),r   r   rG  r   r   r   r`   r?   r@   r3   rr   r   r   r   rA   r   r   r   r   r   r   r   r   r   r>   r   r   r   r   r   r   rV   r5   r
   r   rd   r   rX   r   rw   r   r   rB   rD   )r   r   r   r   r`   rH  r   rI  r   r   r   rJ  rK  r   r   r0   s                   r1   rt  rt    sh   
 2I
,,.CGL$$..$&& /  h~((--/0 
 ))1I1I%%+YYF!$**,"2"23B"7!89I&3,'G7"C **..8NN% !(!" $%?
K\MM+l";+9;M*N'2B@ ==C' - 6 6BI'DYK%PBNJJ%%' ++0<0E0E	WYWkWkHl0l-2:,,./

))+ F*LNN"%%61:tVZ$\ ] NN"%%61:uWXW]W]$_ `S ^ )7;;	  \beev-65SVWXSY [ \ \\s   3D'I""	J +*JJ r   c                 .   |dk  ryddl }ddl}d|  d |j                         j                  dd  d| dt	        t        j                         j                                }d}dd	i}t	        |d
z        }	||	z
  }
||d|	|
|t        d   dddd| dd| d}	  |j                  t        d   ||d      }t        j                  j                  d|j                   d|j                          d}d}d}|j                  dk(  rY|j                         }|j!                  d      }|j!                  dd      }t        j                  j                  d| d|        nBt        j                  j#                  d|j                   d |j                          d!| }d}d}dd"lm}m}  || |r|j*                  nd||||j,                  d#t/        |d$d      ||d%dddt        j                         &      }t0        j2                  j5                  |       t0        j2                  j7                          |||||d'S # t8        $ rS}t        j                  j;                  d(t=        |              t0        j2                  j?                          Y d}~yd}~ww xY w))u  
    사용분 정산 결제를 위해 토스 '결제 준비'를 생성.
    - product_name: '구독 사용분 정산'
    - billing_cycle: 'onetime'로 표기하되, DB에는 monthly처럼 저장해도 무방(실제 갱신 없음)
    - PaymentHistory: PENDING 생성
    r   Nr  rQ  rR  	_onetime_u   반품 정산r   r   r   r   TrS  rT  rU  rV  ut   http://localhost:3001/payment/fail?code=PAY_PROCESS_CANCELED&message=정산 결제가 취소되었습니다&orderId=rX  r   r`  r   z [UsageSettle] toss prepare resp r   r  Fr:   r   re  u5   [UsageSettle] 토스 결제 준비 성공: pay_token=z, checkout=u+   [UsageSettle] 토스 결제 준비 실패: z - 
sim_usage_r   toss_paymentsrP   r  rJ   r\   r   r  r   rV   r   rP   r  rc  r!   r   r   r   r   )r   rf  r   r   r   z[UsageSettle] prepare failed: ) r   r   r   r   r   r   r`   ri  r   r   r   r,   r   r   r   r&   r>   r-   r   r   r   r3   r;  r  r
   r   rc   rd   rB   r;   rD   rg   )rJ   r   r   r   r   r   r  r  r   rJ  rK  payloadresprl  checkoutr   rh   r   r   r   r0   s                        r1   r   r     s    qgYa


(8(8!(<'=Q|nIVYZbZfZfZhZrZrZtVuUvwH"L12G ,$%G

 C  #i(FOPXzYmn O  PX  OY  ZG5x}}[4gG]_`"B4CSCSBTTUVZV_V_U` ab	
s"99;D,Ixx3H##&[\e[ffqrzq{$|} &&)TUYUeUeTffijnjsjsit'uv$XJ/IHJ 	Q/;LOO!##*~|TB%&#"$("&||~!
$ 	

r


  #"$
 	
    #A#a&!JK


s   FH8 8	JA	JJz%/api/subscription/cancel-orchestratedc                  "	   	 t        j                  d      } | st        ddd      dfS t        j                  j                  | t        j                        j                         }|st        ddd      dfS t        j                  d	|j                   d
|j                          d}	 t        j                  j                  |j                  t        j                        j!                  t        j"                  j%                               j                         }|r=t        j                  d|j&                   d|j(                   d|j"                          n#t        j+                  d|j                   d       |j                  t2        j4                  k(  r|r	 t        j                  j                  |j                  t        j                        j!                  t        j"                  j%                               j7                         }|s+t        j                  d       t        dddddddd      dfS t9        |      }t;        |j                  dd            }t        j                  d|        |dk  rKt        ddddd|j                  dd      |j                  dd      |j                  dd      dd d!d      dfS t=        |j>                  ||d   |"      }|rE|j                  d#      r4|j                  d$      r#|j                  d$d%      jA                         d%k(  rt        dd&|xs i d'      d(fS t        d||d#   |d$   |j                  d)d      |j                  d      |j                  d      |j                  d      dd d*d      dfS t        dd-d      d(fS # t,        $ r,}t        j/                  dt1        |              Y d}~ed}~ww xY w# t,        $ r8}t        jC                  d+       t        dt1        |      d      d,fcY d}~S d}~ww xY w# t,        $ rV}tD        j                   jG                          t        jC                  d.       t        dt1        |      d      d,fcY d}~S d}~ww xY w)/u   
    “정산 먼저 → 전체 환불” 모드 시작점.
    1) 사용분 정산 금액 계산
    2) 정산 결제 준비 + payUrl 반환
    rJ   FrK   r   rL   r~   r   r   r   r   Nr   r   r   r   r   r   r   u>   완료된 결제 없음 — 사용분 정산 0원으로 처리Tr   )r   r   r   r   uN   정산할 금액이 없어 바로 취소/환불을 진행할 수 있습니다.)rl   r   r   r:   r   r   r   r   r   r   r   uO   정산 금액이 0원입니다. 바로 전체 환불/취소를 진행하세요.r   r   r   r  u"   정산 결제 링크 생성 실패)rl   r;   detailrQ   r   ub   정산 결제부터 진행해주세요. 결제 완료 후 전체 환불/취소가 이어집니다.u)   [CancelChain] 정산 링크 생성 실패r<   uc   오케스트레이션 취소를 진행할 수 없습니다. (PG 또는 결제 이력 확인 필요)u<   기간 연장형 구독 취소(오케스트레이션) 실패)$r   r>   r   r   r?   r@   r   rb   r   r,   r   r   r3   r   r   rr   r   r   r   r   r   r-   rB   r;   rD   r   r   rA   r   r   r   rJ   striprC   r
   rg   )rJ   r   r   r0   r   r   r   r   s           r1    cancel_subscription_orchestratedr  K  s   tA++i(u7VWXZ]]]#))33$6$=$= 4 

%' 	 u7WXY[^^^=l>Y>Y=ZZmnzn}n}m~  	A 
	G,22(yQYQcQcyd'x(A(A(F(F(HI$uw  @A[A[@\\eftf{f{e|  }J  KY  Kd  Kd  Je  f  g(99bcd
 &&-*E*EE.LI"0"6"6#,9\__U]UgUg9#h#+8N,E,E,J,J,L#M#&35 
 &KK `a"#'&''+&**/	- $t	$ 	 	 	 :.I
":>>.!#DE7
|DE1$"#'&''+&**/.8nn\1.M.8nn\1.M-7^^K-K%
- $u$   " !B(00!-#1!#4!-	! %M,=,=i,HP]PaPabjPko|  pA  pA  BJ  LN  pO  pU  pU  pW  []  p]"#(!E"/"52$  	  #".#0#;"/"9&3&7&7e&L*4..*F*4..*F)3)D!
)  D    * z
   	i  	GLL<SVHEFF	G^  I  !LM53q6BCSHHI  A


WX53q6:;S@@As   &P/ AP/ 81P/ *CN3 >!P/  BO+ ?BO+ A7O+ AO+ #P/ 3	O(<!O#P/ #O((P/ +	P,4-P'!P,"P/ 'P,,P/ /	R8AR	R	Rz/api/user/profilec            	         	 t        j                  d      } | st        ddi      dfS t        j                  j                  |       j                         }|st        ddddddd      dfS t        |j                  xs d|j                  xs d|j                  xs d|j                  xs d|j                  xs d|j                  xs dd      dfS # t        $ r"}t        dt        |      i      d	fcY d
}~S d
}~ww xY w)u   사용자 프로필 조회rJ   r;   rK   rL   r8  r  )company_namebusiness_numberphoneaddress
departmentpositionr:   r<   N)r   r>   r   r   r?   r@   r   r  r  r  r  r  r  rB   rD   )rJ   profiler0   s      r1   get_user_profiler    s   /++i(G%DEFKK##--g->DDF "#%     #006B&66<"]](b,"!,,2((.B
   	  /Q()3../s*   %C AC ,A'C 	C?C:4C?:C?PUTc                  f   	 t        j                  d      } | st        ddi      dfS t        j                         }t
        j                  j                  |       j                         }|s+t        |       }t        j                   j                  |       |j                  dd      |_        |j                  dd      |_        |j                  d	d      |_        |j                  d
d      |_        |j                  dd      |_        |j                  dd      |_        t#        j$                         |_        t        j                   j)                          t        ddi      dfS # t*        $ r@}t        j                   j-                          t        dt/        |      i      dfcY d}~S d}~ww xY w)u    사용자 프로필 업데이트rJ   r;   rK   rL   r8  r  r  r  r  r  r  r  r   u)   프로필이 업데이트되었습니다.r:   r<   N)r   r>   r   r   r^   r   r?   r@   r   r
   rc   r  r  r  r  r  r  r   utcnowrw   rd   rB   rg   rD   )rJ   rh   r  r0   s       r1   update_user_profiler    s[   /++i(G%DEFKK!##--g->DDF!'2GJJNN7##xx;"&((+<b"A"-((9b1!XXlB788J3%__.


	#NOPRUUU /


Q()3../s#   %E' D>E' '	F005F+%F0+F0z//api/subscription/finish-settlement-then-refundc                     	 t        j                  d      } | st        ddd      dfS t        j                  dd      xs i }|j                  d      }t
        j                  j                  | t        j                  	      j                         }|st        dd
d      dfS |rPt        j                  j                  |t        j                  |       j                         }|st        ddd      dfS |j                  }dBd} |||      \  }}|r5t        j                   j#                  d|        t        dd| d      dfS |j                  d      xs |j                  d      }	|	dk7  rt        dd|	 dd      dfS t        j$                  |_        t)        j*                         |_        t.        j                   j1                          t        j                   j3                  d|        nt        j                   j3                  d       t        j                  j                  |j4                  t        j$                        j7                  t        j8                  j;                               j=                         }
dt        dt>        fd}|
D cg c]  } ||      s|r|j@                  |k(  s| }
}|
st        j                   j3                  d        t        jB                  |_        d|_"        t)        j*                         |_#        t)        j*                         |_        t.        j                   j1                          	 tH        j                  j                  |jJ                        }|r%d!|_&        t.        j                   j1                          t        dd"g d#d$      d%fS d"}g }|
D ]C  } ||      r:t        j                   j3                  d&|j4                   d'|j@                          FtQ        |jR                  xs d"      }|d"k  red(tU        jV                         jX                  dd)  }tQ        |d*z        }||z
  }tZ        d+   |j                  |||d"|d"d,d-	}	 t]        j^                  tZ        d.   d/d0i|d12      }|j`                  d%k(  rt        jb                  |_        d3| |_2        t.        j                   j1                          |jf                  r|ti        |jj                  |jF                  tm        tQ        |jf                        4      z
        |_#        t)        j*                         |_        t.        j                   j1                          ||z  }|jo                  |j4                  ||dd5       nnt        j                   j#                  d6|j4                   d7|j`                   d7|jp                          t        dd8|j4                  |jp                  d9      dfc S F t        jB                  |_        d|_"        t)        j*                         |_#        t)        j*                         |_        t.        j                   j1                          	 tH        j                  j                  |jJ                        }|r%d!|_&        t.        j                   j1                          t        dd?||d#d@      d%fS c c}w # tN        $ r Y Bw xY w# tN        $ rR}t        j                   js                  d:       t        dd;tu        |       |j4                  d<      d=fcY d}~c S d}~ww xY w# tN        $ r5}t        j                   jw                  d>tu        |              Y d}~d}~ww xY w# tN        $ r`}t.        j                   jy                          t        j                   js                  dA       t        dtu        |      d      d=fcY d}~S d}~ww xY w)Cu  
    1) 프론트에서 정산 결제(usage_settlement)를 완료한 뒤 호출되는 마무리 단계
    2) (있다면) 정산 결제의 Toss 상태를 확인해 PENDING -> COMPLETED 로 확정
    3) 체인에 속한 COMPLETED 결제들을 최신→과거 순으로 '전액 환불'
    4) 구독을 CANCELLED 처리, 권한 회수
    응답: { success, refund: { total_refund, items: [...] } }
    rJ   FrK   r   rL   TrM   r   r~   r   r   )r  rV   rJ   un   정산 결제 내역(PENDING)을 찾을 수 없습니다. 이미 처리되었거나 잘못된 요청입니다.rQ   Nc                 ,   	 | s|syt        j                  t        d   ddit        d   | |dd      }|j                  d	k(  r|j	                         d fS d d
|j                   d|j
                   fS # t        $ r}d t        |      fcY d }~S d }~ww xY w)N)Nu6   payToken 또는 orderNo 중 하나는 필요합니다.r   r   r   r   )r   r   rY  r`  r   r:   u   status 조회 실패: r   )r   r   r   r   r&   r   rB   rD   )rl  order_nor  r0   s       r1   _fetch_toss_statusz9finish_settlement_then_refund.<locals>._fetch_toss_status5  s    ($X]#==#L1!/1C D(3I(>*3)13 !#D ''3.#yy{D00#9$:J:J9K1TYYK!XXX  (Q<'(s)   A3 AA3 A3 3	B<BBB)rl  r  z"[FinishSettle] toss status error: u$   정산 결제 상태 확인 실패:   	payStatusrV   PAY_COMPLETEu8   정산 결제가 완료되지 않았습니다. (상태: )z-[FinishSettle] settlement COMPLETED: orderId=zG[FinishSettle] settlement is zero-amount; skipping payment verificationr   r   r   c                     | j                   xs d}| j                  xs d}|j                  d      xs' | j                  xs dj	                         dk(  xs d|v S )Nr  r  r  r  )r  r  r  r!   r  )r   r  r4   s      r1   _is_usage_settlementz;finish_settlement_then_refund.<locals>._is_usage_settlementc  s_    ;;$"COO)rD ~. $$$*113y@$t#r  u:   [FinishSettle] 환불할 COMPLETED 결제가 없습니다.r   r   rC  )rl   r   r:   z,[FinishSettle] skip usage settlement PH: id=z, order=r   r   r   r   u#   반품 취소(체인 전체 환불)r   r   r   r   rT   r   r   rR   )rF  r   r   rl   u$   [FinishSettle] 환불 실패: ph_id=r   u.   일부 환불 실패로 중단되었습니다.r   u   [FinishSettle] 환불 예외u   환불 처리 중 오류: )rl   r;   r   r<   u-   [FinishSettle] 역할 회수 실패(무시): uR   정산 결제 확인 후 전체 환불 및 구독 해지가 완료되었습니다.r   z[FinishSettle] unexpected error)NN)=r   r>   r   r   r^   r   r?   r@   r   rb   r   r   r   r;  r   r   r,   r;   rr   rV   r   r`   rw   r
   rd   r   r3   r   r   r   rA   boolr  ro   r[   rX   r   rJ   r   rB   r   r   r   r   r   r   r   r   r   r   r5   r   r  rW   r   r   r   rC   rD   r-   rg   )rJ   rh   settlement_order_idr   	settle_phrl  r  status_dataerr
pay_statusr   r  r   r   r   rD  r   r   rJ  rK  refund_payloadr   r0   s                          r1   finish_settlement_then_refundr    sM   PA++i(u7VWXZ]]]d48>B"hhy1 $))33$6$=$= 4 

%' 	 u7WXY[^^^ &,,66,'' 7  eg	  $ N    
 "--I($  2IPcdK""((+McU)ST5=abeaf;ghiknnn$5R9RJ^+$WXbWccde      (11I#+<<>I JJ##&STgSh$ij ##$mn )..$9\__XM_M_9`#8N$=$=$B$B$DE35 	
	^ 	 	 (
'2'+(R[[<O-O  	 
 ##$`a"4">">L-1L*$,LLNL!&.llnL#JJJJNN<#7#78(AFJJ%%' tTV7WXY[^^^ B#B'""''*VWYW\W\V]]efhfqfqer(sta(F{!$**,"2"23B"7!89I&3,'G7"C%i0NN% !(!" $%?
N'MM-+-?@'	 ==C' ( 1 1BI'7	{%CBNJJ%%' ++03(33(11I3rG[G[C\4]]1- 3;,,./

))+ F*LLL/8T"K L  &&,,/STVTYTYSZZ[\]\i\i[jjklmlrlrks-tu"#(!Q-/UU vv	$ 
  	L] !D 1::)-& ("*,,.


	a

|334A$

!!# k ,
   	U
&  @  "",,-KL$9#a&B)+   	 (  	a&&)VWZ[\W]V^'_``	a  A


$$%FG53q6:;S@@As   &^ A:^ $A^ 7A^ <^ D^ "[>B^ A[ ^ 1B?^ 1F[$8^ ;A/^ +A] ;^ ^ 	[!^  [!!^ $	\?-A\:2\?3^ :\??^ 	^ +];6^ ;^  ^ 	_,A_'!_,'_,z!/api/subscription/cancel-estimatec            
      $   	 t        j                  d      } | st        ddd      dfS t        j                  j                  dt
              }t        t        j                  j                  d            }t        j                  j                  | t        j                  	      j                         }|s#t        d
dddddiddddi|rg nddd      dfS t        |      }t        |      }t        d |D              }t!        |      }d}|rct"        j                  j                  |      }	|	rBt%        ||	      }|j'                  |t        |	j(                  xs d      |	j*                  d       d
t        |      t        |j                  dd            ddt        |      i|dt        |j                  dd            t        |j                  dd            t        |j                  dd            |j                  d      r|j                  d      j-                         ndddd}
|rt/        |      |
d   d<   t        |
      dfS # t0        $ r8}t2        j5                  d       t        dt7        |      d      dfcY d}~S d}~ww xY w) u   
    여러 시나리오의 예상 금액을 한 번에 제공.
    쿼리:
      - new_product_id (옵션): 캐리오버(플랜 변경) 시뮬레이션용
      - detailed=1/true   : 결제건별 브레이크다운 포함
    rJ   FrK   r   rL   rp  )typedetailedr~   Tr   )r   settlement_amountr   N)settle_then_refundrefund_future_onlyrN  r(  )	scenariosr   	breakdown)rl   estimater:   c              3   N   K   | ]  }t        |j                  xs d         ywr   r   r   s     r1   r   z*estimate_cancel_amounts.<locals>.<genexpr>  s      HxRYY^!!4xr   )rp  new_product_pricenew_product_cycler   r   r   r   )r   r   r   r(  )r  r   r  r  zcancel-estimate failedr<   )r   r>   r   r   r=   r   
_is_truthyr   r?   r@   r   rb   r   _get_chain_completed_ph_calc_usage_charge_for_chain_v2r   _estimate_refund_future_onlyr   &_estimate_carryover_for_current_periodupdater6   r!   rf   _build_chain_breakdownrB   r,   rC   rD   )rJ   rp  r  subchain_phr   refund_total_chainr  rN  r}  r  r0   s               r1   estimate_cancel_amountsr    s   NA++i(u7VWXZ]]] ))*:)Egll..z:;  **$6$=$= + 

%' 	  @AWX.Y/=q.A%)"
 +D1'/T    +3/ 5X>
  Hx HH :(C 	!--++N;KB3T	  &4),[->->-C!)D)4)B)B"   ),,>(?-0PQ1R-S+
 ',>(?+ "+	 #&jnn\1&E"F"%jnn\1&E"F!$Z^^K%C!D'1~~m'D %/NN=$A$K$K$MJN
. /Eh/OGJ,w$$ A1253q6:;S@@As*   &I B.I E5I 	J-J
J
Jc                     | j                   xs d}| j                  xs dj                         }| j                  xs d}|j	                  d      xs |dk(  xs d|v S )uB   정산/일회성 결제 식별(체인 환불 대상에서 제외).r  r  r  r  )r  r!   r  r  r  )r   r  r  r4   s       r1   _is_settlement_phr  @  sZ    ;;"C!r
(
(
*COO!rD>>.)SSI-=S(dBRSr  c                 (   t         j                  j                  | j                  t        j
                        j                  t         j                  j                               j                         }|D cg c]  }t        |      r| c}S c c}w )u;   체인 COMPLETED 결제(정산건 제외) 최신순 목록.r   )r   r?   r@   r3   r   rr   r   r   r   rA   r  )r   r)  r   s      r1   r  r  H  sj    ##	,//(BTBT	U22779:  !>2(9"(=B>>>s   7BBr"  c                 8    | xs dj                         dk(  rdS dS )Nr  r$   rU   rT   )r  )r"  s    r1   _days_for_cycler  Q  s!    8""$038b8r  c                 t   | sddddddS d}t        j                         }d}d}d}d}| D ]K  }t        |j                  xs d      }||z  }|j                  xs |j
                  }	|j                  }
|	s|}	|
s%t        t        |dd            }|	t        |      z   }
|rt        ||	      n|	}t        d|
j                         |	j                         z
  j                        }||z  }d}|j                         |	j                         k\  rZt        |t        dt        |j                         |
j                               |	j                         z
  j                  dz               }||z  }||z  }|||z  z  }N t        t        |            }t        |      t        |      t        |      t        |      |d	S )
u@  
    '구간별 일일단가'로 사용가치 합산.
    - 각 결제별 interval = [subscription_start_date, subscription_end_date]
      (없으면 created_at 기준 + 30/365 fallback)
    - interval∩[chain_start..today]의 일수만 사용
    - usage_value += (amount / interval_days) * used_days_in_interval
    r   N)r   r   r(  r   r   r&  r!   r"   rR   r}   r'  )r   r`   r   r   r   r   r   r  r  r   r  r  r  rS   r   r  )r)  r(  r  r   r   used_days_accumusage_valuer   amtr  r0  bc_daysinterval_daysused_in_intervalr6  r   s                   r1   r  r  U  s    4a1 	1 KLLNEJJOKBIIN#c
-->))H%gb/9&MNG	w 77F 5@c+x0X A ?EEFm#
 ::<8==?*"=#&q3uzz|V[[]+Khmmo+]*c*cfg*g#h j 	++%w!111? B u[)*L *o*o)L)" r  c                     | syt        j                         }d}| D ]A  }|j                  xs |j                  }|s ||kD  s&|t	        |j
                  xs d      z  }C t	        |      S )uX   
    완전한 미래 구간(시작일 > now)만 환불했을 때의 총 환불액.
    r   )r   r`   r   r   r   r   )r)  r`   totalr   r  s        r1   r  r    sd     
,,.CE-->3Sa((E  u:r  c                 p   t        j                         j                         }t        j                  j                  t        j                  | j                  k(  t        j                  t        j                  k(  t        j                  t        j                  |t         j                  j                               k  t        j                  t        j                  |t         j                  j                               k\        j!                  t        j"                  j%                               j'                         }|r|j(                  r|j(                  dk  rdddS t+        |j(                        }t-        d|j                  j                         |z
  j.                        }|dk  rdddS t+        t1        t3        |j4                        ||z  z              }t7        t9        |dd            }|j:                  xs dt-        d|      z  }|dkD  rt+        ||z        nd}	t+        |      t+        |	      dS )u   
    현재 진행중인 구간 남은 가치를 새 상품 기준으로 '일수'로 환산(추정치).
    (실환불 없이 계산만)
    r   rr  r!   r"   r}   )r   r`   r  r   r?   r:  r\   r3   rV   r   rr   r   combiner  timer   r   r   r   r   r   r   r  rS   r  r   r   r  r  r6   )
r   r}  r  r~  r   r   ra  r  r  rb  s
             r1   r  r    s   
 LLN!EF--@$$(:(::559I9I%QYQ]Q]QbQbQd9ee33x7G7Gx||O`O`Ob7cc	 H^..3356EG  c++s/D/D/I!"155S**+JS66;;=EKKLN!"155eCJJ/>J3NOPQM$W[/9%UVN$**/a3q.3II<MPQ<Q#m&778WXK /K@PQQr  sc                 8    t        |       j                         dv S )N)1trueyesy)rD   r  )r  s    r1   r  r    s    q6<<>666r  c                 \   | j                   xs | j                  }|st        j                         }| j                  }|s5t        | dd      xs dj                         }|t        |dk(  rdnd      z   }t        d|j                         |j                         z
  j                        }|||fS )u@   결제 1건의 [start, end], 총일수 계산 (fallback 포함).r!   r  r$   rU   rT   rR   r}   )r   r   r   r`   r   r  r  r   r  r  rS   )r   r  r0  r"  r  s        r1   _interval_boundsr    s    )):R]]H<<>%%Fb/2.4";;=I".3bIIFKKMHMMO;AABMV]**r  c                 X   g }t        j                         j                         }| D ]  }t        |      \  }}}t	        |j
                  xs d      }||z  }d}	||j                         k\  rLt        |t        dt        ||j                               |j                         z
  j                  dz               }	||	z  }
|j                         |kD  }|rt        |      nd}|j                  |j                  |j                  |j                  |j                  xs dj                         t        |      |j!                         |j!                         t        |      t	        t#        |d            t        |	      t        t#        |
            t%        |      t        |      d        |S )u   
    결제건별 상세 브레이크다운 생성:
    - 결제 금액, 주기, interval, 일일단가, interval 내 사용일수, 사용가치
    - 완전미래구간 여부 및 환불가능액(미래구간 환불 시나리오 참고용)
    r   r}   r     )rF  r  r  r!   r   rW   rX   r  r6  used_days_in_intervalr  fully_futurerefundable_future_amount)r   r`   r  r  r   r   r  r  rS   r   r   r3   r  r  r!   r  rf   r  r  )r)  rD  r  r   r  r0  r  r   r6  r  r  r  r  s                r1   r  r    sr    ELLN!E*:2*>'&-ryy~A&=( !"HMMO#$'AE6;;=1HMMOCIIAMN%!
  55}}.2>3v;A "$%%OO ..4";;=&k",,.((* /U7A./%()>%?u[12 .(+,D(E
 	% D Lr  z/api/payments/statusc                     t         j                  j                  d      } | st        dd      dfS t        j
                  j                  |       j                  t        j                  j                               j                         }|st        dd      d	fS t        d
|j                  j                  j                               S )Nr   FzorderId requiredr   rQ   )r  	NOT_FOUND)rl   rV   r   T)r   r=   r>   r   r   r?   r@   r   r   r   r   rV   re   r  )r  r   s     r1   payment_statusr    s    ||	*Hu,>?DD				'	'	'	:	C	CND]D]DbDbDd	e	k	k	mBu[93>>4		(=(=(?@@r  z/api/subscription/early-renewc                  >   t        j                  d      } | st        ddd      dfS t        j                  j                  | d      j                         }|st        dd      d	fS t        j                  j                  |j                  d
      j                  t        j                  j                               j                         }|r|j                  st        dd      d	fS t        j                  j                  t        j                  |j                  k(  t        j                  |j                  kD  t        j                  j!                  d            j                         }|rt        dd      dfS |j"                  xs |j$                  j"                  xs d}|dk(  rdnd}|j                  t'        d      z   }|t'        |dz
        z   }|dk(  r|j$                  j(                  n|j$                  j*                  }d|j                   dt-        t/        j.                                }	t-        t1        |dz              }
||
z
  }|	|d|
||j$                  j2                  t4        d   dddd|	 dd |	 d!}t7        j8                  t4        d"   d#d$i|d%&      }|j:                  d'k7  r#	 |j=                         }t        dd)|       d*fS |j=                         }|j                  d+      }|j                  d,d-      }t        | |j                  ||	|tB        jD                  tF        jH                  |jJ                  |j$                  j2                  |||||tM        jN                         .      }tP        j                   jS                  |       tP        j                   jU                          t        d||	||j$                  j2                  |d/|jW                         |jW                         |d01      d'fS # t>        $ r d(|j@                  i}Y Vw xY w)2NrJ   FrK   r   rL   rb   r~   r   rQ   rr   r   u5   마지막 결제 내역이 올바르지 않습니다.)r;  rr   u/   이미 다음 구간 결제가 존재합니다.i  r"   rT   rU   r}   rR   r$   mlink_renew_rQ  r   r   r   TrS  rT  rU  rV  rW  rX  r   r   r   r`  r   r:   r   u   Toss 준비 실패: r  r   re  r  r  )rf  r   r   rg  r   )startend
cycle_days)rl   ry   period),r   r>   r   r   r?   r@   r   r   r3   r   r   r   r:  r\   r   rV   in_r!   ri   r   
price_yearr6   r   r  r  r4   r   r   r   r   r&   rB   r   r   r;  r   r   rP   r   r`   r
   rc   rd   rf   )rJ   r  last_phfuture_existscycler  
next_startnext_endr   r  r   r   rj  r   rh   rk  rl  checkout_urlrm  s                      r1   early_renewr    s   kk)$G53RSTVYYY



&
&wx
&
H
N
N
PCu,LMsRR ##	#&&	E==BBDE  '77u,cdfiii $))f^;;SVVC+CCgFcFcc+22667NOQ eg	 
 u,]^`cccG!:!:GiEi'SJ..1BBJiZ\::H',hS[[##CKK<M<MFcffXQs499;'7&89H v|,-N.(J '{{''i(FOPXzYmn H  IQ  HR  SL 	J!34		A 	}}	'668D u.B4&,IJCOOVVXF

:&I::nb1L $$$22>>[[%%$ *&<<>G" JJNN7JJ#))"
  ))+%%'$
  ;  	'qvv&D	's   5P PPr  )Oflaskr   r   r   r   r   flask_loginr   r	   r   r
   r   rG  r   r   r   r   r   r   r   r   r   r   r   r   r&   base64hashlibhmacloggingr  r   r   r   __name__subscription_bp	getLoggerr,   r   r>   r   routerG   rj   r{   r   r   r   r   dictr
  r   r@  rM  r   rn  rw  ru  rt  r   r  r  r  r  r  r  r  r  rD   r  r  r  r  r  r  r  r  r   r  r%  r  r1   <module>r     s   C C 4 $ B  B  B H (        NH5			8	$ zz~~n-5657 3eWE$/ F$/R 4vhG./ H./b%?N :UGL'/ M'/X @6(S/ T/> 1F8Dm/ Em/`*^$ >_ _D :UGLV MV>m<^Ts TW TQUX\Q\ Tl 6ID/ JD/N/6b@<HXs X`c Xv >QzA RzA~ *UG</ =/> *UG</ =/> HSYRZ[XA \XAv :UGLUA MUApTT T?9 9 9= =@S R RB7#* 7 7
++Z -.A /A 56k 7kr  