npm package: @ritas-inc/sapb1commandapi-client
Description: A stateless TypeScript client for SAP B1 Service Layer Command API with comprehensive error handling, type safety, and batch operations
Author: Ritas Inc
Latest version: 1.5.3

# @ritas-inc/sapb1commandapi-client

A stateless TypeScript client for the SAP B1 Service Layer Command API. This client provides a clean, type-safe interface for managing production plans and work orders in SAP Business One.

## Features

- 🚀 **Modern TypeScript**: Built for Node.js 24+ with native ESM support
- 🔒 **Type-safe**: Full TypeScript support with Zod schema validation
- 🔄 **Automatic retry**: Built-in retry logic for network failures
- 🏗️ **Stateless design**: No session management needed - perfect for serverless
- 📦 **Minimal dependencies**: Only axios, axios-retry, and zod required
- 🛡️ **Comprehensive error handling**: Detailed error messages with RFC 7807 Problem Details format
- ✅ **Consistent API responses**: All responses follow a unified success/error structure

## Requirements

- Node.js >= 24.0.0
- SAP B1 Service Layer Command API endpoint

## Installation

```bash
npm install @ritas-inc/sapb1commandapi-client
```

## Quick Start

```typescript
import { SAPB1CommandClient } from '@ritas-inc/sapb1commandapi-client';

// Initialize the client
const client = new SAPB1CommandClient({
  baseUrl: 'https://your-api-endpoint.com',
  timeout: 30000,
  retryConfig: {
    retries: 3
  }
});

// Authenticate
const authResponse = await client.auth.login({
  dbName: 'YourDatabase',
  user: 'manager',
  password: 'your-password'
});

if (authResponse.success) {
  const userId = authResponse.data.userId;
  
  // Create a plan
  const planResponse = await client.plans.create(userId, 12345, [
    { itemCode: 'ITEM001', quantity: 10 },
    { itemCode: 'ITEM002', quantity: 5 }
  ]);
  
  if (planResponse.success) {
    console.log(`Created plan with ID: ${planResponse.data.planId}`);
  }
}
```

## Idempotency

All mutating operations (POST, PATCH, DELETE) support idempotency to prevent duplicate operations when requests are retried.

### What is Idempotency?

Idempotency ensures that multiple identical requests have the same effect as a single request. This is critical for:
- **Retry safety**: Safely retry failed requests without creating duplicates
- **Network resilience**: Handle connection timeouts and network errors
- **Reliability**: Guarantee operations complete exactly once

### Usage

Pass an optional `idempotencyKey` parameter (must be a valid UUID) to any mutating operation:

```typescript
import { SAPB1CommandClient, generateIdempotencyKey } from '@ritas-inc/sapb1commandapi-client';

const client = new SAPB1CommandClient({ baseUrl: 'https://your-api.com' });

// Generate a UUID v4 idempotency key
const idempotencyKey = generateIdempotencyKey();

// Create a plan with idempotency
const response = await client.plans.create(
  userId,
  12345,
  [{ itemCode: 'ITEM001', quantity: 10 }],
  null,           // salesFromDate
  null,           // salesToDate
  idempotencyKey  // UUID for idempotent requests
);

// If the request fails or times out, retry with the same key
if (!response.success && response.problem.type === 'network-error') {
  // Retry with the SAME idempotency key
  const retryResponse = await client.plans.create(
    userId,
    12345,
    [{ itemCode: 'ITEM001', quantity: 10 }],
    null,
    null,
    idempotencyKey  // Same key = no duplicate created
  );
}
```

### Key Requirements

- **Format**: Must be a valid UUID (v1-v5)
- **Uniqueness**: Each distinct operation should have a unique key
- **Consistency**: Use the same key for retrying the same operation
- **Storage**: Store the key for 24 hours (API retention period)

### Utility Functions

```typescript
import {
  generateIdempotencyKey,
  isValidIdempotencyKey
} from '@ritas-inc/sapb1commandapi-client';

// Generate a new UUID v4 key
const key = generateIdempotencyKey();
// "550e8400-e29b-41d4-a716-446655440000"

// Validate a key format
isValidIdempotencyKey('550e8400-e29b-41d4-a716-446655440000'); // true
isValidIdempotencyKey('invalid-key'); // false
```

### Supported Operations

All mutating operations support idempotency:

**Plans**:
- `client.plans.create(userId, user, products, salesFromDate, salesToDate, idempotencyKey)`
- `client.plans.updateStatus(userId, planId, status, idempotencyKey)`
- `client.plans.updateProducts(userId, planId, products, salesFromDate, salesToDate, idempotencyKey)`
- `client.plans.updateReleaseDate(userId, planId, releaseDate, idempotencyKey)`
- `client.plans.cancel(userId, planId, idempotencyKey)`

**Work Orders**:
- `client.workOrders.create(userId, planId, workOrder, origin, idempotencyKey)`
- `client.workOrders.release(userId, workOrderId, idempotencyKey)`
- `client.workOrders.cancel(userId, workOrderId, idempotencyKey)`
- `client.workOrders.close(userId, workOrderId, idempotencyKey)`
- `client.workOrders.batch.create(userId, workOrders, idempotencyKey)`
- `client.workOrders.batch.release(userId, workOrderIds, idempotencyKey)`
- `client.workOrders.batch.cancel(userId, workOrderIds, idempotencyKey)`
- `client.workOrders.batch.close(userId, workOrderIds, idempotencyKey)`

**Tags**:
- `client.tags.create(userId, workorderEntry, idempotencyKey)`
- `client.tags.activate(userId, tagCode, options, idempotencyKey)`
- `client.tags.quantify(userId, tagCode, request, idempotencyKey)`
- `client.tags.discard(userId, tagCode, options, idempotencyKey)`
- `client.tags.clone(userId, tagCode, request, idempotencyKey)`
- `client.tags.clear(userId, tagCode, idempotencyKey)`
- `client.tags.createMultiple(userId, workorderEntry, count, idempotencyKey)`
- `client.tags.batch.create(userId, tags, idempotencyKey)`

**Production Entries**:
- `client.productionEntries.create(userId, lines, journalMemo, idempotencyKey)`

### Error Responses

Invalid idempotency key format returns a 400 error:

```typescript
{
  success: false,
  problem: {
    status: 400,
    type: 'bad-request',
    title: 'Invalid Idempotency Key Format',
    detail: 'Idempotency-Key header must be a valid UUID format (e.g., "550e8400-e29b-41d4-a716-446655440000")',
    issues: ['Expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (UUID v1-v5)']
  }
}
```

Concurrent requests with the same key return a 409 conflict:

```typescript
{
  success: false,
  problem: {
    status: 409,
    type: 'bad-request',
    title: 'Request Already Processing',
    detail: 'A request with idempotency key "..." is currently being processed',
    issues: ['Original request ID: req-0001']
  }
}
```

### Best Practices

1. **Generate keys per operation**: Create a new key for each distinct operation
```typescript
// Good: Different keys for different operations
const planKey = generateIdempotencyKey();
const workOrderKey = generateIdempotencyKey();

await client.plans.create(userId, user, products, null, null, planKey);
await client.workOrders.create(userId, planId, workOrder, origin, workOrderKey);
```

2. **Store keys for retries**: Persist keys to retry failed operations
```typescript
const operation = {
  idempotencyKey: generateIdempotencyKey(),
  userId,
  planId,
  products
};

// Store operation to database
await db.save(operation);

try {
  await client.plans.create(userId, user, products, null, null, operation.idempotencyKey);
} catch (error) {
  // Retry later with same key from database
}
```

3. **Handle cached responses**: Successful requests are cached for 24 hours
```typescript
const key = generateIdempotencyKey();

// First request creates the resource
const response1 = await client.plans.create(userId, user, products, null, null, key);
// Returns: { success: true, data: { planId: 123 } }

// Duplicate request returns cached response (no duplicate created)
const response2 = await client.plans.create(userId, user, products, null, null, key);
// Returns: { success: true, data: { planId: 123 } } (same response)
```

## API Response Structure

All API responses follow a consistent structure:

### Success Response
```typescript
{
  success: true,
  data: T,           // Response data (type varies by endpoint)
  metadata?: {       // Optional metadata
    // Additional information like timestamps, user info, etc.
  }
}
```

### Error Response
```typescript
{
  success: false,
  problem: {
    status: number,        // HTTP status code
    type: 'unauthorized' | 'bad-request' | 'not-found' | 'internal-server-error',          // Error type identifier
    title: string,         // Error title
    detail: string,        // Detailed error message
    instance: string,      // Request path
    context: {
      request: string,     // Request method and path
      responseText: string // Raw response text
    },
    issues: string[]       // List of specific issues
  }
}
```

