# @isa/crm/data-access

A comprehensive Customer Relationship Management (CRM) data access library for Angular applications providing customer, shipping address, payer, and bonus card management with reactive data loading using Angular resources.

## Overview

The CRM Data Access library provides a unified interface for managing customer data, shipping addresses, payers, bonus cards, and country information. It integrates with the generated CRM API client and provides intelligent data loading through Angular resources, tab-based state management, Zod schema validation, and type-safe transformations. The library follows modern Angular patterns with signals, resources, and dependency injection.

## Table of Contents

- [Features](#features)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Resource Pattern](#resource-pattern)
- [Tab Metadata Management](#tab-metadata-management)
- [Validation with Zod](#validation-with-zod)
- [Error Handling](#error-handling)
- [Testing](#testing)
- [Architecture Notes](#architecture-notes)

## Features

- **Customer management** - Fetch customer data with configurable eager loading
- **Shipping address operations** - Retrieve customer shipping addresses with pagination
- **Bonus card support** - Manage customer bonus cards with primary card selection
- **Payer information** - Handle assigned payers and payment settings
- **Country data** - Cached country information retrieval
- **Angular resources** - Reactive data loading with automatic cancellation
- **Tab-based state** - Integration with tab service for context-aware data loading
- **Zod validation** - Runtime schema validation for all parameters
- **Request cancellation** - AbortSignal support for all operations
- **Type-safe transformations** - Full TypeScript integration with Zod schemas
- **Comprehensive logging** - Integration with @isa/core/logging for debugging
- **Caching support** - Automatic caching for country data

## Quick Start

### 1. Import and Inject Services

```typescript
import { Component, inject } from '@angular/core';
import { CrmSearchService, ShippingAddressService } from '@isa/crm/data-access';

@Component({
  selector: 'app-customer-detail',
  template: '...'
})
export class CustomerDetailComponent {
  #crmSearchService = inject(CrmSearchService);
  #shippingAddressService = inject(ShippingAddressService);
}
```

### 2. Fetch Customer Data

```typescript
async loadCustomer(customerId: number): Promise<void> {
  const response = await this.#crmSearchService.fetchCustomer({
    customerId: customerId,
    eagerLoading: 3  // Load related data
  });

  const customer = response.result;
  console.log(`Customer: ${customer.firstName} ${customer.lastName}`);
  console.log(`Customer number: ${customer.customerNumber}`);
}
```

### 3. Use Angular Resources for Reactive Loading

```typescript
import { Component } from '@angular/core';
import { CustomerResource } from '@isa/crm/data-access';

@Component({
  selector: 'app-customer-view',
  providers: [CustomerResource],
  template: `
    @if (customerResource.resource.value(); as customer) {
      <div>{{ customer.firstName }} {{ customer.lastName }}</div>
    } @else if (customerResource.resource.isLoading()) {
      <div>Loading customer...</div>
    } @else if (customerResource.resource.error()) {
      <div>Error loading customer</div>
    }
  `
})
export class CustomerViewComponent {
  customerResource = inject(CustomerResource);

  ngOnInit() {
    // Trigger customer load
    this.customerResource.params({ customerId: 12345 });
  }
}
```

### 4. Fetch Shipping Addresses with Pagination

```typescript
async loadShippingAddresses(customerId: number): Promise<void> {
  const response = await this.#shippingAddressService.fetchCustomerShippingAddresses({
    customerId: customerId,
    take: 10,  // Pagination: take 10 records
    skip: 0    // Pagination: skip 0 records
  });

  console.log(`Total addresses: ${response.totalCount}`);
  console.log(`Addresses loaded: ${response.result.length}`);

  response.result.forEach(address => {
    console.log(`${address.firstName} ${address.lastName}`);
    console.log(`${address.address?.street}, ${address.address?.city}`);
  });
}
```

## Core Concepts

### Customer Data Model

The library provides comprehensive customer data management with the following structure:

```typescript
interface Customer {
  id: number;                        // Customer ID
  customerNumber: string;            // Customer number
  customerType: CustomerType;        // Customer type enum
  firstName?: string;                // First name
  lastName?: string;                 // Last name
  dateOfBirth?: string;             // Date of birth (ISO format)
  gender?: Gender;                   // Gender enum
  title?: string;                    // Title (Mr., Mrs., etc.)

  // Address information
  address?: Address;                 // Primary address

  // Communication details
  communicationDetails?: CommunicationDetails;  // Email, phone, etc.

  // Organization (for business customers)
  organisation?: Organisation;       // Organization details

  // Related entities
  bonusCard?: EntityContainer<BonusCard>;  // Primary bonus card
  shippingAddresses?: EntityContainer<ShippingAddress>[];  // Shipping addresses
  payers?: AssignedPayer[];         // Assigned payers

  // Customer status
  customerStatus?: number;           // Status code
  statusComment?: string;            // Status comment
  hasOnlineAccount?: boolean;        // Online account flag
  isGuestAccount?: boolean;          // Guest account flag

  // Metadata
  orderCount?: number;               // Total order count
  createdInBranch?: EntityContainer<Branch>;  // Creation branch
  attributes?: EntityContainer<Attribute>[];  // Custom attributes
  linkedRecords?: LinkedRecord[];   // Linked records

  // Comments
  agentComment?: string;             // Agent comment
  deactivationComment?: string;      // Deactivation comment

  // Settings
  preferredPaymentType?: number;     // Preferred payment type
  notificationChannels?: NotificationChannel;  // Notification preferences
  campaignCode?: string;             // Campaign code
  fetchOnDeliveryNote?: boolean;     // Delivery note flag
}
```

### Customer Types

```typescript
enum CustomerType {
  NotSet = 0,        // Not specified
  Private = 1,       // Private customer
  Business = 2,      // Business customer
  Employee = 4,      // Employee
}
```

### Shipping Address Model

```typescript
interface ShippingAddress {
  id: number;                        // Shipping address ID
  firstName?: string;                // First name
  lastName?: string;                 // Last name
  gender?: Gender;                   // Gender enum
  title?: string;                    // Title

  // Address details
  address?: Address;                 // Address information

  // Communication
  communicationDetails?: CommunicationDetails;  // Contact information

  // Organization (for business addresses)
  organisation?: Organisation;       // Organization details

  // Address metadata
  type?: number;                     // Address type
  validated?: string;                // Validation timestamp
  validationResult?: number;         // Validation result code

  // Comments
  agentomment?: string;              // Agent comment (typo in API)
}
```

### Bonus Card Model

```typescript
interface BonusCardInfo {
  id: number;                        // Bonus card ID
  cardNumber?: string;               // Card number
  cardProvider?: number;             // Card provider ID
  bonusValue?: number;               // Current bonus value

  // Card status
  isLocked?: boolean;                // Locked flag
  isPaymentEnabled?: boolean;        // Payment enabled flag
  markedAsLost?: string;            // Lost timestamp

  // Validity
  validFrom?: string;                // Valid from date (ISO format)
  validThrough?: string;             // Valid through date (ISO format)

  // Suspension
  suspensionComment?: string;        // Suspension comment

  // Extended properties (from BonusCardInfo model)
  firstName: string;                 // Card holder first name
  lastName: string;                  // Card holder last name
  isActive: boolean;                 // Active status
  isPrimary: boolean;                // Primary card flag
  totalPoints: number;               // Total points accumulated
}
```

### Payer Model

```typescript
interface Payer {
  id: number;                        // Payer ID
  payerNumber?: string;              // Payer number
  payerType?: PayerType;            // Payer type enum
  firstName?: string;                // First name
  lastName?: string;                 // Last name
  gender?: Gender;                   // Gender enum
  title?: string;                    // Title

  // Address and communication
  address?: Address;                 // Primary address
  communicationDetails?: CommunicationDetails;  // Contact details
  organisation?: Organisation;       // Organization (for business payers)

  // Payer status
  payerStatus?: PayerStatus;        // Status information
  payerGroup?: string;               // Payer group

  // Payment settings
  paymentTypes?: PaymentSettings[];  // Allowed payment types
  defaultPaymentPeriod?: number;     // Default payment period (days)
  standardInvoiceText?: string;      // Standard invoice text

  // Flags
  isGuestAccount?: boolean;          // Guest account flag

  // Comments
  agentComment?: string;             // Agent comment
  statusComment?: string;            // Status comment
  statusChangeComment?: string;      // Status change comment
  deactivationComment?: string;      // Deactivation comment

  // Label
  label?: EntityContainer<Label>;   // Label information
}
```

### Angular Resource Pattern

The library uses Angular's resource API for reactive data loading:

```typescript
// Resource automatically manages loading state, errors, and cancellation
const customerResource = resource({
  params: () => ({ customerId: 12345, eagerLoading: 3 }),
  loader: async ({ params, abortSignal }) => {
    const response = await service.fetchCustomer(params, abortSignal);
    return response.result;
  }
});

// In template:
// customerResource.value() - Current data
// customerResource.isLoading() - Loading state
// customerResource.error() - Error state
// customerResource.reload() - Manual reload
```

### Tab-Based State Management

The library integrates with the tab service for context-aware data loading:

```typescript
// Metadata keys for tab storage
const SELECTED_CUSTOMER_ID = 'crm-data-access.selectedCustomerId';
const SELECTED_SHIPPING_ADDRESS_ID = 'crm-data-access.selectedShippingAddressId';
const SELECTED_PAYER_ADDRESS_ID = 'crm-data-access.selectedPayerAddressId';

// Store selected customer ID in current tab
crmTabMetadataService.setSelectedCustomerId(tabId, 12345);

// Retrieve selected customer ID from tab
const customerId = crmTabMetadataService.selectedCustomerId(tabId);
```

### Validation with Zod

All input parameters are validated using Zod schemas:

```typescript
// Example: Customer fetch params validation
const params = {
  customerId: '123',        // String coerced to number
  eagerLoading: 3
};

// Validation happens automatically
const result = await service.fetchCustomer(params);
// Throws ZodError if validation fails
```

## API Reference

### CrmSearchService

Main service for fetching customer and bonus card data.

#### `fetchCustomer(params, abortSignal?): Promise<ResponseArgs<Customer>>`

Fetches a single customer by ID with optional eager loading.

**Parameters:**
- `params: FetchCustomerInput` - Customer fetch parameters (automatically validated)
  - `customerId: number` - Customer ID (required)
  - `eagerLoading?: number` - Eager loading level (optional, controls related data loading)
- `abortSignal?: AbortSignal` - Optional abort signal for request cancellation

**Returns:** Promise resolving to ResponseArgs containing Customer data

**Eager Loading Levels:**
- `0` - Minimal data (customer record only)
- `1` - Basic related data
- `2` - Extended related data
- `3` - Full related data (default in resources)

**Example:**
```typescript
const response = await service.fetchCustomer({
  customerId: 12345,
  eagerLoading: 3
});

const customer = response.result;
console.log(`${customer.firstName} ${customer.lastName}`);
console.log(`Bonus card: ${customer.bonusCard?.entity?.cardNumber}`);
```

#### `fetchCustomerCards(params, abortSignal?): Promise<ResponseArgs<BonusCardInfo[]>>`

Fetches all bonus cards for a customer.

**Parameters:**
- `params: FetchCustomerCardsInput` - Bonus card fetch parameters
  - `customerId: number` - Customer ID (required)
- `abortSignal?: AbortSignal` - Optional abort signal for request cancellation

**Returns:** Promise resolving to ResponseArgs containing array of BonusCardInfo

**Example:**
```typescript
const response = await service.fetchCustomerCards({
  customerId: 12345
});

const cards = response.result;
cards.forEach(card => {
  console.log(`Card ${card.cardNumber}: ${card.totalPoints} points`);
  if (card.isPrimary) {
    console.log('This is the primary card');
  }
});
```

### ShippingAddressService

Service for managing customer shipping addresses.

#### `fetchCustomerShippingAddresses(params, abortSignal?): Promise<ListResponseArgs<ShippingAddress>>`

Fetches shipping addresses for a customer with pagination support.

**Parameters:**
- `params: FetchCustomerShippingAddressesInput` - Shipping address fetch parameters
  - `customerId: number` - Customer ID (required)
  - `take?: number | null` - Number of records to retrieve (optional, for pagination)
  - `skip?: number | null` - Number of records to skip (optional, for pagination)
- `abortSignal?: AbortSignal` - Optional abort signal for request cancellation

**Returns:** Promise resolving to ListResponseArgs with ShippingAddress array and totalCount

**Example:**
```typescript
// Fetch first 10 shipping addresses
const response = await service.fetchCustomerShippingAddresses({
  customerId: 12345,
  take: 10,
  skip: 0
});

console.log(`Total addresses: ${response.totalCount}`);
console.log(`Loaded: ${response.result.length}`);

response.result.forEach(address => {
  console.log(`${address.firstName} ${address.lastName}`);
  console.log(`${address.address?.city}, ${address.address?.zipCode}`);
});

// Fetch next page
const nextPage = await service.fetchCustomerShippingAddresses({
  customerId: 12345,
  take: 10,
  skip: 10
});
```

#### `fetchShippingAddress(params, abortSignal?): Promise<ResponseArgs<ShippingAddress>>`

Fetches a single shipping address by ID.

**Parameters:**
- `params: FetchShippingAddressInput` - Shipping address fetch parameters
  - `shippingAddressId: number` - Shipping address ID (required)
- `abortSignal?: AbortSignal` - Optional abort signal for request cancellation

**Returns:** Promise resolving to ResponseArgs containing ShippingAddress

**Example:**
```typescript
const response = await service.fetchShippingAddress({
  shippingAddressId: 67890
});

const address = response.result;
console.log(`Address: ${address.address?.street}`);
console.log(`City: ${address.address?.city}`);
console.log(`Validated: ${address.validated}`);
```

### CountryService

Service for retrieving country information with automatic caching.

#### `getCountries(abortSignal?): Promise<Country[]>`

Retrieves all available countries with automatic caching.

**Parameters:**
- `abortSignal?: AbortSignal` - Optional abort signal for request cancellation

**Returns:** Promise resolving to array of Country objects

**Caching:** Results are automatically cached using the `@Cache()` decorator

**Example:**
```typescript
const countries = await service.getCountries();

countries.forEach(country => {
  console.log(`${country.name} (${country.code})`);
});

// Second call returns cached result
const cachedCountries = await service.getCountries();
```

### CrmTabMetadataService

Service for managing CRM-related metadata in tabs.

#### `selectedCustomerId(tabId): number | undefined`

Retrieves the selected customer ID from tab metadata.

**Parameters:**
- `tabId: number` - Tab ID

**Returns:** Customer ID or undefined if not set

#### `setSelectedCustomerId(tabId, customerId)`

Stores the selected customer ID in tab metadata.

**Parameters:**
- `tabId: number` - Tab ID
- `customerId: number | undefined` - Customer ID to store

#### `selectedShippingAddressId(tabId): number | undefined`

Retrieves the selected shipping address ID from tab metadata.

**Parameters:**
- `tabId: number` - Tab ID

**Returns:** Shipping address ID or undefined if not set

#### `setSelectedShippingAddressId(tabId, shippingAddressId)`

Stores the selected shipping address ID in tab metadata.

**Parameters:**
- `tabId: number` - Tab ID
- `shippingAddressId: number | undefined` - Shipping address ID to store

#### `selectedPayerId(tabId): number | undefined`

Retrieves the selected payer ID from tab metadata.

**Parameters:**
- `tabId: number` - Tab ID

**Returns:** Payer ID or undefined if not set

#### `setSelectedPayerId(tabId, payerAddressId)`

Stores the selected payer ID in tab metadata.

**Parameters:**
- `tabId: number` - Tab ID
- `payerAddressId: number | undefined` - Payer ID to store

**Example:**
```typescript
// Store customer ID in current tab
const tabId = tabService.activatedTabId();
crmTabMetadataService.setSelectedCustomerId(tabId, 12345);

// Later, retrieve customer ID from same tab
const customerId = crmTabMetadataService.selectedCustomerId(tabId);
if (customerId) {
  console.log(`Selected customer: ${customerId}`);
}
```

### CustomerFacade

Pass-through facade for customer operations.

#### `fetchCustomer(params, abortSignal?): Promise<Customer | undefined>`

Simplified customer fetch that returns the customer directly (unwraps ResponseArgs).

**Parameters:**
- `params: FetchCustomerInput` - Customer fetch parameters
- `abortSignal?: AbortSignal` - Optional abort signal

**Returns:** Promise resolving to Customer or undefined

**Example:**
```typescript
const customer = await facade.fetchCustomer({
  customerId: 12345,
  eagerLoading: 2
});

if (customer) {
  console.log(`Customer: ${customer.firstName} ${customer.lastName}`);
}
```

### CustomerCardsFacade

Pass-through facade for bonus card operations.

#### `get(params, abortSignal?): Promise<ResponseArgs<BonusCardInfo[]>>`

Fetches customer bonus cards.

**Parameters:**
- `params: FetchCustomerCardsInput` - Bonus card fetch parameters
- `abortSignal?: AbortSignal` - Optional abort signal

**Returns:** Promise resolving to ResponseArgs with BonusCardInfo array

### Resources

Angular resource-based data loading classes for reactive data management.

#### `CustomerResource`

Base resource for loading customer data.

**Properties:**
- `resource: Resource<Customer | undefined>` - Angular resource for customer data

**Methods:**
- `params(params)` - Update resource parameters
  - `customerId?: number` - Customer ID
  - `eagerLoading?: number` - Eager loading level (default: 3)

**Example:**
```typescript
@Component({
  providers: [CustomerResource]
})
export class CustomerComponent {
  customerResource = inject(CustomerResource);

  ngOnInit() {
    this.customerResource.params({ customerId: 12345 });
  }
}
```

#### `SelectedCustomerResource`

Extended resource that automatically loads customer based on tab metadata.

**Additional Features:**
- Automatically syncs with `CrmTabMetadataService.selectedCustomerId`
- Reactively updates when tab selection changes

**Methods:**
- `setEagerLoading(eagerLoading)` - Update eager loading level

**Example:**
```typescript
@Component({
  providers: [SelectedCustomerResource]
})
export class CustomerDetailComponent {
  selectedCustomerResource = inject(SelectedCustomerResource);

  ngOnInit() {
    // Automatically loads customer based on tab metadata
    this.selectedCustomerResource.setEagerLoading(3);
  }
}
```

#### `CustomerShippingAddressesResource`

Base resource for loading customer shipping addresses.

**Properties:**
- `resource: Resource<ShippingAddress[] | undefined>` - Angular resource for shipping addresses

**Methods:**
- `params(params)` - Update resource parameters
  - `customerId?: number` - Customer ID
  - `take?: number | null` - Pagination: number of records
  - `skip?: number | null` - Pagination: offset

**Example:**
```typescript
@Component({
  providers: [CustomerShippingAddressesResource]
})
export class ShippingAddressesComponent {
  addressesResource = inject(CustomerShippingAddressesResource);

  loadAddresses(customerId: number, page: number, pageSize: number) {
    this.addressesResource.params({
      customerId,
      take: pageSize,
      skip: page * pageSize
    });
  }
}
```

#### `SelectedCustomerShippingAddressesResource`

Extended resource that automatically loads addresses based on tab metadata.

**Additional Features:**
- Automatically syncs with `CrmTabMetadataService.selectedCustomerId`
- Reactively updates when tab customer selection changes

#### `CustomerShippingAddressResource`

Base resource for loading a single shipping address.

**Properties:**
- `resource: Resource<ShippingAddress | undefined>` - Angular resource for single address

**Methods:**
- `params(params)` - Update resource parameters
  - `shippingAddressId?: number` - Shipping address ID

#### `SelectedCustomerShippingAddressResource`

Extended resource that automatically loads address based on tab metadata.

**Additional Features:**
- Automatically syncs with `CrmTabMetadataService.selectedShippingAddressId`

#### `CustomerBonusCardsResource`

Base resource for loading customer bonus cards.

**Properties:**
- `resource: Resource<BonusCardInfo[] | undefined>` - Angular resource for bonus cards

**Methods:**
- `params(params)` - Update resource parameters
  - `customerId?: number` - Customer ID

#### `SelectedCustomerBonusCardsResource`

Extended resource that automatically loads cards based on tab metadata.

**Additional Features:**
- Automatically syncs with `CrmTabMetadataService.selectedCustomerId`

#### `CountryResource`

Resource for loading country list with automatic caching.

**Properties:**
- `resource: Resource<Country[]>` - Angular resource for countries

**Features:**
- Automatically caches results via CountryService
- No parameters required (loads all countries)

**Example:**
```typescript
@Component({
  providers: [CountryResource]
})
export class CountrySelectComponent {
  countryResource = inject(CountryResource);

  // In template: countryResource.resource.value()
}
```

### Helper Functions

#### `getCustomerName(customer): string`

Generates a formatted customer name from first and last name.

**Parameters:**
- `customer: { firstName?: string; lastName?: string } | undefined` - Customer object

**Returns:** Formatted name string (trimmed, handles missing values)

**Example:**
```typescript
import { getCustomerName } from '@isa/crm/data-access';

const name = getCustomerName(customer);
console.log(name);  // "John Doe"

const nameWithMissing = getCustomerName({ firstName: 'John' });
console.log(nameWithMissing);  // "John"

const emptyName = getCustomerName({});
console.log(emptyName);  // ""
```

#### `getPrimaryBonusCard(bonusCards): BonusCardInfo | undefined`

Finds the primary bonus card from an array of bonus cards.

**Parameters:**
- `bonusCards: BonusCardInfo[]` - Array of bonus cards

**Returns:** Primary bonus card or undefined if none found

**Example:**
```typescript
import { getPrimaryBonusCard } from '@isa/crm/data-access';

const primaryCard = getPrimaryBonusCard(bonusCards);
if (primaryCard) {
  console.log(`Primary card: ${primaryCard.cardNumber}`);
  console.log(`Points: ${primaryCard.totalPoints}`);
}
```

## Usage Examples

### Loading Customer with Eager Loading

```typescript
import { Component, inject } from '@angular/core';
import { CrmSearchService } from '@isa/crm/data-access';

@Component({
  selector: 'app-customer-detail',
  template: '...'
})
export class CustomerDetailComponent {
  #crmSearchService = inject(CrmSearchService);

  async loadCustomerWithAllData(customerId: number): Promise<void> {
    const response = await this.#crmSearchService.fetchCustomer({
      customerId: customerId,
      eagerLoading: 3  // Load all related data
    });

    const customer = response.result;

    // Access customer data
    console.log(`Name: ${customer.firstName} ${customer.lastName}`);
    console.log(`Customer #: ${customer.customerNumber}`);
    console.log(`Email: ${customer.communicationDetails?.email}`);

    // Access related data loaded via eager loading
    if (customer.bonusCard?.entity) {
      console.log(`Bonus card: ${customer.bonusCard.entity.cardNumber}`);
    }

    if (customer.payers && customer.payers.length > 0) {
      console.log(`Assigned payers: ${customer.payers.length}`);
      const defaultPayer = customer.payers.find(p => p.isDefault === 'true');
      if (defaultPayer?.payer?.entity) {
        console.log(`Default payer: ${defaultPayer.payer.entity.payerNumber}`);
      }
    }
  }
}
```

### Using Resource for Reactive Customer Loading

```typescript
import { Component, inject } from '@angular/core';
import { CustomerResource } from '@isa/crm/data-access';

@Component({
  selector: 'app-customer-view',
  standalone: true,
  providers: [CustomerResource],
  template: `
    @if (customerResource.resource.value(); as customer) {
      <div class="customer-details">
        <h2>{{ customer.firstName }} {{ customer.lastName }}</h2>
        <p>Customer Number: {{ customer.customerNumber }}</p>
        <p>Email: {{ customer.communicationDetails?.email }}</p>

        @if (customer.address) {
          <div class="address">
            <h3>Address</h3>
            <p>{{ customer.address.street }}</p>
            <p>{{ customer.address.zipCode }} {{ customer.address.city }}</p>
          </div>
        }
      </div>
    } @else if (customerResource.resource.isLoading()) {
      <div class="loading">Loading customer data...</div>
    } @else if (customerResource.resource.error()) {
      <div class="error">Error loading customer</div>
    }
  `
})
export class CustomerViewComponent {
  customerResource = inject(CustomerResource);

  ngOnInit() {
    // Trigger customer load
    this.customerResource.params({
      customerId: 12345,
      eagerLoading: 2  // Load with moderate related data
    });
  }

  reloadCustomer() {
    this.customerResource.resource.reload();
  }
}
```

### Tab-Based Customer Selection

```typescript
import { Component, inject } from '@angular/core';
import { SelectedCustomerResource, CrmTabMetadataService } from '@isa/crm/data-access';
import { TabService } from '@isa/core/tabs';

@Component({
  selector: 'app-customer-tab',
  standalone: true,
  providers: [SelectedCustomerResource],
  template: `
    @if (selectedCustomerResource.resource.value(); as customer) {
      <div>
        <h2>Selected Customer</h2>
        <p>{{ customer.firstName }} {{ customer.lastName }}</p>
        <button (click)="clearSelection()">Clear Selection</button>
      </div>
    } @else {
      <div>No customer selected</div>
    }
  `
})
export class CustomerTabComponent {
  #tabService = inject(TabService);
  #crmTabMetadata = inject(CrmTabMetadataService);
  selectedCustomerResource = inject(SelectedCustomerResource);

  selectCustomer(customerId: number) {
    const tabId = this.#tabService.activatedTabId();
    if (tabId) {
      // Store in tab metadata - resource automatically reloads
      this.#crmTabMetadata.setSelectedCustomerId(tabId, customerId);
    }
  }

  clearSelection() {
    const tabId = this.#tabService.activatedTabId();
    if (tabId) {
      this.#crmTabMetadata.setSelectedCustomerId(tabId, undefined);
    }
  }
}
```

### Paginated Shipping Addresses

```typescript
import { Component, inject, signal } from '@angular/core';
import { ShippingAddressService, ShippingAddress } from '@isa/crm/data-access';

@Component({
  selector: 'app-shipping-addresses',
  template: '...'
})
export class ShippingAddressesComponent {
  #shippingAddressService = inject(ShippingAddressService);

  currentPage = signal(0);
  pageSize = signal(10);
  totalCount = signal(0);
  addresses = signal<ShippingAddress[]>([]);

  async loadPage(customerId: number, page: number): Promise<void> {
    const response = await this.#shippingAddressService.fetchCustomerShippingAddresses({
      customerId: customerId,
      take: this.pageSize(),
      skip: page * this.pageSize()
    });

    this.addresses.set(response.result);
    this.totalCount.set(response.totalCount);
    this.currentPage.set(page);

    console.log(`Loaded page ${page + 1} of ${this.totalPages()}`);
    console.log(`Showing ${response.result.length} of ${response.totalCount} addresses`);
  }

  totalPages(): number {
    return Math.ceil(this.totalCount() / this.pageSize());
  }

  nextPage(customerId: number): void {
    if (this.currentPage() < this.totalPages() - 1) {
      this.loadPage(customerId, this.currentPage() + 1);
    }
  }

  previousPage(customerId: number): void {
    if (this.currentPage() > 0) {
      this.loadPage(customerId, this.currentPage() - 1);
    }
  }
}
```

### Using Resource for Shipping Addresses

```typescript
import { Component, inject } from '@angular/core';
import { SelectedCustomerShippingAddressesResource } from '@isa/crm/data-access';

@Component({
  selector: 'app-customer-shipping-list',
  standalone: true,
  providers: [SelectedCustomerShippingAddressesResource],
  template: `
    @if (addressesResource.resource.value(); as addresses) {
      <div class="shipping-addresses">
        <h3>Shipping Addresses ({{ addresses.length }})</h3>
        @for (address of addresses; track address.id) {
          <div class="address-card">
            <h4>{{ address.firstName }} {{ address.lastName }}</h4>
            @if (address.address) {
              <p>{{ address.address.street }}</p>
              <p>{{ address.address.zipCode }} {{ address.address.city }}</p>
            }
            @if (address.validated) {
              <span class="validated">Validated</span>
            }
          </div>
        }
      </div>
    } @else if (addressesResource.resource.isLoading()) {
      <div>Loading addresses...</div>
    }
  `
})
export class CustomerShippingListComponent {
  addressesResource = inject(SelectedCustomerShippingAddressesResource);

  // Resource automatically loads based on selected customer in tab metadata
  // No manual parameter setting needed
}
```

### Bonus Card Management

```typescript
import { Component, inject } from '@angular/core';
import { CrmSearchService, getPrimaryBonusCard } from '@isa/crm/data-access';

@Component({
  selector: 'app-bonus-cards',
  template: '...'
})
export class BonusCardsComponent {
  #crmSearchService = inject(CrmSearchService);

  async loadBonusCards(customerId: number): Promise<void> {
    const response = await this.#crmSearchService.fetchCustomerCards({
      customerId: customerId
    });

    const cards = response.result;

    // Find primary card
    const primaryCard = getPrimaryBonusCard(cards);
    if (primaryCard) {
      console.log(`Primary Card: ${primaryCard.cardNumber}`);
      console.log(`Card Holder: ${primaryCard.firstName} ${primaryCard.lastName}`);
      console.log(`Total Points: ${primaryCard.totalPoints}`);
      console.log(`Bonus Value: ${primaryCard.bonusValue}`);
      console.log(`Active: ${primaryCard.isActive}`);

      if (primaryCard.isLocked) {
        console.warn('Card is locked!');
      }

      if (primaryCard.markedAsLost) {
        console.warn(`Card marked as lost on ${primaryCard.markedAsLost}`);
      }
    }

    // List all cards
    console.log(`Total cards: ${cards.length}`);
    cards.forEach((card, index) => {
      console.log(`Card ${index + 1}: ${card.cardNumber}`);
      console.log(`  Points: ${card.totalPoints}`);
      console.log(`  Payment enabled: ${card.isPaymentEnabled}`);
      if (card.validThrough) {
        console.log(`  Valid through: ${card.validThrough}`);
      }
    });
  }
}
```

### Country Selection with Caching

```typescript
import { Component, inject, signal } from '@angular/core';
import { CountryService, Country } from '@isa/crm/data-access';

