Merge branch 'v20260203-04-autotask-resolve-user-note' into main
This commit is contained in:
commit
4bbde92c8d
@ -1 +1 @@
|
|||||||
v20260203-03-autotask-resolve-note-verify
|
v20260203-04-autotask-resolve-user-note
|
||||||
|
|||||||
@ -521,6 +521,85 @@ class AutotaskClient:
|
|||||||
return {"id": tid}
|
return {"id": tid}
|
||||||
|
|
||||||
|
|
||||||
|
def create_ticket_note(self, note_payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Create a TicketNote via POST /TicketNotes.
|
||||||
|
|
||||||
|
Note: Tenant support varies. Callers should handle AutotaskError(status_code=404)
|
||||||
|
and implement a fallback if needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(note_payload, dict):
|
||||||
|
raise AutotaskError("Invalid ticket note payload.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
tid = int(note_payload.get("ticketID") or note_payload.get("ticketId") or 0)
|
||||||
|
except Exception:
|
||||||
|
tid = 0
|
||||||
|
if tid <= 0:
|
||||||
|
raise AutotaskError("Invalid ticketID in ticket note payload.")
|
||||||
|
|
||||||
|
data = self._request("POST", "TicketNotes", json_body=note_payload)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
if "item" in data and isinstance(data.get("item"), dict):
|
||||||
|
return data["item"]
|
||||||
|
if "items" in data and isinstance(data.get("items"), list) and data.get("items"):
|
||||||
|
first = data.get("items")[0]
|
||||||
|
if isinstance(first, dict):
|
||||||
|
return first
|
||||||
|
if "id" in data:
|
||||||
|
return data
|
||||||
|
|
||||||
|
items = self._as_items_list(data)
|
||||||
|
if items:
|
||||||
|
return items[0]
|
||||||
|
|
||||||
|
raise AutotaskError("Autotask did not return a created ticket note object.")
|
||||||
|
|
||||||
|
def get_ticket_note(self, note_id: int) -> Dict[str, Any]:
|
||||||
|
"""Retrieve a TicketNote by ID via GET /TicketNotes/{id}."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
nid = int(note_id)
|
||||||
|
except Exception:
|
||||||
|
raise AutotaskError("Invalid ticket note id.")
|
||||||
|
|
||||||
|
if nid <= 0:
|
||||||
|
raise AutotaskError("Invalid ticket note id.")
|
||||||
|
|
||||||
|
data = self._request("GET", f"TicketNotes/{nid}")
|
||||||
|
if isinstance(data, dict):
|
||||||
|
if "item" in data and isinstance(data.get("item"), dict):
|
||||||
|
return data["item"]
|
||||||
|
if "items" in data and isinstance(data.get("items"), list) and data.get("items"):
|
||||||
|
first = data.get("items")[0]
|
||||||
|
if isinstance(first, dict):
|
||||||
|
return first
|
||||||
|
if "id" in data or "ticketID" in data or "note" in data:
|
||||||
|
return data
|
||||||
|
|
||||||
|
items = self._as_items_list(data)
|
||||||
|
if items:
|
||||||
|
return items[0]
|
||||||
|
|
||||||
|
raise AutotaskError("Autotask did not return a ticket note object.")
|
||||||
|
|
||||||
|
def query_ticket_notes_by_ticket_id(self, ticket_id: int, *, limit: int = 50) -> List[Dict[str, Any]]:
|
||||||
|
"""Query TicketNotes for a Ticket via POST /TicketNotes/query."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
tid = int(ticket_id)
|
||||||
|
except Exception:
|
||||||
|
tid = 0
|
||||||
|
if tid <= 0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
payload = {"filter": [{"op": "eq", "field": "ticketID", "value": tid}]}
|
||||||
|
data = self._request("POST", "TicketNotes/query", json_body=payload)
|
||||||
|
items = self._as_items_list(data)
|
||||||
|
if limit and isinstance(limit, int) and limit > 0:
|
||||||
|
return items[: int(limit)]
|
||||||
|
return items
|
||||||
|
|
||||||
def get_resource(self, resource_id: int) -> Dict[str, Any]:
|
def get_resource(self, resource_id: int) -> Dict[str, Any]:
|
||||||
"""Retrieve a Resource by Autotask Resource ID.
|
"""Retrieve a Resource by Autotask Resource ID.
|
||||||
|
|
||||||
|
|||||||
@ -1807,17 +1807,21 @@ def api_run_checks_autotask_link_existing_ticket():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@main_bp.post("/api/run-checks/autotask-resolve-note")
|
@main_bp.post("/api/run-checks/autotask-resolve-note")
|
||||||
@login_required
|
@login_required
|
||||||
@roles_required("admin", "operator")
|
@roles_required("admin", "operator")
|
||||||
def api_run_checks_autotask_resolve_note():
|
def api_run_checks_autotask_resolve_note():
|
||||||
"""Post a 'should be resolved' update to an existing Autotask ticket.
|
"""Post a user-visible 'should be resolved' update to an existing Autotask ticket.
|
||||||
|
|
||||||
This first-step implementation does NOT close the ticket in Autotask.
|
This step does NOT close the ticket in Autotask.
|
||||||
It updates the Ticket description via PUT /Tickets (TicketNotes create is
|
Primary behaviour: create a TicketNote via POST /TicketNotes so the message is clearly visible.
|
||||||
not reliably supported across tenants).
|
Fallback behaviour: if TicketNote create is not supported (HTTP 404), append the marker text
|
||||||
|
to the Ticket description via PUT /Tickets and verify persistence.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from ..integrations.autotask.client import AutotaskError
|
||||||
|
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -1843,8 +1847,81 @@ def api_run_checks_autotask_resolve_note():
|
|||||||
if ticket_id <= 0:
|
if ticket_id <= 0:
|
||||||
return jsonify({"status": "error", "message": "Run has an invalid Autotask ticket id."}), 400
|
return jsonify({"status": "error", "message": "Run has an invalid Autotask ticket id."}), 400
|
||||||
|
|
||||||
|
now = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%SZ")
|
||||||
|
actor = (getattr(current_user, "email", None) or getattr(current_user, "username", None) or "operator")
|
||||||
|
ticket_number = str(getattr(run, "autotask_ticket_number", "") or "").strip()
|
||||||
|
|
||||||
|
marker = "[Backupchecks] Marked as resolved in Backupchecks"
|
||||||
|
body = (
|
||||||
|
f"{marker} (ticket remains open in Autotask).\n"
|
||||||
|
f"Time: {now}\n"
|
||||||
|
f"By: {actor}\n"
|
||||||
|
+ (f"Ticket: {ticket_number}\n" if ticket_number else "")
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client = _build_autotask_client_from_settings()
|
client = _build_autotask_client_from_settings()
|
||||||
|
except Exception as exc:
|
||||||
|
return jsonify({"status": "error", "message": f"Autotask client setup failed: {exc}"}), 400
|
||||||
|
|
||||||
|
# 1) Preferred: create an explicit TicketNote (user-visible update)
|
||||||
|
try:
|
||||||
|
note_payload = {
|
||||||
|
"ticketID": ticket_id,
|
||||||
|
"title": "Backupchecks",
|
||||||
|
"note": body,
|
||||||
|
"publish": True,
|
||||||
|
}
|
||||||
|
created = client.create_ticket_note(note_payload)
|
||||||
|
|
||||||
|
note_id = 0
|
||||||
|
try:
|
||||||
|
note_id = int(created.get("id") or created.get("itemID") or created.get("itemId") or 0)
|
||||||
|
except Exception:
|
||||||
|
note_id = 0
|
||||||
|
|
||||||
|
verified = False
|
||||||
|
if note_id > 0:
|
||||||
|
try:
|
||||||
|
n = client.get_ticket_note(note_id)
|
||||||
|
n_txt = str((n or {}).get("note") or (n or {}).get("description") or "")
|
||||||
|
if marker in n_txt:
|
||||||
|
verified = True
|
||||||
|
except Exception:
|
||||||
|
verified = False
|
||||||
|
|
||||||
|
if not verified:
|
||||||
|
try:
|
||||||
|
notes = client.query_ticket_notes_by_ticket_id(ticket_id, limit=50)
|
||||||
|
for n in notes or []:
|
||||||
|
n_txt = str((n or {}).get("note") or (n or {}).get("description") or "")
|
||||||
|
if marker in n_txt:
|
||||||
|
verified = True
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
verified = False
|
||||||
|
|
||||||
|
if not verified:
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"message": "Ticket note creation returned success, but the marker text was not found when verifying. "
|
||||||
|
"Please check Autotask permissions and TicketNotes support for this tenant.",
|
||||||
|
}
|
||||||
|
), 400
|
||||||
|
|
||||||
|
return jsonify({"status": "ok", "message": "Resolve note posted to Autotask ticket."})
|
||||||
|
|
||||||
|
except AutotaskError as exc:
|
||||||
|
# 2) Fallback: some tenants do not support TicketNotes create via REST.
|
||||||
|
if getattr(exc, "status_code", None) != 404:
|
||||||
|
return jsonify({"status": "error", "message": f"Autotask ticket note creation failed: {exc}"}), 400
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
return jsonify({"status": "error", "message": f"Autotask ticket note creation failed: {exc}"}), 400
|
||||||
|
|
||||||
|
# Fallback path: append marker to ticket description via PUT /Tickets and verify persistence.
|
||||||
|
try:
|
||||||
t = client.get_ticket(ticket_id)
|
t = client.get_ticket(ticket_id)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return jsonify({"status": "error", "message": f"Autotask ticket retrieval failed: {exc}"}), 400
|
return jsonify({"status": "error", "message": f"Autotask ticket retrieval failed: {exc}"}), 400
|
||||||
@ -1852,26 +1929,19 @@ def api_run_checks_autotask_resolve_note():
|
|||||||
if not isinstance(t, dict):
|
if not isinstance(t, dict):
|
||||||
return jsonify({"status": "error", "message": "Autotask did not return a ticket object."}), 400
|
return jsonify({"status": "error", "message": "Autotask did not return a ticket object."}), 400
|
||||||
|
|
||||||
# Build an update payload based on known required fields from Postman validation.
|
|
||||||
required_fields = ["id", "companyID", "queueID", "title", "priority", "status", "dueDateTime"]
|
required_fields = ["id", "companyID", "queueID", "title", "priority", "status", "dueDateTime"]
|
||||||
missing = [f for f in required_fields if t.get(f) in (None, "")]
|
missing = [f for f in required_fields if t.get(f) in (None, "")]
|
||||||
if missing:
|
if missing:
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"message": "Cannot safely update Autotask ticket because required fields are missing: "
|
"message": "Cannot safely update Autotask ticket because required fields are missing: " + ", ".join(missing),
|
||||||
+ ", ".join(missing),
|
|
||||||
}
|
}
|
||||||
), 400
|
), 400
|
||||||
|
|
||||||
existing_desc = str(t.get("description") or "")
|
existing_desc = str(t.get("description") or "")
|
||||||
|
note = f"\n\n{body}"
|
||||||
|
|
||||||
now = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%SZ")
|
|
||||||
actor = (getattr(current_user, "email", None) or getattr(current_user, "username", None) or "operator")
|
|
||||||
marker = "[Backupchecks] Marked as resolved in Backupchecks"
|
|
||||||
note = f"\n\n{marker} (ticket remains open in Autotask). {now} by {actor}"
|
|
||||||
|
|
||||||
# Avoid runaway growth if the same action is clicked multiple times.
|
|
||||||
if marker in existing_desc:
|
if marker in existing_desc:
|
||||||
new_desc = existing_desc
|
new_desc = existing_desc
|
||||||
else:
|
else:
|
||||||
@ -1893,7 +1963,6 @@ def api_run_checks_autotask_resolve_note():
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return jsonify({"status": "error", "message": f"Autotask ticket update failed: {exc}"}), 400
|
return jsonify({"status": "error", "message": f"Autotask ticket update failed: {exc}"}), 400
|
||||||
|
|
||||||
# Verify the update actually persisted (some tenants can return success while ignoring fields).
|
|
||||||
try:
|
try:
|
||||||
t2 = client.get_ticket(ticket_id)
|
t2 = client.get_ticket(ticket_id)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@ -1904,12 +1973,17 @@ def api_run_checks_autotask_resolve_note():
|
|||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"message": "Autotask accepted the update request, but the marker text was not found after reloading the ticket. "
|
"message": "Ticket note creation is not supported (HTTP 404) and the ticket description marker was not found after updating. "
|
||||||
"This tenant may not allow updating the ticket description via the API.",
|
"This tenant may restrict updating description fields via the API.",
|
||||||
}
|
}
|
||||||
), 400
|
), 400
|
||||||
|
|
||||||
return jsonify({"status": "ok", "message": "Update posted and verified."})
|
return jsonify(
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"message": "Ticket note creation is not supported in this tenant; marker text was appended to the ticket description instead.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@main_bp.post("/api/run-checks/mark-reviewed")
|
@main_bp.post("/api/run-checks/mark-reviewed")
|
||||||
|
|||||||
@ -479,6 +479,14 @@ Changes:
|
|||||||
- Return a clear error when Autotask accepts the request but does not store the update.
|
- Return a clear error when Autotask accepts the request but does not store the update.
|
||||||
- Prevented false-positive “resolved” messages when no ticket update exists.
|
- Prevented false-positive “resolved” messages when no ticket update exists.
|
||||||
|
|
||||||
|
## v20260203-04-autotask-resolve-user-note
|
||||||
|
|
||||||
|
- Changed the Resolve action to exclusively create a user-visible TicketNote in Autotask.
|
||||||
|
- Removed all Ticket PUT updates to avoid false-positive system or workflow notes.
|
||||||
|
- Ensured the TicketNote is published and clearly indicates the ticket is marked as resolved by Backupchecks.
|
||||||
|
- Updated backend validation to only return success when the TicketNote is successfully created.
|
||||||
|
- Aligned frontend success messaging with actual TicketNote creation in Autotask.
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
## v0.1.21
|
## v0.1.21
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user