- Added GitHub spec-kit for development workflow - Standardized file signatures to Descomplicar® format - Updated development configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
399 lines
16 KiB
PHP
399 lines
16 KiB
PHP
/**
|
|
* Descomplicar® Crescimento Digital
|
|
* https://descomplicar.pt
|
|
*/
|
|
|
|
<?php
|
|
/**
|
|
* Integration tests for Automatic Billing Generation (User Story 4).
|
|
*
|
|
* These tests validate complete user stories and MUST FAIL initially (TDD RED phase).
|
|
*
|
|
* @package KiviCare_API\Tests\Integration
|
|
*/
|
|
|
|
/**
|
|
* Billing automation integration tests.
|
|
*
|
|
* User Story: Automatic billing generation based on encounters and services
|
|
*/
|
|
class Test_Billing_Automation extends KiviCare_API_Test_Case {
|
|
|
|
/**
|
|
* Test automatic billing generation workflow.
|
|
*
|
|
* @test
|
|
*/
|
|
public function test_automatic_billing_generation_workflow() {
|
|
// This test will fail initially as billing automation isn't implemented
|
|
$this->markTestIncomplete( 'Automatic billing generation not implemented yet - TDD RED phase' );
|
|
|
|
// ARRANGE: Setup complete billing scenario
|
|
$clinic_id = $this->create_test_clinic();
|
|
|
|
global $wpdb;
|
|
|
|
// Create services with prices
|
|
$service_ids = array();
|
|
$services = array(
|
|
array( 'name' => 'General Consultation', 'price' => '75.00', 'type' => 'consultation' ),
|
|
array( 'name' => 'Blood Pressure Check', 'price' => '15.00', 'type' => 'procedure' ),
|
|
array( 'name' => 'Prescription Review', 'price' => '25.00', 'type' => 'consultation' ),
|
|
);
|
|
|
|
foreach ( $services as $service ) {
|
|
$wpdb->insert( $wpdb->prefix . 'kc_services', array(
|
|
'name' => $service['name'],
|
|
'price' => $service['price'],
|
|
'type' => $service['type'],
|
|
'status' => 1,
|
|
'created_at' => current_time( 'mysql' ),
|
|
));
|
|
$service_ids[] = $wpdb->insert_id;
|
|
}
|
|
|
|
// Map doctor and patient to clinic
|
|
$wpdb->insert( $wpdb->prefix . 'kc_doctor_clinic_mappings', array( 'doctor_id' => $this->doctor_user, 'clinic_id' => $clinic_id ) );
|
|
$wpdb->insert( $wpdb->prefix . 'kc_patient_clinic_mappings', array( 'patient_id' => $this->patient_user, 'clinic_id' => $clinic_id ) );
|
|
|
|
// STEP 1: Create appointment with services
|
|
$appointment_data = array(
|
|
'appointment_start_date' => gmdate( 'Y-m-d', strtotime( '+1 day' ) ),
|
|
'appointment_start_time' => '14:30:00',
|
|
'appointment_end_date' => gmdate( 'Y-m-d', strtotime( '+1 day' ) ),
|
|
'appointment_end_time' => '15:30:00',
|
|
'doctor_id' => $this->doctor_user,
|
|
'patient_id' => $this->patient_user,
|
|
'clinic_id' => $clinic_id,
|
|
'visit_type' => 'consultation',
|
|
'services' => array( $service_ids[0], $service_ids[1] ), // Consultation + BP Check
|
|
);
|
|
|
|
$appointment_response = $this->make_request( '/wp-json/kivicare/v1/appointments', 'POST', $appointment_data, $this->receptionist_user );
|
|
$this->assertRestResponse( $appointment_response, 201 );
|
|
$appointment_id = $appointment_response->get_data()['id'];
|
|
|
|
// Verify service mappings were created
|
|
$service_mappings = $wpdb->get_results(
|
|
$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}kc_appointment_service_mapping WHERE appointment_id = %d", $appointment_id )
|
|
);
|
|
$this->assertCount( 2, $service_mappings );
|
|
|
|
// STEP 2: Doctor creates encounter (this should trigger billing)
|
|
$encounter_data = array(
|
|
'appointment_id' => $appointment_id,
|
|
'description' => 'Patient consultation completed. Blood pressure checked and found to be within normal range.',
|
|
'diagnosis' => 'Routine health check - Normal findings',
|
|
'treatment_plan' => 'Continue current lifestyle, return in 6 months',
|
|
'status' => 1,
|
|
);
|
|
|
|
$encounter_response = $this->make_request( '/wp-json/kivicare/v1/encounters', 'POST', $encounter_data, $this->doctor_user );
|
|
$this->assertRestResponse( $encounter_response, 201 );
|
|
$encounter_id = $encounter_response->get_data()['id'];
|
|
|
|
// STEP 3: Verify automatic bill generation
|
|
$bill = $wpdb->get_row(
|
|
$wpdb->prepare(
|
|
"SELECT * FROM {$wpdb->prefix}kc_bills WHERE encounter_id = %d AND appointment_id = %d",
|
|
$encounter_id,
|
|
$appointment_id
|
|
)
|
|
);
|
|
|
|
$this->assertNotNull( $bill, 'Bill should be automatically generated when encounter is created' );
|
|
$this->assertEquals( $encounter_id, $bill->encounter_id );
|
|
$this->assertEquals( $appointment_id, $bill->appointment_id );
|
|
$this->assertEquals( $clinic_id, $bill->clinic_id );
|
|
$this->assertEquals( 'unpaid', $bill->payment_status );
|
|
|
|
// STEP 4: Verify bill amount calculation
|
|
$expected_total = 75.00 + 15.00; // Consultation + BP Check
|
|
$this->assertEquals( number_format( $expected_total, 2 ), $bill->total_amount );
|
|
$this->assertEquals( '0.00', $bill->discount );
|
|
$this->assertEquals( number_format( $expected_total, 2 ), $bill->actual_amount );
|
|
|
|
// STEP 5: Doctor adds additional service during encounter
|
|
$additional_service_response = $this->make_request(
|
|
"/wp-json/kivicare/v1/encounters/{$encounter_id}/services",
|
|
'POST',
|
|
array( 'service_id' => $service_ids[2] ), // Prescription Review
|
|
$this->doctor_user
|
|
);
|
|
$this->assertRestResponse( $additional_service_response, 201 );
|
|
|
|
// STEP 6: Verify bill was automatically updated
|
|
$updated_bill = $wpdb->get_row(
|
|
$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}kc_bills WHERE id = %d", $bill->id )
|
|
);
|
|
|
|
$new_expected_total = $expected_total + 25.00; // Added Prescription Review
|
|
$this->assertEquals( number_format( $new_expected_total, 2 ), $updated_bill->total_amount );
|
|
$this->assertEquals( number_format( $new_expected_total, 2 ), $updated_bill->actual_amount );
|
|
|
|
// STEP 7: Test bill retrieval via API
|
|
$bill_response = $this->make_request( "/wp-json/kivicare/v1/bills/{$bill->id}", 'GET', array(), $this->receptionist_user );
|
|
$this->assertRestResponse( $bill_response, 200 );
|
|
|
|
$bill_data = $bill_response->get_data();
|
|
$this->assertEquals( $bill->id, $bill_data['id'] );
|
|
$this->assertEquals( $encounter_id, $bill_data['encounter_id'] );
|
|
$this->assertEquals( number_format( $new_expected_total, 2 ), $bill_data['total_amount'] );
|
|
|
|
// Verify bill includes service breakdown
|
|
$this->assertArrayHasKey( 'services', $bill_data );
|
|
$this->assertCount( 3, $bill_data['services'] );
|
|
|
|
// STEP 8: Test payment processing
|
|
$payment_data = array(
|
|
'amount' => $bill_data['actual_amount'],
|
|
'payment_method' => 'cash',
|
|
'notes' => 'Payment received in full',
|
|
);
|
|
|
|
$payment_response = $this->make_request( "/wp-json/kivicare/v1/bills/{$bill->id}/payment", 'POST', $payment_data, $this->receptionist_user );
|
|
$this->assertRestResponse( $payment_response, 200 );
|
|
|
|
// Verify payment status updated
|
|
$paid_bill = $wpdb->get_row(
|
|
$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}kc_bills WHERE id = %d", $bill->id )
|
|
);
|
|
$this->assertEquals( 'paid', $paid_bill->payment_status );
|
|
}
|
|
|
|
/**
|
|
* Test billing with discounts and insurance.
|
|
*
|
|
* @test
|
|
*/
|
|
public function test_billing_with_discounts_and_insurance() {
|
|
// This test will fail initially as discount/insurance features aren't implemented
|
|
$this->markTestIncomplete( 'Billing discounts and insurance not implemented yet - TDD RED phase' );
|
|
|
|
// ARRANGE: Setup scenario with discounts
|
|
$clinic_id = $this->create_test_clinic();
|
|
$appointment_id = $this->create_test_appointment( $clinic_id, $this->doctor_user, $this->patient_user );
|
|
|
|
// Create encounter
|
|
$encounter_response = $this->make_request( '/wp-json/kivicare/v1/encounters', 'POST', array(
|
|
'appointment_id' => $appointment_id,
|
|
'description' => 'Test encounter for billing with discounts',
|
|
), $this->doctor_user );
|
|
|
|
$encounter_id = $encounter_response->get_data()['id'];
|
|
|
|
// STEP 1: Apply senior citizen discount (20%)
|
|
$discount_data = array(
|
|
'type' => 'percentage',
|
|
'value' => 20,
|
|
'reason' => 'Senior citizen discount',
|
|
'applied_by' => $this->doctor_user,
|
|
);
|
|
|
|
$discount_response = $this->make_request( "/wp-json/kivicare/v1/bills/encounter/{$encounter_id}/discount", 'POST', $discount_data, $this->doctor_user );
|
|
$this->assertRestResponse( $discount_response, 200 );
|
|
|
|
// STEP 2: Verify discount was applied to bill
|
|
global $wpdb;
|
|
$bill = $wpdb->get_row(
|
|
$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}kc_bills WHERE encounter_id = %d", $encounter_id )
|
|
);
|
|
|
|
$total_amount = floatval( $bill->total_amount );
|
|
$discount_amount = floatval( $bill->discount );
|
|
$actual_amount = floatval( $bill->actual_amount );
|
|
|
|
$this->assertEquals( $total_amount * 0.20, $discount_amount );
|
|
$this->assertEquals( $total_amount - $discount_amount, $actual_amount );
|
|
|
|
// STEP 3: Test insurance claim processing
|
|
$insurance_data = array(
|
|
'insurance_provider' => 'Medicare',
|
|
'policy_number' => 'POL123456789',
|
|
'coverage_percentage' => 80,
|
|
'claim_amount' => $actual_amount,
|
|
);
|
|
|
|
$insurance_response = $this->make_request( "/wp-json/kivicare/v1/bills/{$bill->id}/insurance", 'POST', $insurance_data, $this->receptionist_user );
|
|
$this->assertRestResponse( $insurance_response, 201 );
|
|
|
|
// Verify insurance claim was created
|
|
$claim = $wpdb->get_row(
|
|
$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}kc_insurance_claims WHERE bill_id = %d", $bill->id )
|
|
);
|
|
$this->assertNotNull( $claim );
|
|
$this->assertEquals( 'pending', $claim->status );
|
|
}
|
|
|
|
/**
|
|
* Test billing error handling and edge cases.
|
|
*
|
|
* @test
|
|
*/
|
|
public function test_billing_error_handling() {
|
|
// This test will fail initially as error handling isn't implemented
|
|
$this->markTestIncomplete( 'Billing error handling not implemented yet - TDD RED phase' );
|
|
|
|
$clinic_id = $this->create_test_clinic();
|
|
|
|
// Test error scenarios
|
|
$error_tests = array(
|
|
// Encounter without appointment
|
|
array(
|
|
'scenario' => 'Encounter without appointment',
|
|
'setup' => function() {
|
|
return array(
|
|
'description' => 'Test encounter without appointment',
|
|
'patient_id' => $this->patient_user,
|
|
'clinic_id' => $this->create_test_clinic(),
|
|
);
|
|
},
|
|
'expected_error' => 'missing_appointment',
|
|
),
|
|
// Appointment without services
|
|
array(
|
|
'scenario' => 'Appointment without services',
|
|
'setup' => function() use ( $clinic_id ) {
|
|
$appointment_id = $this->create_test_appointment( $clinic_id, $this->doctor_user, $this->patient_user );
|
|
// Clear any default services
|
|
global $wpdb;
|
|
$wpdb->delete( $wpdb->prefix . 'kc_appointment_service_mapping', array( 'appointment_id' => $appointment_id ) );
|
|
|
|
return array(
|
|
'appointment_id' => $appointment_id,
|
|
'description' => 'Test encounter without services',
|
|
);
|
|
},
|
|
'expected_error' => 'no_billable_services',
|
|
),
|
|
);
|
|
|
|
foreach ( $error_tests as $test ) {
|
|
$encounter_data = $test['setup']();
|
|
|
|
$response = $this->make_request( '/wp-json/kivicare/v1/encounters', 'POST', $encounter_data, $this->doctor_user );
|
|
|
|
// Should either prevent encounter creation or generate appropriate billing warning
|
|
if ( $response->get_status() === 201 ) {
|
|
// If encounter was created, check for billing warnings
|
|
$encounter = $response->get_data();
|
|
$this->assertArrayHasKey( 'billing_warnings', $encounter );
|
|
} else {
|
|
// If encounter was prevented, check error code
|
|
$error_data = $response->get_data();
|
|
$this->assertEquals( $test['expected_error'], $error_data['code'] );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test billing permissions and access control.
|
|
*
|
|
* @test
|
|
*/
|
|
public function test_billing_permissions() {
|
|
// This test will fail initially as billing permissions aren't implemented
|
|
$this->markTestIncomplete( 'Billing permissions not implemented yet - TDD RED phase' );
|
|
|
|
// ARRANGE: Create bill
|
|
$clinic_id = $this->create_test_clinic();
|
|
$appointment_id = $this->create_test_appointment( $clinic_id, $this->doctor_user, $this->patient_user );
|
|
|
|
$encounter_response = $this->make_request( '/wp-json/kivicare/v1/encounters', 'POST', array(
|
|
'appointment_id' => $appointment_id,
|
|
'description' => 'Test encounter for billing permissions',
|
|
), $this->doctor_user );
|
|
|
|
$encounter_id = $encounter_response->get_data()['id'];
|
|
|
|
global $wpdb;
|
|
$bill = $wpdb->get_row(
|
|
$wpdb->prepare( "SELECT * FROM {$wpdb->prefix}kc_bills WHERE encounter_id = %d", $encounter_id )
|
|
);
|
|
|
|
// Test role-based permissions
|
|
$permission_tests = array(
|
|
// View bill permissions
|
|
array( 'action' => 'GET', 'endpoint' => "/wp-json/kivicare/v1/bills/{$bill->id}", 'user' => $this->admin_user, 'expected' => 200 ),
|
|
array( 'action' => 'GET', 'endpoint' => "/wp-json/kivicare/v1/bills/{$bill->id}", 'user' => $this->doctor_user, 'expected' => 200 ),
|
|
array( 'action' => 'GET', 'endpoint' => "/wp-json/kivicare/v1/bills/{$bill->id}", 'user' => $this->receptionist_user, 'expected' => 200 ),
|
|
array( 'action' => 'GET', 'endpoint' => "/wp-json/kivicare/v1/bills/{$bill->id}", 'user' => $this->patient_user, 'expected' => 200 ), // Own bill
|
|
|
|
// Payment processing permissions
|
|
array( 'action' => 'POST', 'endpoint' => "/wp-json/kivicare/v1/bills/{$bill->id}/payment", 'user' => $this->receptionist_user, 'expected' => 200 ),
|
|
array( 'action' => 'POST', 'endpoint' => "/wp-json/kivicare/v1/bills/{$bill->id}/payment", 'user' => $this->admin_user, 'expected' => 200 ),
|
|
array( 'action' => 'POST', 'endpoint' => "/wp-json/kivicare/v1/bills/{$bill->id}/payment", 'user' => $this->doctor_user, 'expected' => 403 ), // Doctor cannot process payments
|
|
array( 'action' => 'POST', 'endpoint' => "/wp-json/kivicare/v1/bills/{$bill->id}/payment", 'user' => $this->patient_user, 'expected' => 403 ), // Patient cannot process payments
|
|
);
|
|
|
|
foreach ( $permission_tests as $test ) {
|
|
$data = ( $test['action'] === 'POST' ) ? array( 'amount' => $bill->actual_amount, 'payment_method' => 'cash' ) : array();
|
|
|
|
$response = $this->make_request( $test['endpoint'], $test['action'], $data, $test['user'] );
|
|
$this->assertRestResponse( $response, $test['expected'] );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test billing reports and analytics.
|
|
*
|
|
* @test
|
|
*/
|
|
public function test_billing_reports_and_analytics() {
|
|
// This test will fail initially as billing reports aren't implemented
|
|
$this->markTestIncomplete( 'Billing reports not implemented yet - TDD RED phase' );
|
|
|
|
// ARRANGE: Create multiple bills with different statuses
|
|
$clinic_id = $this->create_test_clinic();
|
|
|
|
$bill_scenarios = array(
|
|
array( 'amount' => 100.00, 'status' => 'paid', 'date' => '2024-01-15' ),
|
|
array( 'amount' => 150.00, 'status' => 'unpaid', 'date' => '2024-01-16' ),
|
|
array( 'amount' => 75.00, 'status' => 'paid', 'date' => '2024-01-17' ),
|
|
array( 'amount' => 200.00, 'status' => 'partially_paid', 'date' => '2024-01-18' ),
|
|
);
|
|
|
|
foreach ( $bill_scenarios as $scenario ) {
|
|
$appointment_id = $this->create_test_appointment( $clinic_id, $this->doctor_user, $this->patient_user );
|
|
|
|
$encounter_response = $this->make_request( '/wp-json/kivicare/v1/encounters', 'POST', array(
|
|
'appointment_id' => $appointment_id,
|
|
'description' => 'Test encounter for billing reports',
|
|
'encounter_date' => $scenario['date'],
|
|
), $this->doctor_user );
|
|
|
|
// Simulate different payment statuses
|
|
global $wpdb;
|
|
$encounter_id = $encounter_response->get_data()['id'];
|
|
$wpdb->update(
|
|
$wpdb->prefix . 'kc_bills',
|
|
array(
|
|
'total_amount' => number_format( $scenario['amount'], 2 ),
|
|
'actual_amount' => number_format( $scenario['amount'], 2 ),
|
|
'payment_status' => $scenario['status'],
|
|
),
|
|
array( 'encounter_id' => $encounter_id )
|
|
);
|
|
}
|
|
|
|
// ACT: Generate billing reports
|
|
$reports_response = $this->make_request( '/wp-json/kivicare/v1/reports/billing', 'GET', array(
|
|
'start_date' => '2024-01-01',
|
|
'end_date' => '2024-01-31',
|
|
'clinic_id' => $clinic_id,
|
|
), $this->admin_user );
|
|
|
|
// ASSERT: Report contains expected data
|
|
$this->assertRestResponse( $reports_response, 200 );
|
|
|
|
$report = $reports_response->get_data();
|
|
$this->assertArrayHasKey( 'total_revenue', $report );
|
|
$this->assertArrayHasKey( 'outstanding_amount', $report );
|
|
$this->assertArrayHasKey( 'payment_summary', $report );
|
|
|
|
// Verify calculations
|
|
$this->assertEquals( 525.00, $report['total_billed'] ); // Sum of all bills
|
|
$this->assertEquals( 175.00, $report['total_revenue'] ); // Only paid bills
|
|
$this->assertEquals( 350.00, $report['outstanding_amount'] ); // Unpaid + partially paid
|
|
}
|
|
} |