Files

236 lines
10 KiB
Swift

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
}
}