First POC valid. 16k lines or so pumped.

This commit is contained in:
2026-05-14 12:38:39 +08:00
commit 2e1a71864c
5 changed files with 455 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.env.local

211
collector.py Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
{
"last_successful_run": "2026-05-14T12:37:01.814813",
"last_rows_fetched": 16187,
"last_rows_upserted": 16187
}