@Component({
  selector: 'app-country-select',
  template: `
    <select [(ngModel)]="selectedCountry">
      @for (country of countries(); track country.id) {
        <option [value]="country.id">{{ country.name }}</option>
      }
    </select>
  `
})
export class CountrySelectComponent {
  #countryService = inject(CountryService);
  countries = signal<Country[]>([]);
  selectedCountry = signal<string>('');

  async ngOnInit() {
    // First call fetches from API
    const countries = await this.#countryService.getCountries();
    this.countries.set(countries);

    // Subsequent calls return cached result (instant)
    const cachedCountries = await this.#countryService.getCountries();
    console.log('Same result from cache:', countries === cachedCountries);
  }
}
```

### Using CountryResource (Recommended)

```typescript
import { Component, inject } from '@angular/core';
import { CountryResource } from '@isa/crm/data-access';

@Component({
  selector: 'app-country-dropdown',
  standalone: true,
  providers: [CountryResource],
  template: `
    @if (countryResource.resource.value(); as countries) {
      <select>
        @for (country of countries; track country.id) {
          <option [value]="country.id">{{ country.name }}</option>
        }
      </select>
    } @else if (countryResource.resource.isLoading()) {
      <div>Loading countries...</div>
    }
  `
})
export class CountryDropdownComponent {
  countryResource = inject(CountryResource);
  // Resource automatically loads on initialization
}
```

### Request Cancellation

```typescript
import { Component, inject } from '@angular/core';
import { CrmSearchService } from '@isa/crm/data-access';

