# Invoice System Optimization Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Remove the N+1 subscription invoice scanning, clean up the billing service layer, and simplify the InvoicesList component.

**Architecture:** Delete the subscription-to-invoice reverse lookup (BFF endpoint + frontend hook + scanning code). Replace with a simple "View all invoices" link on the subscription detail page. Remove the pass-through BillingOrchestrator and inject integration services directly into the controller.

**Tech Stack:** NestJS 11 (BFF), Next.js 15 / React 19 (Portal), Zod, React Query, Tailwind CSS

**Design doc:** `docs/plans/2026-03-05-invoice-optimization-design.md`

---

### Task 1: Remove subscription invoice endpoint from BFF controller

**Files:**

- Modify: `apps/bff/src/modules/subscriptions/subscriptions.controller.ts`

**Step 1: Edit the controller**

Remove the subscription invoice endpoint and all its related imports/DTOs. The file should become:

```typescript
import { Controller, Get, Param, Query, Request, Header } from "@nestjs/common";
import { SubscriptionsOrchestrator } from "./subscriptions-orchestrator.service.js";
import {
  subscriptionQuerySchema,
  subscriptionListSchema,
  subscriptionArraySchema,
  subscriptionIdParamSchema,
  subscriptionSchema,
  subscriptionStatsSchema,
} from "@customer-portal/domain/subscriptions";
import type {
  Subscription,
  SubscriptionList,
  SubscriptionStats,
} from "@customer-portal/domain/subscriptions";
import { createZodDto, ZodResponse } from "nestjs-zod";
import type { RequestWithUser } from "@bff/modules/auth/auth.types.js";
import { CACHE_CONTROL } from "@bff/core/constants/http.constants.js";

// DTOs
class SubscriptionQueryDto extends createZodDto(subscriptionQuerySchema) {}
class SubscriptionIdParamDto extends createZodDto(subscriptionIdParamSchema) {}

// Response DTOs
class SubscriptionListDto extends createZodDto(subscriptionListSchema) {}
class ActiveSubscriptionsDto extends createZodDto(subscriptionArraySchema) {}
class SubscriptionDto extends createZodDto(subscriptionSchema) {}
class SubscriptionStatsDto extends createZodDto(subscriptionStatsSchema) {}

/**
 * Subscriptions Controller - Core subscription endpoints
 *
 * Handles basic subscription listing and stats.
 * SIM-specific endpoints are in SimController (sim-management/sim.controller.ts)
 * Internet-specific endpoints are in InternetController (internet-management/internet.controller.ts)
 * Call/SMS history endpoints are in CallHistoryController (call-history/call-history.controller.ts)
 */
@Controller("subscriptions")
export class SubscriptionsController {
  constructor(private readonly subscriptionsOrchestrator: SubscriptionsOrchestrator) {}

  @Get()
  @Header("Cache-Control", CACHE_CONTROL.PRIVATE_5M)
  @ZodResponse({ description: "List subscriptions", type: SubscriptionListDto })
  async getSubscriptions(
    @Request() req: RequestWithUser,
    @Query() query: SubscriptionQueryDto
  ): Promise<SubscriptionList> {
    const { status } = query;
    return this.subscriptionsOrchestrator.getSubscriptions(
      req.user.id,
      status === undefined ? {} : { status }
    );
  }

  @Get("active")
  @Header("Cache-Control", CACHE_CONTROL.PRIVATE_5M)
  @ZodResponse({ description: "List active subscriptions", type: ActiveSubscriptionsDto })
  async getActiveSubscriptions(@Request() req: RequestWithUser): Promise<Subscription[]> {
    return this.subscriptionsOrchestrator.getActiveSubscriptions(req.user.id);
  }

  @Get("stats")
  @Header("Cache-Control", CACHE_CONTROL.PRIVATE_5M)
  @ZodResponse({ description: "Get subscription stats", type: SubscriptionStatsDto })
  async getSubscriptionStats(@Request() req: RequestWithUser): Promise<SubscriptionStats> {
    return this.subscriptionsOrchestrator.getSubscriptionStats(req.user.id);
  }

  @Get(":id")
  @Header("Cache-Control", CACHE_CONTROL.PRIVATE_5M)
  @ZodResponse({ description: "Get subscription", type: SubscriptionDto })
  async getSubscriptionById(
    @Request() req: RequestWithUser,
    @Param() params: SubscriptionIdParamDto
  ): Promise<Subscription> {
    return this.subscriptionsOrchestrator.getSubscriptionById(req.user.id, params.id);
  }
}
```

