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
| Method | Endpoint | Description |
|---|---|---|
| POST | /agreements | Create a new agreement in DRAFT status |
| GET | /agreements | Get all agreements |
| GET | /agreements/:id | Get a single agreement by ID |
| PATCH | /agreements/:id | Update an agreement (DRAFT only) |
| DELETE | /agreements/:id | Soft delete an agreement (DRAFT only) |
eSign Operations
| Method | Endpoint | Description |
|---|---|---|
| GET | /agreements/:id/signatures | Get signature status for an agreement |
| POST | /agreements/:id/sign | Initiate eSign process for a signer |
Agreement Lifecycle
┌─────────┐ ┌─────────┐ ┌─────────┐
│ DRAFT │ ──► │ PENDING │ ──► │ ACTIVE │
└─────────┘ └─────────┘ └─────────┘
│ │
│ │ ┌───────────┐
▼ └───────► │TERMINATED │
(delete) └───────────┘- DRAFT: Initial state. Agreement can be created, modified, or deleted.
- PENDING: Signing has been initiated. Waiting for all parties to sign.
- ACTIVE: All parties have signed. Agreement is locked and in effect.
- TERMINATED: Agreement ended (manually or at tenure end).
- 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:
- Quickstart: https://docs.setu.co/data/esign/quickstart
- Flexi eSign: https://docs.setu.co/data/esign/flexi-esign
API Endpoints Used
| Setu Endpoint | Description |
|---|---|
POST /api/esign | Create new eSign request |
GET /api/esign/{id} | Get eSign request status |
GET /api/esign/{id}/pdf | Download signed document |
Webhook Events
| Event | Description |
|---|---|
ESIGN_REQUEST_CREATED | eSign request created successfully |
ESIGN_COMPLETE | Document signed successfully |
ESIGN_EXPIRED | Request expired before signing |
ESIGN_FAILED | eSign process failed |
ESIGN_OTP_FAILED | OTP 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
-
Create a new provider class implementing
IESignProvider:// src/integrations/esign/new-provider.provider.ts @Injectable() export class NewESignProvider implements IESignProvider { // Implement all interface methods } -
Register the provider in
ESignModule:@Module({ providers: [SetuESignProvider, NewESignProvider, ESignService], exports: [ESignService], }) export class ESignModule {} -
Add to
ESignServiceconstructor 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); } -
Add new enum value to
ESignProviderin@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/callbackError Handling
| Error | HTTP Status | Description |
|---|---|---|
| Property unit not found | 404 | Property unit ID doesn’t exist |
| Agreement not found | 404 | Agreement ID doesn’t exist |
| User not found | 404 | Signer user ID doesn’t exist |
| Cannot modify agreement | 403 | Agreement is not in DRAFT status |
| Cannot delete agreement | 403 | Agreement is not in DRAFT status |
| Cannot sign agreement | 403 | Agreement is not in signable status |
| Already signed | 400 | User has already signed this agreement |
Testing
Run the agreement module tests:
pnpm --filter api-app test -- agreement.service.spec.ts