@Component({
  selector: 'app-search',
  template: '...'
})
export class SearchComponent {
  #crmSearchService = inject(CrmSearchService);
  #currentRequest?: AbortController;

  async searchCustomer(customerId: number): Promise<void> {
    // Cancel previous request if still running
    this.#currentRequest?.abort();

    // Create new abort controller
    this.#currentRequest = new AbortController();

    try {
      const response = await this.#crmSearchService.fetchCustomer(
        { customerId, eagerLoading: 2 },
        this.#currentRequest.signal
      );

      console.log('Customer loaded:', response.result);
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log('Request cancelled');
      } else {
        console.error('Error loading customer:', error);
      }
    } finally {
      this.#currentRequest = undefined;
    }
  }

  cancelSearch(): void {
    this.#currentRequest?.abort();
  }
}
```

### Handling Customer Name Formatting

```typescript
import { Component, inject, signal } from '@angular/core';
import { CustomerFacade, getCustomerName } from '@isa/crm/data-access';

@Component({
  selector: 'app-customer-display',
  template: `
    <div>{{ displayName() }}</div>
  `
})
export class CustomerDisplayComponent {
  #customerFacade = inject(CustomerFacade);
  displayName = signal<string>('');

  async loadCustomer(customerId: number): Promise<void> {
    const customer = await this.#customerFacade.fetchCustomer({
      customerId: customerId
    });

    // Generate formatted name
    const name = getCustomerName(customer);
    this.displayName.set(name || 'Unknown Customer');

    // Helper handles edge cases:
    // - Missing first or last name
    // - Both names missing
    // - Extra whitespace trimming
  }
}
```

## Resource Pattern

### Understanding Angular Resources

Angular resources provide a reactive way to load asynchronous data with automatic state management:

```typescript
// Resource definition
const customerResource = resource({
  params: () => this.#params(),  // Reactive parameters
  loader: async ({ params, abortSignal }) => {
    // Async data loading
    return await service.fetch(params, abortSignal);
  }
});