Removed:

- `subscriptionInvoiceQuerySchema` and `Validation` import
- `SubscriptionInvoiceQueryDto` and `InvoiceListDto` DTOs
- `invoiceListSchema` and `InvoiceList` type imports
- `getSubscriptionInvoices()` endpoint method

**Step 2: Run type-check**

Run: `pnpm type-check`
Expected: May show errors in orchestrator (expected — we fix that next)

**Step 3: Commit**

```bash
git add apps/bff/src/modules/subscriptions/subscriptions.controller.ts
git commit -m "refactor: remove subscription invoice endpoint from controller"
```

---

### Task 2: Remove subscription invoice methods from orchestrator

**Files:**

- Modify: `apps/bff/src/modules/subscriptions/subscriptions-orchestrator.service.ts`

**Step 1: Edit the orchestrator**

Remove all subscription-invoice-related code. Remove `WhmcsInvoiceService` dependency (only used for subscription invoices). Remove `InvoiceItem`, `InvoiceList`, `Invoice` type imports. Remove `WhmcsCacheService` if only used for subscription invoice caching.

The methods to remove are:

- `getSubscriptionInvoices()` (lines 300-349)
- `tryGetCachedInvoices()` (lines 351-376)
- `fetchAllRelatedInvoices()` (lines 378-411)
- `paginateInvoices()` (lines 413-425)
- `cacheInvoiceResults()` (lines 427-438)

The imports/constructor to update:

- Remove `import type { Invoice, InvoiceItem, InvoiceList } from "@customer-portal/domain/billing"`
- Remove `import { WhmcsInvoiceService }` line
- Remove `import { WhmcsCacheService }` line
- Remove `private readonly whmcsInvoiceService: WhmcsInvoiceService` from constructor
- Remove `private readonly cacheService: WhmcsCacheService` from constructor

The resulting file should keep: `getSubscriptions`, `getSubscriptionById`, `getActiveSubscriptions`, `getSubscriptionsByStatus`, `getSubscriptionStats`, `getExpiringSoon`, `getRecentActivity`, `searchSubscriptions`, `invalidateCache`, `healthCheck`.

**Step 2: Run type-check**

Run: `pnpm type-check`
Expected: PASS (or errors in cache service — fixed in next task)

**Step 3: Commit**

```bash
git add apps/bff/src/modules/subscriptions/subscriptions-orchestrator.service.ts
git commit -m "refactor: remove subscription invoice scanning from orchestrator"
```

---

### Task 3: Remove getInvoicesWithItems from WHMCS invoice service

**Files:**

- Modify: `apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts`

**Step 1: Edit the service**

Remove the `getInvoicesWithItems()` method (lines 102-170). Update the import on line 1 to remove `chunkArray` and `sleep` (only used by `getInvoicesWithItems`):

```typescript
// Before:
import { chunkArray, sleep, extractErrorMessage } from "@bff/core/utils/index.js";

// After:
import { extractErrorMessage } from "@bff/core/utils/index.js";
```

Keep everything else: `getInvoices()`, `getInvoiceById()`, `invalidateInvoiceCache()`, `transformInvoicesResponse()`, `createInvoice()`, `updateInvoice()`, `capturePayment()`, `refundPayment()`, `getUserFriendlyPaymentError()`.

**Step 2: Run type-check**

Run: `pnpm type-check`
Expected: PASS

**Step 3: Commit**

```bash
git add apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts
git commit -m "refactor: remove getInvoicesWithItems N+1 method"
```

---

### Task 4: Clean up subscription invoice cache methods

**Files:**

- Modify: `apps/bff/src/integrations/whmcs/cache/whmcs-cache.service.ts`

**Step 1: Remove subscription invoice cache configs**

Remove from `cacheConfigs` object (lines 45-54):

```typescript
    subscriptionInvoices: { ... },
    subscriptionInvoicesAll: { ... },
```

