# Directive: hydratePlan Implementation Guide

**Author**: Matthew Raymer
**Date**: 2025-10-29 12:38:40 UTC
**Status**: 🎯 **ACTIVE** - Implementation directive for PlanActionClaim hydration

## Overview

This directive provides a complete implementation guide for creating a
`hydratePlan()` function that follows the same pattern as `hydrateGive()` and
`hydrateOffer()`. This function constructs and hydrates `PlanActionClaim`
entities (projects) for submission to the endorser server.

## Pattern Reference

The `hydratePlan()` function should follow the established pattern from:

- **`hydrateGive()`** (`src/libs/endorserServer.ts:831-920`)
- **`hydrateOffer()`** (`src/libs/endorserServer.ts:1018-1072`)

### Key Pattern Elements

1. **Function Signature**: Accepts optional original claim and parameters
2. **Clone or Create**: Clone existing claim or create new base structure
3. **Edit Detection**: Handle `lastClaimId` for edit operations
4. **Field Hydration**: Set optional fields only when provided
5. **Cleanup**: Use `undefined` to remove fields (not empty strings)
6. **Return**: Return properly typed claim object

## PlanActionClaim Interface

```typescript
export interface PlanActionClaim extends ClaimObject {
  "@context": "https://schema.org";
  "@type": "PlanAction";
  name: string;                              // REQUIRED
  agent?: { identifier: string };            // Optional: creator DID
  description?: string;                      // Optional: project description
  endTime?: string;                          // Optional: ISO 8601 date string
  identifier?: string;                       // Auto-set by server on first create
  image?: string;                            // Optional: image URL
  lastClaimId?: string;                      // Required for edits
  location?: {                               // Optional: geographic location
    geo: {
      "@type": "GeoCoordinates";
      latitude: number;
      longitude: number;
    };
  };
  startTime?: string;                        // Optional: ISO 8601 date string
  url?: string;                              // Optional: project URL
}
```

## Implementation

### Function Signature