// Resource automatically provides:
// - value(): Current data value
// - isLoading(): Loading state
// - error(): Error state
// - status(): 'idle' | 'loading' | 'resolved' | 'error'
// - reload(): Manual refresh
// - hasValue(): True if data is available
```

### Resource Lifecycle

1. **Initialization**: Resource created with loader function
2. **Reactive Updates**: When params signal changes, loader automatically re-runs
3. **Automatic Cancellation**: Previous requests cancelled when new params trigger reload
4. **State Management**: Loading, error, and data states automatically tracked
5. **Template Integration**: Use signals in templates with @if control flow

### Base vs Selected Resources

The library provides two patterns for each resource type:

**Base Resources** (manual parameter control):
```typescript
// CustomerResource - manual control
customerResource.params({ customerId: 12345 });
```

**Selected Resources** (tab metadata integration):
```typescript
// SelectedCustomerResource - automatic tab integration
// Automatically loads based on tab metadata
// No manual params() call needed
```

### When to Use Each Pattern

**Use Base Resources When:**
- Data loading is not tied to tab state
- You need explicit control over parameters
- Component is reusable across different contexts

**Use Selected Resources When:**
- Data is tied to tab-based navigation
- You want automatic synchronization with tab state
- Component is used in tab-specific context

## Tab Metadata Management

### Metadata Storage

The library stores CRM-related IDs in tab metadata:

```typescript
// Storage keys (namespaced)
const SELECTED_CUSTOMER_ID = 'crm-data-access.selectedCustomerId';
const SELECTED_SHIPPING_ADDRESS_ID = 'crm-data-access.selectedShippingAddressId';
const SELECTED_PAYER_ADDRESS_ID = 'crm-data-access.selectedPayerAddressId';
```

### Metadata Lifecycle

1. **Set**: Store ID when user selects entity
2. **Retrieve**: Read ID when component needs data
3. **React**: Resources automatically react to metadata changes
4. **Clear**: Set to undefined when selection is cleared
5. **Persist**: Metadata persists during tab navigation

### Integration with Tab Service

```typescript
// Get current tab ID
const tabId = tabService.activatedTabId();