**Step 2: Remove subscription invoice cache methods**

Remove these methods:

- `getSubscriptionInvoices()` (lines 165-173)
- `setSubscriptionInvoices()` (lines 178-187)
- `getSubscriptionInvoicesAll()` (lines 192-198)
- `setSubscriptionInvoicesAll()` (lines 203-210)
- `buildSubscriptionInvoicesKey()` (lines 477-484)
- `buildSubscriptionInvoicesAllKey()` (lines 489-491)

**Step 3: Clean up invalidation methods**

In `invalidateUserCache()` (line 248), remove the `subscriptionInvoicesAll` pattern (line 255):

```typescript
// Remove this line:
`${this.cacheConfigs["subscriptionInvoicesAll"]?.prefix}:${userId}:*`,
```

In `invalidateInvoice()` (line 308), remove the `subscriptionInvoicesPattern` (lines 312, 317):

```typescript
// Before:
const subscriptionInvoicesPattern = `${this.cacheConfigs["subscriptionInvoicesAll"]?.prefix}:${userId}:*`;

await Promise.all([
  this.cacheService.del(specificKey),
  this.cacheService.delPattern(listPattern),
  this.cacheService.delPattern(subscriptionInvoicesPattern),
]);

// After:
await Promise.all([this.cacheService.del(specificKey), this.cacheService.delPattern(listPattern)]);
```

In `invalidateSubscription()` (line 332), remove the `invoicesKey` (lines 336, 341):

```typescript
// Before:
const invoicesKey = this.buildSubscriptionInvoicesAllKey(userId, subscriptionId);
await Promise.all([
  this.cacheService.del(specificKey),
  this.cacheService.del(listKey),
  this.cacheService.del(invoicesKey),
]);

// After:
await Promise.all([this.cacheService.del(specificKey), this.cacheService.del(listKey)]);
```

Also clean up unused type imports if `InvoiceList` is no longer needed (check if `getInvoicesList`/`setInvoicesList` still use it — they do, so keep `InvoiceList` and `Invoice` imports).

**Step 4: Run type-check**

Run: `pnpm type-check`
Expected: PASS

**Step 5: Commit**

```bash
git add apps/bff/src/integrations/whmcs/cache/whmcs-cache.service.ts
git commit -m "refactor: remove subscription invoice cache methods"
```

---

### Task 5: Remove BillingOrchestrator and wire controller directly

**Files:**

- Delete: `apps/bff/src/modules/billing/services/billing-orchestrator.service.ts`
- Modify: `apps/bff/src/modules/billing/billing.controller.ts`
- Modify: `apps/bff/src/modules/billing/billing.module.ts`

**Step 1: Update billing controller**

Replace `BillingOrchestrator` import and injection with direct service imports. The full file becomes:

```typescript
import { Controller, Get, Post, Param, Query, Request, HttpCode, HttpStatus } from "@nestjs/common";
import { InvoiceRetrievalService } from "./services/invoice-retrieval.service.js";
import { WhmcsPaymentService } from "@bff/integrations/whmcs/services/whmcs-payment.service.js";
import { WhmcsSsoService } from "@bff/integrations/whmcs/services/whmcs-sso.service.js";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { createZodDto, ZodResponse } from "nestjs-zod";
import type { RequestWithUser } from "@bff/modules/auth/auth.types.js";

import type { Invoice, InvoiceList, InvoiceSsoLink } from "@customer-portal/domain/billing";
import {
  invoiceIdParamSchema,
  invoiceListQuerySchema,
  invoiceListSchema,
  invoiceSchema,
  invoiceSsoLinkSchema,
  invoiceSsoQuerySchema,
} from "@customer-portal/domain/billing";
import type { PaymentMethodList } from "@customer-portal/domain/payments";
import { paymentMethodListSchema } from "@customer-portal/domain/payments";

class InvoiceListQueryDto extends createZodDto(invoiceListQuerySchema) {}
class InvoiceIdParamDto extends createZodDto(invoiceIdParamSchema) {}
class InvoiceListDto extends createZodDto(invoiceListSchema) {}
class InvoiceDto extends createZodDto(invoiceSchema) {}
class InvoiceSsoLinkDto extends createZodDto(invoiceSsoLinkSchema) {}
class InvoiceSsoQueryDto extends createZodDto(invoiceSsoQuerySchema) {}
class PaymentMethodListDto extends createZodDto(paymentMethodListSchema) {}

/**
 * Billing Controller
 *
 * All request validation is handled by Zod schemas via global ZodValidationPipe.
 * Business logic is delegated to service layer.
 */
@Controller("invoices")
export class BillingController {
  constructor(
    private readonly invoicesService: InvoiceRetrievalService,
    private readonly paymentService: WhmcsPaymentService,
    private readonly ssoService: WhmcsSsoService,
    private readonly mappingsService: MappingsService
  ) {}

  @Get()
  @ZodResponse({ description: "List invoices", type: InvoiceListDto })
  async getInvoices(
    @Request() req: RequestWithUser,
    @Query() query: InvoiceListQueryDto
  ): Promise<InvoiceList> {
    return this.invoicesService.getInvoices(req.user.id, query);
  }

  @Get("payment-methods")
  @ZodResponse({ description: "List payment methods", type: PaymentMethodListDto })
  async getPaymentMethods(@Request() req: RequestWithUser): Promise<PaymentMethodList> {
    const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(req.user.id);
    return this.paymentService.getPaymentMethods(whmcsClientId, req.user.id);
  }

  @Post("payment-methods/refresh")
  @HttpCode(HttpStatus.OK)
  @ZodResponse({ description: "Refresh payment methods", type: PaymentMethodListDto })
  async refreshPaymentMethods(@Request() req: RequestWithUser): Promise<PaymentMethodList> {
    await this.paymentService.invalidatePaymentMethodsCache(req.user.id);
    const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(req.user.id);
    return this.paymentService.getPaymentMethods(whmcsClientId, req.user.id);
  }

  @Get(":id")
  @ZodResponse({ description: "Get invoice by id", type: InvoiceDto })
  async getInvoiceById(
    @Request() req: RequestWithUser,
    @Param() params: InvoiceIdParamDto
  ): Promise<Invoice> {
    return this.invoicesService.getInvoiceById(req.user.id, params.id);
  }

  @Post(":id/sso-link")
  @HttpCode(HttpStatus.OK)
  @ZodResponse({ description: "Create invoice SSO link", type: InvoiceSsoLinkDto })
  async createSsoLink(
    @Request() req: RequestWithUser,
    @Param() params: InvoiceIdParamDto,
    @Query() query: InvoiceSsoQueryDto
  ): Promise<InvoiceSsoLink> {
    const whmcsClientId = await this.mappingsService.getWhmcsClientIdOrThrow(req.user.id);
    const ssoUrl = await this.ssoService.whmcsSsoForInvoice(whmcsClientId, params.id, query.target);
    return {
      url: ssoUrl,
      expiresAt: new Date(Date.now() + 60000).toISOString(),
    };
  }
}
```

**Step 2: Update billing module**

```typescript
import { Module } from "@nestjs/common";
import { BillingController } from "./billing.controller.js";
import { WhmcsModule } from "@bff/integrations/whmcs/whmcs.module.js";
import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
import { InvoiceRetrievalService } from "./services/invoice-retrieval.service.js";

@Module({
  imports: [WhmcsModule, MappingsModule],
  controllers: [BillingController],
  providers: [InvoiceRetrievalService],
  exports: [InvoiceRetrievalService],
})
export class BillingModule {}
```

**Step 3: Delete the orchestrator file**

Delete: `apps/bff/src/modules/billing/services/billing-orchestrator.service.ts`

**Step 4: Run type-check**

Run: `pnpm type-check`
Expected: PASS

**Step 5: Commit**

```bash
git add apps/bff/src/modules/billing/billing.controller.ts apps/bff/src/modules/billing/billing.module.ts
git rm apps/bff/src/modules/billing/services/billing-orchestrator.service.ts
git commit -m "refactor: remove BillingOrchestrator pass-through, inject services directly"
```

---

### Task 6: Simplify InvoicesList component (remove dual-mode)

**Files:**

- Modify: `apps/portal/src/features/billing/components/InvoiceList/InvoiceList.tsx`

**Step 1: Rewrite the component**

