340 lines
12 KiB
TypeScript
340 lines
12 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}`);
|
|
}
|
|
|
|
getNextAvailableLeaflet(branchId?: number, sponsorBankId?: number, inventoryId?: number): Observable<any> {
|
|
const params: any = {};
|
|
if (branchId) params.branchId = branchId;
|
|
if (sponsorBankId) params.sponsorBankId = sponsorBankId;
|
|
if (inventoryId) params.inventoryId = inventoryId;
|
|
return this.http.get<any>('/sponsordd/leaflets/next-available', { params });
|
|
}
|
|
|
|
getSponsorBanks(): Observable<any[]> {
|
|
return this.http.get<any[]>('/sponsorbanks');
|
|
}
|
|
|
|
// ── 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' | 'GL';
|
|
savingsAccountId?: number;
|
|
currentAccountId?: number;
|
|
chequeNumber?: string;
|
|
chequeBankName?: string;
|
|
cashierDepositRequestId?: number;
|
|
denominationData?: string;
|
|
glAccountId?: 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 / GL debit funds several drafts) ──
|
|
// CASH is the one funding method a slip cannot use: a single counted pile of notes cannot be
|
|
// apportioned across drafts without the denomination total ceasing to prove what was taken.
|
|
submitIssueRequestBatch(
|
|
inventoryId: number,
|
|
data: {
|
|
clientId?: number;
|
|
issuingBranchId: number;
|
|
issuedDate: string; // yyyy-MM-dd
|
|
paymentMethod: 'ACCOUNT_DEBIT' | 'CHEQUE' | 'GL';
|
|
savingsAccountId?: number;
|
|
currentAccountId?: number;
|
|
chequeNumber?: string;
|
|
chequeBankName?: string;
|
|
/** The GL account a GL-funded slip is debited from; omitted for every other method. */
|
|
glAccountId?: number;
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Approves a slip. Omitting approvedRequestIds issues every draft on it; supplying them issues
|
|
* only those, debits only for them, and rejects the rest — whose DD numbers return to stock.
|
|
*/
|
|
approveIssueRequestBatch(batchId: number, approvedRequestIds?: number[]): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/issue-requests/batch/${batchId}/approve`, { approvedRequestIds });
|
|
}
|
|
|
|
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 });
|
|
}
|
|
|
|
// ── DD revoke requests (maker-checker) ──
|
|
// A leaflet ruined at the printer. Its number can never be handed to anyone, so the draft moves
|
|
// onto the next available blank and both leaflets stay on the register — one spoiled, one
|
|
// carrying the draft. Distinct from a reprint, which re-prints the same leaflet.
|
|
requestRevoke(leafletId: number, reason: string): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/leaflets/${leafletId}/revoke-request`, { reason });
|
|
}
|
|
|
|
getAllRevokeRequests(status?: string): Observable<any[]> {
|
|
const params = status ? { status } : {};
|
|
return this.http.get<any[]>('/sponsordd/revoke-requests', { params });
|
|
}
|
|
|
|
approveRevokeRequest(id: number): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/revoke-requests/${id}/approve`, {});
|
|
}
|
|
|
|
rejectRevokeRequest(id: number, rejectionReason: string): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/revoke-requests/${id}/reject`, { rejectionReason });
|
|
}
|
|
|
|
// ── DD amendment requests (maker-checker) ──
|
|
// Correcting an issued draft's beneficiary, or the branch and city it is payable at. Nothing else
|
|
// is amendable: the money behind the face value has already moved, so a draft wrong in that way
|
|
// is cancelled and re-issued rather than edited.
|
|
|
|
/**
|
|
* Maker step. A field left blank keeps what the draft already carries, so correcting only the
|
|
* city does not mean retyping the beneficiary and risking a second mistake.
|
|
*/
|
|
requestDdAmendment(
|
|
leafletId: number,
|
|
changes: { beneficiaryName?: string; payableAtBranch?: string; payableAtCity?: string; reason: string }
|
|
): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/leaflets/${leafletId}/amendment-request`, changes);
|
|
}
|
|
|
|
getAllAmendmentRequests(status?: string): Observable<any[]> {
|
|
const params = status ? { status } : {};
|
|
return this.http.get<any[]>('/sponsordd/amendment-requests', { params });
|
|
}
|
|
|
|
approveAmendmentRequest(id: number): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/amendment-requests/${id}/approve`, {});
|
|
}
|
|
|
|
rejectAmendmentRequest(id: number, rejectionReason: string): Observable<any> {
|
|
return this.http.post<any>(`/sponsordd/amendment-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);
|
|
})
|
|
)
|
|
);
|
|
})
|
|
);
|
|
}
|
|
}
|