# Recommended Portal Structure: `lib/` vs `features/`

## 🎯 Core Principle: Co-location

**Feature-specific code belongs WITH the feature, not in a centralized `lib/` folder.**

---

## 📂 Proposed Structure

```
apps/portal/src/
├── lib/                          # ✅ ONLY truly generic utilities
│   ├── api/
│   │   ├── client.ts             # API client instance & configuration
│   │   ├── query-keys.ts         # React Query key factory
│   │   ├── helpers.ts            # getDataOrThrow, getDataOrDefault
│   │   └── index.ts              # Barrel export
│   │
│   ├── utils/
│   │   ├── cn.ts                 # Tailwind className utility (used everywhere)
│   │   ├── error-handling.ts    # Generic error handling (used everywhere)
│   │   └── index.ts
│   │
│   ├── providers.tsx             # Root-level React context providers
│   └── index.ts                  # Main barrel export
│
└── features/                     # ✅ Feature-specific code lives here
    ├── billing/
    │   ├── components/
    │   │   ├── InvoiceList.tsx
    │   │   └── InvoiceCard.tsx
    │   ├── hooks/
    │   │   └── useBilling.ts     # ← Feature-specific hooks HERE
    │   ├── utils/
    │   │   └── invoice-helpers.ts # ← Feature-specific utilities HERE
    │   └── index.ts
    │
    ├── subscriptions/
    │   ├── components/
    │   ├── hooks/
    │   │   └── useSubscriptions.ts
    │   └── index.ts
    │
    ├── orders/
    │   ├── components/
    │   ├── hooks/
    │   │   └── useOrders.ts
    │   └── index.ts
    │
    └── auth/
        ├── components/
        ├── hooks/
        │   └── useAuth.ts
        ├── services/
        │   └── auth.store.ts
        └── index.ts
```

---

## 🎨 What Goes Where?

### ✅ `lib/` - Truly Generic, Reusable Across Features

| File | Purpose | Used By |
|------|---------|---------|
| `lib/api/client.ts` | API client instance | All features |
| `lib/api/query-keys.ts` | React Query keys factory | All features |
| `lib/api/helpers.ts` | `getDataOrThrow`, `getDataOrDefault` | All features |
| `lib/utils/cn.ts` | Tailwind className merger | All components |
| `lib/utils/error-handling.ts` | Generic error parsing | All features |
| `lib/providers.tsx` | Root providers (QueryClient, Theme) | App root |

### ✅ `features/*/hooks/` - Feature-Specific Hooks

| File | Purpose | Used By |
|------|---------|---------|
| `features/billing/hooks/useBilling.ts` | Invoice queries & mutations | Billing pages only |
| `features/subscriptions/hooks/useSubscriptions.ts` | Subscription queries | Subscription pages only |
| `features/orders/hooks/useOrders.ts` | Order queries | Order pages only |
| `features/auth/hooks/useAuth.ts` | Auth state & actions | Auth-related components |

---

## ❌ Anti-Pattern: Centralized Feature Hooks

```
lib/
├── hooks/
│   ├── use-billing.ts        # ❌ BAD - billing-specific
│   ├── use-subscriptions.ts  # ❌ BAD - subscriptions-specific
│   └── use-orders.ts         # ❌ BAD - orders-specific
```

**Why this is bad:**
- Hard to find (is it in `lib` or `features`?)
- Breaks feature encapsulation
- Harder to delete features
- Makes the `lib` folder bloated

---

## 📝 Implementation

### 1. **`lib/api/index.ts`** - Clean API Exports

```typescript
/**
 * API Client Utilities
 * Central export for all API-related functionality
 */

export { apiClient } from "./client";
export { queryKeys } from "./query-keys";
export { getDataOrThrow, getDataOrDefault, isApiError } from "./helpers";

// Re-export common types from generated client
export type { QueryParams, PathParams } from "./runtime/client";
```

### 2. **`lib/api/helpers.ts`** - API Helper Functions

```typescript
import type { ApiResponse } from "@customer-portal/domain/common";

export function getDataOrThrow<T>(
  response: { data?: T; error?: unknown },
  errorMessage: string
): T {
  if (response.error || !response.data) {
    throw new Error(errorMessage);
  }
  return response.data;
}

export function getDataOrDefault<T>(
  response: { data?: T; error?: unknown },
  defaultValue: T
): T {
  return response.data ?? defaultValue;
}

export function isApiError(error: unknown): error is Error {
  return error instanceof Error;
}
```

