o
    {hA                     @  s  d Z ddlmZ ddlZddlZddlZddlmZmZ ddlm	Z	 ddl
mZ ddlmZmZmZmZmZmZmZmZmZmZ ddlmZ dd	lmZ dd
lmZ ddlmZ ddlm Z  ddl!m"Z" ddl#m$Z$ ddl%m&Z& ddl'm(Z( ddl)m*Z* ddl+m,Z, ddl-m.Z. ddl/m0Z0 ddl1m2Z2 e3e4Z5ede4Z6dhZ7dZ8e" Z9e9:de$ e9:de& e9:de( e9:de* e9:de, e9:de. e9:de2 dNd$d%Z;dOd(d)Z<dPd-d.Z=dQd1d2Z>dRd4d5Z?e6@d6e d7d8d9d: ZAe6@d;e d7d8d<d= ZBe6Cd>ejDd?ed@e d7d8dAdB ZEe6CdCejDdDed@e d7d8dEdF ZFe6CdGejDdHed@e d7d8dIdJ ZGe6@dKe d7d8dLdM ZHdS )Su  
Загрузка и обработка банковских выписок (PDF-файлы).
Добавлены:
  • RBAC-декоратор @role_required.
  • rate limit на все POST-эндпоинты.
  • жёсткая валидация входящих данных (bank_key, INN, тип файла).
  • Path-sandbox – сохранение только во внутренний UPLOAD_FOLDER.
  • автоматическая очистка временных файлов после генерации.
    )annotationsN)datetime	timedelta)Path)Any)
	Blueprintcurrent_appflashjsonifyredirectrender_templaterequest	send_filesessionurl_for)get_remote_address)secure_filename)get_db_connection)limiter)role_required)BankStatementService)PrivatBankPdfParser)TaskombankPdfParser)MonobankPdfParser)MTBBankPdfParser)UkrgasbankPdfParser)RaiffeisenBankPdfParser)StatsService)SenseBankPdfParserbank_bpZpdfi  @
privat_pdftaskombank_pdfmonobank_pdfmtbbank_pdfukrgasbank_pdfraiffeisen_pdfsensebank_pdffilenamestrreturnboolc                 C  s    d| v o|  ddd  tv S )N.   )rsplitlowerALLOWED_EXTENSIONS)r'    r0   +/var/www/html/app/blueprints/bank/routes.py_allowed_fileH   s    r2   dst_dirr   c                 C  sH   | ddd  }t  d| }| |  }| |jvr"td|S )u   
    Создаёт абсолютный путь вида <UPLOAD_FOLDER>/<uuid>.<ext>,
    гарантируя, что он остаётся внутри UPLOAD_FOLDER.
    r+   r,   zBad upload path)r-   r.   uuiduuid4resolveparents
ValueError)r3   r'   extZ	safe_nameoutr0   r0   r1   
_safe_pathL   s   
r;   user_id
int | Nonedict[str, Any] | Nonec              	   C  s   | sd S t  /}| }|d| f | W  d    W  d    S 1 s)w   Y  W d    d S 1 s9w   Y  d S )Nz!SELECT * FROM users WHERE id = %s)r   cursorexecutefetchone)r<   conncurr0   r0   r1   _fetch_userZ   s   RrD   userdict[str, Any]c              	   C  s   t jd | d }| d}t }|r||k rf|du rdn|}|tdd }t +}| }|d||| d	 f |	  W d   n1 sJw   Y  W d   n1 sYw   Y  || d
< || d< | S )ug   
    Оновлюємо лічильник генерацій, якщо минуло >30 днів.
    PLAN_LIMITSsubscription_planparse_reset_dateNi?B    )daysz@UPDATE users SET parse_count=%s, parse_reset_date=%s WHERE id=%sidparse_count)
r   configgetr   utcnowr   r   r?   r@   commit)rE   Z
plan_limitrI   nowZ	new_countZ	new_resetrB   rC   r0   r0   r1   _check_and_reset_parse_countb   s$   

 rS   (tuple[dict[str, Any] | None, str | None]c               	   C  sF  t td} | sdS | d dkrdS | d dkr| dfS | dro| d	}|s+d
