Skip to content

Conversation

sundayonah
Copy link
Collaborator

@sundayonah sundayonah commented Oct 8, 2025

Description

This PR implements automatic Redis queue management for provider profiles when their availability status changes. The feature ensures that providers are immediately removed from all relevant Redis queues when their IsAvailable status is set to false, preventing the system from routing orders to unavailable providers.

Key Changes:

  • Added removeProviderFromRedisQueues() function to handle Redis queue removal
  • Integrated Redis queue removal logic into UpdateProviderProfile function
  • Implemented comprehensive test coverage with 4 test scenarios
  • Added proper error handling that doesn't block profile updates

Technical Implementation:

  • Uses the exact Redis algorithm provided in requirements (LIndex → LSet → LRem)
  • Processes only relevant currency-bucket combinations for efficiency
  • Maintains atomic operations that don't block the main profile update
  • Includes comprehensive error logging for debugging

Impact:

  • Prevents orders from being routed to unavailable providers
  • Improves system reliability and order fulfillment accuracy
  • Maintains backward compatibility with existing functionality
  • No breaking changes to existing APIs

References

This PR addresses the user story: #435

Acceptance Criteria Met:

  • ✅ Providers are removed from Redis queues when IsAvailable is set to false
  • ✅ Redis operation failures are logged but don't block profile updates
  • ✅ Uses the exact Redis algorithm specified in requirements
  • ✅ Handles edge cases gracefully (provider not found, invalid data format)

Testing

Test Coverage Added:

  • Happy Path Test: Provider successfully removed from Redis queues when IsAvailable set to false
  • Negative Test: Provider NOT removed when IsAvailable set to true
  • Error Resilience Test: Profile update succeeds even when Redis operations fail
  • Edge Case Test: Graceful handling when provider not found in Redis queues

Manual Testing Steps:

  1. Set up a provider with provision buckets and currencies
  2. Add provider data to Redis queues using the correct format
  3. Update provider profile with IsAvailable: false
  4. Verify provider is removed from all relevant Redis queues
  5. Verify other providers remain unaffected

Checklist

  • I have added documentation and tests for new/changed functionality in this PR
  • All active GitHub checks for tests, formatting, and security are passing
  • The correct base branch is being used, if not main

Files Modified:

  • controllers/accounts/profile.go - Added Redis queue removal logic
  • controllers/accounts/profile_test.go - Added comprehensive test coverage

Key Features:

  • 🔒 Atomic Operations: Redis operations don't block profile updates
  • 🛡️ Error Resilience: Redis failures are logged but don't prevent updates
  • 🔄 Reusable Code: Modular function can be used elsewhere
  • 📝 Comprehensive Logging: All operations are properly logged
  • Full Test Coverage: 4 test scenarios covering all edge cases

By submitting a PR, I agree to Paycrest's Contributor Code of Conduct and Contribution Guide.

Summary by CodeRabbit

  • New Features

    • Providers set to unavailable are removed from active matching queues immediately after profile updates; cleanup runs as a background step and won’t block the save.
  • Tests

    • Expanded automated coverage with Redis-backed scenarios validating queue removal, preservation when available, graceful handling of Redis errors, and behavior when provider entries are absent.

- Add functionality to remove providers from Redis queues when their availability is set to false.
- Introduce a new method, removeProviderFromRedisQueues, to handle the removal process.
- Enhance tests to verify correct behavior when updating provider availability, including scenarios for both true and false settings.
- Ensure graceful handling of Redis errors during profile updates.

This update improves the management of provider data in Redis, ensuring that inactive providers are properly removed from relevant queues.
Copy link
Contributor

coderabbitai bot commented Oct 8, 2025

Walkthrough

Adds ProfileController.removeProviderFromRedisQueues and calls it from UpdateProviderProfile after a successful DB commit when IsAvailable is set to false; introduces Redis-backed tests (miniredis) covering removal, no-op, error-tolerance, and absent-provider scenarios.

Changes

Cohort / File(s) Summary of changes
Controller logic: provider de-queueing
controllers/accounts/profile.go
Added removeProviderFromRedisQueues(ctx, providerID, currencyCode) which fetches provider currency buckets and provision buckets, constructs Redis bucket keys, and removes provider entries using an LSet/LRem pattern; invoked post-commit in UpdateProviderProfile when availability flips to false; errors are logged and do not abort the flow.
Tests: Redis-backed profile updates
controllers/accounts/profile_test.go
Added miniredis-based tests and Redis client wiring; set up/populated Redis queues and asserted provider removal when IsAvailable=false, preservation when true, graceful handling of Redis errors, and no-op when provider absent.