// Store customer ID in tab
crmTabMetadataService.setSelectedCustomerId(tabId, 12345);

// Later, retrieve customer ID
const customerId = crmTabMetadataService.selectedCustomerId(tabId);

// Resources react automatically
@Component({
  providers: [SelectedCustomerResource]  // Auto-loads from tab metadata
})
```

### Validation

All metadata values are validated using Zod schemas:

```typescript
// Metadata is validated as optional number
getMetadataHelper(
  tabId,
  SELECTED_CUSTOMER_ID,
  z.number().optional(),  // Validation schema
  tabService.entityMap()
);
```

## Validation with Zod

### Input Validation

All service methods validate input parameters using Zod schemas:

```typescript
// Schema definition
const FetchCustomerSchema = z.object({
  customerId: z.number().int(),      // Integer required
  eagerLoading: z.number().optional() // Optional number
});

// Automatic validation
const result = await service.fetchCustomer({
  customerId: '123',  // String coerced to number
  eagerLoading: 3
});
// Throws ZodError if validation fails
```

### Type Coercion

Zod schemas support automatic type coercion:

```typescript
// String to number coercion
customerId: z.number().int()  // '123' → 123

// Optional with default
eagerLoading: z.number().optional()  // undefined if not provided

// Nullable values
take: z.number().int().optional().nullable()  // Allows null or undefined
```

### Validation Errors

Zod validation errors provide detailed information:

```typescript
try {
  await service.fetchCustomer({ customerId: 'invalid' });
} catch (error) {
  if (error instanceof ZodError) {
    console.error('Validation errors:', error.errors);
    // error.errors contains array of validation issues
  }
}
```

### Schema Types

The library exports both inferred types and input types:

```typescript
// Inferred type (after validation)
type FetchCustomer = z.infer<typeof FetchCustomerSchema>;