S t '}| }|d|f | }W d   n1 sIw   Y  W d   n1 sXw   Y  |sadS |d dvrk| dfS | dfS t| } t	 
 }| d r| d |k r| dfS tjd | d }|dur| d dkr| dfS | dfS )u  
    Повертає (user | None, error | None) з такою логікою:
      • Якщо user не знайдений → (None, "Ви не авторизовані")
      • Якщо role='disabled' → (None, "Учётна запис вимкнена")
      • Якщо role='admin' → (user, None)
      • Якщо is_pf_client=1:
          – Дістанемо pay_status із planfix_companies
          – Якщо pay_status ∉ {"Сплачений", "Постоплата"}:
                → повернути (user, "Оплата по договору відсутня")
          – Інакше (user, None)
        (Під PF-клієнтом не знімаємо лічильник parse_count, не дивимося payment_date)
      • Інакше (звичайний зовнішній клієнт):
          – user = _check_and_reset_parse_count(user)
          – Якщо payment_date прострочена → повернути (user, "Оплата прострочена")
          – Якщо ліміт генерацій ≤ 0 → повернути (user, "Ліміт генерацій вичерпано")
          – Інакше → (user, None)
    r<   )Nu"   Ви не авторизованіroledisabled)Nu(   Учётна запис вимкненаadminNis_pf_clientpf_company_id)Nu*   Не вказано договір PlanFixz?SELECT pay_status FROM planfix_companies WHERE pf_company_id=%s)Nu8   Договір не знайдено у PlanFix-кеші
pay_status)u   Сплаченийu   Постоплатаu3   Оплата по договору відсутняpayment_dateu#   Оплата простроченаrG   rH   rM   r   0   Ліміт генерацій вичерпано)rD   r   rO   r   r?   r@   rA   rS   r   rP   dater   rN   )rE   pf_idrB   rC   rowtodaylimitr0   r0   r1   _current_userz   sB   


 rb   z/statusclientrW   c                  C  sR   t  \} }|rtd|ddfS | d p| d dk}|rdn| d }td	|d
S )Nerrorstatusmessage  rX   rH   pro_plusu   ∞rM   OK)rf   rM   )rb   r
   )rE   err	unlimitedcountr0   r0   r1   bank_status   s   
