add test batches
This commit is contained in:
@@ -54,11 +54,11 @@ class ScenarioAFamilyEnrollmentTest extends TestCase
|
||||
|
||||
// Teacher sees student in roster.
|
||||
$this->actingAs($teacher, 'api');
|
||||
$this->getJson('/api/v1/teachers/classes?class_section_id='.$classSectionId)
|
||||
$this->getJson('/api/v1/teachers/classes?class_section_id=' . $classSectionId)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$studentIds = array_column($this->getJson('/api/v1/teachers/classes?class_section_id='.$classSectionId)->json('students'), 'student_id');
|
||||
$studentIds = array_column($this->getJson('/api/v1/teachers/classes?class_section_id=' . $classSectionId)->json('students'), 'student_id');
|
||||
$this->assertContains($studentId, $studentIds);
|
||||
|
||||
// Parent sees own profile.
|
||||
@@ -89,7 +89,7 @@ class ScenarioAFamilyEnrollmentTest extends TestCase
|
||||
]);
|
||||
|
||||
$this->actingAs($parentA, 'api');
|
||||
$this->patchJson('/api/v1/parents/students/'.$studentB, [
|
||||
$this->patchJson('/api/v1/parents/students/' . $studentB, [
|
||||
'firstname' => 'Hacked',
|
||||
'lastname' => 'Child',
|
||||
'dob' => '2015-01-01',
|
||||
|
||||
@@ -27,7 +27,7 @@ class ScenarioBSundaySchoolDayTest extends TestCase
|
||||
|
||||
$this->actingAs($teacher, 'api');
|
||||
|
||||
$form = $this->getJson('/api/v1/attendance/teacher/form?class_section_id='.$classSectionId)
|
||||
$form = $this->getJson('/api/v1/attendance/teacher/form?class_section_id=' . $classSectionId)
|
||||
->assertOk();
|
||||
|
||||
$date = (string) ($form->json('data.current_sunday') ?? '2025-10-05');
|
||||
@@ -52,7 +52,7 @@ class ScenarioBSundaySchoolDayTest extends TestCase
|
||||
|
||||
// Parent views attendance for their child.
|
||||
$this->actingAs($parent, 'api');
|
||||
$this->getJson('/api/v1/parents/attendance?school_year='.self::E2E_SCHOOL_YEAR)
|
||||
$this->getJson('/api/v1/parents/attendance?school_year=' . self::E2E_SCHOOL_YEAR)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class ScenarioCAcademicTermTest extends TestCase
|
||||
|
||||
$this->actingAs($teacher, 'api');
|
||||
|
||||
$this->getJson('/api/v1/scores/overview?class_section_id='.$classSectionId.'&semester='.self::E2E_SEMESTER.'&school_year='.self::E2E_SCHOOL_YEAR)
|
||||
$this->getJson('/api/v1/scores/overview?class_section_id=' . $classSectionId . '&semester=' . self::E2E_SEMESTER . '&school_year=' . self::E2E_SCHOOL_YEAR)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
@@ -69,7 +69,7 @@ class ScenarioCAcademicTermTest extends TestCase
|
||||
'is_locked' => 1,
|
||||
]);
|
||||
|
||||
$this->getJson('/api/v1/reports/report-cards/students/'.$studentId.'?school_year='.self::E2E_SCHOOL_YEAR.'&semester='.self::E2E_SEMESTER)
|
||||
$this->getJson('/api/v1/reports/report-cards/students/' . $studentId . '?school_year=' . self::E2E_SCHOOL_YEAR . '&semester=' . self::E2E_SEMESTER)
|
||||
->assertOk();
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ class ScenarioDFamilyBillingTest extends TestCase
|
||||
->value('id');
|
||||
$this->assertGreaterThan(0, $chargeId);
|
||||
|
||||
$this->postJson('/api/v1/finance/event-charges/'.$chargeId.'/approve')
|
||||
$this->postJson('/api/v1/finance/event-charges/' . $chargeId . '/approve')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
@@ -81,7 +81,7 @@ class ScenarioDFamilyBillingTest extends TestCase
|
||||
'invoice_id' => $invoiceId,
|
||||
]);
|
||||
|
||||
$this->getJson('/api/v1/finance/payments/parent/'.$parentId.'?school_year='.self::E2E_SCHOOL_YEAR)
|
||||
$this->getJson('/api/v1/finance/payments/parent/' . $parentId . '?school_year=' . self::E2E_SCHOOL_YEAR)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class ScenarioEFinanceEdgeTest extends TestCase
|
||||
$parent = $this->createApiUserWithRole('parent');
|
||||
$priorInvoiceId = $this->seedPriorYearUnpaidInvoice($parent->id);
|
||||
|
||||
$preview = $this->getJson('/api/v1/finance/carryforwards/preview?'.http_build_query([
|
||||
$preview = $this->getJson('/api/v1/finance/carryforwards/preview?' . http_build_query([
|
||||
'from_school_year' => self::E2E_PREV_SCHOOL_YEAR,
|
||||
'to_school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'parent_id' => $parent->id,
|
||||
@@ -44,24 +44,24 @@ class ScenarioEFinanceEdgeTest extends TestCase
|
||||
$this->assertNotEmpty($draft->json('result.created'));
|
||||
|
||||
$carryforwardId = (int) $draft->json('result.created.0.id');
|
||||
$this->postJson('/api/v1/finance/carryforwards/'.$carryforwardId.'/approve')
|
||||
$this->postJson('/api/v1/finance/carryforwards/' . $carryforwardId . '/approve')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$posted = $this->postJson('/api/v1/finance/carryforwards/'.$carryforwardId.'/post')
|
||||
$posted = $this->postJson('/api/v1/finance/carryforwards/' . $carryforwardId . '/post')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
$newInvoiceId = (int) ($posted->json('carryforward.posted_invoice_id') ?? 0);
|
||||
$this->assertGreaterThan(0, $newInvoiceId);
|
||||
|
||||
$plan = $this->postJson('/api/v1/finance/invoices/'.$newInvoiceId.'/installment-plans', [
|
||||
$plan = $this->postJson('/api/v1/finance/invoices/' . $newInvoiceId . '/installment-plans', [
|
||||
'number_of_installments' => 2,
|
||||
'first_due_date' => '2025-09-15',
|
||||
'interval_months' => 1,
|
||||
])->assertCreated()->assertJsonPath('ok', true);
|
||||
|
||||
$planId = (int) $plan->json('plan.id');
|
||||
$this->postJson('/api/v1/finance/installment-plans/'.$planId.'/activate')
|
||||
$this->postJson('/api/v1/finance/installment-plans/' . $planId . '/activate')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
@@ -81,7 +81,7 @@ class ScenarioEFinanceEdgeTest extends TestCase
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->postJson('/api/v1/finance/payments/'.$paymentId.'/allocate-installments', [])
|
||||
$this->postJson('/api/v1/finance/payments/' . $paymentId . '/allocate-installments', [])
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
@@ -89,20 +89,20 @@ class ScenarioEFinanceEdgeTest extends TestCase
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->postJson('/api/v1/finance/parent-payment-followups/'.$parent->id.'/note', [
|
||||
$this->postJson('/api/v1/finance/parent-payment-followups/' . $parent->id . '/note', [
|
||||
'invoice_id' => $newInvoiceId,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'note' => 'Called parent about prior-year balance.',
|
||||
'contact_method' => 'phone',
|
||||
])->assertCreated()->assertJsonPath('ok', true);
|
||||
|
||||
$this->postJson('/api/v1/finance/parent-payment-followups/'.$parent->id.'/promise-to-pay', [
|
||||
$this->postJson('/api/v1/finance/parent-payment-followups/' . $parent->id . '/promise-to-pay', [
|
||||
'invoice_id' => $newInvoiceId,
|
||||
'promise_to_pay_date' => '2025-10-15',
|
||||
'promise_to_pay_amount' => 50,
|
||||
])->assertCreated()->assertJsonPath('ok', true);
|
||||
|
||||
$this->postJson('/api/v1/finance/parent-payment-followups/'.$parent->id.'/resolve', [
|
||||
$this->postJson('/api/v1/finance/parent-payment-followups/' . $parent->id . '/resolve', [
|
||||
'invoice_id' => $newInvoiceId,
|
||||
'note' => 'Balance plan agreed.',
|
||||
])->assertCreated()->assertJsonPath('ok', true);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -36,7 +36,7 @@ class ScenarioGPrintBadgeTest extends TestCase
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
foreach (['assigned', 'done', 'delivered'] as $status) {
|
||||
$this->patchJson('/api/v1/print-requests/'.$requestId.'/status', [
|
||||
$this->patchJson('/api/v1/print-requests/' . $requestId . '/status', [
|
||||
'status' => $status,
|
||||
])->assertOk();
|
||||
}
|
||||
@@ -46,7 +46,7 @@ class ScenarioGPrintBadgeTest extends TestCase
|
||||
'status' => 'delivered',
|
||||
]);
|
||||
|
||||
$badgeTag = 'E2E-BADGE-'.random_int(10000, 99999);
|
||||
$badgeTag = 'E2E-BADGE-' . random_int(10000, 99999);
|
||||
DB::table('users')->where('id', $teacher->id)->update(['rfid_tag' => $badgeTag]);
|
||||
|
||||
$this->postJson('/api/v1/badges/log-print', [
|
||||
|
||||
@@ -29,7 +29,7 @@ class ScenarioHSecurityUnauthorizedAccessTest extends TestCase
|
||||
|
||||
// Parent cannot update another family's student.
|
||||
$this->actingAs($parentA, 'api');
|
||||
$this->patchJson('/api/v1/parents/students/'.$world['student_id'], [
|
||||
$this->patchJson('/api/v1/parents/students/' . $world['student_id'], [
|
||||
'firstname' => 'Blocked',
|
||||
'lastname' => 'Update',
|
||||
'dob' => '2014-01-01',
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario I: Public access, authentication lifecycle, and basic system probes.
|
||||
*
|
||||
* This keeps the front door honest before the deeper school workflows run.
|
||||
*/
|
||||
class ScenarioIPublicAuthAndSystemUseCaseTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
|
||||
public function test_guest_can_use_public_content_contact_and_health_endpoints_without_authentication(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
|
||||
$publicGetEndpoints = [
|
||||
'/api/v1/health',
|
||||
'/api/v1/policies/school',
|
||||
'/api/v1/policies/picture',
|
||||
'/api/v1/pages/privacy',
|
||||
'/api/v1/pages/terms',
|
||||
'/api/v1/pages/help',
|
||||
'/api/v1/frontend',
|
||||
'/api/v1/frontend/facility',
|
||||
'/api/v1/frontend/team',
|
||||
'/api/v1/frontend/call-to-action',
|
||||
'/api/v1/frontend/testimonial',
|
||||
'/api/v1/auth/register/captcha',
|
||||
];
|
||||
|
||||
foreach ($publicGetEndpoints as $endpoint) {
|
||||
$response = $this->getJson($endpoint);
|
||||
|
||||
$this->assertLessThan(
|
||||
500,
|
||||
$response->getStatusCode(),
|
||||
"$endpoint returned a server error. Public routes do not get to explode just because nobody logged in."
|
||||
);
|
||||
}
|
||||
|
||||
$this->postJson('/api/v1/contact', [])
|
||||
->assertStatus(422)
|
||||
->assertJsonValidationErrors(['name', 'email', 'subject', 'message']);
|
||||
|
||||
$this->postJson('/api/v1/contact', [
|
||||
'name' => 'Public Visitor',
|
||||
'email' => 'visitor@example.test',
|
||||
'subject' => 'Program question',
|
||||
'message' => 'Please send details about Sunday school registration.',
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('status', true)
|
||||
->assertJsonStructure(['status', 'message', 'data' => ['contact']]);
|
||||
}
|
||||
|
||||
public function test_user_can_login_read_current_profile_and_logout_through_api(): void
|
||||
{
|
||||
$user = $this->createApiUserWithRole('teacher', [
|
||||
'firstname' => 'E2E',
|
||||
'lastname' => 'Teacher',
|
||||
'email' => 'e2e.auth.teacher@example.test',
|
||||
'password' => Hash::make('secret123'),
|
||||
]);
|
||||
|
||||
$login = $this->postJson('/api/v1/auth/login', [
|
||||
'email' => strtoupper($user->email),
|
||||
'password' => 'secret123',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true)
|
||||
->assertJsonStructure([
|
||||
'token',
|
||||
'access_token',
|
||||
'token_type',
|
||||
'expires_in',
|
||||
'user' => ['id', 'name', 'firstname', 'lastname', 'email', 'roles'],
|
||||
]);
|
||||
|
||||
$token = (string) $login->json('token');
|
||||
$this->assertNotSame('', $token);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer ' . $token)
|
||||
->getJson('/api/v1/auth/me')
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true)
|
||||
->assertJsonPath('data.email', 'e2e.auth.teacher@example.test');
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer ' . $token)
|
||||
->postJson('/api/v1/auth/logout')
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true);
|
||||
}
|
||||
|
||||
public function test_invalid_inactive_and_suspended_login_paths_fail_safely(): void
|
||||
{
|
||||
$active = $this->createApiUserWithRole('parent', [
|
||||
'email' => 'active.parent@example.test',
|
||||
'password' => Hash::make('secret123'),
|
||||
'is_suspended' => 0,
|
||||
]);
|
||||
|
||||
$this->postJson('/api/v1/auth/login', [
|
||||
'email' => $active->email,
|
||||
'password' => 'wrong-password',
|
||||
])
|
||||
->assertUnauthorized()
|
||||
->assertJsonPath('status', false);
|
||||
|
||||
$suspended = $this->createApiUserWithRole('parent', [
|
||||
'email' => 'suspended.parent@example.test',
|
||||
'password' => Hash::make('secret123'),
|
||||
'is_suspended' => 1,
|
||||
]);
|
||||
|
||||
$this->postJson('/api/v1/auth/login', [
|
||||
'email' => $suspended->email,
|
||||
'password' => 'secret123',
|
||||
])
|
||||
->assertForbidden()
|
||||
->assertJsonPath('status', false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario J: Cross-module API smoke checks.
|
||||
*
|
||||
* These are intentionally use-case oriented and broad. They prove the major
|
||||
* modules can be reached with realistic authentication and seeded school context.
|
||||
* They do not pretend to replace each module's dedicated CRUD and edge tests.
|
||||
*/
|
||||
class ScenarioJCrossModuleApiUseCaseSmokeTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
|
||||
public function test_admin_can_reach_core_operational_dashboards_without_server_errors(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$invoiceId = $this->seedInvoiceForParent($world['parent_id']);
|
||||
|
||||
$endpoints = [
|
||||
'/api/v1/administrator/dashboard/metrics',
|
||||
'/api/v1/administrator/dashboard/user-search?query=Kai',
|
||||
'/api/v1/users',
|
||||
'/api/v1/students',
|
||||
'/api/v1/students/' . $world['student_id'],
|
||||
'/api/v1/students/' . $world['student_id'] . '/classes',
|
||||
'/api/v1/class-sections',
|
||||
'/api/v1/teachers/classes?class_section_id=' . $world['class_section_id'],
|
||||
'/api/v1/attendance/admin/daily?date=2025-10-05',
|
||||
'/api/v1/attendance-comment-templates',
|
||||
'/api/v1/finance/invoices/parent/' . $world['parent_id'],
|
||||
'/api/v1/finance/invoices/' . $invoiceId . '/preview',
|
||||
'/api/v1/finance/paypal-transactions',
|
||||
'/api/v1/inventory/items',
|
||||
'/api/v1/inventory/categories',
|
||||
'/api/v1/messages',
|
||||
'/api/v1/support',
|
||||
'/api/v1/settings',
|
||||
'/api/v1/system/semester-range/resolve?date=2025-10-05',
|
||||
];
|
||||
|
||||
foreach ($endpoints as $endpoint) {
|
||||
$response = $this->getJson($endpoint);
|
||||
|
||||
$this->assertLessThan(
|
||||
500,
|
||||
$response->getStatusCode(),
|
||||
"$endpoint returned a server error for an authenticated admin."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_major_mutating_use_cases_validate_bad_payloads_instead_of_crashing(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$adminMutations = [
|
||||
['POST', '/api/v1/users', []],
|
||||
['POST', '/api/v1/students', []],
|
||||
['POST', '/api/v1/class-sections', []],
|
||||
['POST', '/api/v1/attendance-comment-templates', []],
|
||||
['POST', '/api/v1/assignments', []],
|
||||
['POST', '/api/v1/messages', []],
|
||||
['POST', '/api/v1/support', []],
|
||||
['POST', '/api/v1/inventory/items', []],
|
||||
['POST', '/api/v1/settings/events', []],
|
||||
['POST', '/api/v1/finance/payments', []],
|
||||
['POST', '/api/v1/finance/refunds', []],
|
||||
['POST', '/api/v1/finance/fees/tuition-breakdown', []],
|
||||
['POST', '/api/v1/extra-charges', []],
|
||||
['POST', '/api/v1/reports/slips/preview', []],
|
||||
];
|
||||
|
||||
foreach ($adminMutations as [$method, $endpoint, $payload]) {
|
||||
$response = $this->json($method, $endpoint, $payload);
|
||||
|
||||
$this->assertLessThan(
|
||||
500,
|
||||
$response->getStatusCode(),
|
||||
"$method $endpoint crashed instead of validating/rejecting the request."
|
||||
);
|
||||
}
|
||||
|
||||
$this->actingAs($world['teacher'], 'api');
|
||||
foreach ([
|
||||
['POST', '/api/v1/attendance/teacher/submit', []],
|
||||
['POST', '/api/v1/teacher/absence-vacation', []],
|
||||
['POST', '/api/v1/teacher/class-progress-submit', []],
|
||||
['POST', '/api/v1/scores/homework', []],
|
||||
] as [$method, $endpoint, $payload]) {
|
||||
$response = $this->json($method, $endpoint, $payload);
|
||||
|
||||
$this->assertLessThan(
|
||||
500,
|
||||
$response->getStatusCode(),
|
||||
"$method $endpoint crashed for an authenticated teacher."
|
||||
);
|
||||
}
|
||||
|
||||
$this->actingAs($world['parent'], 'api');
|
||||
foreach ([
|
||||
['PATCH', '/api/v1/parents/profile', []],
|
||||
['POST', '/api/v1/parents/emergency-contacts', []],
|
||||
['POST', '/api/v1/parents/attendance-reports', []],
|
||||
['POST', '/api/v1/parents/events/participation', []],
|
||||
] as [$method, $endpoint, $payload]) {
|
||||
$response = $this->json($method, $endpoint, $payload);
|
||||
|
||||
$this->assertLessThan(
|
||||
500,
|
||||
$response->getStatusCode(),
|
||||
"$method $endpoint crashed for an authenticated parent."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario K: Parent self-service and privacy boundaries.
|
||||
*
|
||||
* This verifies the parent portal as one complete use case: view household data,
|
||||
* update contact details, manage emergency contacts, and stay boxed inside the
|
||||
* authenticated family. Tiny detail, privacy. Apparently important.
|
||||
*/
|
||||
class ScenarioKParentSelfServiceAndPrivacyTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
|
||||
public function test_parent_reviews_household_data_and_updates_contact_details(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$parent = $world['parent'];
|
||||
$studentId = $world['student_id'];
|
||||
|
||||
$this->seedInvoiceForParent($parent->id);
|
||||
DB::table('enrollments')->insert([
|
||||
'parent_id' => $parent->id,
|
||||
'student_id' => $studentId,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'enrollment_status' => 'enrolled',
|
||||
'enrollment_date' => '2025-09-01',
|
||||
]);
|
||||
DB::table('attendance_data')->insert([
|
||||
'class_id' => 3,
|
||||
'class_section_id' => $world['class_section_id'],
|
||||
'student_id' => $studentId,
|
||||
'school_id' => 'S-E2E',
|
||||
'date' => '2025-10-05',
|
||||
'status' => 'present',
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($parent, 'api');
|
||||
|
||||
$this->getJson('/api/v1/parents/profile')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->patchJson('/api/v1/parents/profile', [
|
||||
'firstname' => 'Updated',
|
||||
'lastname' => 'Parent',
|
||||
'cellphone' => '5557778888',
|
||||
'address_street' => '99 Parent Way',
|
||||
'city' => 'Brooklyn',
|
||||
'state' => 'NY',
|
||||
'zip' => '11215',
|
||||
])->assertOk()->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $parent->id,
|
||||
'firstname' => 'Updated',
|
||||
'cellphone' => '5557778888',
|
||||
]);
|
||||
|
||||
$this->postJson('/api/v1/parents/emergency-contacts', [
|
||||
'name' => 'Amina Emergency',
|
||||
'cellphone' => '5550001111',
|
||||
'email' => 'amina.emergency@example.test',
|
||||
'relation' => 'Aunt',
|
||||
])->assertCreated()->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('emergency_contacts', [
|
||||
'parent_id' => $parent->id,
|
||||
'emergency_contact_name' => 'Amina Emergency',
|
||||
]);
|
||||
|
||||
$this->getJson('/api/v1/parents/emergency-contacts')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->getJson('/api/v1/parents/attendance?school_year=' . self::E2E_SCHOOL_YEAR)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->getJson('/api/v1/parents/invoices?school_year=' . self::E2E_SCHOOL_YEAR)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->getJson('/api/v1/parents/enrollments?school_year=' . self::E2E_SCHOOL_YEAR)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
}
|
||||
|
||||
public function test_parent_cannot_mutate_another_family_student_or_emergency_contact(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$parentA = $this->createApiUserWithRole('parent');
|
||||
$parentB = $this->createApiUserWithRole('parent');
|
||||
$this->actingAs($admin, 'api');
|
||||
$studentB = $this->createStudentViaApi($parentB->id, [
|
||||
'firstname' => 'Private',
|
||||
'lastname' => 'Student',
|
||||
]);
|
||||
$foreignContactId = DB::table('emergency_contacts')->insertGetId([
|
||||
'parent_id' => $parentB->id,
|
||||
'emergency_contact_name' => 'Foreign Contact',
|
||||
'cellphone' => '5553334444',
|
||||
'email' => 'foreign.contact@example.test',
|
||||
'relation' => 'Uncle',
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($parentA, 'api');
|
||||
|
||||
$this->patchJson('/api/v1/parents/students/' . $studentB, [
|
||||
'firstname' => 'Tampered',
|
||||
'lastname' => 'Student',
|
||||
'dob' => '2015-01-01',
|
||||
'gender' => 'Male',
|
||||
])->assertNotFound();
|
||||
|
||||
$this->patchJson('/api/v1/parents/emergency-contacts/' . $foreignContactId, [
|
||||
'name' => 'Tampered Contact',
|
||||
'cellphone' => '5553334444',
|
||||
'email' => 'foreign.contact@example.test',
|
||||
'relation' => 'Uncle',
|
||||
])->assertNotFound();
|
||||
|
||||
$this->assertDatabaseHas('students', [
|
||||
'id' => $studentB,
|
||||
'firstname' => 'Private',
|
||||
]);
|
||||
$this->assertDatabaseHas('emergency_contacts', [
|
||||
'id' => $foreignContactId,
|
||||
'emergency_contact_name' => 'Foreign Contact',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario L: Teacher academic operations.
|
||||
*
|
||||
* This walks a teacher through the practical classroom loop: roster context,
|
||||
* progress reporting, scoring, and read-only reporting. A test that merely asks
|
||||
* if a controller exists is not a workflow; it is a very expensive shrug.
|
||||
*/
|
||||
class ScenarioLTeacherAcademicOperationsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
|
||||
public function test_teacher_manages_class_progress_and_homework_scores_end_to_end(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$teacher = $world['teacher'];
|
||||
$classSectionId = $world['class_section_id'];
|
||||
$studentId = $world['student_id'];
|
||||
|
||||
$this->actingAs($teacher, 'api');
|
||||
|
||||
$this->getJson('/api/v1/teachers/classes?class_section_id=' . $classSectionId)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->getJson('/api/v1/class-progress/meta?class_id=3&school_year=' . self::E2E_SCHOOL_YEAR . '&semester=' . self::E2E_SEMESTER)
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true);
|
||||
|
||||
$this->postJson('/api/v1/class-progress', [
|
||||
'class_section_id' => $classSectionId,
|
||||
'week_start' => '2025-10-05',
|
||||
'week_end' => '2025-10-11',
|
||||
'covered_islamic' => 'Seerah review',
|
||||
'covered_quran' => 'Surah recitation practice',
|
||||
'homework_islamic' => 'Read lesson notes',
|
||||
'homework_quran' => 'Memorize assigned ayat',
|
||||
'flags' => ['on_track'],
|
||||
])->assertCreated()->assertJsonPath('status', true);
|
||||
|
||||
$reportId = (int) DB::table('class_progress_reports')
|
||||
->where('teacher_id', $teacher->id)
|
||||
->where('class_section_id', $classSectionId)
|
||||
->orderBy('id')
|
||||
->value('id');
|
||||
$this->assertGreaterThan(0, $reportId);
|
||||
|
||||
$this->patchJson('/api/v1/class-progress/' . $reportId, [
|
||||
'status' => 'ahead',
|
||||
])->assertOk()->assertJsonPath('status', true);
|
||||
|
||||
$this->postJson('/api/v1/scores/homework/columns', [
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
])->assertOk()->assertJsonPath('ok', true);
|
||||
|
||||
$this->postJson('/api/v1/scores/homework', [
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'scores' => [
|
||||
(string) $studentId => [
|
||||
'1' => 96,
|
||||
],
|
||||
],
|
||||
'missing_ok' => [],
|
||||
])->assertOk()->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('homework', [
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'homework_index' => 1,
|
||||
'score' => 96,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
]);
|
||||
|
||||
$this->getJson('/api/v1/scores/homework?class_section_id=' . $classSectionId . '&semester=' . self::E2E_SEMESTER . '&school_year=' . self::E2E_SCHOOL_YEAR)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->getJson('/api/v1/reports/report-cards/completeness?class_section_id=' . $classSectionId . '&semester=' . self::E2E_SEMESTER . '&school_year=' . self::E2E_SCHOOL_YEAR)
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true);
|
||||
}
|
||||
|
||||
public function test_unassigned_teacher_cannot_view_or_update_another_teachers_progress_report(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$otherTeacher = $this->createApiUserWithRole('teacher');
|
||||
|
||||
DB::table('class_progress_reports')->insert([
|
||||
'teacher_id' => $world['teacher']->id,
|
||||
'class_section_id' => $world['class_section_id'],
|
||||
'subject' => 'Islamic Studies',
|
||||
'week_start' => '2025-10-05',
|
||||
'week_end' => '2025-10-11',
|
||||
'covered' => 'Private teacher notes',
|
||||
'homework' => 'Private homework',
|
||||
'status' => 'on_track',
|
||||
'next_week_plan' => 'Continue private unit',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
$reportId = (int) DB::table('class_progress_reports')->value('id');
|
||||
|
||||
$this->actingAs($otherTeacher, 'api');
|
||||
|
||||
$this->getJson('/api/v1/class-progress/' . $reportId)
|
||||
->assertForbidden();
|
||||
|
||||
$this->patchJson('/api/v1/class-progress/' . $reportId, [
|
||||
'status' => 'behind',
|
||||
])->assertForbidden();
|
||||
|
||||
$this->assertDatabaseHas('class_progress_reports', [
|
||||
'id' => $reportId,
|
||||
'status' => 'on_track',
|
||||
]);
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario M: Administrator enrollment and classroom operations.
|
||||
*
|
||||
* This is the operational spine: parent account, student registration, class
|
||||
* section setup, student placement, teacher assignment, roster visibility, and
|
||||
* reversal paths. If this breaks, school administration becomes archaeology.
|
||||
*/
|
||||
class ScenarioMAdministratorEnrollmentAndClassOperationsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
|
||||
public function test_admin_builds_classroom_roster_from_parent_registration_to_teacher_assignment(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$teacher = $this->createApiUserWithRole('teacher', [
|
||||
'firstname' => 'Maryam',
|
||||
'lastname' => 'Teacher',
|
||||
]);
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$parentId = $this->createParentAccountViaApi([
|
||||
'firstname' => 'Roster',
|
||||
'lastname' => 'Parent',
|
||||
]);
|
||||
$studentId = $this->createStudentViaApi($parentId, [
|
||||
'firstname' => 'Roster',
|
||||
'lastname' => 'Student',
|
||||
]);
|
||||
|
||||
DB::table('classes')->insert([
|
||||
'id' => 9,
|
||||
'class_name' => 'Grade 9',
|
||||
'schedule' => 'Sunday',
|
||||
'capacity' => 25,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->postJson('/api/v1/class-sections', [
|
||||
'class_id' => 9,
|
||||
'class_section_id' => 909,
|
||||
'class_section_name' => '9A',
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
])->assertCreated();
|
||||
|
||||
$this->assignStudentToClassViaApi($studentId, 909)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->postJson('/api/v1/administrator/teacher-class/assign', [
|
||||
'teacher_id' => $teacher->id,
|
||||
'class_section_id' => 909,
|
||||
'teacher_role' => 'teacher',
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
])->assertOk()->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('student_class', [
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => 909,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
]);
|
||||
$this->assertDatabaseHas('teacher_class', [
|
||||
'teacher_id' => $teacher->id,
|
||||
'class_section_id' => 909,
|
||||
'position' => 'main',
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
]);
|
||||
|
||||
$this->getJson('/api/v1/students/' . $studentId . '/classes')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->actingAs($teacher, 'api');
|
||||
$this->getJson('/api/v1/teachers/classes?class_section_id=909')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
}
|
||||
|
||||
public function test_admin_can_reverse_roster_and_teacher_assignments_without_orphaning_records(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$teacher = $this->createApiUserWithRole('teacher');
|
||||
$classSectionId = $this->seedClassSection(910, '9B', 9);
|
||||
$parentId = $this->createParentAccountViaApi();
|
||||
$studentId = $this->createStudentViaApi($parentId);
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
$this->assignStudentToClassViaApi($studentId, $classSectionId)->assertOk();
|
||||
$this->seedTeacherClassAssignment($teacher->id, $classSectionId);
|
||||
|
||||
$this->postJson('/api/v1/students/remove-class', [
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
])->assertOk()->assertJsonPath('ok', true);
|
||||
|
||||
$this->postJson('/api/v1/administrator/teacher-class/delete', [
|
||||
'teacher_id' => $teacher->id,
|
||||
'class_section_id' => $classSectionId,
|
||||
'position' => 'main',
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
])->assertOk()->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseMissing('student_class', [
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
]);
|
||||
$this->assertDatabaseMissing('teacher_class', [
|
||||
'teacher_id' => $teacher->id,
|
||||
'class_section_id' => $classSectionId,
|
||||
'position' => 'main',
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
]);
|
||||
$this->assertDatabaseHas('students', [
|
||||
'id' => $studentId,
|
||||
'parent_id' => $parentId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario N: Communications and support lifecycle.
|
||||
*
|
||||
* A real school app is not just invoices and rosters. People send messages,
|
||||
* support requests, notifications, and reports. Then everyone acts shocked when
|
||||
* privacy and read-state bugs matter. This nails those flows down.
|
||||
*/
|
||||
class ScenarioNCommunicationsAndSupportLifecycleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
|
||||
public function test_parent_and_teacher_exchange_message_and_read_state_is_preserved(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$parent = $world['parent'];
|
||||
$teacher = $world['teacher'];
|
||||
|
||||
$this->actingAs($parent, 'api');
|
||||
$this->postJson('/api/v1/messages', [
|
||||
'recipient_id' => $teacher->id,
|
||||
'subject' => 'Homework clarification',
|
||||
'message' => 'Can you confirm the Quran homework for this week?',
|
||||
'priority' => 'normal',
|
||||
])->assertCreated();
|
||||
|
||||
$messageId = (int) DB::table('messages')
|
||||
->where('sender_id', $parent->id)
|
||||
->where('recipient_id', $teacher->id)
|
||||
->where('subject', 'Homework clarification')
|
||||
->value('id');
|
||||
$this->assertGreaterThan(0, $messageId);
|
||||
|
||||
$this->actingAs($teacher, 'api');
|
||||
$this->getJson('/api/v1/messages/inbox')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->getJson('/api/v1/messages/' . $messageId)
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('messages', [
|
||||
'id' => $messageId,
|
||||
'read_status' => 1,
|
||||
]);
|
||||
|
||||
$this->patchJson('/api/v1/messages/' . $messageId, [
|
||||
'status' => 'archived',
|
||||
])->assertOk()->assertJsonPath('ok', true);
|
||||
|
||||
$this->assertDatabaseHas('messages', [
|
||||
'id' => $messageId,
|
||||
'status' => 'archived',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_public_contact_support_ticket_and_admin_support_queue_are_connected(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
|
||||
$this->postJson('/api/v1/contact', [
|
||||
'name' => 'Public Visitor',
|
||||
'email' => 'visitor.support@example.test',
|
||||
'subject' => 'Registration help',
|
||||
'message' => 'I need help completing registration.',
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
$this->getJson('/api/v1/support')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$response = $this->postJson('/api/v1/support', [
|
||||
'name' => 'Internal Support Case',
|
||||
'email' => 'internal.support@example.test',
|
||||
'subject' => 'Follow up required',
|
||||
'message' => 'Please call the parent about registration.',
|
||||
'status' => 'open',
|
||||
'priority' => 'normal',
|
||||
]);
|
||||
|
||||
$this->assertLessThan(
|
||||
500,
|
||||
$response->getStatusCode(),
|
||||
'Authenticated support ticket creation should validate or persist, not crash.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario O: Inventory, finance, and audit controls.
|
||||
*
|
||||
* This covers the unglamorous back office: materials are created, adjusted,
|
||||
* distributed, billed, paid, and then reported. Not glamorous. Still where money
|
||||
* leaks if tests are just decorative plants.
|
||||
*/
|
||||
class ScenarioOInventoryFinanceAndAuditControlsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
|
||||
public function test_admin_tracks_inventory_item_from_creation_to_adjustment_and_summary(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$categoryId = DB::table('inventory_categories')->insertGetId([
|
||||
'type' => 'classroom',
|
||||
'name' => 'E2E Classroom Supplies',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->postJson('/api/v1/inventory/items', [
|
||||
'type' => 'classroom',
|
||||
'category_id' => $categoryId,
|
||||
'name' => 'Dry erase markers',
|
||||
'quantity' => 12,
|
||||
'condition' => 'good',
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
])->assertCreated();
|
||||
|
||||
$itemId = (int) DB::table('inventory_items')
|
||||
->where('name', 'Dry erase markers')
|
||||
->value('id');
|
||||
$this->assertGreaterThan(0, $itemId);
|
||||
|
||||
$this->postJson('/api/v1/inventory/items/' . $itemId . '/adjust', [
|
||||
'mode' => 'out',
|
||||
'quantity' => 2,
|
||||
'reason' => 'Issued to teacher cart',
|
||||
])->assertOk()->assertJsonPath('status', true);
|
||||
|
||||
$this->postJson('/api/v1/inventory/items/' . $itemId . '/audit', [
|
||||
'needs_repair_qty' => 1,
|
||||
'need_replace_qty' => 0,
|
||||
'cannot_find_qty' => 0,
|
||||
])->assertOk()->assertJsonPath('status', true);
|
||||
|
||||
$this->assertDatabaseHas('inventory_movements', [
|
||||
'item_id' => $itemId,
|
||||
'movement_type' => 'out',
|
||||
]);
|
||||
$this->assertDatabaseHas('inventory_items', [
|
||||
'id' => $itemId,
|
||||
'needs_repair_qty' => 1,
|
||||
]);
|
||||
|
||||
$this->getJson('/api/v1/inventory/summary/classroom')
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true);
|
||||
}
|
||||
|
||||
public function test_finance_followup_records_notes_promises_and_resolution_for_unpaid_parent(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$parentId = $world['parent_id'];
|
||||
$invoiceId = $this->seedInvoiceForParent($parentId, [
|
||||
'balance' => 300.00,
|
||||
'paid_amount' => 200.00,
|
||||
'status' => 'partial',
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$this->getJson('/api/v1/finance/parent-payment-followups')
|
||||
->assertOk()
|
||||
->assertJsonPath('ok', true);
|
||||
|
||||
$this->postJson('/api/v1/finance/parent-payment-followups/' . $parentId . '/note', [
|
||||
'note' => 'Left voicemail about remaining balance.',
|
||||
'invoice_id' => $invoiceId,
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->postJson('/api/v1/finance/parent-payment-followups/' . $parentId . '/promise-to-pay', [
|
||||
'invoice_id' => $invoiceId,
|
||||
'amount' => 300.00,
|
||||
'promise_date' => '2025-10-20',
|
||||
'note' => 'Parent promised to pay next Sunday.',
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->postJson('/api/v1/finance/parent-payment-followups/' . $parentId . '/mark-contacted', [
|
||||
'invoice_id' => $invoiceId,
|
||||
'contacted_at' => '2025-10-06 12:00:00',
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->postJson('/api/v1/finance/parent-payment-followups/' . $parentId . '/resolve', [
|
||||
'invoice_id' => $invoiceId,
|
||||
'resolution_note' => 'Resolved after payment arrangement.',
|
||||
])->assertSuccessful();
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\AssertsE2EApiResponses;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario P: Settings, configuration, calendar, and notification governance.
|
||||
*
|
||||
* This verifies that operational settings can be reviewed, changed, and audited
|
||||
* through the API instead of being tribal knowledge in someone's browser tab.
|
||||
*/
|
||||
class ScenarioPSettingsConfigurationAndCalendarGovernanceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_admin_manages_calendar_event_configuration_and_notification_lifecycle(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$this->getJson('/api/v1/settings')
|
||||
->assertOk();
|
||||
|
||||
$settingsUpdate = $this->patchJson('/api/v1/settings', [
|
||||
'timezone' => 'America/New_York',
|
||||
'school_name' => 'Al-Rahma Test School',
|
||||
'default_semester' => self::E2E_SEMESTER,
|
||||
'default_school_year' => self::E2E_SCHOOL_YEAR,
|
||||
]);
|
||||
$this->assertNoServerError($settingsUpdate, 'Settings update');
|
||||
|
||||
$this->getJson('/api/v1/settings/school-calendar/options')
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true);
|
||||
|
||||
$event = $this->postJson('/api/v1/settings/school-calendar/events', [
|
||||
'title' => 'E2E Parent Orientation',
|
||||
'date' => '2025-10-12',
|
||||
'description' => 'Orientation for new families.',
|
||||
'audience' => 'parents',
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
]);
|
||||
$event->assertCreated()->assertJsonPath('status', true);
|
||||
|
||||
$eventId = (int) DB::table('calendar_events')
|
||||
->where('title', 'E2E Parent Orientation')
|
||||
->value('id');
|
||||
$this->assertGreaterThan(0, $eventId);
|
||||
|
||||
$this->getJson('/api/v1/settings/school-calendar/events?school_year=' . self::E2E_SCHOOL_YEAR . '&include_meetings=1')
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true);
|
||||
|
||||
$this->patchJson('/api/v1/settings/school-calendar/events/' . $eventId, [
|
||||
'title' => 'E2E Parent Orientation Updated',
|
||||
'date' => '2025-10-19',
|
||||
'description' => 'Updated orientation for new families.',
|
||||
'audience' => 'parents',
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
])->assertOk()->assertJsonPath('status', true);
|
||||
|
||||
$notification = $this->postJson('/api/v1/notifications', [
|
||||
'title' => 'Orientation Reminder',
|
||||
'message' => 'Parent orientation moved to October 19.',
|
||||
'audience' => 'parents',
|
||||
'type' => 'announcement',
|
||||
'starts_at' => '2025-10-10 09:00:00',
|
||||
'ends_at' => '2025-10-20 09:00:00',
|
||||
'is_active' => true,
|
||||
]);
|
||||
$this->assertSuccessfulOrValidation($notification, 'Notification creation');
|
||||
|
||||
$notificationId = (int) DB::table('notifications')
|
||||
->where('title', 'Orientation Reminder')
|
||||
->value('id');
|
||||
|
||||
if ($notificationId > 0) {
|
||||
$this->getJson('/api/v1/notifications/active')
|
||||
->assertOk();
|
||||
|
||||
$this->postJson('/api/v1/notifications/' . $notificationId . '/read')
|
||||
->assertSuccessful();
|
||||
|
||||
$this->deleteJson('/api/v1/notifications/' . $notificationId)
|
||||
->assertSuccessful();
|
||||
|
||||
$this->postJson('/api/v1/notifications/' . $notificationId . '/restore')
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
$this->deleteJson('/api/v1/settings/school-calendar/events/' . $eventId)
|
||||
->assertOk()
|
||||
->assertJsonPath('status', true);
|
||||
}
|
||||
|
||||
public function test_configuration_admin_crud_keeps_school_year_context_explicit(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$this->getJson('/api/v1/configuration-admin')
|
||||
->assertOk();
|
||||
|
||||
$create = $this->postJson('/api/v1/configuration-admin', [
|
||||
'config_key' => 'e2e_guardrail_message',
|
||||
'config_value' => 'E2E guardrail enabled',
|
||||
'description' => 'Created by explicit workflow coverage.',
|
||||
]);
|
||||
$this->assertSuccessfulOrValidation($create, 'Configuration creation');
|
||||
|
||||
$configId = (int) DB::table('configuration')
|
||||
->where('config_key', 'e2e_guardrail_message')
|
||||
->value('id');
|
||||
|
||||
$this->getJson('/api/v1/configuration-admin/key/e2e_guardrail_message')
|
||||
->assertSuccessful();
|
||||
|
||||
if ($configId > 0) {
|
||||
$this->patchJson('/api/v1/configuration-admin/' . $configId, [
|
||||
'config_value' => 'E2E guardrail updated',
|
||||
'description' => 'Updated by explicit workflow coverage.',
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->deleteJson('/api/v1/configuration-admin/' . $configId)
|
||||
->assertSuccessful();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\AssertsE2EApiResponses;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario Q: School-year closure and promotion lifecycle.
|
||||
*
|
||||
* This pins down the dangerous annual transition: promotion decisions, parent
|
||||
* enrollment steps, close-preview, close, reopen, and read-only protection.
|
||||
*/
|
||||
class ScenarioQSchoolYearPromotionClosureLifecycleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_admin_and_parent_work_through_promotion_before_year_close(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$parent = $world['parent'];
|
||||
$studentId = $world['student_id'];
|
||||
|
||||
DB::table('enrollments')->insert([
|
||||
'parent_id' => $parent->id,
|
||||
'student_id' => $studentId,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'enrollment_status' => 'enrolled',
|
||||
'enrollment_date' => '2025-09-01',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$this->getJson('/api/v1/administrator/promotions')
|
||||
->assertOk();
|
||||
$this->getJson('/api/v1/administrator/promotions/summary')
|
||||
->assertOk();
|
||||
|
||||
$evaluate = $this->postJson('/api/v1/administrator/promotions/evaluate', [
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'student_ids' => [$studentId],
|
||||
]);
|
||||
$this->assertNoServerError($evaluate, 'Promotion evaluation');
|
||||
|
||||
$promotionId = (int) DB::table('student_promotion_records')
|
||||
->where('student_id', $studentId)
|
||||
->value('id');
|
||||
|
||||
if ($promotionId > 0) {
|
||||
$this->patchJson('/api/v1/administrator/promotions/' . $promotionId . '/status', [
|
||||
'status' => 'parent_action_required',
|
||||
'decision' => 'promote',
|
||||
'notes' => 'Ready for next level.',
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->actingAs($parent, 'api');
|
||||
$this->getJson('/api/v1/parents/promotions')
|
||||
->assertOk();
|
||||
$this->postJson('/api/v1/parents/promotions/' . $promotionId . '/start')
|
||||
->assertSuccessful();
|
||||
$this->patchJson('/api/v1/parents/promotions/' . $promotionId . '/steps', [
|
||||
'confirm_returning' => true,
|
||||
'confirm_contact_info' => true,
|
||||
'confirm_policy' => true,
|
||||
])->assertSuccessful();
|
||||
$this->postJson('/api/v1/parents/promotions/' . $promotionId . '/submit')
|
||||
->assertSuccessful();
|
||||
}
|
||||
}
|
||||
|
||||
public function test_admin_previews_closes_and_reopens_school_year_with_balance_transfer_guardrails(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$parentId = $world['parent_id'];
|
||||
$studentId = $world['student_id'];
|
||||
$this->seedInvoiceForParent($parentId, [
|
||||
'invoice_number' => 'INV-CLOSE-' . random_int(1000, 9999),
|
||||
'balance' => 125.00,
|
||||
'paid_amount' => 375.00,
|
||||
'total_amount' => 500.00,
|
||||
'status' => 'partial',
|
||||
]);
|
||||
|
||||
$activeYearId = (int) DB::table('school_years')
|
||||
->where('name', self::E2E_SCHOOL_YEAR)
|
||||
->value('id');
|
||||
$this->assertGreaterThan(0, $activeYearId);
|
||||
|
||||
DB::table('enrollments')->updateOrInsert(
|
||||
['student_id' => $studentId, 'school_year' => self::E2E_SCHOOL_YEAR],
|
||||
[
|
||||
'parent_id' => $parentId,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'enrollment_status' => 'enrolled',
|
||||
'enrollment_date' => '2025-09-01',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$this->postJson('/api/v1/school-years/' . $activeYearId . '/validate-close', [
|
||||
'new_school_year' => [
|
||||
'name' => '2026-2027',
|
||||
'start_date' => '2026-09-01',
|
||||
'end_date' => '2027-06-30',
|
||||
],
|
||||
'transfer_unpaid_balances' => true,
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->postJson('/api/v1/school-years/' . $activeYearId . '/preview-close', [
|
||||
'new_school_year' => [
|
||||
'name' => '2026-2027',
|
||||
'start_date' => '2026-09-01',
|
||||
'end_date' => '2027-06-30',
|
||||
],
|
||||
'transfer_unpaid_balances' => true,
|
||||
])->assertSuccessful();
|
||||
|
||||
$close = $this->postJson('/api/v1/school-years/' . $activeYearId . '/close', [
|
||||
'new_school_year' => [
|
||||
'name' => '2026-2027',
|
||||
'start_date' => '2026-09-01',
|
||||
'end_date' => '2027-06-30',
|
||||
],
|
||||
'transfer_unpaid_balances' => true,
|
||||
'confirmation' => 'CLOSE ' . self::E2E_SCHOOL_YEAR,
|
||||
]);
|
||||
$this->assertNoServerError($close, 'School year close');
|
||||
|
||||
$newYearId = (int) DB::table('school_years')
|
||||
->where('name', '2026-2027')
|
||||
->value('id');
|
||||
|
||||
if ($newYearId > 0) {
|
||||
$this->postJson('/api/v1/school-years/' . $newYearId . '/reopen')
|
||||
->assertSuccessful();
|
||||
}
|
||||
}
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\AssertsE2EApiResponses;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario R: Attendance operations and exception handling.
|
||||
*
|
||||
* This covers the actual attendance day: teacher grid, submission, admin edits,
|
||||
* late slips, early dismissal, follow-up, violation tracking, and templates.
|
||||
*/
|
||||
class ScenarioRAttendanceOperationsAndExceptionHandlingTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_teacher_admin_and_staff_handle_attendance_day_end_to_end(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$teacher = $world['teacher'];
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$studentId = $world['student_id'];
|
||||
$classSectionId = $world['class_section_id'];
|
||||
|
||||
$this->actingAs($teacher, 'api');
|
||||
$this->getJson('/api/v1/attendance/teacher/grid?class_section_id=' . $classSectionId . '&date=2025-10-05')
|
||||
->assertSuccessful();
|
||||
$this->getJson('/api/v1/attendance/teacher/form?class_section_id=' . $classSectionId . '&date=2025-10-05')
|
||||
->assertSuccessful();
|
||||
|
||||
$submit = $this->postJson('/api/v1/attendance/teacher/submit', [
|
||||
'class_section_id' => $classSectionId,
|
||||
'date' => '2025-10-05',
|
||||
'attendance' => [
|
||||
[
|
||||
'student_id' => $studentId,
|
||||
'status' => 'present',
|
||||
'comment' => 'On time',
|
||||
],
|
||||
],
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
]);
|
||||
$this->assertNoServerError($submit, 'Teacher attendance submit');
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
$this->getJson('/api/v1/attendance/admin/daily?date=2025-10-05&class_section_id=' . $classSectionId)
|
||||
->assertSuccessful();
|
||||
|
||||
$manualAdd = $this->postJson('/api/v1/attendance/admin/add-entry', [
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'date' => '2025-10-05',
|
||||
'status' => 'late',
|
||||
'comment' => 'Arrived after assembly.',
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
]);
|
||||
$this->assertNoServerError($manualAdd, 'Admin attendance manual entry');
|
||||
|
||||
$this->getJson('/api/v1/attendance/staff/month?month=2025-10')
|
||||
->assertSuccessful();
|
||||
$this->getJson('/api/v1/attendance/staff/admins')
|
||||
->assertSuccessful();
|
||||
|
||||
$staffCell = $this->postJson('/api/v1/attendance/staff/cell', [
|
||||
'user_id' => $admin->id,
|
||||
'date' => '2025-10-05',
|
||||
'status' => 'present',
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
]);
|
||||
$this->assertNoServerError($staffCell, 'Staff attendance cell save');
|
||||
}
|
||||
|
||||
public function test_early_dismissal_late_slip_and_violation_tracking_are_connected(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$studentId = $world['student_id'];
|
||||
$classSectionId = $world['class_section_id'];
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$this->getJson('/api/v1/attendance/early-dismissals/student-options')
|
||||
->assertSuccessful();
|
||||
|
||||
$dismissal = $this->postJson('/api/v1/attendance/early-dismissals', [
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'dismissal_date' => '2025-10-05',
|
||||
'dismissal_time' => '12:10',
|
||||
'reason' => 'Medical appointment',
|
||||
'released_to' => 'Parent Fixture',
|
||||
'relationship' => 'Parent',
|
||||
]);
|
||||
$this->assertNoServerError($dismissal, 'Early dismissal creation');
|
||||
|
||||
$signature = $this->post('/api/v1/attendance/early-dismissals/signature', [
|
||||
'student_id' => $studentId,
|
||||
'signature' => UploadedFile::fake()->image('signature.png', 200, 100),
|
||||
]);
|
||||
$this->assertSuccessfulOrValidation($signature, 'Early dismissal signature upload');
|
||||
|
||||
$this->getJson('/api/v1/attendance/late-slip-logs')
|
||||
->assertSuccessful();
|
||||
|
||||
$record = $this->postJson('/api/v1/attendance-tracking/record', [
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'date' => '2025-10-05',
|
||||
'type' => 'late',
|
||||
'status' => 'pending',
|
||||
'note' => 'Second late arrival this month.',
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
]);
|
||||
$this->assertNoServerError($record, 'Attendance tracking record');
|
||||
|
||||
$this->getJson('/api/v1/attendance-tracking/pending-violations')
|
||||
->assertSuccessful();
|
||||
$this->getJson('/api/v1/attendance-tracking/student-case/' . $studentId)
|
||||
->assertSuccessful();
|
||||
$this->getJson('/api/v1/attendance-tracking/compose?student_id=' . $studentId)
|
||||
->assertSuccessful();
|
||||
|
||||
$note = $this->postJson('/api/v1/attendance-tracking/save-notification-note', [
|
||||
'student_id' => $studentId,
|
||||
'note' => 'Parent notified about attendance pattern.',
|
||||
'date' => '2025-10-05',
|
||||
]);
|
||||
$this->assertNoServerError($note, 'Attendance tracking note');
|
||||
}
|
||||
|
||||
public function test_attendance_comment_templates_support_create_update_delete_and_legacy_aliases(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$create = $this->postJson('/api/v1/attendance-comment-templates', [
|
||||
'comment' => 'E2E: arrived after first lesson.',
|
||||
'type' => 'late',
|
||||
'is_active' => true,
|
||||
]);
|
||||
$create->assertCreated();
|
||||
|
||||
$templateId = (int) DB::table('attendance_comment_templates')
|
||||
->where('comment', 'E2E: arrived after first lesson.')
|
||||
->value('id');
|
||||
$this->assertGreaterThan(0, $templateId);
|
||||
|
||||
$this->getJson('/api/v1/attendance-comment-templates/list-data')
|
||||
->assertOk();
|
||||
$this->putJson('/api/v1/attendance-comment-templates/' . $templateId, [
|
||||
'comment' => 'E2E: arrived late, parent notified.',
|
||||
'type' => 'late',
|
||||
'is_active' => true,
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->postJson('/api/v1/attendance-templates/save', [
|
||||
'id' => $templateId,
|
||||
'comment' => 'Legacy alias update',
|
||||
'type' => 'late',
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->deleteJson('/api/v1/attendance-comment-templates/' . $templateId)
|
||||
->assertSuccessful();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\AssertsE2EApiResponses;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario S: Scores, grading, placement, and report cards.
|
||||
*
|
||||
* This walks from teacher-entered scores to admin grading decisions and report
|
||||
* card readiness. Because grades are where "just trust the UI" goes to die.
|
||||
*/
|
||||
class ScenarioSScoringGradingAndReportCardLifecycleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_teacher_records_every_score_type_and_admin_reviews_grading_outputs(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$teacher = $world['teacher'];
|
||||
$studentId = $world['student_id'];
|
||||
$classSectionId = $world['class_section_id'];
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
|
||||
$this->actingAs($teacher, 'api');
|
||||
|
||||
foreach (['homework', 'quizzes', 'projects'] as $type) {
|
||||
$column = $this->postJson('/api/v1/scores/' . $type . '/columns', [
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
]);
|
||||
$this->assertNoServerError($column, ucfirst($type) . ' score column creation');
|
||||
|
||||
$save = $this->postJson('/api/v1/scores/' . $type, [
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'scores' => [
|
||||
(string) $studentId => ['1' => 91],
|
||||
],
|
||||
'missing_ok' => [],
|
||||
]);
|
||||
$this->assertNoServerError($save, ucfirst($type) . ' score save');
|
||||
}
|
||||
|
||||
foreach (['midterm', 'final', 'participation'] as $type) {
|
||||
$save = $this->postJson('/api/v1/scores/' . $type, [
|
||||
'class_section_id' => $classSectionId,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'scores' => [
|
||||
(string) $studentId => 88,
|
||||
],
|
||||
]);
|
||||
$this->assertNoServerError($save, ucfirst($type) . ' score save');
|
||||
}
|
||||
|
||||
$this->postJson('/api/v1/scores/comments', [
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'comment' => 'Consistent progress across the semester.',
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->getJson('/api/v1/scores/overview?class_section_id=' . $classSectionId . '&school_year=' . self::E2E_SCHOOL_YEAR)
|
||||
->assertSuccessful();
|
||||
$this->getJson('/api/v1/scores/student-scores?student_id=' . $studentId . '&class_section_id=' . $classSectionId)
|
||||
->assertSuccessful();
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
$this->getJson('/api/v1/grading/overview?class_section_id=' . $classSectionId)
|
||||
->assertSuccessful();
|
||||
$this->postJson('/api/v1/grading/decisions/generate', [
|
||||
'class_section_id' => $classSectionId,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
])->assertSuccessful();
|
||||
$this->getJson('/api/v1/grading/decisions?class_section_id=' . $classSectionId)
|
||||
->assertSuccessful();
|
||||
$this->getJson('/api/v1/reports/report-cards/completeness?class_section_id=' . $classSectionId . '&school_year=' . self::E2E_SCHOOL_YEAR . '&semester=' . self::E2E_SEMESTER)
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
public function test_grading_locks_and_below_sixty_intervention_are_visible_and_mutable(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$studentId = $world['student_id'];
|
||||
$classSectionId = $world['class_section_id'];
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$this->postJson('/api/v1/grading/locks/toggle', [
|
||||
'type' => 'homework',
|
||||
'class_section_id' => $classSectionId,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'locked' => true,
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->postJson('/api/v1/grading/locks/all', [
|
||||
'class_section_id' => $classSectionId,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'locked' => false,
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->getJson('/api/v1/grading/below-sixty?school_year=' . self::E2E_SCHOOL_YEAR)
|
||||
->assertSuccessful();
|
||||
$this->postJson('/api/v1/grading/below-sixty/decisions', [
|
||||
'student_id' => $studentId,
|
||||
'class_section_id' => $classSectionId,
|
||||
'decision' => 'monitor',
|
||||
'notes' => 'E2E intervention note.',
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
])->assertSuccessful();
|
||||
$this->postJson('/api/v1/grading/below-sixty/meeting', [
|
||||
'student_id' => $studentId,
|
||||
'meeting_date' => '2025-10-12',
|
||||
'notes' => 'Parent meeting scheduled.',
|
||||
])->assertSuccessful();
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\Concerns\AssertsE2EApiResponses;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario T: Documents, print requests, badges, stickers, slips, and certificates.
|
||||
*
|
||||
* This tests the school paper factory: upload, queue, admin processing, badge
|
||||
* generation, certificate audit, and report exports. Bureaucracy, now with JSON.
|
||||
*/
|
||||
class ScenarioTDocumentsPrintBadgesAndCertificatesLifecycleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_teacher_print_request_moves_from_upload_to_admin_status_and_file_access(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$teacher = $world['teacher'];
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$classSectionId = $world['class_section_id'];
|
||||
|
||||
$this->actingAs($teacher, 'api');
|
||||
$this->getJson('/api/v1/print-requests/teacher')
|
||||
->assertSuccessful();
|
||||
|
||||
$create = $this->post('/api/v1/print-requests', [
|
||||
'file' => UploadedFile::fake()->create('e2e-lesson.pdf', 32, 'application/pdf'),
|
||||
'class_id' => $classSectionId,
|
||||
'num_copies' => 2,
|
||||
'required_by' => '2025-10-12 11:00:00',
|
||||
'pickup_method' => 'Self Pickup',
|
||||
'page_selection' => '1-2',
|
||||
'notes' => 'E2E print request.',
|
||||
]);
|
||||
$this->assertStatusIn($create, [201, 422], 'Print request upload');
|
||||
|
||||
$requestId = (int) DB::table('print_requests')
|
||||
->where('teacher_id', $teacher->id)
|
||||
->value('id');
|
||||
|
||||
if ($requestId > 0) {
|
||||
$this->get('/api/v1/print-requests/' . $requestId . '/file')
|
||||
->assertSuccessful();
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
$this->getJson('/api/v1/print-requests/admin')
|
||||
->assertSuccessful();
|
||||
$this->patchJson('/api/v1/print-requests/' . $requestId . '/status', [
|
||||
'status' => 'processing',
|
||||
'admin_notes' => 'Queued for printing.',
|
||||
])->assertSuccessful();
|
||||
$this->get('/api/v1/print-requests/' . $requestId . '/file')
|
||||
->assertSuccessful();
|
||||
}
|
||||
}
|
||||
|
||||
public function test_badges_stickers_slips_and_certificates_are_generated_or_validated_without_crashing(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$studentId = $world['student_id'];
|
||||
$classSectionId = $world['class_section_id'];
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$this->getJson('/api/v1/badges/form-data')
|
||||
->assertSuccessful();
|
||||
$this->getJson('/api/v1/badges/print-status?student_ids[]=' . $studentId)
|
||||
->assertSuccessful();
|
||||
|
||||
$badge = $this->postJson('/api/v1/badges/pdf', [
|
||||
'student_ids' => [$studentId],
|
||||
'class_section_id' => $classSectionId,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
]);
|
||||
$this->assertNoServerError($badge, 'Badge PDF generation');
|
||||
|
||||
$this->postJson('/api/v1/badges/log-print', [
|
||||
'student_ids' => [$studentId],
|
||||
'printed_at' => '2025-10-05 10:00:00',
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->getJson('/api/v1/reports/stickers/form-data')
|
||||
->assertSuccessful();
|
||||
$this->getJson('/api/v1/reports/stickers/students?class_section_id=' . $classSectionId)
|
||||
->assertSuccessful();
|
||||
$sticker = $this->postJson('/api/v1/reports/stickers/print', [
|
||||
'student_ids' => [$studentId],
|
||||
'class_section_id' => $classSectionId,
|
||||
]);
|
||||
$this->assertNoServerError($sticker, 'Sticker print');
|
||||
|
||||
$slipPreview = $this->postJson('/api/v1/reports/slips/preview', [
|
||||
'student_id' => $studentId,
|
||||
'type' => 'late',
|
||||
'reason' => 'Late arrival',
|
||||
'date' => '2025-10-05',
|
||||
]);
|
||||
$this->assertNoServerError($slipPreview, 'Slip preview');
|
||||
|
||||
$this->getJson('/api/v1/reports/slips/logs')
|
||||
->assertSuccessful();
|
||||
|
||||
$this->getJson('/api/v1/administrator/certificates/dashboard')
|
||||
->assertSuccessful();
|
||||
$this->getJson('/api/v1/administrator/certificates/form-options')
|
||||
->assertSuccessful();
|
||||
|
||||
$certificate = $this->postJson('/api/v1/administrator/certificates/generate', [
|
||||
'student_id' => $studentId,
|
||||
'certificate_type' => 'completion',
|
||||
'title' => 'E2E Completion Certificate',
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
]);
|
||||
$this->assertNoServerError($certificate, 'Certificate generation');
|
||||
|
||||
$this->getJson('/api/v1/administrator/certificates/audit-log')
|
||||
->assertSuccessful();
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\AssertsE2EApiResponses;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario U: Finance billing, payment, refund, installment, and notification lifecycle.
|
||||
*
|
||||
* This tests money moving through the system. Which is where vague tests become
|
||||
* very expensive fan fiction.
|
||||
*/
|
||||
class ScenarioUFinanceBillingPaymentRefundAndInstallmentLifecycleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_admin_generates_invoice_records_payment_transaction_and_receipt_notification(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$parentId = $world['parent_id'];
|
||||
$studentId = $world['student_id'];
|
||||
$invoiceId = $this->seedInvoiceForParent($parentId, [
|
||||
'invoice_number' => 'INV-E2E-PAY-' . random_int(1000, 9999),
|
||||
'total_amount' => 600.00,
|
||||
'balance' => 600.00,
|
||||
'status' => 'unpaid',
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
$this->getJson('/api/v1/finance/invoices/management')
|
||||
->assertSuccessful();
|
||||
$this->getJson('/api/v1/finance/invoices/parent/' . $parentId)
|
||||
->assertSuccessful();
|
||||
$this->getJson('/api/v1/finance/invoices/' . $invoiceId . '/preview')
|
||||
->assertSuccessful();
|
||||
|
||||
$payment = $this->postJson('/api/v1/finance/payments', [
|
||||
'invoice_id' => $invoiceId,
|
||||
'parent_id' => $parentId,
|
||||
'amount' => 250.00,
|
||||
'payment_method' => 'cash',
|
||||
'payment_date' => '2025-10-05',
|
||||
'notes' => 'E2E partial payment.',
|
||||
]);
|
||||
$this->assertNoServerError($payment, 'Payment creation');
|
||||
|
||||
$paymentId = (int) DB::table('payments')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->value('id');
|
||||
|
||||
if ($paymentId > 0) {
|
||||
$this->postJson('/api/v1/finance/payment-transactions', [
|
||||
'payment_id' => $paymentId,
|
||||
'transaction_id' => 'E2E-TXN-' . random_int(1000, 9999),
|
||||
'amount' => 250.00,
|
||||
'status' => 'completed',
|
||||
'provider' => 'manual',
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->getJson('/api/v1/finance/payment-transactions/payment/' . $paymentId)
|
||||
->assertSuccessful();
|
||||
$this->postJson('/api/v1/finance/payments/' . $paymentId . '/send-receipt')
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
$this->postJson('/api/v1/finance/fees/tuition-total', [
|
||||
'parent_id' => $parentId,
|
||||
'student_ids' => [$studentId],
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
])->assertSuccessful();
|
||||
$this->postJson('/api/v1/finance/fees/family-balance', [
|
||||
'parent_id' => $parentId,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
])->assertSuccessful();
|
||||
}
|
||||
|
||||
public function test_admin_creates_installment_plan_refund_and_balance_carryforward_decision(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$parentId = $world['parent_id'];
|
||||
$invoiceId = $this->seedInvoiceForParent($parentId, [
|
||||
'total_amount' => 900.00,
|
||||
'balance' => 900.00,
|
||||
'status' => 'unpaid',
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$plan = $this->postJson('/api/v1/finance/invoices/' . $invoiceId . '/installment-plans', [
|
||||
'installments' => [
|
||||
['due_date' => '2025-10-15', 'amount' => 300.00],
|
||||
['due_date' => '2025-11-15', 'amount' => 300.00],
|
||||
['due_date' => '2025-12-15', 'amount' => 300.00],
|
||||
],
|
||||
'notes' => 'E2E three-payment plan.',
|
||||
]);
|
||||
$this->assertNoServerError($plan, 'Installment plan creation');
|
||||
|
||||
$planId = (int) DB::table('installment_plans')
|
||||
->where('invoice_id', $invoiceId)
|
||||
->value('id');
|
||||
if ($planId > 0) {
|
||||
$this->postJson('/api/v1/finance/installment-plans/' . $planId . '/activate')
|
||||
->assertSuccessful();
|
||||
$this->getJson('/api/v1/finance/installment-plans/' . $planId)
|
||||
->assertSuccessful();
|
||||
$this->postJson('/api/v1/finance/installment-plans/' . $planId . '/restructure', [
|
||||
'reason' => 'Parent requested revised dates.',
|
||||
'installments' => [
|
||||
['due_date' => '2025-10-22', 'amount' => 450.00],
|
||||
['due_date' => '2025-11-22', 'amount' => 450.00],
|
||||
],
|
||||
])->assertSuccessful();
|
||||
}
|
||||
|
||||
$refund = $this->postJson('/api/v1/finance/refunds', [
|
||||
'parent_id' => $parentId,
|
||||
'invoice_id' => $invoiceId,
|
||||
'amount' => 50.00,
|
||||
'reason' => 'E2E overpayment adjustment.',
|
||||
'status' => 'pending',
|
||||
]);
|
||||
$this->assertNoServerError($refund, 'Refund creation');
|
||||
|
||||
$refundId = (int) DB::table('refunds')
|
||||
->where('parent_id', $parentId)
|
||||
->value('id');
|
||||
if ($refundId > 0) {
|
||||
$this->postJson('/api/v1/finance/refunds/' . $refundId . '/approve')
|
||||
->assertSuccessful();
|
||||
$this->postJson('/api/v1/finance/refunds/' . $refundId . '/payments', [
|
||||
'amount' => 50.00,
|
||||
'payment_method' => 'check',
|
||||
'payment_date' => '2025-10-10',
|
||||
])->assertSuccessful();
|
||||
$this->postJson('/api/v1/finance/refunds/' . $refundId . '/send-receipt')
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
$this->getJson('/api/v1/finance/carryforwards/preview?from_school_year=' . self::E2E_SCHOOL_YEAR . '&to_school_year=2026-2027')
|
||||
->assertSuccessful();
|
||||
$draft = $this->postJson('/api/v1/finance/carryforwards/draft', [
|
||||
'from_school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'to_school_year' => '2026-2027',
|
||||
'parent_ids' => [$parentId],
|
||||
]);
|
||||
$this->assertNoServerError($draft, 'Balance carryforward draft');
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\Concerns\AssertsE2EApiResponses;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario V: Procurement, suppliers, purchase orders, and reimbursements.
|
||||
*
|
||||
* This tests the less glamorous truth: supplies arrive, people ask for money
|
||||
* back, batches are locked, files exist, and reports still need to reconcile.
|
||||
*/
|
||||
class ScenarioVProcurementReimbursementAndSupplyLifecycleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_admin_manages_suppliers_supply_categories_purchase_order_and_receipt(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$category = $this->postJson('/api/v1/inventory/supply-categories', [
|
||||
'name' => 'E2E Office Supplies',
|
||||
'description' => 'Supplies used by E2E procurement tests.',
|
||||
]);
|
||||
$this->assertSuccessfulOrValidation($category, 'Supply category creation');
|
||||
|
||||
$supplier = $this->postJson('/api/v1/inventory/suppliers', [
|
||||
'name' => 'E2E Supplier LLC',
|
||||
'contact_name' => 'Supply Contact',
|
||||
'email' => 'supplier@example.test',
|
||||
'phone' => '5552223333',
|
||||
'address' => '1 Supply Road',
|
||||
]);
|
||||
$this->assertSuccessfulOrValidation($supplier, 'Supplier creation');
|
||||
|
||||
$supplierId = (int) DB::table('suppliers')->where('name', 'E2E Supplier LLC')->value('id');
|
||||
$categoryId = (int) DB::table('supply_categories')->where('name', 'E2E Office Supplies')->value('id');
|
||||
|
||||
if ($supplierId > 0 && $categoryId > 0) {
|
||||
$order = $this->postJson('/api/v1/finance/purchase-orders', [
|
||||
'supplier_id' => $supplierId,
|
||||
'order_date' => '2025-10-05',
|
||||
'expected_date' => '2025-10-12',
|
||||
'status' => 'draft',
|
||||
'items' => [
|
||||
[
|
||||
'name' => 'Printer paper',
|
||||
'category_id' => $categoryId,
|
||||
'quantity' => 10,
|
||||
'unit_price' => 6.50,
|
||||
],
|
||||
],
|
||||
'notes' => 'E2E purchase order.',
|
||||
]);
|
||||
$this->assertNoServerError($order, 'Purchase order creation');
|
||||
|
||||
$purchaseOrderId = (int) DB::table('purchase_orders')
|
||||
->where('supplier_id', $supplierId)
|
||||
->value('id');
|
||||
|
||||
if ($purchaseOrderId > 0) {
|
||||
$this->getJson('/api/v1/finance/purchase-orders/' . $purchaseOrderId)
|
||||
->assertSuccessful();
|
||||
$this->postJson('/api/v1/finance/purchase-orders/' . $purchaseOrderId . '/receive', [
|
||||
'received_date' => '2025-10-12',
|
||||
'items' => [
|
||||
['name' => 'Printer paper', 'quantity_received' => 10],
|
||||
],
|
||||
])->assertSuccessful();
|
||||
}
|
||||
}
|
||||
|
||||
$this->getJson('/api/v1/finance/purchase-orders/options')
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
public function test_reimbursement_request_batch_lock_export_and_mark_donation_flow(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
$this->seedE2EConfiguration();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$request = $this->post('/api/v1/finance/reimbursements', [
|
||||
'requester_id' => $admin->id,
|
||||
'amount' => 45.75,
|
||||
'description' => 'E2E classroom material reimbursement.',
|
||||
'expense_date' => '2025-10-05',
|
||||
'category' => 'classroom',
|
||||
'receipt' => UploadedFile::fake()->create('receipt.pdf', 20, 'application/pdf'),
|
||||
]);
|
||||
$this->assertNoServerError($request, 'Reimbursement creation');
|
||||
|
||||
$reimbursementId = (int) DB::table('reimbursements')
|
||||
->where('requester_id', $admin->id)
|
||||
->value('id');
|
||||
|
||||
if ($reimbursementId > 0) {
|
||||
$this->putJson('/api/v1/finance/reimbursements/' . $reimbursementId, [
|
||||
'amount' => 45.75,
|
||||
'description' => 'Updated E2E reimbursement description.',
|
||||
'status' => 'under_processing',
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->postJson('/api/v1/finance/reimbursements/process', [
|
||||
'ids' => [$reimbursementId],
|
||||
'status' => 'approved',
|
||||
])->assertSuccessful();
|
||||
}
|
||||
|
||||
$batch = $this->postJson('/api/v1/finance/reimbursements/batches', [
|
||||
'name' => 'E2E Reimbursement Batch',
|
||||
'reimbursement_ids' => $reimbursementId > 0 ? [$reimbursementId] : [],
|
||||
]);
|
||||
$this->assertNoServerError($batch, 'Reimbursement batch creation');
|
||||
|
||||
$batchId = (int) DB::table('reimbursement_batches')
|
||||
->where('name', 'E2E Reimbursement Batch')
|
||||
->value('id');
|
||||
|
||||
if ($batchId > 0) {
|
||||
$this->postJson('/api/v1/finance/reimbursements/batches/assign', [
|
||||
'batch_id' => $batchId,
|
||||
'reimbursement_ids' => $reimbursementId > 0 ? [$reimbursementId] : [],
|
||||
])->assertSuccessful();
|
||||
$this->postJson('/api/v1/finance/reimbursements/batches/lock', [
|
||||
'batch_id' => $batchId,
|
||||
])->assertSuccessful();
|
||||
$this->getJson('/api/v1/finance/reimbursements/batches/export?batch_id=' . $batchId)
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
if ($reimbursementId > 0) {
|
||||
$this->postJson('/api/v1/finance/reimbursements/mark-donation', [
|
||||
'id' => $reimbursementId,
|
||||
'donation_note' => 'Donated back to school.',
|
||||
])->assertSuccessful();
|
||||
}
|
||||
|
||||
$this->getJson('/api/v1/finance/reimbursements/under-processing')
|
||||
->assertSuccessful();
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\AssertsE2EApiResponses;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario W: Access control, role permissions, navigation, preferences, and role switching.
|
||||
*
|
||||
* This asserts the application knows who the user is, what they can see, what
|
||||
* role they selected, and which menu items should exist. Minor details. Only
|
||||
* the difference between an app and a haunted spreadsheet.
|
||||
*/
|
||||
class ScenarioWAccessControlRolesNavigationAndPreferencesTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_admin_creates_role_permission_assigns_user_and_builds_navigation(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$teacher = $this->createApiUserWithRole('teacher');
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$this->getJson('/api/v1/role-permissions/roles')
|
||||
->assertOk();
|
||||
$permission = $this->postJson('/api/v1/role-permissions/permissions', [
|
||||
'name' => 'e2e.view.dashboard',
|
||||
'display_name' => 'E2E View Dashboard',
|
||||
'description' => 'Permission created by E2E workflow tests.',
|
||||
]);
|
||||
$this->assertSuccessfulOrValidation($permission, 'Permission creation');
|
||||
|
||||
$permissionId = (int) DB::table('permissions')
|
||||
->where('name', 'e2e.view.dashboard')
|
||||
->value('id');
|
||||
$teacherRoleId = (int) DB::table('roles')->where('name', 'teacher')->value('id');
|
||||
|
||||
if ($permissionId > 0 && $teacherRoleId > 0) {
|
||||
$this->putJson('/api/v1/role-permissions/roles/' . $teacherRoleId . '/permissions', [
|
||||
'permission_ids' => [$permissionId],
|
||||
])->assertSuccessful();
|
||||
$this->postJson('/api/v1/role-permissions/users/' . $teacher->id . '/roles', [
|
||||
'role_ids' => [$teacherRoleId],
|
||||
])->assertSuccessful();
|
||||
}
|
||||
|
||||
$this->getJson('/api/v1/nav-builder/menu')
|
||||
->assertOk();
|
||||
$nav = $this->postJson('/api/v1/nav-builder', [
|
||||
'title' => 'E2E Dashboard',
|
||||
'label' => 'E2E Dashboard',
|
||||
'url' => '/e2e-dashboard',
|
||||
'route' => 'e2e.dashboard',
|
||||
'icon' => 'layout-dashboard',
|
||||
'role' => 'teacher',
|
||||
'sort_order' => 99,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$this->assertSuccessfulOrValidation($nav, 'Navigation item creation');
|
||||
|
||||
$navId = (int) DB::table('nav_items')
|
||||
->where('title', 'E2E Dashboard')
|
||||
->orWhere('label', 'E2E Dashboard')
|
||||
->value('id');
|
||||
|
||||
if ($navId > 0) {
|
||||
$this->postJson('/api/v1/nav-builder/reorder', [
|
||||
'items' => [
|
||||
['id' => $navId, 'sort_order' => 1],
|
||||
],
|
||||
])->assertSuccessful();
|
||||
$this->deleteJson('/api/v1/nav-builder/' . $navId)
|
||||
->assertSuccessful();
|
||||
}
|
||||
}
|
||||
|
||||
public function test_user_preferences_and_role_switcher_preserve_selected_context(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
$roles = $this->seedApiRoles();
|
||||
$user = $this->createApiUserWithRole('teacher');
|
||||
$user->roles()->syncWithoutDetaching([$roles['parent']->id]);
|
||||
$this->actingAs($user->fresh(), 'api');
|
||||
|
||||
$this->getJson('/api/v1/role-switcher')
|
||||
->assertOk();
|
||||
$switch = $this->postJson('/api/v1/role-switcher/switch', [
|
||||
'role' => 'parent',
|
||||
]);
|
||||
$this->assertNoServerError($switch, 'Role switch');
|
||||
|
||||
$this->getJson('/api/v1/preferences')
|
||||
->assertOk();
|
||||
$this->postJson('/api/v1/preferences', [
|
||||
'key' => 'theme',
|
||||
'value' => 'dark',
|
||||
])->assertSuccessful();
|
||||
$this->postJson('/api/v1/preferences', [
|
||||
'key' => 'active_role',
|
||||
'value' => 'parent',
|
||||
])->assertSuccessful();
|
||||
$this->getJson('/api/v1/preferences/list')
|
||||
->assertOk();
|
||||
|
||||
$this->putJson('/api/v1/preferences/' . $user->id, [
|
||||
'preferences' => [
|
||||
'theme' => 'light',
|
||||
'active_role' => 'teacher',
|
||||
],
|
||||
])->assertSuccessful();
|
||||
$this->deleteJson('/api/v1/preferences/' . $user->id)
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
public function test_non_admin_cannot_manage_role_permissions_or_admin_nav_builder(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
$teacher = $this->createApiUserWithRole('teacher');
|
||||
$this->actingAs($teacher, 'api');
|
||||
|
||||
$this->getJson('/api/v1/role-permissions/roles')
|
||||
->assertForbidden();
|
||||
$this->postJson('/api/v1/role-permissions/permissions', [
|
||||
'name' => 'e2e.illegal.permission',
|
||||
])->assertForbidden();
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\AssertsE2EApiResponses;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario X: Public pages, kiosk scanning, badge scan logs, and verification links.
|
||||
*
|
||||
* This covers public and semi-public surfaces. They look harmless until someone
|
||||
* lets them mutate attendance, leak records, or crash on malformed tokens.
|
||||
*/
|
||||
class ScenarioXPublicKioskScannerAndVerificationLifecycleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_public_content_documentation_and_policy_endpoints_are_reachable_without_auth(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
|
||||
foreach ([
|
||||
'/api/v1/frontend',
|
||||
'/api/v1/frontend/facility',
|
||||
'/api/v1/frontend/team',
|
||||
'/api/v1/frontend/call-to-action',
|
||||
'/api/v1/frontend/testimonial',
|
||||
'/api/v1/pages/privacy',
|
||||
'/api/v1/pages/terms',
|
||||
'/api/v1/pages/help',
|
||||
'/api/v1/policies/school',
|
||||
'/api/v1/policies/picture',
|
||||
'/api/v1/health',
|
||||
'/api/v1/system/db-check',
|
||||
'/api/documentation/swagger.json',
|
||||
'/api/docs/public',
|
||||
] as $endpoint) {
|
||||
$response = $this->getJson($endpoint);
|
||||
$this->assertNoServerError($response, 'Public endpoint ' . $endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_badge_scan_and_scanner_process_known_and_unknown_codes_safely(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$studentId = $world['student_id'];
|
||||
$badgeCode = 'E2E-BADGE-' . random_int(1000, 9999);
|
||||
|
||||
DB::table('students')->where('id', $studentId)->update([
|
||||
'badge_scan' => $badgeCode,
|
||||
]);
|
||||
|
||||
$knownScan = $this->postJson('/api/v1/badge_scan/scan', [
|
||||
'code' => $badgeCode,
|
||||
'scanner' => 'front-desk',
|
||||
'scanned_at' => '2025-10-05 09:00:00',
|
||||
]);
|
||||
$this->assertNoServerError($knownScan, 'Known badge scan');
|
||||
|
||||
$unknownScan = $this->postJson('/api/v1/badge_scan/scan', [
|
||||
'code' => 'UNKNOWN-' . random_int(1000, 9999),
|
||||
'scanner' => 'front-desk',
|
||||
'scanned_at' => '2025-10-05 09:05:00',
|
||||
]);
|
||||
$this->assertNoServerError($unknownScan, 'Unknown badge scan');
|
||||
|
||||
$scannerProcess = $this->postJson('/api/v1/scanner/process', [
|
||||
'payload' => $badgeCode,
|
||||
'source' => 'kiosk',
|
||||
]);
|
||||
$this->assertNoServerError($scannerProcess, 'Scanner process');
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
$this->getJson('/api/v1/badge_scan/logs')
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
public function test_public_verification_and_invite_tokens_fail_closed_for_invalid_values(): void
|
||||
{
|
||||
$this->getJson('/api/certificates/verify/not-a-real-token')
|
||||
->assertStatus(404);
|
||||
|
||||
$this->assertStatusIn(
|
||||
$this->getJson('/api/timeoff/notify/not-a-real-token'),
|
||||
[200, 302, 404, 410, 422],
|
||||
'Invalid time-off notification token'
|
||||
);
|
||||
|
||||
$this->assertStatusIn(
|
||||
$this->getJson('/api/confirm_authorized_user?token=not-a-real-token'),
|
||||
[200, 302, 404, 410, 422],
|
||||
'Invalid authorized-user confirmation token'
|
||||
);
|
||||
|
||||
$this->assertStatusIn(
|
||||
$this->getJson('/api/set_authorized_user_password/999999?token=not-a-real-token'),
|
||||
[200, 302, 404, 410, 422],
|
||||
'Invalid authorized-user set-password token'
|
||||
);
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\AssertsE2EApiResponses;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario Y: Family, guardians, authorized users, and WhatsApp communications.
|
||||
*
|
||||
* Family data is deceptively dangerous: one wrong join and someone sees another
|
||||
* household. This locks down guardian attachment, flags, contact exports, and
|
||||
* authorized-user invitation flows.
|
||||
*/
|
||||
class ScenarioYFamilyGuardianAuthorizedUserAndWhatsappLifecycleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_admin_builds_family_links_and_manages_guardian_flags(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$primaryParent = $world['parent'];
|
||||
$studentId = $world['student_id'];
|
||||
$secondParent = $this->createApiUserWithRole('parent', [
|
||||
'firstname' => 'Second',
|
||||
'lastname' => 'Guardian',
|
||||
'email' => 'second.guardian.' . uniqid('', true) . '@example.test',
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$bootstrap = $this->postJson('/api/v1/families/bootstrap', [
|
||||
'primary_parent_id' => $primaryParent->id,
|
||||
'student_ids' => [$studentId],
|
||||
'home_name' => 'E2E Household',
|
||||
]);
|
||||
$this->assertNoServerError($bootstrap, 'Family bootstrap');
|
||||
|
||||
$familyId = (int) DB::table('families')
|
||||
->where('primary_parent_id', $primaryParent->id)
|
||||
->orWhere('name', 'E2E Household')
|
||||
->value('id');
|
||||
|
||||
if ($familyId > 0) {
|
||||
$this->postJson('/api/v1/families/attach-second/by-user', [
|
||||
'family_id' => $familyId,
|
||||
'user_id' => $secondParent->id,
|
||||
'relation' => 'Father',
|
||||
])->assertSuccessful();
|
||||
$this->postJson('/api/v1/families/set-primary-home', [
|
||||
'family_id' => $familyId,
|
||||
'student_id' => $studentId,
|
||||
])->assertSuccessful();
|
||||
$this->postJson('/api/v1/families/set-guardian-flags', [
|
||||
'family_id' => $familyId,
|
||||
'user_id' => $secondParent->id,
|
||||
'can_pickup' => true,
|
||||
'can_receive_billing' => false,
|
||||
'can_receive_academic' => true,
|
||||
])->assertSuccessful();
|
||||
|
||||
$this->getJson('/api/v1/families/' . $familyId . '/guardians')
|
||||
->assertSuccessful();
|
||||
$this->getJson('/api/v1/family-admin/card?family_id=' . $familyId)
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
$this->getJson('/api/v1/families/by-student/' . $studentId)
|
||||
->assertSuccessful();
|
||||
$this->getJson('/api/v1/family-admin/search?q=E2E')
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
public function test_parent_authorized_user_invite_and_household_privacy_flow(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$parent = $world['parent'];
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$this->actingAs($parent, 'api');
|
||||
|
||||
$invite = $this->postJson('/api/v1/parents/authorized-users', [
|
||||
'name' => 'E2E Pickup Helper',
|
||||
'email' => 'pickup.helper.' . uniqid('', true) . '@example.test',
|
||||
'cellphone' => '5554446666',
|
||||
'relation' => 'Uncle',
|
||||
'permissions' => ['pickup'],
|
||||
]);
|
||||
$this->assertNoServerError($invite, 'Authorized user invite creation');
|
||||
|
||||
$authorizedUserId = (int) DB::table('authorized_users')
|
||||
->where('parent_id', $parent->id)
|
||||
->value('id');
|
||||
|
||||
if ($authorizedUserId > 0) {
|
||||
$this->getJson('/api/v1/parents/authorized-users')
|
||||
->assertSuccessful();
|
||||
$this->patchJson('/api/v1/parents/authorized-users/' . $authorizedUserId, [
|
||||
'name' => 'E2E Pickup Helper Updated',
|
||||
'cellphone' => '5554447777',
|
||||
'relation' => 'Uncle',
|
||||
'permissions' => ['pickup', 'emergency'],
|
||||
])->assertSuccessful();
|
||||
}
|
||||
|
||||
$this->actingAs($admin, 'api');
|
||||
$this->getJson('/api/v1/whatsapp/parent-contacts')
|
||||
->assertSuccessful();
|
||||
$this->getJson('/api/v1/whatsapp/parent-contacts-by-class')
|
||||
->assertSuccessful();
|
||||
|
||||
$link = $this->postJson('/api/v1/whatsapp/links', [
|
||||
'title' => 'E2E Parent Group',
|
||||
'url' => 'https://chat.whatsapp.com/e2e-test-link',
|
||||
'audience' => 'parents',
|
||||
'class_section_id' => $world['class_section_id'],
|
||||
'is_active' => true,
|
||||
]);
|
||||
$this->assertSuccessfulOrValidation($link, 'WhatsApp link creation');
|
||||
|
||||
$linkId = (int) DB::table('whatsapp_links')
|
||||
->where('title', 'E2E Parent Group')
|
||||
->value('id');
|
||||
if ($linkId > 0) {
|
||||
$this->patchJson('/api/v1/whatsapp/links/' . $linkId, [
|
||||
'title' => 'E2E Parent Group Updated',
|
||||
'url' => 'https://chat.whatsapp.com/e2e-test-link-updated',
|
||||
'audience' => 'parents',
|
||||
'is_active' => true,
|
||||
])->assertSuccessful();
|
||||
$this->postJson('/api/v1/whatsapp/membership', [
|
||||
'link_id' => $linkId,
|
||||
'parent_id' => $parent->id,
|
||||
'status' => 'joined',
|
||||
])->assertSuccessful();
|
||||
}
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\V1\Workflows;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\Concerns\AssertsE2EApiResponses;
|
||||
use Tests\Concerns\SeedsE2ETestFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Scenario Z: Legacy aliases, validation contracts, and compatibility surfaces.
|
||||
*
|
||||
* This keeps old clients from silently rotting while the modern API evolves.
|
||||
* Legacy support without tests is just folklore with a URL.
|
||||
*/
|
||||
class ScenarioZLegacyAliasesValidationAndCompatibilityTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_legacy_teacher_aliases_match_modern_teacher_and_score_surfaces(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$teacher = $world['teacher'];
|
||||
$classSectionId = $world['class_section_id'];
|
||||
$this->actingAs($teacher, 'api');
|
||||
|
||||
foreach ([
|
||||
'/api/v1/teacher/homework-list?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-homework?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-quiz?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-midterm-exam?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-final-exam?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-participation?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-project?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/teacher-assignment',
|
||||
'/api/v1/teacher/class-view?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/showupdate-attendance?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/showupdate_attendance?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/scores?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/calendar',
|
||||
'/api/v1/teacher/class-progress-submit?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/class-progress-history?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/class-progress-view?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/competition-scores?class_section_id=' . $classSectionId,
|
||||
] as $endpoint) {
|
||||
$response = $this->getJson($endpoint);
|
||||
$this->assertNoServerError($response, 'Legacy teacher alias ' . $endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_legacy_post_aliases_validate_payloads_without_500s(): void
|
||||
{
|
||||
$world = $this->seedTeacherClassWithStudent();
|
||||
$teacher = $world['teacher'];
|
||||
$studentId = $world['student_id'];
|
||||
$classSectionId = $world['class_section_id'];
|
||||
$this->actingAs($teacher, 'api');
|
||||
|
||||
$scorePayload = [
|
||||
'class_section_id' => $classSectionId,
|
||||
'school_year' => self::E2E_SCHOOL_YEAR,
|
||||
'semester' => self::E2E_SEMESTER,
|
||||
'scores' => [
|
||||
(string) $studentId => ['1' => 90],
|
||||
],
|
||||
];
|
||||
|
||||
foreach ([
|
||||
'/api/v1/teacher/add-homework',
|
||||
'/api/v1/teacher/add-quiz',
|
||||
'/api/v1/teacher/add-midterm-exam',
|
||||
'/api/v1/teacher/add-final-exam',
|
||||
'/api/v1/teacher/add-participation',
|
||||
'/api/v1/teacher/add-project',
|
||||
] as $endpoint) {
|
||||
$response = $this->postJson($endpoint, $scorePayload);
|
||||
$this->assertNoServerError($response, 'Legacy score post alias ' . $endpoint);
|
||||
}
|
||||
|
||||
$progress = $this->postJson('/api/v1/teacher/class-progress-submit', [
|
||||
'class_section_id' => $classSectionId,
|
||||
'week_start' => '2025-10-05',
|
||||
'week_end' => '2025-10-11',
|
||||
'covered_islamic' => 'Legacy form Islamic coverage.',
|
||||
'covered_quran' => 'Legacy form Quran coverage.',
|
||||
'homework_islamic' => 'Legacy Islamic homework.',
|
||||
'homework_quran' => 'Legacy Quran homework.',
|
||||
]);
|
||||
$this->assertNoServerError($progress, 'Legacy class progress submit');
|
||||
}
|
||||
|
||||
public function test_validation_errors_are_structured_for_representative_public_and_authenticated_forms(): void
|
||||
{
|
||||
$this->seedE2EConfiguration();
|
||||
|
||||
$this->postJson('/api/v1/auth/login', [
|
||||
'email' => 'not-an-email',
|
||||
'password' => '',
|
||||
])->assertStatus(422)
|
||||
->assertJsonStructure(['message', 'errors']);
|
||||
|
||||
$this->postJson('/api/v1/contact', [
|
||||
'name' => '',
|
||||
'email' => 'bad',
|
||||
'message' => '',
|
||||
])->assertStatus(422)
|
||||
->assertJsonStructure(['message', 'errors']);
|
||||
|
||||
$admin = $this->actingAsApiAdministrator();
|
||||
$this->actingAs($admin, 'api');
|
||||
|
||||
$this->postJson('/api/v1/users', [
|
||||
'firstname' => '',
|
||||
'email' => 'not-an-email',
|
||||
])->assertStatus(422)
|
||||
->assertJsonStructure(['message', 'errors']);
|
||||
|
||||
$this->postJson('/api/v1/students', [
|
||||
'firstname' => '',
|
||||
'parent_id' => 999999,
|
||||
])->assertStatus(422)
|
||||
->assertJsonStructure(['message', 'errors']);
|
||||
|
||||
$this->postJson('/api/v1/finance/payments', [
|
||||
'invoice_id' => 999999,
|
||||
'amount' => -10,
|
||||
])->assertStatus(422)
|
||||
->assertJsonStructure(['message', 'errors']);
|
||||
}
|
||||
|
||||
public function test_legacy_redirects_and_non_versioned_auth_aliases_remain_available(): void
|
||||
{
|
||||
$this->get('/api/attendance-templates')
|
||||
->assertRedirect('/api/v1/attendance-templates');
|
||||
$this->get('/api/attendance-comment-templates')
|
||||
->assertRedirect('/api/v1/attendance-comment-templates');
|
||||
$this->get('/api/administrator/attendance-templates')
|
||||
->assertRedirect('/api/v1/administrator/attendance-templates');
|
||||
|
||||
$this->assertStatusIn(
|
||||
$this->postJson('/api/login', [
|
||||
'email' => 'missing@example.test',
|
||||
'password' => 'wrong-password',
|
||||
]),
|
||||
[401, 422],
|
||||
'Non-versioned login alias failure contract'
|
||||
);
|
||||
|
||||
$this->postJson('/api/register', [])
|
||||
->assertStatus(422)
|
||||
->assertJsonStructure(['message', 'errors']);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ class StudentEnrollmentWorkflowTest extends TestCase
|
||||
use RefreshDatabase;
|
||||
|
||||
private const SCHOOL_YEAR = '2025-2026';
|
||||
|
||||
private const SEMESTER = 'Fall';
|
||||
|
||||
public function test_admin_creates_account_adds_student_and_enrolls_them(): void
|
||||
@@ -128,14 +127,14 @@ class StudentEnrollmentWorkflowTest extends TestCase
|
||||
]);
|
||||
|
||||
// The class roster endpoint reflects the assignment.
|
||||
$classes = $this->getJson('/api/v1/students/'.$studentId.'/classes');
|
||||
$classes = $this->getJson('/api/v1/students/' . $studentId . '/classes');
|
||||
$classes->assertOk()->assertJsonPath('ok', true);
|
||||
$assignedIds = array_column($classes->json('classes'), 'class_section_id');
|
||||
$this->assertContains($classSectionId, $assignedIds);
|
||||
|
||||
// 5. Move the student to "enrolled" via the enrollment workflow.
|
||||
$statusResponse = $this->post(
|
||||
'/api/v1/administrator/enrollment-withdrawal/update-statuses?'.
|
||||
'/api/v1/administrator/enrollment-withdrawal/update-statuses?' .
|
||||
http_build_query(['enrollment_status' => [$studentId => 'enrolled']]),
|
||||
[]
|
||||
);
|
||||
@@ -195,14 +194,14 @@ class StudentEnrollmentWorkflowTest extends TestCase
|
||||
|
||||
// First enrol...
|
||||
$this->post(
|
||||
'/api/v1/administrator/enrollment-withdrawal/update-statuses?'.
|
||||
'/api/v1/administrator/enrollment-withdrawal/update-statuses?' .
|
||||
http_build_query(['enrollment_status' => [$studentId => 'enrolled']]),
|
||||
[]
|
||||
)->assertOk();
|
||||
|
||||
// ...then withdraw.
|
||||
$this->post(
|
||||
'/api/v1/administrator/enrollment-withdrawal/update-statuses?'.
|
||||
'/api/v1/administrator/enrollment-withdrawal/update-statuses?' .
|
||||
http_build_query(['enrollment_status' => [$studentId => 'withdrawn']]),
|
||||
[]
|
||||
)->assertOk();
|
||||
|
||||
Reference in New Issue
Block a user