# @isa/crm/data-access

Data access layer for CRM (Customer Relationship Management) operations, providing services, facades, resources, and state management for customer, payer, bonus card, and shipping address management.

## Overview

This library serves as the centralized data access layer for all CRM-related operations in the ISA-Frontend application. It provides a clean abstraction over the CRM API with:

- **Facades**: Simplified, high-level APIs for common CRM operations
- **Services**: Low-level API interactions with typed schemas and validation
- **Resources**: Angular Resource API implementations for reactive data loading
- **Stores**: NgRx Signal Store implementations for component-level state
- **Schemas**: Zod schemas for runtime type validation and type safety
- **Helpers**: Utility functions for CRM data manipulation

The library follows modern Angular patterns with:
- Standalone services with `providedIn: 'root'`
- Zod schema validation for all API inputs
- Resource API for automatic race condition prevention
- NgRx Signal Store for reactive state management
- Comprehensive logging via `@isa/core/logging`

## Installation

```typescript
import {
  // Facades
  CustomerFacade,
  CustomerCardsFacade,
  CustomerCardBookingFacade,
  CustomerBonRedemptionFacade,

  // Services
  CrmSearchService,
  CountryService,
  PayerService,
  ShippingAddressService,

  // Resources
  CustomerResource,
  SelectedCustomerResource,
  CustomerBonusCardsResource,
  CustomerShippingAddressesResource,

  // Stores
  BonRedemptionStore,

  // Schemas & Types
  Customer,
  CrmPayer,
  ShippingAddress,
  BonusCardInfo,

  // Helpers
  getPrimaryBonusCard,
  getEnabledCustomerFeature,
  deduplicateAddressees,

  // Constants
  SELECTED_CUSTOMER_ID,
  SELECTED_SHIPPING_ADDRESS_ID,
} from '@isa/crm/data-access';
```

## Architecture

### Facades

Facades provide simplified, business-focused APIs by combining multiple service calls and handling common patterns.

#### CustomerFacade

High-level API for customer operations.

```typescript
@Component({...})
export class CustomerDetailComponent {
  #customerFacade = inject(CustomerFacade);

  async loadCustomer(customerId: number) {
    const customer = await this.#customerFacade.fetchCustomer({
      customerId,
      eagerLoading: 3, // Load related data up to 3 levels deep
    });
  }
}
```

**Methods:**
- `fetchCustomer(params: FetchCustomerInput, abortSignal?: AbortSignal): Promise<Customer | undefined>`

#### CustomerCardsFacade

Manages customer bonus cards (loyalty cards).

```typescript
@Component({...})
export class BonusCardsComponent {
  #cardsFacade = inject(CustomerCardsFacade);

  async loadCards(customerId: number) {
    const response = await this.#cardsFacade.get({ customerId });
    return response.result; // BonusCardInfo[]
  }

  async addCard(customerId: number, cardCode: string) {
    const result = await this.#cardsFacade.addCard({
      customerId,
      loyaltyCardValues: { cardCode },
    });
  }

  async lockCard(cardCode: string) {
    await this.#cardsFacade.lockCard({ cardCode });
  }

  async unlockCard(customerId: number, cardCode: string) {
    await this.#cardsFacade.unlockCard({ customerId, cardCode });
  }
}
```

**Methods:**
- `get(params: FetchCustomerCardsInput, abortSignal?: AbortSignal): Promise<ResponseArgs<BonusCardInfo[]>>`
- `addCard(params: AddCardInput): Promise<AccountDetailsDTO | undefined>`
- `lockCard(params: LockCardInput): Promise<boolean | undefined>`
- `unlockCard(params: UnlockCardInput): Promise<boolean | undefined>`

#### CustomerCardBookingFacade

Manages loyalty card point bookings.

```typescript
@Component({...})
export class BookingComponent {
  #bookingFacade = inject(CustomerCardBookingFacade);

  async init() {
    // Load booking reasons (dropdown options)
    const reasons = await this.#bookingFacade.fetchBookingReasons();

    // Get current store for bookings
    const store = await this.#bookingFacade.fetchCurrentBookingPartnerStore();
  }

  async addPoints(cardCode: string, points: number, reason: number) {
    const result = await this.#bookingFacade.addBooking({
      cardCode,
      booking: { points, reason, storeId: '123' },
    });
  }
}
```