```typescript
/**
 * Construct PlanAction VC for submission to server
 *
 * Creates or updates a PlanAction claim (project) with all optional fields
 * properly handled. Follows the same pattern as hydrateGive() and
 * hydrateOffer().
 *
 * @param vcClaimOrig - Optional existing claim to clone and modify
 * @param name - Required project name
 * @param agentDid - Optional DID of the project creator
 * @param description - Optional project description
 * @param startTime - Optional ISO 8601 start date/time string
 * @param endTime - Optional ISO 8601 end date/time string
 * @param imageUrl - Optional image URL for the project
 * @param url - Optional project URL
 * @param latitude - Optional location latitude
 * @param longitude - Optional location longitude
 * @param lastClaimId - Required when editing an existing project
 *
 * @returns Properly hydrated PlanActionClaim ready for submission
 *
 * @example
 * // Create new project
 * const claim = hydratePlan(
 *   undefined,
 *   "Community Garden Project",
 *   "did:ethr:0x123...",
 *   "Building a community garden",
 *   "2025-06-01T00:00:00Z",
 *   "2025-08-31T23:59:59Z"
 * );
 *
 * @example
 * // Edit existing project
 * const updated = hydratePlan(
 *   existingClaim,
 *   "Updated Project Name",
 *   "did:ethr:0x123...",
 *   "New description",
 *   undefined,
 *   undefined,
 *   undefined,
 *   undefined,
 *   undefined,
 *   undefined,
 *   "01ABC123..." // lastClaimId
 * );
 */
export function hydratePlan(
  vcClaimOrig?: PlanActionClaim,
  name?: string,
  agentDid?: string,
  description?: string,
  startTime?: string,
  endTime?: string,
  imageUrl?: string,
  url?: string,
  latitude?: number,
  longitude?: number,
  lastClaimId?: string,
): PlanActionClaim {
  // Clone existing claim or create base structure
  const vcClaim: PlanActionClaim = vcClaimOrig
    ? R.clone(vcClaimOrig)
    : {
        "@context": SCHEMA_ORG_CONTEXT,
        "@type": "PlanAction",
      };

  // Handle edit operation (lastClaimId indicates edit)
  if (lastClaimId) {
    vcClaim.lastClaimId = lastClaimId;
    // Remove identifier on edit - server assigns new one
    delete vcClaim.identifier;
  }

  // Set required name field (must be provided for new claims)
  if (name) {
    vcClaim.name = name;
  }

  // Handle agent (creator DID)
  if (agentDid) {
    vcClaim.agent = { identifier: agentDid };
  } else {
    // Remove agent if not provided (allows clearing)
    delete vcClaim.agent;
  }

  // Handle description
  vcClaim.description = description || undefined;

  // Handle start time (ISO 8601 format required)
  if (startTime) {
    // Validate it's a proper ISO 8601 string
    try {
      const date = new Date(startTime);
      if (isNaN(date.getTime())) {
        throw new Error("Invalid startTime format");
      }
      vcClaim.startTime = startTime;
    } catch {
      // Invalid date - remove the field
      delete vcClaim.startTime;
    }
  } else {
    delete vcClaim.startTime;
  }

  // Handle end time (ISO 8601 format required)
  if (endTime) {
    try {
      const date = new Date(endTime);
      if (isNaN(date.getTime())) {
        throw new Error("Invalid endTime format");
      }
      vcClaim.endTime = endTime;
    } catch {
      delete vcClaim.endTime;
    }
  } else {
    delete vcClaim.endTime;
  }

  // Handle image URL
  vcClaim.image = imageUrl || undefined;

  // Handle project URL
  vcClaim.url = url || undefined;

  // Handle location (both latitude and longitude required)
  if (latitude !== undefined && longitude !== undefined) {
    // Validate coordinates are numbers
    if (
      typeof latitude === "number" &&
      typeof longitude === "number" &&
      !isNaN(latitude) &&
      !isNaN(longitude)
    ) {
      vcClaim.location = {
        geo: {
          "@type": "GeoCoordinates",
          latitude,
          longitude,
        },
      };
    } else {
      // Invalid coordinates - remove location
      delete vcClaim.location;
    }
  } else {
    // Remove location if either coordinate missing
    delete vcClaim.location;
  }

  return vcClaim;
}
```

## Helper Functions (Recommended)

Following the pattern from `createAndSubmitGive()` and
`createAndSubmitOffer()`, create wrapper functions:

### Create and Submit

```typescript
/**
 * Create and submit a new PlanAction claim
 *
 * @param axios - Axios instance for HTTP requests
 * @param apiServer - Endorser API server URL
 * @param issuerDid - DID to sign the claim (must match agentDid or
 *                    be authorized)
 * @param name - Required project name
 * @param agentDid - Optional creator DID
 * @param description - Optional project description
 * @param startTime - Optional ISO 8601 start time
 * @param endTime - Optional ISO 8601 end time
 * @param imageUrl - Optional image URL
 * @param url - Optional project URL
 * @param latitude - Optional location latitude
 * @param longitude - Optional location longitude
 *
 * @returns Promise with submission result (handleId, claimId, etc.)
 */
export async function createAndSubmitPlan(
  axios: Axios,
  apiServer: string,
  issuerDid: string,
  name: string,
  agentDid?: string,
  description?: string,
  startTime?: string,
  endTime?: string,
  imageUrl?: string,
  url?: string,
  latitude?: number,
  longitude?: number,
): Promise<CreateAndSubmitClaimResult> {
  const vcClaim = hydratePlan(
    undefined,
    name,
    agentDid,
    description,
    startTime,
    endTime,
    imageUrl,
    url,
    latitude,
    longitude,
    undefined, // No lastClaimId for new claims
  );
  return createAndSubmitClaim(
    vcClaim as GenericVerifiableCredential,
    issuerDid,
    apiServer,
    axios,
  );
}
```

