fix(sync): make transaction creation idempotent (#38)

## Summary
- When a transaction ID is provided during sync, check if it already
exists before creating
- If it exists, return the existing transaction with 200 status instead
of failing with duplicate key error
- Prevents duplicate transactions when sync retries occur due to network
issues

## Test plan
- [x] Attempt to create a transaction with a specific ID
- [x] Attempt to create another transaction with the same ID
- [x] Verify the second request returns the existing transaction instead
of an error
This commit is contained in:
Víctor Falcón 2025-12-27 13:23:05 +00:00 committed by Víctor Falcón
parent 6f42b91585
commit 3cbe0a7879
7 changed files with 21 additions and 6 deletions

View File

@ -36,6 +36,21 @@ class TransactionSyncController extends Controller
$labelIds = $data['label_ids'] ?? [];
unset($data['label_ids']);
// If ID is provided, check if transaction already exists (idempotent create)
if (isset($data['id'])) {
$existing = Transaction::query()
->where('id', $data['id'])
->where('user_id', $request->user()->id)
->first();
if ($existing) {
// Transaction already exists, return it as success (idempotent)
return response()->json([
'data' => $existing->load('labels:id,name,color'),
], 200);
}
}
// Create transaction with provided ID if available
$transaction = new Transaction([
...$data,

View File

@ -58,6 +58,6 @@ class FeedbackEmail extends Mailable implements ShouldQueue
*/
public function middleware(): array
{
return [new RateLimited('emails', releaseAfter: 1)];
return [(new RateLimited('emails'))->releaseAfter(1)];
}
}

View File

@ -58,6 +58,6 @@ class ImportHelpEmail extends Mailable implements ShouldQueue
*/
public function middleware(): array
{
return [new RateLimited('emails', releaseAfter: 1)];
return [(new RateLimited('emails'))->releaseAfter(1)];
}
}

View File

@ -58,6 +58,6 @@ class OnboardingReminderEmail extends Mailable implements ShouldQueue
*/
public function middleware(): array
{
return [new RateLimited('emails', releaseAfter: 1)];
return [(new RateLimited('emails'))->releaseAfter(1)];
}
}

View File

@ -59,6 +59,6 @@ class PromoCodeEmail extends Mailable implements ShouldQueue
*/
public function middleware(): array
{
return [new RateLimited('emails', releaseAfter: 1)];
return [(new RateLimited('emails'))->releaseAfter(1)];
}
}

View File

@ -58,6 +58,6 @@ class WelcomeEmail extends Mailable implements ShouldQueue
*/
public function middleware(): array
{
return [new RateLimited('emails', releaseAfter: 1)];
return [(new RateLimited('emails'))->releaseAfter(1)];
}
}

View File

@ -74,6 +74,6 @@ class UserLeadInvitation extends Mailable implements ShouldQueue
*/
public function middleware(): array
{
return [new RateLimited('emails', releaseAfter: 1)];
return [(new RateLimited('emails'))->releaseAfter(1)];
}
}