Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code c0b0b16766 fix(billing): replace circular billingProduct self-join with direct metadata access in listPlans
https://sonarly.com/issue/21144?type=bug

The `CheckoutSession` mutation takes ~2.8s due to a 1.9s database query caused by a circular self-join pattern in `BillingPlanService.listPlans()`, degrading checkout UX for users subscribing to a plan.

Fix: Changed `relations: ['billingPrices.billingProduct']` to `relations: ['billingPrices']` in `BillingPlanService.listPlans()`, eliminating the circular 3-table self-join (billingProduct → billingPrice → billingProduct) that was causing a 1.9s database query.

Added a simple in-memory loop to populate the `billingProduct` back-reference on each price from its parent product, preserving the same data contract for all downstream callers (e.g., `price.billingProduct?.metadata.productKey` in `billing-portal.workspace-service.ts:309`).

This is semantically equivalent — the circular DB join was loading the exact same parent product entity that we already have in memory. The fix eliminates the self-join from the SQL query while keeping the same runtime data structure.

**What this changes in the generated SQL:**
- Before: `SELECT ... FROM billingProduct LEFT JOIN billingPrice ... LEFT JOIN billingProduct ...` (3 tables, circular)
- After: `SELECT ... FROM billingProduct LEFT JOIN billingPrice ...` (2 tables, no circular join)

**Additional recommendation (not in this PR):** Add a database index on `core.billingPrice.stripeProductId` via a migration to improve the remaining JOIN performance. This column is used as the FK/join column but has no index in the current schema.
2026-04-02 11:20:20 +00:00
@@ -70,9 +70,17 @@ export class BillingPlanService {
active: true,
},
},
relations: ['billingPrices.billingProduct'],
relations: ['billingPrices'],
});
// Populate the billingProduct back-reference on each price in memory
// to avoid a circular self-join (billingProduct → billingPrice → billingProduct)
for (const product of products) {
for (const price of product.billingPrices) {
price.billingProduct = product;
}
}
return planKeys.map((planKey) => {
const planProducts = products.filter(
(product) => product.metadata.planKey === planKey,