// Input type (before validation, with coercion)
type FetchCustomerInput = z.input<typeof FetchCustomerSchema>;

// Use input type for function parameters
async fetchCustomer(params: FetchCustomerInput): Promise<...>
```

## Error Handling

### Error Types

#### ResponseArgsError

Thrown when the API returns an error:

```typescript
import { ResponseArgsError } from '@isa/common/data-access';

try {
  await service.fetchCustomer({ customerId: 99999 });
} catch (error) {
  if (error instanceof ResponseArgsError) {
    console.error('API error:', error.message);
  }
}
```

#### ZodError

Thrown when input validation fails:

```typescript
import { ZodError } from 'zod';

try {
  await service.fetchCustomer({ customerId: -1 });  // Negative ID invalid
} catch (error) {
  if (error instanceof ZodError) {
    console.error('Validation error:', error.errors);
  }
}
```

### Service Error Handling

Services handle errors gracefully and return fallback values:

```typescript
// CrmSearchService.fetchCustomer
try {
  const response = await firstValueFrom(req$);
  return response as ResponseArgs<Customer>;
} catch (error) {
  this.#logger.error('Error fetching customer', error);
  return undefined as unknown as ResponseArgs<Customer>;
}
```

### Resource Error States

Resources automatically track error states:

```typescript
@if (customerResource.resource.error(); as error) {
  <div class="error">
    Error loading customer: {{ error }}
  </div>
}
```

### Request Cancellation

Use AbortSignal for request cancellation:

```typescript
const controller = new AbortController();

