Phase 6: security & ops hardening (T6.1-T6.5)
- T6.1: named rate limiters (api-read/api-write/api-auth/api-webhooks), applied per module route group with tighter limits on booking/payment writes and the KBZ webhook than read-only catalog/routing endpoints. - T6.2: install spatie/laravel-activitylog; LogsActivity on Booking/ Payment/Refund status transitions and catalog/pricing admin CRUD (EvCompany, Destination, DepartureTimeSlot, EvRoute, RoutePricing). New IdentityPlugin with a read-only AuditLogResource gated by view_audit_log. - T6.3: JSON error envelope for api/* in bootstrap/app.php (401/403/404/ 405/429/500 fallback), plus PaymentGatewayException (422 declined / 502 unavailable). - T6.4: feature tests proving the FastAPI agent token gets 403 on refund/cancel-not-owned and 405 (no write handler) on catalog/routing writes. - T6.5: install gboquizosanchez/filament-log-viewer with a custom Filament admin theme (required for its views' Tailwind classes to compile), LOG_CHANNEL/FILAMENT_LOG_VIEWER_DRIVER=daily, registered under Operations in the sidebar. 252 tests passing.
This commit is contained in:
@@ -5,7 +5,7 @@ use Modules\Payment\Http\Controllers\PaymentController;
|
||||
use Modules\Payment\Http\Controllers\PaymentWebhookController;
|
||||
use Modules\Payment\Http\Controllers\RefundController;
|
||||
|
||||
Route::prefix('api/v1')->middleware(['api', 'auth:sanctum', 'throttle:60,1'])->group(function () {
|
||||
Route::prefix('api/v1')->middleware(['api', 'auth:sanctum', 'throttle:api-write'])->group(function () {
|
||||
Route::post('/payments/{booking:booking_ref}/initiate', [PaymentController::class, 'initiate'])->name('payment.payments.initiate');
|
||||
Route::post('/bookings/{booking:booking_ref}/refund', [RefundController::class, 'refund'])->name('payment.bookings.refund');
|
||||
});
|
||||
@@ -13,6 +13,6 @@ Route::prefix('api/v1')->middleware(['api', 'auth:sanctum', 'throttle:60,1'])->g
|
||||
// No auth:sanctum — the gateway authenticates itself via its own signed
|
||||
// payload (verified inside each gateway's handleWebhook()), not a bearer
|
||||
// token (domain.md §6).
|
||||
Route::prefix('api/v1')->middleware(['api', 'throttle:60,1'])->group(function () {
|
||||
Route::prefix('api/v1')->middleware(['api', 'throttle:api-webhooks'])->group(function () {
|
||||
Route::post('/webhooks/{method}/{encryptBookingId?}', [PaymentWebhookController::class, 'handle'])->name('payment.webhooks.handle');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Payment\Exceptions;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Carries a gateway's own error code/message for an unexpected failure that
|
||||
* isn't already captured as a typed Failed result (e.g. a malformed
|
||||
* response the gateway strategy can't parse into a PaymentResultData/
|
||||
* RefundResultData). Never allowed to surface as a raw 500 (domain.md §6,
|
||||
* T6.3): a declined/rejected call from the gateway itself is a 422 (client
|
||||
* can retry/fix), an unreachable/misbehaving gateway is a 502.
|
||||
*/
|
||||
class PaymentGatewayException extends RuntimeException
|
||||
{
|
||||
private function __construct(
|
||||
string $message,
|
||||
private readonly int $statusCode,
|
||||
private readonly ?string $gatewayCode = null,
|
||||
) {
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* The gateway responded but rejected/declined the request — surfaced as
|
||||
* 422 since it's a business outcome the caller can act on.
|
||||
*/
|
||||
public static function declined(string $message, ?string $gatewayCode = null): self
|
||||
{
|
||||
return new self($message, 422, $gatewayCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* The gateway didn't respond usefully at all (unreachable, malformed
|
||||
* payload, unexpected HTTP status) — surfaced as 502, our fault for
|
||||
* depending on it, not the caller's.
|
||||
*/
|
||||
public static function unavailable(string $message, ?string $gatewayCode = null): self
|
||||
{
|
||||
return new self($message, 502, $gatewayCode);
|
||||
}
|
||||
|
||||
public function render(Request $request): ?JsonResponse
|
||||
{
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json([
|
||||
'message' => $this->getMessage(),
|
||||
'gateway_code' => $this->gatewayCode,
|
||||
], $this->statusCode);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ use Modules\Booking\Models\Booking;
|
||||
use Modules\Payment\Database\Factories\PaymentFactory;
|
||||
use Modules\Payment\Enums\PaymentMethod;
|
||||
use Modules\Payment\Enums\PaymentStatus;
|
||||
use Spatie\Activitylog\Models\Concerns\LogsActivity;
|
||||
use Spatie\Activitylog\Support\LogOptions;
|
||||
|
||||
/**
|
||||
* One attempt to pay for a Booking through a gateway — a Booking can have
|
||||
@@ -19,7 +21,19 @@ use Modules\Payment\Enums\PaymentStatus;
|
||||
class Payment extends Model
|
||||
{
|
||||
/** @use HasFactory<PaymentFactory> */
|
||||
use HasFactory;
|
||||
use HasFactory, LogsActivity;
|
||||
|
||||
/**
|
||||
* Audit trail on status transitions only (domain.md §6; T6.2).
|
||||
*/
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly(['status'])
|
||||
->logOnlyDirty()
|
||||
->dontLogEmptyChanges()
|
||||
->useLogName('payment');
|
||||
}
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
|
||||
@@ -8,6 +8,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Modules\Payment\Database\Factories\RefundFactory;
|
||||
use Modules\Payment\Enums\RefundStatus;
|
||||
use Spatie\Activitylog\Models\Concerns\LogsActivity;
|
||||
use Spatie\Activitylog\Support\LogOptions;
|
||||
|
||||
/**
|
||||
* A reversal against a specific successful Payment (not against the Booking
|
||||
@@ -17,7 +19,19 @@ use Modules\Payment\Enums\RefundStatus;
|
||||
class Refund extends Model
|
||||
{
|
||||
/** @use HasFactory<RefundFactory> */
|
||||
use HasFactory;
|
||||
use HasFactory, LogsActivity;
|
||||
|
||||
/**
|
||||
* Audit trail on status transitions only (domain.md §6; T6.2).
|
||||
*/
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly(['status'])
|
||||
->logOnlyDirty()
|
||||
->dontLogEmptyChanges()
|
||||
->useLogName('refund');
|
||||
}
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
|
||||
Reference in New Issue
Block a user