Skip to content

fix(billing): prevent 0 amount payments#1904

Merged
baktun14 merged 6 commits intomainfrom
fix/zero-payment-fix
Sep 16, 2025
Merged

fix(billing): prevent 0 amount payments#1904
baktun14 merged 6 commits intomainfrom
fix/zero-payment-fix

Conversation

@baktun14
Copy link
Contributor

@baktun14 baktun14 commented Sep 15, 2025

Summary by CodeRabbit

  • New Features

    • Enforces a $20 minimum payment before discounts in API and web checkout.
    • Centralized minimum-amount constant for consistent validation.
  • Bug Fixes

    • Rejects non-positive amounts with clear messaging.
    • Ensures final amount after discounts is at least $1.
    • API validation now enforces the $20 minimum and blocks negative/zero amounts with user-friendly errors.
  • Tests

    • Updated test fixtures to use nested card details; no production behavior changes.

@baktun14 baktun14 requested a review from a team as a code owner September 15, 2025 19:10
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 15, 2025

Walkthrough

Updated test fixtures to use nested card: { brand, last4 } for card payment methods; added client-side minimum payment constant and validation; tightened server request schema to require amount >= 20; only minor formatting edits in Stripe service implementation. No public function signatures changed.

Changes

Cohort / File(s) Summary
Stripe service tests
apps/api/src/billing/services/stripe/stripe.service.spec.ts
Replaced flat cardBrand/cardLast4 test fields with nested card: { brand, last4 } in CSV and streaming test fixtures; updated mocks accordingly.
Web payment validation
apps/deploy-web/src/pages/payment.tsx
Added MINIMUM_PAYMENT_AMOUNT = 20 and updated validateAmount to reject non-positive amounts, enforce the $20 minimum when no discounts, and keep post-discount minimum-of-$1 check.
API schema validation
apps/api/src/billing/http-schemas/stripe.schema.ts
Tightened ConfirmPaymentRequestSchema so amount is validated as z.number().gte(20, "Amount must be greater or equal to $20") (enforces amount >= 20).
Stripe service implementation (formatting)
apps/api/src/billing/services/stripe/stripe.service.ts
Only formatting changes (blank-line adjustments); no functional or signature changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant W as Web Payment Page
  participant A as API (ConfirmPayment)
  participant S as Stripe

  U->>W: Enter amount + payment method
  W->>W: validateAmount(value, discounts, MINIMUM_PAYMENT_AMOUNT)
  alt value <= 0
    W-->>U: "Amount must be greater than $0"
  else no discounts AND value < 20
    W-->>U: "Minimum amount is $20"
  else Valid
    W->>A: POST /confirm-payment { amount, ... }
    A->>A: Validate request against schema (amount >= 20)
    alt amount < 20 (schema)
      A-->>W: 400 Validation Error ("Amount must be greater or equal to $20")
    else Proceed
      A->>S: Create/confirm PaymentIntent
      S-->>A: Client secret / success
      A-->>W: Return client secret
      W-->>U: Continue payment flow
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • ygrishajev

Poem

A rabbit hops by with a ledger so spry,
"Twenty carrots, please" — no smaller will fly.
Cards nest like burrows: brand and last-four snug,
Tests hum, schemas guard, validations give a hug.
Hop — payments tidy, and the console’s snug. 🐰💳

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title is concise and correctly captures the primary intent—preventing zero-value payments—and matches the changes shown: client-side validation now rejects non‑positive amounts and enforces a minimum, the server-side Zod schema requires a minimum amount, and tests were updated accordingly.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/zero-payment-fix

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21c6dd6 and d26f000.

⛔ Files ignored due to path filters (1)
  • apps/api/test/functional/__snapshots__/docs.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (2)
  • apps/api/src/billing/http-schemas/stripe.schema.ts (1 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/api/src/billing/services/stripe/stripe.service.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/billing/http-schemas/stripe.schema.ts
⏰ 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). (10)
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build

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

@codecov
Copy link

codecov bot commented Sep 15, 2025

Codecov Report

❌ Patch coverage is 0% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 44.59%. Comparing base (1cffbf6) to head (d26f000).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
apps/deploy-web/src/pages/payment.tsx 0.00% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1904      +/-   ##
==========================================
- Coverage   44.62%   44.59%   -0.03%     
==========================================
  Files         979      979              
  Lines       27438    27454      +16     
  Branches     7099     7089      -10     