Remove all subscription-mode logic. The simplified component:

```tsx
"use client";

import React, { useMemo, useState } from "react";
import {
  DocumentTextIcon,
  CheckCircleIcon,
  ExclamationTriangleIcon,
  ClockIcon,
} from "@heroicons/react/24/outline";
import { Spinner } from "@/components/atoms";
import { SummaryStats, ClearFiltersButton } from "@/components/molecules";
import type { StatItem } from "@/components/molecules/SummaryStats";
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
import { PaginationBar } from "@/components/molecules/PaginationBar/PaginationBar";
import { SearchFilterBar } from "@/components/molecules/SearchFilterBar/SearchFilterBar";
import { InvoiceTable } from "@/features/billing/components/InvoiceTable/InvoiceTable";
import { useInvoices } from "@/features/billing/hooks/useBilling";
import {
  VALID_INVOICE_QUERY_STATUSES,
  type Invoice,
  type InvoiceQueryStatus,
} from "@customer-portal/domain/billing";
import { cn } from "@/shared/utils";

interface InvoicesListProps {
  pageSize?: number;
  compact?: boolean;
  className?: string;
}

const INVOICE_STATUS_OPTIONS = [
  { value: "all", label: "All Statuses" },
  ...VALID_INVOICE_QUERY_STATUSES.map(status => ({
    value: status,
    label: status,
  })),
];

function computeInvoiceStats(invoices: Invoice[]) {
  const result = { total: 0, paid: 0, unpaid: 0, overdue: 0 };
  for (const invoice of invoices) {
    result.total++;
    if (invoice.status === "Paid") result.paid++;
    else if (invoice.status === "Unpaid") result.unpaid++;
    else if (invoice.status === "Overdue") result.overdue++;
  }
  return result;
}

function buildSummaryStatsItems(
  stats: ReturnType<typeof computeInvoiceStats>,
  totalItems?: number
): StatItem[] {
  return [
    {
      icon: <DocumentTextIcon className="h-4 w-4" />,
      label: "Total",
      value: totalItems ?? stats.total,
      tone: "muted",
    },
    {
      icon: <CheckCircleIcon className="h-4 w-4" />,
      label: "Paid",
      value: stats.paid,
      tone: "success",
    },
    {
      icon: <ClockIcon className="h-4 w-4" />,
      label: "Unpaid",
      value: stats.unpaid,
      tone: "warning",
      show: stats.unpaid > 0,
    },
    {
      icon: <ExclamationTriangleIcon className="h-4 w-4" />,
      label: "Overdue",
      value: stats.overdue,
      tone: "warning",
      show: stats.overdue > 0,
    },
  ];
}

function InvoicesListPending() {
  return (
    <div className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden">
      <div className="flex items-center justify-center py-16">
        <Spinner size="lg" className="text-primary" />
      </div>
    </div>
  );
}

function InvoicesListError({ error }: { error: unknown }) {
  return (
    <div className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden">
      <AsyncBlock isLoading={false} error={error}>
        <div />
      </AsyncBlock>
    </div>
  );
}

function useInvoiceListState(props: InvoicesListProps) {
  const { pageSize = 10 } = props;
  const [searchTerm, setSearchTerm] = useState("");
  const [statusFilter, setStatusFilter] = useState<InvoiceQueryStatus | "all">("all");
  const [currentPage, setCurrentPage] = useState(1);

  const { data, isLoading, isPending, error } = useInvoices({
    page: currentPage,
    limit: pageSize,
    status: statusFilter === "all" ? undefined : statusFilter,
  });

  const rawInvoices = data?.invoices;
  const invoices = useMemo(() => rawInvoices ?? [], [rawInvoices]);
  const pagination = data?.pagination;

  const filtered = useMemo(() => {
    if (!searchTerm) return invoices;
    const term = searchTerm.toLowerCase();
    return invoices.filter(
      inv =>
        inv.number.toLowerCase().includes(term) ||
        (inv.description ? inv.description.toLowerCase().includes(term) : false)
    );
  }, [invoices, searchTerm]);

  const stats = useMemo(() => computeInvoiceStats(invoices), [invoices]);
  const summaryStatsItems = useMemo(
    () => buildSummaryStatsItems(stats, pagination?.totalItems),
    [pagination?.totalItems, stats]
  );

  return {
    isLoading,
    isPending,
    error,
    invoices,
    filtered,
    pagination,
    summaryStatsItems,
    searchTerm,
    setSearchTerm,
    statusFilter,
    setStatusFilter,
    currentPage,
    setCurrentPage,
    hasActiveFilters: searchTerm.trim() !== "" || statusFilter !== "all",
  };
}

export function InvoicesList(props: InvoicesListProps) {
  const { compact = false, className } = props;
  const state = useInvoiceListState(props);

  if (state.isPending) return <InvoicesListPending />;
  if (state.error) return <InvoicesListError error={state.error} />;

  const clearFilters = () => {
    state.setSearchTerm("");
    state.setStatusFilter("all");
    state.setCurrentPage(1);
  };

  return (
    <div className={cn("space-y-4", className)}>
      {state.invoices.length > 0 && (
        <SummaryStats variant="inline" items={state.summaryStatsItems} />
      )}

      <SearchFilterBar
        searchValue={state.searchTerm}
        onSearchChange={state.setSearchTerm}
        searchPlaceholder="Search by invoice number..."
        filterValue={state.statusFilter}
        onFilterChange={(value: string) => {
          state.setStatusFilter(value as InvoiceQueryStatus | "all");
          state.setCurrentPage(1);
        }}
        filterOptions={INVOICE_STATUS_OPTIONS}
        filterLabel="Filter by status"
      >
        <ClearFiltersButton onClick={clearFilters} show={state.hasActiveFilters} />
      </SearchFilterBar>

      <div className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden">
        <InvoiceTable
          invoices={state.filtered}
          loading={state.isLoading}
          compact={compact}
          className="border-0 rounded-none shadow-none"
        />
        {state.pagination && state.filtered.length > 0 && (
          <div className="border-t border-border px-6 py-4">
            <PaginationBar
              currentPage={state.currentPage}
              pageSize={props.pageSize ?? 10}
              totalItems={state.pagination.totalItems || 0}
              onPageChange={state.setCurrentPage}
            />
          </div>
        )}
      </div>
    </div>
  );
}

export type { InvoicesListProps };
```