## API Reference

### Client Configuration

```typescript
const client = new SAPB1CommandClient({
  baseUrl: string;              // Required: API base URL
  timeout?: number;             // Optional: Request timeout (default: 30000ms)
  retryConfig?: {
    retries?: number;           // Number of retries (default: 3)
    retryDelay?: (retryCount: number) => number;
    retryCondition?: (error: any) => boolean;
  };
  headers?: Record<string, string>; // Optional: Custom headers
});
```

### Authentication

#### Login

```typescript
const response = await client.auth.login({
  dbName: string;
  user: string;
  password: string;
});

// Success response type:
{
  success: true,
  data: {
    userId: string
  },
  metadata?: {
    CompanyDB: string,
    UserName: string
  }
}
```

The `userId` from the response must be passed to all subsequent authenticated requests.

### Session Management

#### Check Session Validity

Validate if your current SAP B1 session is still active. Useful for long-running applications or when you need to verify session status before performing operations.

```typescript
// Detailed session check
const sessionStatus = await client.session.checkSession(userId);

if (sessionStatus.data.sessionValid) {
  console.log('Session is active');
  console.log('Session checked at:', sessionStatus.metadata.checkedAt);
} else {
  console.log('Session is invalid');
}
```

#### Simple Boolean Check

For convenience, you can also use a simple boolean check:

```typescript
const isValid = await client.session.isSessionValid(userId);

if (!isValid) {
  // Re-authenticate
  const authResponse = await client.auth.login(credentials);
  userId = authResponse.data.userId;
}

// Proceed with operations
await client.plans.create(userId, planData);
```

The `isSessionValid()` method automatically handles authentication errors and returns `false` for invalid sessions, while re-throwing other types of errors (network issues, service problems).

### Plans Management

#### Create Plan

```typescript
const response = await client.plans.create(
  userId: string,
  user: number,        // Planner user ID
  products: Array<{
    itemCode: string;
    quantity: number;  // Must be positive
  }>
);

// Success response type:
{
  success: true,
  data: {
    planId: number
  }
}
```

#### Update Plan Status

```typescript
const response = await client.plans.updateStatus(
  userId: string,
  planId: number,
  status: 'Draft' | 'Generating' | 'Generated' | 'Releasing' | 
          'Released' | 'Completed' | 'Canceling' | 'Canceled'
);
```

#### Update Plan Products

```typescript
const response = await client.plans.updateProducts(
  userId: string,
  planId: number,
  products: Array<{
    itemCode: string;
    quantity: number;
  }>
);
```

#### Cancel Plan

```typescript
const response = await client.plans.cancel(userId: string, planId: number);
```

### Work Orders Management

#### Create Work Order

```typescript
const response = await client.workOrders.create(
  userId: string,
  planId: number,
  workOrder: {
    itemCode: string;
    quantity: number;
    injections?: number;
    productionSector?: string;
  },
  origin?: {
    origin: 'manual' | 'workorder',
    originAbsoluteEntry: number,
    originDocumentNumber: number
  }
);

// Success response type:
{
  success: true,
  data: {
    absoluteEntry: number,
    documentEntry: number,
    createDate: string
  }
}
```

#### Release Work Order

```typescript
const response = await client.workOrders.release(
  userId: string,
  workOrderId: number
);
```

#### Cancel Work Order

```typescript
const response = await client.workOrders.cancel(
  userId: string,
  workOrderId: number
);
```

### Batch Operations

#### Create Multiple Work Orders

```typescript
const response = await client.workOrders.batch.create(userId, [
  {
    planId: number,
    workOrder: {
      itemCode: string,
      quantity: number,
      injections?: number,
      productionSector?: string
    },
    origin: {
      origin: 'manual' | 'workorder',
      originAbsoluteEntry: number,
      originDocumentNumber: number
    }
  },
  // ... more work orders
]);

// Success response type:
{
  success: true,
  data: Array<{
    absoluteEntry: number,
    documentEntry: number,
    createDate: string
  }>
}
```

#### Release Multiple Work Orders

```typescript
const response = await client.workOrders.batch.release(
  userId: string,
  workOrderIds: number[]
);

// Success response type:
{
  success: true,
  data: Array<{
    absoluteEntry: number
  }>
}
```

#### Cancel Multiple Work Orders