==========================================
  Hits        12243    12243              
- Misses      14098    14846     +748     
+ Partials     1097      365     -732     
Flag Coverage Δ
api 81.43% <ø> (-0.10%) ⬇️
deploy-web 22.67% <0.00%> (-0.02%) ⬇️
log-collector 75.35% <ø> (ø)
notifications 87.94% <ø> (ø)
provider-console 81.48% <ø> (ø)
provider-proxy 84.47% <ø> (ø)
Files with missing lines Coverage Δ
apps/api/src/billing/http-schemas/stripe.schema.ts 100.00% <ø> (ø)
.../api/src/billing/services/stripe/stripe.service.ts 92.38% <ø> (+0.03%) ⬆️
apps/deploy-web/src/pages/payment.tsx 0.00% <0.00%> (ø)

... and 222 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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 (7)
apps/api/src/billing/services/stripe-error/stripe-error.service.ts (2)

39-42: Add error-code mapping for the new message to avoid "unknown_payment_error".

Right now, "Amount must be greater than $0" will surface with code "unknown_payment_error". Map it in getPaymentErrorCodeFromMessage so clients can branch reliably.

Apply this diff:

@@
   private getPaymentErrorCodeFromMessage(message: string): string {
     const messageLower = message.toLowerCase();
 
     if (messageLower.includes("insufficient funds")) {
       return "insufficient_funds";
@@
     } else if (messageLower.includes("coupon id is required")) {
       return "coupon_id_required";
+    } else if (messageLower.includes("must be greater than $0")) {
+      return "amount_must_be_positive";
     }
 
     return "unknown_payment_error";
   }

69-99: Consider stabilizing against strict message matching.

The includes-based lookup on full English strings is brittle. Prefer internal codes (thrown or attached) and a single translation table.

If you want, I can sketch a minimal Error subclass + code plumbing to phase in without breaking existing messages.

Also applies to: 171-195

apps/deploy-web/src/pages/payment.tsx (2)

196-205: Handle NaN early to prevent false “valid” cases.