**Step 2: Run type-check**

Run: `pnpm type-check`
Expected: Errors in SubscriptionDetail.tsx (expected — fixed next task)

**Step 3: Commit**

```bash
git add apps/portal/src/features/billing/components/InvoiceList/InvoiceList.tsx
git commit -m "refactor: simplify InvoicesList to single-purpose component"
```

---

### Task 7: Update subscription detail page

**Files:**

- Modify: `apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx`
- Modify: `apps/portal/src/app/account/subscriptions/[id]/loading.tsx`

**Step 1: Update SubscriptionDetail.tsx**

In the imports section, remove:

```typescript
// Remove these imports:
import { InvoicesList } from "@/features/billing/components/InvoiceList/InvoiceList";
import { InvoiceListSkeleton } from "@/components/atoms/loading-skeleton";
```

Add `CreditCardIcon` to the heroicons import:

```typescript
import {
  ServerIcon,
  CalendarIcon,
  DocumentTextIcon,
  XCircleIcon,
  CreditCardIcon,
} from "@heroicons/react/24/outline";
```

In `SubscriptionDetailContent` (around line 202-207), replace the billing history section:

```tsx
// Before:
{
  activeTab === "overview" && (
    <div className="space-y-4">
      <h3 className="text-lg font-semibold text-foreground">Billing History</h3>
      <InvoicesList subscriptionId={subscriptionId} pageSize={5} showFilters={false} />
    </div>
  );
}

// After:
{
  activeTab === "overview" && (
    <div className="bg-card border border-border rounded-xl shadow-[var(--cp-shadow-1)] p-6">
      <div className="flex items-center justify-between">
        <div className="flex items-center gap-3">
          <CreditCardIcon className="h-5 w-5 text-muted-foreground" />
          <h3 className="text-lg font-semibold text-foreground">Billing</h3>
        </div>
        <Link
          href="/account/billing/invoices"
          className="text-sm font-medium text-primary hover:text-primary/80 transition-colors"
        >
          View all invoices &rarr;
        </Link>
      </div>
      <p className="mt-2 text-sm text-muted-foreground">
        Invoices and payment history are available on the billing page.
      </p>
    </div>
  );
}
```

