Technical DocumentationAPI ApplicationAgreement Module

Agreement Module & eSign Integration

Overview

The Agreement module provides CRUD operations for lease agreements with integrated electronic signature (eSign) functionality. The module enforces a draft-to-signed state transition, ensuring agreements can only be modified while in DRAFT status.

Key Features

  • CRUD Operations: Create, read, update, and delete lease agreements
  • Draft-to-Signed State Management: Agreements are locked once signed by all parties
  • Vendor-Abstracted eSign: Electronic signature integration with Setu (easily switchable to other providers)
  • Flexi eSign Support: Dynamic document generation with templates
  • Multi-Party Signing: Support for landlord/manager and tenant signatures

API Endpoints

Agreements CRUD

MethodEndpointDescription
POST/agreementsCreate a new agreement in DRAFT status
GET/agreementsGet all agreements
GET/agreements/:idGet a single agreement by ID
PATCH/agreements/:idUpdate an agreement (DRAFT only)
DELETE/agreements/:idSoft delete an agreement (DRAFT only)

eSign Operations

MethodEndpointDescription
GET/agreements/:id/signaturesGet signature status for an agreement
POST/agreements/:id/signInitiate eSign process for a signer

Agreement Lifecycle

┌─────────┐     ┌─────────┐     ┌─────────┐
│  DRAFT  │ ──► │ PENDING │ ──► │  ACTIVE │
└─────────┘     └─────────┘     └─────────┘
     │               │
     │               │         ┌───────────┐
     ▼               └───────► │TERMINATED │
  (delete)                     └───────────┘
  1. DRAFT: Initial state. Agreement can be created, modified, or deleted.
  2. PENDING: Signing has been initiated. Waiting for all parties to sign.
  3. ACTIVE: All parties have signed. Agreement is locked and in effect.
  4. TERMINATED: Agreement ended (manually or at tenure end).
  5. COMPLETED: Agreement fulfilled successfully.

State Transition Rules

  • DRAFT → PENDING: When first signature request is initiated
  • PENDING → ACTIVE: When all required parties have signed
  • Modification: Only allowed in DRAFT status
  • Deletion: Only allowed in DRAFT status

eSign Integration Architecture

The eSign functionality is implemented with a vendor-abstraction layer, allowing easy switching between providers.

Provider Interface

interface IESignProvider {
  getProvider(): ESignProvider;
  initiateSigningRequest(
    request: InitiateESignRequest
  ): Promise<InitiateESignResponse>;
  getSigningStatus(requestId: string): Promise<ESignStatusResponse>;
  cancelSigningRequest(requestId: string): Promise<boolean>;
  verifyWebhookSignature(payload: string, signature: string): boolean;
  parseWebhookPayload(payload: Record<string, unknown>): ESignStatusResponse;
}

Setu eSign Integration

The integration follows Setu’s official API documentation:

API Endpoints Used

Setu EndpointDescription
POST /api/esignCreate new eSign request
GET /api/esign/{id}Get eSign request status
GET /api/esign/{id}/pdfDownload signed document

Webhook Events

EventDescription
ESIGN_REQUEST_CREATEDeSign request created successfully
ESIGN_COMPLETEDocument signed successfully
ESIGN_EXPIREDRequest expired before signing
ESIGN_FAILEDeSign process failed
ESIGN_OTP_FAILEDOTP verification failed

Constants Configuration

All Setu-specific configuration is centralized in setu-esign.constants.ts:

// API Environments
SETU_ESIGN_ENVIRONMENTS = {
  SANDBOX: "https://dg-sandbox.setu.co",
  PRODUCTION: "https://dg.setu.co",
};
 
// Default Configuration
SETU_ESIGN_DEFAULTS = {
  VALIDITY_HOURS: 72,
  MAX_DOCUMENT_SIZE_BYTES: 10 * 1024 * 1024,
  REQUEST_TIMEOUT_MS: 30000,
  RETRY_ATTEMPTS: 3,
};
 
// Status Mappings (Setu → Standard)
mapSetuStatusToStandardStatus(setuStatus);
mapSetuWebhookEventToStatus(event);

Flexi eSign Support

For dynamic document generation with templates:

// Request with flexi-esign options
{
  documentId: "agreement-123",
  documentUrl: "https://...",
  signer: { ... },
  flexiOptions: {
    templateId: "template-xyz",
    signaturePosition: {
      pageNumber: 5,
      x: 100,
      y: 200,
      width: 200,
      height: 50
    },
    templateVariables: {
      "tenant_name": "John Doe",
      "rent_amount": "25000"
    }
  }
}