parseFloat can yield NaN, which currently slips through and clears errors. Add a finite-number guard.

   const validateAmount = (value: number) => {
     const finalAmount = getFinalAmount(value.toString());
+    if (!Number.isFinite(value)) {
+      setAmountError("Enter a valid amount");
+      return false;
+    }
     // Prevent $0 payments
     if (value <= 0) {
       setAmountError("Amount must be greater than $0");
       return false;
     }
 
     // Only check for minimum amount if no coupon is applied
     if (!discounts.length && value < MINIMUM_PAYMENT_AMOUNT) {
       setAmountError(`Minimum amount is $${MINIMUM_PAYMENT_AMOUNT}`);
       return false;
     }

196-205: Optional: align UI/server wording for the min-amount error.

Backend returns “Minimum payment amount is $20 (before any discounts)”; UI says “Minimum amount is $20”. Consider aligning to reduce confusion.

apps/api/src/billing/services/stripe-error/stripe-error.service.spec.ts (1)

80-90: Add an assertion for the frontend-facing error code.

Once you map the code in the service, assert it here via getPaymentErrorCode for end-to-end safety.

   describe("payment errors", () => {
     it("should handle 'Amount must be greater than $0'", () => {
       const { service } = setup();
       const error = new Error("Amount must be greater than $0");
       const result = service.toAppError(error, "payment");
 
       expect(result).toHaveProperty("status", 400);
       expect(result).toHaveProperty("message", "Amount must be greater than $0");
     });
+
+    it("should expose code 'amount_must_be_positive' for UI mapping", () => {
+      const { service } = setup();
+      const code = service.getPaymentErrorCode(new Error("Amount must be greater than $0"));
+      expect(code).toEqual({
+        message: "Amount must be greater than $0",
+        code: "amount_must_be_positive",
+        type: "payment_error"
+      });
+    });

Also applies to: 351-360

apps/api/src/billing/services/stripe/stripe.service.ts (2)

30-30: Consider sharing MINIMUM_PAYMENT_AMOUNT across API and UI to prevent drift.

A small shared constants package (e.g., @src/shared/payments) would avoid divergence.

I can draft a minimal shared module if desired.


184-189: Remove redundant post-check for the same min condition.

The later branch re-enforces the same “no-discount < $20” rule. Keeping one source reduces drift.

-    } else if (!discounts.length && finalAmountDollars > 0 && finalAmountDollars < 20) {
-      throw new Error("Minimum payment amount is $20 (before any discounts)");
-    }
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1cffbf6 and 8b3440e.

📒 Files selected for processing (5)
  • apps/api/src/billing/services/stripe-error/stripe-error.service.spec.ts (1 hunks)
  • apps/api/src/billing/services/stripe-error/stripe-error.service.ts (1 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts (2 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (2 hunks)
  • apps/deploy-web/src/pages/payment.tsx (2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() to mock dependencies in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test.

**/*.spec.{ts,tsx}: Use setup function instead of beforeEach in test files
setup function must be at the bottom of the root describe block in test files
setup function creates an object under test and returns it
setup function should accept a single parameter with inline type definition
Don't use shared state in setup function
Don't specify return type of setup function

Files:

  • apps/api/src/billing/services/stripe-error/stripe-error.service.spec.ts
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/api/src/billing/services/stripe-error/stripe-error.service.spec.ts
  • apps/api/src/billing/services/stripe-error/stripe-error.service.ts
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts
  • apps/deploy-web/src/pages/payment.tsx
  • apps/api/src/billing/services/stripe/stripe.service.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/api/src/billing/services/stripe-error/stripe-error.service.spec.ts
  • apps/api/src/billing/services/stripe-error/stripe-error.service.ts
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts
  • apps/deploy-web/src/pages/payment.tsx
  • apps/api/src/billing/services/stripe/stripe.service.ts
⏰ 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: validate / validate-app
🔇 Additional comments (6)
apps/deploy-web/src/pages/payment.tsx (1)

18-18: Nice: single source for the UI minimum.

Defining MINIMUM_PAYMENT_AMOUNT locally improves readability.

apps/api/src/billing/services/stripe-error/stripe-error.service.spec.ts (1)

81-88: Good coverage for the new mapping.

Test correctly asserts 400 and message preservation.

apps/api/src/billing/services/stripe/stripe.service.spec.ts (3)

75-85: Tests for zero amount look good and match new contract.

Covers the <= 0 guard.


86-96: Covers negative amounts too — nice.

Complements the zero-amount case.


584-587: CSV tests updated to nested card shape — consistent with transformer.

Matches transformTransactionForCsv’s accessors. Good.

apps/api/src/billing/services/stripe/stripe.service.ts (1)

148-156: Pre-discount guards are correct and user-friendly.

Blocking non-positive amounts and enforcing the min when no discounts exist mirrors the UI behavior.

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: 1

🧹 Nitpick comments (1)
apps/api/src/billing/http-schemas/stripe.schema.ts (1)

58-60: Currency-agnostic error text

Message hardcodes “$”. If multi-currency is possible, prefer a generic string.

-    amount: z.number().gt(0, "Amount must be greater than $0"),
+    amount: z.number().gt(0, "Amount must be greater than 0"),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b31a92 and 838dd60.

📒 Files selected for processing (3)
  • apps/api/src/billing/http-schemas/stripe.schema.ts (1 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts (1 hunks)
  • apps/api/src/billing/services/stripe/stripe.service.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/api/src/billing/services/stripe/stripe.service.ts
  • apps/api/src/billing/services/stripe/stripe.service.spec.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/api/src/billing/http-schemas/stripe.schema.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/api/src/billing/http-schemas/stripe.schema.ts
⏰ 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). (4)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build

@baktun14 baktun14 merged commit e32155e into main Sep 16, 2025
57 of 58 checks passed
@baktun14 baktun14 deleted the fix/zero-payment-fix branch September 16, 2025 09:31
stalniy pushed a commit that referenced this pull request Nov 20, 2025
* fix(billing): prevent 0 amount payments

* fix(billing): remove comments

* fix(billing): move min amount of 0$ to zod

* fix(billing): update amount validation to ensure it is greater than $0

* fix(billing): enhance amount validation to include exclusive minimum and minimum constraints

* fix(billing): update minimum payment amount validation to $20
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

Comments