diff --git a/app/Console/Commands/RevokeApiToken.php b/app/Console/Commands/RevokeApiToken.php index 480b622..dbbb7dc 100644 --- a/app/Console/Commands/RevokeApiToken.php +++ b/app/Console/Commands/RevokeApiToken.php @@ -43,7 +43,7 @@ public function handle() return self::SUCCESS; } - $token->forceFill(['revoked_at' => now()])->save(); + $token->fill(['revoked_at' => now()])->save(); $this->info('API token revoked.'); diff --git a/app/Http/Controllers/Api/FormSubmissionOutboxController.php b/app/Http/Controllers/Api/FormSubmissionOutboxController.php index 3e2e119..7f781ed 100644 --- a/app/Http/Controllers/Api/FormSubmissionOutboxController.php +++ b/app/Http/Controllers/Api/FormSubmissionOutboxController.php @@ -8,9 +8,33 @@ class FormSubmissionOutboxController extends Controller { + public function index(Request $request) + { + $perPage = (int) $request->query('per_page', 50); + $perPage = max(1, min($perPage, 200)); + + return FormSubmissionOutbox::query() + ->select([ + 'id', + 'form_submission_id', + 'processed_at', + 'last_error', + 'last_error_at', + 'created_at', + ]) + ->orderBy('id') + ->paginate($perPage); + } + public function next() { + $cutoff = now()->subMinutes(5); + $outboxItem = FormSubmissionOutbox::whereNull('processed_at') + ->where(function ($query) use ($cutoff) { + $query->whereNull('last_error_at') + ->orWhere('last_error_at', '<=', $cutoff); + }) ->orderBy('created_at') ->first(); @@ -33,24 +57,31 @@ public function next() 'user_id' => $formSubmission->user_id, 'data' => json_decode($formSubmission->data, true), 'created_at' => optional($formSubmission->created_at)->toDateTimeString(), + 'last_error' => $outboxItem->last_error, + 'last_error_at' => optional($outboxItem->last_error_at)->toDateTimeString(), ]); } public function markProcessed(Request $request, FormSubmissionOutbox $formSubmissionOutbox) { $lastError = $request->input('last_error'); - $updates = ['last_error' => $lastError]; + $updates = []; if (! $lastError) { $updates['processed_at'] = now(); + } else { + $updates['processed_at'] = null; + $updates['last_error'] = $lastError; + $updates['last_error_at'] = now(); } - $formSubmissionOutbox->forceFill($updates)->save(); + $formSubmissionOutbox->fill($updates)->save(); return response()->json([ 'outbox_id' => $formSubmissionOutbox->id, 'processed_at' => optional($formSubmissionOutbox->processed_at)->toDateTimeString(), 'last_error' => $formSubmissionOutbox->last_error, + 'last_error_at' => optional($formSubmissionOutbox->last_error_at)->toDateTimeString(), ]); } @@ -71,6 +102,8 @@ public function show(FormSubmissionOutbox $formSubmissionOutbox) 'user_id' => $formSubmission->user_id, 'data' => json_decode($formSubmission->data, true), 'created_at' => optional($formSubmission->created_at)->toDateTimeString(), + 'last_error' => $formSubmissionOutbox->last_error, + 'last_error_at' => optional($formSubmissionOutbox->last_error_at)->toDateTimeString(), ]); } } diff --git a/app/Http/Controllers/Api/FormSubmissionsController.php b/app/Http/Controllers/Api/FormSubmissionsController.php index 3bd7848..272abcc 100644 --- a/app/Http/Controllers/Api/FormSubmissionsController.php +++ b/app/Http/Controllers/Api/FormSubmissionsController.php @@ -8,10 +8,28 @@ class FormSubmissionsController extends Controller { + public function index(Request $request) + { + $perPage = (int) $request->query('per_page', 50); + $perPage = max(1, min($perPage, 200)); + + return FormSubmission::query() + ->select([ + 'id', + 'form_id', + 'form_name', + 'user_id', + 'created_at', + ]) + ->orderBy('id') + ->paginate($perPage); + } + public function show(Request $request, FormSubmission $formSubmission) { return response()->json([ 'form_id' => $formSubmission->form_id, + 'form_submission_id' => $formSubmission->id, 'form_name' => $formSubmission->form_name, 'user_id' => $formSubmission->user_id, 'data' => json_decode($formSubmission->data, true), diff --git a/app/Http/Middleware/ApiTokenAuth.php b/app/Http/Middleware/ApiTokenAuth.php index f35493b..2b8df0b 100644 --- a/app/Http/Middleware/ApiTokenAuth.php +++ b/app/Http/Middleware/ApiTokenAuth.php @@ -30,7 +30,7 @@ public function handle(Request $request, Closure $next) return response()->json(['message' => 'Invalid API token.'], 401); } - $apiToken->forceFill(['last_used_at' => now()])->save(); + $apiToken->fill(['last_used_at' => now()])->save(); $request->attributes->set('api_token', $apiToken); return $next($request); diff --git a/app/Models/FormSubmissionOutbox.php b/app/Models/FormSubmissionOutbox.php index 09c5e14..676a9e9 100644 --- a/app/Models/FormSubmissionOutbox.php +++ b/app/Models/FormSubmissionOutbox.php @@ -15,6 +15,13 @@ class FormSubmissionOutbox extends Model 'form_submission_id', 'processed_at', 'last_error', + 'last_error_at', + ]; + + protected $casts = [ + 'created_at' => 'datetime', + 'processed_at' => 'datetime', + 'last_error_at' => 'datetime', ]; /** diff --git a/database/migrations/2026_01_27_000001_add_last_error_at_to_form_submission_outbox_table.php b/database/migrations/2026_01_27_000001_add_last_error_at_to_form_submission_outbox_table.php new file mode 100644 index 0000000..a2844bd --- /dev/null +++ b/database/migrations/2026_01_27_000001_add_last_error_at_to_form_submission_outbox_table.php @@ -0,0 +1,33 @@ +timestamp('last_error_at')->nullable()->after('last_error'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('form_submission_outbox', function (Blueprint $table) { + $table->dropColumn('last_error_at'); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index 753b2fc..3dfda36 100644 --- a/routes/api.php +++ b/routes/api.php @@ -26,7 +26,9 @@ Route::middleware('api_token')->get('/token', [ApiTokenController::class, 'show']); Route::middleware('api_token')->get('/users', [UsersController::class, 'index']); Route::middleware('api_token')->get('/users/{user}', [UsersController::class, 'show']); +Route::middleware('api_token')->get('/form_submissions', [FormSubmissionsController::class, 'index']); Route::middleware('api_token')->get('/form_submissions/{form_submission}', [FormSubmissionsController::class, 'show']); +Route::middleware('api_token')->get('/form_submissions/outbox', [FormSubmissionOutboxController::class, 'index']); Route::middleware('api_token')->get('/form_submissions/outbox/next', [FormSubmissionOutboxController::class, 'next']); Route::middleware('api_token')->post('/form_submissions/outbox/{form_submission_outbox}', [FormSubmissionOutboxController::class, 'markProcessed']); Route::middleware('api_token')->get('/form_submissions/outbox/{form_submission_outbox}', [FormSubmissionOutboxController::class, 'show']);