/** * 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 { return this.http.get('/sponsordd/inventory'); } getInventoryById(id: number): Observable { return this.http.get(`/sponsordd/inventory/${id}`); } registerInventory(data: { sponsorBankId: number; branchId: number; seriesStart: number; seriesEnd: number; receivedDate: string; // yyyy-MM-dd }): Observable { return this.http.post('/sponsordd/inventory', data); } // ── Leaflets ── getLeafletsByInventory(inventoryId: number): Observable { return this.http.get(`/sponsordd/inventory/${inventoryId}/leaflets`); } getLeafletById(leafletId: number): Observable { return this.http.get(`/sponsordd/leaflets/${leafletId}`); } getLeafletByDdNumber(ddNumber: string): Observable { return this.http.get(`/sponsordd/leaflets/by-number/${ddNumber}`); } getNextAvailableLeaflet(branchId?: number, sponsorBankId?: number, inventoryId?: number): Observable { const params: any = {}; if (branchId) params.branchId = branchId; if (sponsorBankId) params.sponsorBankId = sponsorBankId; if (inventoryId) params.inventoryId = inventoryId; return this.http.get('/sponsordd/leaflets/next-available', { params }); } getSponsorBanks(): Observable { return this.http.get('/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 { return this.http.post(`/sponsordd/leaflets/${leafletId}/issue`, data); } cancelDemandDraft( leafletId: number, data: { remarks: string; refundSavingsTransactionId?: number; refundCurrentTransactionId?: number; } ): Observable { return this.http.post(`/sponsordd/leaflets/${leafletId}/cancel`, data); } stopPaymentDemandDraft(leafletId: number, data: { remarks: string }): Observable { return this.http.post(`/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 { return this.http.post(`/sponsordd/leaflets/${leafletId}/issue-request`, data); } getAvailableCashierDepositRequests(): Observable { return this.http.get('/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 { return this.http.post(`/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 { return this.http.post(`/sponsordd/issue-requests/batch/${batchId}/approve`, { approvedRequestIds }); } rejectIssueRequestBatch(batchId: number, rejectionReason: string): Observable { return this.http.post(`/sponsordd/issue-requests/batch/${batchId}/reject`, { rejectionReason }); } // ── Sponsor DD GL mapping (back-office configuration) ── getGlMappings(): Observable { return this.http.get('/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 { return this.http.post('/sponsordd/gl-mapping', data); } getAllIssueRequests(status?: string): Observable { const params = status ? { status } : {}; return this.http.get('/sponsordd/issue-requests', { params }); } getMyIssueRequests(): Observable { return this.http.get('/sponsordd/issue-requests/mine'); } getIssueRequestById(id: number): Observable { return this.http.get(`/sponsordd/issue-requests/${id}`); } approveIssueRequest(id: number): Observable { return this.http.post(`/sponsordd/issue-requests/${id}/approve`, {}); } rejectIssueRequest(id: number, rejectionReason: string): Observable { return this.http.post(`/sponsordd/issue-requests/${id}/reject`, { rejectionReason }); } // ── DD reprint requests (maker-checker) ── recordPrint(leafletId: number): Observable { return this.http.post(`/sponsordd/leaflets/${leafletId}/print`, {}); } requestReprint(leafletId: number, reason: string): Observable { return this.http.post(`/sponsordd/leaflets/${leafletId}/reprint-request`, { reason }); } getAllReprintRequests(status?: string): Observable { const params = status ? { status } : {}; return this.http.get('/sponsordd/reprint-requests', { params }); } approveReprintRequest(id: number): Observable { return this.http.post(`/sponsordd/reprint-requests/${id}/approve`, {}); } rejectReprintRequest(id: number, rejectionReason: string): Observable { return this.http.post(`/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 { return this.http.post(`/sponsordd/leaflets/${leafletId}/revoke-request`, { reason }); } getAllRevokeRequests(status?: string): Observable { const params = status ? { status } : {}; return this.http.get('/sponsordd/revoke-requests', { params }); } approveRevokeRequest(id: number): Observable { return this.http.post(`/sponsordd/revoke-requests/${id}/approve`, {}); } rejectRevokeRequest(id: number, rejectionReason: string): Observable { return this.http.post(`/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 { return this.http.post(`/sponsordd/leaflets/${leafletId}/amendment-request`, changes); } getAllAmendmentRequests(status?: string): Observable { const params = status ? { status } : {}; return this.http.get('/sponsordd/amendment-requests', { params }); } approveAmendmentRequest(id: number): Observable { return this.http.post(`/sponsordd/amendment-requests/${id}/approve`, {}); } rejectAmendmentRequest(id: number, rejectionReason: string): Observable { return this.http.post(`/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 { return this.http.get(`/${SponsorDDService.SIGNATURE_ENTITY_TYPE}/${userId}/documents`); } uploadSignature(userId: number, file: File): Observable { 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(`/${SponsorDDService.SIGNATURE_ENTITY_TYPE}/${userId}/documents`, formData); } deleteSignature(userId: number, documentId: number): Observable { return this.http.delete(`/${SponsorDDService.SIGNATURE_ENTITY_TYPE}/${userId}/documents/${documentId}`); } getSignatureBlob(userId: number, documentId: number): Observable { 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 { 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 src or print HTML. */ getSignatureDataUri(userId: number): Observable { return this.findSignatureDocument(userId).pipe( switchMap((doc) => { if (!doc) { return of(null); } return this.getSignatureBlob(userId, doc.id).pipe( switchMap( (blob) => new Observable((subscriber) => { const reader = new FileReader(); reader.onload = () => { subscriber.next(reader.result as string); subscriber.complete(); }; reader.onerror = () => subscriber.error(reader.error); reader.readAsDataURL(blob); }) ) ); }) ); } }