form 91/98 report, min max value update in AML rule and whihc rule apply day, week, month and some more updates
This commit is contained in:
@@ -0,0 +1,638 @@
|
||||
/**
|
||||
* 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 { GL_BRANCHES, GL_PERIOD } from '../gl-reports/gl-reports-data';
|
||||
|
||||
/**
|
||||
* Transactions of customers who banked on a Form 97 / Form 98 declaration.
|
||||
*
|
||||
* Rule 114B of the Income-tax Rules lists transactions where quoting PAN is
|
||||
* mandatory. A customer who has no PAN gives a declaration instead:
|
||||
*
|
||||
* Form 97 — person with no PAN (income may be taxable).
|
||||
* Form 98 — person whose only income is agricultural and who has no PAN.
|
||||
*
|
||||
* The bank must be able to produce, for any period, every transaction put
|
||||
* through on the strength of such a declaration. That is what this report is.
|
||||
*/
|
||||
|
||||
export type FormType = 'FORM_60' | 'FORM_61';
|
||||
|
||||
/** Screen and workbook labels — this bank numbers the declarations 97 and 98. */
|
||||
export const FORM_LABEL: Record<FormType, string> = {
|
||||
FORM_60: 'Form 97',
|
||||
FORM_61: 'Form 98'
|
||||
};
|
||||
|
||||
export interface FormDeclarant {
|
||||
/** Declaration acknowledgement number, as issued by the branch. */
|
||||
ackNo: string;
|
||||
formType: FormType;
|
||||
name: string;
|
||||
fatherName: string;
|
||||
dateOfBirth: string;
|
||||
address: string;
|
||||
branchCode: string;
|
||||
accountNo: string;
|
||||
/** Why the customer has no PAN — printed on the declaration. */
|
||||
reasonForNoPan: string;
|
||||
estimatedIncome: number;
|
||||
declaredOn: string;
|
||||
aadhaarLast4: string;
|
||||
}
|
||||
|
||||
export type TxnMode = 'CASH' | 'TRANSFER' | 'CLEARING' | 'NEFT' | 'RTGS' | 'UPI' | 'ATM';
|
||||
|
||||
export interface FormDeclarantTxn {
|
||||
txnId: string;
|
||||
ackNo: string;
|
||||
date: string;
|
||||
branchCode: string;
|
||||
accountNo: string;
|
||||
mode: TxnMode;
|
||||
drCr: 'DR' | 'CR';
|
||||
amount: number;
|
||||
/** What the transaction was — a Rule 114B category or ordinary activity. */
|
||||
nature: string;
|
||||
/** Coarse bucket the nature belongs to, e.g. Cash, Clearing, Loan. */
|
||||
group: string;
|
||||
/** True when the nature is one of the categories listed in Rule 114B. */
|
||||
rule114B: boolean;
|
||||
particulars: string;
|
||||
voucherNo: string;
|
||||
}
|
||||
|
||||
/** Deterministic linear congruential generator — same figures on every load. */
|
||||
function lcg(seed: number): () => number {
|
||||
let s = seed >>> 0;
|
||||
return () => {
|
||||
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
|
||||
return s / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
function pick<T>(rand: () => number, list: T[]): T {
|
||||
return list[Math.floor(rand() * list.length)];
|
||||
}
|
||||
|
||||
function isoDate(d: Date): string {
|
||||
const y = d.getUTCFullYear();
|
||||
const m = `${d.getUTCMonth() + 1}`.padStart(2, '0');
|
||||
const day = `${d.getUTCDate()}`.padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thresholds that make a row worth a second look.
|
||||
*
|
||||
* `RULE_114B_CASH` is the cash-deposit limit above which a declaration is
|
||||
* required at all; `CTR` is the aggregate cash figure that puts the customer
|
||||
* into the monthly Cash Transaction Report.
|
||||
*/
|
||||
export const FORM_THRESHOLD = {
|
||||
RULE_114B_CASH: 50_000,
|
||||
CTR: 1_000_000
|
||||
};
|
||||
|
||||
const FIRST_NAMES = [
|
||||
'Ramesh',
|
||||
'Sunita',
|
||||
'Vitthal',
|
||||
'Kamala',
|
||||
'Ganpat',
|
||||
'Shobha',
|
||||
'Dnyaneshwar',
|
||||
'Mangala',
|
||||
'Bhaskar',
|
||||
'Anita',
|
||||
'Tukaram',
|
||||
'Sarika',
|
||||
'Nivrutti',
|
||||
'Rekha',
|
||||
'Popat',
|
||||
'Chhaya',
|
||||
'Ashok',
|
||||
'Vaishali',
|
||||
'Bapu',
|
||||
'Nanda',
|
||||
'Sitaram',
|
||||
'Jayshree',
|
||||
'Madhukar',
|
||||
'Pushpa',
|
||||
'Eknath',
|
||||
'Sulochana',
|
||||
'Baban',
|
||||
'Indubai',
|
||||
'Hanumant',
|
||||
'Kalpana'
|
||||
];
|
||||
|
||||
const SURNAMES = [
|
||||
'Jadhav',
|
||||
'Pawar',
|
||||
'Shinde',
|
||||
'Gaikwad',
|
||||
'More',
|
||||
'Kadam',
|
||||
'Bhosale',
|
||||
'Patil',
|
||||
'Sawant',
|
||||
'Chavan',
|
||||
'Salunkhe',
|
||||
'Mane',
|
||||
'Deshmukh',
|
||||
'Thorat',
|
||||
'Nikam',
|
||||
'Wagh',
|
||||
'Kale',
|
||||
'Ghadge',
|
||||
'Bhoir',
|
||||
'Tandel'
|
||||
];
|
||||
|
||||
const VILLAGES = [
|
||||
'Wadgaon',
|
||||
'Kolhewadi',
|
||||
'Shirgaon',
|
||||
'Nandgaon',
|
||||
'Pimpalgaon',
|
||||
'Bhosari',
|
||||
'Rajgurunagar',
|
||||
'Talegaon',
|
||||
'Karjat',
|
||||
'Murbad',
|
||||
'Shahapur',
|
||||
'Vasind'
|
||||
];
|
||||
|
||||
/** Reasons a Form 97 declarant gives — Form 98 has its own single reason. */
|
||||
const NO_PAN_REASONS = [
|
||||
'Applied for PAN, allotment awaited',
|
||||
'Total income below taxable limit',
|
||||
'Not required to file return of income',
|
||||
'First-time account holder, PAN not yet obtained'
|
||||
];
|
||||
|
||||
/**
|
||||
* Every kind of transaction one of these customers puts through — not only the
|
||||
* ones that triggered a declaration.
|
||||
*
|
||||
* `rule114B` marks the categories listed in Rule 114B, where quoting PAN (and
|
||||
* therefore the Form 97/98 declaration) is mandatory above the limit. The rest
|
||||
* are ordinary account activity, which the report still has to show: the point
|
||||
* is the customer's whole picture, not just the declared slice.
|
||||
*
|
||||
* `drCr` is fixed where the category only goes one way; 'EITHER' lets the
|
||||
* generator choose. `cashOnly` forces counter cash, `nonCash` excludes it.
|
||||
*/
|
||||
interface TxnKind {
|
||||
nature: string;
|
||||
group: string;
|
||||
rule114B: boolean;
|
||||
drCr: 'DR' | 'CR' | 'EITHER';
|
||||
cashOnly?: boolean;
|
||||
nonCash?: boolean;
|
||||
/** Relative frequency — small routine postings outnumber big declared ones. */
|
||||
weight: number;
|
||||
/** Typical value band, before the occasional large-value tail. */
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
const TXN_KINDS: TxnKind[] = [
|
||||
// ── Rule 114B categories: the transactions the declaration was taken for ──
|
||||
{
|
||||
nature: 'Cash deposit exceeding ₹50,000 in a day',
|
||||
group: 'Cash',
|
||||
rule114B: true,
|
||||
drCr: 'CR',
|
||||
cashOnly: true,
|
||||
weight: 14,
|
||||
min: 50_000,
|
||||
max: 470_000
|
||||
},
|
||||
{
|
||||
nature: 'Cash withdrawal exceeding ₹50,000',
|
||||
group: 'Cash',
|
||||
rule114B: true,
|
||||
drCr: 'DR',
|
||||
cashOnly: true,
|
||||
weight: 8,
|
||||
min: 50_000,
|
||||
max: 380_000
|
||||
},
|
||||
{
|
||||
nature: 'Opening of deposit account',
|
||||
group: 'Account',
|
||||
rule114B: true,
|
||||
drCr: 'CR',
|
||||
cashOnly: true,
|
||||
weight: 3,
|
||||
min: 5_000,
|
||||
max: 50_000
|
||||
},
|
||||
{
|
||||
nature: 'Term deposit exceeding ₹50,000',
|
||||
group: 'Deposit',
|
||||
rule114B: true,
|
||||
drCr: 'DR',
|
||||
weight: 5,
|
||||
min: 50_000,
|
||||
max: 900_000
|
||||
},
|
||||
{
|
||||
nature: 'Purchase of demand draft in cash',
|
||||
group: 'Remittance',
|
||||
rule114B: true,
|
||||
drCr: 'DR',
|
||||
cashOnly: true,
|
||||
weight: 4,
|
||||
min: 50_000,
|
||||
max: 260_000
|
||||
},
|
||||
{
|
||||
nature: 'Sale/purchase of goods or services above ₹2,00,000',
|
||||
group: 'Trade',
|
||||
rule114B: true,
|
||||
drCr: 'EITHER',
|
||||
weight: 3,
|
||||
min: 200_000,
|
||||
max: 850_000
|
||||
},
|
||||
|
||||
// ── Ordinary account activity ────────────────────────────────────────────
|
||||
{
|
||||
nature: 'Cash deposit',
|
||||
group: 'Cash',
|
||||
rule114B: false,
|
||||
drCr: 'CR',
|
||||
cashOnly: true,
|
||||
weight: 16,
|
||||
min: 500,
|
||||
max: 49_900
|
||||
},
|
||||
{
|
||||
nature: 'Cash withdrawal',
|
||||
group: 'Cash',
|
||||
rule114B: false,
|
||||
drCr: 'DR',
|
||||
cashOnly: true,
|
||||
weight: 15,
|
||||
min: 500,
|
||||
max: 49_900
|
||||
},
|
||||
{
|
||||
nature: 'Cheque deposit — outward clearing',
|
||||
group: 'Clearing',
|
||||
rule114B: false,
|
||||
drCr: 'CR',
|
||||
nonCash: true,
|
||||
weight: 7,
|
||||
min: 2_000,
|
||||
max: 180_000
|
||||
},
|
||||
{
|
||||
nature: 'Cheque paid — inward clearing',
|
||||
group: 'Clearing',
|
||||
rule114B: false,
|
||||
drCr: 'DR',
|
||||
nonCash: true,
|
||||
weight: 6,
|
||||
min: 2_000,
|
||||
max: 160_000
|
||||
},
|
||||
{
|
||||
nature: 'NEFT inward credit',
|
||||
group: 'Remittance',
|
||||
rule114B: false,
|
||||
drCr: 'CR',
|
||||
nonCash: true,
|
||||
weight: 6,
|
||||
min: 1_000,
|
||||
max: 220_000
|
||||
},
|
||||
{
|
||||
nature: 'NEFT outward remittance',
|
||||
group: 'Remittance',
|
||||
rule114B: false,
|
||||
drCr: 'DR',
|
||||
nonCash: true,
|
||||
weight: 5,
|
||||
min: 1_000,
|
||||
max: 200_000
|
||||
},
|
||||
{
|
||||
nature: 'RTGS inward credit',
|
||||
group: 'Remittance',
|
||||
rule114B: false,
|
||||
drCr: 'CR',
|
||||
nonCash: true,
|
||||
weight: 2,
|
||||
min: 200_000,
|
||||
max: 1_200_000
|
||||
},
|
||||
{
|
||||
nature: 'UPI / mobile banking transfer',
|
||||
group: 'Digital',
|
||||
rule114B: false,
|
||||
drCr: 'EITHER',
|
||||
nonCash: true,
|
||||
weight: 10,
|
||||
min: 100,
|
||||
max: 24_000
|
||||
},
|
||||
{
|
||||
nature: 'ATM cash withdrawal',
|
||||
group: 'Digital',
|
||||
rule114B: false,
|
||||
drCr: 'DR',
|
||||
cashOnly: true,
|
||||
weight: 9,
|
||||
min: 500,
|
||||
max: 20_000
|
||||
},
|
||||
{
|
||||
nature: 'Transfer between own accounts',
|
||||
group: 'Transfer',
|
||||
rule114B: false,
|
||||
drCr: 'EITHER',
|
||||
nonCash: true,
|
||||
weight: 5,
|
||||
min: 1_000,
|
||||
max: 120_000
|
||||
},
|
||||
{
|
||||
nature: 'Loan instalment (EMI) recovery',
|
||||
group: 'Loan',
|
||||
rule114B: false,
|
||||
drCr: 'DR',
|
||||
weight: 7,
|
||||
min: 2_500,
|
||||
max: 42_000
|
||||
},
|
||||
{
|
||||
nature: 'Loan disbursement credited',
|
||||
group: 'Loan',
|
||||
rule114B: false,
|
||||
drCr: 'CR',
|
||||
nonCash: true,
|
||||
weight: 2,
|
||||
min: 50_000,
|
||||
max: 700_000
|
||||
},
|
||||
{
|
||||
nature: 'Interest credited on deposit',
|
||||
group: 'Interest',
|
||||
rule114B: false,
|
||||
drCr: 'CR',
|
||||
nonCash: true,
|
||||
weight: 6,
|
||||
min: 120,
|
||||
max: 14_000
|
||||
},
|
||||
{
|
||||
nature: 'Interest debited on loan',
|
||||
group: 'Interest',
|
||||
rule114B: false,
|
||||
drCr: 'DR',
|
||||
nonCash: true,
|
||||
weight: 4,
|
||||
min: 200,
|
||||
max: 11_000
|
||||
},
|
||||
{
|
||||
nature: 'Term deposit matured and credited',
|
||||
group: 'Deposit',
|
||||
rule114B: false,
|
||||
drCr: 'CR',
|
||||
nonCash: true,
|
||||
weight: 3,
|
||||
min: 25_000,
|
||||
max: 620_000
|
||||
},
|
||||
{
|
||||
nature: 'Recurring deposit instalment',
|
||||
group: 'Deposit',
|
||||
rule114B: false,
|
||||
drCr: 'DR',
|
||||
weight: 5,
|
||||
min: 500,
|
||||
max: 12_000
|
||||
},
|
||||
{
|
||||
nature: 'Service charges / commission recovered',
|
||||
group: 'Charges',
|
||||
rule114B: false,
|
||||
drCr: 'DR',
|
||||
nonCash: true,
|
||||
weight: 6,
|
||||
min: 25,
|
||||
max: 2_400
|
||||
},
|
||||
{
|
||||
nature: 'Cheque return charges',
|
||||
group: 'Charges',
|
||||
rule114B: false,
|
||||
drCr: 'DR',
|
||||
nonCash: true,
|
||||
weight: 2,
|
||||
min: 100,
|
||||
max: 900
|
||||
},
|
||||
{
|
||||
nature: 'Locker rent recovered',
|
||||
group: 'Charges',
|
||||
rule114B: false,
|
||||
drCr: 'DR',
|
||||
nonCash: true,
|
||||
weight: 1,
|
||||
min: 800,
|
||||
max: 4_500
|
||||
},
|
||||
{
|
||||
nature: 'Dividend credited on shares',
|
||||
group: 'Shares',
|
||||
rule114B: false,
|
||||
drCr: 'CR',
|
||||
nonCash: true,
|
||||
weight: 2,
|
||||
min: 200,
|
||||
max: 9_000
|
||||
},
|
||||
{
|
||||
nature: 'Share capital subscription',
|
||||
group: 'Shares',
|
||||
rule114B: false,
|
||||
drCr: 'DR',
|
||||
weight: 2,
|
||||
min: 500,
|
||||
max: 25_000
|
||||
}
|
||||
];
|
||||
|
||||
/** Flattened by weight so a single draw picks a category at the right rate. */
|
||||
const KIND_POOL: TxnKind[] = TXN_KINDS.flatMap((k) => Array<TxnKind>(k.weight).fill(k));
|
||||
|
||||
/** The distinct groups, for the report's category filter. */
|
||||
export const TXN_GROUPS: string[] = [...new Set(TXN_KINDS.map((k) => k.group))].sort();
|
||||
|
||||
let declarantCache: FormDeclarant[] | null = null;
|
||||
let txnCache: FormDeclarantTxn[] | null = null;
|
||||
|
||||
/**
|
||||
* The customers who filed a declaration. Roughly one in five is a Form 98
|
||||
* (agricultural income only), which matches the mix a semi-urban co-operative
|
||||
* bank sees — the rural branches carry most of them.
|
||||
*/
|
||||
export function formDeclarants(): FormDeclarant[] {
|
||||
if (declarantCache) {
|
||||
return declarantCache;
|
||||
}
|
||||
const rand = lcg(31071998);
|
||||
const branches = GL_BRANCHES.filter((b) => b.code !== 'HO');
|
||||
const out: FormDeclarant[] = [];
|
||||
|
||||
for (let i = 0; i < 148; i++) {
|
||||
const branch = branches[i % branches.length];
|
||||
// Form 98 is the agricultural-income declaration, so it clusters in the
|
||||
// outlying branches rather than being spread evenly.
|
||||
const rural = branch.code === '0004' || branch.code === '0006';
|
||||
const formType: FormType = rand() < (rural ? 0.42 : 0.08) ? 'FORM_61' : 'FORM_60';
|
||||
|
||||
const first = pick(rand, FIRST_NAMES);
|
||||
const last = pick(rand, SURNAMES);
|
||||
const seq = `${i + 1}`.padStart(3, '0');
|
||||
|
||||
// Declarations are taken in the run-up to and during the ledger period.
|
||||
const declOffset = Math.floor(rand() * 150);
|
||||
const declDate = new Date(`${GL_PERIOD.fyStart}T00:00:00Z`);
|
||||
declDate.setUTCDate(declDate.getUTCDate() - 60 + declOffset);
|
||||
|
||||
const birthYear = 1955 + Math.floor(rand() * 45);
|
||||
const birthMonth = 1 + Math.floor(rand() * 12);
|
||||
const birthDay = 1 + Math.floor(rand() * 28);
|
||||
|
||||
out.push({
|
||||
ackNo: `${branch.code}/F${formType === 'FORM_60' ? '97' : '98'}/${seq}`,
|
||||
formType,
|
||||
name: `${first} ${last}`,
|
||||
fatherName: `${pick(rand, FIRST_NAMES)} ${last}`,
|
||||
dateOfBirth: `${birthYear}-${`${birthMonth}`.padStart(2, '0')}-${`${birthDay}`.padStart(2, '0')}`,
|
||||
address: `${pick(rand, VILLAGES)}, Tal. ${pick(rand, VILLAGES)}, Dist. Thane`,
|
||||
branchCode: branch.code,
|
||||
accountNo: `${branch.code}${(4100000 + i * 137).toString().padStart(8, '0')}`,
|
||||
reasonForNoPan:
|
||||
formType === 'FORM_61' ? 'Income consists only of agricultural income' : pick(rand, NO_PAN_REASONS),
|
||||
// Form 98 declarants report agricultural income, which the Act exempts, so
|
||||
// their declared figures sit below the Form 97 band.
|
||||
estimatedIncome:
|
||||
formType === 'FORM_61' ? 90_000 + Math.floor(rand() * 160_000) : 140_000 + Math.floor(rand() * 380_000),
|
||||
declaredOn: isoDate(declDate),
|
||||
aadhaarLast4: `${1000 + Math.floor(rand() * 9000)}`
|
||||
});
|
||||
}
|
||||
|
||||
declarantCache = out;
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every transaction these customers put through inside the ledger period.
|
||||
*
|
||||
* A declaration is taken per transaction category, so an active customer shows
|
||||
* several rows; a customer who only opened an account shows one.
|
||||
*/
|
||||
export function formDeclarantTxns(): FormDeclarantTxn[] {
|
||||
if (txnCache) {
|
||||
return txnCache;
|
||||
}
|
||||
const rand = lcg(19980731);
|
||||
const start = new Date(`${GL_PERIOD.fyStart}T00:00:00Z`);
|
||||
const end = new Date(`${GL_PERIOD.fyEnd}T00:00:00Z`);
|
||||
const span = Math.round((end.getTime() - start.getTime()) / 86_400_000);
|
||||
|
||||
const out: FormDeclarantTxn[] = [];
|
||||
let voucher = 1;
|
||||
|
||||
for (const d of formDeclarants()) {
|
||||
// Weighted low — most declarants are occasional users and a handful bank
|
||||
// every week. Squaring the draw gives that long tail rather than a flat
|
||||
// spread, while still leaving enough volume for a day-wise filter to show.
|
||||
const count = 1 + Math.floor(rand() * rand() * 22);
|
||||
|
||||
for (let n = 0; n < count; n++) {
|
||||
const day = new Date(start.getTime());
|
||||
day.setUTCDate(day.getUTCDate() + Math.floor(rand() * span));
|
||||
if (day.getUTCDay() === 0) {
|
||||
day.setUTCDate(day.getUTCDate() + 1); // Sunday — no counter business.
|
||||
}
|
||||
|
||||
const kind = pick(rand, KIND_POOL);
|
||||
const drCr: 'DR' | 'CR' = kind.drCr === 'EITHER' ? (rand() < 0.5 ? 'DR' : 'CR') : kind.drCr;
|
||||
|
||||
// The channel follows the category: cash categories go over the counter,
|
||||
// clearing goes through the house, and the rest split across the
|
||||
// electronic rails a co-operative bank offers.
|
||||
let mode: TxnMode;
|
||||
if (kind.cashOnly) {
|
||||
mode = kind.nature.startsWith('ATM') ? 'ATM' : 'CASH';
|
||||
} else if (kind.nonCash) {
|
||||
mode =
|
||||
kind.group === 'Clearing'
|
||||
? 'CLEARING'
|
||||
: kind.nature.startsWith('RTGS')
|
||||
? 'RTGS'
|
||||
: kind.nature.startsWith('NEFT')
|
||||
? 'NEFT'
|
||||
: kind.group === 'Digital'
|
||||
? 'UPI'
|
||||
: 'TRANSFER';
|
||||
} else {
|
||||
// Categories that can go either way — a term deposit or an EMI is paid
|
||||
// in cash as often as it is transferred at this kind of bank.
|
||||
mode = rand() < 0.55 ? 'CASH' : 'TRANSFER';
|
||||
}
|
||||
|
||||
// Inside the category's own band, with a thin large-value tail on the
|
||||
// Rule 114B rows so the report has CTR-sized entries to flag.
|
||||
let amount = kind.min + Math.floor(rand() * (kind.max - kind.min));
|
||||
if (kind.rule114B && kind.cashOnly && rand() < 0.09) {
|
||||
amount = FORM_THRESHOLD.CTR + Math.floor(rand() * 1_400_000);
|
||||
}
|
||||
amount = Math.round(amount / 100) * 100 || 100;
|
||||
|
||||
const cashWord = mode === 'CASH' || mode === 'ATM';
|
||||
out.push({
|
||||
txnId: `TXN${`${out.length + 1}`.padStart(5, '0')}`,
|
||||
ackNo: d.ackNo,
|
||||
date: isoDate(day),
|
||||
branchCode: d.branchCode,
|
||||
accountNo: d.accountNo,
|
||||
mode,
|
||||
drCr,
|
||||
amount,
|
||||
nature: kind.nature,
|
||||
group: kind.group,
|
||||
rule114B: kind.rule114B,
|
||||
particulars:
|
||||
drCr === 'DR'
|
||||
? `${cashWord ? 'Cash paid to' : 'Debit to account of'} ${d.name} — ${kind.nature.toLowerCase()}`
|
||||
: `${cashWord ? 'Cash received from' : 'Credit to account of'} ${d.name} — ${kind.nature.toLowerCase()}`,
|
||||
voucherNo: `${d.branchCode}/V/${`${voucher++}`.padStart(5, '0')}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
out.sort((a, b) => (a.date === b.date ? a.txnId.localeCompare(b.txnId) : a.date.localeCompare(b.date)));
|
||||
txnCache = out;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function declarantByAck(ackNo: string): FormDeclarant | undefined {
|
||||
return formDeclarants().find((d) => d.ackNo === ackNo);
|
||||
}
|
||||
Reference in New Issue
Block a user