import Foundation private let BML_API_BASE = "https://www.bankofmaldives.com.mv/internetbanking" private let BML_API_UA = "bml-mobile-banking/348 (Apple; iOS 17.0; iPhone)" private let BML_API_VERSION = "2.1.44.348" final class BmlAccountClient { private let session: BmlSession private let urlSession: URLSession = { let cfg = URLSessionConfiguration.default cfg.timeoutIntervalForRequest = 30 return URLSession(configuration: cfg) }() init(session: BmlSession) { self.session = session } // MARK: - Dashboard / Accounts func fetchAccounts( loginTag: String, profileName: String = "Personal", profileId: String = "" ) async throws -> [BankAccount] { let data = try await apiGet("/api/mobile/dashboard") guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any], root["success"] as? Bool == true, let payload = root["payload"] as? [String: Any], let dashboard = payload["dashboard"] as? [[String: Any]] else { return [] } return parseDashboard(dashboard, loginTag: loginTag, profileName: profileName, profileId: profileId) } // MARK: - Profile check (lightweight ping to verify session) func checkProfile() async throws { let data = try await apiGet("/api/mobile/profile") if let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any], root["success"] as? Bool == false { throw BmlError.sessionExpired } } // MARK: - User info func fetchUserInfo() async throws -> BmlUserInfo? { let data = try await apiGet("/api/mobile/userinfo") guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any], root["success"] as? Bool == true, let payload = root["payload"] as? [String: Any], let user = payload["user"] as? [String: Any] else { return nil } return BmlUserInfo( fullName: (user["fullname"] as? String ?? "").trimmingCharacters(in: .whitespaces), email: (user["email"] as? String ?? "").trimmingCharacters(in: .whitespaces), mobile: (user["mobile_phone"] as? String ?? "").trimmingCharacters(in: .whitespaces), customerId: (user["customer_number"] as? String ?? "").trimmingCharacters(in: .whitespaces), idCard: (user["idcard"] as? String ?? "").trimmingCharacters(in: .whitespaces), birthdate: (user["birthdate"] as? String ?? "").trimmingCharacters(in: .whitespaces) ) } // MARK: - Loan detail func fetchLoanDetail(internalId: String) async throws -> BmlLoanDetail? { let data = try await apiGet("/api/mobile/account/\(internalId)") guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any], root["success"] as? Bool == true, let p = root["payload"] as? [String: Any] else { return nil } return BmlLoanDetail( loanAmount: p["loanAmount"] as? Double ?? 0, outstandingAmt: p["outstandingAmt"] as? Double ?? 0, repayAmount: p["repayAmount"] as? Double ?? 0, intRate: p["intRate"] as? Double ?? 0, loanStatus: p["loanStatus"] as? String ?? "", startDate: p["startDate"] as? String ?? "", endDate: p["endDate"] as? String ?? "", noOfRepayOverdue: p["noOfRepayOverdue"] as? Int ?? 0, overdueAmount: p["overdueAmount"] as? Double ?? 0 ) } // MARK: - Transfer OTP channels func fetchTransferChannels() async throws -> [BmlOtpChannel] { let data = try await apiGet("/api/mobile/transfer") guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any], root["success"] as? Bool == true, let arr = (root["payload"] as? [String: Any])?["transfer"] as? [String: Any], let channels = arr["otpChannel"] as? [[String: Any]] else { return [] } return channels.map { BmlOtpChannel( channel: $0["channel"] as? String ?? "", description: $0["description"] as? String ?? "", masked: $0["masked"] as? String ?? "" ) } } // MARK: - Dashboard parsing private func parseDashboard( _ dashboard: [[String: Any]], loginTag: String, profileName: String, profileId: String ) -> [BankAccount] { var casaAccounts: [BankAccount] = [] var cardAccounts: [BankAccount] = [] var loanAccounts: [BankAccount] = [] for item in dashboard { let currency = item["currency"] as? String ?? "MVR" let accountType = item["account_type"] as? String ?? "CASA" let product = item["product"] as? String ?? "" let accountNum = Self.stringValue(item["account"]) let status = item["account_status"] as? String ?? "Active" let internalId = Self.stringValue(item["id"]) switch accountType { case "CASA": let avail = item["availableBalance"] as? Double ?? 0 casaAccounts.append(BankAccount( id: "BML_\(accountNum)_\(loginTag)", bank: "BML", profileName: profileName, profileType: "BML", productCode: item["product_code"] as? String ?? "", accountNumber: accountNum, accountBriefName: item["alias"] as? String ?? "", currencyName: currency, accountTypeName: product, availableBalance: avail, currentBalance: item["ledgerBalance"] as? Double ?? 0, blockedAmount: item["lockedAmount"] as? Double ?? 0, mvrBalance: currency == "MVR" ? avail : nil, statusDesc: status, profileImageHash: nil, loginTag: loginTag, profileId: profileId, internalId: internalId )) case "Loan": let outstanding = abs(item["availableBalance"] as? Double ?? 0) loanAccounts.append(BankAccount( id: "BML_LOAN_\(accountNum)_\(loginTag)", bank: "BML", profileName: profileName, profileType: "BML_LOAN", productCode: item["product_code"] as? String ?? "", accountNumber: accountNum, accountBriefName: item["alias"] as? String ?? "", currencyName: currency, accountTypeName: product, availableBalance: outstanding, currentBalance: outstanding, blockedAmount: 0, mvrBalance: nil, statusDesc: status, profileImageHash: nil, loginTag: loginTag, profileId: profileId, internalId: internalId )) case "Card": let isPrepaid = item["prepaid_card"] as? Bool ?? false let isVisible = item["account_visible"] as? Bool ?? false let cardBalance = item["cardBalance"] as? [String: Any] let avail = cardBalance?["AvailableLimit"] as? Double ?? 0 let current = cardBalance?["CurrentBalance"] as? Double ?? 0 let cardType: String = isPrepaid ? "BML_PREPAID" : isVisible ? "BML_CREDIT" : "BML_DEBIT" let alias = (item["alias"] as? String ?? "").isEmpty ? product : (item["alias"] as? String ?? product) cardAccounts.append(BankAccount( id: "BML_CARD_\(accountNum)_\(loginTag)", bank: "BML", profileName: profileName, profileType: cardType, productCode: item["product_code"] as? String ?? "", accountNumber: accountNum, accountBriefName: alias, currencyName: currency, accountTypeName: product, availableBalance: avail, currentBalance: current, blockedAmount: 0, mvrBalance: currency == "MVR" ? avail : nil, statusDesc: status, profileImageHash: nil, loginTag: loginTag, profileId: profileId, internalId: internalId )) default: break } } return casaAccounts + cardAccounts + loanAccounts } private static func stringValue(_ value: Any?) -> String { switch value { case let string as String: return string case let int as Int: return String(int) case let int64 as Int64: return String(int64) case let double as Double: return double.rounded() == double ? String(Int64(double)) : String(double) default: return "" } } // MARK: - HTTP private func apiGet(_ path: String) async throws -> Data { var req = URLRequest(url: URL(string: "\(BML_API_BASE)\(path)")!) req.setValue("Bearer \(session.accessToken)", forHTTPHeaderField: "Authorization") req.setValue(BML_API_UA, forHTTPHeaderField: "User-Agent") req.setValue(BML_API_VERSION, forHTTPHeaderField: "x-app-version") let (data, response) = try await urlSession.data(for: req) let code = (response as? HTTPURLResponse)?.statusCode ?? 0 if code == 401 || code == 419 { throw BmlError.sessionExpired } if code >= 500 { throw BmlError.networkError("Server error HTTP \(code)") } return data } }