/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatButtonModule } from '@angular/material/button';
import { FormsModule } from '@angular/forms';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { ClientsService } from 'app/clients/clients.service';
@Component({
selector: 'mifosx-dip-verify-dialog',
template: `
DIP Verification
@if (verifying) {
{{ progress }}%
Please wait while the system verifies the entered mobile number against the
Mobile Number Revocation List (MNRL) available on the
Digital Intelligence Platform (DIP) of the Department of Telecommunications (DoT),
Government of India.
This may take a few seconds. Do not close or refresh this window.
}
@if (!verifying && serviceUnavailable) {
Could not verify — DIP/MNRL service unavailable
This number has not been checked against the DoT Digital Intelligence Platform. Retry once
the service is reachable, or proceed with a recorded reason.
}
@if (!verifying && !suspected && !serviceUnavailable && !error) {
Mobile number verified — Not found in MNRL revocation list.
}
@if (!verifying && suspected) {
{{ resultMessage }}
@if (suspectedReason) {
Status: {{ suspectedStatus }}
Reason: {{ suspectedReason }}
Reported by: {{ suspectedReportedBy }}
}
}
@if (!verifying && error) {
}
@if (!verifying && priorRejection) {
This number was rejected before.
Rejected by user {{ priorRejection.decidedBy }} on {{ priorRejection.decidedOn | date: 'medium' }} —
status was {{ priorRejection.lookupStatus }}.
}
@if (!verifying && needsDecision && showReasonForm) {
}
@if (!verifying && !needsDecision) {
OK
}
@if (!verifying && needsDecision && !showReasonForm) {
Reject this Number
Proceed with Reason
}
@if (!verifying && needsDecision && showReasonForm) {
Back
{{ saving ? 'Saving…' : 'Save Decision & Proceed' }}
}
`,
styles: [
`
:host {
display: block;
min-width: 450px;
}
h2[mat-dialog-title] {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
padding: 16px 24px;
font-size: 18px;
font-weight: 600;
color: #1074b9;
}
.dialog-title-icon {
font-size: 20px;
}
mat-dialog-content {
padding: 16px 24px;
}
mat-progress-bar {
margin-bottom: 4px;
}
.progress-percent {
text-align: center;
font-size: 22px;
font-weight: 700;
color: #1074b9;
margin: 4px 0 12px;
}
.verify-info {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
background: #e3f2fd;
border-radius: 8px;
margin-bottom: 12px;
margin-top: 12px;
}
.info-icon {
font-size: 24px;
color: #1074b9;
margin-top: 2px;
flex-shrink: 0;
}
.verify-msg {
font-size: 14px;
color: #333;
line-height: 1.6;
margin: 0;
}
.warning-row {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #fff3e0;
border-radius: 6px;
}
.warning-icon {
font-size: 16px;
color: #e65100;
flex-shrink: 0;
}
.warning-text {
font-size: 13px;
color: #e65100;
font-weight: 500;
}
.verify-result {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 16px 0;
}
.result-icon {
font-size: 48px;
color: #388e3c;
}
.result-msg {
font-size: 16px;
font-weight: 600;
color: #388e3c;
margin: 0;
text-align: center;
}
.suspected-icon {
color: #e65100;
}
.suspected-msg {
color: #e65100;
}
.suspected-detail {
background: #fff3e0;
border-radius: 8px;
padding: 12px 16px;
width: 100%;
text-align: left;
}
.suspected-detail p {
margin: 4px 0;
font-size: 14px;
font-weight: 400;
color: #333;
}
.success-detail {
display: flex;
align-items: center;
gap: 8px;
background: #e8f5e9;
border: 1px solid #a5d6a7;
border-radius: 8px;
padding: 10px 16px;
width: 100%;
font-size: 13px;
color: #2e7d32;
}
.success-detail-icon {
font-size: 16px;
color: #388e3c;
flex-shrink: 0;
}
.error-icon {
color: #d32f2f;
}
.error-msg {
color: #d32f2f;
}
.unavailable-icon {
color: #b45309;
}
.unavailable-msg {
color: #b45309;
}
.unavailable-detail {
font-size: 13px;
color: #92400e;
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 6px;
padding: 10px 12px;
line-height: 1.6;
margin-top: 8px;
}
.prior-rejection {
display: flex;
align-items: flex-start;
gap: 10px;
font-size: 13px;
color: #7f1d1d;
background: #fef2f2;
border: 1px solid #fecaca;
border-left: 4px solid #dc2626;
border-radius: 6px;
padding: 10px 12px;
margin-top: 12px;
line-height: 1.5;
}
.prior-icon {
color: #dc2626;
font-size: 16px;
margin-top: 2px;
}
.prior-detail {
font-size: 12px;
color: #991b1b;
margin-top: 2px;
}
.reason-form {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 14px;
}
.reason-label {
font-size: 12px;
font-weight: 600;
color: #374151;
}
.reason-input {
width: 100%;
box-sizing: border-box;
font-family: inherit;
font-size: 13px;
padding: 8px 10px;
border: 1px solid #d1d5db;
border-radius: 4px;
resize: vertical;
}
.reason-input:focus {
outline: none;
border-color: #1074b9;
}
.reason-hint {
font-size: 11px;
color: #9ca3af;
}
.reason-error {
color: #dc2626;
font-weight: 600;
}
mat-dialog-actions {
padding: 8px 24px 16px;
gap: 8px;
}
`
],
imports: [
CommonModule,
FormsModule,
MatDialogModule,
MatProgressBarModule,
MatButtonModule,
FaIconComponent
]
})
export class DipVerifyDialogComponent implements OnInit, OnDestroy {
private dialogRef = inject(MatDialogRef);
private data: { mobileNo: string } = inject(MAT_DIALOG_DATA);
private clientsService = inject(ClientsService);
verifying = true;
progress = 0;
suspected = false;
serviceUnavailable = false;
error: string | null = null;
resultMessage = '';
suspectedStatus = '';
suspectedReason = '';
suspectedReportedBy = '';
/** Set when this number was rejected on an earlier client — history the officer must see. */
priorRejection: any = null;
/** Officer decision capture for a flagged or unverifiable number. */
showReasonForm = false;
decisionReason = '';
reasonTouched = false;
saving = false;
decisionError = '';
/** A hit or an unverified number cannot simply be dismissed with OK. */
get needsDecision(): boolean {
return this.suspected || this.serviceUnavailable;
}
get reasonValid(): boolean {
return this.decisionReason.trim().length >= 10;
}
private subscription: any;
private intervalId: any;
private apiResponse: any = null;
private apiError: any = null;
private apiDone = false;
ngOnInit() {
this.dialogRef.disableClose = true;
// Animate progress bar over ~3 seconds (0 → 100)
this.intervalId = setInterval(() => {
this.progress += 2;
if (this.progress >= 100) {
clearInterval(this.intervalId);
this.intervalId = null;
this.tryShowResult();
}
}, 60);
// Fire the API call in parallel
this.subscription = this.clientsService.checkDipLookup(this.data.mobileNo).subscribe({
next: (res: any) => {
this.apiResponse = res;
this.apiDone = true;
this.tryShowResult();
},
error: (err: any) => {
this.apiError = err;
this.apiDone = true;
this.tryShowResult();
}
});
}
/** Show result only after both progress bar completes and API responds */
private tryShowResult() {
if (!this.apiDone || this.progress < 100) {
return;
}
this.verifying = false;
this.dialogRef.disableClose = false;
if (this.apiError) {
this.error =
this.apiError?.error?.message || this.apiError?.message || 'DIP verification failed. Please try again.';
return;
}
const res = this.apiResponse;
this.priorRejection = res?.priorRejection || null;
if (res?.data?.status === 'SERVICE_UNAVAILABLE') {
this.serviceUnavailable = true;
this.resultMessage = res.message || 'DIP/MNRL lookup service is unavailable. Number treated as CLEAN by default.';
} else if (res?.data?.suspected) {
this.suspected = true;
this.resultMessage = res.message || `Mobile number is ${res.data.status} — found in DIP records`;
const detail = res.data.suspectedDetail;
if (detail) {
this.suspectedStatus = detail.dipStatus || res.data.status;
this.suspectedReason = detail.reason || '';
this.suspectedReportedBy = detail.reportedBy || '';
}
}
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
startProceed() {
this.showReasonForm = true;
this.reasonTouched = false;
this.decisionError = '';
}
cancelProceed() {
this.showReasonForm = false;
this.decisionReason = '';
this.decisionError = '';
}
submitProceed() {
this.reasonTouched = true;
if (!this.reasonValid) {
return;
}
this.recordDecision('PROCEEDED', this.decisionReason.trim(), true);
}
reject() {
this.recordDecision('REJECTED', 'Number rejected at onboarding following DIP/MNRL result.', false);
}
/**
* Persists the decision before closing. The number is only usable downstream once the backend
* has stored PROCEEDED — the client-create gate re-checks this, so closing without saving
* would simply fail at save time with no explanation.
*/
private recordDecision(decision: string, reason: string, verified: boolean) {
const logId = this.apiResponse?.lookupLogId;
if (!logId) {
this.decisionError = 'Lookup reference missing — re-run the check before recording a decision.';
return;
}
this.saving = true;
this.decisionError = '';
this.clientsService.recordDipDecision(logId, decision, reason).subscribe({
next: () => {
this.saving = false;
this.dialogRef.close({
verified,
decision,
lookupLogId: logId,
status: this.apiResponse?.data?.status || null,
timestamp: this.apiResponse?.timestamp || null
});
},
error: (err: any) => {
this.saving = false;
this.decisionError =
err?.error?.defaultUserMessage || err?.error?.message || err?.message || 'Could not save the decision.';
}
});
}
close() {
const verified = !this.suspected && !this.error && !this.serviceUnavailable;
this.dialogRef.close({
verified,
lookupLogId: this.apiResponse?.lookupLogId || null,
status: this.apiResponse?.data?.status || null,
timestamp: this.apiResponse?.timestamp || null
});
}
}