```typescript
const response = await client.workOrders.batch.cancel(
  userId: string,
  workOrderIds: number[]
);

// Success response type:
{
  success: true,
  data: Array<{
    absoluteEntry: number
  }>
}
```

### Health Check

```typescript
const health = await client.health(); // No authentication required
```

## Error Handling

The client provides detailed error information through custom error classes:

```typescript
import { 
  SAPB1APIError, 
  AuthError, 
  NetworkError, 
  ValidationError,
  isErrorResponse 
} from '@ritas-inc/sapb1commandapi-client';

try {
  const response = await client.auth.login({ ... });
  
  // Type-safe error checking
  if (!response.success) {
    console.error('Login failed:', response.problem.detail);
    console.error('Issues:', response.problem.issues);
    return;
  }
  
  // Success - TypeScript knows response.data exists
  const userId = response.data.userId;
  
} catch (error) {
  if (error instanceof AuthError) {
    console.error('Authentication failed:', error.detail);
    console.error('Issues:', error.issues);
  } else if (error instanceof ValidationError) {
    console.error('Validation failed:', error.issues);
  } else if (error instanceof NetworkError) {
    console.error('Network error:', error.message);
  } else if (error instanceof SAPB1APIError) {
    console.error('API error:', {
      status: error.status,
      type: error.type,
      title: error.title,
      detail: error.detail,
      issues: error.issues
    });
  }
}
```

### Using the Error Response Helper

```typescript
const response = await client.plans.create(userId, user, products);

if (isErrorResponse(response)) {
  // TypeScript knows this is an error response
  console.error(`Error: ${response.problem.detail}`);
} else {
  // TypeScript knows this is a success response
  console.log(`Created plan: ${response.data.planId}`);
}
```

## Stateless Design

This client is completely stateless - it does not store any session information. You must:

1. Store the `userId` returned from login
2. Pass the `userId` to every authenticated API call
3. Handle session expiry (30 minutes) by re-authenticating when needed

This design makes the client perfect for:
- Serverless functions
- Multi-tenant applications
- Microservices
- Load-balanced environments

## TypeScript Support

All methods are fully typed with TypeScript. The client exports all necessary types:

```typescript
import type {
  // Request types
  AuthRequest,
  CreatePlanRequest,
  UpdatePlanStatusRequest,
  UpdatePlanProductsRequest,
  CreateWorkOrderItem,
  
  // Response types
  AuthResponse,
  CreatePlanResponse,
  UpdatePlanStatusResponse,
  CreateWorkOrderResponse,
  CreateWorkOrderBatchResponse,
  
  // Enum types
  PlanStatus,
  WorkOrderOriginType,
  
  // Configuration
  ClientConfig
} from '@ritas-inc/sapb1commandapi-client';
```

## Testing

This package includes comprehensive tests:

### Unit Tests
```bash
npm test
```

### Integration Tests
Integration tests perform a complete API lifecycle test:

1. Authenticate with SAP B1
2. Create plan with 3 products
3. Update plan with 4 products
4. Generate 4 work orders
5. Release and cancel individual work orders
6. Batch release and cancel work orders

Configure test environment:
```bash
cp .env.test.example .env.test
# Edit .env.test with your test API credentials
INTEGRATION_TEST=true npm test
```

See [tests/README.md](tests/README.md) for detailed testing documentation.

## Migration Guide from v1.0.x

Version 1.1.0 introduces a new response structure. Here are the key changes:

### Response Structure
All responses now follow a consistent success/error pattern:

```typescript
// Before (v1.0.x)
const { userId } = await client.auth.login(credentials);

// After (v1.1.0)
const response = await client.auth.login(credentials);
if (response.success) {
  const userId = response.data.userId;
}
```

### Error Handling
Errors are now included in the response object:

```typescript
// Before (v1.0.x)
try {
  const plan = await client.plans.create(userId, user, products);
} catch (error) {
  // Handle error
}

// After (v1.1.0)
const response = await client.plans.create(userId, user, products);
if (!response.success) {
  console.error(response.problem.detail);
}
```

### Type Imports
Types are now exported from schema files:

```typescript
// Before (v1.0.x)
import type { AuthRequest } from '@ritas-inc/sapb1commandapi-client/types';

// After (v1.1.0)
import type { AuthRequest } from '@ritas-inc/sapb1commandapi-client';
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## Support

For issues and feature requests, please use the [GitHub issue tracker](https://github.com/ritas-inc/sapb1commandapi-client/issues).