### Edit and Submit

```typescript
/**
 * Edit and submit an existing PlanAction claim
 *
 * @param axios - Axios instance for HTTP requests
 * @param apiServer - Endorser API server URL
 * @param fullClaim - Existing claim wrapper with ID
 * @param issuerDid - DID to sign the claim
 * @param name - Updated project name (if changed)
 * @param agentDid - Updated creator DID (if changed)
 * @param description - Updated description (if changed)
 * @param startTime - Updated start time (if changed)
 * @param endTime - Updated end time (if changed)
 * @param imageUrl - Updated image URL (if changed)
 * @param url - Updated project URL (if changed)
 * @param latitude - Updated location latitude (if changed)
 * @param longitude - Updated location longitude (if changed)
 *
 * @returns Promise with submission result
 */
export async function editAndSubmitPlan(
  axios: Axios,
  apiServer: string,
  fullClaim: GenericCredWrapper<PlanActionClaim>,
  issuerDid: string,
  name?: string,
  agentDid?: string,
  description?: string,
  startTime?: string,
  endTime?: string,
  imageUrl?: string,
  url?: string,
  latitude?: number,
  longitude?: number,
): Promise<CreateAndSubmitClaimResult> {
  const vcClaim = hydratePlan(
    fullClaim.claim,
    name,
    agentDid,
    description,
    startTime,
    endTime,
    imageUrl,
    url,
    latitude,
    longitude,
    fullClaim.id, // Use existing claim ID for edit
  );
  return createAndSubmitClaim(
    vcClaim as GenericVerifiableCredential,
    issuerDid,
    apiServer,
    axios,
  );
}
```

## Required Dependencies

Ensure these imports are available:

```typescript
import * as R from "ramda"; // For R.clone()
import { Axios } from "axios";
import { PlanActionClaim } from "../interfaces/claims";
import { 
  GenericCredWrapper,
  GenericVerifiableCredential,
  CreateAndSubmitClaimResult 
} from "../interfaces/common";

// Constants
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
```

## Usage Examples

### Example 1: Create Simple Project

```typescript
const claim = hydratePlan(
  undefined,              // New claim
  "My Project",          // Required name
  "did:ethr:0x123...",   // Creator DID
  "Project description"  // Description
);

// Result:
// {
//   "@context": "https://schema.org",
//   "@type": "PlanAction",
//   "name": "My Project",
//   "agent": { "identifier": "did:ethr:0x123..." },
//   "description": "Project description"
// }
```

### Example 2: Create Project with Dates and Location

```typescript
const claim = hydratePlan(
  undefined,
  "Community Event",
  "did:ethr:0x456...",
  "Annual community gathering",
  "2025-07-01T10:00:00Z",  // Start time
  "2025-07-03T18:00:00Z",  // End time
  "https://example.com/img.jpg",  // Image
  "https://example.com/event",     // URL
  40.7128,  // Latitude
  -74.0060  // Longitude
);
```

### Example 3: Edit Existing Project

```typescript
const existingClaim: PlanActionClaim = {
  "@context": "https://schema.org",
  "@type": "PlanAction",
  "name": "Old Name",
  "identifier": "01ABC123...",
  // ... other fields
};

const updated = hydratePlan(
  existingClaim,         // Original claim to clone
  "New Name",            // Updated name
  "did:ethr:0x123...",   // Keep same agent
  "Updated description", // Updated description
  undefined,             // Keep existing startTime
  undefined,             // Keep existing endTime
  undefined,             // Keep existing image
  undefined,             // Keep existing url
  undefined,             // Keep existing location
  undefined,             // Keep existing location
  "01ABC123..."          // Required: lastClaimId
);

// Result will have:
// - lastClaimId: "01ABC123..."
// - identifier: undefined (removed for edit)
// - Updated name and description
// - All other fields preserved from original
```

### Example 4: Remove Location from Existing Project

