Skip to content

feat: billing + tier enforcement + Stripe integration (#9)#10

Merged
CIKR-Repos merged 2 commits intomainfrom
feat/billing-tier-enforcement
Feb 18, 2026
Merged

feat: billing + tier enforcement + Stripe integration (#9)#10
CIKR-Repos merged 2 commits intomainfrom
feat/billing-tier-enforcement

Conversation

@CIKR-Repos
Copy link
Owner

@CIKR-Repos CIKR-Repos commented Feb 18, 2026

Stripe checkout/portal, usage tracking, tier limits (Free/Pro/Enterprise), billing UI

Summary by Sourcery

Introduce Stripe-powered billing, subscription management, and tier-based usage enforcement across the app, alongside a revamped pipeline builder UI and a new billing dashboard.

New Features:

  • Add Stripe-backed billing service with checkout, customer portal, and webhook handling for Pro and Enterprise subscriptions.
  • Introduce tiered usage tracking (queries, documents, projects, storage) with per-user daily and aggregate limits.
  • Add middleware to enforce plan limits on chat queries, document uploads, and project creation before processing requests.
  • Expose billing and usage APIs plus a new authenticated billing page with plan selection and usage dashboard in the client.
  • Add widget configuration entity and authenticated API for managing embeddable chat widget settings per project.

Bug Fixes:

  • Ensure numeric pipeline configuration values are consistently parsed and cast before use in pipeline summaries and previews.

Enhancements:

  • Redesign the pipeline builder UI into a block-based configuration experience with inline block selection, status, and chunk preview.
  • Refactor pipeline builder state management into typed steps, computed progress, and richer block config summaries.
  • Surface a Billing link and current tier in the navbar to make subscription management discoverable.

Documentation:

  • Document the new billing, Stripe integration, tier limits, and related API endpoints in a dedicated feature markdown file.

Tests:

  • Add initial test scaffolding for pipeline builder models, services, and page component to support future coverage.

@sourcery-ai
Copy link

sourcery-ai bot commented Feb 18, 2026

Reviewer's Guide

Implements Stripe-based billing, subscription and usage tracking with tier enforcement (Free/Pro/Enterprise), adds a billing UI and usage dashboard on the client, and refactors the pipeline builder into a block-based editor with richer configuration and preview.

Sequence diagram for Stripe checkout and subscription activation

sequenceDiagram
  actor U as User
  participant BP as BillingComponent
  participant BS as BillingService_Angular
  participant BC as BillingController
  participant SS as StripeService
  participant S as Stripe
  participant SW as StripeWebhooks
  participant BWC as BillingController_webhook
  participant SS2 as StripeService_webhook
  participant DB as PipeRagDbContext

  U->>BP: Click Upgrade(Pro)
  BP->>BS: upgrade("Pro")
  BS->>BC: POST /api/billing/create-checkout-session
  BC->>SS: CreateCheckoutSessionAsync(userId, Pro, successUrl, cancelUrl)
  SS->>DB: Load User and Subscription
  SS->>S: Create Customer and Checkout Session
  S-->>SS: CheckoutSession(url)
  SS-->>BC: url
  BC-->>BS: { url }
  BS-->>U: Redirect to Stripe Checkout

  SW->>BWC: POST /api/billing/webhook
  BWC->>SS2: HandleWebhookAsync(json, signature)
  SS2->>S: Verify event using webhook secret
  S-->>SS2: checkout.session.completed
  SS2->>DB: Load or create Subscription
  SS2->>DB: Update Subscription(Tier=Pro, Status=Active)
  SS2->>DB: Update User.Tier = Pro
  SS2-->>BWC: success
  BWC-->>SW: 200 OK
Loading

Sequence diagram for tier enforcement on write operations

sequenceDiagram
  participant C as Client
  participant API as ASP.NET_Core
  participant TEM as TierEnforcementMiddleware
  participant UTS as IUsageTrackingService
  participant CTRL as TargetController

  C->>API: POST /api/chat
  API->>TEM: InvokeAsync(HttpContext)
  TEM->>TEM: Check user authenticated and path
  TEM->>UTS: CanPerformQueryAsync(userId)
  UTS-->>TEM: true or false
  alt limit not reached
    TEM->>API: Call next middleware
    API->>CTRL: Handle chat request
    CTRL-->>C: 200 OK with response
  else limit reached
    TEM-->>C: 429 Too Many Requests
  end
Loading

Entity relationship diagram for subscriptions, usage and widget config

erDiagram
  User ||--o| Subscription : has
  User ||--o{ UsageRecord : has
  User ||--o{ Project : owns
  Project ||--o{ Document : has
  Project ||--|| WidgetConfig : has

  User {
    guid Id
    string Email
    string Tier
  }

  Subscription {
    guid Id
    guid UserId
    string StripeCustomerId
    string StripeSubscriptionId
    string StripePriceId
    string Tier
    string Status
    datetime CurrentPeriodStart
    datetime CurrentPeriodEnd
    datetime CreatedAt
    datetime UpdatedAt
    datetime CancelledAt
  }

  UsageRecord {
    guid Id
    guid UserId
    date Date
    int QueryCount
    int DocumentCount
    int ProjectCount
    long StorageBytes
    datetime UpdatedAt
  }

  Project {
    guid Id
    guid OwnerId
  }

  Document {
    guid Id
    guid ProjectId
    long FileSizeBytes
  }

  WidgetConfig {
    guid Id
    guid ProjectId
    string PrimaryColor
    string BackgroundColor
    string TextColor
    string Position
    string AvatarUrl
    string Title
    string Subtitle
    string PlaceholderText
    string AllowedOrigins
    bool IsActive
    datetime CreatedAt
    datetime UpdatedAt
  }
Loading

Class diagram for billing, usage tracking and tier enforcement

classDiagram
  class IBillingService {
    <<interface>>
    +Task~string~ CreateCheckoutSessionAsync(Guid userId, UserTier tier, string successUrl, string cancelUrl)
    +Task~string~ CreatePortalSessionAsync(Guid userId, string returnUrl)
    +Task HandleWebhookAsync(string json, string stripeSignature)
    +Task~SubscriptionDto?~ GetSubscriptionAsync(Guid userId)
  }

  class IUsageTrackingService {
    <<interface>>
    +Task IncrementQueryCountAsync(Guid userId)
    +Task~UsageDto~ GetUsageAsync(Guid userId)
    +Task~bool~ CanPerformQueryAsync(Guid userId)
    +Task~bool~ CanCreateDocumentAsync(Guid userId)
    +Task~bool~ CanCreateProjectAsync(Guid userId)
    +Task~bool~ CanUploadStorageAsync(Guid userId, long additionalBytes)
    +Task RecalculateDocumentCountAsync(Guid userId)
    +Task RecalculateProjectCountAsync(Guid userId)
    +Task RecalculateStorageAsync(Guid userId)
  }

  class StripeService {
    -PipeRagDbContext _db
    -IConfiguration _config
    -ILogger_StripeService_ _logger
    +StripeService(PipeRagDbContext db, IConfiguration config, ILogger_StripeService_ logger)
    +Task~string~ CreateCheckoutSessionAsync(Guid userId, UserTier tier, string successUrl, string cancelUrl)
    +Task~string~ CreatePortalSessionAsync(Guid userId, string returnUrl)
    +Task HandleWebhookAsync(string json, string stripeSignature)
    +Task~SubscriptionDto?~ GetSubscriptionAsync(Guid userId)
    -Task HandleCheckoutCompleted(Event stripeEvent)
    -Task HandleSubscriptionUpdated(Event stripeEvent)
    -Task HandleSubscriptionDeleted(Event stripeEvent)
  }

  class UsageTrackingService {
    -PipeRagDbContext _db
    +UsageTrackingService(PipeRagDbContext db)
    +Task IncrementQueryCountAsync(Guid userId)
    +Task~UsageDto~ GetUsageAsync(Guid userId)
    +Task~bool~ CanPerformQueryAsync(Guid userId)
    +Task~bool~ CanCreateDocumentAsync(Guid userId)
    +Task~bool~ CanCreateProjectAsync(Guid userId)
    +Task~bool~ CanUploadStorageAsync(Guid userId, long additionalBytes)
    +Task RecalculateDocumentCountAsync(Guid userId)
    +Task RecalculateProjectCountAsync(Guid userId)
    +Task RecalculateStorageAsync(Guid userId)
    -Task~UsageRecord~ GetOrCreateTodayRecord(Guid userId)
  }

  class TierEnforcementMiddleware {
    -RequestDelegate _next
    -ILogger_TierEnforcementMiddleware_ _logger
    -static HashSet~string~ QueryPaths
    -static HashSet~string~ DocumentPaths
    -static HashSet~string~ ProjectPaths
    +TierEnforcementMiddleware(RequestDelegate next, ILogger_TierEnforcementMiddleware_ logger)
    +Task InvokeAsync(HttpContext context, IUsageTrackingService usageService)
  }

  class BillingController {
    -IBillingService _billing
    -IUsageTrackingService _usage
    +BillingController(IBillingService billing, IUsageTrackingService usage)
    +Task~ActionResult~ CreateCheckoutSession(CheckoutRequest request)
    +Task~ActionResult~ CreatePortalSession(PortalRequest request)
    +Task~ActionResult~ Webhook()
    +Task~ActionResult~ GetSubscription()
    +Task~ActionResult~ GetUsage()
    -Guid GetUserId()
  }

  class WidgetController {
    -PipeRagDbContext _db
    +WidgetController(PipeRagDbContext db)
    +Task~ActionResult~ Get(Guid projectId, CancellationToken ct)
    +Task~ActionResult~ Upsert(Guid projectId, WidgetConfigRequest request, CancellationToken ct)
    +Task~IActionResult~ Delete(Guid projectId, CancellationToken ct)
    -Task~Project?~ GetAuthorizedProjectAsync(Guid projectId, CancellationToken ct)
    -Guid GetUserId()
    -WidgetConfigResponse MapToResponse(WidgetConfig c)
  }

  class SubscriptionDto {
    +UserTier Tier
    +string Status
    +DateTime CurrentPeriodEnd
    +string StripeCustomerId
  }

  class UsageDto {
    +int QueriesUsed
    +int QueriesLimit
    +int DocumentsUsed
    +int DocumentsLimit
    +int ProjectsUsed
    +int ProjectsLimit
    +long StorageBytesUsed
    +long StorageBytesLimit
    +UserTier Tier
  }

  class Subscription {
    +Guid Id
    +Guid UserId
    +string StripeCustomerId
    +string StripeSubscriptionId
    +string StripePriceId
    +UserTier Tier
    +SubscriptionStatus Status
    +DateTime CurrentPeriodStart
    +DateTime CurrentPeriodEnd
    +DateTime CreatedAt
    +DateTime UpdatedAt
    +DateTime CancelledAt
  }

  class UsageRecord {
    +Guid Id
    +Guid UserId
    +DateTime Date
    +int QueryCount
    +int DocumentCount
    +int ProjectCount
    +long StorageBytes
    +DateTime UpdatedAt
  }

  class WidgetConfig {
    +Guid Id
    +Guid ProjectId
    +string PrimaryColor
    +string BackgroundColor
    +string TextColor
    +string Position
    +string AvatarUrl
    +string Title
    +string Subtitle
    +string PlaceholderText
    +string AllowedOrigins
    +bool IsActive
    +DateTime CreatedAt
    +DateTime UpdatedAt
  }

  class User {
    +Guid Id
    +string Email
    +UserTier Tier
  }

  class TierLimits {
    +static GetLimits(UserTier tier) (int,int,int,long)
  }

  IBillingService <|.. StripeService
  IUsageTrackingService <|.. UsageTrackingService

  BillingController --> IBillingService
  BillingController --> IUsageTrackingService

  TierEnforcementMiddleware --> IUsageTrackingService

  StripeService --> Subscription
  StripeService --> User

  UsageTrackingService --> UsageRecord
  UsageTrackingService --> User
  UsageTrackingService --> Document
  UsageTrackingService --> Project

  WidgetController --> WidgetConfig
  WidgetController --> Project

  TierLimits ..> UsageTrackingService
  TierLimits ..> IUsageTrackingService
Loading

File-Level Changes

Change Details Files
Refactor pipeline builder UI into a block-based editor with block selection, inline configuration, and chunk previews.
  • Replaces stepper-style pipeline wizard with a two-column layout showing draggable blocks and a sticky configuration sidebar for the selected block.
  • Adds chunk preview panel driven by chunking settings and binds configuration controls for source, chunking, embedding, retrieval, and generation blocks.
  • Simplifies pipeline component TS, delegating more state management to the service and updating block summary formatting.
client/src/app/pages/pipeline/pipeline.html
client/src/app/pages/pipeline/pipeline.ts
client/src/app/features/pipeline-builder/pipeline-block.model.ts
Enhance pipeline service with step tracking, chunk preview generation, document loading, and pipeline execution.
  • Introduces PipelineStep type and step metadata/signals for current step, progress, and navigation.
  • Normalizes numeric/string configs when deriving chunk previews and pipeline configuration DTO.
  • Adds loadDocuments and runPipeline methods, tracks pipeline status and running/saving state, and integrates document fetch into pipeline load.
client/src/app/features/pipeline-builder/pipeline.service.ts
Introduce Stripe-based billing, subscription entities, and usage tracking with tier limits in the backend.
  • Adds Subscription, UsageRecord, and WidgetConfig entities to the EF Core model and configures their relationships and indexes in DbContext.
  • Implements StripeService as IBillingService to handle checkout/portal sessions, subscription webhooks, and subscription lookup.
  • Implements UsageTrackingService and IUsageTrackingService with tier limits, per-user daily usage records, and limit checks for queries, documents, projects, and storage.
src/PipeRAG.Infrastructure/Data/PipeRagDbContext.cs
src/PipeRAG.Infrastructure/Services/StripeService.cs
src/PipeRAG.Infrastructure/Services/UsageTrackingService.cs
src/PipeRAG.Core/Entities/Subscription.cs
src/PipeRAG.Core/Entities/UsageRecord.cs
src/PipeRAG.Core/Entities/WidgetConfig.cs
src/PipeRAG.Core/Enums/Enums.cs
src/PipeRAG.Core/Interfaces/IBillingService.cs
src/PipeRAG.Core/Interfaces/IUsageTrackingService.cs
src/PipeRAG.Core/DTOs/WidgetDtos.cs
src/PipeRAG.Core/Entities/User.cs
src/PipeRAG.Infrastructure/PipeRAG.Infrastructure.csproj
Add API surface for billing, usage, and widget configuration plus tier enforcement middleware.
  • Registers billing and usage services in Program and adds TierEnforcementMiddleware after auth to gate POSTs for chat, documents, and projects based on limits.
  • Adds BillingController for checkout/portal session creation, subscription/usage APIs, and Stripe webhook endpoint.
  • Adds WidgetController and DTOs to manage per-project chat widget configuration, scoped to project owner.
src/PipeRAG.Api/Program.cs
src/PipeRAG.Api/Controllers/BillingController.cs
src/PipeRAG.Api/Controllers/WidgetController.cs
src/PipeRAG.Api/Middleware/TierEnforcementMiddleware.cs
Expose billing and usage to the Angular client via a billing page, service, and navbar integration.
  • Adds /billing route, BillingComponent with current plan, usage dashboard, and plan comparison UI, and wire-up to BillingService.
  • Implements BillingService for subscription/usage fetch plus checkout/portal session creation, redirecting browser to Stripe URLs.
  • Displays Billing link in navbar and keeps showing user tier badge sourced from auth service.
client/src/app/app.routes.ts
client/src/app/pages/billing/billing.ts
client/src/app/core/services/billing.service.ts
client/src/app/shared/components/navbar/navbar.ts
Document billing and tier-enforcement behavior for PR #9.
  • Adds markdown doc summarizing tier limits, API endpoints, backend services, webhook events, and Stripe configuration keys required.
  • Describes how Free/Pro/Enterprise limits map to queries, documents, projects, and storage.
  • Links conceptual architecture of billing/usage services between backend and frontend.
docs/features/pr-009-billing-tier-enforcement.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-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.

Hey - I've found 6 issues, and left some high level feedback:

  • In PipelineComponent the new onConfigChangeNum/onConfigChangeFloat implementations always update the config with NaN on parse failures, whereas the previous versions guarded with isNaN; consider restoring the guards so invalid slider/number input doesn’t silently corrupt block config.
  • In StripeService.HandleSubscriptionUpdated, CurrentPeriodStart/CurrentPeriodEnd are set from stripeSub.Created and stripeSub.EndedAt; Stripe subscriptions expose CurrentPeriodStart/CurrentPeriodEnd explicitly, which are a better fit for billing cycles than creation/ended timestamps.
  • The TierEnforcementMiddleware currently only checks POST requests and matches paths against static prefixes like /api/chat and /api/documents; verify the actual API routes (e.g. project-scoped document uploads) align with these prefixes, otherwise some tier limits may never be enforced.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `PipelineComponent` the new `onConfigChangeNum`/`onConfigChangeFloat` implementations always update the config with `NaN` on parse failures, whereas the previous versions guarded with `isNaN`; consider restoring the guards so invalid slider/number input doesn’t silently corrupt block config.
- In `StripeService.HandleSubscriptionUpdated`, `CurrentPeriodStart`/`CurrentPeriodEnd` are set from `stripeSub.Created` and `stripeSub.EndedAt`; Stripe subscriptions expose `CurrentPeriodStart`/`CurrentPeriodEnd` explicitly, which are a better fit for billing cycles than creation/ended timestamps.
- The `TierEnforcementMiddleware` currently only checks `POST` requests and matches paths against static prefixes like `/api/chat` and `/api/documents`; verify the actual API routes (e.g. project-scoped document uploads) align with these prefixes, otherwise some tier limits may never be enforced.

## Individual Comments

### Comment 1
<location> `client/src/app/pages/pipeline/pipeline.ts:36-38` </location>
<code_context>
-    if (this.projectId) {
-      this.svc.runPipeline(this.projectId);
-    }
+  onConfigChange(blockId: string, key: string, event: Event): void {
+    const el = event.target as HTMLInputElement;
+    this.svc.updateBlockConfig(blockId, key, el.value);
+  }
+
</code_context>

<issue_to_address>
**issue (bug_risk):** Restore broader element typing and NaN-guards for config change handlers to avoid runtime issues.

The handlers now assume `event.target` is always an `HTMLInputElement` and unconditionally write `el.value` (or parsed numbers) into `block.config`. This drops support for `HTMLSelectElement` / `HTMLTextAreaElement` and removes the previous `NaN` checks in numeric handlers, so `parseInt` / `parseFloat` results of `NaN` will still be written to config and can break code that expects valid numbers.

Please restore the broader element union type on `onConfigChange` and reintroduce `isNaN` guards in the numeric handlers so config is only updated with valid values.
</issue_to_address>

### Comment 2
<location> `client/src/app/pages/pipeline/pipeline.ts:88-90` </location>
<code_context>
   }

   blockSummary(block: PipelineBlock): string {
+    const c = block.config;
     switch (block.type) {
-      case 'source': return `${block.config['sourceType']} · ${block.config['fileTypes']}`;
-      case 'chunking': return `${block.config['strategy']} · ${block.config['chunkSize']} tokens · ${block.config['chunkOverlap']} overlap`;
-      case 'embedding': return `${block.config['model']} · ${block.config['dimensions']}d`;
-      case 'retrieval': return `${block.config['strategy']} · top ${block.config['topK']} · threshold ${block.config['scoreThreshold']}`;
-      case 'generation': return `${block.config['model']} · temp ${block.config['temperature']}`;
+      case 'source': return c['sourceType'] + ' · ' + c['fileTypes'];
+      case 'chunking': return c['strategy'] + ' · ' + c['chunkSize'] + ' chars';
+      case 'embedding': return c['model'] + ' · ' + c['dimensions'] + 'd';
+      case 'retrieval': return c['strategy'] + ' · top ' + c['topK'];
</code_context>

<issue_to_address>
**suggestion:** Align `blockSummary` units and content with the underlying configuration semantics.

Two regressions in `blockSummary`:
- `chunking` now labels `chunkSize` as `'chars'`, but elsewhere it’s treated as tokens (e.g. `chunkPreviews`, UI sliders). This misalignment can confuse users about what they’re tuning.
- `retrieval` no longer surfaces `scoreThreshold`, reducing the value of the summary.
Please keep the unit consistent with the actual backend semantics (likely tokens) and re-include `scoreThreshold` in the retrieval summary (e.g. `strategy · top {topK} · threshold {scoreThreshold}`).

```suggestion
      case 'chunking': return c['strategy'] + ' · ' + c['chunkSize'] + ' tokens · ' + c['chunkOverlap'] + ' overlap';
      case 'embedding': return c['model'] + ' · ' + c['dimensions'] + 'd';
      case 'retrieval': return c['strategy'] + ' · top ' + c['topK'] + ' · threshold ' + c['scoreThreshold'];
```
</issue_to_address>

### Comment 3
<location> `src/PipeRAG.Infrastructure/Services/StripeService.cs:168-169` </location>
<code_context>
+            "trialing" => SubscriptionStatus.Trialing,
+            _ => SubscriptionStatus.Active
+        };
+        sub.CurrentPeriodStart = stripeSub.Created;
+        sub.CurrentPeriodEnd = stripeSub.EndedAt;
+        sub.UpdatedAt = DateTime.UtcNow;
+
</code_context>

<issue_to_address>
**issue (bug_risk):** Use Stripe’s current period properties instead of `Created`/`EndedAt` for subscription periods.

In `HandleSubscriptionUpdated`, mapping period fields this way will persist incorrect data. `Created` is the subscription creation time, not the current billing period start, and `EndedAt` is only populated once the subscription actually ends (so it will usually be null for active subs). Please use `stripeSub.CurrentPeriodStart` and `stripeSub.CurrentPeriodEnd` instead so your stored period values are correct for both active and canceled subscriptions.
</issue_to_address>

### Comment 4
<location> `src/PipeRAG.Api/Middleware/TierEnforcementMiddleware.cs:26-7` </location>
<code_context>
+        var path = context.Request.Path.Value?.ToLower() ?? "";
+        var method = context.Request.Method;
+
+        if (method != "POST" || !context.User.Identity?.IsAuthenticated == true)
+        {
+            await _next(context);
+            return;
</code_context>

<issue_to_address>
**nitpick:** Simplify and clarify the authentication guard expression in the middleware.

The expression `!context.User.Identity?.IsAuthenticated == true` is hard to parse and depends on nullable-bool negation. To improve readability and avoid nullability pitfalls, consider:
```csharp
if (!HttpMethods.IsPost(method) || context.User.Identity?.IsAuthenticated != true)
{
    await _next(context);
    return;
}
```
This preserves the behavior while making the control flow clearer.
</issue_to_address>

### Comment 5
<location> `src/PipeRAG.Api/Controllers/BillingController.cs:22` </location>
<code_context>
+        _usage = usage;
+    }
+
+    private Guid GetUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
+
+    [Authorize]
</code_context>

<issue_to_address>
**issue (bug_risk):** Guard against missing or malformed user ID claims to avoid 500s in billing endpoints.

`Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!)` will throw if the claim is missing or not a valid GUID, causing a 500 on billing requests.

Instead, consider:
- Using `Guid.TryParse` and returning `Unauthorized`/`Forbid` when parsing fails, or
- Calling a shared helper that already does robust user ID extraction and error handling.

That way billing endpoints fail gracefully on bad or unexpected tokens rather than erroring.
</issue_to_address>

### Comment 6
<location> `docs/features/pr-009-billing-tier-enforcement.md:41-44` </location>
<code_context>
+- `customer.subscription.deleted` — Downgrade to Free tier on cancellation
+
+## Configuration
+Required in `appsettings.json`:
+```json
+{
+  "Stripe": {
+    "SecretKey": "sk_...",
+    "WebhookSecret": "whsec_...",
</code_context>

<issue_to_address>
**🚨 suggestion (security):** Clarify that Stripe keys should be sourced from secure configuration rather than committed directly in appsettings.json.

Consider adding a brief note that `SecretKey`, `WebhookSecret`, and price IDs should be supplied via secure configuration (e.g., environment variables, user secrets, key vault) rather than committed in `appsettings.json`, to avoid accidental exposure of real Stripe secrets in source control.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +36 to +38
onConfigChange(blockId: string, key: string, event: Event): void {
const el = event.target as HTMLInputElement;
this.svc.updateBlockConfig(blockId, key, el.value);
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): Restore broader element typing and NaN-guards for config change handlers to avoid runtime issues.

The handlers now assume event.target is always an HTMLInputElement and unconditionally write el.value (or parsed numbers) into block.config. This drops support for HTMLSelectElement / HTMLTextAreaElement and removes the previous NaN checks in numeric handlers, so parseInt / parseFloat results of NaN will still be written to config and can break code that expects valid numbers.

Please restore the broader element union type on onConfigChange and reintroduce isNaN guards in the numeric handlers so config is only updated with valid values.

Comment on lines +88 to +90
case 'chunking': return c['strategy'] + ' · ' + c['chunkSize'] + ' chars';
case 'embedding': return c['model'] + ' · ' + c['dimensions'] + 'd';
case 'retrieval': return c['strategy'] + ' · top ' + c['topK'];
Copy link

Choose a reason for hiding this comment

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

suggestion: Align blockSummary units and content with the underlying configuration semantics.

Two regressions in blockSummary:

  • chunking now labels chunkSize as 'chars', but elsewhere it’s treated as tokens (e.g. chunkPreviews, UI sliders). This misalignment can confuse users about what they’re tuning.
  • retrieval no longer surfaces scoreThreshold, reducing the value of the summary.
    Please keep the unit consistent with the actual backend semantics (likely tokens) and re-include scoreThreshold in the retrieval summary (e.g. strategy · top {topK} · threshold {scoreThreshold}).
Suggested change
case 'chunking': return c['strategy'] + ' · ' + c['chunkSize'] + ' chars';
case 'embedding': return c['model'] + ' · ' + c['dimensions'] + 'd';
case 'retrieval': return c['strategy'] + ' · top ' + c['topK'];
case 'chunking': return c['strategy'] + ' · ' + c['chunkSize'] + ' tokens · ' + c['chunkOverlap'] + ' overlap';
case 'embedding': return c['model'] + ' · ' + c['dimensions'] + 'd';
case 'retrieval': return c['strategy'] + ' · top ' + c['topK'] + ' · threshold ' + c['scoreThreshold'];

Comment on lines +168 to +169
sub.CurrentPeriodStart = stripeSub.Created;
sub.CurrentPeriodEnd = stripeSub.EndedAt;
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): Use Stripe’s current period properties instead of Created/EndedAt for subscription periods.

In HandleSubscriptionUpdated, mapping period fields this way will persist incorrect data. Created is the subscription creation time, not the current billing period start, and EndedAt is only populated once the subscription actually ends (so it will usually be null for active subs). Please use stripeSub.CurrentPeriodStart and stripeSub.CurrentPeriodEnd instead so your stored period values are correct for both active and canceled subscriptions.

namespace PipeRAG.Api.Middleware;

public class TierEnforcementMiddleware
{
Copy link

Choose a reason for hiding this comment

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

nitpick: Simplify and clarify the authentication guard expression in the middleware.

The expression !context.User.Identity?.IsAuthenticated == true is hard to parse and depends on nullable-bool negation. To improve readability and avoid nullability pitfalls, consider:

if (!HttpMethods.IsPost(method) || context.User.Identity?.IsAuthenticated != true)
{
    await _next(context);
    return;
}

This preserves the behavior while making the control flow clearer.

_usage = usage;
}

private Guid GetUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): Guard against missing or malformed user ID claims to avoid 500s in billing endpoints.

Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!) will throw if the claim is missing or not a valid GUID, causing a 500 on billing requests.

Instead, consider:

  • Using Guid.TryParse and returning Unauthorized/Forbid when parsing fails, or
  • Calling a shared helper that already does robust user ID extraction and error handling.

That way billing endpoints fail gracefully on bad or unexpected tokens rather than erroring.

Comment on lines +41 to +44
Required in `appsettings.json`:
```json
{
"Stripe": {
Copy link

Choose a reason for hiding this comment

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

🚨 suggestion (security): Clarify that Stripe keys should be sourced from secure configuration rather than committed directly in appsettings.json.

Consider adding a brief note that SecretKey, WebhookSecret, and price IDs should be supplied via secure configuration (e.g., environment variables, user secrets, key vault) rather than committed in appsettings.json, to avoid accidental exposure of real Stripe secrets in source control.

@CIKR-Repos CIKR-Repos merged commit 448dc91 into main Feb 18, 2026
1 check passed
@CIKR-Repos CIKR-Repos deleted the feat/billing-tier-enforcement branch February 18, 2026 04:16
CIKR-Repos pushed a commit that referenced this pull request Feb 18, 2026
CIKR-Repos added a commit that referenced this pull request Feb 18, 2026
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