Sequence Diagram(s)

sequenceDiagram
  participant C as Client
  participant PC as ProfileController
  participant DB as Database
  participant R as Redis

  C->>PC: PATCH /settings/provider (IsAvailable toggled)
  PC->>DB: Update provider profile (tx)
  DB-->>PC: Commit success

  alt IsAvailable set to false
    note over PC,R #DFF2DF: Post-commit Redis cleanup (new)
    PC->>R: removeProviderFromRedisQueues(providerID, currency)
    alt Removal succeeds
      R-->>PC: OK
      PC-->>C: 200 OK
    else Removal error
      PC->>PC: Log error (non-blocking)
      PC-->>C: 200 OK
    end
  else IsAvailable true or unchanged
    PC-->>C: 200 OK
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Poem

A whisker twitch, a queue undone,
I hop through Redis, one by one.
Commit first, then tidy the lanes,
Remove the IDs, leave no remains.
Tests hum softly — carrots for gains. 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The provided PR title concisely and accurately summarizes the main feature being introduced by focusing on removing providers from Redis queues when their availability is set to false, matching the core change implemented in the code.
Description Check ✅ Passed The PR description closely follows the repository’s template by including a comprehensive Description with background and implementation details, a References section linking the related user story, and a Testing section outlining both automated and manual test steps along with a checklist confirming test coverage and passing checks.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/remove-provider-from-queue-when-unavailable

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 793c80d and cdada4b.