### 3. **`features/billing/hooks/useBilling.ts`** - Clean Hook Implementation

```typescript
"use client";

import { useQuery, useMutation } from "@tanstack/react-query";
import { apiClient, queryKeys, getDataOrThrow, getDataOrDefault } from "@/lib/api";
import type { QueryParams } from "@/lib/api";

// ✅ Single consolidated import from domain
import {
  // Types
  type Invoice,
  type InvoiceList,
  type InvoiceQueryParams,
  type InvoiceSsoLink,
  type PaymentMethodList,
  // Schemas
  invoiceSchema,
  invoiceListSchema,
  // Constants
  INVOICE_STATUS,
  type InvoiceStatus,
} from "@customer-portal/domain/billing";

// Constants
const EMPTY_INVOICE_LIST: InvoiceList = {
  invoices: [],
  pagination: { page: 1, totalItems: 0, totalPages: 0 },
};

const EMPTY_PAYMENT_METHODS: PaymentMethodList = {
  paymentMethods: [],
  totalCount: 0,
};

// Helper functions
function ensureInvoiceStatus(invoice: Invoice): Invoice {
  return {
    ...invoice,
    status: (invoice.status as InvoiceStatus) ?? INVOICE_STATUS.DRAFT,
  };
}

function normalizeInvoiceList(list: InvoiceList): InvoiceList {
  return {
    ...list,
    invoices: list.invoices.map(ensureInvoiceStatus),
    pagination: {
      page: list.pagination?.page ?? 1,
      totalItems: list.pagination?.totalItems ?? 0,
      totalPages: list.pagination?.totalPages ?? 0,
      nextCursor: list.pagination?.nextCursor,
    },
  };
}

function toQueryParams(params: InvoiceQueryParams): QueryParams {
  return Object.entries(params).reduce((acc, [key, value]) => {
    if (value !== undefined) {
      acc[key] = value;
    }
    return acc;
  }, {} as QueryParams);
}

// API functions (keep as internal implementation details)
async function fetchInvoices(params?: InvoiceQueryParams): Promise<InvoiceList> {
  const query = params ? toQueryParams(params) : undefined;
  const response = await apiClient.GET<InvoiceList>(
    "/api/invoices",
    query ? { params: { query } } : undefined
  );
  const data = getDataOrDefault(response, EMPTY_INVOICE_LIST);
  const parsed = invoiceListSchema.parse(data);
  return normalizeInvoiceList(parsed);
}

async function fetchInvoice(id: string): Promise<Invoice> {
  const response = await apiClient.GET<Invoice>("/api/invoices/{id}", {
    params: { path: { id } },
  });
  const invoice = getDataOrThrow(response, "Invoice not found");
  const parsed = invoiceSchema.parse(invoice);
  return ensureInvoiceStatus(parsed);
}

async function fetchPaymentMethods(): Promise<PaymentMethodList> {
  const response = await apiClient.GET<PaymentMethodList>("/api/invoices/payment-methods");
  return getDataOrDefault(response, EMPTY_PAYMENT_METHODS);
}

// Exported hooks
export function useInvoices(params?: InvoiceQueryParams) {
  return useQuery({
    queryKey: queryKeys.billing.invoices(params),
    queryFn: () => fetchInvoices(params),
  });
}

export function useInvoice(id: string, enabled = true) {
  return useQuery({
    queryKey: queryKeys.billing.invoice(id),
    queryFn: () => fetchInvoice(id),
    enabled: Boolean(id) && enabled,
  });
}

export function usePaymentMethods() {
  return useQuery({
    queryKey: queryKeys.billing.paymentMethods(),
    queryFn: fetchPaymentMethods,
  });
}

export function useCreateInvoiceSsoLink() {
  return useMutation({
    mutationFn: async ({ 
      invoiceId, 
      target 
    }: { 
      invoiceId: number; 
      target?: "view" | "download" | "pay" 
    }) => {
      const response = await apiClient.POST<InvoiceSsoLink>("/api/invoices/{id}/sso-link", {
        params: {
          path: { id: invoiceId },
          query: target ? { target } : undefined,
        },
      });
      return getDataOrThrow(response, "Failed to create SSO link");
    },
  });
}

export function useCreatePaymentMethodsSsoLink() {
  return useMutation({
    mutationFn: async () => {
      const response = await apiClient.POST<InvoiceSsoLink>("/auth/sso-link", {
        body: { destination: "index.php?rp=/account/paymentmethods" },
      });
      return getDataOrThrow(response, "Failed to create payment methods SSO link");
    },
  });
}
```

