# Copilot Instructions for `@studyportals/sp-hs-misc`
> **Extends:** [Organization-Level Copilot Instructions](https://github.com/studyportals/.github/blob/main/.github/copilot-instructions.md)  
> Copilot: Apply all org-level rules first, then enforce the **repository-specific** rules below when reviewing PRs.

## Project Overview

This is a **shared utility library** used across HouseStark projects at Studyportals. The package provides reusable code for:

- **ServiceLayer communication** (HTTP client, data model factories)
- **AWS Cognito authentication** (deprecated - use `@studyportals/client-internal-platform-sso`)
- **Request signing and throttling** (deprecated - use `@studyportals/mb-platform-http-requests`)
- **User authorization** (deprecated - use `@studyportals/client-internal-platform-authorization`)
- **Data structures** (Dictionary, TreeNode, TimeInterval)
- **Caching** (EagerInMemoryCache)
- **Utility helpers** (ArrayHelper, MatrixHelper, Randomization, Throttler)

### Package Information

- **Package name:** `@studyportals/sp-hs-misc`
- **Version:** 3.x
- **Entry point:** `bin/index.js` (types: `bin/index.d.ts`)
- **Node.js:** >=24.0.0 <25.0.0
- **npm:** >=11.0.0 <12.0.0
- **TypeScript target:** ES2024
- **Module system:** CommonJS with NodeNext resolution

---

## Architecture & Key Dependencies

### External Dependencies

| Package | Purpose |
|---------|---------|
| `@studyportals/sp-millennium-falcon` | Domain models (ICampaign, IStudy, IOrganisation, etc.) |
| `amazon-cognito-identity-js` | AWS Cognito authentication |
| `@aws-sdk/client-cognito-identity` | AWS Cognito identity client |
| `@aws-sdk/client-cognito-identity-provider` | AWS Cognito provider client |
| `aws4` | AWS Signature Version 4 request signing |
| `superagent` | HTTP client (v3.x - legacy) |
| `js-cookie` | Browser cookie management |
| `qs` | Query string parsing |

### Module Structure

```
src/
├── adapters/           # ServiceLayer client & data transformation
├── caching/            # In-memory cache implementations
├── data-structures/    # Dictionary, TreeNode, TimeInterval
├── logging/            # Console logger (ILogger interface)
├── requests/           # Request signing, throttling, sending
├── security/           # Authentication & authorization (deprecated)
└── utils/              # Array/Matrix helpers, Throttler, Randomization
```

---

## Key Classes & Usage

### ServiceLayer Client

The `ServiceLayerClient` is a **generic HTTP client** for communicating with Studyportals ServiceLayer API.

```typescript
import { ServiceLayerClient, BaseSuperAgentRequestFactory } from '@studyportals/sp-hs-misc';

const requestFactory = new BaseSuperAgentRequestFactory();
const client = new ServiceLayerClient(requestFactory, 'https://servicelayer.example.com', 3);

// GET request (no caching)
const data = await client.get('/data/organisations/any/details/');

// GET request (with caching headers)
const cachedData = await client.getCached('/data/studies/any/list/');

// POST/PUT/DELETE requests (generic - use responsibly)
await client.put('/data/campaigns/any/details/123/customproducts/', { entity_id: 456 });
```

**Important:** The `ServiceLayerClient` is a generic client. Write methods (`post`, `put`, `delete`) should NOT be used for main campaign entity endpoints (`/data/campaigns/any/details/`) as these are now read-only. Only custom products and preset products sub-component endpoints still allow writes.

### ServiceLayerDataModelsFactory

Transforms ServiceLayer JSON responses into typed domain models:

```typescript
import { ServiceLayerDataModelsFactory } from '@studyportals/sp-hs-misc';

const factory = new ServiceLayerDataModelsFactory();

// Transform raw API data to typed models
const campaign = factory.createCampaign(serviceLayerCampaignData);
const study = factory.createStudy(serviceLayerStudyData);
const organisation = factory.createOrganisation(serviceLayerOrgData);
const country = factory.createCountry(serviceLayerCountryData);
const venue = factory.createVenue(serviceLayerVenueData);
```

**Campaign Types Supported:**
- `rbp`, `nrbp`, `hybrid_ebp`, `full_ebp`, `free_trial`
- Any other value maps to `CampaignType.UNKNOWN`

### Caching

```typescript
import { EagerInMemoryCache } from '@studyportals/sp-hs-misc';

// Extend for custom caching logic
class MyCampaignsCache extends EagerInMemoryCache<string, ICampaign> {
    protected async retrieveData(): Promise<Array<[string, ICampaign]>> {
        // Fetch and return key-value pairs
    }
}
```

### Data Structures

```typescript
import { Dictionary, TreeNode, TimeInterval } from '@studyportals/sp-hs-misc';

// Dictionary with lazy initialization
const dict = new Dictionary<string, number[]>();
const values = dict.obtain('key', () => []);  // Creates [] if key doesn't exist

// TreeNode for hierarchical data
const root = new TreeNode<string>();
root.content = 'Root';
const child = new TreeNode<string>(root);
root.addChild(child);

// TimeInterval for date ranges
const interval = new TimeInterval(startDate, endDate);
const overlaps = interval.overlaps(otherInterval);
```

### Request Throttling

```typescript
import { Throttler, Throttleable } from '@studyportals/sp-hs-misc';

const throttler = new Throttler(6);  // Max 6 concurrent operations
throttler.enqueueTask(myThrottleableTask);
```

---

## Deprecated Classes

The following classes are deprecated and should be replaced:

| Deprecated Class | Replacement Package |
|-----------------|---------------------|
| `CognitoAuthenticationServicesProvider` | `@studyportals/client-internal-platform-sso` |
| `UserSessionCookieManager` | `@studyportals/client-internal-platform-sso` |
| `K2SOUserPrivilegesDataHelper` | `@studyportals/client-internal-platform-authorization` |
| `AWS4RequestSigner` | `@studyportals/mb-platform-http-requests` |
| `SignedRequestSender` | `@studyportals/mb-platform-http-requests` |

---

## Development Guidelines

### Code Style

- **Indentation:** Tabs (not spaces)
- **Max line length:** 200 characters
- **Linting:** TSLint (see `tslint.json`)
- **Strict TypeScript:** `strictNullChecks`, `noImplicitAny`, `noImplicitReturns`
- **Experimental decorators:** Enabled (for test framework)

### Testing

- **Framework:** Mocha with `@testdeck/mocha` decorators
- **Mocking:** TypeMoq
- **Assertions:** Chai
- **Coverage:** NYC

```bash
# Run all tests
npm test

# Run tests without coverage
npm run test-u

# Run tests only (no build)
npm run test-u-o

# Generate coverage report
npm run coverage
```

**Test file conventions:**
- Tests live in `tests-u/` mirroring `src/` structure
- Test files named `*.test.ts`
- Use `@suite` and `@test` decorators from `@testdeck/mocha`
- Use `before()` method for test setup

### Build & Deploy

```bash
# Build TypeScript to bin/
npm run build

# Publish to npm registry
npm run deploy

# Version bump and publish
npm run deploy-patch   # 3.0.0 -> 3.0.1
npm run deploy-minor   # 3.0.0 -> 3.1.0
npm run deploy-major   # 3.0.0 -> 4.0.0
npm run deploy-beta    # 3.0.0 -> 3.0.1-0 (beta tag)
```

---

## Important Context: Campaigns API Migration

This package was recently updated for the **Campaigns Table Revamp** (see `Campaigns-Revamp-Migration-Plan.md`).

### Key Changes Made

1. **`ServiceLayerDataModelsFactory.createCampaign()`**
   - No longer extracts `external_notes` (field dropped from API)
   - `CampaignDto` constructor now takes 7 arguments (was 8)

2. **`translateCampaignType()`**
   - Removed cases for `advanced_nrbp` and `rbp_cpl` (deprecated enum values)
   - Only supports: `rbp`, `nrbp`, `hybrid_ebp`, `full_ebp`, `free_trial`

3. **Dependencies**
   - Updated to `@studyportals/sp-millennium-falcon` ^3.0.0

### Campaign API Constraints

When working with campaign-related code:

- **DO NOT** call write methods (`post`, `put`, `delete`) on `/data/campaigns/any/details/` endpoints
- **ALLOWED** write methods only on:
  - `/data/campaigns/any/details/{id}/customproducts/`
  - `/data/campaigns/any/details/{id}/presetproducts/`
- **REMOVED** sub-component endpoints (no longer exist):
  - `/data/campaigns/any/details/{id}/banners/` — deprecated, removed
  - `/data/campaigns/any/details/{id}/lead-campaigns/` — deprecated, removed
  - `/data/campaigns/any/details/{id}/organisations/` — use Campaign Management API
  - `/data/campaigns/any/details/{id}/countries/` — deprecated, removed
- **Expect** all campaign fields to be non-null (API now enforces NOT NULL constraints)
- **Valid campaign types:** `rbp`, `nrbp`, `hybrid_ebp`, `full_ebp`, `free_trial`
- **Valid currencies:** `EUR`, `GBP`, `USD`, `AUD`, `CAD`

---

## Common Patterns

### Extending ServiceLayerClient

```typescript
class MyServiceLayerClient extends ServiceLayerClient {
    async getCampaigns(ids: string[]): Promise<ICampaign[]> {
        const path = `/data/campaigns/any/details/?ids=${ids.join(',')}`;
        const response = await this.get(path);
        return Object.values(response).map(data => 
            this.factory.createCampaign(data)
        );
    }
}
```

### Extending EagerInMemoryCache

```typescript
class OrganisationsCache extends EagerInMemoryCache<string, IOrganisation> {
    constructor(private client: ServiceLayerClient) {
        super();
    }

    protected async retrieveData(): Promise<Array<[string, IOrganisation]>> {
        const factory = new ServiceLayerDataModelsFactory();
        const response = await this.client.get('/data/organisations/any/list/');
        return Object.entries(response).map(([id, data]) => 
            [id, factory.createOrganisation(data)]
        );
    }

    protected createCacheFromData(data: Array<[string, IOrganisation]>): Map<string, IOrganisation> {
        return new Map(data);
    }
}
```

### Test Setup Pattern

```typescript
import { suite, test } from "@testdeck/mocha";
import { assert } from "chai";
import * as Moq from "typemoq";

@suite
class MyClassTest {
    private testInstanceMock: Moq.IMock<MyClass>;
    private dependencyMock: Moq.IMock<IDependency>;

    private get testInstance(): MyClass {
        return this.testInstanceMock.object;
    }

    public before() {
        this.dependencyMock = Moq.Mock.ofType<IDependency>();
        this.testInstanceMock = Moq.Mock.ofType(MyClass);
        this.testInstanceMock.callBase = true;
        this.testInstanceMock
            .setup(_ => _['dependency'])
            .returns(() => this.dependencyMock.object);
    }

    @test
    public myTest_Condition_ExpectedResult() {
        // Arrange
        this.dependencyMock
            .setup(x => x.someMethod())
            .returns(() => 'value');

        // Act
        const result = this.testInstance.methodUnderTest();

        // Assert
        assert.equal(result, 'expectedValue');
    }
}
```

---

## Troubleshooting

### Build Errors

**"Cannot find module '@studyportals/sp-millennium-falcon'"**
- Ensure you have access to the Studyportals npm registry
- Run `npm install` to fetch dependencies

**TypeScript errors about missing types**
- Check that `@types/*` packages are installed as devDependencies
- Ensure `tsconfig.json` includes proper `typeRoots`

### Test Failures

**"Cookies.get is not a function"**
- Ensure `@types/js-cookie` matches the installed `js-cookie` version
- `js-cookie` v3.x requires `@types/js-cookie` ^3.0.0

**Mocha decorator issues**
- Use `@testdeck/mocha` (not the deprecated `mocha-typescript`)
- Ensure `experimentalDecorators` is enabled in `tsconfig.json`

---

## Related Documentation

- [Class Diagrams](https://www.lucidchart.com/documents/view/6df6b7d7-357a-4cfc-bc7d-c3857140498f)
- [Campaigns Revamp Migration Plan](../Campaigns-Revamp-Migration-Plan.md)