**Methods:**
- `fetchBookingReasons(abortSignal?: AbortSignal): Promise<KeyValueDTOOfStringAndInteger[]>`
- `fetchCurrentBookingPartnerStore(abortSignal?: AbortSignal): Promise<KeyValueDTOOfStringAndString | undefined>`
- `addBooking(params: AddBookingInput): Promise<LoyaltyBookingInfoDTO | undefined>`

#### CustomerBonRedemptionFacade

Validates and redeems customer receipts (Bons) for loyalty points.

```typescript
@Component({...})
export class BonRedemptionComponent {
  #bonFacade = inject(CustomerBonRedemptionFacade);

  async validateBon(cardCode: string, bonNumber: string) {
    // Validates receipt and returns details
    const response = await this.#bonFacade.checkBon({
      cardCode,
      bonNr: bonNumber,
      storeId: '123', // Optional, auto-fetched if not provided
    });

    if (response.result) {
      console.log('Bon date:', response.result.date);
      console.log('Bon total:', response.result.total);
    }
  }

  async redeemBon(cardCode: string, bonNumber: string) {
    // Redeems receipt for points
    const success = await this.#bonFacade.addBon({
      cardCode,
      bonNr: bonNumber,
      storeId: '123', // Optional, auto-fetched if not provided
    });
  }
}
```

**Methods:**
- `checkBon(params: CheckBonInput, abortSignal?: AbortSignal): Promise<ResponseArgs<LoyaltyBonResponse>>`
- `addBon(params: AddBonInput): Promise<boolean>`

### Services

Low-level services that directly interact with the API.

#### CrmSearchService

Core service for CRM API operations with Zod validation and logging.

```typescript
@Injectable({ providedIn: 'root' })
export class CrmSearchService {
  async fetchCustomer(params: FetchCustomerInput, abortSignal?: AbortSignal): Promise<ResponseArgs<Customer>>
  async fetchCustomerCards(params: FetchCustomerCardsInput, abortSignal?: AbortSignal): Promise<ResponseArgs<BonusCardInfo[]>>
  async fetchLoyaltyBookings(customerId: number, abortSignal?: AbortSignal): Promise<LoyaltyBookingInfoDTO[]>
  async fetchBookingReasons(abortSignal?: AbortSignal): Promise<KeyValueDTOOfStringAndInteger[]>
  async fetchCurrentBookingPartnerStore(abortSignal?: AbortSignal): Promise<KeyValueDTOOfStringAndString | undefined>
  async addCard(params: AddCardInput): Promise<AccountDetailsDTO | undefined>
  async lockCard(params: LockCardInput): Promise<boolean | undefined>
  async unlockCard(params: UnlockCardInput): Promise<boolean | undefined>
  async addBooking(params: AddBookingInput): Promise<LoyaltyBookingInfoDTO | undefined>
  async checkBon(params: CheckBonInput, abortSignal?: AbortSignal): Promise<ResponseArgs<LoyaltyBonResponse>>
  async addBon(params: AddBonInput): Promise<boolean>
}
```

**Features:**
- All inputs validated with Zod schemas
- Automatic error catching via `catchResponseArgsErrorPipe()`
- Abort signal support for cancellation
- Comprehensive logging for debugging
- Method caching where appropriate (e.g., `fetchCurrentBookingPartnerStore`)

#### CountryService

Provides country list data.

```typescript
@Component({...})
export class AddressFormComponent {
  #countryService = inject(CountryService);

  countries = signal<Country[]>([]);

  async ngOnInit() {
    this.countries.set(await this.#countryService.getCountries());
  }
}
```

**Methods:**
- `getCountries(abortSignal?: AbortSignal): Promise<Country[]>` - Cached with `@Cache()` decorator

#### PayerService

Manages payer (billing entity) data.