// Start request
const promise = service.fetchCustomer(
  { customerId: 12345 },
  controller.signal
);

// Cancel if needed
controller.abort();

try {
  await promise;
} catch (error) {
  // Handle cancellation or other errors
  console.log('Request cancelled or failed');
}
```

### Logging

All services include comprehensive logging:

```typescript
// Service initialization
#logger = logger(() => ({
  service: 'CrmSearchService'
}));

// Operation logging
this.#logger.info('Fetching customer from API');
this.#logger.debug('Successfully fetched customer');
this.#logger.error('Error fetching customer', error);
```

## Testing

The library uses **Vitest** with **Angular Testing Utilities** for testing.

### Running Tests

```bash
# Run tests for this library
npx nx test crm-data-access --skip-nx-cache

# Run tests with coverage
npx nx test crm-data-access --code-coverage --skip-nx-cache

# Run tests in watch mode
npx nx test crm-data-access --watch
```

### Test Structure

The library includes comprehensive unit tests covering:

- **Service methods** - Tests for all CrmSearchService, ShippingAddressService, CountryService methods
- **Validation** - Tests Zod schema validation for all input types
- **Resources** - Tests Angular resource behavior and state management
- **Tab metadata** - Tests CrmTabMetadataService integration
- **Helper functions** - Tests getPrimaryBonusCard, getCustomerName utilities
- **Error handling** - Tests API errors, validation failures, abort signal behavior
- **Caching** - Tests CountryService caching decorator

### Example Test

```typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { TestBed } from '@angular/core/testing';
import { getPrimaryBonusCard } from './get-primary-bonus-card.helper';
import { BonusCardInfo } from '../models';