In the loading state (around line 230-234), replace `InvoiceListSkeleton`:

```tsx
// Before:
<div className="space-y-6">
  <SubscriptionDetailStatsSkeleton />
  <InvoiceListSkeleton rows={5} />
</div>

// After:
<div className="space-y-6">
  <SubscriptionDetailStatsSkeleton />
</div>
```

**Step 2: Update the loading.tsx file**

Replace `apps/portal/src/app/account/subscriptions/[id]/loading.tsx` with:

```tsx
import { RouteLoading } from "@/components/molecules/RouteLoading";
import { Server } from "lucide-react";
import { SubscriptionDetailStatsSkeleton } from "@/components/atoms/loading-skeleton";

export default function SubscriptionDetailLoading() {
  return (
    <RouteLoading
      icon={<Server />}
      title="Subscription"
      description="Loading subscription details..."
      mode="content"
    >
      <div className="space-y-6">
        <SubscriptionDetailStatsSkeleton />
      </div>
    </RouteLoading>
  );
}
```

**Step 3: Run type-check**

Run: `pnpm type-check`
Expected: PASS

**Step 4: Commit**

```bash
git add apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx apps/portal/src/app/account/subscriptions/[id]/loading.tsx
git commit -m "refactor: replace subscription invoice list with billing link"
```

---

### Task 8: Remove useSubscriptionInvoices hook and query key

**Files:**

- Modify: `apps/portal/src/features/subscriptions/hooks/useSubscriptions.ts`
- Modify: `apps/portal/src/core/api/index.ts`

**Step 1: Remove useSubscriptionInvoices from hooks**

Remove lines 94-117 (the `useSubscriptionInvoices` function) and the `InvoiceList` type import (line 9):

```typescript
// Remove this import:
import type { InvoiceList } from "@customer-portal/domain/billing";

// Remove the entire useSubscriptionInvoices function (lines 94-117)
```

Keep all other hooks: `useSubscriptions`, `useActiveSubscriptions`, `useSubscriptionStats`, `useSubscription`.

**Step 2: Remove query key**

In `apps/portal/src/core/api/index.ts`, remove lines 110-111 from the `subscriptions` query keys:

```typescript
// Remove:
    invoices: (id: number, params?: Record<string, unknown>) =>
      ["subscriptions", "invoices", id, params] as const,
```

The `subscriptions` key object becomes:

```typescript
  subscriptions: {
    all: () => ["subscriptions"] as const,
    list: (params?: Record<string, unknown>) => ["subscriptions", "list", params] as const,
    active: () => ["subscriptions", "active"] as const,
    stats: () => ["subscriptions", "stats"] as const,
    detail: (id: string) => ["subscriptions", "detail", id] as const,
  },
```

**Step 3: Run type-check**

Run: `pnpm type-check`
Expected: PASS

**Step 4: Run lint**

Run: `pnpm lint`
Expected: PASS

**Step 5: Commit**

```bash
git add apps/portal/src/features/subscriptions/hooks/useSubscriptions.ts apps/portal/src/core/api/index.ts
git commit -m "refactor: remove subscription invoices hook and query key"
```

---

### Task 9: Final verification

**Step 1: Run full type-check**

Run: `pnpm type-check`
Expected: PASS with zero errors

**Step 2: Run lint**

Run: `pnpm lint`
Expected: PASS

**Step 3: Run tests**

Run: `pnpm test`
Expected: PASS (any tests referencing subscription invoices should be updated or removed)

**Step 4: Verify no dangling references**

Search for any remaining references to removed code:

```bash
# Should return no results:
pnpm exec grep -r "useSubscriptionInvoices\|getSubscriptionInvoices\|getInvoicesWithItems\|BillingOrchestrator\|subscriptionInvoices" --include="*.ts" --include="*.tsx" apps/ packages/ --exclude-dir=node_modules
```

If any results found, clean them up.

**Step 5: Commit any cleanup**

```bash
git add -A
git commit -m "refactor: final cleanup after invoice optimization"
```