```typescript
const updated = hydratePlan(
  existingClaim,
  existingClaim.name,     // Keep name
  existingClaim.agent?.identifier,
  existingClaim.description,
  existingClaim.startTime,
  existingClaim.endTime,
  existingClaim.image,
  existingClaim.url,
  undefined,  // latitude undefined = remove location
  undefined,  // longitude undefined = remove location
  "01ABC123..."
);
```

## Important Notes

### Date/Time Handling

- **Format**: All date/time strings must be ISO 8601 format
- **Validation**: Function validates date strings but caller should ensure
  proper format
- **Timezone**: Include timezone information (Z for UTC, or +/- offset)
- **Example**: `"2025-06-01T10:00:00Z"` or `"2025-06-01T10:00:00-05:00"`

### Location Handling

- **Both Required**: Both `latitude` and `longitude` must be provided
  together, or both omitted
- **Validation**: Coordinates must be valid numbers (not NaN)
- **Removal**: Setting either to `undefined` removes the entire location
  object
- **Range**: Latitude: -90 to 90, Longitude: -180 to 180 (validation
  recommended)

### Edit Operations

- **lastClaimId Required**: Must provide `lastClaimId` when editing
- **identifier Removed**: Server assigns new identifier on edit
- **Field Preservation**: Only explicitly changed fields are modified;
  others preserved from original
- **Undefined vs Missing**: Use `undefined` to explicitly remove optional
  fields

### Field Cleanup

- **Use undefined**: Set fields to `undefined` rather than empty strings
- **Delete for Edit**: For edits, explicitly delete fields that should be
  removed
- **Optional Field Pattern**: Check existence before setting to preserve
  optional nature

## Testing Recommendations

### Unit Tests

Test the following scenarios:

1. **New Claim Creation**
   - Minimum required fields (name only)
   - All optional fields provided
   - Validate structure matches interface

2. **Edit Operations**
   - Clone existing claim correctly
   - lastClaimId sets identifier removal
   - Partial field updates preserve other fields

3. **Field Validation**
   - Invalid date strings removed
   - Location requires both coordinates
   - Undefined fields properly handled

4. **Edge Cases**
   - Empty string handling (should become undefined)
   - NaN coordinate values
   - Missing required fields on new claims

### Integration Tests

Test with actual server submission:

```typescript
// Test createAndSubmitPlan
const result = await createAndSubmitPlan(
  axiosInstance,
  "https://test-api.endorser.ch",
  testDid,
  "Test Project",
  testDid,
  "Test description"
);

expect(result.success).toBe(true);
expect(result.handleId).toBeDefined();
```

## Security Considerations

1. **DID Validation**: Validate agentDid matches issuerDid or is authorized
2. **Date Validation**: Ensure date strings don't contain injection attempts
3. **URL Validation**: Validate image and URL fields are proper URLs
4. **Coordinate Validation**: Ensure lat/lng are within valid ranges

## References

- **hydrateGive()**: `src/libs/endorserServer.ts:831-920`
- **hydrateOffer()**: `src/libs/endorserServer.ts:1018-1072`
- **PlanActionClaim Interface**: `src/interfaces/claims.ts:76-91`
- **Pattern Example**: `src/views/NewEditProjectView.vue:536-597`

## Implementation Checklist

- [ ] Implement `hydratePlan()` function with all parameters
- [ ] Add date/time validation logic
- [ ] Add location coordinate validation
- [ ] Implement `createAndSubmitPlan()` wrapper
- [ ] Implement `editAndSubmitPlan()` wrapper
- [ ] Add comprehensive JSDoc comments
- [ ] Write unit tests for all scenarios
- [ ] Write integration tests with server
- [ ] Update any existing code to use new function
- [ ] Document breaking changes if refactoring existing code

---

**Status**: Active implementation directive
**Priority**: Medium
**Estimated Effort**: 2-4 hours for complete implementation including tests
**Dependencies**: Requires ramda, axios, and existing claim interfaces
**Stakeholders**: Development team implementing claim hydration utilities

