SpecTrace traceability matrix
NBK-BRD-2026-014 v1.0 → bank-app · generated 2026-09-27T04:18:05+00:00 · statuses verified against Surefire results
27
Requirements
11
Covered
11
Newly tested
3
Bugs found
2
Not implemented
39
Tests added
30 min per requirement to locate code, find tests, write missing tests and update the matrix by hand
| Requirement | Status | Implementation | Tests | Notes |
|---|---|---|---|---|
REQ-LN-03 Monthly instalment calculation | Bug found | LoanService#monthlyInstalment LoanService.java:60 | NEW instalment_25000_at_5_5pct_36months FAILexpected: 754.90 but was: 754.89 NEW instalment_60000_at_4_5pct_48months FAILexpected: 1368.21 but was: 1368.20 | Spec requires HALF_UP rounding; code uses RoundingMode.DOWN at LoanService.java:60 — 25,000/5.5%/36m yields 754.89 (expected 754.90) and 60,000/4.5%/48m yields 1368.20 (expected 1368.21). |
REQ-SEC-05 Account ownership | Bug found | SecurityConfig#filterChain SecurityConfig.java:44JwtAuthFilter#doFilterInternal JwtAuthFilter.java:33AccountController#get AccountController.java:44TransferController#transfer TransferController.java:35 | NEW customerAttemptingTransferFromAnotherCustomersAccountIsRefusedWith403 FAILStatus expected:<403> but was:<200> NEW customerCanViewTheirOwnAccountDetails PASSNEW customerRequestingAnotherCustomersAccountDetailsIsRefusedWith403 FAILStatus expected:<403> but was:<200> NEW requestWithoutValidSessionTokenIsRefused PASS | BUG 1: spec expects HTTP 403 when customer A reads customer B's account, but AccountController#get (line 44) returns 200 — no ownership check against the JWT principal. BUG 2: spec expects HTTP 403 when customer B initiates a transfer from customer A's account, but TransferController#transfer (line 35) returns 200 — no ownership check on fromAccount against the JWT principal. |
REQ-TRF-03 Daily transfer limit | Bug found | TransferService#transfer TransferService.java:50 | NEW dailyLimitExactly5000IsAccepted FAILDaily transfer limit exceeded for account SG38833806 NEW dailyLimitExceededIsRejected PASSNEW previousDayTransfersDoNotCountTowardsLimit PASS | BUG: spec says exactly SGD 5,000.00 is allowed (inclusive), but line 50 uses `>= 0` (sentToday + amount >= DAILY_LIMIT) which rejects a transfer that brings the day's total to exactly 5,000.00; it should be `> 0`. (`TransferService.java:50`) |
REQ-ACC-07 PIN lockout | Not implemented | none found | no tests | AccountService has no failed-PIN counter or lockout logic. LoginRateLimiter (com.spectrace.bank.security) is HTTP-level login throttling, not account-level PIN lockout, and does not satisfy this requirement. |
REQ-TRF-07 Scheduled transfers | Not implemented | none found | no tests | No scheduled transfer entity, scheduler, or cancellation logic exists anywhere in the transfers package or common package. The feature is entirely absent. |
REQ-ACC-01 Account opening | Newly tested | AccountService#open AccountService.java:27 | opensAccountWithOwnerAndInitialDeposit PASSrejectsBlankOwnerOrNegativeDeposit PASSNEW zeroInitialDepositIsAllowed PASS | Existing tests cover blank-name rejection, negative-deposit rejection, and initial balance/status. New test covers the missing criterion that SGD 0.00 initial deposit is allowed. |
REQ-ACC-05 Account closure | Newly tested | AccountService#close AccountService.java:81 | NEW closingAccountWithNonZeroBalanceIsRejected PASSNEW closingAccountWithZeroBalanceSetsStatusToClosed PASS | close() guards on signum()!=0 and throws BankException, leaving status ACTIVE. With zero balance it sets status to CLOSED. Both acceptance criteria verified by new tests. |
REQ-ACC-06 Frozen accounts | Newly tested | AccountService#requireActive AccountService.java:102 | NEW depositToFrozenAccountIsRejected PASSNEW frozenAccountBalanceUnchangedAfterRejectedOperations PASSNEW withdrawalFromFrozenAccountIsRejected PASS | requireActive() throws BankException for any non-ACTIVE status; both deposit() and withdraw() delegate to it. All three acceptance criteria covered by new tests. |
REQ-LN-04 Affordability check | Newly tested | LoanService#evaluate LoanService.java:79 | NEW incomeOfExactly3xInstalmentIsEligible PASSNEW insufficientIncomeIsRejectedWithAffordabilityReason PASS | evaluate() computes requiredIncome = instalment * 3 and rejects if monthlyIncome < requiredIncome; income equal to 3x is allowed. Note: affordability check uses the instalment computed by monthlyInstalment, which itself has the HALF_UP bug (REQ-LN-03), but the affordability logic itself is correct. |
REQ-LN-06 Early repayment fee | Newly tested | LoanService#earlyRepaymentFee LoanService.java:63 | NEW earlyRepaymentFee_10000_00 PASSNEW earlyRepaymentFee_12345_67 PASS | earlyRepaymentFee() multiplies outstanding principal by 0.015 and rounds HALF_UP via Money.round(); both spec values (185.19 and 150.00) verified. |
REQ-SEC-01 PIN protection | Newly tested | AccountService#open AccountService.java:38Account#pinMatches Account.java:48Account Account.java:15 | NEW apiResponseContainsNoPinOrPinHashField PASSNEW correctPinVerifiesAndIncorrectPinDoesNot PASSNEW storedPinIsNotPlainText PASS | PIN is BCrypt-hashed in AccountService#open (line 38) and never returned by the API thanks to @JsonIgnore on Account#pinHash (line 15). All three acceptance criteria verified and passing. |
REQ-SEC-02 Session tokens | Newly tested | JwtUtil#generate JwtUtil.java:33JwtUtil#validate JwtUtil.java:47JwtUtil#isValid JwtUtil.java:57 | NEW expiredTokenIsRejected PASSNEW freshlyIssuedTokenIsAcceptedAndIdentifiesAccount PASSNEW tamperedTokenIsRejected PASSNEW tokenLifetimeIs60Minutes PASS | JwtUtil signs HS256 tokens with subject=accountNumber and expiry=3600 s (60 min) as configured in application.properties. All four acceptance criteria verified and passing. |
REQ-SEC-03 Login throttling | Newly tested | LoginRateLimiter#isBlocked LoginRateLimiter.java:33LoginRateLimiter#recordFailure LoginRateLimiter.java:40LoginRateLimiter#clearFailures LoginRateLimiter.java:46AuthController#login AuthController.java:46 | NEW after5FailedAttemptsNextAttemptIsRefused PASSNEW failedAttemptsOnOneAccountDoNotAffectOtherAccounts PASSNEW fourFailedAttemptsDoNotBlockAccount PASSNEW successfulLoginResetsFailedAttemptCount PASS | Sliding-window rate limiter blocks after 5 failures in 900 s window; clearFailures() resets on success; per-account isolation verified. All four acceptance criteria passing. |
REQ-SEC-04 Audit trail | Newly tested | AuditService#record AuditService.java:27AuditService#getAll AuditService.java:32AuthController#login AuthController.java:50AccountService#deposit AccountService.java:59AccountService#withdraw AccountService.java:69AccountService#freeze AccountService.java:94AccountService#close AccountService.java:87 | NEW accountFreezesAndClosuresAreRecorded PASSNEW auditEntriesCannotBeRemovedThroughInterface PASSNEW depositsAndWithdrawalsAreRecorded PASSNEW successfulAndFailedLoginsAreRecorded PASS | AuditService uses a CopyOnWriteArrayList and returns Collections.unmodifiableList; all required event types are recorded in the relevant service and controller methods. All four acceptance criteria passing. |
REQ-TRF-04 New payee review | Newly tested | TransferService#transfer TransferService.java:56 | NEW knownPayeeOver1000IsNotHeld PASSNEW newPayeeExactly1000CompletesImmediately PASSNEW newPayeeOver1000IsPendingReview PASS | All three acceptance criteria pass: >1000 to new payee → PENDING_REVIEW with no balance change; exactly 1000 → COMPLETED immediately; known payee (prior COMPLETED transfer) → not held regardless of amount. |
REQ-TRF-06 Transfer history | Newly tested | TransferService#history TransferService.java:74 | NEW historyIncludesBothIncomingAndOutgoing PASSNEW historyIsNewestFirst PASS | All three acceptance criteria pass: incoming and outgoing transfers both appear; third-party transfers are excluded; results are ordered newest-first via reverse ledger iteration. |
REQ-ACC-02 Account number format | Covered | AccountService#nextAccountNumber AccountService.java:110 | accountNumbersFollowFormatAndAreUnique PASS | nextAccountNumber() generates 'SG' + 8-digit zero-padded random int and loops until unique. Test asserts both accounts match SG\d{8} and are distinct. |
REQ-ACC-03 Deposits | Covered | AccountService#deposit AccountService.java:53 | depositIncreasesBalanceAndMustBePositive PASS | deposit() rejects non-positive amounts and adds the exact amount (Money.round) to the balance. Test asserts 100 + 50.25 = 150.25 and zero is rejected. |
REQ-ACC-04 Withdrawals and insufficient funds | Covered | AccountService#withdraw AccountService.java:63 | withdrawalBeyondBalanceIsRejected PASS | withdraw() throws InsufficientFundsException when amount exceeds balance; balance is unchanged after the rejection. Test asserts both criteria. |
REQ-ACC-08 Balance enquiry | Covered | AccountService#getBalance AccountService.java:77 | balanceEnquiryIsRoundedToTwoDecimals PASS | getBalance() calls Money.round (HALF_UP, 2 d.p.). Existing test opens account with 10.005 and asserts getBalance() returns 10.01. |
REQ-LN-01 Loan amount bounds | Covered | LoanService#evaluate LoanService.java:68 | loanAmountMustBeWithinBounds PASS | evaluate() rejects amounts below SGD 1,000.00 and above SGD 100,000.00; boundary values 999.99 and 100,000.01 are rejected, 1,000.00 and 100,000.00 are accepted. |
REQ-LN-02 Loan tenure | Covered | LoanService#evaluate LoanService.java:73 | tenureMustBeBetween12And60Months PASS | evaluate() rejects tenure < 12 or > 60 months; boundary values 11 and 61 are rejected, 12 and 60 are accepted. |
REQ-LN-05 Interest rate tiers | Covered | LoanService#interestRateFor LoanService.java:45 | interestRateDependsOnAmountTier PASS | interestRateFor() returns 6.5% below SGD 20,000, 5.5% for SGD 20,000–50,000 inclusive, 4.5% above SGD 50,000; all tier boundaries tested. |
REQ-LN-07 Loan decision | Covered | LoanService#apply LoanService.java:30LoanService#get LoanService.java:37 | everyApplicationIsApprovedOrRejectedWithReason PASS | Approved loans have null rejectionReason; rejected loans carry a non-blank reason. Applied loans are stored in a map and retrievable by id via get(). |
REQ-TRF-01 Atomic funds transfer | Covered | TransferService#transfer TransferService.java:37 | failedCreditLeavesSourceUnchanged PASStransferMovesFundsBetweenAccounts PASS | Both acceptance criteria covered by pre-existing tests: atomic debit+credit verified, and rollback on failed credit verified. |
REQ-TRF-02 No self-transfers | Covered | TransferService#transfer TransferService.java:38 | cannotTransferToSameAccount PASS | Self-transfer guard at line 38 throws IllegalArgumentException; existing test covers the criterion. |
REQ-TRF-05 Transfer reference | Covered | TransferService#nextReference TransferService.java:104 | everyTransferHasUniqueReferenceAndTimestamp PASS | Existing test verifies TRF-[A-Z0-9]{8} pattern, uniqueness across two transfers, and non-null timestamp. |