Skip to content

Feat: Add auto replenish bundles#1298

Merged
ogbanugot merged 2 commits into
developfrom
autorewnew-bundles
May 19, 2026
Merged

Feat: Add auto replenish bundles#1298
ogbanugot merged 2 commits into
developfrom
autorewnew-bundles

Conversation

@ogbanugot

@ogbanugot ogbanugot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Closes #<GH_issue_number>

Description of the Problem / Feature

Explanation of the solution

Instructions on making this work

UI changes for review

When major UI changes will happen with this PR, please include links to URLS to compare or screenshots demonstrating the difference and notify design

Summary by CodeRabbit

  • New Features

    • Added bundle auto-replenishment with per-user enable/disable, configurable threshold, and attempt tracking (timestamps & failure reasons).
    • New API endpoints to view and update auto-replenishment settings.
    • Payment methods can be saved and reused for seamless off-session billing during replenishment.
  • Billing & Webhooks

    • Checkout flow supports bundle off-session payment setup; webhooks handle auto-replenishment success/failure.
  • Tests

    • Added integration tests covering the auto-replenishment flow.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0bf8d0aa-64ea-404f-84bd-581e96651b43

📥 Commits

Reviewing files that changed from the base of the PR and between 63700b1 and 8de71e0.

📒 Files selected for processing (1)
  • desci-server/test/integration/bundleAutoReplenishment.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • desci-server/test/integration/bundleAutoReplenishment.test.ts

📝 Walkthrough

Walkthrough

Adds per-user bundle auto-replenishment: DB model and indexes, service APIs to read/update status and trigger off‑session Stripe payments when bundle usage hits a threshold, webhook handlers to fulfill or disable on outcomes, payment-method persistence for reuse, controller endpoints, and integration tests.

Changes

Bundle Auto-Replenishment

Layer / File(s) Summary
Database schema and persistence
desci-server/prisma/migrations/20260518120000_add_bundle_auto_replenishment/migration.sql, desci-server/prisma/schema.prisma
New BundleAutoReplenishment table with per-user unique userId, isEnabled, threshold, replenishmentInProgress, timestamps and failure reason; unique and composite indexes and FK to User were added.
Service interface and data types
desci-server/src/services/SubscriptionService.ts
Adds BundleAutoReplenishmentStatus type and related exports used by controllers and tests.
Status read and configuration write
desci-server/src/services/SubscriptionService.ts
getBundleAutoReplenishmentStatus reads persisted settings and latest bundle/payment-method summary; updateBundleAutoReplenishment upserts settings and enforces enabling prerequisites.
Payment method persistence for reuse
desci-server/src/services/SubscriptionService.ts
Helpers getReusablePaymentMethodId, saveBundlePaymentMethodForReuse, and upsertPaymentMethodRecord retrieve/upsert Stripe payment methods and set defaults for off‑session reuse.
Feature usage consumption trigger
desci-server/src/services/FeatureLimits/FeatureUsageService.ts
consumeUsage computes remainingUsesAfterConsume and dynamically calls SubscriptionService.triggerBundleAutoReplenishmentIfNeeded after successful usage creation.
Auto-replenishment trigger and payment creation
desci-server/src/services/SubscriptionService.ts
triggerBundleAutoReplenishmentIfNeeded claims in-progress state, validates customer/payment method/latest bundle price, then creates and confirms an off‑session PaymentIntent with bundle metadata.
Webhook handlers for payment outcomes
desci-server/src/controllers/stripe/webhook.ts, desci-server/src/services/SubscriptionService.ts
Dispatcher adds payment_intent.payment_failed; success handler handleBundleAutoReplenishmentSucceeded fulfills bundles and records success; failure handler handleBundleAutoReplenishmentFailed disables replenishment and records failure reason.
Controller endpoints and route registration
desci-server/src/controllers/stripe/subscription.ts, desci-server/src/routes/v1/stripe.ts
New authenticated endpoints GET /bundle-auto-replenishment and POST /bundle-auto-replenishment with Zod validation exposing and updating settings.
Integration into checkout and portal flows
desci-server/src/controllers/stripe/subscription.ts, desci-server/src/services/SubscriptionService.ts
Bundle checkout sets setup_future_usage: 'off_session'; createCustomerPortal uses direct Stripe customer lookup; checkout completion saves payment method for reuse; payment-method attach now upserts records.
Integration test suite
desci-server/test/integration/bundleAutoReplenishment.test.ts
Integration tests mock Stripe and validate enabling replenishment, PaymentIntent creation when threshold reached, and successful fulfillment with persisted state and granted units.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • desci-labs/nodes#1190: Overlaps changes to SubscriptionService.getStripeCustomerId and handlePaymentIntentSucceeded logging/flow used by auto-replenishment.
  • desci-labs/nodes#1167: Also modifies Stripe webhook/payment-intent success branching which intersects with the auto-replenishment path.
  • desci-labs/nodes#1233: Related at the bundle checkout fulfillment layer (handleBundleCheckoutFulfillment) used by auto-replenishment success flow.

Suggested reviewers

  • hubsmoke
  • shadrach-tayo
  • kadamidev

Poem

🐰 I nibble logs and chase the queue,
Saved cards whisper, "we'll charge for you."
When bundles dip and counters fall,
Off‑session payments answer the call.
Hooray — more chats, no stall!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat: Add auto replenish bundles' directly aligns with the primary change: introducing bundle auto-replenishment functionality across database schema, controllers, services, and routes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch autorewnew-bundles

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
desci-server/src/controllers/stripe/subscription.ts (1)

