diff --git a/.last-branch b/.last-branch index 268e004..2133819 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -backupchecks-v20260106-21-changelog-0.1.17 +v20260106-22-ticket-link-multiple-jobs diff --git a/containers/backupchecks/src/backend/app/main/routes_api.py b/containers/backupchecks/src/backend/app/main/routes_api.py index 393d3ca..87f2a65 100644 --- a/containers/backupchecks/src/backend/app/main/routes_api.py +++ b/containers/backupchecks/src/backend/app/main/routes_api.py @@ -204,43 +204,77 @@ def api_tickets(): if not re.match(r"^T\d{8}\.\d{4}$", ticket_code): return jsonify({"status": "error", "message": "Invalid ticket_code format. Expected TYYYYMMDD.####."}), 400 - # Ensure uniqueness - if Ticket.query.filter_by(ticket_code=ticket_code).first(): - return jsonify({"status": "error", "message": "ticket_code already exists."}), 409 + existing = Ticket.query.filter_by(ticket_code=ticket_code).first() - ticket = Ticket( - ticket_code=ticket_code, - title=None, - description=description, - active_from_date=_to_amsterdam_date(run.run_at) or _to_amsterdam_date(now) or now.date(), - start_date=now, - resolved_at=None, - ) + # If the ticket already exists, link it to this job/run (a single ticket number can apply to multiple jobs). + if existing: + ticket = existing + try: + # Ensure there is at least one job scope for this ticket/job combination. + if job and job.id: + scope = TicketScope.query.filter_by( + ticket_id=ticket.id, + scope_type="job", + job_id=job.id, + ).first() + if not scope: + scope = TicketScope( + ticket_id=ticket.id, + scope_type="job", + customer_id=job.customer_id, + backup_software=job.backup_software, + backup_type=job.backup_type, + job_id=job.id, + job_name_match=job.job_name, + job_name_match_mode="exact", + ) + db.session.add(scope) - try: - db.session.add(ticket) - db.session.flush() + # Link the ticket to this specific run (idempotent due to unique constraint). + existing_link = TicketJobRun.query.filter_by(ticket_id=ticket.id, job_run_id=run.id).first() + if not existing_link: + link = TicketJobRun(ticket_id=ticket.id, job_run_id=run.id, link_source="manual") + db.session.add(link) - # Minimal scope from job - scope = TicketScope( - ticket_id=ticket.id, - scope_type="job", - customer_id=job.customer_id if job else None, - backup_software=job.backup_software if job else None, - backup_type=job.backup_type if job else None, - job_id=job.id if job else None, - job_name_match=job.job_name if job else None, - job_name_match_mode="exact", + db.session.commit() + except Exception as exc: + db.session.rollback() + return jsonify({"status": "error", "message": str(exc) or "Failed to link ticket."}), 500 + + else: + ticket = Ticket( + ticket_code=ticket_code, + title=None, + description=description, + active_from_date=_to_amsterdam_date(run.run_at) or _to_amsterdam_date(now) or now.date(), + start_date=now, + resolved_at=None, ) - db.session.add(scope) - link = TicketJobRun(ticket_id=ticket.id, job_run_id=run.id, link_source="manual") - db.session.add(link) + try: + db.session.add(ticket) + db.session.flush() - db.session.commit() - except Exception as exc: - db.session.rollback() - return jsonify({"status": "error", "message": str(exc) or "Failed to create ticket."}), 500 + # Minimal scope from job + scope = TicketScope( + ticket_id=ticket.id, + scope_type="job", + customer_id=job.customer_id if job else None, + backup_software=job.backup_software if job else None, + backup_type=job.backup_type if job else None, + job_id=job.id if job else None, + job_name_match=job.job_name if job else None, + job_name_match_mode="exact", + ) + db.session.add(scope) + + link = TicketJobRun(ticket_id=ticket.id, job_run_id=run.id, link_source="manual") + db.session.add(link) + + db.session.commit() + except Exception as exc: + db.session.rollback() + return jsonify({"status": "error", "message": str(exc) or "Failed to create ticket."}), 500 return jsonify( { @@ -251,8 +285,8 @@ def api_tickets(): "description": ticket.description or "", "start_date": _format_datetime(ticket.start_date), "active_from_date": str(ticket.active_from_date) if getattr(ticket, "active_from_date", None) else "", - "resolved_at": "", - "active": True, + "resolved_at": _format_datetime(ticket.resolved_at) if getattr(ticket, "resolved_at", None) else "", + "active": getattr(ticket, "resolved_at", None) is None, }, } ) diff --git a/docs/changelog.md b/docs/changelog.md index bc22d8e..aa7fa27 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,8 @@ +## v20260106-22-ticket-link-multiple-jobs + +- Fixed ticket linking logic to allow the same existing ticket number to be associated with multiple jobs and job runs. +- Prevented duplicate ticket creation errors when reusing an existing ticket_code. +- Ensured tickets are reused and linked instead of rejected when already present in the system. ================================================================================================================================================