### 4. **`features/billing/index.ts`** - Feature Barrel Export

```typescript
/**
 * Billing Feature Exports
 */

// Hooks
export * from "./hooks/useBilling";

// Components (if you want to export them)
export * from "./components/InvoiceList";
export * from "./components/InvoiceCard";
```

### 5. **`lib/index.ts`** - Generic Utilities Only

```typescript
/**
 * Portal Library
 * ONLY generic utilities used across features
 */

// API utilities (used by all features)
export * from "./api";

// Generic utilities (used by all features)
export * from "./utils";

// NOTE: Feature-specific hooks are NOT exported here
// Import them from their respective features:
// import { useInvoices } from "@/features/billing";
```

---

## 🎯 Usage After Restructure

### In Billing Pages/Components

```typescript
// ✅ Import from feature
import { useInvoices, useInvoice, usePaymentMethods } from "@/features/billing";

// Or direct import
import { useInvoices } from "@/features/billing/hooks/useBilling";

function InvoicesPage() {
  const { data: invoices, isLoading } = useInvoices({ status: "Unpaid" });
  
  // ...
}
```

### In Feature Hooks (within features/billing/hooks/)

```typescript
// ✅ Import generic utilities from lib
import { apiClient, queryKeys, getDataOrThrow } from "@/lib/api";

// ✅ Import domain types
import { 
  type Invoice,
  type InvoiceList,
  invoiceSchema,
  type InvoiceQueryParams 
} from "@customer-portal/domain/billing";

export function useInvoices(params?: InvoiceQueryParams) {
  // ...
}
```

### In Other Feature Hooks (features/subscriptions/hooks/)

```typescript
// ✅ Generic utilities from lib
import { apiClient, queryKeys, getDataOrThrow } from "@/lib/api";

// ✅ Domain types for subscriptions
import { 
  type Subscription, 
  subscriptionSchema,
  type SubscriptionQueryParams 
} from "@customer-portal/domain/subscriptions";

export function useSubscriptions(params?: SubscriptionQueryParams) {
  // ...
}
```

---

## 📋 Benefits of This Structure

### 1. **Clear Separation of Concerns**
- `api/` - HTTP client & infrastructure
- `hooks/` - React Query abstractions
- `utils/` - Helper functions

### 2. **Clean Imports**
```typescript
// ❌ Before: Messy
import { apiClient, queryKeys, getDataOrDefault, getDataOrThrow } from "@/lib/api";
import type { QueryParams } from "@/lib/api/runtime/client";
import { Invoice, InvoiceList } from "@customer-portal/domain/billing";
import { invoiceSchema } from "@customer-portal/domain/validation/shared/entities";
import { INVOICE_STATUS } from "@customer-portal/domain/billing";

// ✅ After: Clean
import { apiClient, queryKeys, getDataOrThrow, type QueryParams } from "@/lib/api";
import {
  type Invoice,
  type InvoiceList,
  invoiceSchema,
  INVOICE_STATUS,
} from "@customer-portal/domain/billing";
```

### 3. **Easy to Find Things**
- Need a query hook? → `lib/hooks/queries/`
- Need API utilities? → `lib/api/`
- Need to update a domain type? → `packages/domain/billing/`

### 4. **Testable**
Each piece can be tested independently:
- API helpers are pure functions
- Hooks can be tested with React Testing Library
- Domain logic is already in domain package

---

## 🔧 What About That Weird Validation Import?

```typescript
// ❌ This should NOT exist
import { invoiceSchema } from "@customer-portal/domain/validation/shared/entities";
```

This path suggests you might have old validation code. The schema should be:

```typescript
// ✅ Correct
import { invoiceSchema } from "@customer-portal/domain/billing";
```

Let me check if this old validation path exists and needs cleanup.

