Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
.env
.env.backup
.phpunit.result.cache
.phpunit.cache/
DOCUMENTATION_API_V1_PRESENTATION.md
Homestead.json
Homestead.yaml
auth.json
Expand Down
21 changes: 21 additions & 0 deletions API_ENDPOINTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# API endpoints ajoutés

Tous les endpoints utilisent le préfixe `/api/v1` et Sanctum.

## Élèves inscrits

`GET /students?page=1&per_page=20`

Retourne les élèves avec une inscription approuvée pour l'année courante. La réponse contient `data` et `pagination`. `limit` est accepté comme alias de `per_page`.

## Recettes

`GET /revenues?period=day|month|year`

Les recettes sont les perceptions dont `paid_at` est renseigné. Les totaux sont séparés dans `data.USD` et `data.CDF`. Une période explicite est possible avec `startDate=YYYY-MM-DD&endDate=YYYY-MM-DD`. Les éléments détaillés sont dans `revenues`.

## Paiement

`POST /payments`

Le paiement s'appuie sur une perception existante. Utiliser `{ "perception_id": "01..." }`, ou `{ "student_id": "01...", "amount": 100, "currency": "USD" }` pour sélectionner la perception impayée correspondante. Une réussite retourne `201` avec `data.transactionId` et `data.status=success`; une perception déjà payée retourne `409`.
50 changes: 50 additions & 0 deletions app/Http/Controllers/Api/V1/Finance/PaymentController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace App\Http\Controllers\Api\V1\Finance;

use App\Enums\Devise;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V1\Finance\PaymentStoreRequest;
use App\Models\Annee;
use App\Models\Perception;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;

class PaymentController extends Controller
{
public function store(PaymentStoreRequest $request): JsonResponse
{
$perception = DB::transaction(function () use ($request) {
$perception = $request->filled('perception_id')
? Perception::query()->lockForUpdate()->findOrFail($request->string('perception_id')->toString())
: Perception::query()
->whereHas('inscription', fn ($query) => $query->where('eleve_id', $request->string('student_id')->toString()))
->where('devise', $request->string('currency')->toString())
->where('frais_montant', (float) $request->input('amount'))
->whereNull('paid_at')
Comment on lines +22 to +24
->when(Annee::encours(), fn ($query, $annee) => $query->where('annee_id', $annee->id))
->lockForUpdate()->firstOrFail();
Comment on lines +24 to +26

if ($perception->paid_at) {
abort(409, 'Cette perception a déjà été payée.');
}

$perception->forceFill([
'montant' => $request->filled('amount') ? (float) $request->input('amount') : $perception->montant,
'paid_by' => $request->input('paid_by', auth()->id()),
'paid_at' => now(),
])->save();

return $perception->fresh();
});

return response()->json([
'data' => [
'transactionId' => $perception->id,
'status' => 'success',
'currency' => $perception->devise instanceof Devise ? $perception->devise->value : $perception->devise,
'amount' => (float) $perception->montant,
],
], 201);
}
}
54 changes: 54 additions & 0 deletions app/Http/Controllers/Api/V1/Finance/RevenueController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace App\Http\Controllers\Api\V1\Finance;

use App\Enums\Devise;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V1\Finance\RevenueIndexRequest;
use App\Models\Annee;
use App\Models\Perception;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;

class RevenueController extends Controller
{
public function index(RevenueIndexRequest $request): JsonResponse
{
$query = Perception::query()->whereNotNull('paid_at');
$annee = Annee::encours();
$query->when($annee, fn ($query) => $query->where('annee_id', $annee->id));

if ($request->filled('startDate')) {
$query->whereBetween('paid_at', [
Carbon::parse($request->string('startDate'))->startOfDay(),
Carbon::parse($request->string('endDate'))->endOfDay(),
Comment on lines +23 to +24
]);
} elseif ($request->filled('period')) {
$query->whereBetween('paid_at', match ($request->string('period')->toString()) {
'day' => [now()->startOfDay(), now()->endOfDay()],
'month' => [now()->startOfMonth(), now()->endOfMonth()],
'year' => [now()->startOfYear(), now()->endOfYear()],
});
}

$totals = (clone $query)->selectRaw('devise, COALESCE(SUM(montant), 0) as total')
->groupBy('devise')->pluck('total', 'devise');

$revenues = $query->latest('paid_at')
->paginate((int) $request->get('per_page', 20));

return response()->json([
'data' => [
Devise::USD->value => (float) ($totals[Devise::USD->value] ?? 0),
Devise::CDF->value => (float) ($totals[Devise::CDF->value] ?? 0),
],
'revenues' => $revenues->items(),
'pagination' => [
'current_page' => $revenues->currentPage(),
'per_page' => $revenues->perPage(),
'total' => $revenues->total(),
'last_page' => $revenues->lastPage(),
],
]);
}
}
30 changes: 24 additions & 6 deletions app/Http/Controllers/Api/V1/Scolarite/EleveController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,40 @@

