
    5jD                     V   d Z ddl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	Z		 ddlZ ej                  d      Z ee      j                         j                   Z eej$                  j'                  dedz              Z eej$                  j'                  ded	z              Z eed
d      xs edz  ZdZdZdZdZd Zd)dZd)dZd)dZd)dZ d)dZ!d Z"d)dZ#ddddddddZ$d Z%ddddZ&d)dd d!Z'd" Z(d# Z)d*d$Z*d% Z+d& Z,d' Z-d( Z.y# e
$ r dZ	Y w xY w# e
$ r dZY w xY w)+u  
scada_report_approvals.py — SCADA report approver→send workflow (DVI-1102,
SCADA View Reporting F4).

Part of SCADA View Reporting (DVI-1095). A small, Flask-independent engine
module (mirrors ``scada_report.py`` / ``scada_store.py``) shared by BOTH the web
app (``app.py`` — approver UI + approve/reject routes) and the standalone report
scheduler (``scada_report_scheduler.py`` — enqueues external sends for approval).

Why this exists
---------------
Scheduled and manual SCADA reports are emailed to three recipient groups
(DVI-487): per-schedule *notify*, global *enforced*, and global *external*. F4
adds a single-approver gate — modeled on the WO Approvals pattern — so **no
report is ever auto-sent to EXTERNAL recipients without an explicit approval**.

Scope decision (documented, not silent — the issue names "External recipients"):
  * The gate applies ONLY to the *external* recipient list. Internal delivery
    (per-schedule notify + enforced recipients) still happens at generation
    time, unchanged — those audiences are trusted staff, and gating them would
    silently stop every scheduled report the moment the feature deploys.
  * When a report has external recipients, instead of sending to them the
    caller enqueues an ``awaiting_approval`` request here and notifies the
    configured approver(s). An approver approves (→ the persisted XLSX is
    emailed to the external list + the send is logged) or rejects (→ recorded,
    never sent).
  * "Single-approver" = a single approval suffices; any configured approver
    (or a Togen/SCADA admin, enforced in app.py) may action a request.

No-approver safety
------------------
If a report has external recipients but NO approver is configured, the request
is still enqueued as ``awaiting_approval`` (the invariant "never auto-sent
externally" holds) and ``enqueue_external_send`` returns ``notified=False`` so
the caller can surface a LOUD warning to the enforced/internal recipients rather
than silently dropping external delivery (the DVI-1114 class of failure).

State
-----
``scada_report_approvals.json`` — a JSON list of request records, newest last.
The persisted XLSX itself lives in the shared ``reports/`` dir (written by the
scheduler / on-demand generation); a request references it by bare filename.

Record shape::

    {
      "id":            "<uuid4 hex>",
      "created_at":    ISO-8601,
      "source":        "schedule" | "manual",
      "schedule_id":   str | null,
      "report_name":   str,           # human schedule/report name
      "report_type":   str,           # registry id (DVI-1101), e.g. "kiln_temp"
      "date_from":     "YYYY-MM-DD" | null,
      "date_to":       "YYYY-MM-DD" | null,
      "row_count":     int,
      "report_file":   "<name>.xlsx", # bare filename in the reports dir
      "attach_name":   str,           # email attachment filename
      "subject":       str,
      "recipients":    ["a@b.com", ...],  # external list to send on approve
      "requested_by":  str,           # "scheduler" or a user email
      "status":        "awaiting_approval" | "approved" | "rejected",
      "actioned_by":   str | null,
      "actioned_at":   ISO-8601 | null,
      "reject_reason": str | null,
      "send_status":   null | "sent" | "send_failed"
    }
    N)datetimetimezone)Pathscada_report_approvalsSCADA_REPORT_APPROVALSzscada_report_approvals.jsonSCADA_REPORTS_DIRreportsSCADA_CONFIG_PATHzscada_config.jsonzAapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetawaiting_approvalapprovedrejectedc                  d    t        j                  t        j                        j	                         S N)r   nowr   utc	isoformat     -/var/www/html/togen/scada_report_approvals.py_now_isor   q   s    <<%//11r   c                 F   | rt        |       nt        } | j                         sg S 	 t        j                  | j                               }t        |t              r!|D cg c]  }t        |t              s| c}S g S # t        j                  t        f$ r g cY S w xY wc c}w )z8Load the approval request list ([] when absent/invalid).)r   APPROVALS_FILEis_filejsonloads	read_textJSONDecodeErrorOSError