```typescript
@Component({...})
export class PayerDetailComponent {
  #payerService = inject(PayerService);

  async loadPayer(payerId: number) {
    const response = await this.#payerService.fetchPayer({ payerId });
    return response.result; // CrmPayer
  }
}
```

**Methods:**
- `fetchPayer(params: FetchPayerInput, abortSignal?: AbortSignal): Promise<ResponseArgs<CrmPayer>>`

#### ShippingAddressService

Manages customer shipping addresses.

```typescript
@Component({...})
export class ShippingAddressListComponent {
  #shippingService = inject(ShippingAddressService);

  async loadAddresses(customerId: number, page: number) {
    const response = await this.#shippingService.fetchCustomerShippingAddresses({
      customerId,
      take: 20,
      skip: page * 20,
    });

    console.log('Total:', response.totalCount);
    console.log('Items:', response.result);
  }

  async loadSingleAddress(shippingAddressId: number) {
    const response = await this.#shippingService.fetchShippingAddress({
      shippingAddressId,
    });
    return response.result;
  }
}
```

**Methods:**
- `fetchCustomerShippingAddresses(params: FetchCustomerShippingAddressesInput, abortSignal?: AbortSignal): Promise<ListResponseArgs<ShippingAddress>>`
- `fetchShippingAddress(params: FetchShippingAddressInput, abortSignal?: AbortSignal): Promise<ResponseArgs<ShippingAddress>>`

#### CrmTabMetadataService

Manages CRM-related metadata for tab-based navigation.

```typescript
@Component({...})
export class CustomerTabComponent {
  #metadata = inject(CrmTabMetadataService);
  #tabService = inject(TabService);

  selectCustomer(customerId: number) {
    const tabId = this.#tabService.activatedTabId();
    if (tabId) {
      this.#metadata.setSelectedCustomerId(tabId, customerId);
    }
  }
}
```

### Resources

Angular Resource API implementations for reactive data loading with automatic race condition prevention.

#### CustomerResource

Base resource for loading customer data reactively.

```typescript
@Component({
  providers: [CustomerResource],
})
export class CustomerPanelComponent {
  #resource = inject(CustomerResource);

  customer = this.#resource.resource.value; // Signal<Customer | undefined>
  isLoading = this.#resource.resource.isLoading; // Signal<boolean>
  error = this.#resource.resource.error; // Signal<Error | undefined>

  loadCustomer(customerId: number) {
    this.#resource.params({ customerId, eagerLoading: 3 });
  }
}
```

**Usage Pattern:**
- Inject via component providers (not root)
- Call `params()` to trigger data loading
- Access reactive signals: `value()`, `isLoading()`, `error()`

#### SelectedCustomerResource

Automatically loads customer based on active tab's selected customer ID.

```typescript
@Component({...})
export class CustomerDetailTabComponent {
  #selectedCustomer = inject(SelectedCustomerResource);

  // Automatically updates when tab changes or customer selection changes
  customer = this.#selectedCustomer.resource.value;
  isLoading = this.#selectedCustomer.resource.isLoading;

  setEagerLoading(level: number) {
    this.#selectedCustomer.setEagerLoading(level);
  }
}
```

**Features:**
- Automatically syncs with `CrmTabMetadataService`
- Reacts to tab changes
- Provided in root (singleton)

#### Other Resources

- `CustomerBonusCardsResource` - Loads customer bonus cards reactively
- `CustomerCardTransactionsResource` - Loads loyalty card transaction history
- `CustomerShippingAddressesResource` - Loads customer shipping addresses with pagination
- `CustomerShippingAddressResource` - Loads a single shipping address
- `CustomerPayerAddressResource` - Loads payer address details
- `CustomerBookingReasonsResource` - Loads booking reason options
- `CustomerBonCheckResource` - Validates Bon numbers reactively
- `PrimaryCustomerCardResource` - Loads the primary bonus card for a customer
- `PayerResource` - Loads payer details reactively
- `CountryResource` - Loads country list reactively

All resources follow the same pattern:
- Call `params()` to set parameters and trigger loading
- Access `resource.value()` for data
- Access `resource.isLoading()` for loading state
- Access `resource.error()` for errors

### Stores

NgRx Signal Store implementations for component-level state management.

