Skip to content

Documents — E-Signing (Leegality) & PAP Agreements

This document describes how the MAS101 Pay-After-Placement (PAP) agreement is generated, signed electronically through Leegality (Aadhaar-OTP eSign), captured back into the system, and reviewed by admins. It covers the agreement-template configuration, the per-student PAP workflow state machine, PDF generation/filling (structured + WYSIWYG HTML), the Leegality request/callback (poll) sequence, and the upfront/PAP payment + placement stages that bracket the signing step.

Status: documented from source on this branch. All endpoints, statuses, enums, and behaviors below are derived from the actual TypeScript source under mr-mentor-backend/src (controllers, services, entities, routes) and the backend docs docs/LEEGALITY_SANDBOX_INTEGRATION.md / docs/LEEGALITY_TEMPLATE_SETUP_GUIDE.md.


Overview

MAS101 is a Pay-After-Placement program: a student pays a small registration fee up front, then signs a legal MOU (the Pay after Placement Agreement) committing to either an upfront discounted payment or a PAP success-fee payable after placement. The signing is done electronically via Leegality with Aadhaar-based OTP consent, so no wet signature is required.

The domain spans three personas:

Persona Where What they do
Student mas-website-live /student/application Fills MOU details, uploads a signature image, starts/refreshes the Leegality eSign, optionally uploads a manually-signed PDF, picks a payment option, pays the upfront amount, submits placement proof.
Admin / Sales Head mr-mentor-frontend admin panel Configures the active agreement template (amounts, PDF body, Leegality credentials), reviews signed MOUs, approves/rejects PAP enrollment and placement, deboards/restores students.
System mr-mentor-backend Creates the workflow on payment-received, generates/fills the PDF, talks to Leegality, polls for completion, stores the signed PDF in S3, and reconciles the parent Application status.

Where it sits in the suite: the workflow is gated by a verified MAS101 registration payment (Application.paymentVerified). When a MAS101 application's payment is verified inside CourseEnrollmentService, ensureWorkflowForPaymentReceived lazily creates the Mas101PapWorkflow row. From there the student drives the rest from the public website, and admins review from the Sales CRM area. The signed PDF lands in the student-documents S3 bucket alongside other student docs.

Key source files:

File Role
src/controllers/mas101PapWorkflow.controller.ts HTTP handlers (student + admin)
src/services/Mas101PapWorkflowService.ts Workflow state machine + orchestration (2400+ lines)
src/services/LeegalitySandboxService.ts Low-level Leegality HTTP client + config resolution
src/services/PdfFillerService.ts PDF generation: fillMouPdf (structured) + fillMouFromHtml (WYSIWYG)
src/entities/Mas101PapWorkflow.ts Per-student workflow row + all status enums
src/entities/Mas101PapAgreementTemplate.ts Active agreement template (amounts, PDF body)
src/routes/student.routes.ts Student routes mounted at /api/student
src/routes/sales.routes.ts Admin routes mounted at /api/sales
src/services/CourseEnrollmentService.ts Payment-received trigger that bootstraps the workflow

Key concepts & entities

Glossary

  • MOU — the Pay after Placement Agreement PDF the student signs. In this code "MOU" and "PAP agreement" are used interchangeably.
  • PAP (Pay-After-Placement) — payment option where the success fee (papFixedAmount, default 69999) is collected after placement, instead of paying upfront.
  • Upfront — discounted one-time payment option (upfrontAmount, default 35000 on the entity, but the agreement body quotes a MAS101 upfront of Rs. 41,999) paid via Razorpay before placement.
  • Leegality — third-party eSign provider. The MOU is sent to Leegality, the student signs with Aadhaar OTP, and the signed PDF + audit trail are pulled back.
  • Document ID — Leegality's identifier for a signing request. Stored at mouDocument.leegality.documentId; used to poll status. Never silently dropped (see mouDocumentHistory).
  • Active template — the single isActive=true Mas101PapAgreementTemplate row used to build every new agreement. Holds amounts and the PDF body (structured pdfSections or rich-text pdfHtml).
  • Verification modedemo_aadhaar (default) or leegality_sandbox. Decides whether the student uploads a PDF manually or signs through Leegality.

Entities

  • Mas101PapWorkflow (src/entities/Mas101PapWorkflow.ts, table mas101_pap_workflows) — one row per application (unique index on applicationId). Holds the workflow status, mouStudentData (PAN, Aadhaar, age, address, consents), mouDocument (S3 + embedded leegality sub-object), the mouDocumentHistory audit array, signature URL, MOU verification status, payment option + Razorpay upfront fields, PAP collection schedule, and placement details.
  • Mas101PapAgreementTemplate (src/entities/Mas101PapAgreementTemplate.ts, table mas101_pap_agreement_templates) — the agreement template: batchCode, academicYear, versionLabel, amounts (upfrontAmount, papFixedAmount, papPercentage), the PDF body (pdfSections legacy structured, or pdfHtml WYSIWYG), metadata (payment-plan options, PDF header/footer), sourceFilePath (uploaded source PDF), and isActive.
  • Application (src/entities/Application.ts, related) — the MAS101 application; carries paymentVerified, mouSigned, placed, and the parent status the workflow reconciles.
  • Leegality credentials are not an entity — they live in system_config rows via SystemConfigService, encrypted with encryptSecret/decryptSecret.

Architecture

flowchart TD
  subgraph FE["Frontends"]
    Student["Student UI (mas-website-live /student/application)"]
    Admin["Admin UI (mr-mentor-frontend Sales CRM)"]
  end

  subgraph API["mr-mentor-backend routes"]
    SR["student.routes.ts (/api/student/mas101-pap/*)"]
    AR["sales.routes.ts (/api/sales/mas101-pap/*)"]
  end

  Ctrl["Mas101PapWorkflowController"]

  subgraph SVC["Services"]
    WF["Mas101PapWorkflowService (state machine)"]
    LG["LeegalitySandboxService (HTTP client)"]
    PDF["PdfFillerService (fillMouPdf / fillMouFromHtml)"]
    S3["S3Service (uploadMas101SignedMou, presign)"]
    NOTIF["NotificationService"]
    SYS["SystemConfigService (encrypted creds)"]
    RZP["Razorpay (upfront orders)"]
  end

  subgraph DB["PostgreSQL"]
    WFRow["mas101_pap_workflows"]
    TplRow["mas101_pap_agreement_templates"]
    AppRow["applications"]
    Cfg["system_config (leegality creds)"]
  end

  Leegality["Leegality eSign API (sandbox or prod)"]
  S3B["AWS S3 student-documents bucket"]
  Enroll["CourseEnrollmentService (payment-received hook)"]

  Student --> SR
  Admin --> AR
  SR --> Ctrl
  AR --> Ctrl
  Ctrl --> WF
  WF --> LG
  WF --> PDF
  WF --> S3
  WF --> NOTIF
  WF --> RZP
  LG --> SYS
  SYS --> Cfg
  LG --> Leegality
  S3 --> S3B
  WF --> WFRow
  WF --> TplRow
  WF --> AppRow
  Enroll --> WF

Data model

erDiagram
  APPLICATION ||--o| MAS101_PAP_WORKFLOW : "has one"
  USER ||--o{ MAS101_PAP_WORKFLOW : "owns"
  BATCH ||--o{ MAS101_PAP_WORKFLOW : "groups"
  MAS101_PAP_AGREEMENT_TEMPLATE ||--o{ MAS101_PAP_WORKFLOW : "governs"

  MAS101_PAP_WORKFLOW {
    uuid id PK
    uuid applicationId FK
    uuid userId FK
    uuid batchId FK
    uuid agreementTemplateId FK
    enum status
    json mouStudentData
    json mouDocument
    json mouDocumentHistory
    varchar verificationMode
    varchar studentSignatureUrl
    enum mouVerificationStatus
    enum paymentOption
    enum paymentPlanType
    decimal upfrontAmount
    enum upfrontPaymentStatus
    varchar upfrontRazorpayOrderId
    decimal papFixedAmount
    json papCollectionSchedule
    enum placementStatus
    json placementDetails
    timestamp createdAt
    timestamp updatedAt
  }

  MAS101_PAP_AGREEMENT_TEMPLATE {
    uuid id PK
    varchar batchCode
    varchar academicYear
    varchar versionLabel
    varchar title
    date effectiveDate
    decimal upfrontAmount
    decimal papFixedAmount
    decimal papPercentage
    text sourceFilePath
    json metadata
    json pdfSections
    text pdfHtml
    boolean isActive
  }

Notable enums (src/entities/Mas101PapWorkflow.ts)

  • Mas101PapWorkflowStatus: pending_mou_details, pending_mou_upload, pending_mou_review, mou_signed_approved, pending_payment_option, pending_pap_approval, pending_upfront_payment, enrolled, placement_under_review, payment_collection_pending.
  • Mas101PapVerificationStatus: not_started, pending, approved, rejected.
  • Mas101PapPaymentOption: upfront, pap.
  • Mas101PapPaymentPlanType: one_time, monthly, quarterly.
  • Mas101PapPaymentStatus: not_started, pending, completed, failed.
  • Mas101PapPlacementStatus: not_started, pending, approved, rejected.

The mouDocument.leegality JSON sub-object holds: provider, environment, documentId, signUrl, invitationUrl, status, requestPayload, latestResponse, fileUrl, auditTrailUrl, initiatedAt, completedAt, lastSyncedAt.


API surface

Student routes are mounted at /api/student (createStudentRoutes in src/routes/index.ts); admin routes at /api/sales (createSalesRoutes). All student routes require authMiddleware. Admin write routes require adminMiddleware; admin read routes require requireHeadPagePermission('mas101_post_payment').

Student-facing (/api/student)

Method Path Auth/role Purpose
GET /api/student/mas101-pap auth Get the caller's workflow (creates it lazily if payment verified)
GET /api/student/mas101-pap/template/:templateId/download auth Download the agreement template (redirects to S3 signed URL or local file)
POST /api/student/mas101-pap/mou-details auth Save MOU details (name, PAN, Aadhaar, age, address, consents, payment pick)
PATCH /api/student/mas101-pap/mou-address auth Narrow update of address/state/pincode only
POST /api/student/mas101-pap/signature-presign auth Get a presigned S3 PUT URL for the signature image
PUT /api/student/mas101-pap/signature auth Persist the uploaded signature image URL
DELETE /api/student/mas101-pap/signature auth Clear the saved signature
POST /api/student/mas101-pap/leegality/start auth Start the Leegality eSign request; returns sign URL
POST /api/student/mas101-pap/leegality/refresh auth Poll Leegality for completion; pulls signed PDF into S3
POST /api/student/mas101-pap/mou-upload auth Upload a manually-signed MOU PDF (document multipart)
POST /api/student/mas101-pap/mou-submit auth Submit the signed MOU for admin verification
POST /api/student/mas101-pap/payment-option auth Select upfront or pap (+ plan type)
POST /api/student/mas101-pap/upfront-payment/create auth Create a Razorpay order for the upfront amount
POST /api/student/mas101-pap/upfront-payment/verify auth Verify Razorpay payment signature, mark enrolled
POST /api/student/mas101-pap/placement auth Submit placement details + offer letter (offerLetter multipart)

Admin / Sales (/api/sales)

Method Path Auth/role Purpose
GET /api/sales/applications/:id/mas101-pap head-perm mas101_post_payment Get workflow for a specific application
GET /api/sales/mas101-pap/workflows head-perm Paginated list of all workflows for the review board
GET /api/sales/mas101-pap/template head-perm Get the active agreement template
GET /api/sales/mas101-pap/leegality-config admin Get masked Leegality config + sources
POST /api/sales/mas101-pap/template/upload admin Upload a source template file (template multipart)
PATCH /api/sales/applications/:id/mas101-pap/review admin Approve/reject a stage (mou / pap_enrollment / placement)
PATCH /api/sales/applications/:id/admin-details admin Admin edit of application + MOU data
PATCH /api/sales/applications/:id/mas101-pap/deboard admin Deboard a MAS101 student
PATCH /api/sales/applications/:id/mas101-pap/restore-enrollment admin Restore a deboarded student
PATCH /api/sales/mas101-pap/template admin Update the active template (amounts, labels, plan options)
PATCH /api/sales/mas101-pap/leegality-config admin Save Leegality base URL / auth token / request template
POST /api/sales/mas101-pap/leegality-refresh-token admin Log in to Leegality and refresh the stored auth token
GET /api/sales/mas101-pap/pdf-sections admin Get custom structured PDF sections
PUT /api/sales/mas101-pap/pdf-sections admin Save custom structured PDF sections
DELETE /api/sales/mas101-pap/pdf-sections admin Revert to code-default PDF sections
POST /api/sales/mas101-pap/pdf-preview admin Render a preview PDF with sample data
GET /api/sales/mas101-pap/pdf-html admin Get saved rich-text HTML + header/footer
PUT /api/sales/mas101-pap/pdf-html admin Save rich-text HTML body
DELETE /api/sales/mas101-pap/pdf-html admin Clear rich-text HTML (revert to structured)
POST /api/sales/mas101-pap/mou-image-presign admin Presigned S3 PUT URL for images embedded in the PDF body

A batch-lead alternate also exists at /api/batchlead/students/:id/mas101-payment and /api/batchlead/students/:id/mas101-payment/mou-upload (src/routes/batchLead.routes.ts) for sales/batch-lead-driven configuration and MOU upload. It is outside the student/admin Leegality flow but writes to the same workflow.


User journeys

Journey 0 — Workflow bootstrap on payment-received

When a MAS101 application's registration payment is verified, CourseEnrollmentService calls ensureWorkflowForPaymentReceived, which lazily creates the workflow seeded with the active template's amounts. Non-MAS101 applications skip this and go straight to ENROLLED.

sequenceDiagram
  participant Student as Student
  participant API as Backend API
  participant Enroll as CourseEnrollmentService
  participant WF as Mas101PapWorkflowService
  participant DB as PostgreSQL

  Student->>API: verify registration payment
  API->>Enroll: confirm Razorpay payment
  Enroll->>DB: set application paymentVerified true and status PAYMENT_RECEIVED
  alt application is MAS101
    Enroll->>WF: ensureWorkflowForPaymentReceived
    WF->>DB: load active template
    WF->>DB: upsert mas101_pap_workflows row status PENDING_MOU_DETAILS
  else non MAS101
    Enroll->>DB: set status ENROLLED and create Enrollment
  end
  API-->>Student: payment verified

Journey 1 — Configure the active PAP agreement template (admin)

Admins shape the agreement that every new student signs: amounts, the rich-text PDF body, and embedded images. The PDF body can be authored as legacy structured pdfSections or, preferred, as WYSIWYG pdfHtml rendered by fillMouFromHtml. Admins preview before saving.

sequenceDiagram
  participant Admin as Admin UI
  participant API as Backend API
  participant Ctrl as Mas101PapWorkflowController
  participant WF as Mas101PapWorkflowService
  participant PDF as PdfFillerService
  participant S3 as S3Service
  participant DB as PostgreSQL

  Admin->>API: PATCH mas101-pap/template amounts and labels
  API->>Ctrl: updateActiveTemplate
  Ctrl->>WF: updateActiveTemplate
  WF->>DB: save mas101_pap_agreement_templates
  WF-->>Admin: updated template

  Admin->>API: POST mas101-pap/mou-image-presign filename and contentType
  Ctrl->>S3: generateMouImageUploadUrl
  S3-->>Admin: uploadUrl and publicUrl
  Admin->>S3: PUT image bytes to uploadUrl

  Admin->>API: PUT mas101-pap/pdf-html html with placeholders
  Ctrl->>WF: updateActiveTemplate pdfHtml
  WF->>DB: save pdfHtml

  Admin->>API: POST mas101-pap/pdf-preview with sample data
  Ctrl->>PDF: fillMouFromHtml html and sample data
  PDF-->>Admin: preview PDF inline

Placeholders in pdfHtml are resolved by buildPlaceholderMap: {{fullName}}, {{pan}}, {{aadharNumber}} / {{aadhaar}}, {{age}}, {{address}}, {{date}}, {{day}}, {{month}}, {{year}}. Special tick/signature markers {{studentSignature}}, {{paymentOptionUpfrontTick}}, {{paymentOptionPapTick}} make the matching student inputs required before Step 1 can be saved.

Journey 2 — Student fills MOU details (Step 1)

The student fills personal/legal details and consents, optionally uploads a signature image, and may pre-pick a payment option. The save resets any stale signature unless the content is unchanged and a real signature already exists, with a self-healing safeguard against destroying an in-flight Leegality signature (see gotchas).

sequenceDiagram
  participant Student as Student UI
  participant API as Backend API
  participant Ctrl as Mas101PapWorkflowController
  participant WF as Mas101PapWorkflowService
  participant LG as LeegalitySandboxService
  participant DB as PostgreSQL

  opt template requires signature image
    Student->>API: POST mas101-pap/signature-presign
    Ctrl->>WF: generate presigned S3 PUT
    WF-->>Student: uploadUrl and publicUrl
    Student->>API: PUT mas101-pap/signature with public url
  end

  Student->>API: POST mas101-pap/mou-details with PAN Aadhaar age address consents
  API->>Ctrl: saveMouDetails
  Ctrl->>WF: saveMouDetails
  WF->>DB: load workflow and active template
  Note over WF: guard required signature and payment pick
  alt existing document in flight and content unchanged
    WF->>LG: getDocumentStatus documentId
    alt live signature completed
      WF->>WF: healCompletedLeegalitySignature keep document
    else not confirmed
      WF->>WF: preserve documentId rather than wipe
    end
  else content changed or no document
    WF->>WF: archive documentId into mouDocumentHistory and clear document
    WF->>DB: set status PENDING_MOU_UPLOAD
  end
  WF->>DB: save mouStudentData and workflow
  WF-->>Student: serialized workflow with nextAction

Journey 3 — Start Leegality eSign and student signs (happy path)

The student starts the eSign. The service builds the agreement PDF (when the template uses file-upload mode), generates the Leegality signing request from the configured JSON request template, and returns a sign URL. The student signs with Aadhaar OTP on Leegality and is redirected back. The backend then polls Leegality, downloads the signed PDF, stores it in S3, and moves the workflow to MOU review.

sequenceDiagram
  participant Student as Student UI
  participant API as Backend API
  participant WF as Mas101PapWorkflowService
  participant PDF as PdfFillerService
  participant LG as LeegalitySandboxService
  participant Leg as Leegality API
  participant S3 as S3Service
  participant DB as PostgreSQL

  Student->>API: POST mas101-pap/leegality/start
  API->>WF: startLeegalitySigning
  Note over WF: require complete MOU details and both consents
  WF->>LG: getResolvedConfig
  alt request template uses FILE_BASE64 placeholder
    WF->>PDF: fillMouFromHtml or fillMouPdf with student data
    PDF-->>WF: agreement PDF buffer
    WF->>S3: uploadMas101SignedMou store generated PDF
  end
  WF->>LG: createSigningRequest with template values and invitee
  LG->>Leg: POST v3.0 sign request
  Leg-->>LG: documentId and signUrl
  LG-->>WF: documentId signUrl invitationUrl
  WF->>DB: set verificationMode leegality_sandbox status PENDING_MOU_UPLOAD store leegality block
  WF-->>Student: signUrl

  Student->>Leg: open signUrl and complete Aadhaar OTP eSign
  Leg-->>Student: redirect to REDIRECT_URL esign complete

  Student->>API: POST mas101-pap/leegality/refresh
  API->>WF: refreshLeegalitySigning
  WF->>LG: getDocumentStatus documentId
  LG->>Leg: GET v3.3 document details
  Leg-->>LG: status and invitation signed flag
  alt isCompleted and not past MOU stage and not rejected
    WF->>LG: getDocumentStatus documentId includeFiles true
    LG->>Leg: GET document details with file and auditTrail
    Leg-->>LG: signed file url
    WF->>LG: downloadFile signed pdf
    WF->>S3: uploadMas101SignedMou store signed PDF
    WF->>DB: set mouVerificationStatus PENDING status PENDING_MOU_REVIEW
  else still pending
    WF->>DB: update leegality status only
  end
  WF-->>Student: serialized workflow

Journey 4 — Manual signed-PDF upload (fallback)

When Leegality is not configured, or the student signs offline, the student can upload a signed MOU PDF directly. demo_aadhaar verification mode supports this path.

sequenceDiagram
  participant Student as Student UI
  participant API as Backend API
  participant WF as Mas101PapWorkflowService
  participant S3 as S3Service
  participant DB as PostgreSQL

  Student->>API: POST mas101-pap/mou-upload with document file
  API->>WF: uploadMouDocument
  WF->>S3: uploadMas101SignedMou file buffer
  S3-->>WF: url and s3Key
  WF->>DB: set mouDocument and status PENDING_MOU_REVIEW
  WF-->>Student: serialized workflow
  Student->>API: POST mas101-pap/mou-submit
  WF->>DB: set mouVerificationStatus PENDING or auto approve when demo flag set
  WF-->>Student: serialized workflow

If MAS101_PAP_DEMO_AUTOVERIFY=true, submitMouForVerification auto-approves the MOU and advances the workflow without an admin step (demo/dev convenience only).

Journey 5 — Admin reviews the signed MOU

An admin opens the review board, views the signed PDF (S3 signed URL minted on demand), and approves or rejects. Approval applies the student's pre-selected payment option and fires a notification.

sequenceDiagram
  participant Admin as Admin UI
  participant API as Backend API
  participant WF as Mas101PapWorkflowService
  participant S3 as S3Service
  participant NOTIF as NotificationService
  participant DB as PostgreSQL

  Admin->>API: GET mas101-pap/workflows
  API->>WF: listWorkflowsForReview
  WF-->>Admin: workflows without signed urls
  Admin->>API: GET applications/:id/mas101-pap
  WF->>S3: getStudentDocumentSignedUrl mouDocument s3Key
  WF-->>Admin: workflow with signed MOU url

  Admin->>API: PATCH applications/:id/mas101-pap/review stage mou decision approve
  API->>WF: reviewWorkflow
  alt decision approve
    WF->>DB: set mouVerificationStatus APPROVED status MOU_SIGNED_APPROVED
    WF->>WF: applyPreSelectedPaymentOption advance to upfront or pap approval
    WF->>DB: set application mouSigned true
    WF->>NOTIF: fromTemplate mas101_mou_approved
  else decision reject
    WF->>DB: set mouVerificationStatus REJECTED status PENDING_MOU_UPLOAD
  end
  WF-->>Admin: serialized workflow

Journey 6 — Payment option, upfront payment, and placement

After MOU approval the student chooses upfront vs PAP. Upfront goes through Razorpay; PAP requires admin enrollment approval. PAP students later submit placement proof for review.

sequenceDiagram
  participant Student as Student UI
  participant API as Backend API
  participant WF as Mas101PapWorkflowService
  participant RZP as Razorpay
  participant Admin as Admin UI
  participant DB as PostgreSQL

  Student->>API: POST mas101-pap/payment-option upfront or pap
  WF->>DB: set paymentOption and status PENDING_UPFRONT_PAYMENT or PENDING_PAP_APPROVAL

  alt upfront
    Student->>API: POST mas101-pap/upfront-payment/create
    WF->>RZP: create order for upfront amount
    RZP-->>Student: order id
    Student->>RZP: pay
    Student->>API: POST mas101-pap/upfront-payment/verify with signature
    WF->>WF: validate Razorpay signature
    WF->>DB: set upfrontPaymentStatus COMPLETED status ENROLLED
  else pap
    Admin->>API: PATCH mas101-pap/review stage pap_enrollment decision approve
    WF->>DB: set status ENROLLED application ENROLLED
    Student->>API: POST mas101-pap/placement with offer letter
    WF->>DB: set placementStatus PENDING status PLACEMENT_UNDER_REVIEW
    Admin->>API: PATCH mas101-pap/review stage placement decision approve
    WF->>DB: set placementStatus APPROVED status PAYMENT_COLLECTION_PENDING
  end
  WF-->>Student: serialized workflow

Background jobs & async

This domain has no dedicated BullMQ worker, cron, socket events, or inbound webhook. Notably:

  • Leegality completion is poll-based, not webhook-based. There is no callback endpoint that Leegality POSTs to. Instead the student's browser is redirected to MAS_WEBSITE_URL/student/application?esign=complete (the REDIRECT_URL placeholder), and the frontend then calls POST /api/student/mas101-pap/leegality/refresh to pull the latest status. The "callback" is therefore the redirect-plus-refresh sequence in Journey 3, not a server webhook.
  • Self-heal on save. saveMouDetails performs a live getDocumentStatus check (Journey 2) so a completed-but-unsynced signature is reconciled even outside the explicit refresh.
  • Notifications are fired in-process and non-blocking: reviewWorkflow calls NotificationService.fromTemplate(userId, 'mas101_mou_approved', {}) on MOU approval, with errors swallowed so the response is unaffected.
  • Razorpay upfront orders are created/verified synchronously within the request; there is also an invoice-via-WhatsApp helper (sendUpfrontPaymentInvoiceWhatsApp) invoked inline.
  • Status reconciliation runs lazily on every serializeWorkflow via reconcileApplicationStatus, so application/workflow drift self-heals on the next read rather than via a scheduled job.

External integrations

Leegality eSign

  • Client: src/services/LeegalitySandboxService.ts. Auth via X-Auth-Token header. Endpoints used: POST /v3.0/sign/request (create), GET /v3.3/document/details (status + optional file/auditTrail), POST /v2/login (refresh token).
  • Config resolution order (getResolvedConfig): env var first, then encrypted DB value via SystemConfigService, then a default. Keys:
  • LEEGALITY_SANDBOX_BASE_URL / DB mas101_leegality_sandbox_base_url (default https://sandbox.leegality.com/api).
  • LEEGALITY_SANDBOX_AUTH_TOKEN / DB mas101_leegality_sandbox_auth_token.
  • MAS101_LEEGALITY_SANDBOX_CREATE_REQUEST_TEMPLATE / DB mas101_leegality_sandbox_create_request_template — a JSON request template with {{PLACEHOLDER}} tokens (FULL_NAME, PAN, AADHAR_NUMBER/AADHAAR_NUMBER, AGE, ADDRESS, EMAIL, PHONE, DATE, REDIRECT_URL, FILE_BASE64, FILE_NAME, plus Aadhaar fields). Resolved by replacePlaceholders.
  • Configured? isConfigured() requires both an auth token and a request template; otherwise startLeegalitySigning throws LEEGALITY_SANDBOX_NOT_CONFIGURED.
  • Sandbox vs production: controlled purely by baseUrl. Sandbox is https://sandbox.leegality.com/api; production is https://app1.leegality.com/api (per docs/LEEGALITY_SANDBOX_INTEGRATION.md). The stored leegality.environment is hard-coded to 'sandbox' in startLeegalitySigning.
  • Secret storage: DB-stored config values are encrypted with encryptSecret/decryptSecret using PLATFORM_ENCRYPTION_KEY (falls back to WHATSAPP_CREDENTIALS_ENCRYPTION_KEY). The admin config endpoint returns only a masked token.
  • Failure modes: Leegality returns HTTP 200 with status:0 for logical errors (e.g. profile not found) — handled in request(). Concurrent-login on token refresh raises a friendly LEEGALITY_CONCURRENT_LOGIN message. File download failures raise LEEGALITY_FILE_DOWNLOAD_FAILED_<code>. If the signed file URL is not yet retrievable, completion metadata is still recorded and the file is fetched on a later refresh.
  • Two request modes: template-based (Leegality fills its own template — no PDF upload) vs file-upload (backend generates the PDF and sends it as base64). Auto-detected by whether the request template contains {{FILE_BASE64}}.

PDF generation (src/services/PdfFillerService.ts)

  • fillMouPdf — pdfkit-based renderer for the legacy structured template (DEFAULT_CONFIG holds the full 18-clause MAS101 PAP agreement text + company details: UNIQUE MINDCRAFTED TENXX PRIVATE LIMITED, CIN, PAN, GSTIN).
  • fillMouFromHtml — renders the WYSIWYG pdfHtml (parses HTML, supports headings, lists, tables, images, inline styles, page-break <hr>, and {{placeholder}} substitution with bolding).

AWS S3

  • S3Service.uploadMas101SignedMou stores generated + signed MOUs (year-partitioned) in the student documents bucket. generateStudentSignatureUploadUrl and generateMouImageUploadUrl issue presigned PUT URLs. Signed GET URLs (getStudentDocumentSignedUrl, 1h) are minted on demand when an admin views a private MOU.

Razorpay

  • Upfront payment orders are created/verified directly in Mas101PapWorkflowService for the upfront option (see Journey 6).

Status lifecycles

Workflow status (Mas101PapWorkflowStatus)

stateDiagram-v2
  [*] --> pending_mou_details : payment verified
  pending_mou_details --> pending_mou_upload : details saved no signed doc
  pending_mou_upload --> pending_mou_review : signed PDF stored or uploaded
  pending_mou_review --> mou_signed_approved : admin approves MOU
  pending_mou_review --> pending_mou_upload : admin rejects MOU
  mou_signed_approved --> pending_payment_option : no pre selected option
  mou_signed_approved --> pending_upfront_payment : pre selected upfront
  mou_signed_approved --> pending_pap_approval : pre selected pap
  pending_payment_option --> pending_upfront_payment : choose upfront
  pending_payment_option --> pending_pap_approval : choose pap
  pending_upfront_payment --> enrolled : upfront payment verified
  pending_pap_approval --> enrolled : admin approves pap enrollment
  pending_pap_approval --> pending_payment_option : admin rejects pap enrollment
  enrolled --> placement_under_review : student submits placement
  placement_under_review --> payment_collection_pending : admin approves placement
  placement_under_review --> enrolled : admin rejects placement
  payment_collection_pending --> [*]

MOU verification status (Mas101PapVerificationStatus)

stateDiagram-v2
  [*] --> not_started
  not_started --> pending : MOU submitted or eSign completed
  pending --> approved : admin approves
  pending --> rejected : admin rejects
  rejected --> not_started : student re uploads or re signs
  approved --> [*]

Placement status (Mas101PapPlacementStatus)

stateDiagram-v2
  [*] --> not_started
  not_started --> pending : student submits placement
  pending --> approved : admin approves placement
  pending --> rejected : admin rejects placement
  rejected --> pending : student resubmits
  approved --> [*]

Edge cases, limits & gotchas

  • Never strand a real signature (incident 2026-06-27). saveMouDetails will not discard a Leegality documentId it has not confirmed dead. If a save fires after the student signed but before the completion synced, it calls getDocumentStatus live; on completion it self-heals via healCompletedLeegalitySignature, and if the check fails it preserves the document rather than risk silent data loss. Any dropped documentId is archived into mouDocumentHistory (with a reason: rejected / details_changed / superseded). There is a dedicated recovery skill (recover-stuck-pap-mou) for students who signed but whose UI is stuck on Step 1.
  • MOU_TEMPLATE_FILE_NOT_AVAILABLE. In file-upload mode, if the template has no pdfHtml the service falls back to the source PDF file via getTemplateDownloadInfo; a missing source file raises this error. Authoring the body as pdfHtml avoids needing a source file. (See memory note: a prod bug where pdfHtml-vs-source-file mismatch caused this, fixed in PR #288.)
  • Idempotency / re-sign. Editing material MOU fields (name, PAN, Aadhaar, age, address, gender, pincode, state, yearOfBirth) marks an existing signature stale and forces a re-sign; unchanged content keeps the signature. A narrow address-only edit (updateMouAddress) does not reset the workflow and does not rewrite the already-signed PDF — it only updates stored display data, and is blocked once enrollment is complete (MOU_ADDRESS_LOCKED, HTTP 409).
  • Payment option locks at MOU approval. Once mouVerificationStatus === APPROVED, the pre-signature payment pick is locked (paymentOptionLocked) so it cannot diverge from the signed PDF.
  • Refresh guards. refreshLeegalitySigning will not reset the workflow if the student is already past the MOU stage, if admin already rejected, or if a signed PDF is already stored — preventing a late poll from clobbering downstream state.
  • Single active template. ensureActiveTemplate assumes one isActive=true row per batchCode. Amount fields on the workflow are snapshotted from the template at creation and only backfilled if null, so changing the template later does not retroactively change existing workflows' amounts.
  • Auth nuances. Student endpoints derive identity from req.user.id (JWT). Admin read endpoints use requireHeadPagePermission('mas101_post_payment') (sales-head page permission), while write/config endpoints require full adminMiddleware. The Leegality config + PDF-template editing endpoints are admin-only.
  • Demo auto-verify. MAS101_PAP_DEMO_AUTOVERIFY=true bypasses admin MOU review — for dev/demo only.
  • Multi-platform. This is a MAS101-specific flow; it does not branch on the x-platform header. The REDIRECT_URL always points at MAS_WEBSITE_URL (defaults to http://localhost:8088).
  • Workflow creation requires verified payment. getStudentWorkflow returns null if the latest MAS101 application is not paymentVerified, so the workflow is never exposed before registration is paid.