isinstancelistdict)pathdatars      r   load_approvalsr%   u   s    4:>D<<>	zz$..*+ 8B$7Mt3!z!T2A3USUU   '* 	3s   #A= B4B=BBc                     |rt        |      nt        }	 |j                  t        j                  | d             y# t
        $ r t        j                  d|       Y yw xY w)N   )indentTzFailed to persist %sF)r   r   
write_textr   dumpsr   _LOG	exception)recordsr"   s     r   _save_approvalsr.      sO    4:>D

7156 -t4s   &< AAc                 T    t        |      D ]  }|j                  d      | k(  s|c S  y)z'Return a single request by id, or None.idN)r%   get)
request_idr"   r$   s      r   get_requestr3      s0    D! 55;*$H r   c                     t        t        |             D cg c]  }|j                  d      t        k(  r| c}S c c}w )z/Requests still awaiting approval, newest first.status)reversedr%   r1   STATUS_AWAITING)r"   r$   s     r   pendingr8      s;    t 45 3!uuX/1  3 3 3s   !;c                 2   | rt        |       nt        }|j                         sg S 	 t        j                  |j                               }t        |t              r|j                  d      nd}t        |      S # t        j                  t        f$ r g cY S w xY w)u  Return the configured approver emails (lowercased, de-duped).

    Reads ``report_approvers`` from scada_config.json — the same file the Flask
    UI and the scheduler share — so both processes see one source of truth
    without app.py having to hand the list to the scheduler.
    report_approversN)r   _SCADA_CONFIG_PATHr   r   r   r   r   r   r   r!   r1   normalize_approvers)config_pathr"   cfgraws       r   r:   r:      s     !,41CD<<>	jj)* *4C)>#''$
%DCs##   '* 	s   #A8 8BBc                    t        | t              sg S t               g }}| D ]`  }t        |t              s|j	                         j                         }|s5d|v s:||vs?|j                  |       |j                  |       b |S )zNormalize an approver list: lowercased, trimmed, de-duped, order-stable.

    Also used by app.py to validate the list on save.
    @)r   r    setstrstriploweraddappend)r?   seenoutenorms        r   r<   r<      sz    
 c4 	r#D !S!wwy C4KD$4HHTNJJt Jr   c                 v    t        | t              sy| j                         j                         t	        |      v S )z5True if ``email`` is in the configured approver list.F)r   rC   rD   rE   r:   )emailr=   s     r   is_approverrN      s/    eS!;;= $4[$AAAr   schedule	schedulerT)sourceschedule_idrequested_byr"   r=   notifyc                    i dt        j                         j                  dt               d|	d|
d| d|d|d|d	t	        |xs d
      d|d|d|dt        |xs g       d|dt        ddddddd}t        |      }|j                  |       t        ||       t        |      }d}|r|rt        ||      }|||dS )u   Record an ``awaiting_approval`` request for an EXTERNAL report send and
    (optionally) notify the configured approver(s).

    ``report_file`` is the bare filename of the already-persisted XLSX in the
    reports dir. ``recipients`` is the external recipient list to email on
    approval. Returns ``{"request": record, "approvers": [...],
    "notified": bool}``. ``notified`` is False when no approver is configured
    (caller should surface a loud warning — external delivery is held, not
    sent).
    r0   
created_atrQ   rR   report_namereport_type	date_fromdate_to	row_countr   report_fileattach_namesubject
recipientsrS   r5   actioned_byNactioned_at)reject_reasonsend_statusF)request	approversnotified)uuiduuid4hexr   intr    r7   r%   rG   r.   r:   _notify_approvers)rW   rX   rY   rZ   r[   r\   r]   r^   r_   rQ   rR   rS   r"   r=   rT   recordr-   re   rf   s                      r   enqueue_external_sendrm      sN   djjlhj 	& 	{	
 	{ 	{ 	Y 	7 	Sa( 	{ 	{ 	7 	d:+, 	 	/  	t!" 	t#$ 'F* T"GNN6GT" -IH)$VY7I8LLr   c                 d    | D ]+  }|j                  d      |k(  s|j                  |       |c S  y )Nr0   )r1   update)r-   r2   fieldsr$   s       r   _setrq      s7     55;*$HHVH r   )r"   reports_dirc                $    t        |      }t         fd|D        d      }|ddddS |j                  d      t        k7  rd|d|j                  d       ddS |rt	        |      nt
        }|j                  d	      xs d
}|r|t	        |      j                  k7  rd|ddS ||z  j                         }	 |j                  |j                                |j                         sd|ddS |j                  d      xs g }	|	s9t        | t        |t               d       t        ||       dt         |      ddS |j!                         }
t#        |      }d}t$        Bt%        j&                  |	|j                  dd      ||j                  d      xs ||
t(              }t        | t        |t               |rdnd       t        ||       t+        ||	|       |sdt         |      ddS dt         |      ddS # t        $ r	 d|ddcY S w xY w)al  Approve a request and email the persisted XLSX to its external recipients.

    Returns ``{"ok": bool, "record": record|None, "error": str|None}``. The
    record's ``status`` becomes ``approved`` regardless of send outcome (the
    approval decision stands); ``send_status`` records whether the email went
    out so a failed send can be retried from the UI.
    c              3   L   K   | ]  }|j                  d       k(  s|  ywr0   Nr1   .0r$   r2   s     r   	<genexpr>zapprove.<locals>.<genexpr>        Ct
)B1C   $$NFApproval request not found.okrl   errorr5   Request already .r\    zInvalid report file.z*Report file no longer exists; cannot send.r_   no_recipients)r5   r`   ra   rc   Tr^   SCADA Reportr]   )filenamecontentcontent_typesentsend_failedzCApproved, but the email send failed. Retry from the approvals list.)r%   nextr1   r7   r   REPORTS_DIRnameresolverelative_to
ValueErrorr   rq   STATUS_APPROVEDr   r.   r3   
read_bytes_approved_email_htmlnotificationssend_email_with_attachmentXLSX_CONTENT_TYPE_log_external_send)r2   r`   r"   rr   r-   rl   r	   r   report_pathr_   r#   htmlr   s   `            r   approver     s3    T"GCgCTJF~t6STTzz(.v+FJJx,@+ACE 	E $/d;KG::m$*D44:??*v8NOOT>**,KP 12  vEG 	G L)/RJWj$(*(	* 	&k*d&CdSS!!#D'DD 77

9n=tZZ.6$'8: 	*_+ZtVPGT"vz40{:t'D+, 	, +j$"?$OO?  Pv8NOOPs   7G= =HH)r"   c          	      T    t        |      }t         fd|D        d      }|ddddS |j                  d      t        k7  rd|d|j                  d       ddS t	        | t
        |t               |xs d	j                         xs d
       t        ||       dt         |      ddS )zBReject a request. The report is never sent to external recipients.c              3   L   K   | ]  }|j                  d       k(  s|  ywru   rv   rw   s     r   ry   zreject.<locals>.<genexpr>D  rz   r{   NFr|   r}   r5   r   r   r   )r5   r`   ra   rb   T)
r%   r   r1   r7   rq   STATUS_REJECTEDr   rD   r.   r3   )r2   r`   reasonr"   r-   rl   s   `     r   rejectr   A  s    T"GCgCTJF~t6STTzz(.v+FJJx,@+ACE 	E*_+Z"/C/C/E/MOGT"+j$"?$OOr   c                 x    t        |       j                  dd      j                  dd      j                  dd      S )N&z&amp;<z&lt;>z&gt;)rC   replace)values    r   _escr   T  s1    JWS'"773#7V8LNr   c           	         d}| j                  d      s| j                  d      r-| j                  d      xs d d| j                  d      xs d }d| j                  dd      fd|fd	t        | j                  d
d            fddj                  | j                  d      xs g       fg}dj                  d |D              S )Nr   rY   rZ   u   …u    – ReportrW   z
Date rangeRowsr[   r   zExternal recipientsz, r_   c              3   p   K   | ].  \  }}|r'd j                  t        |      t        |             0 yw)z<tr><td style="padding:4px 12px 4px 0;color:#6b7280;font-size:13px;white-space:nowrap;vertical-align:top;">{lbl}</td><td style="padding:4px 0;color:#111827;font-size:13px;">{val}</td></tr>)lblvalN)formatr   )rx   r   r   s      r   ry   z_detail_rows.<locals>.<genexpr>c  s@      R :BcS	 49$s)4	5Rs   46)r1   rC   join)rl   rngrowss      r   _detail_rowsr   Y  s    
Czz+&**Y"7K(1E2%

98M8VQV7WX	6::mR01	s	VZZQ/01			&**\*B*Hb IJ	D 77 R FJ	R R Rr   c                     t         2t        t         d      r"t        j                  t        |       |||      S dt        |        d| | dS )Nrender_email_shelltitle	body_htmlsubtitlefooter_htmlz<html><body><h1>z</h1>z</body></html>)r   hasattrr   r   r   s       r   _shellr   j  sR     W]<P%Q//u+X#% 	% d5k]%	{;-~VVr   c                  f    t         j                  j                  d      xs dj                  d      S )NTOGEN_BASE_URLzhttps://togen.icastinc.com/)osenvironr1   rstripr   r   r   _togen_base_urlr   r  s(    JJNN+, ,+VVC[9r   c                 <   t         yt                d}dt        |       z   dt        |       dz   }d}d| j	                  dd	       d
}t        ||d|      }	 t        t        j                  |||            S # t        $ r t        j                  d       Y yw xY w)zHEmail the approver(s) that a report awaits their approval. Returns bool.Fz/#tool-scadaz<p style="margin:0;font-size:15px;line-height:1.6;">A SCADA report is awaiting your approval before it can be emailed to external recipients.</p><table role="presentation" cellpadding="0" cellspacing="0" style="margin:12px 0 4px;">uS   </table><p style="margin:14px 0 0;font-size:14px;">Review it in Togen → <a href="u[   " style="color:#0077A8;">SCADA View → Report Notifications → Pending Approvals</a>.</p>z<hr style="margin:8px 0 14px;border:none;border-top:1px solid #e5e7eb;"><p style="font-size:12px;color:#9ca3af;text-align:center;margin:0;line-height:1.6;">You are receiving this because you are a configured SCADA report approver.</p>u!   Approval needed: SCADA report “rW   r   u   ”zSCADA Report ApprovalzFailed to notify approvers)r   r   r   r   r1   r   boolsend_html_email	Exceptionr+   r,   )rl   re   linkbodyfooterr^   r   s          r   rk   rk   w  s     -D	& )5V(<		=@J<  7@7		7 		% 
 2&**]22N1OsSG'4!8&ADM11)WdKLL 34s   A: :BBc                 d    dt        |       z   dz   }d}t        | j                  dd      |d|      S )Nz<p style="margin:0;font-size:15px;line-height:1.6;">The attached SCADA report has been approved and is provided for your records.</p><table role="presentation" cellpadding="0" cellspacing="0" style="margin:12px 0 4px;">z</table>z<hr style="margin:8px 0 14px;border:none;border-top:1px solid #e5e7eb;"><p style="font-size:12px;color:#9ca3af;text-align:center;margin:0;line-height:1.6;">You are receiving this because you are an external recipient of this SCADA report.</p>r^   r   )r   r   r1   )rl   r   r   s      r   r   r     sU    	& )5V(<	= @J	J 		. 
 &**Y7 &* *r   c                     t         y	 |D ]/  }t        j                  d| j                  d       |rdnd|       1 y# t        $ r t        j                  d       Y yw xY w)zABest-effort notification-log entry for the external send (audit).Nzscada_report:r0   r   r   z(Failed to log external SCADA report send)r   _record_sentr1   r   r+   r,   )rl   r_   r   r$   s       r   r   r     sk    C 	6A&&

4 012M16	6  CABCs   4> AAr   )r   )/__doc__r   loggingr   rg   r   r   pathlibr   r   ImportErrorscada_report	getLoggerr+   __file__r   parent	_BASE_DIRr   r1   r   r   getattrr;   r   r7   r   r   r   r%   r.   r3   r8   r:   r<   rN   rm   rq   r   r   r   r   r   r   rk   r   r   r   r   r   <module>r      s  BH   	  '  w12N""$++	JJNN+<<>? 2::>>"5y97LMN
 L-t4 )'' 
 H  &2	V3$$$B .8T'2&*4	,Mf .2t 7PtP P&N
R"W9
8*
Cg
  M
  Ls"   D D DDD('D(