#### BonRedemptionStore

Manages Bon redemption workflow state.

```typescript
@Component({
  providers: [BonRedemptionStore], // Component-scoped
})
export class BonRedemptionComponent {
  store = inject(BonRedemptionStore);

  // Signals (read)
  bonNumber = this.store.bonNumber;
  isValidating = this.store.isValidating;
  isRedeeming = this.store.isRedeeming;
  validatedBon = this.store.validatedBon;
  errorMessage = this.store.errorMessage;

  // Computed signals
  disableSearch = this.store.disableSearch;
  disableRedemption = this.store.disableRedemption;
  hasValidBon = this.store.hasValidBon;
  hasError = this.store.hasError;

  // Methods (write)
  updateBonNumber(value: string) {
    this.store.setBonNumber(value);
  }

  async validateBon() {
    this.store.setValidating(true);
    this.store.setValidationAttempted(true);

    try {
      const result = await this.#bonFacade.checkBon({...});
      this.store.setValidatedBon({
        bonNumber: result.bonNr,
        date: result.date,
        total: result.total,
      });
    } catch (error) {
      this.store.setError('Validation failed');
    } finally {
      this.store.setValidating(false);
    }
  }

  reset() {
    this.store.reset();
  }
}
```

**State:**
- `bonNumber: string` - Current Bon number input
- `isValidating: boolean` - Validation in progress
- `isRedeeming: boolean` - Redemption in progress
- `validationAttempted: boolean` - Tracks if validation button was clicked
- `validatedBon: ValidatedBon | undefined` - Validated Bon details
- `errorMessage: string | undefined` - Error message

**Computed Signals:**
- `disableSearch: boolean` - Whether search button should be disabled
- `disableRedemption: boolean` - Whether redemption button should be disabled
- `hasValidBon: boolean` - Whether a valid Bon is loaded
- `hasError: boolean` - Whether there is an error

**Methods:**
- `setBonNumber(bonNumber: string)` - Update Bon number input
- `setValidating(isValidating: boolean)` - Set validation loading state
- `setRedeeming(isRedeeming: boolean)` - Set redemption loading state
- `setValidationAttempted(attempted: boolean)` - Mark validation attempt
- `setValidatedBon(bon: ValidatedBon | undefined)` - Set validated Bon data
- `setError(errorMessage: string | undefined)` - Set error message
- `reset()` - Reset store to initial state

## Schemas & Types

All input schemas use Zod for runtime validation.

### Key Types

```typescript
// Core entities
type Customer = z.infer<typeof CustomerSchema>;
type CrmPayer = z.infer<typeof PayerSchema>;
type ShippingAddress = z.infer<typeof ShippingAddressSchema>;
type BonusCardInfo = BonusCardInfoDTO & {
  firstName: string;
  lastName: string;
  isActive: boolean;
  isPrimary: boolean;
  totalPoints: number;
};

// Input schemas
type FetchCustomerInput = z.infer<typeof FetchCustomerSchema>;
type FetchCustomerCardsInput = z.infer<typeof FetchCustomerCardsSchema>;
type AddCardInput = z.infer<typeof AddCardSchema>;
type LockCardInput = z.infer<typeof LockCardSchema>;
type UnlockCardInput = z.infer<typeof UnlockCardSchema>;
type AddBookingInput = z.infer<typeof AddBookingSchema>;
type CheckBonInput = z.infer<typeof CheckBonSchema>;
type AddBonInput = z.infer<typeof AddBonSchema>;
type FetchPayerInput = z.infer<typeof FetchPayerSchema>;
type FetchShippingAddressInput = z.infer<typeof FetchShippingAddressSchema>;
type FetchCustomerShippingAddressesInput = z.infer<typeof FetchCustomerShippingAddressesSchema>;

// Enums
enum CustomerType {
  Private = 0,
  Business = 1,
}

enum CustomerFeatureKey {
  DAccount = 'd-account',
  DNoAccount = 'd-no-account',
  // ... more keys
}

enum CustomerFeatureGroup {
  DCustomerType = 'd-customertype',
  // ... more groups
}
```

## Helpers