use App\Http\Controllers\Controller;
use App\Models\Eleve;
use App\Models\Annee;
use App\Enums\InscriptionStatus;
use App\Http\Resources\EleveResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class EleveController extends Controller
{
public function index(Request $request): JsonResponse
{
$eleves = Eleve::query()
$perPage = (int) $request->get('per_page', $request->get('limit', 20));
abort_if($perPage < 1 || $perPage > 100, 422, 'Le nombre d\'éléments par page doit être compris entre 1 et 100.');
Comment on lines +17 to +18

$annee = Annee::encours();

->with([
'inscriptions.classe',
])
->paginate($request->get('per_page', 20));
$eleves = Eleve::query()
->with(['inscriptions' => fn ($query) => $query
->with('classe')
->when($annee, fn ($query) => $query->where('annee_id', $annee->id))
->where('status', InscriptionStatus::approved)])
->whereHas('inscriptions', fn ($query) => $query
->when($annee, fn ($query) => $query->where('annee_id', $annee->id))
->where('status', InscriptionStatus::approved))
->latest()
->paginate($perPage);

return response()->json([
'data' => $eleves,
'data' => EleveResource::collection($eleves->getCollection()),
'pagination' => [
'current_page' => $eleves->currentPage(),
'per_page' => $eleves->perPage(),
'total' => $eleves->total(),
'last_page' => $eleves->lastPage(),
],
]);
}
}
21 changes: 21 additions & 0 deletions app/Http/Requests/Api/V1/Finance/PaymentStoreRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace App\Http\Requests\Api\V1\Finance;

use App\Enums\Devise;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class PaymentStoreRequest extends FormRequest
{
public function rules(): array
{
return [
'perception_id' => ['nullable', 'string', 'exists:perceptions,id', 'required_without:student_id'],
'student_id' => ['nullable', 'string', 'exists:eleves,id', 'required_without:perception_id'],
'amount' => ['required_with:student_id', 'numeric', 'gt:0'],
'currency' => ['required_with:student_id', Rule::enum(Devise::class)],
Comment on lines +16 to +17
'paid_by' => ['nullable', 'string', 'max:255'],
];
}
}
19 changes: 19 additions & 0 deletions app/Http/Requests/Api/V1/Finance/RevenueIndexRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App\Http\Requests\Api\V1\Finance;

use Illuminate\Foundation\Http\FormRequest;

class RevenueIndexRequest extends FormRequest
{
public function rules(): array
{
return [
'period' => ['nullable', 'in:day,month,year'],
'startDate' => ['nullable', 'date_format:Y-m-d', 'required_with:endDate'],
'endDate' => ['nullable', 'date_format:Y-m-d', 'required_with:startDate', 'after_or_equal:startDate'],
'page' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
];
}
}
12 changes: 11 additions & 1 deletion app/Http/Resources/EleveResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ class EleveResource extends JsonResource
{
public function toArray($request)
{
return parent::toArray($request);
return [
'id' => $this->id,
'matricule' => $this->matricule,
'nom' => $this->nom,
'sexe' => $this->sexe?->value ?? $this->sexe,
'date_naissance' => $this->date_naissance,
'adresse' => $this->adresse,
'email' => $this->email,
'telephone' => $this->telephone,
'inscriptions' => InscriptionResource::collection($this->whenLoaded('inscriptions')),
];
}
}
7 changes: 2 additions & 5 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
Expand All @@ -13,8 +10,8 @@
<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/>
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
Expand Down
8 changes: 8 additions & 0 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use App\Http\Controllers\Api\V1\Finance\PerceptionClasseController;
use App\Http\Controllers\Api\V1\Finance\PerceptionController;
use App\Http\Controllers\Api\V1\Finance\StudentController;
use App\Http\Controllers\Api\V1\Finance\RevenueController;
use App\Http\Controllers\Api\V1\Finance\PaymentController;
use App\Http\Controllers\Api\V1\Scolarite\EleveController;
use Illuminate\Support\Facades\Route;

Expand All @@ -25,6 +27,12 @@

Route::get('/scolarite/eleves', [EleveController::class, 'index'])
->name('api.v1.scolarite.eleves.index');
Route::get('/students', [EleveController::class, 'index'])
->name('api.v1.students.index');
Comment on lines +30 to +31
Route::get('/revenues', [RevenueController::class, 'index'])
->name('api.v1.revenues.index');
Route::post('/payments', [PaymentController::class, 'store'])
->name('api.v1.payments.store');

Route::post(
'/finance/perceptions/classe',
Expand Down
27 changes: 27 additions & 0 deletions tests/Feature/ApiEndpointsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

it('protects the students, revenues and payments endpoints', function () {
$this->getJson('/api/v1/students')->assertUnauthorized();
$this->getJson('/api/v1/revenues')->assertUnauthorized();
$this->postJson('/api/v1/payments')->assertUnauthorized();
});

it('rejects invalid students pagination', function () {
$this->withoutMiddleware()
->getJson('/api/v1/students?per_page=101')
->assertStatus(422);
});

it('rejects invalid revenue filters', function () {
$this->withoutMiddleware()
->getJson('/api/v1/revenues?period=week')
->assertStatus(422)
->assertJsonValidationErrors(['period']);
});

it('rejects incomplete payment data', function () {
$this->withoutMiddleware()
->postJson('/api/v1/payments', ['student_id' => 'not-a-student'])
->assertStatus(422)
->assertJsonValidationErrors(['student_id', 'amount', 'currency']);
});