Adding a New Provider

  1. Create a new provider class implementing IESignProvider:

    // src/integrations/esign/new-provider.provider.ts
    @Injectable()
    export class NewESignProvider implements IESignProvider {
      // Implement all interface methods
    }
  2. Register the provider in ESignModule:

    @Module({
      providers: [SetuESignProvider, NewESignProvider, ESignService],
      exports: [ESignService],
    })
    export class ESignModule {}
  3. Add to ESignService constructor and providers map:

    constructor(
      private readonly setuProvider: SetuESignProvider,
      private readonly newProvider: NewESignProvider
    ) {
      this.providers.set(ESignProvider.SETU, setuProvider);
      this.providers.set(ESignProvider.NEW_PROVIDER, newProvider);
    }
  4. Add new enum value to ESignProvider in @repo/types:

    export enum ESignProvider {
      SETU = "setu",
      NEW_PROVIDER = "new_provider",
    }

Data Models

Agreement Entity

Agreement {
  id: number;                    // Primary key
  agreement_id: string;          // UUID for external reference
  agreement_number?: string;     // Human-readable number
  document_url: string;          // URL to agreement PDF
  property_unit_id: number;      // Reference to property unit
  agreement_start_date: Date;
  tenure_in_months: number;
  auto_renew: boolean;
  rent_amount: number;
  deposit_amount?: number;
  rent_due_day: number;          // 1-31
  notice_period_days: number;
  status: AgreementStatus;
  amenities?: string[];
  pets_allowed: boolean;
  non_veg_allowed: boolean;
  smoking_allowed: boolean;
  bachelor_allowed: boolean;
  additional_policies?: string;
  improvements?: string;
  signed_date?: Date;
  termination_date?: Date;
  payment_terms?: string;
  late_fee?: number;
}

AgreementSignature Entity

AgreementSignature {
  id: number;
  signature_id: string;          // UUID
  agreement_id: number;
  signer_id: number;
  signer_role: SignerRole;       // LANDLORD | MANAGER | TENANT
  status: SignatureStatus;       // PENDING | SIGNED | DECLINED | EXPIRED
  provider?: ESignProvider;
  provider_request_id?: string;
  provider_signature_id?: string;
  signing_url?: string;
  signed_at?: Date;
  expires_at?: Date;
  decline_reason?: string;
}

Usage Examples

Create Agreement

POST /agreements
Authorization: Bearer <jwt_token>
Content-Type: application/json
 
{
  "property_unit_id": 1,
  "document_url": "https://storage.example.com/agreements/lease.pdf",
  "agreement_start_date": "2024-01-01",
  "tenure_in_months": 12,
  "rent_amount": 25000,
  "deposit_amount": 75000,
  "rent_due_day": 5,
  "notice_period_days": 30,
  "tenant_ids": [2, 3]
}

Initiate Signing

POST /agreements/1/sign
Authorization: Bearer <jwt_token>
Content-Type: application/json
 
{
  "signer_id": 1
}

Response:

{
  "message": "Signing process initiated successfully",
  "data": {
    "signature_id": "uuid-123",
    "signing_url": "https://dg-sandbox.setu.co/esign/request_123",
    "expires_at": "2024-01-04T00:00:00.000Z",
    "status": "pending"
  }
}

Check Signature Status

GET /agreements/1/signatures
Authorization: Bearer <jwt_token>

Response:

{
  "message": "Signature status retrieved successfully",
  "data": {
    "signatures": [
      {
        "id": 1,
        "status": "signed",
        "signer_role": "landlord",
        "signed_at": "2024-01-02T10:30:00.000Z"
      },
      {
        "id": 2,
        "status": "pending",
        "signer_role": "tenant"
      }
    ],
    "allSigned": false
  }
}

Environment Variables

For Setu eSign integration, configure these environment variables:

# Setu eSign Configuration
SETU_ESIGN_API_URL=https://dg-sandbox.setu.co  # or https://dg.setu.co for production
SETU_ESIGN_CLIENT_ID=your_client_id
SETU_ESIGN_CLIENT_SECRET=your_client_secret
SETU_ESIGN_PRODUCT_INSTANCE_ID=your_product_instance_id
SETU_ESIGN_WEBHOOK_SECRET=your_webhook_secret
SETU_ESIGN_REDIRECT_URL=https://your-app.com/esign/callback

Error Handling

ErrorHTTP StatusDescription
Property unit not found404Property unit ID doesn’t exist
Agreement not found404Agreement ID doesn’t exist
User not found404Signer user ID doesn’t exist
Cannot modify agreement403Agreement is not in DRAFT status
Cannot delete agreement403Agreement is not in DRAFT status
Cannot sign agreement403Agreement is not in signable status
Already signed400User has already signed this agreement

Testing

Run the agreement module tests:

pnpm --filter api-app test -- agreement.service.spec.ts