feat(dds): premature payout calculation, back to agent, income and TDS GLs on products
- Premature closure dialog: interest still to credit through yesterday, total interest earned, a day-by-day interest table, and the agent penalty worked out by the server on this account's collection (with its cap). Dates follow the bank's business date rather than the computer's clock, and 'yyyy-MM-dd' strings are read as local days. - DDS account page: Back returns to the agent the account was opened from. - DDS product Accounting tab: Premature Closure Income (income) and TDS Payable (194H, liability); the agent penalty field is relabelled as a percentage of the account's collection. - Agent top-up transactions use the stored PREMATURE_AGENT_TOPUP type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -666,7 +666,8 @@ export class DdsAccountViewComponent implements OnInit {
|
|||||||
id: account.id,
|
id: account.id,
|
||||||
agentId: account.agentId,
|
agentId: account.agentId,
|
||||||
interestCalc: account.interestCalc,
|
interestCalc: account.interestCalc,
|
||||||
collectedToDate: account.collectedToDate
|
collectedToDate: account.collectedToDate,
|
||||||
|
openingBalance: account.openingBalance
|
||||||
},
|
},
|
||||||
width: '960px',
|
width: '960px',
|
||||||
maxWidth: '96vw',
|
maxWidth: '96vw',
|
||||||
|
|||||||
+111
-20
@@ -62,16 +62,6 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (pendingInterest > 0) {
|
|
||||||
<div class="pcd-alert">
|
|
||||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
|
||||||
<span
|
|
||||||
>₹{{ pendingInterest | number: '1.2-2' }} interest is calculated but not posted yet. Run
|
|
||||||
<b>Interest Posting</b> first — the closure is refused until it is in the balance.</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="pcd-grid">
|
<div class="pcd-grid">
|
||||||
<div class="pcd-main">
|
<div class="pcd-main">
|
||||||
<!-- Step 1 -->
|
<!-- Step 1 -->
|
||||||
@@ -131,7 +121,8 @@
|
|||||||
<span
|
<span
|
||||||
>Either way, {{ data.agentCode }} is charged
|
>Either way, {{ data.agentCode }} is charged
|
||||||
<strong>{{ scheme.prematureAgentCommissionPenalty | number: '1.0-2' }}%</strong>
|
<strong>{{ scheme.prematureAgentCommissionPenalty | number: '1.0-2' }}%</strong>
|
||||||
of the commission owed to them — the round was not kept going.</span
|
of what was collected on this account, never more than the commission it earned them — taken off their next
|
||||||
|
commission.</span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
@@ -258,7 +249,12 @@
|
|||||||
<b>₹{{ deposits | number: '1.2-2' }}</b>
|
<b>₹{{ deposits | number: '1.2-2' }}</b>
|
||||||
</div>
|
</div>
|
||||||
<div class="pcd-calc__row">
|
<div class="pcd-calc__row">
|
||||||
<span>Interest credited so far</span>
|
<span
|
||||||
|
>Interest credited so far
|
||||||
|
@if (creditedThrough) {
|
||||||
|
<small>till {{ creditedThrough | dateFormat }}</small>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
<b>
|
<b>
|
||||||
@if (interestCredited === null) {
|
@if (interestCredited === null) {
|
||||||
…
|
…
|
||||||
@@ -267,18 +263,107 @@
|
|||||||
}
|
}
|
||||||
</b>
|
</b>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Interest still owed through yesterday: the closure credits it before anything else. -->
|
||||||
|
@if (interestDue > 0) {
|
||||||
|
<div class="pcd-calc__row pcd-pos">
|
||||||
|
<span
|
||||||
|
>Interest still to credit
|
||||||
|
@if (interestThrough) {
|
||||||
|
<small>till {{ interestThrough | dateFormat }} — added at closing</small>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<b>+ ₹{{ interestDue | number: '1.2-2' }}</b>
|
||||||
|
</div>
|
||||||
|
} @else if (interestThrough) {
|
||||||
|
<div class="pcd-calc__row pcd-calc__row--ok">
|
||||||
|
<span
|
||||||
|
><span><fa-icon icon="check-circle"></fa-icon> Interest is up to date</span>
|
||||||
|
<small>credited till {{ interestThrough | dateFormat }}</small></span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<!-- Everything the account will have earned by closing, credited or not - what the customer gets as interest. -->
|
||||||
|
<div class="pcd-calc__row pcd-calc__row--interest">
|
||||||
|
<span
|
||||||
|
>Total interest earned
|
||||||
|
@if (interestThrough) {
|
||||||
|
<small>{{ data.openingDate | dateFormat }} to {{ interestThrough | dateFormat }}</small>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<b>₹{{ totalInterest | number: '1.2-2' }}</b>
|
||||||
|
</div>
|
||||||
<div class="pcd-calc__row pcd-calc__row--sum">
|
<div class="pcd-calc__row pcd-calc__row--sum">
|
||||||
<span>Current balance</span>
|
<span
|
||||||
|
>Balance on closing day
|
||||||
|
<small>{{ businessDate | dateFormat }} — this day earns no interest, the money leaves today</small></span
|
||||||
|
>
|
||||||
<b>₹{{ balance | number: '1.2-2' }}</b>
|
<b>₹{{ balance | number: '1.2-2' }}</b>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Where the interest comes from, day by day: the balance each day and what it earned. -->
|
||||||
|
@if (interestDays.length > 0) {
|
||||||
|
<button type="button" class="pcd-days__toggle" (click)="showDays = !showDays">
|
||||||
|
<fa-icon [icon]="showDays ? 'chevron-up' : 'chevron-down'"></fa-icon>
|
||||||
|
{{ showDays ? 'Hide' : 'Show' }} day-by-day interest ({{ interestDays.length }}
|
||||||
|
{{ interestDays.length === 1 ? 'day' : 'days' }})
|
||||||
|
</button>
|
||||||
|
@if (showDays) {
|
||||||
|
<div class="pcd-days">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th class="num">Balance</th>
|
||||||
|
<th class="num">Interest</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (day of interestDays; track day.date.getTime()) {
|
||||||
|
<tr [class.pcd-days__due]="!day.credited">
|
||||||
|
<td>{{ day.date | dateFormat }}</td>
|
||||||
|
<td class="num">₹{{ day.balance | number: '1.2-2' }}</td>
|
||||||
|
<td class="num">₹{{ day.interest | number: '1.4-4' }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="pcd-days__tag" [class.pcd-days__tag--due]="!day.credited">{{
|
||||||
|
day.credited ? 'Credited' : 'To credit'
|
||||||
|
}}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">Credited so far</td>
|
||||||
|
<td class="num" colspan="2">₹{{ interestCredited ?? 0 | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
@if (interestDue > 0) {
|
||||||
|
<tr class="pcd-days__due">
|
||||||
|
<td colspan="2">Added at closing</td>
|
||||||
|
<td class="num" colspan="2">+ ₹{{ interestDue | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
<tr class="pcd-days__sum">
|
||||||
|
<td colspan="2">Total interest</td>
|
||||||
|
<td class="num" colspan="2">₹{{ totalInterest | number: '1.2-2' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
<p class="pcd-days__note">
|
||||||
|
Balance × {{ data.interestRate | number: '1.0-2' }}% ÷ 365 for each day. Each run's total is rounded to
|
||||||
|
paise, so the totals can differ from the day figures by a paisa. {{ businessDate | dateFormat }}, the
|
||||||
|
closing day, earns nothing.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@if (borneBy === 'CUSTOMER') {
|
@if (borneBy === 'CUSTOMER') {
|
||||||
<div class="pcd-calc__row pcd-neg">
|
<div class="pcd-calc__row pcd-neg">
|
||||||
<span
|
<span
|
||||||
>Interest taken back
|
>Interest taken back
|
||||||
<small
|
<small
|
||||||
>₹{{ interestCredited ?? 0 | number: '1.2-2' }} ×
|
>₹{{ totalInterest | number: '1.2-2' }} × {{ data.interestRate - reducedRate | number: '1.0-2' }}% ÷
|
||||||
{{ data.interestRate - reducedRate | number: '1.0-2' }}% ÷
|
|
||||||
{{ data.interestRate | number: '1.0-2' }}%</small
|
{{ data.interestRate | number: '1.0-2' }}%</small
|
||||||
></span
|
></span
|
||||||
>
|
>
|
||||||
@@ -319,13 +404,19 @@
|
|||||||
<div class="pcd-calc__row">
|
<div class="pcd-calc__row">
|
||||||
<span
|
<span
|
||||||
>Commission penalty
|
>Commission penalty
|
||||||
@if (scheme?.prematureAgentCommissionPenalty > 0) {
|
@if (agentPenalty && (agentPenalty.penaltyRate ?? 0) > 0) {
|
||||||
<small
|
<small
|
||||||
>{{ scheme.prematureAgentCommissionPenalty | number: '1.0-2' }}% of ₹{{
|
>₹{{ agentPenalty.collection ?? 0 | number: '1.2-2' }} collected on this account ×
|
||||||
agentCommission ?? 0 | number: '1.2-2'
|
{{ agentPenalty.penaltyRate | number: '1.0-2' }}%</small
|
||||||
}}
|
|
||||||
owed</small
|
|
||||||
>
|
>
|
||||||
|
@if (agentPenalty.commissionEarned !== null && agentPenalty.commissionEarned !== undefined) {
|
||||||
|
<small
|
||||||
|
>{{ penaltyCapped ? 'Capped at' : 'Never more than' }} ₹{{
|
||||||
|
agentPenalty.commissionEarned | number: '1.2-2'
|
||||||
|
}}
|
||||||
|
this account earned them ({{ agentPenalty.commissionRate | number: '1.0-2' }}% commission)</small
|
||||||
|
>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
<b>₹{{ commissionPenalty | number: '1.2-2' }}</b>
|
<b>₹{{ commissionPenalty | number: '1.2-2' }}</b>
|
||||||
|
|||||||
+113
@@ -449,6 +449,23 @@
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--ok {
|
||||||
|
color: #15803d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--interest {
|
||||||
|
margin: 4px 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #15803d;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&--sum {
|
&--sum {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
border-top: 1px dashed #cbd5e1;
|
border-top: 1px dashed #cbd5e1;
|
||||||
@@ -526,6 +543,102 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The interest day by day, opened from the calculation panel */
|
||||||
|
.pcd-days {
|
||||||
|
max-height: 260px;
|
||||||
|
margin: 2px 0 6px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
&__toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin: 4px 0 6px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: #4338ca;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 5px 8px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: #f8fafc;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.num {
|
||||||
|
text-align: right;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
tfoot td {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__due td {
|
||||||
|
color: #15803d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sum td {
|
||||||
|
border-top: 1px solid #cbd5e1;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tag {
|
||||||
|
padding: 1px 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
|
||||||
|
&--due {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #15803d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__note {
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 8px 8px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #94a3b8;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (width <= 760px) {
|
@media (width <= 760px) {
|
||||||
.pcd-grid {
|
.pcd-grid {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
|||||||
+130
-30
@@ -23,6 +23,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|||||||
/** Custom Services */
|
/** Custom Services */
|
||||||
import { OrganizationService } from 'app/organization/organization.service';
|
import { OrganizationService } from 'app/organization/organization.service';
|
||||||
import { ProductsService } from 'app/products/products.service';
|
import { ProductsService } from 'app/products/products.service';
|
||||||
|
import { SettingsService } from 'app/settings/settings.service';
|
||||||
|
|
||||||
/** Custom Pipes */
|
/** Custom Pipes */
|
||||||
import { DateFormatPipe } from 'app/pipes/date-format.pipe';
|
import { DateFormatPipe } from 'app/pipes/date-format.pipe';
|
||||||
@@ -66,6 +67,7 @@ import { DateFormatPipe } from 'app/pipes/date-format.pipe';
|
|||||||
export class PrematureCloseDialogComponent implements OnInit {
|
export class PrematureCloseDialogComponent implements OnInit {
|
||||||
private organizationService = inject(OrganizationService);
|
private organizationService = inject(OrganizationService);
|
||||||
private productsService = inject(ProductsService);
|
private productsService = inject(ProductsService);
|
||||||
|
private settingsService = inject(SettingsService);
|
||||||
private dialogRef = inject(MatDialogRef<PrematureCloseDialogComponent>);
|
private dialogRef = inject(MatDialogRef<PrematureCloseDialogComponent>);
|
||||||
data = inject<any>(MAT_DIALOG_DATA);
|
data = inject<any>(MAT_DIALOG_DATA);
|
||||||
|
|
||||||
@@ -183,6 +185,7 @@ export class PrematureCloseDialogComponent implements OnInit {
|
|||||||
this.organizationService.getPartyTransactionsOfParty(this.data.id).subscribe({
|
this.organizationService.getPartyTransactionsOfParty(this.data.id).subscribe({
|
||||||
next: (txns: any) => {
|
next: (txns: any) => {
|
||||||
const list: any[] = Array.isArray(txns) ? txns : txns?.pageItems || [];
|
const list: any[] = Array.isArray(txns) ? txns : txns?.pageItems || [];
|
||||||
|
this.transactions = list;
|
||||||
this.interestCredited = list
|
this.interestCredited = list
|
||||||
.filter((t) => PrematureCloseDialogComponent.INTEREST_TYPES.includes(t.transactionType))
|
.filter((t) => PrematureCloseDialogComponent.INTEREST_TYPES.includes(t.transactionType))
|
||||||
.reduce((sum, t) => sum + (t.direction === 'DEBIT' ? -1 : 1) * Number(t.amount || 0), 0);
|
.reduce((sum, t) => sum + (t.direction === 'DEBIT' ? -1 : 1) * Number(t.amount || 0), 0);
|
||||||
@@ -190,11 +193,18 @@ export class PrematureCloseDialogComponent implements OnInit {
|
|||||||
error: () => (this.interestCredited = null)
|
error: () => (this.interestCredited = null)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// The commission the agent is owed now, which the scheme's penalty is a share of.
|
// Interest earned through yesterday but not yet credited. The closure credits it first, so the
|
||||||
if (this.data.agentId) {
|
// figures below start from it rather than from a balance that is a few days behind.
|
||||||
this.organizationService.getAgent(this.data.agentId).subscribe({
|
if (this.data.id) {
|
||||||
next: (agent: any) => (this.agentCommission = Number(agent?.commission || 0)),
|
this.organizationService.getPartyInterestDue(this.data.id).subscribe({
|
||||||
error: () => (this.agentCommission = null)
|
next: (due: any) => {
|
||||||
|
this.interestDue = Number(due?.interestDue || 0);
|
||||||
|
this.interestThrough = due?.through ?? null;
|
||||||
|
this.creditedThrough = due?.creditedThrough ?? null;
|
||||||
|
// The agent's penalty as the server works it out: on this account's collection alone.
|
||||||
|
this.agentPenalty = due?.agentPenalty ?? null;
|
||||||
|
},
|
||||||
|
error: () => (this.interestDue = 0)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,33 +217,113 @@ export class PrematureCloseDialogComponent implements OnInit {
|
|||||||
|
|
||||||
/** Interest credited to the account so far; null until read (or if it could not be). */
|
/** Interest credited to the account so far; null until read (or if it could not be). */
|
||||||
interestCredited: number | null = null;
|
interestCredited: number | null = null;
|
||||||
/** What the agent is owed in commission today; null until read. */
|
/** Interest earned through the day before closing but not yet credited - credited by the closure itself. */
|
||||||
agentCommission: number | null = null;
|
interestDue = 0;
|
||||||
|
/** The last day that earns (the day before the business date), and the day interest is credited through now. */
|
||||||
|
interestThrough: string | null = null;
|
||||||
|
creditedThrough: string | null = null;
|
||||||
|
|
||||||
|
/** Everything posted to the account, for the day-by-day interest table. */
|
||||||
|
private transactions: any[] = [];
|
||||||
|
|
||||||
|
/** Whether the day-by-day interest table is open. */
|
||||||
|
showDays = false;
|
||||||
|
|
||||||
|
private static dayKey(date: Date): string {
|
||||||
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Each earning day from opening to the day before closing: the balance at the end of that day - every
|
||||||
|
* movement up to it, interest postings included, as the server's daily product counts it - and the
|
||||||
|
* interest it earns (balance × rate ÷ 365). Days up to the last credited day are marked credited; the
|
||||||
|
* rest are what the closure credits first. The server rounds each run's total, not each day, so the
|
||||||
|
* per-day figures are shown to four places and the totals come from the server.
|
||||||
|
*/
|
||||||
|
get interestDays(): { date: Date; balance: number; interest: number; credited: boolean }[] {
|
||||||
|
const opened = PrematureCloseDialogComponent.toDate(this.data.openingDate);
|
||||||
|
const rate = Number(this.data.interestRate || 0);
|
||||||
|
if (!opened || rate <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const last = new Date(this.businessDate);
|
||||||
|
last.setDate(last.getDate() - 1);
|
||||||
|
const creditedTo = this.creditedThrough ? PrematureCloseDialogComponent.toDate(this.creditedThrough) : null;
|
||||||
|
const byDay = new Map<string, number>();
|
||||||
|
for (const t of this.transactions) {
|
||||||
|
const on = PrematureCloseDialogComponent.toDate(t.transactionDate);
|
||||||
|
if (on) {
|
||||||
|
const key = PrematureCloseDialogComponent.dayKey(on);
|
||||||
|
byDay.set(key, (byDay.get(key) || 0) + (t.direction === 'DEBIT' ? -1 : 1) * Number(t.amount || 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rows: { date: Date; balance: number; interest: number; credited: boolean }[] = [];
|
||||||
|
let balance = Number(this.data.openingBalance || 0);
|
||||||
|
for (
|
||||||
|
let day = new Date(opened);
|
||||||
|
day.getTime() <= last.getTime() && rows.length < 400;
|
||||||
|
day.setDate(day.getDate() + 1)
|
||||||
|
) {
|
||||||
|
balance += byDay.get(PrematureCloseDialogComponent.dayKey(day)) || 0;
|
||||||
|
rows.push({
|
||||||
|
date: new Date(day),
|
||||||
|
balance,
|
||||||
|
interest: (balance * rate) / 36500,
|
||||||
|
credited: !!creditedTo && day.getTime() <= creditedTo.getTime()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The agent's penalty on this account, from the server: its collection, the scheme's penalty rate,
|
||||||
|
* the agent's commission rate and what the account earned them (the cap), and the penalty itself.
|
||||||
|
*/
|
||||||
|
agentPenalty: {
|
||||||
|
collection?: number;
|
||||||
|
penaltyRate?: number;
|
||||||
|
commissionRate?: number | null;
|
||||||
|
commissionEarned?: number | null;
|
||||||
|
penalty?: number;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
private static round2(value: number): number {
|
private static round2(value: number): number {
|
||||||
return Math.round((value + Number.EPSILON) * 100) / 100;
|
return Math.round((value + Number.EPSILON) * 100) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
get balance(): number {
|
/** The balance today, before the interest still owed is credited. */
|
||||||
|
get currentBalance(): number {
|
||||||
return Number(this.data.currentBalance || 0);
|
return Number(this.data.currentBalance || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Interest calculated but not posted - the server refuses to close until it is posted. */
|
/** The balance on the closing day, once the interest owed through yesterday is credited. */
|
||||||
get pendingInterest(): number {
|
get balance(): number {
|
||||||
return Number(this.data.interestCalc || 0);
|
return PrematureCloseDialogComponent.round2(this.currentBalance + this.interestDue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All interest the account will have earned by closing - credited so far plus what is still owed. */
|
||||||
|
get totalInterest(): number {
|
||||||
|
return PrematureCloseDialogComponent.round2((this.interestCredited ?? 0) + this.interestDue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The bank's working day, which the server closes on - not the computer's clock. */
|
||||||
|
get businessDate(): Date {
|
||||||
|
const date = new Date(this.settingsService.businessDate || new Date());
|
||||||
|
date.setHours(0, 0, 0, 0);
|
||||||
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** What the customer paid in: the balance less the interest credited on it. */
|
/** What the customer paid in: the balance less the interest credited on it. */
|
||||||
get deposits(): number {
|
get deposits(): number {
|
||||||
const collected = Number(this.data.collectedToDate || 0);
|
const collected = Number(this.data.collectedToDate || 0);
|
||||||
return collected > 0 ? collected : Math.max(0, this.balance - (this.interestCredited ?? 0));
|
return collected > 0 ? collected : Math.max(0, this.currentBalance - (this.interestCredited ?? 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Customer bears it: credited interest scaled by the share of the rate given up. */
|
/** Customer bears it: credited interest scaled by the share of the rate given up. */
|
||||||
get customerRecovery(): number {
|
get customerRecovery(): number {
|
||||||
const rate = Number(this.data.interestRate || 0);
|
const rate = Number(this.data.interestRate || 0);
|
||||||
const reduction = Number(this.scheme?.prematureInterestReduction || 0);
|
const reduction = Number(this.scheme?.prematureInterestReduction || 0);
|
||||||
const credited = this.interestCredited ?? 0;
|
const credited = this.totalInterest;
|
||||||
if (rate <= 0 || reduction <= 0 || credited <= 0) {
|
if (rate <= 0 || reduction <= 0 || credited <= 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -244,9 +334,9 @@ export class PrematureCloseDialogComponent implements OnInit {
|
|||||||
return PrematureCloseDialogComponent.round2(this.balance - this.customerRecovery);
|
return PrematureCloseDialogComponent.round2(this.balance - this.customerRecovery);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Days from today to the month's end, today counted - the days the agent funds. */
|
/** Days from the closing day to the month's end, the closing day counted - the days the agent funds. */
|
||||||
get agentTopUpDays(): number {
|
get agentTopUpDays(): number {
|
||||||
const today = new Date();
|
const today = this.businessDate;
|
||||||
const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate();
|
const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate();
|
||||||
return lastDay - today.getDate() + 1;
|
return lastDay - today.getDate() + 1;
|
||||||
}
|
}
|
||||||
@@ -264,13 +354,16 @@ export class PrematureCloseDialogComponent implements OnInit {
|
|||||||
return PrematureCloseDialogComponent.round2(this.balance + this.agentTopUp);
|
return PrematureCloseDialogComponent.round2(this.balance + this.agentTopUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The share of the agent's commission taken whichever way it is settled. */
|
/** The agent's penalty on this account, taken whichever way it is settled. */
|
||||||
get commissionPenalty(): number {
|
get commissionPenalty(): number {
|
||||||
const rate = Number(this.scheme?.prematureAgentCommissionPenalty || 0);
|
return Number(this.agentPenalty?.penalty || 0);
|
||||||
if (rate <= 0 || !this.agentCommission) {
|
}
|
||||||
return 0;
|
|
||||||
}
|
/** Whether the penalty was cut down to what the account earned the agent. */
|
||||||
return PrematureCloseDialogComponent.round2((this.agentCommission * rate) / 100);
|
get penaltyCapped(): boolean {
|
||||||
|
const earned = this.agentPenalty?.commissionEarned;
|
||||||
|
const uncapped = (Number(this.agentPenalty?.collection || 0) * Number(this.agentPenalty?.penaltyRate || 0)) / 100;
|
||||||
|
return earned !== null && earned !== undefined && uncapped > Number(earned);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** What the customer receives on the option picked, or null before one is picked. */
|
/** What the customer receives on the option picked, or null before one is picked. */
|
||||||
@@ -294,13 +387,21 @@ export class PrematureCloseDialogComponent implements OnInit {
|
|||||||
|
|
||||||
/** The API sends dates as [year, month, day]; month is 1-based there and 0-based in Date. */
|
/** The API sends dates as [year, month, day]; month is 1-based there and 0-based in Date. */
|
||||||
private static toDate(value: any): Date | null {
|
private static toDate(value: any): Date | null {
|
||||||
const date =
|
if (Array.isArray(value) && value.length >= 3) {
|
||||||
Array.isArray(value) && value.length >= 3
|
return new Date(value[0], value[1] - 1, value[2]);
|
||||||
? new Date(value[0], value[1] - 1, value[2])
|
}
|
||||||
: value
|
// A bare 'yyyy-MM-dd' is a calendar day: read in local time, not as UTC midnight - which in India is
|
||||||
? new Date(value)
|
// 05:30 the same day and would push every day-by-day comparison off by one.
|
||||||
: null;
|
const day = typeof value === 'string' ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(value) : null;
|
||||||
return date && !isNaN(date.getTime()) ? date : null;
|
if (day) {
|
||||||
|
return new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3]));
|
||||||
|
}
|
||||||
|
const date = value ? new Date(value) : null;
|
||||||
|
if (!date || isNaN(date.getTime())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
date.setHours(0, 0, 0, 0);
|
||||||
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly DAY_MS = 24 * 60 * 60 * 1000;
|
private static readonly DAY_MS = 24 * 60 * 60 * 1000;
|
||||||
@@ -320,8 +421,7 @@ export class PrematureCloseDialogComponent implements OnInit {
|
|||||||
if (!opened) {
|
if (!opened) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const today = new Date();
|
const today = this.businessDate;
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
const run = Math.round((today.getTime() - opened.getTime()) / PrematureCloseDialogComponent.DAY_MS) + 1;
|
const run = Math.round((today.getTime() - opened.getTime()) / PrematureCloseDialogComponent.DAY_MS) + 1;
|
||||||
return Math.min(this.termDays, Math.max(0, run));
|
return Math.min(this.termDays, Math.max(0, run));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -490,6 +490,14 @@ export class OrganizationService {
|
|||||||
* @param {string | number} partyId DDS account (party) id.
|
* @param {string | number} partyId DDS account (party) id.
|
||||||
* @returns {Observable<any>} Everything posted to that DDS account, most recent first.
|
* @returns {Observable<any>} Everything posted to that DDS account, most recent first.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Interest a DDS account has earned through the day before the business date but not yet been
|
||||||
|
* credited - what an early closure today credits before it settles.
|
||||||
|
*/
|
||||||
|
getPartyInterestDue(partyId: string | number): Observable<any> {
|
||||||
|
return this.http.get(`/dds/parties/${partyId}/interestdue`);
|
||||||
|
}
|
||||||
|
|
||||||
getPartyTransactionsOfParty(partyId: string | number): Observable<any> {
|
getPartyTransactionsOfParty(partyId: string | number): Observable<any> {
|
||||||
return this.http.get('/dds/partytransactions', {
|
return this.http.get('/dds/partytransactions', {
|
||||||
params: new HttpParams().set('partyId', String(partyId))
|
params: new HttpParams().set('partyId', String(partyId))
|
||||||
|
|||||||
+5
-2
@@ -165,7 +165,7 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="dat-field">
|
<mat-form-field appearance="outline" class="dat-field">
|
||||||
<mat-label>Agent: commission reduced by (%)</mat-label>
|
<mat-label>Agent penalty (% of this account's collection)</mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
required
|
required
|
||||||
@@ -175,7 +175,10 @@
|
|||||||
step="0.25"
|
step="0.25"
|
||||||
formControlName="prematureAgentCommissionPenalty"
|
formControlName="prematureAgentCommissionPenalty"
|
||||||
/>
|
/>
|
||||||
<mat-hint>Charged whoever bears the interest — the round was not kept going.</mat-hint>
|
<mat-hint
|
||||||
|
>Taken off the agent's next commission, whoever bears the interest - never more than the commission the
|
||||||
|
account earned them.</mat-hint
|
||||||
|
>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+5
@@ -285,6 +285,11 @@ $text-soft: #94a3b8;
|
|||||||
background: #fef3c7;
|
background: #fef3c7;
|
||||||
color: #92400e;
|
color: #92400e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--green {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Footer ───────────────────────────────────────────────────────────────────────────────────────────
|
// ── Footer ───────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
+48
-7
@@ -24,8 +24,9 @@ import { SettingsService } from 'app/settings/settings.service';
|
|||||||
import { AccountingService } from 'app/accounting/accounting.service';
|
import { AccountingService } from 'app/accounting/accounting.service';
|
||||||
import { announceHeldForApproval, heldForApproval } from 'app/deposits/dds-approvals/dds-approval-notice';
|
import { announceHeldForApproval, heldForApproval } from 'app/deposits/dds-approvals/dds-approval-notice';
|
||||||
|
|
||||||
/** acc_gl_account classification: 2 liability, 5 expense. */
|
/** acc_gl_account classification: 2 liability, 4 income, 5 expense. */
|
||||||
const GL_LIABILITY = 2;
|
const GL_LIABILITY = 2;
|
||||||
|
const GL_INCOME = 4;
|
||||||
const GL_EXPENSE = 5;
|
const GL_EXPENSE = 5;
|
||||||
|
|
||||||
/** One ledger account the product names, as a row on the Accounting tab. */
|
/** One ledger account the product names, as a row on the Accounting tab. */
|
||||||
@@ -34,7 +35,7 @@ interface AccountingRow {
|
|||||||
label: string;
|
label: string;
|
||||||
inputLabel: string;
|
inputLabel: string;
|
||||||
hint: string;
|
hint: string;
|
||||||
pill: 'blue' | 'amber';
|
pill: 'blue' | 'amber' | 'green';
|
||||||
pillLabel: string;
|
pillLabel: string;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
options: GLAccount[];
|
options: GLAccount[];
|
||||||
@@ -93,7 +94,9 @@ export class CreateDdsAccountTypeComponent implements OnInit {
|
|||||||
readonly accountingControls = [
|
readonly accountingControls = [
|
||||||
'depositLiabilityGlId',
|
'depositLiabilityGlId',
|
||||||
'interestExpenseGlId',
|
'interestExpenseGlId',
|
||||||
'commissionExpenseGlId'
|
'commissionExpenseGlId',
|
||||||
|
'prematureIncomeGlId',
|
||||||
|
'tdsPayableGlId'
|
||||||
];
|
];
|
||||||
|
|
||||||
/** The Accounting tab's rows, grouped by kind as the savings product's are. */
|
/** The Accounting tab's rows, grouped by kind as the savings product's are. */
|
||||||
@@ -196,6 +199,16 @@ export class CreateDdsAccountTypeComponent implements OnInit {
|
|||||||
commissionExpenseGlId: [
|
commissionExpenseGlId: [
|
||||||
null,
|
null,
|
||||||
Validators.required
|
Validators.required
|
||||||
|
],
|
||||||
|
/** What an early closure earns the bank: the customer's interest given back and the agent's penalty. */
|
||||||
|
prematureIncomeGlId: [
|
||||||
|
null,
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
/** Where TDS withheld from the agents' commission (Section 194H) is owed until it is paid over. */
|
||||||
|
tdsPayableGlId: [
|
||||||
|
null,
|
||||||
|
Validators.required
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -226,7 +239,9 @@ export class CreateDdsAccountTypeComponent implements OnInit {
|
|||||||
prematureAgentCommissionPenalty: accountType.prematureAgentCommissionPenalty ?? 1,
|
prematureAgentCommissionPenalty: accountType.prematureAgentCommissionPenalty ?? 1,
|
||||||
depositLiabilityGlId: accountType.depositLiabilityGlId ?? null,
|
depositLiabilityGlId: accountType.depositLiabilityGlId ?? null,
|
||||||
interestExpenseGlId: accountType.interestExpenseGlId ?? null,
|
interestExpenseGlId: accountType.interestExpenseGlId ?? null,
|
||||||
commissionExpenseGlId: accountType.commissionExpenseGlId ?? null
|
commissionExpenseGlId: accountType.commissionExpenseGlId ?? null,
|
||||||
|
prematureIncomeGlId: accountType.prematureIncomeGlId ?? null,
|
||||||
|
tdsPayableGlId: accountType.tdsPayableGlId ?? null
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -258,16 +273,16 @@ export class CreateDdsAccountTypeComponent implements OnInit {
|
|||||||
(a: any) => a.usage?.id === 1 && !a.disabled
|
(a: any) => a.usage?.id === 1 && !a.disabled
|
||||||
);
|
);
|
||||||
const ofType = (...types: number[]) => rows.filter((a: any) => types.includes(a.type?.id));
|
const ofType = (...types: number[]) => rows.filter((a: any) => types.includes(a.type?.id));
|
||||||
this.buildAccountingSections(ofType(GL_LIABILITY), ofType(GL_EXPENSE));
|
this.buildAccountingSections(ofType(GL_LIABILITY), ofType(GL_EXPENSE), ofType(GL_INCOME));
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.glLoadFailed = true;
|
this.glLoadFailed = true;
|
||||||
this.buildAccountingSections([], []);
|
this.buildAccountingSections([], [], []);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildAccountingSections(liabilities: GLAccount[], expenses: GLAccount[]): void {
|
private buildAccountingSections(liabilities: GLAccount[], expenses: GLAccount[], incomes: GLAccount[]): void {
|
||||||
this.accountingSections = [
|
this.accountingSections = [
|
||||||
{
|
{
|
||||||
key: 'liabilities',
|
key: 'liabilities',
|
||||||
@@ -282,6 +297,16 @@ export class CreateDdsAccountTypeComponent implements OnInit {
|
|||||||
pillLabel: 'Liability',
|
pillLabel: 'Liability',
|
||||||
required: true,
|
required: true,
|
||||||
options: liabilities
|
options: liabilities
|
||||||
|
},
|
||||||
|
{
|
||||||
|
controlName: 'tdsPayableGlId',
|
||||||
|
label: 'TDS Payable (194H)',
|
||||||
|
inputLabel: 'TDS payable account',
|
||||||
|
hint: "TDS withheld from the agents' commission on this product - credited when the commission is paid, until it is paid over to the tax department.",
|
||||||
|
pill: 'blue',
|
||||||
|
pillLabel: 'Liability',
|
||||||
|
required: true,
|
||||||
|
options: liabilities
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -310,6 +335,22 @@ export class CreateDdsAccountTypeComponent implements OnInit {
|
|||||||
options: expenses
|
options: expenses
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'income',
|
||||||
|
title: 'Income',
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
controlName: 'prematureIncomeGlId',
|
||||||
|
label: 'Premature Closure Income',
|
||||||
|
inputLabel: 'Premature closure income account',
|
||||||
|
hint: 'What closing early earns the bank: the interest a customer gives back (Premature Closure tab), on the day of closing. The agent penalty is not booked here - it comes off their commission, which reverses that much commission expense.',
|
||||||
|
pill: 'green',
|
||||||
|
pillLabel: 'Income',
|
||||||
|
required: true,
|
||||||
|
options: incomes
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user