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}
|
||||
|
||||
|
||||
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]:
|
||||
"""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")
|
||||
@login_required
|
||||
@roles_required("admin", "operator")
|
||||
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.
|
||||
It updates the Ticket description via PUT /Tickets (TicketNotes create is
|
||||
not reliably supported across tenants).
|
||||
This step does NOT close the ticket in Autotask.
|
||||
Primary behaviour: create a TicketNote via POST /TicketNotes so the message is clearly visible.
|
||||
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 {}
|
||||
|
||||
try:
|
||||
@ -1843,8 +1847,81 @@ def api_run_checks_autotask_resolve_note():
|
||||
if ticket_id <= 0:
|
||||
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:
|
||||
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)
|
||||
except Exception as exc:
|
||||
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):
|
||||
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"]
|
||||
missing = [f for f in required_fields if t.get(f) in (None, "")]
|
||||
if missing:
|
||||
return jsonify(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Cannot safely update Autotask ticket because required fields are missing: "
|
||||
+ ", ".join(missing),
|
||||
"message": "Cannot safely update Autotask ticket because required fields are missing: " + ", ".join(missing),
|
||||
}
|
||||
), 400
|
||||
|
||||
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:
|
||||
new_desc = existing_desc
|
||||
else:
|
||||
@ -1893,7 +1963,6 @@ def api_run_checks_autotask_resolve_note():
|
||||
except Exception as exc:
|
||||
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:
|
||||
t2 = client.get_ticket(ticket_id)
|
||||
except Exception as exc:
|
||||
@ -1904,12 +1973,17 @@ def api_run_checks_autotask_resolve_note():
|
||||
return jsonify(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Autotask accepted the update request, but the marker text was not found after reloading the ticket. "
|
||||
"This tenant may not allow updating the ticket description via the API.",
|
||||
"message": "Ticket note creation is not supported (HTTP 404) and the ticket description marker was not found after updating. "
|
||||
"This tenant may restrict updating description fields via the API.",
|
||||
}
|
||||
), 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")
|
||||
|
||||
@ -479,6 +479,14 @@ Changes:
|
||||
- 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.
|
||||
|
||||
## 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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user