### getPrimaryBonusCard

Retrieves the primary bonus card from a list of cards.

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

const cards: BonusCardInfo[] = [...];
const primaryCard = getPrimaryBonusCard(cards);
```

**Logic:**
1. Returns `undefined` if no cards
2. Filters for primary cards (where `isPrimary === true`)
3. If primary cards exist, uses those; otherwise uses all cards
4. Sorts alphabetically by code (case-insensitive)
5. Returns the first card from sorted list

### getEnabledCustomerFeature

Retrieves the first enabled customer feature that meets validation criteria.

```typescript
import { getEnabledCustomerFeature } from '@isa/crm/data-access';

const features: KeyValueOfStringAndString[] = customer.features;
const enabledFeature = getEnabledCustomerFeature(features);
```

**Validation criteria:**
- `enabled === true`
- `description` is non-empty
- `group === 'd-customertype'`

**Priority (when multiple valid features):**
1. Features with key `'d-account'`
2. Features with key `'d-no-account'`
3. First valid feature in array

### deduplicateAddressees

Removes duplicate addressees from a list.

```typescript
import { deduplicateAddressees, deduplicateBranches } from '@isa/crm/data-access';

const uniqueAddressees = deduplicateAddressees(addresseeList);
const uniqueBranches = deduplicateBranches(branchList);
```

## Constants

```typescript
export const SELECTED_CUSTOMER_ID = 'crm-data-access.selectedCustomerId';
export const SELECTED_SHIPPING_ADDRESS_ID = 'crm-data-access.selectedShippingAddressId';
export const SELECTED_PAYER_ADDRESS_ID = 'crm-data-access.selectedPayerAddressId';
```

Used as keys for storing/retrieving selected entity IDs in metadata services.

## Usage Examples

### Example 1: Customer Detail Page

```typescript
@Component({
  selector: 'app-customer-detail',
  standalone: true,
  template: `
    @if (customer(); as customer) {
      <div>{{ customer.firstName }} {{ customer.lastName }}</div>
      <div>Customer #{{ customer.customerNumber }}</div>
    } @else if (isLoading()) {
      <loading-spinner />
    } @else if (error()) {
      <error-message [error]="error()" />
    }
  `,
})
export class CustomerDetailComponent {
  #selectedCustomer = inject(SelectedCustomerResource);

  customer = this.#selectedCustomer.resource.value;
  isLoading = this.#selectedCustomer.resource.isLoading;
  error = this.#selectedCustomer.resource.error;

  ngOnInit() {
    // Automatically loads based on active tab's selected customer
    this.#selectedCustomer.setEagerLoading(3);
  }
}
```

### Example 2: Bonus Card Management

```typescript
@Component({
  selector: 'app-bonus-cards',
  standalone: true,
  providers: [CustomerBonusCardsResource],
})
export class BonusCardsComponent {
  #cardsResource = inject(CustomerBonusCardsResource);
  #cardsFacade = inject(CustomerCardsFacade);

  cards = this.#cardsResource.resource.value;
  primaryCard = computed(() => {
    const cards = this.cards();
    return cards ? getPrimaryBonusCard(cards) : undefined;
  });

  loadCards(customerId: number) {
    this.#cardsResource.params({ customerId });
  }

  async lockCard(cardCode: string) {
    try {
      await this.#cardsFacade.lockCard({ cardCode });
      // Refresh cards after locking
      this.#cardsResource.reload();
    } catch (error) {
      console.error('Failed to lock card', error);
    }
  }
}
```

### Example 3: Bon Redemption Workflow

```typescript
@Component({
  selector: 'app-bon-redemption',
  standalone: true,
  providers: [BonRedemptionStore],
  template: `
    <input
      [(ngModel)]="bonNumber"
      (ngModelChange)="store.setBonNumber($event)"
      placeholder="Enter Bon number"
    />

    <button
      (click)="validateBon()"
      [disabled]="store.disableSearch()"
    >
      Search
    </button>

    @if (store.hasValidBon(); as bon) {
      <div>Date: {{ store.validatedBon()?.date }}</div>
      <div>Total: {{ store.validatedBon()?.total }}</div>

      <button
        (click)="redeemBon()"
        [disabled]="store.disableRedemption()"
      >
        Redeem
      </button>
    }

    @if (store.hasError()) {
      <error-message>{{ store.errorMessage() }}</error-message>
    }
  `,
})
export class BonRedemptionComponent {
  store = inject(BonRedemptionStore);
  #bonFacade = inject(CustomerBonRedemptionFacade);

