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:
@@ -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