236 lines
10 KiB
Swift
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
|
|
}
|
|
}
|