  @Input() cardCode!: string;

  async validateBon() {
    this.store.setValidating(true);
    this.store.setValidationAttempted(true);

    try {
      const response = await this.#bonFacade.checkBon({
        cardCode: this.cardCode,
        bonNr: this.store.bonNumber(),
      });

      if (response.result) {
        this.store.setValidatedBon({
          bonNumber: response.result.bonNr,
          date: response.result.date,
          total: response.result.total,
        });
      }
    } catch (error) {
      this.store.setError('Bon validation failed');
    } finally {
      this.store.setValidating(false);
    }
  }

  async redeemBon() {
    this.store.setRedeeming(true);

    try {
      const success = await this.#bonFacade.addBon({
        cardCode: this.cardCode,
        bonNr: this.store.bonNumber(),
      });

      if (success) {
        this.store.reset();
        // Show success message
      }
    } catch (error) {
      this.store.setError('Bon redemption failed');
    } finally {
      this.store.setRedeeming(false);
    }
  }
}
```

### Example 4: Shipping Address Selection

```typescript
@Component({
  selector: 'app-shipping-address-list',
  standalone: true,
  providers: [CustomerShippingAddressesResource],
})
export class ShippingAddressListComponent {
  #addressesResource = inject(CustomerShippingAddressesResource);

  addresses = this.#addressesResource.resource.value;
  isLoading = this.#addressesResource.resource.isLoading;
  totalCount = computed(() => this.addresses()?.length ?? 0);

  currentPage = signal(0);
  pageSize = 20;

  constructor() {
    effect(() => {
      const page = this.currentPage();
      this.loadPage(page);
    });
  }

  loadForCustomer(customerId: number) {
    this.#addressesResource.params({
      customerId,
      take: this.pageSize,
      skip: 0,
    });
    this.currentPage.set(0);
  }

  loadPage(page: number) {
    this.#addressesResource.params({
      skip: page * this.pageSize,
      take: this.pageSize,
    });
  }

  nextPage() {
    this.currentPage.update(p => p + 1);
  }

  prevPage() {
    this.currentPage.update(p => Math.max(0, p - 1));
  }
}
```

## Dependencies

This library depends on the following internal libraries:

- `@isa/common/data-access` - Common data access utilities (`ResponseArgs`, `catchResponseArgsErrorPipe`, `takeUntilAborted`)
- `@isa/common/decorators` - Caching decorators (`@Cache`, `CacheTimeToLive`)
- `@isa/core/logging` - Logging infrastructure (`logger`)
- `@isa/core/tabs` - Tab management (`TabService`)
- `@generated/swagger/crm-api` - Generated CRM API clients

External dependencies:

- `@angular/core` - Angular framework
- `@ngrx/signals` - NgRx Signal Store
- `rxjs` - Reactive programming
- `zod` - Runtime schema validation

## Best Practices

1. **Use Facades over Services** for application code - Facades provide simplified APIs
2. **Use Resources for reactive data** - Automatic race condition prevention and loading states
3. **Use Stores for complex UI state** - Component-scoped state management
4. **Always validate inputs** - All inputs are validated via Zod schemas
5. **Handle abort signals** - Pass abort signals for cancellable operations
6. **Check ResponseArgs errors** - Most methods return `ResponseArgs<T>` with `.result` and `.error`
7. **Use computed signals** - Derive state reactively rather than manually tracking

## Testing

Mock implementations are provided for testing:

```typescript
import { CustomerBonRedemptionFacadeMock } from '@isa/crm/data-access';

TestBed.configureTestingModule({
  providers: [
    { provide: CustomerBonRedemptionFacade, useClass: CustomerBonRedemptionFacadeMock },
  ],
});
```

Run tests:

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

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

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