Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@Tufailahmed-Bargir
Copy link
Contributor

@Tufailahmed-Bargir Tufailahmed-Bargir commented Jan 10, 2026

What Does this PR Do?

fixes: #1317


Summary by cubic

Prevents $0 payment records for fully paid invoices and fixes a hydration mismatch in the organization form by using stable form item IDs.

  • Bug Fixes
    • Billing: Skip payment when amountToCharge <= 0 and process the period via processNoMoreDueForBillingPeriod. Move this guard before Stripe ID validation so $0 cases succeed without Stripe IDs.
    • Forms: Add an optional formItemId to FormItem and set it on organization fields (name, country, payment processing, codebase overview, referral) to keep IDs stable across SSR/CSR.

Written for commit afd531f. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented creation of payment records when amount to charge is zero; invoices and billing runs now transition to paid/succeeded states without initiating a payment flow.
  • Tests

    • Added tests confirming that when existing payments fully cover dues, no new payment is created and billing/invoice states update as expected, including variants with and without payment provider IDs.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 10, 2026

📝 Walkthrough

Walkthrough

Added guards in billing run flow to skip creating Payment records when amountToCharge <= 0; both affected call sites call processNoMoreDueForBillingPeriod and return early, updating invoice and billing-period/run state without initiating a payment.

Changes

Cohort / File(s) Summary
Billing run logic
platform/flowglad-next/src/subscriptions/billingRunHelpers.ts
Added guards in executeBillingRunCalculationAndBookkeepingSteps and executeBillingRun to skip Payment creation when amountToCharge <= 0; calls processNoMoreDueForBillingPeriod and returns early with updated invoice/billingPeriod state.
Tests
platform/flowglad-next/src/subscriptions/billingRunHelpers.test.ts
Added two scenarios (with and without Stripe IDs) asserting that when existing payments fully cover an invoice amountToCharge === 0: no new Payment is created, invoice marked Paid, and billing run marked Succeeded.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested reviewers

  • agreea

Poem

🐰 I hopped through ledgers, light and fleet,

