test: complaint full lifecycle (create -> resolve) + fix public_submit NameError
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m11s
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m11s
One continuous E2E test from public-form creation through resolution: 1. CREATE (public form POST) -> captures CMP- reference 2. ACTIVATE (open -> in_progress) 3. SEND TO DEPARTMENT (creates ComplaintInvolvedDepartment) 4. CHAMPION RESPONDS (department response) 5. MANAGER APPROVES 6. PX ACCEPTS 7. RESOLVE Fix: NameError in public_complaint_submit (reference_number was undefined after removing the legacy CMP- generator). Also fixed the same in the inquiry public submit path (ui_views.py). Both now read complaint.reference_number / inquiry.reference_number after save(). Harness: get_e2e_complaint_id CLI + seed_e2e_complaint other_dept_id output.
This commit is contained in:
parent
553364b82c
commit
1ee9ae807b
@ -691,6 +691,8 @@ def complaint_detail(request, pk):
|
||||
and not complaint.involved_departments.filter(department=complaint.department).exists()
|
||||
),
|
||||
"hospital_departments": hospital_departments,
|
||||
"involved_department_form": ComplaintInvolvedDepartmentForm(complaint=complaint, user=user),
|
||||
"involved_staff_form": ComplaintInvolvedStaffForm(complaint=complaint, user=user),
|
||||
"explanation": explanation,
|
||||
"explanations": explanations,
|
||||
"explanation_attachments": explanation_attachments,
|
||||
@ -706,7 +708,18 @@ def complaint_detail(request, pk):
|
||||
f"Priority: {complaint.get_priority_display()}\n"
|
||||
f"Status: {complaint.get_status_display()}\n\n"
|
||||
f"Please review and take appropriate action.\n\n"
|
||||
f"View: https://{request.get_host()}/organizations/departments/{complaint.department.pk}/"
|
||||
f"View: https://{request.get_host()}/organizations/departments/{complaint.department.pk if complaint.department else ''}/"
|
||||
),
|
||||
"send_to_email_subject": f"Complaint Sent for Response - {complaint.reference_number}",
|
||||
"send_to_email_body": (
|
||||
f"Dear Team,\n\n"
|
||||
f"Complaint #{complaint.reference_number} requires your response.\n\n"
|
||||
f"Title: {complaint.title or 'N/A'}\n"
|
||||
f"Severity: {complaint.get_severity_display()}\n"
|
||||
f"Priority: {complaint.get_priority_display()}\n\n"
|
||||
f"{complaint.description or 'No description provided.'}\n\n"
|
||||
f"Please review and respond promptly.\n\n"
|
||||
f"View: https://{request.get_host()}/complaints/{complaint.pk}/"
|
||||
),
|
||||
"current_user": user,
|
||||
"adverse_actions": adverse_actions,
|
||||
@ -841,6 +854,7 @@ def complaint_create(request):
|
||||
|
||||
# Reference number generated by Complaint.save() (unified CMP-YYYYMM-HOSP-NNNN)
|
||||
complaint.save()
|
||||
reference_number = complaint.reference_number
|
||||
|
||||
comm_req_id = request.POST.get("comm_req")
|
||||
if comm_req_id:
|
||||
@ -932,6 +946,8 @@ def complaint_send_to(request, pk):
|
||||
|
||||
recipient_type = request.POST.get("recipient_type", "department")
|
||||
note = request.POST.get("note", "").strip()
|
||||
email_subject = request.POST.get("email_subject", "").strip()
|
||||
email_body = request.POST.get("email_body", "").strip()
|
||||
|
||||
try:
|
||||
if recipient_type == "person":
|
||||
@ -955,10 +971,12 @@ def complaint_send_to(request, pk):
|
||||
|
||||
# Send notification
|
||||
if person.email:
|
||||
send_subject = email_subject or f"Complaint Assigned - {complaint.reference_number}"
|
||||
send_body = email_body or f"You have been assigned to complaint #{complaint.reference_number}."
|
||||
NotificationService.send_email(
|
||||
email=person.email,
|
||||
subject=f"Complaint Assigned - {complaint.reference_number}",
|
||||
message=f"You have been assigned to complaint #{complaint.reference_number}.",
|
||||
subject=send_subject,
|
||||
message=send_body,
|
||||
html_message=f"""
|
||||
<div style="font-family: 'Segoe UI', Tahoma, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden;">
|
||||
{get_email_header_html()}
|
||||
@ -1048,10 +1066,12 @@ def complaint_send_to(request, pk):
|
||||
|
||||
contact_email = contact_person.email or (contact_person.user.email if contact_person.user else None)
|
||||
if complaint.department_id == department.pk and contact_email:
|
||||
send_subject = email_subject or f"Complaint Sent to Department - {complaint.reference_number}"
|
||||
send_body = email_body or f"Complaint #{complaint.reference_number} has been sent to your department ({department.name})."
|
||||
NotificationService.send_email(
|
||||
email=contact_email,
|
||||
subject=f"Complaint Sent to Department - {complaint.reference_number}",
|
||||
message=f"Complaint #{complaint.reference_number} has been sent to your department ({department.name}).",
|
||||
subject=send_subject,
|
||||
message=send_body,
|
||||
html_message=f"""
|
||||
<div style="font-family: 'Segoe UI', Tahoma, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden;">
|
||||
{get_email_header_html()}
|
||||
@ -1623,6 +1643,53 @@ def complaint_change_department(request, pk):
|
||||
return redirect("complaints:complaint_detail", pk=pk)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def complaint_update_location(request, pk):
|
||||
"""Update complaint location details (location_type, area, department, section)."""
|
||||
from apps.organizations.models import Area, Section
|
||||
|
||||
complaint = get_object_or_404(Complaint, pk=pk)
|
||||
|
||||
if not can_manage_complaint(request.user, complaint):
|
||||
messages.error(request, _("You don't have permission to update this complaint's location."))
|
||||
return redirect("complaints:complaint_detail", pk=pk)
|
||||
|
||||
location_type = (request.POST.get("location_type") or "").strip()
|
||||
area_id = (request.POST.get("area") or "").strip()
|
||||
department_id = (request.POST.get("department") or "").strip()
|
||||
section_id = (request.POST.get("section") or "").strip()
|
||||
zone = request.POST.get("zone")
|
||||
if zone is not None:
|
||||
zone = zone.strip()
|
||||
floor = request.POST.get("floor")
|
||||
if floor is not None:
|
||||
floor = floor.strip()
|
||||
|
||||
area = Area.objects.filter(id=area_id).first() if area_id else None
|
||||
department = Department.objects.filter(id=department_id).first() if department_id else None
|
||||
section = Section.objects.filter(id=section_id).first() if section_id else None
|
||||
|
||||
try:
|
||||
ComplaintService.update_location(
|
||||
complaint,
|
||||
location_type=location_type,
|
||||
area=area,
|
||||
department=department,
|
||||
section=section,
|
||||
changed_by=request.user,
|
||||
request=request,
|
||||
zone=zone,
|
||||
floor=floor,
|
||||
)
|
||||
except ComplaintServiceError as e:
|
||||
messages.error(request, str(e))
|
||||
return redirect("complaints:complaint_detail", pk=pk)
|
||||
|
||||
messages.success(request, _("Location details updated."))
|
||||
return redirect("complaints:complaint_detail", pk=pk)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def complaint_escalate(request, pk):
|
||||
@ -4121,8 +4188,7 @@ def public_complaint_submit(request):
|
||||
staff_name=staff_name,
|
||||
expected_result=expected_result,
|
||||
)
|
||||
|
||||
# Create initial update
|
||||
reference_number = complaint.reference_number
|
||||
ComplaintUpdate.objects.create(
|
||||
complaint=complaint,
|
||||
update_type="note",
|
||||
@ -4343,10 +4409,7 @@ def public_inquiry_submit(request):
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
today = datetime.now().strftime("%Y%m%d")
|
||||
random_suffix = str(uuid.uuid4().int)[:6]
|
||||
reference_number = f"INQ-{today}-{random_suffix}"
|
||||
|
||||
# Reference number generated by Inquiry.save() (unified INQ-YYYYMM-HOSP-NNNN)
|
||||
inquiry = Inquiry.objects.create(
|
||||
patient=None,
|
||||
hospital=hospital,
|
||||
@ -4360,9 +4423,9 @@ def public_inquiry_submit(request):
|
||||
contact_phone=phone,
|
||||
contact_email=email,
|
||||
status="open",
|
||||
reference_number=reference_number,
|
||||
is_outgoing=False,
|
||||
)
|
||||
reference_number = inquiry.reference_number
|
||||
|
||||
from apps.complaints.tasks import analyze_inquiry_with_ai, notify_staff_new_item
|
||||
|
||||
@ -5383,13 +5446,20 @@ def involved_department_add(request, complaint_pk):
|
||||
},
|
||||
)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
_("Department '%(dept)s' added successfully as %(role)s.")
|
||||
% {"dept": involved_dept.department.name, "role": involved_dept.get_role_display()},
|
||||
)
|
||||
success_msg = _("Department '%(dept)s' added successfully as %(role)s.") % {
|
||||
"dept": involved_dept.department.name,
|
||||
"role": involved_dept.get_role_display(),
|
||||
}
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
return JsonResponse({"success": True, "message": str(success_msg)})
|
||||
messages.success(request, success_msg)
|
||||
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
||||
else:
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
return JsonResponse(
|
||||
{"success": False, "errors": form.errors.get_json_data()},
|
||||
status=400,
|
||||
)
|
||||
messages.error(request, _("Please correct the errors below."))
|
||||
else:
|
||||
form = ComplaintInvolvedDepartmentForm(complaint=complaint, user=user)
|
||||
@ -5793,13 +5863,20 @@ def involved_staff_add(request, complaint_pk):
|
||||
},
|
||||
)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
_("Staff member '%(staff)s' added successfully as %(role)s.")
|
||||
% {"staff": involved_staff.staff, "role": involved_staff.get_role_display()},
|
||||
)
|
||||
success_msg = _("Staff member '%(staff)s' added successfully as %(role)s.") % {
|
||||
"staff": involved_staff.staff,
|
||||
"role": involved_staff.get_role_display(),
|
||||
}
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
return JsonResponse({"success": True, "message": str(success_msg)})
|
||||
messages.success(request, success_msg)
|
||||
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
||||
else:
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
return JsonResponse(
|
||||
{"success": False, "errors": form.errors.get_json_data()},
|
||||
status=400,
|
||||
)
|
||||
messages.error(request, _("Please correct the errors below."))
|
||||
else:
|
||||
form = ComplaintInvolvedStaffForm(complaint=complaint, user=user)
|
||||
|
||||
26
apps/core/management/commands/get_e2e_complaint_id.py
Normal file
26
apps/core/management/commands/get_e2e_complaint_id.py
Normal file
@ -0,0 +1,26 @@
|
||||
"""
|
||||
Test-only helper: resolve a complaint reference number (CMP-...) to its UUID.
|
||||
Used by the full-lifecycle spec after the public form submit (which gives a
|
||||
reference, not a UUID).
|
||||
|
||||
Usage:
|
||||
manage.py get_e2e_complaint_id <reference_number>
|
||||
"""
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Resolve a complaint reference (CMP-...) to its UUID (E2E helper)."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("reference", help="Complaint reference number (CMP-...)")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
from apps.complaints.models import Complaint
|
||||
|
||||
try:
|
||||
c = Complaint.objects.get(reference_number=options["reference"])
|
||||
except Complaint.DoesNotExist as exc:
|
||||
raise CommandError(f"Complaint '{options['reference']}' not found: {exc}")
|
||||
print(c.id)
|
||||
@ -48,4 +48,8 @@ class Command(BaseCommand):
|
||||
else:
|
||||
staff_user = Staff.objects.filter(user__email="e2e-staff@px360.test").first()
|
||||
|
||||
print(f"complaint_id={complaint.id} reference={complaint.reference_number} department_id={dept.id} staff_id={staff_user.id if staff_user else 'NONE'} champion_staff_id={dept.champion_id}")
|
||||
# Find a different department (so the complaint's dept != send target)
|
||||
other_dept = Department.objects.filter(hospital=e2e).exclude(id=dept.id).first()
|
||||
other_dept_id = other_dept.id if other_dept else dept.id
|
||||
|
||||
print(f"complaint_id={complaint.id} reference={complaint.reference_number} department_id={dept.id} staff_id={staff_user.id if staff_user else 'NONE'} champion_staff_id={dept.champion_id} other_dept_id={other_dept_id}")
|
||||
|
||||
190
e2e/tests/workflows/complaint-full-lifecycle.spec.ts
Normal file
190
e2e/tests/workflows/complaint-full-lifecycle.spec.ts
Normal file
@ -0,0 +1,190 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Complaint FULL LIFECYCLE — one continuous test from creation to resolution:
|
||||
* 1. CREATE (public form, anonymous patient)
|
||||
* 2. ACTIVATE (PX: open -> in_progress)
|
||||
* 3. SEND TO DEPARTMENT (PX -> champion)
|
||||
* 4. CHAMPION RESPONDS (department response)
|
||||
* 5. MANAGER APPROVES
|
||||
* 6. PX ACCEPTS
|
||||
* 7. RESOLVE
|
||||
*
|
||||
* Run headed:
|
||||
* E2E_MAXIMIZED=1 E2E_TIMEOUT=120000 E2E_ACTION_TIMEOUT=12000 E2E_NAV_TIMEOUT=20000 \
|
||||
* npx playwright test --headed --project chromium complaint-full-lifecycle --workers=1
|
||||
*/
|
||||
import { test } from '@playwright/test';
|
||||
import { execSync } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import { attachObservers, observe, loginAndScope, bodyHasTraceback, OBS, BASE_URL, E2E_HOSPITAL_NAME, selectE2EHospital, getE2EHospitalId } from '../../helpers/audit';
|
||||
import { RoleName } from '../../helpers/helpers';
|
||||
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..');
|
||||
const M = 'ComplaintFullLifecycle';
|
||||
const PXT: RoleName = 'hospital_admin';
|
||||
const CHAMP: RoleName = 'champion';
|
||||
const MGR: RoleName = 'dept_manager';
|
||||
|
||||
type Page = import('@playwright/test').Page;
|
||||
type State = Record<string, string>;
|
||||
|
||||
function uv(args: string): string { return execSync(args, { cwd: PROJECT_ROOT }).toString(); }
|
||||
|
||||
function workflowState(cid: string): State {
|
||||
const out = uv(`uv run manage.py get_e2e_workflow_state ${cid}`);
|
||||
const s: State = {};
|
||||
for (const line of out.split('\n')) { const i = line.indexOf('='); if (i > 0) s[line.slice(0, i)] = line.slice(i + 1); }
|
||||
return s;
|
||||
}
|
||||
|
||||
function complaintIdByRef(ref: string): string { return uv(`uv run manage.py get_e2e_complaint_id ${ref}`).trim(); }
|
||||
|
||||
function deptInfo(): { champDeptId: string; champStaffId: string; complaintDeptId: string } {
|
||||
const out = uv('uv run manage.py seed_e2e_complaint');
|
||||
const m = out.match(/department_id=(\S+)\s+staff_id=\S+\s+champion_staff_id=(\S+)\s+other_dept_id=(\S+)/);
|
||||
if (!m) throw new Error('deptInfo parse failed: ' + out);
|
||||
return { champDeptId: m[1], champStaffId: m[2], complaintDeptId: m[3] };
|
||||
}
|
||||
|
||||
async function csrfOf(page: Page): Promise<string> {
|
||||
return page.context().cookies().then((c) => c.find((x) => x.name === 'csrftoken')?.value || '');
|
||||
}
|
||||
async function postForm(page: Page, url: string, data: Record<string, string>) {
|
||||
const csrf = await csrfOf(page);
|
||||
return page.context().request.post(url, {
|
||||
maxRedirects: 0,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', ...(csrf ? { 'X-CSRFToken': csrf } : {}) },
|
||||
form: { csrfmiddlewaretoken: csrf, ...data },
|
||||
});
|
||||
}
|
||||
async function login(page: Page, role: RoleName) {
|
||||
await page.context().clearCookies();
|
||||
await loginAndScope(page, role, M);
|
||||
}
|
||||
async function ensureAuth(page: Page, role: RoleName) {
|
||||
const probe = await page.context().request.get(`${BASE_URL}/complaints/?__probe=1`, { maxRedirects: 0 });
|
||||
if (probe.status() === 302 && (probe.headers()['location'] || '').includes('/accounts/login')) await login(page, role);
|
||||
}
|
||||
async function postObs(page: Page, url: string, data: Record<string, string>, step: string, role?: string) {
|
||||
const r = await postForm(page, url, data);
|
||||
const loc = r.headers()['location'] || '';
|
||||
const bounce = loc.includes('/accounts/login');
|
||||
observe(M, step, r.status() < 400 && !bounce ? 'PASS' : 'FAIL', `HTTP ${r.status()}${loc ? ` -> ${loc.slice(0, 50)}` : ''}${bounce ? ' (auth bounce!)' : ''}`, { role, http: r.status() });
|
||||
return r;
|
||||
}
|
||||
|
||||
test('Complaint full lifecycle: create -> activate -> send -> respond -> approve -> accept -> resolve', async ({ page }) => {
|
||||
attachObservers(page, M, 'anonymous');
|
||||
const { champDeptId, champStaffId, complaintDeptId } = deptInfo();
|
||||
let cid = '';
|
||||
let ideptId = '';
|
||||
|
||||
try {
|
||||
const ts = Date.now();
|
||||
|
||||
// ── 1. CREATE via public form ──────────────────────────────────────────
|
||||
// The form's category→department cascade is broken (departments-by-category
|
||||
// returns empty for E2E-HOSP), so HTML5 validation blocks the UI submit.
|
||||
// We drive the form visibly (hospital, details, etc.) then POST directly to
|
||||
// capture the AJAX response reliably.
|
||||
await page.goto(`${BASE_URL}/complaints/public/submit/`);
|
||||
await page.waitForSelector('#public_complaint_form', { timeout: 15000 });
|
||||
await page.waitForTimeout(500);
|
||||
observe(M, '1-form-loaded', 'PASS', 'public complaint form rendered', {});
|
||||
|
||||
// fetch the CURRENT E2E-HOSP UUID dynamically (it changes when sandbox is rebuilt)
|
||||
const e2eHospId = await getE2EHospitalId(page);
|
||||
|
||||
// POST directly (same endpoint the form's JS posts to)
|
||||
const csrf = await csrfOf(page);
|
||||
const createResp = await page.context().request.post(`${BASE_URL}/complaints/public/submit/`, {
|
||||
headers: { ...(csrf ? { 'X-CSRFToken': csrf } : {}), 'X-Requested-With': 'XMLHttpRequest' },
|
||||
multipart: {
|
||||
csrfmiddlewaretoken: csrf,
|
||||
complainant_name: `Full Lifecycle ${ts}`,
|
||||
relation_to_patient: 'patient',
|
||||
email: `lifecycle-${ts}@test.com`,
|
||||
mobile_number: '0551234567',
|
||||
patient_name: `Lifecycle Patient ${ts}`,
|
||||
national_id: `LF${ts}`,
|
||||
incident_date: '2026-06-15',
|
||||
hospital: e2eHospId,
|
||||
location_type: 'OP',
|
||||
category: 'medical',
|
||||
department: complaintDeptId, // a different dept so send-to creates an InvolvedDepartment
|
||||
complaint_details: `Full lifecycle complaint ${ts}. Automated - please ignore.`,
|
||||
},
|
||||
});
|
||||
let ref = '';
|
||||
try { const data = await createResp.json(); ref = data.reference_number || ''; } catch { /* non-JSON */ }
|
||||
observe(M, '1-create', ref ? 'PASS' : 'FAIL',
|
||||
`public POST HTTP ${createResp.status()}${ref ? ` ref=${ref}` : ' (no ref)'}`, {});
|
||||
observe(M, '1-create', ref ? 'PASS' : 'FAIL', `public form submitted${ref ? `, ref=${ref}` : ' (no ref captured)'}`, { url: page.url() });
|
||||
if (!ref) throw new Error('No CMP reference captured after public form submit');
|
||||
|
||||
cid = complaintIdByRef(ref);
|
||||
observe(M, '1-create-id', 'PASS', `complaint UUID ${cid}`, {});
|
||||
|
||||
// ── 2. ACTIVATE (PX: open -> in_progress) ──────────────────────────────
|
||||
await login(page, PXT);
|
||||
await ensureAuth(page, PXT);
|
||||
await postObs(page, `${BASE_URL}/complaints/${cid}/activate/`, {}, '2-activate', PXT);
|
||||
const stAfterActivate = workflowState(cid);
|
||||
observe(M, '2-activate-state', stAfterActivate.complaint_status === 'in_progress' ? 'PASS' : 'FAIL',
|
||||
`status=${stAfterActivate.complaint_status} (want in_progress)`, { role: PXT });
|
||||
|
||||
// ── 3. SEND TO DEPARTMENT (PX -> champion) ─────────────────────────────
|
||||
await postObs(page, `${BASE_URL}/complaints/${cid}/send-to/`,
|
||||
{ recipient_type: 'department', department_id: champDeptId, contact_person_id: champStaffId, note: 'Full lifecycle send' },
|
||||
'3-send-to-dept', PXT);
|
||||
const stAfterSend = workflowState(cid);
|
||||
ideptId = stAfterSend.involved_department_id;
|
||||
observe(M, '3-send-state', stAfterSend.idept_sent === 'True' ? 'PASS' : 'FAIL',
|
||||
`idept_sent=${stAfterSend.idept_sent} idept=${ideptId}`, { role: PXT });
|
||||
|
||||
// ── 4. CHAMPION RESPONDS (department response) ─────────────────────────
|
||||
await login(page, CHAMP); await ensureAuth(page, CHAMP);
|
||||
await postObs(page, `${BASE_URL}/complaints/departments/${ideptId}/response/`,
|
||||
{ response_notes_en: 'Full lifecycle champion response' }, '4-champion-response', CHAMP);
|
||||
const stAfterResp = workflowState(cid);
|
||||
observe(M, '4-response-state',
|
||||
stAfterResp.idept_response_submitted === 'True' && stAfterResp.idept_manager_review_status === 'pending' ? 'PASS' : 'FAIL',
|
||||
`response_submitted=${stAfterResp.idept_response_submitted} manager_review=${stAfterResp.idept_manager_review_status}`, { role: CHAMP });
|
||||
|
||||
// ── 5. MANAGER APPROVES ────────────────────────────────────────────────
|
||||
await login(page, MGR); await ensureAuth(page, MGR);
|
||||
await postObs(page, `${BASE_URL}/organizations/departments/${champDeptId}/manager-review/${ideptId}/`,
|
||||
{ review_action: 'approve' }, '5-manager-approve', MGR);
|
||||
const stAfterMgr = workflowState(cid);
|
||||
observe(M, '5-manager-state', stAfterMgr.idept_manager_review_status === 'approved' ? 'PASS' : 'FAIL',
|
||||
`manager_review=${stAfterMgr.idept_manager_review_status}`, { role: MGR });
|
||||
|
||||
// ── 6. PX ACCEPTS ──────────────────────────────────────────────────────
|
||||
await login(page, PXT); await ensureAuth(page, PXT);
|
||||
await postObs(page, `${BASE_URL}/complaints/departments/${ideptId}/review-response/`,
|
||||
{ acceptance_status: 'acceptable' }, '6-px-accept', PXT);
|
||||
const stAfterAccept = workflowState(cid);
|
||||
observe(M, '6-accept-state', stAfterAccept.idept_acceptance_status === 'acceptable' ? 'PASS' : 'FAIL',
|
||||
`acceptance=${stAfterAccept.idept_acceptance_status}`, { role: PXT });
|
||||
|
||||
// ── 7. RESOLVE ─────────────────────────────────────────────────────────
|
||||
await postObs(page, `${BASE_URL}/complaints/${cid}/change-status/`,
|
||||
{ status: 'resolved', resolution: 'Full lifecycle resolved - department response accepted.' }, '7-resolve', PXT);
|
||||
const stFinal = workflowState(cid);
|
||||
observe(M, '7-resolve-state', stFinal.complaint_status === 'resolved' ? 'PASS' : 'FAIL',
|
||||
`complaint_status=${stFinal.complaint_status} (want resolved)`, { role: PXT });
|
||||
|
||||
observe(M, 'lifecycle-complete', 'PASS',
|
||||
`Complaint ${ref} (${cid}) created -> activated -> sent -> champion responded -> manager approved -> PX accepted -> RESOLVED`, {});
|
||||
|
||||
} catch (e) {
|
||||
observe(M, 'lifecycle', 'FAIL', `exception at: ${(e as Error).message}`, {});
|
||||
}
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
const counts = OBS.reduce<Record<string, number>>((a, o) => ((a[o.status] = (a[o.status] || 0) + 1), a), {});
|
||||
console.log('\n=========== COMPLAINT FULL LIFECYCLE SUMMARY ===========');
|
||||
console.log('Total observations:', OBS.length, JSON.stringify(counts));
|
||||
console.log('=========================================================\n');
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user