Adds sponsor-dd and sponsor-dd-reconciliation feature modules, a shared customer search dialog, and wires them into routing/shell/ organization screens alongside cashier and deposit-request updates.
268 lines
9.0 KiB
TypeScript
268 lines
9.0 KiB
TypeScript
/**
|
|
* 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 { Injectable, inject } from '@angular/core';
|
|
import { HttpClient } from '@angular/common/http';
|
|
import { Observable, of } from 'rxjs';
|
|
import { map, switchMap } from 'rxjs/operators';
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class SponsorDDService {
|
|
private http = inject(HttpClient);
|
|
|
|
// ── Inventory ──
|
|
getInventories(): Observable<any[]> {
|
|
return this.http.get<any[]>('/sponsordd/inventory');
|
|
}
|
|
|
|
getInventoryById(id: number): Observable<any> {
|
|
return this.http.get<any>(`/sponsordd/inventory/${id}`);
|
|
}
|
|
|
|
registerInventory(data: {
|
|
sponsorBankId: number;
|
|
branchId: number;
|
|
seriesStart: number;
|
|
seriesEnd: number;
|
|
receivedDate: string; // yyyy-MM-dd
|
|
}): Observable<any> {
|
|
return this.http.post<any>('/sponsordd/inventory', data);
|
|
}
|
|
|
|
// ── Leaflets ──
|
|
getLeafletsByInventory(inventoryId: number): Observable<any[]> {
|
|
return this.http.get<any[]>(`/sponsordd/inventory/${inventoryId}/leaflets`);
|
|
}
|
|
|
|
getLeafletById(leafletId: number): Observable<any> {
|
|
return this.http.get<any>(`/sponsordd/leaflets/${leafletId}`);
|
|
}
|
|
|
|
getLeafletByDdNumber(ddNumber: string): Observable<any> {
|
|
return this.http.get<any>(`/sponsordd/leaflets/by-number/${ddNumber}`);
|
|
}
|
|
|
|
// ── Actions ──
|
|
issueDemandDraft(
|
|
leafletId: number,
|
|
data: {
|
|
clientId: number;
|
|
beneficiaryName: string;
|
|
faceValue: number;
|
|
issuingBranchId: number;
|
|
issuedDate: string; // yyyy-MM-dd
|
|
savingsTransactionId?: number;
|
|
currentTransactionId?: number;
|
|
journalEntryId?: number;
|
|
}
|
|
): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/leaflets/${leafletId}/issue`, data);
|
|
}
|
|
|
|
cancelDemandDraft(
|
|
leafletId: number,
|
|
data: {
|
|
remarks: string;
|
|
refundSavingsTransactionId?: number;
|
|
refundCurrentTransactionId?: number;
|
|
}
|
|
): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/leaflets/${leafletId}/cancel`, data);
|
|
}
|
|
|
|
stopPaymentDemandDraft(leafletId: number, data: { remarks: string }): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/leaflets/${leafletId}/stop`, data);
|
|
}
|
|
|
|
// ── DD Issuance Requests (maker-checker) ──
|
|
submitIssueRequest(
|
|
leafletId: number,
|
|
data: {
|
|
clientId?: number;
|
|
walkinPurchaserName?: string;
|
|
walkinPurchaserAddress?: string;
|
|
walkinPurchaserAadhaar?: string;
|
|
beneficiaryName: string;
|
|
payableAtBranch: string;
|
|
payableAtCity: string;
|
|
faceValue: number;
|
|
chargesAmount?: number;
|
|
issuingBranchId: number;
|
|
issuedDate: string; // yyyy-MM-dd
|
|
paymentMethod: 'ACCOUNT_DEBIT' | 'CHEQUE' | 'CASH';
|
|
savingsAccountId?: number;
|
|
currentAccountId?: number;
|
|
chequeNumber?: string;
|
|
chequeBankName?: string;
|
|
cashierDepositRequestId?: number;
|
|
}
|
|
): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/leaflets/${leafletId}/issue-request`, data);
|
|
}
|
|
|
|
getAvailableCashierDepositRequests(): Observable<any[]> {
|
|
return this.http.get<any[]>('/sponsordd/cashier-deposit-requests/available');
|
|
}
|
|
|
|
// ── Multi-DD slip (one account debit/cheque funds several drafts) ──
|
|
submitIssueRequestBatch(
|
|
inventoryId: number,
|
|
data: {
|
|
clientId?: number;
|
|
issuingBranchId: number;
|
|
issuedDate: string; // yyyy-MM-dd
|
|
paymentMethod: 'ACCOUNT_DEBIT' | 'CHEQUE';
|
|
savingsAccountId?: number;
|
|
currentAccountId?: number;
|
|
chequeNumber?: string;
|
|
chequeBankName?: string;
|
|
lines: {
|
|
beneficiaryName: string;
|
|
payableAtBranch: string;
|
|
payableAtCity: string;
|
|
faceValue: number;
|
|
chargesAmount?: number;
|
|
}[];
|
|
}
|
|
): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/inventory/${inventoryId}/issue-request-batch`, data);
|
|
}
|
|
|
|
approveIssueRequestBatch(batchId: number): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/issue-requests/batch/${batchId}/approve`, {});
|
|
}
|
|
|
|
rejectIssueRequestBatch(batchId: number, rejectionReason: string): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/issue-requests/batch/${batchId}/reject`, { rejectionReason });
|
|
}
|
|
|
|
// ── Sponsor DD GL mapping (back-office configuration) ──
|
|
getGlMappings(): Observable<any[]> {
|
|
return this.http.get<any[]>('/sponsordd/gl-mapping');
|
|
}
|
|
|
|
upsertGlMapping(data: {
|
|
sponsorBankId: number;
|
|
ddPayableGlAccountId: number;
|
|
chequeClearingGlAccountId?: number;
|
|
cashGlAccountId?: number;
|
|
chargesIncomeGlAccountId?: number;
|
|
cashSuspenseGlAccountId?: number;
|
|
gstPercentage?: number;
|
|
gstPayableGlAccountId?: number;
|
|
ddLimitAmount?: number | null;
|
|
}): Observable<any> {
|
|
return this.http.post<any>('/sponsordd/gl-mapping', data);
|
|
}
|
|
|
|
getAllIssueRequests(status?: string): Observable<any[]> {
|
|
const params = status ? { status } : {};
|
|
return this.http.get<any[]>('/sponsordd/issue-requests', { params });
|
|
}
|
|
|
|
getMyIssueRequests(): Observable<any[]> {
|
|
return this.http.get<any[]>('/sponsordd/issue-requests/mine');
|
|
}
|
|
|
|
getIssueRequestById(id: number): Observable<any> {
|
|
return this.http.get<any>(`/sponsordd/issue-requests/${id}`);
|
|
}
|
|
|
|
approveIssueRequest(id: number): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/issue-requests/${id}/approve`, {});
|
|
}
|
|
|
|
rejectIssueRequest(id: number, rejectionReason: string): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/issue-requests/${id}/reject`, { rejectionReason });
|
|
}
|
|
|
|
// ── DD reprint requests (maker-checker) ──
|
|
recordPrint(leafletId: number): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/leaflets/${leafletId}/print`, {});
|
|
}
|
|
|
|
requestReprint(leafletId: number, reason: string): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/leaflets/${leafletId}/reprint-request`, { reason });
|
|
}
|
|
|
|
getAllReprintRequests(status?: string): Observable<any[]> {
|
|
const params = status ? { status } : {};
|
|
return this.http.get<any[]>('/sponsordd/reprint-requests', { params });
|
|
}
|
|
|
|
approveReprintRequest(id: number): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/reprint-requests/${id}/approve`, {});
|
|
}
|
|
|
|
rejectReprintRequest(id: number, rejectionReason: string): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/reprint-requests/${id}/reject`, { rejectionReason });
|
|
}
|
|
|
|
// ── Approver signature (stamped onto the printed DD) ──
|
|
// Reuses Fineract's generic document-attachment API (entityType is a free-form string with
|
|
// no per-type backend wiring required) rather than building dedicated signature storage.
|
|
private static readonly SIGNATURE_ENTITY_TYPE = 'users';
|
|
private static readonly SIGNATURE_DOCUMENT_NAME = 'signature';
|
|
|
|
getUserDocuments(userId: number): Observable<any[]> {
|
|
return this.http.get<any[]>(`/${SponsorDDService.SIGNATURE_ENTITY_TYPE}/${userId}/documents`);
|
|
}
|
|
|
|
uploadSignature(userId: number, file: File): Observable<any> {
|
|
const formData = new FormData();
|
|
formData.append('file', file, file.name);
|
|
formData.append('name', SponsorDDService.SIGNATURE_DOCUMENT_NAME);
|
|
formData.append('description', 'Approver signature, stamped onto printed demand drafts');
|
|
return this.http.post<any>(`/${SponsorDDService.SIGNATURE_ENTITY_TYPE}/${userId}/documents`, formData);
|
|
}
|
|
|
|
deleteSignature(userId: number, documentId: number): Observable<any> {
|
|
return this.http.delete<any>(`/${SponsorDDService.SIGNATURE_ENTITY_TYPE}/${userId}/documents/${documentId}`);
|
|
}
|
|
|
|
getSignatureBlob(userId: number, documentId: number): Observable<Blob> {
|
|
return this.http.get(`/${SponsorDDService.SIGNATURE_ENTITY_TYPE}/${userId}/documents/${documentId}/attachment`, {
|
|
responseType: 'blob'
|
|
});
|
|
}
|
|
|
|
/** Finds this user's uploaded signature document, if any, from their full document list. */
|
|
findSignatureDocument(userId: number): Observable<any> {
|
|
return this.getUserDocuments(userId).pipe(
|
|
map((docs) => (docs || []).find((d) => d.name === SponsorDDService.SIGNATURE_DOCUMENT_NAME) || null)
|
|
);
|
|
}
|
|
|
|
/** Fetches this user's signature (if any) as a data URI ready to use in an <img> src or print HTML. */
|
|
getSignatureDataUri(userId: number): Observable<string | null> {
|
|
return this.findSignatureDocument(userId).pipe(
|
|
switchMap((doc) => {
|
|
if (!doc) {
|
|
return of(null);
|
|
}
|
|
return this.getSignatureBlob(userId, doc.id).pipe(
|
|
switchMap(
|
|
(blob) =>
|
|
new Observable<string>((subscriber) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
subscriber.next(reader.result as string);
|
|
subscriber.complete();
|
|
};
|
|
reader.onerror = () => subscriber.error(reader.error);
|
|
reader.readAsDataURL(blob);
|
|
})
|
|
)
|
|
);
|
|
})
|
|
);
|
|
}
|
|
}
|