When nothing's due I skip a beat.
No phantom payments in my trail,
Invoices settle, neat and hale. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 3
❌ Failed checks (1 warning, 2 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes FormItem/formItemId hydration changes unrelated to issue #1317 billing fix; review feedback explicitly requested these be removed and submitted in a separate PR. Remove FormItem hydration changes from this PR and address them in a separate pull request focused on forms.
Title check ❓ Inconclusive The PR title 'Dev1' is vague and generic, providing no meaningful information about the actual changes. Replace with a descriptive title summarizing the main change, e.g., 'Fix zero-amount payment records for fully paid invoices' or similar.
Description check ❓ Inconclusive Description includes the auto-generated summary and addresses issue #1317, but references FormItem hydration changes that review feedback indicates should be in a separate PR. Remove the FormItem/formItemId hydration changes from this PR and submit them separately; keep only the billing fix for $0 payment records.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the core fix for issue #1317: guards payment creation with amountToCharge > 0, calls processNoMoreDueForBillingPeriod instead, and includes regression tests verifying no Payment record is created.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

🧹 Nitpick comments (1)
platform/flowglad-next/src/subscriptions/billingRunHelpers.ts (1)

593-616: Consider extracting common early-return pattern.

The guards at lines 593-616 and 639-662 follow a nearly identical pattern: check a condition, call processNoMoreDueForBillingPeriod, and return early with the same result shape. While both guards are necessary for different scenarios (one checks totalDueAmount, the other checks amountToCharge), the repetition could be reduced.

♻️ Optional refactor to reduce duplication

Consider extracting a helper function:

const handleNoChargeScenario = async (
  billingRun: BillingRun.Record,
  billingPeriod: BillingPeriod.Record,
  invoice: Invoice.Record,
  feeCalculation: FeeCalculation.Record,
  customer: Customer.Record,
  organization: Organization.Record,
  subscription: Subscription.Record,
  paymentMethod: PaymentMethod.Record,
  totalDueAmount: number,
  totalAmountPaid: number,
  amountToCharge: number,
  payments: Payment.Record[],
  transaction: DbTransaction
): Promise<ExecuteBillingRunStepsResult> => {
  const processBillingPeriodResult = await processNoMoreDueForBillingPeriod(
    { billingRun, billingPeriod, invoice },
    transaction
  )
  return {
    invoice: processBillingPeriodResult.invoice,
    feeCalculation,
    customer,
    organization,
    billingPeriod: processBillingPeriodResult.billingPeriod,
    subscription,
    paymentMethod,
    totalDueAmount,
    totalAmountPaid,
    amountToCharge,
    payments,
  }
}

Then use it in both guards:

  if (totalDueAmount <= 0) {
-   const processBillingPeriodResult =
-     await processNoMoreDueForBillingPeriod(
-       {
-         billingRun,
-         billingPeriod,
-         invoice,
-       },
-       transaction
-     )
-   return {
-     invoice: processBillingPeriodResult.invoice,
-     feeCalculation,
-     customer,
-     organization,
-     billingPeriod: processBillingPeriodResult.billingPeriod,
-     subscription,
-     paymentMethod,
-     totalDueAmount,
-     totalAmountPaid,
-     amountToCharge,
-     payments,
-   }
+   return handleNoChargeScenario(
+     billingRun, billingPeriod, invoice, feeCalculation,
+     customer, organization, subscription, paymentMethod,
+     totalDueAmount, totalAmountPaid, amountToCharge, payments,
+     transaction
+   )
  }

Also applies to: 639-662

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between efd3bdb and 168679c.

📒 Files selected for processing (3)
  • platform/flowglad-next/src/components/forms/OrganizationFormFields.tsx
  • platform/flowglad-next/src/components/ui/form.tsx
  • platform/flowglad-next/src/subscriptions/billingRunHelpers.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use idiomatic TypeScript
Avoid type assertions
DO NOT use explicit any type

Files:

  • platform/flowglad-next/src/components/forms/OrganizationFormFields.tsx
  • platform/flowglad-next/src/components/ui/form.tsx
  • platform/flowglad-next/src/subscriptions/billingRunHelpers.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Avoid code duplication
Leave JSDoc comments for complex functions and APIs
DO NOT use IIFEs (Immediately Invoked Function Expressions)
DO NOT use nested ternaries

Files:

  • platform/flowglad-next/src/components/forms/OrganizationFormFields.tsx
  • platform/flowglad-next/src/components/ui/form.tsx
  • platform/flowglad-next/src/subscriptions/billingRunHelpers.ts
🧠 Learnings (3)
📚 Learning: 2026-01-07T20:17:16.342Z
Learnt from: BrooksFlannery
Repo: flowglad/flowglad PR: 1262
File: platform/flowglad-next/seedDatabase.ts:900-909
Timestamp: 2026-01-07T20:17:16.342Z
Learning: In Zod v4, prefer using z.enum(MyEnum) for TypeScript enums and avoid z.nativeEnum(MyEnum) (the latter is deprecated). When reviewing schemas, verify that z.enum is used with a TypeScript enum (not a string-literal array) and that z.nativeEnum is not used for enums. This guidance applies to all TypeScript files in the codebase where Zod schemas are defined.

Applied to files:

  • platform/flowglad-next/src/components/forms/OrganizationFormFields.tsx
  • platform/flowglad-next/src/components/ui/form.tsx
  • platform/flowglad-next/src/subscriptions/billingRunHelpers.ts
📚 Learning: 2026-01-07T16:33:30.505Z
Learnt from: andresthedesigner
Repo: flowglad/flowglad PR: 1254
File: platform/flowglad-next/src/utils/chartIntervalUtils.ts:71-76
Timestamp: 2026-01-07T16:33:30.505Z
Learning: In the flowglad codebase, avoid suggesting defensive validation (e.g., input guards, Math.abs() for date differences) for internal utility functions when all callers are controlled and pass validated inputs from UI components. Per workspace rules, only make changes that are directly requested or clearly necessary.

Applied to files:

  • platform/flowglad-next/src/subscriptions/billingRunHelpers.ts
📚 Learning: 2025-12-08T19:35:29.442Z
Learnt from: BrooksFlannery
Repo: flowglad/flowglad PR: 944
File: platform/flowglad-next/src/db/tableMethods/subscriptionMethods.test.ts:536-593
Timestamp: 2025-12-08T19:35:29.442Z
Learning: In test files for subscription methods (platform/flowglad-next/src/db/tableMethods/subscriptionMethods.test.ts), when passing filters with cross-table fields like productName to selectSubscriptionsTableRowData, cast the filters object as SubscriptionTableFilters instead of Record<string, unknown> to maintain type safety while accommodating fields that aren't on the base subscriptions table.

Applied to files:

  • platform/flowglad-next/src/subscriptions/billingRunHelpers.ts
🧬 Code graph analysis (1)
platform/flowglad-next/src/components/forms/OrganizationFormFields.tsx (1)
platform/flowglad-next/src/components/ui/form.tsx (1)
  • FormItem (219-219)
⏰ 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: cubic · AI code reviewer
🔇 Additional comments (3)
platform/flowglad-next/src/subscriptions/billingRunHelpers.ts (1)

633-662: Excellent fix for zero-amount payment records!

This guard correctly prevents the creation of orphaned $0 payment records when amountToCharge <= 0 (e.g., when an invoice is already fully covered by credits or prior payments). The fix directly addresses issue #1317 by ensuring payment records are only created when there's an actual amount to charge.

The implementation is sound:

  • Invoice and line items are created
  • Billing period and run statuses are updated appropriately via processNoMoreDueForBillingPeriod
  • No payment record is created, avoiding the orphaned Processing payment issue
platform/flowglad-next/src/components/ui/form.tsx (1)

77-80: Clean implementation of optional form item IDs.

The addition of the optional formItemId prop is well-implemented and follows React best practices. The fallback to React.useId() ensures backward compatibility while allowing explicit IDs to prevent hydration mismatches in SSR scenarios (as indicated by the commit message "fix: hydration mismatch error").

The type-safe approach and proper use of the nullish coalescing operator make this a solid enhancement to the FormItem API.

platform/flowglad-next/src/components/forms/OrganizationFormFields.tsx (1)

154-154: Good use of explicit form item IDs.

The addition of explicit formItemId props to the FormItem components follows a consistent naming convention ("org-" prefix) and provides semantic, descriptive IDs. This aligns with the fix for hydration mismatch errors by ensuring stable IDs between server and client renders.

All IDs are unique within the form, and the changes don't affect validation, submission, or error handling logic.

Also applies to: 167-167, 199-199, 325-325, 369-369

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 3 files

@Tufailahmed-Bargir
Copy link
Contributor Author

@agreea review please

@joeysabs
Copy link
Collaborator

Code Review Feedback

1. Please remove the unrelated hydration fix

The FormItem/formItemId changes in form.tsx and OrganizationFormFields.tsx are unrelated to issue #1317. Please remove these changes and submit them as a separate PR if needed.

This PR should only contain the billing fix for the $0 payment record issue.

2. Please add this test case

The billing fix looks good, but please add a test case to prevent regression. Here's the test to add in billingRunHelpers.test.ts after the 'payment.amount equals amountToCharge...' test:

it('does not create a payment record when amountToCharge is 0 due to existing payments fully covering totalDueAmount', async () => {
  // Setup: Create a billing period item with a known price
  const knownPrice = 10000 // $100 in cents
  await adminTransaction(async ({ transaction }) => {
    await updateBillingPeriodItem(
      {
        id: staticBillingPeriodItem.id,
        unitPrice: knownPrice,
        type: SubscriptionItemType.Static,
      },
      transaction
    )
  })

  // Create an invoice for this billing period
  const testInvoice = await setupInvoice({
    billingPeriodId: billingPeriod.id,
    customerId: customer.id,
    organizationId: organization.id,
    priceId: staticPrice.id,
  })

  // Create an existing payment that FULLY covers the totalDueAmount
  // This simulates the scenario from issue #1317 where prior payments
  // have already covered the full amount
  await setupPayment({
    stripeChargeId: 'ch_fullcover_' + core.nanoid(),
    status: PaymentStatus.Succeeded,
    amount: knownPrice, // Full amount - should result in amountToCharge = 0
    livemode: billingPeriod.livemode,
    customerId: customer.id,
    organizationId: organization.id,
    stripePaymentIntentId: 'pi_fullcover_' + core.nanoid(),
    invoiceId: testInvoice.id,
    paymentMethod: paymentMethod.type,
    billingPeriodId: billingPeriod.id,
    subscriptionId: billingPeriod.subscriptionId,
    paymentMethodId: paymentMethod.id,
  })

  // Execute the billing run
  const result = await adminTransaction(({ transaction }) =>
    executeBillingRunCalculationAndBookkeepingSteps(
      billingRun,
      transaction
    )
  )

  // Verify the scenario: totalDueAmount > 0 but amountToCharge = 0
  expect(result.totalDueAmount).toBeGreaterThan(0)
  expect(result.totalAmountPaid).toBe(knownPrice)
  expect(result.amountToCharge).toBe(0)

  // The fix: no payment record should be created when amountToCharge is 0
  // Previously this would create an orphaned $0 payment with status: Processing
  expect(result.payment).toBeUndefined()

  // Verify the invoice is marked as paid
  expect(result.invoice.status).toBe(InvoiceStatus.Paid)

  // Verify the billing run is marked as succeeded
  const updatedBillingRun = await adminTransaction(
    ({ transaction }) =>
      selectBillingRunById(billingRun.id, transaction)
  )
  expect(updatedBillingRun.status).toBe(
    BillingRunStatus.Succeeded
  )
})

This test verifies that when totalDueAmount > 0 but amountToCharge = 0 (due to existing payments), no payment record is created and the billing run completes successfully.

@Tufailahmed-Bargir
Copy link
Contributor Author

sure!

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
platform/flowglad-next/src/subscriptions/billingRunHelpers.ts (1)

618-663: Move the amountToCharge <= 0 guard before Stripe ID validation.

When an invoice is fully paid through prior payments or credits, amountToCharge becomes 0 (line 471: Math.max(0, totalDueAmount - totalAmountPaid)). In this case, no Stripe charge is needed, so the function should return early without requiring valid Stripe customer or payment method IDs. Currently, the Stripe ID checks (lines 620-630) execute first and throw an error even though no Stripe operation will occur.

This causes incorrect billing run failures for legitimately fully-paid invoices when customers lack Stripe credentials—reintroducing the exact problem that the amountToCharge <= 0 guard was designed to prevent.

Proposed fix
-  const stripeCustomerId = customer.stripeCustomerId
-  const stripePaymentMethodId = paymentMethod.stripePaymentMethodId
-  if (!stripeCustomerId) {
-    throw new Error(
-      `Cannot run billing for a billing period with a customer that does not have a stripe customer id.` +
-        ` Customer: ${customer.id}; Billing Period: ${billingPeriod.id}`
-    )
-  }
-  if (!stripePaymentMethodId) {
-    throw new Error(
-      `Cannot run billing for a billing period with a payment method that does not have a stripe payment method id.` +
-        `Payment Method: ${paymentMethod.id}; Billing Period: ${billingPeriod.id}`
-    )
-  }
-
   /**
    * Guard: Only create a payment record if there's actually an amount to charge.
    * This prevents orphaned $0 payment records when the invoice is already fully
    * paid via credits or prior payments (amountToCharge = 0).
    * See: https://github.com/flowglad/flowglad/issues/1317
    */
   if (amountToCharge <= 0) {
     const processBillingPeriodResult =
       await processNoMoreDueForBillingPeriod(
         {
           billingRun,
           billingPeriod,
           invoice,
         },
         transaction
       )
     return {
       invoice: processBillingPeriodResult.invoice,
       feeCalculation,
       customer,
       organization,
       billingPeriod: processBillingPeriodResult.billingPeriod,
       subscription,
       paymentMethod,
       totalDueAmount,
       totalAmountPaid,
       amountToCharge,
       payments,
     }
   }
+
+  const stripeCustomerId = customer.stripeCustomerId
+  const stripePaymentMethodId = paymentMethod.stripePaymentMethodId
+  if (!stripeCustomerId) {
+    throw new Error(
+      `Cannot run billing for a billing period with a customer that does not have a stripe customer id.` +
+        ` Customer: ${customer.id}; Billing Period: ${billingPeriod.id}`
+    )
+  }
+  if (!stripePaymentMethodId) {
+    throw new Error(
+      `Cannot run billing for a billing period with a payment method that does not have a stripe payment method id.` +
+        `Payment Method: ${paymentMethod.id}; Billing Period: ${billingPeriod.id}`
+    )
+  }

Also add a regression test covering totalDueAmount > 0 with prior payments fully covering it (resulting in amountToCharge === 0) and verifying that no Payment record is created.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 168679c and 4b09953.

📒 Files selected for processing (1)
  • platform/flowglad-next/src/subscriptions/billingRunHelpers.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use idiomatic TypeScript
Avoid type assertions
DO NOT use explicit any type

Files:

  • platform/flowglad-next/src/subscriptions/billingRunHelpers.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Avoid code duplication
Leave JSDoc comments for complex functions and APIs
DO NOT use IIFEs (Immediately Invoked Function Expressions)
DO NOT use nested ternaries

Files:

  • platform/flowglad-next/src/subscriptions/billingRunHelpers.ts
🧠 Learnings (3)
📚 Learning: 2026-01-07T16:33:30.505Z
Learnt from: andresthedesigner
Repo: flowglad/flowglad PR: 1254
File: platform/flowglad-next/src/utils/chartIntervalUtils.ts:71-76
Timestamp: 2026-01-07T16:33:30.505Z
Learning: In the flowglad codebase, avoid suggesting defensive validation (e.g., input guards, Math.abs() for date differences) for internal utility functions when all callers are controlled and pass validated inputs from UI components. Per workspace rules, only make changes that are directly requested or clearly necessary.

Applied to files:

  • platform/flowglad-next/src/subscriptions/billingRunHelpers.ts
📚 Learning: 2025-12-08T19:35:29.442Z
Learnt from: BrooksFlannery
Repo: flowglad/flowglad PR: 944
File: platform/flowglad-next/src/db/tableMethods/subscriptionMethods.test.ts:536-593
Timestamp: 2025-12-08T19:35:29.442Z
Learning: In test files for subscription methods (platform/flowglad-next/src/db/tableMethods/subscriptionMethods.test.ts), when passing filters with cross-table fields like productName to selectSubscriptionsTableRowData, cast the filters object as SubscriptionTableFilters instead of Record<string, unknown> to maintain type safety while accommodating fields that aren't on the base subscriptions table.

Applied to files:

  • platform/flowglad-next/src/subscriptions/billingRunHelpers.ts
📚 Learning: 2026-01-07T20:17:16.342Z
Learnt from: BrooksFlannery
Repo: flowglad/flowglad PR: 1262
File: platform/flowglad-next/seedDatabase.ts:900-909
Timestamp: 2026-01-07T20:17:16.342Z
Learning: In Zod v4, prefer using z.enum(MyEnum) for TypeScript enums and avoid z.nativeEnum(MyEnum) (the latter is deprecated). When reviewing schemas, verify that z.enum is used with a TypeScript enum (not a string-literal array) and that z.nativeEnum is not used for enums. This guidance applies to all TypeScript files in the codebase where Zod schemas are defined.

Applied to files:

  • platform/flowglad-next/src/subscriptions/billingRunHelpers.ts

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@Tufailahmed-Bargir
Copy link
Contributor Author

@joeysabs is it good to go now?

@joeysabs
Copy link
Collaborator

Hey it looks like coderabbit actually had a good catch that was outside of the diff range if you wanna check it out I had a quick writeup done on it.

The amountToCharge <= 0 guard is placed after the Stripe ID validation, which doesn't fully fix the issue.

Current order (with this PR):

1. totalDueAmount <= 0 check → returns early
2. stripeCustomerId validation → throws if missing
3. stripePaymentMethodId validation → throws if missing
4. amountToCharge <= 0 check → returns early (NEW)
5. Payment creation

Problem: When amountToCharge == 0 (prior payments fully cover the amount) but the customer lacks Stripe IDs, the code throws at step 2/3 before reaching the new guard at step 4.

Fix: Move the guard before line 621:

   if (totalDueAmount <= 0) {
     // ... existing handling
   }

+  // Guard: Skip Stripe validation and payment creation when nothing to charge
+  if (amountToCharge <= 0) {
+    const processBillingPeriodResult = await processNoMoreDueForBillingPeriod(
+      { billingRun, billingPeriod, invoice },
+      transaction
+    )
+    return {
+      invoice: processBillingPeriodResult.invoice,
+      feeCalculation,
+      customer,
+      organization,
+      billingPeriod: processBillingPeriodResult.billingPeriod,
+      subscription,
+      paymentMethod,
+      totalDueAmount,
+      totalAmountPaid,
+      amountToCharge,
+      payments,
+    }
+  }
+
   const stripeCustomerId = customer.stripeCustomerId

Additional test needed:

it('succeeds without Stripe IDs when amountToCharge is 0', async () => {
  // ... same setup as existing test ...

  // Remove Stripe IDs - should NOT cause failure when amountToCharge is 0
  await adminTransaction(async ({ transaction }) => {
    await updatePaymentMethod(
      { id: paymentMethod.id, stripePaymentMethodId: null },
      transaction
    )
  })

  // Should succeed, not throw
  const result = await adminTransaction(({ transaction }) =>
    executeBillingRunCalculationAndBookkeepingSteps(billingRun, transaction)
  )

  expect(result.amountToCharge).toBe(0)
  expect(result.payment).toBeUndefined()
  expect(result.invoice.status).toBe(InvoiceStatus.Paid)
})

The amountToCharge <= 0 guard must come BEFORE Stripe ID validation
because when prior payments fully cover the invoice amount (amountToCharge = 0),
we don't need Stripe IDs at all - no payment will be created.

Previously, if amountToCharge was 0 but the customer lacked Stripe IDs,
the billing run would incorrectly throw an error at the Stripe ID validation
before reaching the amountToCharge guard.

Also adds a regression test that verifies billing runs succeed without
Stripe IDs when amountToCharge is 0.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@joeysabs joeysabs changed the title Dev1 fix: prevent $0 payment records for fully paid invoices Jan 22, 2026
@joeysabs joeysabs added this pull request to the merge queue Jan 22, 2026
Merged via the queue into flowglad:main with commit 6f52cab Jan 22, 2026
13 checks passed
cursor bot pushed a commit that referenced this pull request Jan 26, 2026
## What Does this PR Do?
<!-- Please provide a clear and concise description of the changes in
this PR -->

fixes: #1317 

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Prevents $0 payment records for fully paid invoices and fixes a
hydration mismatch in the organization form by using stable form item
IDs.

- **Bug Fixes**
- Billing: Skip payment when amountToCharge <= 0 and process the period
via processNoMoreDueForBillingPeriod. Move this guard before Stripe ID
validation so $0 cases succeed without Stripe IDs.
- Forms: Add an optional formItemId to FormItem and set it on
organization fields (name, country, payment processing, codebase
overview, referral) to keep IDs stable across SSR/CSR.

<sup>Written for commit afd531f.
Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Prevented creation of payment records when amount to charge is zero;
invoices and billing runs now transition to paid/succeeded states
without initiating a payment flow.

* **Tests**
* Added tests confirming that when existing payments fully cover dues,
no new payment is created and billing/invoice states update as expected,
including variants with and without payment provider IDs.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: joeysabs <[email protected]>
Co-authored-by: Claude Opus 4.5 <[email protected]>
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.

Zero-Amount Payment Records Created for Fully Paid Invoices

2 participants