diff --git a/app/Jobs/SyncBankingConnectionJob.php b/app/Jobs/SyncBankingConnectionJob.php index 34e1be98..f06f1bac 100644 --- a/app/Jobs/SyncBankingConnectionJob.php +++ b/app/Jobs/SyncBankingConnectionJob.php @@ -188,8 +188,23 @@ class SyncBankingConnectionJob implements ShouldBeUnique, ShouldQueue $connection->update([ 'status' => BankingConnectionStatus::Error, - 'error_message' => $e ? $this->friendlyErrorMessage($e) : __('An unexpected error occurred during sync. Please try again later.'), + // Every message written here describes an out-of-band death, so the + // generic "an unexpected error occurred, please try again" was pushing + // our own infrastructure onto the user. The next scheduled cycle picks + // the connection back up on its own. + 'error_message' => __('The sync did not finish. We will try again later.'), ]); + + // handle() owns every other logSyncAttempt call, and it is precisely what + // an out-of-band death skips - leaving the connection's own history with no + // trace of the most common way this job dies. + $this->logSyncAttempt( + $connection, + BankingSyncLogStatus::Failed, + startTime: null, + error: $e, + metadata: ['reason' => 'job_died_outside_handle'], + ); } /** @@ -302,22 +317,27 @@ class SyncBankingConnectionJob implements ShouldBeUnique, ShouldQueue }); } + /** + * @param float|null $startTime Null when the caller never got to start a + * timer, so the duration is genuinely unknown + * rather than zero. + */ private function logSyncAttempt( BankingConnection $connection, BankingSyncLogStatus $status, - float $startTime, + ?float $startTime, ?\Throwable $error = null, ?array $metadata = null, ): void { - $durationMs = (int) round((microtime(true) - $startTime) * 1000); - BankingSyncLog::create([ 'banking_connection_id' => $connection->id, 'status' => $status, 'attempt' => $this->attempts(), 'error_message' => $error?->getMessage(), 'error_class' => $error ? get_class($error) : null, - 'duration_ms' => $durationMs, + 'duration_ms' => $startTime === null + ? null + : (int) round((microtime(true) - $startTime) * 1000), 'metadata' => $metadata, 'created_at' => now(), ]); diff --git a/lang/es.json b/lang/es.json index 65ba4436..ee3c8729 100644 --- a/lang/es.json +++ b/lang/es.json @@ -332,6 +332,7 @@ "Amount is required": "Se requiere un valor", "An unexpected error occurred during sync.": "Se produjo un error inesperado durante la sincronización.", "An unexpected error occurred during sync. Please try again later.": "Ocurrió un error inesperado durante la sincronización. Inténtalo de nuevo más tarde.", + "The sync did not finish. We will try again later.": "La sincronización no se completó. Volveremos a intentarlo más tarde.", "Annual": "Anual", "Annual Interest Rate": "Tasa de Interés Anual", "Annual Interest Rate (%)": "Tasa de Interés Anual (%)", diff --git a/tests/Feature/OpenBanking/SyncBankingConnectionJobTest.php b/tests/Feature/OpenBanking/SyncBankingConnectionJobTest.php index 466178c5..3b4aee2b 100644 --- a/tests/Feature/OpenBanking/SyncBankingConnectionJobTest.php +++ b/tests/Feature/OpenBanking/SyncBankingConnectionJobTest.php @@ -1642,7 +1642,7 @@ test('failed sync job marks active connection as error so onboarding can continu $connection->refresh(); expect($connection->status)->toBe(BankingConnectionStatus::Error); expect($connection->last_synced_at)->toBeNull(); - expect($connection->error_message)->toBe('An unexpected error occurred during sync. Please try again later.'); + expect($connection->error_message)->toBe('The sync did not finish. We will try again later.'); // A job killed from the outside says nothing about the connection, so it no // longer spends a scheduled retry. expect($connection->consecutive_sync_failures)->toBe(0); diff --git a/tests/Feature/OpenBanking/SyncRetryAndLoggingTest.php b/tests/Feature/OpenBanking/SyncRetryAndLoggingTest.php index e84e3bb5..55914c5c 100644 --- a/tests/Feature/OpenBanking/SyncRetryAndLoggingTest.php +++ b/tests/Feature/OpenBanking/SyncRetryAndLoggingTest.php @@ -945,3 +945,19 @@ test('a second out-of-band death on an already errored connection changes nothin expect($connection->consecutive_sync_failures)->toBe(2); expect($connection->error_message)->toBe('Earlier failure kept.'); }); + +test('an out-of-band job death is recorded in the connection history', function () { + $user = User::factory()->onboarded()->create(); + $connection = BankingConnection::factory()->create(['user_id' => $user->id]); + + (new SyncBankingConnectionJob($connection))->failed( + new TimeoutExceededException('App\Jobs\SyncBankingConnectionJob has timed out.') + ); + + $log = BankingSyncLog::where('banking_connection_id', $connection->id)->sole(); + expect($log->status)->toBe(BankingSyncLogStatus::Failed); + expect($log->error_class)->toBe(TimeoutExceededException::class); + expect($log->metadata['reason'])->toBe('job_died_outside_handle'); + // Unknown rather than zero: nobody timed this attempt. + expect($log->duration_ms)->toBeNull(); +});