describe('getPrimaryBonusCard', () => {
  it('should return the primary bonus card when one exists', () => {
    // Arrange
    const bonusCards: BonusCardInfo[] = [
      {
        firstName: 'John',
        lastName: 'Doe',
        isActive: true,
        isPrimary: false,
        totalPoints: 100,
      } as BonusCardInfo,
      {
        firstName: 'Jane',
        lastName: 'Smith',
        isActive: true,
        isPrimary: true,
        totalPoints: 200,
      } as BonusCardInfo,
    ];

    // Act
    const result = getPrimaryBonusCard(bonusCards);

    // Assert
    expect(result).toBeDefined();
    expect(result?.isPrimary).toBe(true);
    expect(result?.firstName).toBe('Jane');
  });

  it('should return undefined when no primary bonus card exists', () => {
    // Arrange
    const bonusCards: BonusCardInfo[] = [
      {
        firstName: 'John',
        lastName: 'Doe',
        isActive: true,
        isPrimary: false,
        totalPoints: 100,
      } as BonusCardInfo,
    ];

    // Act
    const result = getPrimaryBonusCard(bonusCards);

    // Assert
    expect(result).toBeUndefined();
  });
});
```

## Architecture Notes

### Current Architecture

The library follows a layered architecture:

```
Components/Features
       ↓
  Resources (Angular resource API)
       ↓
  Facades (optional pass-through)
       ↓
  Services (business logic)
       ↓
├─→ CrmSearchService
├─→ ShippingAddressService
├─→ CountryService
├─→ CrmTabMetadataService
       ↓
  Generated API Client (crm-api)
       ↓
  Zod Schemas (validation)
```

### Design Patterns

#### Resource Pattern

The library uses Angular's resource API for reactive data loading:

**Benefits:**
- Automatic loading state management
- Built-in error handling
- Automatic request cancellation
- Reactive parameter updates
- Template-friendly signals

**Implementation:**
```typescript
resource({
  params: () => signal(),     // Reactive parameters
  loader: async ({ params, abortSignal }) => {
    return await service.fetch(params, abortSignal);
  }
})
```

#### Tab Metadata Pattern

Integration with tab service for context-aware data:

**Benefits:**
- State persists during tab navigation
- Automatic synchronization across components
- Clean separation of concerns
- Type-safe metadata storage

**Implementation:**
```typescript
// Store in tab
crmTabMetadataService.setSelectedCustomerId(tabId, customerId);

// Resource automatically reacts to changes
effect(() => {
  const tabId = this.#tabId();
  const customerId = this.#customerMetadata.selectedCustomerId(tabId);
  this.params({ customerId });
});
```

#### Caching Pattern

CountryService uses decorator-based caching:

**Benefits:**
- Reduces API calls
- Improves performance
- Transparent to consumers
- Configurable TTL

**Implementation:**
```typescript
@Cache()  // Decorator from @isa/common/decorators
async getCountries(abortSignal?: AbortSignal): Promise<Country[]> {
  // Method automatically cached
}
```

### Known Architectural Considerations

#### 1. Facade Evaluation (Low Priority)

The facades (`CustomerFacade`, `CustomerCardsFacade`) are simple pass-through wrappers:

**Current State:**
- Minimal added value
- Just delegate to services
- CustomerFacade unwraps ResponseArgs

**Recommendation:**
- Consider removal if not needed for future abstraction
- Direct service injection is simpler
- Keep if planning orchestration logic

**Impact:** Low risk, reduces one layer of indirection

#### 2. Selected Resources vs Base Resources (Good Design)

The library provides both base and selected variants of resources:

**Current State:**
- Base resources for manual control
- Selected resources for tab integration
- Clear separation of concerns

**Strength:**
- Flexible usage patterns
- Reusable across contexts
- Clean abstraction

**Impact:** This is good architecture, no changes needed

#### 3. Error Handling in Services (Medium Priority)

Services return fallback values on error instead of throwing:

**Current State:**
```typescript
catch (error) {
  this.#logger.error('Error fetching customer', error);
  return undefined as unknown as ResponseArgs<Customer>;
}
```

**Consideration:**
- Type assertion `as unknown as ResponseArgs` is not type-safe
- Consumers don't know if undefined means error or not found
- Errors are logged but not propagated

**Alternative:**
- Throw errors and let consumers handle
- Return discriminated union: `{ success: true, data } | { success: false, error }`
- Use Result/Either pattern

**Impact:** Medium - affects error handling patterns

#### 4. Schema Organization (Good Structure)

Schemas are well-organized and comprehensive:

**Strengths:**
- Clear separation by entity
- Proper input/output types
- Zod validation integration
- Extends common schemas

**Best Practice:**
- Maintains type safety throughout
- Reusable schema composition

### Performance Considerations

1. **Automatic Caching** - CountryService caches results to minimize API calls
2. **Resource Cancellation** - Resources automatically cancel pending requests
3. **Eager Loading Control** - Configurable eager loading levels reduce over-fetching
4. **Pagination Support** - Shipping addresses support pagination for large datasets
5. **Reactive Updates** - Resources only reload when parameters change

### Future Enhancements

Potential improvements identified:

1. **Batch Operations** - Support batch customer/address lookups
2. **Offline Support** - Cache customer data for offline access
3. **Search Functionality** - Add customer search capabilities
4. **Mutation Operations** - Add create/update/delete operations
5. **Optimistic Updates** - Resource pattern with optimistic UI updates
6. **Error Recovery** - Automatic retry logic for transient failures
7. **Analytics Integration** - Track data access patterns

## Dependencies

### Required Libraries

- `@angular/core` - Angular framework (20.1.2)
- `@generated/swagger/crm-api` - Generated CRM API client
- `@isa/common/data-access` - Common data access utilities (ResponseArgs, catchResponseArgsErrorPipe, takeUntilAborted)
- `@isa/common/decorators` - Caching decorator (@Cache)
- `@isa/core/logging` - Logging service
- `@isa/core/tabs` - Tab service and metadata helpers
- `zod` - Schema validation library
- `rxjs` - Reactive programming (firstValueFrom)

### Path Alias

Import from: `@isa/crm/data-access`

### Generated API

The library depends on the generated CRM API client:

```typescript
// Generated services used
import { CustomerService, ShippingAddressService, CountryService } from '@generated/swagger/crm-api';

// Generated types used
import { BonusCardInfoDTO, CountryDTO, PayerDTO } from '@generated/swagger/crm-api';
```

To regenerate API clients after backend changes:
```bash
npm run generate:swagger
```

## License

Internal ISA Frontend library - not for external distribution.
