Update CBS Mobile App codebase
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
export interface ApiError {
|
||||
status: number;
|
||||
message: string;
|
||||
/**
|
||||
* The backend's `errorCode`, or a client-synthesised one when the request
|
||||
* never got an answer. Stable across wording changes — branch on this.
|
||||
*/
|
||||
code?: string;
|
||||
/** Field-to-message map that `VALIDATION_ERROR` returns. */
|
||||
fieldErrors?: Record<string, string>;
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
|
||||
+43
-5
@@ -29,11 +29,27 @@ export type CardDeliveryStage =
|
||||
|
||||
export type LoginRole = 'User' | 'Agent';
|
||||
|
||||
export interface AuthTokens {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
/**
|
||||
* What the app holds after a successful sign-in. There is no refresh token in
|
||||
* the CBS API — a session is opened by MPIN and ends at whichever of its two
|
||||
* timeouts bites first.
|
||||
*/
|
||||
export interface AuthSession {
|
||||
/** Sent as `Authorization: Bearer <token>` on every authenticated call. */
|
||||
sessionToken: string;
|
||||
/** Absolute expiry from the server. Never extended. */
|
||||
expiresAt: string;
|
||||
role?: LoginRole;
|
||||
/** Idle window in seconds; any authenticated call resets it. */
|
||||
idleTimeoutSeconds: number;
|
||||
orgId: string;
|
||||
mobileNumber: string;
|
||||
fullName: string;
|
||||
role: LoginRole;
|
||||
/**
|
||||
* False for the agent app, which has no CBS backend behind it yet and runs
|
||||
* on fixtures. Keeps mock sessions from being treated as live ones.
|
||||
*/
|
||||
live: boolean;
|
||||
}
|
||||
|
||||
export interface NotificationPreference {
|
||||
@@ -50,6 +66,8 @@ export interface UserProfile {
|
||||
phone: string;
|
||||
customerId: string;
|
||||
kycStatus: 'verified' | 'pending';
|
||||
/** ISO date of the last completed KYC / re-KYC. */
|
||||
lastKycDate?: string;
|
||||
ckycNumber?: string;
|
||||
language: LanguageCode;
|
||||
avatarUrl?: string;
|
||||
@@ -79,6 +97,10 @@ export interface AccountSummary {
|
||||
type: AccountType;
|
||||
accountHolder: string;
|
||||
branch: string;
|
||||
/** IFSC code of the home branch. */
|
||||
ifscCode?: string;
|
||||
/** Operating status of the account. */
|
||||
status?: 'Active' | 'Dormant' | 'Frozen';
|
||||
maskedNumber: string;
|
||||
availableBalance: number;
|
||||
currentBalance: number;
|
||||
@@ -114,12 +136,24 @@ export interface TransactionRecord {
|
||||
|
||||
export interface Beneficiary {
|
||||
id: string;
|
||||
/** The bank's answer, never what the customer typed. */
|
||||
name: string;
|
||||
bankName: string;
|
||||
accountMasked: string;
|
||||
upiId?: string;
|
||||
/** The one field that is the customer's own. */
|
||||
nickname?: string;
|
||||
/**
|
||||
* Held on this device only. The API has no favourite flag, so starring a
|
||||
* payee is a local preference rather than something the bank knows about.
|
||||
*/
|
||||
favorite: boolean;
|
||||
/** P2A routes by account number and IFSC; P2P by mobile number and MMID. */
|
||||
mode?: 'P2A' | 'P2P';
|
||||
ifsc?: string | null;
|
||||
mobileNumber?: string | null;
|
||||
mmid?: string | null;
|
||||
status?: string | null;
|
||||
}
|
||||
|
||||
export interface TransferDraft {
|
||||
@@ -192,6 +226,10 @@ export interface DepositAccount {
|
||||
interestEarned: number;
|
||||
payoutLabel: string;
|
||||
interestPayoutType?: DepositInterestPayoutType;
|
||||
/** The bank's name for the product, when it supplies one. */
|
||||
productName?: string;
|
||||
/** Recurring deposits only: installments credited so far, of `tenureMonths`. */
|
||||
installmentsPaid?: number;
|
||||
status: DepositStatus;
|
||||
settlementAccountMasked: string;
|
||||
nominee?: DepositNominee;
|
||||
@@ -229,7 +267,7 @@ export interface LoanProduct {
|
||||
features: string[];
|
||||
}
|
||||
|
||||
export type LoanCategory = 'Personal Loan' | 'Home Loan' | 'Car Loan' | 'Gold Loan' | 'Education Loan';
|
||||
export type LoanCategory = 'Personal Loan' | 'Home Loan' | 'Car Loan' | 'Gold Loan' | 'Education Loan' | 'Loan Against DDS Deposit';
|
||||
export type LoanStatus = 'Active' | 'Closed' | 'Overdue';
|
||||
export type LoanApplicationStage = 'Submitted' | 'Under Review' | 'Approved' | 'Rejected' | 'Disbursed';
|
||||
export type LoanDocumentType = 'Aadhaar' | 'PAN' | 'Salary Slip' | 'Bank Statement' | 'Address Proof' | 'Other';
|
||||
|
||||
@@ -0,0 +1,594 @@
|
||||
/**
|
||||
* Wire types for the CBS mobile banking API. These mirror the documented
|
||||
* request and response bodies exactly — mapping into the app's own shapes
|
||||
* happens at the module boundary, never here.
|
||||
*/
|
||||
|
||||
/** Success and failure share one wrapper, so the client parses a single shape. */
|
||||
export interface CbsEnvelope<T> {
|
||||
success: boolean;
|
||||
message: string;
|
||||
/**
|
||||
* Present on success. `VALIDATION_ERROR` is the one failure that also
|
||||
* carries it — as a field-to-message map rather than a payload.
|
||||
*/
|
||||
data?: T;
|
||||
/** Only on failure. This is what the app switches on. */
|
||||
errorCode?: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
/* ── Registration ─────────────────────────────────────────────────────────── */
|
||||
|
||||
export interface StartRegistrationRequest {
|
||||
orgId: string;
|
||||
mobileNumber: string;
|
||||
}
|
||||
|
||||
export interface StartRegistrationResult {
|
||||
otpStatus: string;
|
||||
customerExists: boolean;
|
||||
accountsLinked: number;
|
||||
}
|
||||
|
||||
export interface VerifyOtpRequest {
|
||||
orgId: string;
|
||||
mobileNumber: string;
|
||||
otp: string;
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
export interface VerifyOtpResult {
|
||||
verified: boolean;
|
||||
mpinSetupRequired: boolean;
|
||||
mmidsAssigned: number;
|
||||
/** Single use, valid five minutes. Authorises MPIN creation. */
|
||||
setupToken: string;
|
||||
}
|
||||
|
||||
/* ── Statement ────────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Query for `GET /accounts/statement`.
|
||||
*
|
||||
* `fromDate` and `toDate` go together or not at all — the bank answers
|
||||
* `INVALID_DATE_RANGE` for exactly one of the two. With neither, it applies
|
||||
* its own recent-activity window, whose span it does not publish.
|
||||
*/
|
||||
export interface CbsStatementQuery {
|
||||
/** The customer's own account, unmasked, as `GET /accounts` returned it. */
|
||||
accountNumber: string;
|
||||
/** `yyyy-MM-dd`. Only ever sent alongside `toDate`. */
|
||||
fromDate?: string;
|
||||
/** `yyyy-MM-dd`. Only ever sent alongside `fromDate`. */
|
||||
toDate?: string;
|
||||
/** Zero-based. Defaults to 0 server-side. */
|
||||
page?: number;
|
||||
/** Defaults to 10 server-side and is capped there at 100. */
|
||||
size?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* One ledger entry.
|
||||
*
|
||||
* Note what is still *not* here, because the screen rendering these used to
|
||||
* show both from local fixtures: no settlement status (nothing is "pending")
|
||||
* and no spending category.
|
||||
*
|
||||
* The counterparty *is* here now — the bank added the five `counterparty*`
|
||||
* fields alongside a `transactionDate` that carries a time. Both are
|
||||
* best-effort and all five can be null together, so nothing below may be read
|
||||
* without a null check. See `hasCounterparty` underneath.
|
||||
*/
|
||||
export interface CbsStatementTransaction {
|
||||
/**
|
||||
* `yyyy-MM-ddTHH:mm:ss`, local — a date *and* a time.
|
||||
*
|
||||
* This used to be a bare `yyyy-MM-dd`, which is why anything grouping or
|
||||
* comparing these by string equality has to take the date part off first:
|
||||
* whole-value equality now buckets every entry on its own. `statementDay`
|
||||
* in `utils/statement.ts` is the one place that split lives.
|
||||
*/
|
||||
transactionDate: string;
|
||||
/** The bank's own description of the entry. */
|
||||
narration: string;
|
||||
/** Unique per entry, and the only stable key these have. */
|
||||
referenceNumber: string;
|
||||
/**
|
||||
* NEFT, IMPS, UPI, ATM_WDL, POS, CASH_DEPOSIT, CLEARING_INWARD,
|
||||
* CLEARING_OUTWARD and others — deliberately typed as `string`, because the
|
||||
* bank does not treat this as a closed set and a union here would turn a new
|
||||
* channel into a client bug.
|
||||
*/
|
||||
transactionType: string;
|
||||
/** Direction. The amount itself is always positive. */
|
||||
drCr: 'DR' | 'CR';
|
||||
/** Always positive; `drCr` carries the sign. */
|
||||
amount: number;
|
||||
/** The account's balance immediately after this entry. */
|
||||
balanceAfter: number;
|
||||
/**
|
||||
* Who the money moved to or from.
|
||||
*
|
||||
* All four are null together for `ATM_WDL`, `POS` and `CASH_DEPOSIT` — those
|
||||
* have no other party's account on the far end, so an absent counterparty is
|
||||
* the correct answer for them rather than missing data.
|
||||
*/
|
||||
counterpartyName: string | null;
|
||||
counterpartyAccountNumber: string | null;
|
||||
counterpartyIfsc: string | null;
|
||||
counterpartyBankName: string | null;
|
||||
/**
|
||||
* Set only when `transactionType` is `UPI`. Every other channel moved money
|
||||
* by account number, so there is no VPA to report.
|
||||
*/
|
||||
counterpartyVpa: string | null;
|
||||
}
|
||||
|
||||
/** One page of `GET /accounts/statement`. */
|
||||
export interface CbsStatementPage {
|
||||
content: CbsStatementTransaction[];
|
||||
page: number;
|
||||
size: number;
|
||||
totalElements: number;
|
||||
totalPages: number;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query for `GET /accounts/statement/export`.
|
||||
*
|
||||
* The same range `CbsStatementQuery` describes, minus paging: the whole range
|
||||
* becomes one document, so `page` and `size` are not accepted. The bank reuses
|
||||
* the cached fetch behind `GET /accounts/statement`, which is why exporting a
|
||||
* range just viewed on screen costs nothing extra upstream.
|
||||
*/
|
||||
export type CbsStatementExportQuery = Omit<CbsStatementQuery, 'page' | 'size'>;
|
||||
|
||||
/**
|
||||
* A PDF the bank returned.
|
||||
*
|
||||
* These two endpoints are the only ones that do not answer the JSON envelope,
|
||||
* so nothing about them flows through `apiClient` — see
|
||||
* `services/files/pdfDownload.ts`. The file is written to the app's own
|
||||
* storage, which needs no runtime permission on either platform and is cleared
|
||||
* with the app.
|
||||
*/
|
||||
export interface CbsPdfDocument {
|
||||
/** Absolute path on the device. Not a URL, and not readable by other apps. */
|
||||
path: string;
|
||||
/** From `Content-Disposition`, falling back to one built from the account. */
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface PublicKeyResult {
|
||||
algorithm: string;
|
||||
/** Always `RSA/ECB/OAEPWithSHA-256AndMGF1Padding`. */
|
||||
transformation: string;
|
||||
/** Base64 DER, SPKI. */
|
||||
publicKey: string;
|
||||
}
|
||||
|
||||
export interface CreateMpinRequest {
|
||||
setupToken: string;
|
||||
deviceId: string;
|
||||
encryptedMpin: string;
|
||||
}
|
||||
|
||||
export interface CreateMpinResult {
|
||||
mpinStatus: string;
|
||||
registrationComplete: boolean;
|
||||
}
|
||||
|
||||
/* ── Session ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
export interface LoginRequest {
|
||||
orgId: string;
|
||||
mobileNumber: string;
|
||||
deviceId: string;
|
||||
encryptedMpin: string;
|
||||
/**
|
||||
* Only present on the sign-in that follows a device re-bind.
|
||||
*
|
||||
* verify-otp answers `mpinSetupRequired: false` for a customer who already
|
||||
* has an MPIN and hands back a setup token anyway. There is no MPIN to
|
||||
* create, so the token is spent here instead: it is what tells the server
|
||||
* this sign-in is the far end of an OTP that was just verified, rather than
|
||||
* an ordinary one. Omitted entirely on every other sign-in.
|
||||
*/
|
||||
setupToken?: string;
|
||||
}
|
||||
|
||||
export interface LoginResult {
|
||||
sessionToken: string;
|
||||
/** Absolute expiry. Never extended, however active the user is. */
|
||||
expiresAt: string;
|
||||
idleTimeoutSeconds: number;
|
||||
fullName: string;
|
||||
}
|
||||
|
||||
export interface SessionIdentity {
|
||||
orgId: string;
|
||||
mobileNumber: string;
|
||||
fullName: string;
|
||||
deviceId: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
/* ── Accounts ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Account types `GET /accounts` returns.
|
||||
*
|
||||
* Deposit accounts are in here as well as operative ones: a profile commonly
|
||||
* holds an FD or RD alongside its savings account, and they come back from the
|
||||
* same call. They cannot take part in IMPS, which is why the bank allocates no
|
||||
* MMID against them.
|
||||
*/
|
||||
export type CbsAccountType =
|
||||
| 'SAVINGS'
|
||||
| 'CURRENT'
|
||||
| 'FIXED_DEPOSIT'
|
||||
| 'RECURRING_DEPOSIT';
|
||||
export type CbsAccountStatus = 'ACTIVE' | 'DORMANT' | 'BLOCKED' | 'CLOSED';
|
||||
|
||||
/**
|
||||
* An account as `GET /accounts` returns it.
|
||||
*
|
||||
* Note what is *not* here: there is no balance field, because no endpoint in
|
||||
* the CBS API returns one. Anything showing a balance is not sourced from here.
|
||||
*
|
||||
* The account number is unmasked — this is the only endpoint that returns one —
|
||||
* so it must never be written to disk, logged, or rendered in full where a
|
||||
* masked form would do.
|
||||
*/
|
||||
export interface CbsAccount {
|
||||
accountNumber: string;
|
||||
accountType: CbsAccountType;
|
||||
status: CbsAccountStatus;
|
||||
ifsc: string;
|
||||
/** Null until the bank has allocated one. */
|
||||
mmid: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* One deposit as `GET /accounts/deposits/detail` returns it.
|
||||
*
|
||||
* Looked up by the deposit's own account number, which only `GET /accounts`
|
||||
* hands out in full — a masked or truncated number is not a valid lookup, so
|
||||
* this can only be asked for about an account the profile already listed.
|
||||
*
|
||||
* The nullable fields are the ones that apply to a single kind of deposit:
|
||||
* `interestRate` and `tenureMonths` describe the fixed-deposit side,
|
||||
* `installmentAmount` and `installmentsPaid` the recurring side. The bank
|
||||
* guarantees neither set, so nothing here may be read without a null check.
|
||||
*/
|
||||
export interface CbsDepositDetail {
|
||||
accountNumber: string;
|
||||
productName: string;
|
||||
depositType: Extract<CbsAccountType, 'FIXED_DEPOSIT' | 'RECURRING_DEPOSIT'>;
|
||||
status: string;
|
||||
balance: number;
|
||||
interestRate: number | null;
|
||||
tenureMonths: number | null;
|
||||
installmentAmount: number | null;
|
||||
installmentsPaid: number | null;
|
||||
/** `yyyy-MM-dd`. */
|
||||
maturityDate: string | null;
|
||||
/** `yyyy-MM-dd`. */
|
||||
openedDate: string | null;
|
||||
}
|
||||
|
||||
export interface MmidAccount {
|
||||
/** No endpoint ever returns a full account number. */
|
||||
accountNumberMasked: string;
|
||||
accountType: CbsAccountType;
|
||||
status: CbsAccountStatus;
|
||||
ifsc: string;
|
||||
/** Null until the bank has allocated one. */
|
||||
mmid: string | null;
|
||||
}
|
||||
|
||||
export interface MmidAccountsResult {
|
||||
mobileNumber: string;
|
||||
accounts: MmidAccount[];
|
||||
}
|
||||
|
||||
/* ── Payees ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
/** P2A routes by account number and IFSC; P2P by mobile number and MMID. */
|
||||
export type BeneficiaryMode = 'P2A' | 'P2P';
|
||||
|
||||
/**
|
||||
* How a payee is addressed. Exactly one pair is valid per mode — account
|
||||
* number + IFSC for P2A, mobile + MMID for P2P — and sending the wrong pair
|
||||
* for the chosen mode is answered with `BENEFICIARY_DETAILS_INCOMPLETE`.
|
||||
*
|
||||
* `name` is required by both endpoints. It is what the customer believes the
|
||||
* payee is called; the bank answers with its own resolution, which is the one
|
||||
* that gets stored and shown.
|
||||
*/
|
||||
export interface BeneficiaryPayee {
|
||||
accountNumber?: string;
|
||||
ifsc?: string;
|
||||
mobileNumber?: string;
|
||||
mmid?: string;
|
||||
/** Up to 120 characters. */
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* `POST /beneficiaries/validate`.
|
||||
*
|
||||
* Payer and payee are separate objects by design: the two carry the same field
|
||||
* names, and flattening them is how an account number ends up on the wrong
|
||||
* side of a transfer. The payer must be one of the caller's own accounts —
|
||||
* anything else answers `ACCOUNT_NOT_FOUND`, exactly as a typo would, so the
|
||||
* endpoint cannot be used to discover whether someone else's account exists.
|
||||
*/
|
||||
export interface BeneficiaryValidateRequest {
|
||||
mode: BeneficiaryMode;
|
||||
payer: { accountNumber: string };
|
||||
payee: BeneficiaryPayee;
|
||||
}
|
||||
|
||||
/**
|
||||
* IMPS's own answer to a validate call, passed through verbatim.
|
||||
*
|
||||
* Note this is *not* a beneficiary: nothing has been saved, and the name comes
|
||||
* back masked. A 200 does not mean the payee was verified — that is what
|
||||
* `result` says — so callers must read `result` rather than trusting the
|
||||
* status code.
|
||||
*/
|
||||
export interface BeneficiaryValidateResult {
|
||||
txnId: string;
|
||||
refId: string;
|
||||
/** IMPS's masked rendering of the account holder's name. */
|
||||
maskedName: string;
|
||||
/** `SUCCESS` when the payee was verified. */
|
||||
result: string;
|
||||
npciMessageId: string;
|
||||
timestamp: string;
|
||||
riskScore: string;
|
||||
accountType: string;
|
||||
accountHolderType: string;
|
||||
responseCode: string;
|
||||
errCode: string;
|
||||
errorDiscrp: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* `POST /beneficiaries`.
|
||||
*
|
||||
* Resolved through the bank's own CBS lookup, which is a different path from
|
||||
* the IMPS call `validate` makes — so the two can disagree, and the name that
|
||||
* gets stored is CBS's. The nickname lives here and not on validate, because
|
||||
* until a payee is saved there is nothing to label.
|
||||
*/
|
||||
export interface BeneficiarySaveRequest {
|
||||
mode: BeneficiaryMode;
|
||||
beneficiary: BeneficiaryPayee;
|
||||
/** Up to 60 characters. */
|
||||
nickname?: string;
|
||||
}
|
||||
|
||||
/** True when IMPS verified the payee. A 200 alone does not mean it did. */
|
||||
export const isPayeeVerified = (result: BeneficiaryValidateResult) =>
|
||||
result.result?.toUpperCase() === 'SUCCESS';
|
||||
|
||||
export interface CbsBeneficiary {
|
||||
/** Null from `validate`, which resolves the payee without saving it. */
|
||||
beneficiaryId: string | null;
|
||||
mode: BeneficiaryMode;
|
||||
/** The bank's answer. Sending a name has no effect. */
|
||||
name: string;
|
||||
bankName: string;
|
||||
nickname: string | null;
|
||||
accountNumberMasked: string;
|
||||
ifsc: string | null;
|
||||
mobileNumber: string | null;
|
||||
mmid: string | null;
|
||||
status: string | null;
|
||||
}
|
||||
|
||||
/* ── Positive Pay ─────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* A cheque declared to the bank before it is presented, so a tampered one is
|
||||
* refused at clearing.
|
||||
*
|
||||
* `accountNumber` is the full number, not the masked form every other payload
|
||||
* carries — and no endpoint returns it, so it can only come from the customer
|
||||
* typing it. The server checks it belongs to the caller and answers a
|
||||
* mismatch with the same `ACCOUNT_NOT_FOUND` as a typo, deliberately, so this
|
||||
* cannot be used to discover whether someone else's account exists.
|
||||
*/
|
||||
export interface PositivePayRequest {
|
||||
/** 5–20 digits. Must be one of the caller's own accounts. */
|
||||
accountNumber: string;
|
||||
/** Exactly 6 digits, read from the cheque's MICR band. */
|
||||
chequeNumber: string;
|
||||
/** `yyyy-MM-dd`. */
|
||||
chequeDate: string;
|
||||
/** At least 1.00, at most two decimal places. */
|
||||
amount: number;
|
||||
/** Up to 100 characters, exactly as written on the cheque. */
|
||||
payeeName: string;
|
||||
}
|
||||
|
||||
export interface PositivePayResult {
|
||||
/**
|
||||
* The bank's handle for this registration, and the only record the customer
|
||||
* keeps — nothing is stored on this side to look it up again.
|
||||
*/
|
||||
referenceId: string;
|
||||
status: string;
|
||||
accountNumberMasked: string;
|
||||
chequeNumber: string;
|
||||
chequeDate: string;
|
||||
amount: number;
|
||||
payeeName: string;
|
||||
}
|
||||
|
||||
/* ── Cheques ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* The lookup both `POST /positive-pay/verify` and `POST /check/verify` take.
|
||||
*
|
||||
* One request shape, one core-banking lookup behind it — the two endpoints
|
||||
* differ only in which answer counts as good news, so there is nothing to
|
||||
* distinguish here.
|
||||
*/
|
||||
export interface ChequeLookupRequest {
|
||||
/** 5–20 digits. Must be one of the caller's own accounts. */
|
||||
accountNumber: string;
|
||||
/** Exactly 6 digits, read from the cheque's MICR band. */
|
||||
chequeNumber: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A cheque as `POST /check/verify` reports it.
|
||||
*
|
||||
* Only a cheque declared under `POST /positive-pay` exists to this endpoint —
|
||||
* the bank keeps no separate issuance ledger — so an ordinary unregistered
|
||||
* cheque answers `CHEQUE_NOT_FOUND` rather than turning up with some other
|
||||
* status. That is also why there is no way to stop one that was never
|
||||
* registered.
|
||||
*/
|
||||
export interface ChequeDetail {
|
||||
chequeNumber: string;
|
||||
accountNumberMasked: string;
|
||||
/** `REGISTERED` until a stop takes effect, then `STOPPED`. */
|
||||
status: 'REGISTERED' | 'STOPPED' | string;
|
||||
payeeName: string;
|
||||
amount: number;
|
||||
/** `yyyy-MM-dd`. */
|
||||
chequeDate: string;
|
||||
}
|
||||
|
||||
/** Why the customer is stopping the cheque. The bank accepts only these. */
|
||||
export const STOP_PAYMENT_REASONS = [
|
||||
'LOST',
|
||||
'STOLEN',
|
||||
'DISPUTE',
|
||||
'SIGNATURE_MISMATCH',
|
||||
'ACCOUNT_CLOSURE',
|
||||
'DUPLICATE_ISSUED',
|
||||
'OTHER',
|
||||
] as const;
|
||||
|
||||
export type StopPaymentReason = (typeof STOP_PAYMENT_REASONS)[number];
|
||||
|
||||
export interface StopPaymentRequest extends ChequeLookupRequest {
|
||||
reason: StopPaymentReason;
|
||||
/** Up to 200 characters. Optional, and mainly there to qualify `OTHER`. */
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The outcome of a stop, and nothing more — the endpoint deliberately does not
|
||||
* repeat the cheque's detail back, because `POST /check/verify` already showed
|
||||
* the customer what they were confirming. As with Positive Pay, nothing is
|
||||
* stored on this side: `referenceId` is the handle they quote at a branch.
|
||||
*/
|
||||
export interface StopPaymentResult {
|
||||
referenceId: string;
|
||||
status: string;
|
||||
accountNumberMasked: string;
|
||||
chequeNumber: string;
|
||||
reason: StopPaymentReason | string;
|
||||
}
|
||||
|
||||
/* ── Cheque books ─────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* The leaf counts the bank issues. A closed set sent as text, the same shape
|
||||
* as `reason` on a stop payment — the server takes `"50"`, not `50`, and
|
||||
* answers VALIDATION_ERROR for anything outside these three.
|
||||
*/
|
||||
export const CHEQUE_BOOK_LEAF_COUNTS = ['25', '50', '100'] as const;
|
||||
|
||||
export type ChequeBookLeafCount = (typeof CHEQUE_BOOK_LEAF_COUNTS)[number];
|
||||
|
||||
export interface ChequeBookRequest {
|
||||
/** 5–20 digits. Must be one of the caller's own accounts. */
|
||||
accountNumber: string;
|
||||
/** Sent as text — see `CHEQUE_BOOK_LEAF_COUNTS`. */
|
||||
leafCount: ChequeBookLeafCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* A fee quote for one account and leaf count, from `POST /cheque-book/charges`.
|
||||
*
|
||||
* Note `leafCount` comes back as a number though it was sent as a string —
|
||||
* the asymmetry is the server's, not a mistake here.
|
||||
*/
|
||||
export interface ChequeBookCharges {
|
||||
accountNumberMasked: string;
|
||||
leafCount: number;
|
||||
/** In rupees. Zero is a real answer — a 25-leaf book is free. */
|
||||
charge: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A cheque book as `POST /cheque-book` issues it.
|
||||
*
|
||||
* `charge` is what the bank actually applied, echoed back rather than
|
||||
* re-derived, so it always agrees with the quote the customer accepted.
|
||||
* Serial numbers are per-account and never reissued: a second book starts
|
||||
* exactly where the previous one ended.
|
||||
*/
|
||||
export interface ChequeBookIssued {
|
||||
/** The bank's handle, and what the customer quotes at a branch. */
|
||||
referenceId: string;
|
||||
/** `ISSUED` — the call is synchronous, so there is no pending state. */
|
||||
status: 'ISSUED' | string;
|
||||
accountNumberMasked: string;
|
||||
leafCount: number;
|
||||
charge: number;
|
||||
/** 6 digits, as printed in the MICR band. */
|
||||
startingChequeNumber: string;
|
||||
endingChequeNumber: string;
|
||||
/** `yyyy-MM-dd`. */
|
||||
issueDate: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* One row of `GET /cheque-book`. Deliberately not `ChequeBookIssued`: the
|
||||
* history carries neither `status` nor `accountNumberMasked`, because the
|
||||
* caller already named the account and every book listed is issued.
|
||||
*/
|
||||
export interface ChequeBookHistoryEntry {
|
||||
referenceId: string;
|
||||
leafCount: number;
|
||||
charge: number;
|
||||
startingChequeNumber: string;
|
||||
endingChequeNumber: string;
|
||||
issueDate: string;
|
||||
}
|
||||
|
||||
/* ── Feature configuration ────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Server-driven feature switches.
|
||||
*
|
||||
* `GET /features/config` — callable with or without a session. Unauthenticated
|
||||
* it returns `overrides` only, which is what lets the sign-in screen itself be
|
||||
* gated; with a session it also returns `roleOverrides` for the caller's role
|
||||
* and tenant.
|
||||
*
|
||||
* Keys are feature paths as declared in `FeatureKey` (`'loans.apply.submit'`).
|
||||
* Unknown keys are dropped by the client rather than trusted, so the server can
|
||||
* ship a key ahead of the app release that understands it.
|
||||
*/
|
||||
export interface FeatureConfigResult {
|
||||
/**
|
||||
* Monotonic version of the published configuration. Logged on apply so a
|
||||
* support case can be tied to the exact config a handset was running.
|
||||
*/
|
||||
version: number;
|
||||
/** Org-wide switches. Applied to the `remote` layer. */
|
||||
overrides: Record<string, boolean>;
|
||||
/** Switches for this session's role and tenant. Applied to the `tenant` layer. */
|
||||
roleOverrides?: Record<string, boolean>;
|
||||
}
|
||||
Reference in New Issue
Block a user