Simulate a patient joining Test Clinic (test-client.gp.veripath.co.uk), booking appointments, and going through the complete payment lifecycle for both self-pay and insurance scenarios. Every step is documented with exact URLs, expected behaviour, and what to verify.
| Role | Username | Password | Purpose |
|---|---|---|---|
| Admin | admin |
(your admin password) | Full system access |
| Patient | insurance_patient |
testpass123 |
Insurance patient with saved card |
| Patient | (new sign-up) | (self-set) | Self-pay patient |
Run once before starting:
python3 manage.py seed_insurance_billing
This creates:
UPDATE insurance_billing_privateinsuranceinvoice
SET status='ready', shortfall_status='none', insurer_payout=0.00, patient_owed=0.00,
stripe_shortfall_intent_id='', total_amount=150.00,
pre_auth_code='PA-998877', icd10_code='M54.5', ccsd_code='A5510'
WHERE id=<invoice_id>;
The patient saves a credit/debit card for future shortfall payments.
Step 1.1: Patient gets auth token
# Admin runs this to get the patient's token
curl -X POST https://test-client.gp.veripath.co.uk/api-token-auth/ \
-d "username=insurance_patient&password=testpass123"
→ Returns: {"token": "..."}
Step 1.2: Create Stripe SetupIntent
curl -X POST https://test-client.gp.veripath.co.uk/insurance-billing/api/patient/card/setup-intent/ \
-H "Authorization: Token <PATIENT_TOKEN>" \
-H "Content-Type: application/json"
→ Returns:
{
"client_secret": "seti_..._secret_...",
"setup_intent_id": "seti_..."
}
What happens: Stripe creates a SetupIntent. In a real frontend, the
client_secretwould be passed to Stripe Elements to collect card details. The card data never touches our server (PCI compliance).
Step 1.3: Create and attach a test card
# Admin creates a test PaymentMethod via Stripe
curl -X POST https://api.stripe.com/v1/payment_methods \
-u "sk_test_..." \
-d type=card \
-d "card[number]"=4242424242424242 \
-d "card[exp_month]"=12 \
-d "card[exp_year]"=2028 \
-d "card[cvc]"=123
In production, this happens on the client side — the frontend confirms the SetupIntent using stripe.confirmCardSetup(client_secret), which returns a payment_method ID.
Step 1.4: Attach payment method to patient record
curl -X POST https://test-client.gp.veripath.co.uk/insurance-billing/api/patient/card/attach/ \
-H "Authorization: Token <PATIENT_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"payment_method_id": "pm_..."}'
→ Returns:
{
"id": 1,
"card_brand": "visa",
"card_last4": "4242",
"is_default": true
}
What happens: The Stripe PaymentMethod ID is stored in
PatientPaymentMethodalong with card metadata (brand, last4, expiry). The card is now "on file" for off-session charges.
Step 1.5: Verify saved cards
curl https://test-client.gp.veripath.co.uk/insurance-billing/api/patient/card/methods/ \
-H "Authorization: Token <PATIENT_TOKEN>"
→ Returns list of saved cards. Can set_default or remove.
What to check:
client_secretinsurance_billing_patientpaymentmethod tableis_default is set correctlyStep 2.1: Navigate to the Insurance Billing Queue
URL: https://test-client.gp.veripath.co.uk/insurance-billing/
Expected:
readyStep 2.2: View the Insurance Details Panel on the Appointment
URL: https://test-client.gp.veripath.co.uk/appointments/<id>/
The seeded appointment opens with an Insurance Details panel containing:
What to check:
payment_method == INSURANCE)Step 3.1: Export batch XML
Click "Download Unsubmitted Claims (XML)" on the queue page.
Expected: Downloads healthcode_batch.xml containing:
<?xml version="1.0" ?>
<HealthcodeInvoiceBatch batchReference="BATCH-20260623-...">
<Invoice>
<InsurerCode>BUPA</InsurerCode>
<PolicyNumber>BI0000XX</PolicyNumber>
<PreAuthCode>PA-998877</PreAuthCode>
<ICD10Code>M54.5</ICD10Code>
<CCSDCode>A5510</CCSDCode>
<AmountDue>150.00</AmountDue>
</Invoice>
</HealthcodeInvoiceBatch>
What to check:
ready to exported in the databaseStep 4.1: Navigate to Shortfall form
Click "Shortfall" on the invoice row.
URL: https://test-client.gp.veripath.co.uk/insurance-billing/<id>/shortfall/
Shows:
Step 4.2: Enter insurer payout
Type 120.00 and click "Save & Calculate Shortfall".
Expected:
shortfall_pendingpendingWhat to check:
patient_owed = total_amount - insurer_payout (150 - 120 = 30)paid with no shortfallStep 5.1: Trigger the charge
Click "Charge" button (POST via form with CSRF protection).
Expected:
paidcollectedstripe_shortfall_intent_id populated in databaseWhat to check:
status=paid, shortfall_status=collectedstripe_shortfall_intent_id is a valid Stripe ID starting with pi_Step 5.2: Test failure fallback (if Stripe key is invalid)
If you disable the Stripe key or use a declined card (pm_card_visa_chargeDeclined):
failedStep 6.1: Create invoice from appointment
URL (POST): https://test-client.gp.veripath.co.uk/appointments/<id>/create-invoice/
INV-2026-XXXXXtotal_amount = treatment costStep 6.2: View invoice list
URL: https://test-client.gp.veripath.co.uk/appointments/invoices/
Expected: Table with all invoices showing Invoice#, Patient, Date, Amount, Status, Actions
Step 6.3: View invoice detail
URL: https://test-client.gp.veripath.co.uk/appointments/<id>/invoice/
Expected:
Step 6.4: Download invoice PDF
Click "Download PDF"
Expected: PDF renders inline in browser showing:
Step 6.5: Send invoice by email
Click "Send" (POST)
Expected:
SENTsent_at timestamp recordedWhat to check:
INV-{year}-{00001}Alternative to online payment — staff records cash/card/cheque:
Step 7.1: Navigate to record payment form
URL: https://test-client.gp.veripath.co.uk/appointments/<id>/record-payment/
Shows:
Step 7.2: Record a cash payment
Select CASH, enter amount, add notes, submit.
Expected:
PaymentTransaction(COMPLETED) createdAppointment.payment_status updated to PAIDKnown Gap (#1): Invoice status is NOT automatically updated to PAID when a manual payment is recorded. The invoice remains DRAFT or SENT.
Step 8.1: Patient views dashboard
curl https://test-client.gp.veripath.co.uk/api/patient/dashboard/ \
-H "Authorization: Token <PATIENT_TOKEN>"
Expected response (relevant billing fields):
{
"upcoming_gp_appointments": [
{
"payment_status": "PAID",
"price_amount": "150.00",
"deposit_amount": null
}
]
}
Step 8.2: Patient views invoices
curl https://test-client.gp.veripath.co.uk/api/patient/invoices/ \
-H "Authorization: Token <PATIENT_TOKEN>"
Expected:
[
{
"id": 1,
"invoice_number": "INV-2026-00001",
"appointment_date": "...",
"status": "SENT",
"total_amount": "150.00",
"payer_name": "John Smith"
}
]
Step 8.3: Patient views saved cards
curl https://test-client.gp.veripath.co.uk/insurance-billing/api/patient/card/methods/ \
-H "Authorization: Token <PATIENT_TOKEN>"
Expected:
[
{
"id": 3,
"card_brand": "visa",
"card_last4": "4242",
"is_default": true
}
]
Step 9.1: Check insurance billing record
SELECT id, status, insurer_name, policy_number, total_amount,
insurer_payout, patient_owed, shortfall_status
FROM insurance_billing_privateinsuranceinvoice;
Expected: Complete record with all phases captured (ready → shortfall_pending → paid/collected).
Step 9.2: Check payment transactions
SELECT id, appointment_id, amount, payment_method, payment_status, recorded_by_id
FROM appointments_paymenttransaction;
Expected: PaymentTransaction(COMPLETED) for the recorded payment, with recorded_by linking to the staff user.
Step 9.3: Check invoices
SELECT id, invoice_number, total_amount, status, sent_at
FROM appointments_invoice;
Expected: Invoice record with correct status and timestamps.
Step 9.4: Check patient payment methods
SELECT id, patient_id, card_brand, card_last4, is_default, is_active
FROM insurance_billing_patientpaymentmethod;
Expected: Saved card with real Stripe PaymentMethod ID.
For a self-pay patient, the flow is simpler:
payment_method=SELF_PAY (default)/appointments/<id>/record-payment/ (cash/card/cheque)/appointments/<id>/create-invoice//appointments/<id>/invoice/send//appointments/<id>/invoice//appointments/<id>/invoice/download/For patients who want to pay by card at the practice:
/appointments/<id>/pay-online/ (or frontend uses checkout.html)POST /appointments/<id>/create-payment-intent/ → returns client_secretstripe.confirmCardPayment(client_secret)/webhooks/stripe/) confirms paymentPaymentTransaction updated to COMPLETEDAppointment.payment_status updated to PAID/api/patient/card/setup-intent/ returns client_secret/api/patient/card/attach/ saves card to database/api/patient/card/methods/ lists saved cardsset_default and remove actions workexportedpatient_owed = total_amount - insurer_payoutshortfall_pendingpaid / collected| # | Gap | Phase Affected |
|---|---|---|
| 1 | Invoice status does not auto-update to PAID when payment recorded | 6 → 7 handoff |
| 2 | No webhook URL for insurance shortfall callbacks | 5 |
| 3 | No auto-overdue cron job for invoices | 6 |
| 4 | No refund flow (Stripe or manual) | — |
| 5 | Invoice email link (base_url) not configured |
6.5 |
| 6 | Accounts dashboard metrics all hardcoded to zero | — |
| 7 | Invoice Generation dashboard view is a stub | — |
| 8 | InvoiceListView shows ALL practices (no tenant isolation) | 6.2 |
| 9 | Stripe Connect fields stored but unused | — |
| 10 | No payment receipt page for patient | 5 |
| 11 | Dental transactions have no recorded_by field |
— |
| 12 | PatientPaymentMethodViewSet uses dict instead of serializer | 1 |