📒 Files selected for processing (2)
  • controllers/accounts/profile.go (2 hunks)
  • controllers/accounts/profile_test.go (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
controllers/accounts/profile_test.go (8)
storage/redis.go (1)
  • RedisClient (13-13)
ent/providerprofile/where.go (2)
  • IDEQ (19-21)
  • ID (14-16)
ent/provisionbucket/where.go (4)
  • IDEQ (20-22)
  • ID (15-17)
  • MinAmount (60-62)
  • MaxAmount (65-67)
ent/providercurrencies_query.go (1)
  • ProviderCurrenciesQuery (22-34)
types/types.go (1)
  • ProviderProfilePayload (265-272)
ent/providercurrencies/where.go (1)
  • IsAvailable (76-78)
utils/token/token.go (1)
  • GenerateAccessJWT (21-29)
utils/test/test.go (1)
  • PerformRequest (14-25)
controllers/accounts/profile.go (8)
ent/providerprofile/where.go (2)
  • ID (14-16)
  • IDEQ (19-21)
utils/logger/logger.go (2)
  • WithFields (76-109)
  • Fields (73-73)
storage/database.go (1)
  • Client (21-21)
ent/providercurrencies_query.go (1)
  • ProviderCurrenciesQuery (22-34)
ent/provisionbucket_query.go (1)
  • ProvisionBucketQuery (24-37)
ent/providercurrencies.go (2)
  • ProviderCurrencies (20-40)
  • ProviderCurrencies (76-97)
ent/schema/providercurrencies.go (4)
  • ProviderCurrencies (15-17)
  • ProviderCurrencies (20-36)
  • ProviderCurrencies (39-50)
  • ProviderCurrencies (53-58)
storage/redis.go (1)
  • RedisClient (13-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-test

- Add currency-specific filtering to prevent removing provider from all currencies
- Fix Redis client management in tests to prevent connection pollution
- Update removeProviderFromRedisQueues to accept currencyCode parameter
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
controllers/accounts/profile.go (1)

872-931: Consider simplifying the nested loop structure.

The outer loop over ProviderCurrencies appears redundant since it only validates that the currency exists for the provider, but the actual filtering happens in the inner loop over ProvisionBuckets. You could streamline this by iterating directly over provider.Edges.ProvisionBuckets and filtering by currency code, eliminating the outer loop entirely.

Apply this diff to simplify the logic:

-	// Remove provider from Redis queues for the specific currency only
-	for _, pc := range provider.Edges.ProviderCurrencies {
-		if pc.Edges.Currency == nil || pc.Edges.Currency.Code != currencyCode {
-			continue
-		}
-		for _, bucket := range provider.Edges.ProvisionBuckets {
-			if bucket.Edges.Currency != nil && bucket.Edges.Currency.Code == currencyCode {
+	// Remove provider from Redis queues for the specific currency only
+	for _, bucket := range provider.Edges.ProvisionBuckets {
+		if bucket.Edges.Currency != nil && bucket.Edges.Currency.Code == currencyCode {
+			redisKey := fmt.Sprintf("bucket_%s_%s_%s", bucket.Edges.Currency.Code, bucket.MinAmount, bucket.MaxAmount)
+
+			// Remove provider from Redis queue using the provided algorithm
+			for index := -1; ; index-- {
+				providerData, err := storage.RedisClient.LIndex(ctx, redisKey, int64(index)).Result()
+				if err != nil {
+					break
+				}
+
+				parts := strings.Split(providerData, ":")
+				if len(parts) != 5 {
+					logger.Errorf("invalid provider data format: %s", providerData)
+					continue
+				}
+
+				if parts[0] == providerID {
+					placeholder := "DELETED_PROVIDER"
+					_, err := storage.RedisClient.LSet(ctx, redisKey, int64(index), placeholder).Result()
+					if err != nil {
+						logger.Errorf("failed to set placeholder at index %d: %v", index, err)
+					}
+
+					_, err = storage.RedisClient.LRem(ctx, redisKey, 0, placeholder).Result()
+					if err != nil {
+						logger.Errorf("failed to remove placeholder from circular queue: %v", err)
+					}
+
+					break
+				}
+			}
+		}
+	}

Alternatively, if the intent is to fail fast when the provider doesn't have the specified currency configured, add an early validation check before iterating buckets:

// Verify provider has the specified currency configured
hasCurrency := false
for _, pc := range provider.Edges.ProviderCurrencies {
	if pc.Edges.Currency != nil && pc.Edges.Currency.Code == currencyCode {
		hasCurrency = true
		break
	}
}
if !hasCurrency {
	return fmt.Errorf("provider does not have currency %s configured", currencyCode)
}

// Process buckets for the specified currency
for _, bucket := range provider.Edges.ProvisionBuckets {
	// ... rest of logic
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cdada4b and c8a6f47.

📒 Files selected for processing (2)
  • controllers/accounts/profile.go (2 hunks)
  • controllers/accounts/profile_test.go (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
controllers/accounts/profile_test.go (5)
storage/redis.go (1)
  • RedisClient (13-13)
storage/database.go (1)
  • Client (21-21)
types/types.go (1)
  • ProviderProfilePayload (265-272)
utils/token/token.go (1)
  • GenerateAccessJWT (21-29)
utils/test/test.go (1)
  • PerformRequest (14-25)
controllers/accounts/profile.go (9)
ent/providerprofile/where.go (2)
  • ID (14-16)
  • IDEQ (19-21)
utils/logger/logger.go (2)
  • WithFields (76-109)
  • Fields (73-73)
storage/database.go (1)
  • Client (21-21)
ent/providerprofile.go (2)
  • ProviderProfile (20-43)
  • ProviderProfile (136-153)
ent/provisionbucket_query.go (1)
  • ProvisionBucketQuery (24-37)
ent/providercurrencies.go (2)
  • ProviderCurrencies (20-40)
  • ProviderCurrencies (76-97)
ent/schema/providercurrencies.go (4)
  • ProviderCurrencies (15-17)
  • ProviderCurrencies (20-36)
  • ProviderCurrencies (39-50)
  • ProviderCurrencies (53-58)
ent/provisionbucket/where.go (2)
  • MinAmount (60-62)
  • MaxAmount (65-67)
storage/redis.go (1)
  • RedisClient (13-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-test
🔇 Additional comments (4)
controllers/accounts/profile.go (2)

624-634: LGTM! Redis removal is correctly integrated.

The call to removeProviderFromRedisQueues is appropriately placed after the transaction commit and only executes when availability is being set to false. Redis errors are logged but don't prevent the profile update from succeeding, which aligns with the requirement that Redis failures should be non-blocking.


899-925: Verify behavior for duplicate provider entries.

The current implementation breaks after removing the first occurrence of the provider from each bucket's queue. If duplicate entries exist in the same Redis queue (perhaps from a previous bug or race condition), only the first occurrence will be removed.

Confirm whether this single-removal behavior is intentional or if all occurrences should be removed. If all occurrences should be removed, modify the logic to continue searching instead of breaking:

 				if parts[0] == providerID {
 					placeholder := "DELETED_PROVIDER"
 					_, err := storage.RedisClient.LSet(ctx, redisKey, int64(index), placeholder).Result()
 					if err != nil {
 						logger.Errorf("failed to set placeholder at index %d: %v", index, err)
 					}
 
 					_, err = storage.RedisClient.LRem(ctx, redisKey, 0, placeholder).Result()
 					if err != nil {
 						logger.Errorf("failed to remove placeholder from circular queue: %v", err)
 					}
-
-					break
 				}
 			}

Note: Removing the break will cause the loop to continue iterating backwards through the entire list, removing all occurrences. Consider performance implications if queues are large.

controllers/accounts/profile_test.go (2)

860-1187: Excellent test coverage!

The test suite comprehensively covers the Redis queue removal feature with four well-structured scenarios:

  1. ✅ Happy path: Provider removed when IsAvailable=false
  2. ✅ Negative case: Provider preserved when IsAvailable=true
  3. ✅ Error resilience: Profile update succeeds despite Redis failures
  4. ✅ Edge case: Graceful handling when provider not in queue

The Redis client restoration pattern (prevClient := db.RedisClient + defer) correctly addresses the past review concern and prevents connection leakage between subtests.


919-920: Tests correctly mirror production Redis enqueue format
Production code uses fmt.Sprintf("%s:%s:%s:%s:%s", provider.ID, token.Symbol, rate, minAmount, maxAmount), matching the test’s providerID:USDC:550:minAmount:maxAmount construction.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
controllers/accounts/profile_test.go (1)

860-1187: Consider extracting common setup logic into helper functions.

The four subtests share significant setup code (miniredis initialization, Redis client creation, currency fetching, bucket creation). While test isolation and clarity often justify some duplication, extracting the common setup into helper functions could improve maintainability without sacrificing readability.

For example:

  • setupTestRedis(t) (*miniredis.Miniredis, *redis.Client, func()) for Redis setup and cleanup
  • getProviderCurrency(ctx, providerID) (*ent.FiatCurrency, error) for currency fetching
  • createTestBucket(ctx, currency, min, max) (*ent.ProvisionBucket, error) for bucket creation

This refactoring can be deferred if test clarity is prioritized.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8a6f47 and 8880b91.

📒 Files selected for processing (1)
  • controllers/accounts/profile_test.go (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
controllers/accounts/profile_test.go (9)
storage/redis.go (1)
  • RedisClient (13-13)
storage/database.go (1)
  • Client (21-21)
ent/providerprofile/where.go (2)
  • IDEQ (19-21)
  • ID (14-16)
ent/providercurrencies_query.go (1)
  • ProviderCurrenciesQuery (22-34)
ent/providercurrencies.go (2)
  • ProviderCurrencies (20-40)
  • ProviderCurrencies (76-97)
ent/provisionbucket.go (2)
  • ProvisionBucket (19-34)
  • ProvisionBucket (79-96)
types/types.go (1)
  • ProviderProfilePayload (265-272)
utils/token/token.go (1)
  • GenerateAccessJWT (21-29)
utils/test/test.go (1)
  • PerformRequest (14-25)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-and-test
🔇 Additional comments (4)
controllers/accounts/profile_test.go (4)

6-6: LGTM!

The new imports (fmt, miniredis, redis) are appropriate for the Redis queue removal tests.

Also applies to: 19-19, 33-33


861-964: LGTM!

This test correctly verifies that providers are removed from Redis queues when IsAvailable is set to false. The test setup properly:

  • Uses the actual currency code from the database in both Redis keys and provider data
  • Adds providers to multiple bucket queues
  • Verifies removal from all relevant queues while preserving other providers

1047-1112: LGTM!

This test correctly verifies that Redis errors are handled gracefully without blocking the profile update. Closing the Redis client before the update simulates a realistic failure scenario, and the assertion that the profile update still returns HTTP 200 confirms the non-blocking requirement from the PR acceptance criteria.


916-920: All Redis key and data formats match production code The bucket_%s_%s_%s key and providerID:currencyCode:rate:minAmount:maxAmount data prefix used in tests align with removeProviderFromRedisQueues in profile.go (which only inspects parts[0]).

Comment on lines +1014 to +1015
providerData := testCtx.providerProfile.ID + ":USDC:550:" + bucket.MinAmount.String() + ":" + bucket.MaxAmount.String()
redisClient.RPush(ctx, redisKey, providerData, "other_provider:USDC:550:"+bucket.MinAmount.String()+":"+bucket.MaxAmount.String())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix currency code inconsistency in provider data.

The provider data hardcodes "USDC" while the Redis key (line 1013) uses currency.Code (which is "KES" from the database). This creates a mismatch between the key and the data format that doesn't reflect production usage. Test 1 correctly uses currency.Code in the provider data (line 919), and this test should follow the same pattern.

Apply this diff to use the actual currency code:

-			providerData := testCtx.providerProfile.ID + ":USDC:550:" + bucket.MinAmount.String() + ":" + bucket.MaxAmount.String()
-			redisClient.RPush(ctx, redisKey, providerData, "other_provider:USDC:550:"+bucket.MinAmount.String()+":"+bucket.MaxAmount.String())
+			providerData := testCtx.providerProfile.ID + ":" + currency.Code + ":550:" + bucket.MinAmount.String() + ":" + bucket.MaxAmount.String()
+			redisClient.RPush(ctx, redisKey, providerData, "other_provider:"+currency.Code+":550:"+bucket.MinAmount.String()+":"+bucket.MaxAmount.String())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
providerData := testCtx.providerProfile.ID + ":USDC:550:" + bucket.MinAmount.String() + ":" + bucket.MaxAmount.String()
redisClient.RPush(ctx, redisKey, providerData, "other_provider:USDC:550:"+bucket.MinAmount.String()+":"+bucket.MaxAmount.String())
providerData := testCtx.providerProfile.ID + ":" + currency.Code + ":550:" + bucket.MinAmount.String() + ":" + bucket.MaxAmount.String()
redisClient.RPush(ctx, redisKey, providerData, "other_provider:"+currency.Code+":550:"+bucket.MinAmount.String()+":"+bucket.MaxAmount.String())
🤖 Prompt for AI Agents
In controllers/accounts/profile_test.go around lines 1014 to 1015, the provider
data string hardcodes "USDC" while the Redis key uses currency.Code (e.g.,
"KES"), causing inconsistency; update the test to use currency.Code instead of
the literal "USDC" for both providerData and the second argument passed to
redisClient.RPush so the currency in the pushed provider data matches the key
and mirrors the pattern used earlier in the file.


// Create Redis queue with different provider (provider not found scenario)
redisKey := fmt.Sprintf("bucket_%s_%s_%s", currency.Code, bucket.MinAmount, bucket.MaxAmount)
redisClient.RPush(ctx, redisKey, "other_provider:USDC:550:"+bucket.MinAmount.String()+":"+bucket.MaxAmount.String())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix currency code inconsistency in provider data.

Similar to the issue in test 2, this line hardcodes "USDC" while the Redis key (line 1161) uses currency.Code (which is "KES"). Even though this test is verifying the "provider not found" scenario, the data format should remain consistent with production usage.

Apply this diff:

-			redisClient.RPush(ctx, redisKey, "other_provider:USDC:550:"+bucket.MinAmount.String()+":"+bucket.MaxAmount.String())
+			redisClient.RPush(ctx, redisKey, "other_provider:"+currency.Code+":550:"+bucket.MinAmount.String()+":"+bucket.MaxAmount.String())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
redisClient.RPush(ctx, redisKey, "other_provider:USDC:550:"+bucket.MinAmount.String()+":"+bucket.MaxAmount.String())
redisClient.RPush(ctx, redisKey, "other_provider:"+currency.Code+":550:"+bucket.MinAmount.String()+":"+bucket.MaxAmount.String())
🤖 Prompt for AI Agents
In controllers/accounts/profile_test.go around line 1162, the test pushes
provider data with a hardcoded "USDC" while the Redis key uses currency.Code
("KES"); update the RPush payload to use the same currency.Code variable (e.g.,
build the string with currency.Code instead of the literal "USDC") so the test
data format matches production usage and remains consistent with the Redis key.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants