merging code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Nitesh Yadav
2026-04-20 14:26:25 +05:30
co-authored by Claude Sonnet 4.6
322 changed files with 28779 additions and 4976 deletions
@@ -305,23 +305,85 @@
<mat-datepicker #incorpDatePicker></mat-datepicker>
</mat-form-field>
<!-- GSTIN (all non-individual) -->
<mat-form-field appearance="outline" class="form-field">
<mat-label>GSTIN Number</mat-label>
<input
matInput
formControlName="gstinNumber"
placeholder="15-character GSTIN"
maxlength="15"
style="text-transform: uppercase"
(input)="
kycForm.get('gstinNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
"
/>
@if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) {
<mat-error>Invalid GSTIN format (e.g. 22AAAAA0000A1Z5)</mat-error>
}
</mat-form-field>
<!-- GSTIN (all non-individual) with Verify -->
<div class="form-field verify-field">
<mat-form-field appearance="outline" class="verify-input">
<mat-label>GSTIN Number</mat-label>
<span matPrefix class="field-prefix-icon"><mat-icon>receipt_long</mat-icon></span>
<input
matInput
formControlName="gstinNumber"
placeholder="15-character GSTIN"
maxlength="15"
style="text-transform: uppercase"
(input)="
kycForm.get('gstinNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
"
/>
@if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) {
<mat-error>Invalid GSTIN format (e.g. 22AAAAA0000A1Z5)</mat-error>
}
@if (gstinVerifyStatus === 'VERIFIED') {
<mat-icon matSuffix class="verify-icon-ok">check_circle</mat-icon>
}
@if (gstinVerifyStatus === 'FAILED') {
<mat-icon matSuffix class="verify-icon-fail">cancel</mat-icon>
}
</mat-form-field>
<div class="verify-action">
@if (gstinVerifyStatus === 'NONE' || gstinVerifyStatus === 'FAILED') {
<button
mat-raised-button
color="primary"
type="button"
class="verify-btn-inline"
[disabled]="
readOnly ||
!kycForm.get('gstinNumber')?.value ||
kycForm.get('gstinNumber')?.value.length < 15 ||
gstinVerifying ||
!clientId
"
(click)="verifyGstin()"
>
@if (gstinVerifying) {
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
} @else {
<fa-icon icon="shield-alt"></fa-icon> Verify GSTIN
}
</button>
}
@if (gstinVerifyStatus === 'VERIFIED') {
<span class="verify-chip verified"><fa-icon icon="check-circle"></fa-icon> Verified</span>
}
</div>
</div>
@if (gstinVerifyStatus === 'VERIFIED') {
<div class="pan-result-card full-width">
<div class="pan-result-header">
<fa-icon icon="check-circle" class="pan-result-ok-icon"></fa-icon>
<span>GSTIN Verified Successfully</span>
</div>
<div class="pan-result-body">
@if (gstinVerifyName) {
<div class="pan-result-row">
<span class="pan-result-label">Trade Name / Legal Name</span>
<span class="pan-result-value name">{{ gstinVerifyName }}</span>
</div>
}
@if (gstinVerifyState) {
<div class="pan-result-row">
<span class="pan-result-label">State</span>
<span class="pan-result-value">{{ gstinVerifyState }}</span>
</div>
}
<div class="pan-result-row">
<span class="pan-result-label">Verified On</span>
<span class="pan-result-value date">{{ gstinVerifiedOn }}</span>
</div>
</div>
</div>
}
<!-- PAN — Proprietorship uses Individual PAN, others use Enterprise PAN -->
@if (constitutionMeta?.value === 'PROPRIETORSHIP') {
@@ -263,6 +263,13 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
entPanVerifyName = '';
entPanVerifiedOn = '';
/** GSTIN verification */
gstinVerifyStatus: 'NONE' | 'VERIFIED' | 'FAILED' = 'NONE';
gstinVerifying = false;
gstinVerifyName = '';
gstinVerifiedOn = '';
gstinVerifyState = '';
/** CIN verification (dummy) */
cinVerifyStatus: 'NONE' | 'VERIFIED' | 'FAILED' = 'NONE';
cinVerifying = false;
@@ -480,6 +487,17 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
if (data.panAadhaarSeedingStatus) {
this.panAadhaarSeedingStatus = data.panAadhaarSeedingStatus;
}
if (data.gstinVerified) {
this.gstinVerifyStatus = 'VERIFIED';
this.gstinVerifyName = data.gstinTradeName || data.gstinLegalName || '';
this.gstinVerifyState = data.gstinState || '';
const rawGstinDate = data.gstinVerifiedOn;
if (Array.isArray(rawGstinDate) && rawGstinDate.length >= 3) {
this.gstinVerifiedOn = `${rawGstinDate[2]?.toString().padStart(2, '0')}/${rawGstinDate[1]?.toString().padStart(2, '0')}/${rawGstinDate[0]}`;
} else if (rawGstinDate) {
this.gstinVerifiedOn = rawGstinDate;
}
}
}
// Load existing photo & signature
this.loadExistingPhoto();
@@ -772,6 +790,71 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
);
}
/** Verify GSTIN via API */
verifyGstin() {
if (!this.clientId) return;
const gstin = this.kycForm.value.gstinNumber;
if (!gstin || this.kycForm.get('gstinNumber')?.invalid) return;
this.gstinVerifying = true;
const dateFormat = this.settingsService.dateFormat;
const payload: any = {
locale: 'en',
kycType: 1,
riskCategory: this.kycForm.value.riskCategory || 1,
gstinNumber: gstin,
dateFormat,
panNumber: this.kycForm.value.enterprisePan || this.kycForm.value.panNumber || null,
aadhaarNumberMasked: 'XXXXXXXX0000'
};
this.saveKycAndDo(
payload,
() => {
this.clientsService.verifyGSTIN(this.clientId, this.kycId).subscribe({
next: (res: any) => {
this.gstinVerifying = false;
const changes = res?.changes || res;
if (changes?.gstinVerified === true) {
this.gstinVerifyStatus = 'VERIFIED';
this.gstinVerifyName = changes?.gstinTradeName || changes?.gstinLegalName || '';
this.gstinVerifyState = changes?.gstinState || '';
const rawDate = changes?.gstinVerifiedOn;
if (Array.isArray(rawDate)) {
this.gstinVerifiedOn = `${rawDate[2]?.toString().padStart(2, '0')}/${rawDate[1]?.toString().padStart(2, '0')}/${rawDate[0]}`;
} else {
const now = new Date();
this.gstinVerifiedOn =
rawDate ||
`${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}`;
}
} else {
this.gstinVerifyStatus = 'FAILED';
this.showPanErrorAlert(
changes?.gstinVerificationMessage ||
'GSTIN verification failed. Please check the GSTIN number and try again.'
);
}
},
error: (err: any) => {
this.gstinVerifying = false;
this.gstinVerifyStatus = 'FAILED';
this.showPanErrorAlert(
err?.error?.errors?.[0]?.defaultUserMessage ||
err?.error?.defaultUserMessage ||
'GSTIN verification failed.'
);
}
});
},
(msg) => {
this.gstinVerifying = false;
this.showPanErrorAlert(msg);
}
);
}
/** Verify CIN (dummy — no real API) */
verifyCin() {
const cin = this.kycForm.value.cinNumber;
@@ -1,3 +1,11 @@
<!--
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/.
-->
<!--
Copyright since 2025 Mifos Initiative
-->
@@ -20,11 +28,13 @@
<div class="crop-workspace" [class.hidden]="!imageLoaded">
<div class="crop-source">
<canvas #sourceCanvas
<canvas
#sourceCanvas
(mousedown)="onMouseDown($event)"
(mousemove)="onMouseMove($event)"
(mouseup)="onMouseUp()"
(mouseleave)="onMouseUp()">
(mouseleave)="onMouseUp()"
>
</canvas>
<div class="crop-hint">Drag to move. Drag corner to resize.</div>
</div>
@@ -1,3 +1,11 @@
/**
* 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/.
*/
/**
* Copyright since 2025 Mifos Initiative
*/
@@ -158,7 +166,7 @@ $navy: #0f172a;
}
}
@media (max-width: 600px) {
@media (width <= 600px) {
.crop-workspace {
flex-direction: column;
}
@@ -1,8 +1,12 @@
/**
* 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, Inject, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import { Component, inject, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogContent, MatDialogActions } from '@angular/material/dialog';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
@@ -59,10 +63,8 @@ export class ImageCropDialogComponent implements AfterViewInit {
zoom = 1;
constructor(
public dialogRef: MatDialogRef<ImageCropDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: ImageCropData
) {}
dialogRef = inject(MatDialogRef<ImageCropDialogComponent>);
data = inject<ImageCropData>(MAT_DIALOG_DATA);
ngAfterViewInit() {
const reader = new FileReader();
@@ -127,9 +129,14 @@ export class ImageCropDialogComponent implements AfterViewInit {
ctx.clearRect(this.cropX, this.cropY, this.cropW, this.cropH);
ctx.drawImage(
this.img,
this.cropX * this.scaleX, this.cropY * this.scaleY,
this.cropW * this.scaleX, this.cropH * this.scaleY,
this.cropX, this.cropY, this.cropW, this.cropH
this.cropX * this.scaleX,
this.cropY * this.scaleY,
this.cropW * this.scaleX,
this.cropH * this.scaleY,
this.cropX,
this.cropY,
this.cropW,
this.cropH
);
// Crop border
@@ -141,14 +148,31 @@ export class ImageCropDialogComponent implements AfterViewInit {
const hs = 8;
ctx.fillStyle = '#1353a0';
const corners = [
[this.cropX, this.cropY],
[this.cropX + this.cropW, this.cropY],
[this.cropX, this.cropY + this.cropH],
[this.cropX + this.cropW, this.cropY + this.cropH]
[
this.cropX,
this.cropY
],
[
this.cropX + this.cropW,
this.cropY
],
[
this.cropX,
this.cropY + this.cropH
],
[
this.cropX + this.cropW,
this.cropY + this.cropH
]
];
corners.forEach(([x, y]) => {
ctx.fillRect(x - hs / 2, y - hs / 2, hs, hs);
});
corners.forEach(
([
x,
y
]) => {
ctx.fillRect(x - hs / 2, y - hs / 2, hs, hs);
}
);
}
private updatePreview() {
@@ -163,9 +187,14 @@ export class ImageCropDialogComponent implements AfterViewInit {
pCtx.clearRect(0, 0, pw, ph);
pCtx.drawImage(
this.img,
this.cropX * this.scaleX, this.cropY * this.scaleY,
this.cropW * this.scaleX, this.cropH * this.scaleY,
0, 0, pw, ph
this.cropX * this.scaleX,
this.cropY * this.scaleY,
this.cropW * this.scaleX,
this.cropH * this.scaleY,
0,
0,
pw,
ph
);
}
@@ -229,9 +258,14 @@ export class ImageCropDialogComponent implements AfterViewInit {
const ctx = offscreen.getContext('2d')!;
ctx.drawImage(
this.img,
this.cropX * this.scaleX, this.cropY * this.scaleY,
this.cropW * this.scaleX, this.cropH * this.scaleY,
0, 0, offscreen.width, offscreen.height
this.cropX * this.scaleX,
this.cropY * this.scaleY,
this.cropW * this.scaleX,
this.cropH * this.scaleY,
0,
0,
offscreen.width,
offscreen.height
);
offscreen.toBlob((blob) => {