rn   /c            	      C  s   t  \} }| d u rt|pdd ttdS | d dkrdn| d }t| d}|p.|dk}|s5| d	 nd
}| d}dd | dddD }|d uoS| d dk}td||| d tj	d ||t
  ||||dS )Nu%   Помилка авторизаціїdangerauth_bp.loginrU   rW   rH   rX   ri   rM   u   Безлімітr[   c                 S  s   g | ]
}|  r|  qS r0   )strip).0br0   r0   r1   
<listcomp>   s    zbank_main.<locals>.<listcomp>available_banks ,zparsing_landing.html	ALL_BANKS)rH   rM   Z	user_role	all_banksZavailable_banks_listr[   r`   rX   Zis_unlimited_plandisable_actions	error_msg)rb   r	   r   r   r*   rO   splitr   r   rN   r   rP   r]   )	rE   rk   planrX   Zis_unlimitedZ
left_countr[   rv   r{   r0   r0   r1   	bank_main   s2   


r   z/uploadz	15/minute)key_funcc                  C  s  t  \} }|rtd|ddfS tjdd }|tjd vr)tddddfS tjd	d }| s?tdd
ddfS t	tjd }|j
ddd g }tjdD ]]}|r\|js]qUt|jsmtddddf  S |jr|jtkrtddddf  S t|t|j}|| td| ||j|jt|||tjddd t| d d|j qUtdg | tddd td D dS )Nrd   re   rh   bank_keyrw   ry   u   Невірний банк  	payer_innu8   ІПН повинен містити лише цифриUPLOAD_FOLDERT)r7   exist_okZ	pdf_filesu*   Дозволені лише PDF-файлиu&   Файл занадто великийzSaved upload to %ssenderu   ФОП)rL   originalName	file_pathr   r   r   rL   Zuploadupload_listrj   c                 S  s   g | ]}d d |  D qS )c                 S  s   i | ]\}}|d v r||qS )rL   r   r0   )rs   kvr0   r0   r1   
<dictcomp>      z*bank_upload.<locals>.<listcomp>.<dictcomp>)itemsrs   xr0   r0   r1   ru     r   zbank_upload.<locals>.<listcomp>rf   files)rb   r
   r   formrO   rr   r   rN   isdigitr   mkdirr   getlistr'   r2   content_lengthMAX_FILE_SIZEr;   r   savelogdebugappendstemr(   r   r   
setdefaultextend)rE   rk   r   r   
upload_diraddedfZdst_pathr0   r0   r1   bank_upload   sH   



r   z/deletez	30/minutec                  C  s   t  \} }|rtd|ddfS tjdd}|s"tddddfS g }tdg D ]5}|d	 |krZzt|d
 jdd W q* tyY } zt	
d|d
 | W Y d }~q*d }~ww || q*|td< dd |D }td|dS )Nrd   re   rh   file_idrw   u   file_id необхіднийr   r   rL   r   T
missing_okzCan't delete file %s: %sc                 S  s   g | ]}|d  |d dqS )rL   r   r   r0   r   r0   r0   r1   ru   7  r   zbank_delete.<locals>.<listcomp>rj   r   )rb   r
   r   r   rO   r   r   unlinkOSErrorr   warningr   )rE   rk   r   new_listitemexcZ
files_metar0   r0   r1   bank_delete!  s&   
r   z	/generatez	10/minutec                  C  s  t  \} }|rtd|ddfS | d p| d dk}| d dkr1|s1| d	 d
kr1tddddfS tdg }|sBtddddfS g }|D ]H}t|d }| sSqFz|tt	||d |d |d 
  W qF ty } ztd tdt	|ddfW  Y d }~  S d }~ww d|}|ds|d7 }ttjd }	t|	d}
z	|
j|dd W n" ty } ztd tdt	|ddfW  Y d }~S d }~ww | d dkr|s| dst *}| }|d| d f |  W d    n	1 sw   Y  W d    n	1 sw   Y  |D ]}zt|d jdd  W q ty7   Y qw g td< t	|
td!< t| d d"|
j t|
dd#d$S )%Nrd   re   rh   rX   rH   ri   rU   rW   rM   r   r\   r   u0   Немає завантажених файлівr   r   r   r   r   zParser errori  
u   КонецФайлаu   
КонецФайлаr   z
result.txtcp1251)encodingzWrite errorzGUPDATE users SET parse_count = GREATEST(parse_count - 1, 0) WHERE id=%srL   Tr   last_generated_filegenerateresult_out.txtas_attachmentdownload_name)rb   r
   r   rO   r   existsr   serviceZprocess_filer(   rr   	Exceptionr   	exceptionjoinendswithr   rN   r;   
write_textr   r?   r@   rQ   r   r   r   namer   )rE   rk   rl   r   Zresult_partsr   pathr   Zcombined_textr   out_pathrB   rC   Zitmr0   r0   r1   bank_generate<  sz   

&



" 
 
r   z
/last_filec                  C  s`   t  \} }|rt|d ttdS ttdd}| s)tdd ttdS t|dd	d
S )Nrp   rq   r   rw   uH   Нет последнего файла или файл не найденr   zbank_bp.bank_mainTr   r   )	rb   r	   r   r   r   r   rO   r   r   )rE   rk   r   r0   r0   r1   bank_last_file  s   


r   )r'   r(   r)   r*   )r3   r   r'   r(   r)   r   )r<   r=   r)   r>   )rE   rF   r)   rF   )r)   rT   )I__doc__
__future__r   loggingosr4   r   r   pathlibr   typingr   flaskr   r   r	   r
   r   r   r   r   r   r   Zflask_limiter.utilr   Zwerkzeug.utilsr   app.databaser   app.extensionsr   app.security.rbacr   Z#app.services.bank_statement_servicer   Z!app.parsers.privatbank_pdf_parserr   Z!app.parsers.taskombank_pdf_parserr   Zapp.parsers.mono_pdf_parserr   Zapp.parsers.mtbbank_pdf_parserr   Z!app.parsers.ukrgasbank_pdf_parserr   Z!app.parsers.raiffeisen_pdf_parserr   app.services.stats_servicer   Z app.parsers.sensebank_pdf_parserr   	getLogger__name__r   r   r/   r   r   Zregister_parserr2   r;   rD   rS   rb   rO   rn   r   postra   r   r   r   r   r0   r0   r0   r1   <module>   sz   
0






F
 0E