Files
cbsnexora/src/app/sponsor-dd/sponsor-dd.service.ts
T

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);
})
)
);
})
);
}
}