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 docsdocs/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, default69999) is collected after placement, instead of paying upfront. - Upfront — discounted one-time payment option (
upfrontAmount, default35000on 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 (seemouDocumentHistory). - Active template — the single
isActive=trueMas101PapAgreementTemplaterow used to build every new agreement. Holds amounts and the PDF body (structuredpdfSectionsor rich-textpdfHtml). - Verification mode —
demo_aadhaar(default) orleegality_sandbox. Decides whether the student uploads a PDF manually or signs through Leegality.
Entities
Mas101PapWorkflow(src/entities/Mas101PapWorkflow.ts, tablemas101_pap_workflows) — one row per application (unique index onapplicationId). Holds the workflowstatus,mouStudentData(PAN, Aadhaar, age, address, consents),mouDocument(S3 + embeddedleegalitysub-object), themouDocumentHistoryaudit array, signature URL, MOU verification status, payment option + Razorpay upfront fields, PAP collection schedule, and placement details.Mas101PapAgreementTemplate(src/entities/Mas101PapAgreementTemplate.ts, tablemas101_pap_agreement_templates) — the agreement template:batchCode,academicYear,versionLabel, amounts (upfrontAmount,papFixedAmount,papPercentage), the PDF body (pdfSectionslegacy structured, orpdfHtmlWYSIWYG),metadata(payment-plan options, PDF header/footer),sourceFilePath(uploaded source PDF), andisActive.Application(src/entities/Application.ts, related) — the MAS101 application; carriespaymentVerified,mouSigned,placed, and the parentstatusthe workflow reconciles.- Leegality credentials are not an entity — they live in
system_configrows viaSystemConfigService, encrypted withencryptSecret/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-paymentand/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
pdfHtmlare resolved bybuildPlaceholderMap:{{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,submitMouForVerificationauto-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(theREDIRECT_URLplaceholder), and the frontend then callsPOST /api/student/mas101-pap/leegality/refreshto pull the latest status. The "callback" is therefore the redirect-plus-refresh sequence in Journey 3, not a server webhook. - Self-heal on save.
saveMouDetailsperforms a livegetDocumentStatuscheck (Journey 2) so a completed-but-unsynced signature is reconciled even outside the explicit refresh. - Notifications are fired in-process and non-blocking:
reviewWorkflowcallsNotificationService.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
serializeWorkflowviareconcileApplicationStatus, 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 viaX-Auth-Tokenheader. 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 viaSystemConfigService, then a default. Keys: LEEGALITY_SANDBOX_BASE_URL/ DBmas101_leegality_sandbox_base_url(defaulthttps://sandbox.leegality.com/api).LEEGALITY_SANDBOX_AUTH_TOKEN/ DBmas101_leegality_sandbox_auth_token.MAS101_LEEGALITY_SANDBOX_CREATE_REQUEST_TEMPLATE/ DBmas101_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 byreplacePlaceholders.- Configured?
isConfigured()requires both an auth token and a request template; otherwisestartLeegalitySigningthrowsLEEGALITY_SANDBOX_NOT_CONFIGURED. - Sandbox vs production: controlled purely by
baseUrl. Sandbox ishttps://sandbox.leegality.com/api; production ishttps://app1.leegality.com/api(perdocs/LEEGALITY_SANDBOX_INTEGRATION.md). The storedleegality.environmentis hard-coded to'sandbox'instartLeegalitySigning. - Secret storage: DB-stored config values are encrypted with
encryptSecret/decryptSecretusingPLATFORM_ENCRYPTION_KEY(falls back toWHATSAPP_CREDENTIALS_ENCRYPTION_KEY). The admin config endpoint returns only a masked token. - Failure modes: Leegality returns HTTP 200 with
status:0for logical errors (e.g. profile not found) — handled inrequest(). Concurrent-login on token refresh raises a friendlyLEEGALITY_CONCURRENT_LOGINmessage. File download failures raiseLEEGALITY_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_CONFIGholds the full 18-clause MAS101 PAP agreement text + company details: UNIQUE MINDCRAFTED TENXX PRIVATE LIMITED, CIN, PAN, GSTIN).fillMouFromHtml— renders the WYSIWYGpdfHtml(parses HTML, supports headings, lists, tables, images, inline styles, page-break<hr>, and{{placeholder}}substitution with bolding).
AWS S3¶
S3Service.uploadMas101SignedMoustores generated + signed MOUs (year-partitioned) in the student documents bucket.generateStudentSignatureUploadUrlandgenerateMouImageUploadUrlissue 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
Mas101PapWorkflowServicefor theupfrontoption (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).
saveMouDetailswill not discard a LeegalitydocumentIdit has not confirmed dead. If a save fires after the student signed but before the completion synced, it callsgetDocumentStatuslive; on completion it self-heals viahealCompletedLeegalitySignature, and if the check fails it preserves the document rather than risk silent data loss. Any droppeddocumentIdis archived intomouDocumentHistory(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
pdfHtmlthe service falls back to the source PDF file viagetTemplateDownloadInfo; a missing source file raises this error. Authoring the body aspdfHtmlavoids 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.
refreshLeegalitySigningwill 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.
ensureActiveTemplateassumes oneisActive=truerow perbatchCode. 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 userequireHeadPagePermission('mas101_post_payment')(sales-head page permission), while write/config endpoints require fulladminMiddleware. The Leegality config + PDF-template editing endpoints are admin-only. - Demo auto-verify.
MAS101_PAP_DEMO_AUTOVERIFY=truebypasses admin MOU review — for dev/demo only. - Multi-platform. This is a MAS101-specific flow; it does not branch on the
x-platformheader. TheREDIRECT_URLalways points atMAS_WEBSITE_URL(defaults tohttp://localhost:8088). - Workflow creation requires verified payment.
getStudentWorkflowreturnsnullif the latest MAS101 application is notpaymentVerified, so the workflow is never exposed before registration is paid.
Related docs¶
- Sales CRM — leads, assignment & telephony
- Finance — payments, GST invoicing & reports
- LMS — courses, batches & enrollment
- Data model overview
- Backend source docs:
mr-mentor-backend/docs/LEEGALITY_SANDBOX_INTEGRATION.md,mr-mentor-backend/docs/LEEGALITY_TEMPLATE_SETUP_GUIDE.md