464-478: 💤 Low value

Catch-all 400 may mask unexpected server errors.

The catch block returns 400 for all non-ZodError exceptions. While business rule violations from the service (e.g., "no prior bundle purchase") should return 400, unexpected errors (database failures, etc.) should return 500 to aid debugging.

Consider checking for known business error types or messages to differentiate.

♻️ Suggested approach
   } catch (error: any) {
     if (error instanceof ZodError) {
       return res.status(400).json({
         error: 'Invalid request body',
         details: formatZodIssues(error),
       });
     }

     logger.error({ err: error, userId: req.user?.id }, 'Failed to update bundle auto-replenishment');

-    return res.status(400).json({
-      error: error instanceof Error ? error.message : 'Failed to update bundle auto-replenishment',
-    });
+    // Known business rule violations from service
+    const knownClientErrors = [
+      'No prior bundle purchase found',
+      'Active paid subscription exists',
+      'No Stripe customer found',
+      'No reusable payment method found',
+    ];
+    const isClientError = error instanceof Error && knownClientErrors.some((msg) => error.message.includes(msg));
+    
+    return res.status(isClientError ? 400 : 500).json({
+      error: error instanceof Error ? error.message : 'Failed to update bundle auto-replenishment',
+    });
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@desci-server/src/controllers/stripe/subscription.ts` around lines 464 - 478,
The catch-all currently returns 400 for every non-ZodError in the update bundle
auto-replenishment handler; change it to distinguish expected business errors
from unexpected server errors by checking for known business error
types/messages (e.g., a custom BusinessError/SubscriptionError thrown by the
service, or specific error codes/text) and return res.status(400) with that
error message for those cases, but for all other errors log them via
logger.error({ err: error, userId: req.user?.id }, 'Failed to update bundle
auto-replenishment') and return res.status(500). Ensure you still handle
ZodError with formatZodIssues and preserve the existing logging; reference the
existing ZodError check, formatZodIssues, logger.error and the final res.status
response when implementing the branching.
desci-server/test/integration/bundleAutoReplenishment.test.ts (1)

184-229: ⚡ Quick win

Add a failure-path integration test for auto-replenishment disablement.

The suite validates enable/trigger/success, but it misses the payment-failure branch. Add one test that exercises the failure handler and asserts isEnabled is turned off, replenishmentInProgress is cleared, and failure metadata is persisted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@desci-server/test/integration/bundleAutoReplenishment.test.ts` around lines
184 - 229, Add an integration test mirroring the success test that calls
SubscriptionService.handleBundleAutoReplenishmentFailed with a failed payment
intent payload (e.g., id 'pi_auto_fail_123', matching metadata type
'bundle_auto_replenishment' and userId), then assert the bundleAutoReplenishment
row for that user has isEnabled === false and replenishmentInProgress === false,
assert a failure timestamp/field was set (e.g., lastFailedAt is not null) on the
bundleAutoReplenishment record, and assert a related stripe record was created
(e.g., prisma.stripeCheckoutFulfillment found with stripeSessionId
'auto_replenishment:pi_auto_fail_123') to confirm failure metadata was
persisted.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@desci-server/src/controllers/stripe/subscription.ts`:
- Around line 464-478: The catch-all currently returns 400 for every
non-ZodError in the update bundle auto-replenishment handler; change it to
distinguish expected business errors from unexpected server errors by checking
for known business error types/messages (e.g., a custom
BusinessError/SubscriptionError thrown by the service, or specific error
codes/text) and return res.status(400) with that error message for those cases,
but for all other errors log them via logger.error({ err: error, userId:
req.user?.id }, 'Failed to update bundle auto-replenishment') and return
res.status(500). Ensure you still handle ZodError with formatZodIssues and
preserve the existing logging; reference the existing ZodError check,
formatZodIssues, logger.error and the final res.status response when
implementing the branching.

In `@desci-server/test/integration/bundleAutoReplenishment.test.ts`:
- Around line 184-229: Add an integration test mirroring the success test that
calls SubscriptionService.handleBundleAutoReplenishmentFailed with a failed
payment intent payload (e.g., id 'pi_auto_fail_123', matching metadata type
'bundle_auto_replenishment' and userId), then assert the bundleAutoReplenishment
row for that user has isEnabled === false and replenishmentInProgress === false,
assert a failure timestamp/field was set (e.g., lastFailedAt is not null) on the
bundleAutoReplenishment record, and assert a related stripe record was created
(e.g., prisma.stripeCheckoutFulfillment found with stripeSessionId
'auto_replenishment:pi_auto_fail_123') to confirm failure metadata was
persisted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3f41010f-e62f-4d6b-8261-327b2a7c460b

📥 Commits

Reviewing files that changed from the base of the PR and between 93d7ba5 and 63700b1.

📒 Files selected for processing (8)
  • desci-server/prisma/migrations/20260518120000_add_bundle_auto_replenishment/migration.sql
  • desci-server/prisma/schema.prisma
  • desci-server/src/controllers/stripe/subscription.ts
  • desci-server/src/controllers/stripe/webhook.ts
  • desci-server/src/routes/v1/stripe.ts
  • desci-server/src/services/FeatureLimits/FeatureUsageService.ts
  • desci-server/src/services/SubscriptionService.ts
  • desci-server/test/integration/bundleAutoReplenishment.test.ts

@ogbanugot ogbanugot merged commit 0736481 into develop May 19, 2026
3 checks passed
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.

1 participant