First POC valid. 16k lines or so pumped.
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.env.local
|
||||
211
collector.py
Normal file
211
collector.py
Normal file
@@ -0,0 +1,211 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pyodbc
|
||||
import pymysql
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
ENV_PATH = BASE_DIR / ".env.local"
|
||||
QUERY_PATH = BASE_DIR / "query.sql"
|
||||
LOG_DIR = BASE_DIR / "logs"
|
||||
STATE_DIR = BASE_DIR / "state"
|
||||
STATE_FILE = STATE_DIR / "last_run.json"
|
||||
|
||||
LOG_DIR.mkdir(exist_ok=True)
|
||||
STATE_DIR.mkdir(exist_ok=True)
|
||||
|
||||
load_dotenv(ENV_PATH)
|
||||
|
||||
|
||||
logging.basicConfig(
|
||||
filename=LOG_DIR / "collector.log",
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
)
|
||||
|
||||
console = logging.StreamHandler(sys.stdout)
|
||||
console.setLevel(logging.INFO)
|
||||
console.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
|
||||
logging.getLogger().addHandler(console)
|
||||
|
||||
|
||||
def get_env(name: str, required: bool = True, default: str | None = None) -> str | None:
|
||||
value = os.getenv(name, default)
|
||||
if required and not value:
|
||||
raise RuntimeError(f"Missing required environment variable: {name}")
|
||||
return value
|
||||
|
||||
|
||||
def connect_psa_sql():
|
||||
server = get_env("PSA_SQL_SERVER")
|
||||
database = get_env("PSA_SQL_DATABASE")
|
||||
username = get_env("PSA_SQL_USERNAME")
|
||||
password = get_env("PSA_SQL_PASSWORD")
|
||||
driver = get_env("PSA_SQL_DRIVER", required=False, default="ODBC Driver 18 for SQL Server")
|
||||
trust_cert = get_env("PSA_SQL_TRUST_CERT", required=False, default="yes")
|
||||
|
||||
conn_str = (
|
||||
f"DRIVER={{{driver}}};"
|
||||
f"SERVER={server};"
|
||||
f"DATABASE={database};"
|
||||
f"UID={username};"
|
||||
f"PWD={password};"
|
||||
f"TrustServerCertificate={trust_cert};"
|
||||
)
|
||||
|
||||
return pyodbc.connect(conn_str)
|
||||
|
||||
|
||||
def connect_mariadb():
|
||||
return pymysql.connect(
|
||||
host=get_env("MARIADB_HOST"),
|
||||
port=int(get_env("MARIADB_PORT", required=False, default="3306")),
|
||||
user=get_env("MARIADB_USERNAME"),
|
||||
password=get_env("MARIADB_PASSWORD"),
|
||||
database=get_env("MARIADB_DATABASE"),
|
||||
charset="utf8mb4",
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
autocommit=False,
|
||||
)
|
||||
|
||||
|
||||
def read_query() -> str:
|
||||
if not QUERY_PATH.exists():
|
||||
raise FileNotFoundError(f"Missing query file: {QUERY_PATH}")
|
||||
return QUERY_PATH.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def fetch_psa_rows():
|
||||
query = read_query()
|
||||
|
||||
logging.info("Connecting to PSA SQL source")
|
||||
with connect_psa_sql() as conn:
|
||||
cursor = conn.cursor()
|
||||
logging.info("Executing PSA query")
|
||||
cursor.execute(query)
|
||||
|
||||
columns = [col[0] for col in cursor.description]
|
||||
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||
|
||||
logging.info("Fetched %s rows from PSA SQL source", len(rows))
|
||||
return rows
|
||||
|
||||
|
||||
def upsert_rows(rows):
|
||||
if not rows:
|
||||
logging.info("No rows to upsert")
|
||||
return 0
|
||||
|
||||
sql = """
|
||||
INSERT INTO psa_ticket_fact (
|
||||
`id`,
|
||||
`ticket_number`,
|
||||
`company_name`,
|
||||
`board`,
|
||||
`summary`,
|
||||
`status`,
|
||||
`date_opened`,
|
||||
`date_last_updated`,
|
||||
`date_closed`,
|
||||
`hours_actual`,
|
||||
`hours_billable`,
|
||||
`type`,
|
||||
`subtype`,
|
||||
`priority`,
|
||||
`ticket_owner`,
|
||||
`resolved_flag`,
|
||||
`closed_flag`,
|
||||
`collected_at`
|
||||
)
|
||||
VALUES (
|
||||
%(id)s,
|
||||
%(Ticket_Number)s,
|
||||
%(Company_Name)s,
|
||||
%(Board)s,
|
||||
%(Summary)s,
|
||||
%(Status)s,
|
||||
%(date_opened)s,
|
||||
%(date_last_updated)s,
|
||||
%(date_closed)s,
|
||||
%(Hours_Actual)s,
|
||||
%(Hours_Billable)s,
|
||||
%(Type)s,
|
||||
%(SubType)s,
|
||||
%(Priority)s,
|
||||
%(Ticket_Owner)s,
|
||||
%(Resolved_Flag)s,
|
||||
%(Closed_Flag)s,
|
||||
NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`ticket_number` = VALUES(`ticket_number`),
|
||||
`company_name` = VALUES(`company_name`),
|
||||
`board` = VALUES(`board`),
|
||||
`summary` = VALUES(`summary`),
|
||||
`status` = VALUES(`status`),
|
||||
`date_opened` = VALUES(`date_opened`),
|
||||
`date_last_updated` = VALUES(`date_last_updated`),
|
||||
`date_closed` = VALUES(`date_closed`),
|
||||
`hours_actual` = VALUES(`hours_actual`),
|
||||
`hours_billable` = VALUES(`hours_billable`),
|
||||
`type` = VALUES(`type`),
|
||||
`subtype` = VALUES(`subtype`),
|
||||
`priority` = VALUES(`priority`),
|
||||
`ticket_owner` = VALUES(`ticket_owner`),
|
||||
`resolved_flag` = VALUES(`resolved_flag`),
|
||||
`closed_flag` = VALUES(`closed_flag`),
|
||||
`collected_at` = NOW();
|
||||
"""
|
||||
|
||||
logging.info("Connecting to MariaDB destination")
|
||||
|
||||
with connect_mariadb() as conn:
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.executemany(sql, rows)
|
||||
|
||||
conn.commit()
|
||||
logging.info("Upserted %s rows into MariaDB", len(rows))
|
||||
return len(rows)
|
||||
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
|
||||
def write_state(rows_fetched: int, rows_upserted: int):
|
||||
state = {
|
||||
"last_successful_run": datetime.now().isoformat(),
|
||||
"last_rows_fetched": rows_fetched,
|
||||
"last_rows_upserted": rows_upserted,
|
||||
}
|
||||
|
||||
STATE_FILE.write_text(json.dumps(state, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def main():
|
||||
logging.info("Starting PSA gap analysis collector")
|
||||
|
||||
try:
|
||||
rows = fetch_psa_rows()
|
||||
upserted = upsert_rows(rows)
|
||||
write_state(len(rows), upserted)
|
||||
|
||||
logging.info(
|
||||
"Collector completed successfully. Rows fetched: %s. Rows upserted: %s",
|
||||
len(rows),
|
||||
upserted,
|
||||
)
|
||||
|
||||
except Exception as ex:
|
||||
logging.exception("Collector failed: %s", ex)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
95
logs/collector.log
Normal file
95
logs/collector.log
Normal file
@@ -0,0 +1,95 @@
|
||||
2026-05-14 12:28:06,043 [INFO] Starting PSA gap analysis collector
|
||||
2026-05-14 12:28:06,044 [INFO] Connecting to PSA SQL source
|
||||
2026-05-14 12:28:06,044 [ERROR] Collector failed: Missing required environment variable: PSA_SQL_SERVER
|
||||
Traceback (most recent call last):
|
||||
File "C:\Users\btaylor\Git\PSA-Gap-Analysis\collector.py", line 184, in main
|
||||
rows = fetch_psa_rows()
|
||||
File "C:\Users\btaylor\Git\PSA-Gap-Analysis\collector.py", line 88, in fetch_psa_rows
|
||||
with connect_psa_sql() as conn:
|
||||
~~~~~~~~~~~~~~~^^
|
||||
File "C:\Users\btaylor\Git\PSA-Gap-Analysis\collector.py", line 46, in connect_psa_sql
|
||||
server = get_env("PSA_SQL_SERVER")
|
||||
File "C:\Users\btaylor\Git\PSA-Gap-Analysis\collector.py", line 41, in get_env
|
||||
raise RuntimeError(f"Missing required environment variable: {name}")
|
||||
RuntimeError: Missing required environment variable: PSA_SQL_SERVER
|
||||
2026-05-14 12:28:24,743 [INFO] Starting PSA gap analysis collector
|
||||
2026-05-14 12:28:24,743 [INFO] Connecting to PSA SQL source
|
||||
2026-05-14 12:28:24,751 [ERROR] Collector failed: ('IM002', '[IM002] [Microsoft][ODBC Driver Manager] Data source name not found and no default driver specified (0) (SQLDriverConnect)')
|
||||
Traceback (most recent call last):
|
||||
File "C:\Users\btaylor\Git\PSA-Gap-Analysis\collector.py", line 184, in main
|
||||
rows = fetch_psa_rows()
|
||||
File "C:\Users\btaylor\Git\PSA-Gap-Analysis\collector.py", line 88, in fetch_psa_rows
|
||||
with connect_psa_sql() as conn:
|
||||
~~~~~~~~~~~~~~~^^
|
||||
File "C:\Users\btaylor\Git\PSA-Gap-Analysis\collector.py", line 62, in connect_psa_sql
|
||||
return pyodbc.connect(conn_str)
|
||||
~~~~~~~~~~~~~~^^^^^^^^^^
|
||||
pyodbc.InterfaceError: ('IM002', '[IM002] [Microsoft][ODBC Driver Manager] Data source name not found and no default driver specified (0) (SQLDriverConnect)')
|
||||
2026-05-14 12:30:19,695 [INFO] Starting PSA gap analysis collector
|
||||
2026-05-14 12:30:19,695 [INFO] Connecting to PSA SQL source
|
||||
2026-05-14 12:31:05,045 [ERROR] Collector failed: ('08001', '[08001] [Microsoft][ODBC Driver 17 for SQL Server]Named Pipes Provider: Could not open a connection to SQL Server [64]. (64) (SQLDriverConnect); [08001] [Microsoft][ODBC Driver 17 for SQL Server]Login timeout expired (0); [08001] [Microsoft][ODBC Driver 17 for SQL Server]A network-related or instance-specific error has occurred while establishing a connection to SQL Server. Server is not found or not accessible. Check if instance name is correct and if SQL Server is configured to allow remote connections. For more information see SQL Server Books Online. (64)')
|
||||
Traceback (most recent call last):
|
||||
File "C:\Users\btaylor\Git\PSA-Gap-Analysis\collector.py", line 184, in main
|
||||
rows = fetch_psa_rows()
|
||||
File "C:\Users\btaylor\Git\PSA-Gap-Analysis\collector.py", line 88, in fetch_psa_rows
|
||||
with connect_psa_sql() as conn:
|
||||
~~~~~~~~~~~~~~~^^
|
||||
File "C:\Users\btaylor\Git\PSA-Gap-Analysis\collector.py", line 62, in connect_psa_sql
|
||||
return pyodbc.connect(conn_str)
|
||||
~~~~~~~~~~~~~~^^^^^^^^^^
|
||||
pyodbc.OperationalError: ('08001', '[08001] [Microsoft][ODBC Driver 17 for SQL Server]Named Pipes Provider: Could not open a connection to SQL Server [64]. (64) (SQLDriverConnect); [08001] [Microsoft][ODBC Driver 17 for SQL Server]Login timeout expired (0); [08001] [Microsoft][ODBC Driver 17 for SQL Server]A network-related or instance-specific error has occurred while establishing a connection to SQL Server. Server is not found or not accessible. Check if instance name is correct and if SQL Server is configured to allow remote connections. For more information see SQL Server Books Online. (64)')
|
||||
2026-05-14 12:31:52,251 [INFO] Starting PSA gap analysis collector
|
||||
2026-05-14 12:31:52,251 [INFO] Connecting to PSA SQL source
|
||||
2026-05-14 12:31:52,372 [INFO] Executing PSA query
|
||||
2026-05-14 12:31:56,310 [INFO] Fetched 16187 rows from PSA SQL source
|
||||
2026-05-14 12:31:56,312 [INFO] Connecting to MariaDB destination
|
||||
2026-05-14 12:31:56,397 [ERROR] Collector failed: 'ticket_id'
|
||||
Traceback (most recent call last):
|
||||
File "C:\Users\btaylor\Git\PSA-Gap-Analysis\collector.py", line 185, in main
|
||||
upserted = upsert_rows(rows)
|
||||
File "C:\Users\btaylor\Git\PSA-Gap-Analysis\collector.py", line 159, in upsert_rows
|
||||
cursor.executemany(sql, rows)
|
||||
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
|
||||
File "C:\Users\btaylor\AppData\Local\Programs\Python\Python313\Lib\site-packages\pymysql\cursors.py", line 195, in executemany
|
||||
self.rowcount = sum(self.execute(query, arg) for arg in args)
|
||||
~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "C:\Users\btaylor\AppData\Local\Programs\Python\Python313\Lib\site-packages\pymysql\cursors.py", line 195, in <genexpr>
|
||||
self.rowcount = sum(self.execute(query, arg) for arg in args)
|
||||
~~~~~~~~~~~~^^^^^^^^^^^^
|
||||
File "C:\Users\btaylor\AppData\Local\Programs\Python\Python313\Lib\site-packages\pymysql\cursors.py", line 155, in execute
|
||||
query = self.mogrify(query, args)
|
||||
File "C:\Users\btaylor\AppData\Local\Programs\Python\Python313\Lib\site-packages\pymysql\cursors.py", line 133, in mogrify
|
||||
query = query % self._escape_args(args, conn)
|
||||
~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
KeyError: 'ticket_id'
|
||||
2026-05-14 12:33:17,682 [INFO] Starting PSA gap analysis collector
|
||||
2026-05-14 12:33:17,682 [INFO] Connecting to PSA SQL source
|
||||
2026-05-14 12:33:17,778 [INFO] Executing PSA query
|
||||
2026-05-14 12:33:20,725 [INFO] Fetched 16187 rows from PSA SQL source
|
||||
2026-05-14 12:33:20,727 [INFO] Connecting to MariaDB destination
|
||||
2026-05-14 12:33:20,773 [ERROR] Collector failed: 'company_name'
|
||||
Traceback (most recent call last):
|
||||
File "C:\Users\btaylor\Git\PSA-Gap-Analysis\collector.py", line 185, in main
|
||||
upserted = upsert_rows(rows)
|
||||
File "C:\Users\btaylor\Git\PSA-Gap-Analysis\collector.py", line 159, in upsert_rows
|
||||
cursor.executemany(sql, rows)
|
||||
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
|
||||
File "C:\Users\btaylor\AppData\Local\Programs\Python\Python313\Lib\site-packages\pymysql\cursors.py", line 195, in executemany
|
||||
self.rowcount = sum(self.execute(query, arg) for arg in args)
|
||||
~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "C:\Users\btaylor\AppData\Local\Programs\Python\Python313\Lib\site-packages\pymysql\cursors.py", line 195, in <genexpr>
|
||||
self.rowcount = sum(self.execute(query, arg) for arg in args)
|
||||
~~~~~~~~~~~~^^^^^^^^^^^^
|
||||
File "C:\Users\btaylor\AppData\Local\Programs\Python\Python313\Lib\site-packages\pymysql\cursors.py", line 155, in execute
|
||||
query = self.mogrify(query, args)
|
||||
File "C:\Users\btaylor\AppData\Local\Programs\Python\Python313\Lib\site-packages\pymysql\cursors.py", line 133, in mogrify
|
||||
query = query % self._escape_args(args, conn)
|
||||
~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
KeyError: 'company_name'
|
||||
2026-05-14 12:36:04,850 [INFO] Starting PSA gap analysis collector
|
||||
2026-05-14 12:36:04,851 [INFO] Connecting to PSA SQL source
|
||||
2026-05-14 12:36:04,937 [INFO] Executing PSA query
|
||||
2026-05-14 12:36:09,779 [INFO] Fetched 16187 rows from PSA SQL source
|
||||
2026-05-14 12:36:09,782 [INFO] Connecting to MariaDB destination
|
||||
2026-05-14 12:37:01,814 [INFO] Upserted 16187 rows into MariaDB
|
||||
2026-05-14 12:37:01,816 [INFO] Collector completed successfully. Rows fetched: 16187. Rows upserted: 16187
|
||||
143
query.sql
Normal file
143
query.sql
Normal file
@@ -0,0 +1,143 @@
|
||||
SELECT
|
||||
s.TicketNbr AS 'id'
|
||||
,s.TicketNbr AS 'Ticket_Number'
|
||||
,s.company_name AS 'Company_Name'
|
||||
,co.company_id AS 'Company_ID'
|
||||
,s.contact_name AS 'Contact'
|
||||
,s.source AS 'Source'
|
||||
,s.Site_Name AS customer_site
|
||||
,s.team_name
|
||||
,s.Territory
|
||||
,s.location AS 'Location'
|
||||
,s.board_name AS 'Board'
|
||||
,s.summary AS 'Summary'
|
||||
,s.status_description AS 'Status'
|
||||
,CAST(s.date_entered AS DATETIME) AS 'date_opened'
|
||||
,CAST(s.last_update AS DATETIME) AS 'date_last_updated'
|
||||
,CAST(s.Date_Required AS DATETIME) AS 'Date_Required'
|
||||
,COALESCE(s.Responded_Minutes,0) + COALESCE(s.Responded_Skipped_Minutes,0) AS 'time_to_acknowledgement_minutes'
|
||||
,CASE
|
||||
WHEN s.Date_Responded_UTC is NULL THEN NULL
|
||||
ELSE COALESCE(s.Responded_Minutes,0) + COALESCE(s.Responded_Skipped_Minutes,0)
|
||||
END AS 'time_to_response_minutes'
|
||||
,CAST(s.date_responded_utc AS DATETIME) AS acknowledgement_date
|
||||
,CAST(s.date_responded_utc AS DATETIME) AS response_date
|
||||
,CASE WHEN s.Date_Responded_UTC IS NOT NULL THEN (CASE WHEN s.Responded_Minutes + s.Responded_Skipped_Minutes <=
|
||||
(CASE WHEN slap.Responded_Hours IS NOT NULL THEN slap.Responded_Hours ELSE sla.Responded_Hours END
|
||||
* 60) THEN 'Met' ELSE 'Unmet' END) ELSE NULL END AS 'metresponsesla'
|
||||
,CAST(CAST (s.Resplan_Minutes + s.Resplan_Skipped_Minutes + s.Responded_Minutes AS DECIMAL (9, 2)) / 60.0 AS DECIMAL(10,2)) AS 'time_to_resolution_plan(hours)'
|
||||
,CAST(s.date_resplan_utc AS DATETIME) AS 'resolution_plan_date'
|
||||
,CASE WHEN s.Date_Resplan_UTC IS NOT NULL THEN (CASE WHEN s.Resplan_Minutes + s.Resplan_Skipped_Minutes + s.Responded_Minutes <=
|
||||
(CASE WHEN slap.Resplan_Hours IS NOT NULL THEN slap.Resplan_Hours ELSE sla.Resplan_Hours END
|
||||
* 60) THEN 'Met' ELSE 'Unmet' END) ELSE NULL END AS 'metresplansla'
|
||||
,CAST(CAST (s.Resolved_Minutes + s.Resplan_Minutes + s.Responded_Minutes AS DECIMAL (9, 2)) / 60.0 AS DECIMAL(10,2)) AS 'time_to_resolution(hours)'
|
||||
,CAST(s.date_resolved_utc AS DATETIME) AS 'resolution_date'
|
||||
,CASE WHEN s.Date_Resolved_UTC IS NOT NULL THEN (CASE WHEN s.Resolved_Minutes + s.resplan_minutes + s.Responded_Minutes <=
|
||||
(CASE WHEN slap.Resolution_Hours IS NOT NULL THEN slap.Resolution_Hours ELSE sla.Resolution_Hours END
|
||||
* 60) THEN 'Met' ELSE 'Unmet' END) ELSE NULL END AS 'metresolutionsla'
|
||||
,CAST(s.date_closed AS DATETIME) AS 'date_closed'
|
||||
,CASE
|
||||
When DATEDIFF(DD, s.date_entered, s.date_closed) = 0 Then 'Y'
|
||||
ELSE 'N'
|
||||
END AS 'same_day_close'
|
||||
,CASE
|
||||
WHEN DATEDIFF(DD,s.Date_Responded_UTC,s.Date_Resolved_UTC) = 0 Then 'Y'
|
||||
ELSE 'N'
|
||||
END AS 'same_day_resolved'
|
||||
,s.servicetype AS 'Type'
|
||||
,s.servicesubtype AS 'SubType'
|
||||
,s.servicesubtypeitem AS 'Service_Item'
|
||||
,s.urgency AS 'Priority'
|
||||
,s.Severity
|
||||
,s.Impact
|
||||
,s.Hours_Actual
|
||||
,s.Hours_Budget
|
||||
,s.Hours_Scheduled
|
||||
,s.Hours_Billable
|
||||
,s.Hours_NonBillable
|
||||
,s.Hours_Invoiced
|
||||
,s.Hours_Agreement
|
||||
,s.agreement_name
|
||||
,CASE WHEN s.Date_Resolved_UTC IS NOT NULL THEN CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, s.Date_Resolved_UTC)/24.0, 0) AS NUMERIC)
|
||||
ELSE CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) END AS 'Age (Days)'
|
||||
,CASE WHEN s.Date_Resolved_UTC IS NULL THEN
|
||||
CASE WHEN
|
||||
CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) < 8 THEN '1. Current'
|
||||
WHEN
|
||||
CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) > 7 AND CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) < 15 THEN '2. 1 Week'
|
||||
WHEN
|
||||
CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) > 14 AND CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) < 22 THEN '3. 2 Weeks'
|
||||
WHEN
|
||||
CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) > 21 AND CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) < 30 THEN '4. 3 Weeks'
|
||||
WHEN
|
||||
CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) > 29 THEN '5. 1+ Month'
|
||||
END
|
||||
ELSE 'Resolved' END AS 'Unresolved Age (Weeks)'
|
||||
,CASE WHEN s.date_closed IS NULL THEN
|
||||
CASE WHEN
|
||||
CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) < 8 THEN '1. Current'
|
||||
WHEN
|
||||
CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) > 7 AND CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) < 15 THEN '2. 1 Week'
|
||||
WHEN
|
||||
CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) > 14 AND CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) < 22 THEN '3. 2 Weeks'
|
||||
WHEN
|
||||
CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) > 21 AND CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) < 30 THEN '4. 3 Weeks'
|
||||
WHEN
|
||||
CAST(ROUND(DATEDIFF(Hour, s.Date_Entered, CURRENT_TIMESTAMP)/24.0, 0) AS NUMERIC) > 29 THEN '5. 1+ Month'
|
||||
END
|
||||
ELSE 'Resolved' END AS 'Unsolved Age (Weeks)'
|
||||
,CASE
|
||||
WHEN s.date_responded_utc is NULL THEN '1. Pending Response'
|
||||
WHEN s.Date_Resplan_UTC is NULL THEN '2. Pending Resolution Plan'
|
||||
WHEN s.Date_Resolved_UTC is NULL THEN '3. Pending Resolution'
|
||||
WHEN s.Date_Resolved_UTC is NOT NULL THEN '4. Resolved'
|
||||
WHEN s.date_closed is NOT NULL THEN '5. Closed'
|
||||
END AS SLA_Escalation_Status
|
||||
,LOWER(s.resolved_by) AS Resolved_By
|
||||
,LOWER(s.closed_by) AS Closed_By
|
||||
,LOWER(s.Responded_By) AS responded_by
|
||||
,LOWER(town.member_id) AS Ticket_Owner_ID
|
||||
,CAST(town.First_Name AS varchar) + ' ' + cast(town.Last_Name AS varchar) AS Ticket_Owner
|
||||
,CASE
|
||||
WHEN (s.date_resolved_utc IS NOT NULL) THEN 'Resolved'
|
||||
ELSE 'Open'
|
||||
END AS Resolved_Flag
|
||||
,CASE
|
||||
WHEN (s.Closed_Flag = 'True') THEN 'Closed'
|
||||
ELSE 'Open'
|
||||
END AS Closed_Flag
|
||||
,CASE
|
||||
WHEN (SELECT recid FROM Schedule
|
||||
WHERE close_flag != 'True'
|
||||
AND Schedule_Type_RecID = 4
|
||||
AND s.ticketnbr = RecID
|
||||
GROUP BY recid) IS NOT NULL THEN 'Y'
|
||||
ELSE 'N'
|
||||
END AS Is_Assigned
|
||||
,CASE
|
||||
WHEN s.config_recids IS NULL THEN 'False'
|
||||
ELSE 'True'
|
||||
END AS Config_Attached
|
||||
,COALESCE((SELECT COUNT(t.time_recid) FROM time_entry t WHERE t.sr_service_recid = s.sr_service_recid),0) AS Time_Entry_Count
|
||||
,al.agr_type_desc AS agreement_type
|
||||
,al.Agreement_Status
|
||||
,CAST(CASE
|
||||
WHEN s.Closed_Flag = 'True' THEN NULL
|
||||
ELSE (SELECT MIN(Date_Time_Start_UTC)
|
||||
FROM schedule
|
||||
WHERE recid = s.sr_service_Recid
|
||||
AND schedule_type_recid = 4
|
||||
AND close_flag = 'False')
|
||||
END AS DATETIME) AS next_date
|
||||
FROM v_rpt_service AS s
|
||||
LEFT JOIN company AS co ON s.company_recid = co.company_recid
|
||||
LEFT JOIN SR_SLA AS sla ON s.SR_SLA_RecID = sla.SR_SLA_RECID
|
||||
LEFT JOIN SR_Urgency AS sru ON s.SR_Urgency_RecID = sru.SR_Urgency_RecID
|
||||
LEFT JOIN SR_SLAPriority AS slap ON s.SR_SLA_RecID = slap.SR_SLA_RecID AND sru.SR_Urgency_RecID = slap.SR_Urgency_RecID
|
||||
LEFT JOIN member AS town ON town.member_recid = s.Ticket_Owner_RecID
|
||||
LEFT JOIN v_rpt_agreementlist AS al ON al.agr_header_recid = s.agr_header_recid
|
||||
INNER JOIN SR_Service AS sr ON s.ticketnbr = sr.sr_service_Recid
|
||||
|
||||
WHERE
|
||||
(DATEADD(DAY, -210 , Current_Timestamp) <= sr.Last_Update)
|
||||
AND sr.Parent_Recid is null
|
||||
5
state/last_run.json
Normal file
5
state/last_run.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"last_successful_run": "2026-05-14T12:37:01.814813",
|
||||
"last_rows_fetched": 16187,
|
||||
"last_rows_upserted": 16187
|
||||
}
|
||||
Reference in New Issue
Block a user