Skip to content

Commit

Permalink
Add Improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
cp-amisha-i committed Dec 26, 2024
1 parent dabcfe3 commit 3a51c7b
Show file tree
Hide file tree
Showing 51 changed files with 770 additions and 554 deletions.
4 changes: 4 additions & 0 deletions Data/Data.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
D88721432B99F133009DC5BE /* StorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88721422B99F133009DC5BE /* StorageManager.swift */; };
D8910E382BB6D1D300877CE0 /* ExpenseStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8910E372BB6D1D300877CE0 /* ExpenseStore.swift */; };
D895F2282CA297B900C2E4EB /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D895F2272CA297B900C2E4EB /* NetworkManager.swift */; };
D89BC2802D1C3DB200CDE86B /* DateTimeHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89BC27F2D1C3DB200CDE86B /* DateTimeHelper.swift */; };
D89C934C2BC68D9000FACD16 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89C934B2BC68D9000FACD16 /* Date+Extension.swift */; };
D89C934E2BC694C200FACD16 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89C934D2BC694C200FACD16 /* Double+Extension.swift */; };
D89DBE1D2B872F0B00E5F1BD /* NonceGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89DBE1C2B872F0B00E5F1BD /* NonceGenerator.swift */; };
Expand Down Expand Up @@ -84,6 +85,7 @@
D88721422B99F133009DC5BE /* StorageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageManager.swift; sourceTree = "<group>"; };
D8910E372BB6D1D300877CE0 /* ExpenseStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpenseStore.swift; sourceTree = "<group>"; };
D895F2272CA297B900C2E4EB /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = "<group>"; };
D89BC27F2D1C3DB200CDE86B /* DateTimeHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeHelper.swift; sourceTree = "<group>"; };
D89C934B2BC68D9000FACD16 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = "<group>"; };
D89C934D2BC694C200FACD16 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
D89DBE1C2B872F0B00E5F1BD /* NonceGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonceGenerator.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -247,6 +249,7 @@
D8D42AA62B872710009B345D /* Firebase */,
D89DBE1C2B872F0B00E5F1BD /* NonceGenerator.swift */,
D895F2272CA297B900C2E4EB /* NetworkManager.swift */,
D89BC27F2D1C3DB200CDE86B /* DateTimeHelper.swift */,
);
path = Helper;
sourceTree = "<group>";
Expand Down Expand Up @@ -521,6 +524,7 @@
D89DBE2B2B88817E00E5F1BD /* JSONUtils.swift in Sources */,
21559CA42CBD05570039F127 /* ActivityLog.swift in Sources */,
D8A7CA802BA867F80014EC67 /* String+Extension.swift in Sources */,
D89BC2802D1C3DB200CDE86B /* DateTimeHelper.swift in Sources */,
213D0CCA2C89DBC800D65C73 /* Notification+Extension.swift in Sources */,
D895F2282CA297B900C2E4EB /* NetworkManager.swift in Sources */,
D8910E382BB6D1D300877CE0 /* ExpenseStore.swift in Sources */,
Expand Down
30 changes: 30 additions & 0 deletions Data/Data/Helper/DateTimeHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// DateTimeHelper.swift
// Data
//
// Created by Amisha Italiya on 25/12/24.
//

import Foundation

public func sortMonthYearStrings(_ s1: String, _ s2: String) -> Bool {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMMM yyyy"

guard let date1 = dateFormatter.date(from: s1),
let date2 = dateFormatter.date(from: s2) else {
return false
}

let components1 = Calendar.current.dateComponents([.year, .month], from: date1)
let components2 = Calendar.current.dateComponents([.year, .month], from: date2)

// Compare years first
if components1.year != components2.year {
return (components1.year ?? 0) > (components2.year ?? 0)
}
// If years are the same, compare months
else {
return (components1.month ?? 0) > (components2.month ?? 0)
}
}
3 changes: 2 additions & 1 deletion Data/Data/Model/AppUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public struct AppUser: Identifiable, Codable, Hashable, Sendable {
}

enum CodingKeys: String, CodingKey {
case id
case id = "id"
case firstName = "first_name"
case lastName = "last_name"
case emailId = "email_id"
Expand All @@ -67,4 +67,5 @@ public enum LoginType: String, Codable, Sendable {
case Apple = "apple"
case Google = "google"
case Email = "email"
case None = ""
}
121 changes: 88 additions & 33 deletions Data/Data/Model/Expense.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@ public struct Expense: Codable, Hashable, Identifiable {
public var name: String
public var amount: Double
public var date: Timestamp
public var updatedAt: Timestamp
public var updatedAt: Timestamp?
public var paidBy: [String: Double]
public let addedBy: String
public var updatedBy: String
public var updatedBy: String?
public var note: String?
public var imageUrl: String?
public var splitTo: [String] // Reference to user ids involved in the split
public var splitType: SplitType
public var splitData: [String: Double]? // Use this to store percentage or share data
public var isActive: Bool

public init(name: String, amount: Double, date: Timestamp, updatedAt: Timestamp = Timestamp(), paidBy: [String: Double],
addedBy: String, updatedBy: String, note: String? = nil, imageUrl: String? = nil, splitTo: [String],
public init(name: String, amount: Double, date: Timestamp, updatedAt: Timestamp? = nil, paidBy: [String: Double],
addedBy: String, updatedBy: String? = nil, note: String? = nil, imageUrl: String? = nil, splitTo: [String],
splitType: SplitType = .equally, splitData: [String: Double]? = [:], isActive: Bool = true) {
self.name = name
self.amount = amount
Expand Down Expand Up @@ -73,42 +73,18 @@ extension Expense {

switch self.splitType {
case .equally:
return calculateEqualSplitAmount(for: member)
return calculateEqualSplitAmount(memberId: member, amount: self.amount, splitTo: self.splitTo)
case .fixedAmount:
return self.splitData?[member] ?? 0
case .percentage:
let totalPercentage = self.splitData?.values.reduce(0, +) ?? 0
return self.amount * (self.splitData?[member] ?? 0) / totalPercentage
return calculatePercentageSplitAmount(memberId: member, amount: self.amount,
splitTo: self.splitTo, splitData: self.splitData ?? [:])
case .shares:
let totalShares = self.splitData?.values.reduce(0, +) ?? 0
return self.amount * ((self.splitData?[member] ?? 0) / totalShares)
return calculateSharesSplitAmount(memberId: member, amount: self.amount,
splitTo: self.splitTo, splitData: self.splitData ?? [:])
}
}

/// Returns the equal split amount for the member
private func calculateEqualSplitAmount(for member: String) -> Double {
let totalMembers = Double(self.splitTo.count)
let baseAmount = (self.amount / totalMembers).rounded(to: 2) // Base amount each member owes
let totalSplitAmount = baseAmount * totalMembers // The total split amount after rounding all members base amounts
let remainder = self.amount - totalSplitAmount // The leftover amount due to rounding

// Sort members deterministically to ensure consistent assignment of the remainder.
let sortedMembers = self.splitTo.sorted()

// Assign base amount to each member
var splitAmounts: [String: Double] = [:]
for splitMember in sortedMembers {
splitAmounts[splitMember] = baseAmount
}

// Distribute remainder to the first member in the sorted list
if remainder > 0, let firstMember = sortedMembers.first {
splitAmounts[firstMember]! += remainder
}

return splitAmounts[member] ?? 0
}

/// It will return the owing amount to the member for that expense that he have to get or pay back
public func getCalculatedSplitAmountOf(member: String) -> Double {
let paidAmount = self.paidBy[member] ?? 0
Expand Down Expand Up @@ -141,3 +117,82 @@ public struct ExpenseWithUser: Hashable {
self.user = user
}
}

public func calculateEqualSplitAmount(memberId: String, amount: Double, splitTo: [String]) -> Double {
let totalMembers = Double(splitTo.count)
let baseAmount = (amount / totalMembers).rounded(to: 2) // Base amount each member owes
let remainder = amount - (baseAmount * totalMembers) // The leftover amount due to rounding
let sortedMembers = splitTo.sorted() // Sort members deterministically for consistent assignment of remainder

// Assign base amount to each member
var splitAmounts: [String: Double] = [:]
for splitMember in sortedMembers {
splitAmounts[splitMember] = baseAmount
}

// Distribute remainder, if there is any, to the first member in the sorted list
if let firstMember = sortedMembers.first, memberId == firstMember {
splitAmounts[firstMember]! += remainder
}

return splitAmounts[memberId] ?? 0
}

public func calculatePercentageSplitAmount(memberId: String, amount: Double, splitTo: [String], splitData: [String: Double]) -> Double {
let totalPercentage = splitData.values.reduce(0, +)
if totalPercentage == 0 { return 0 }

var splitAmounts: [String: Double] = [:]
var totalUnroundedAmount = 0.0
var totalRoundedAmount = 0.0
var roundedSplitAmounts: [String: Double] = [:]
let sortedMembers = splitTo.sorted()

// Calculate unrounded amounts based on percentages
for splitMember in splitTo {
let percentage = Double(splitData[splitMember] ?? 0)
let unroundedAmount = totalPercentage == 0 ? 0 : (amount * (percentage / totalPercentage))
splitAmounts[splitMember] = unroundedAmount
totalUnroundedAmount += unroundedAmount
}

// Round the unrounded amounts to 2 decimal places
for (splitMember, unroundedAmount) in splitAmounts {
let roundedAmount = unroundedAmount.rounded(to: 2)
roundedSplitAmounts[splitMember] = roundedAmount
totalRoundedAmount += roundedAmount
}

// Distribute remainder, if there is any, to the first member in the sorted list
if let firstMember = sortedMembers.first, memberId == firstMember {
let remainder = amount - totalRoundedAmount
roundedSplitAmounts[firstMember]! += remainder
}

return roundedSplitAmounts[memberId] ?? 0
}

public func calculateSharesSplitAmount(memberId: String, amount: Double, splitTo: [String], splitData: [String: Double]) -> Double {
let totalShares = splitData.values.reduce(0, +)
if totalShares == 0 { return 0 }

var totalRoundedAmount = 0.0
var splitAmounts: [String: Double] = [:]
let sortedMembers = splitTo.sorted()

// Assign rounded share amounts to each member
for splitMember in splitTo {
let unroundedAmount = amount * (Double(splitData[splitMember] ?? 0) / totalShares)
let roundedAmount = unroundedAmount.rounded(to: 2)
splitAmounts[splitMember] = roundedAmount
totalRoundedAmount += roundedAmount
}

// Distribute remainder, if there is any, to the first member in the sorted list
if let firstMember = sortedMembers.first, memberId == firstMember {
let remainder = amount - totalRoundedAmount
splitAmounts[firstMember]! += remainder
}

return splitAmounts[memberId] ?? 0
}
4 changes: 2 additions & 2 deletions Data/Data/Model/Groups.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public struct Groups: Codable, Identifiable {

public var name: String
public var createdBy: String
public var updatedBy: String
public var updatedBy: String?
public var imageUrl: String?
public var members: [String]
public var balances: [GroupMemberBalance]
Expand All @@ -22,7 +22,7 @@ public struct Groups: Codable, Identifiable {
public var hasExpenses: Bool
public var isActive: Bool

public init(name: String, createdBy: String, updatedBy: String, imageUrl: String? = nil,
public init(name: String, createdBy: String, updatedBy: String? = nil, imageUrl: String? = nil,
members: [String], balances: [GroupMemberBalance], createdAt: Timestamp = Timestamp(),
updatedAt: Timestamp = Timestamp(), hasExpenses: Bool = false, isActive: Bool = true) {
self.name = name
Expand Down
12 changes: 6 additions & 6 deletions Data/Data/Model/Transaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,21 @@ public struct Transactions: Codable, Hashable, Identifiable {

public var id: String? // Automatically generated ID by Firestore

public let payerId: String
public let receiverId: String
public var payerId: String
public var receiverId: String
public let addedBy: String
public var updatedBy: String
public var updatedBy: String?
public var note: String?
public var imageUrl: String?
public var reason: String?
public var amount: Double
public var date: Timestamp
public var updatedAt: Timestamp
public var updatedAt: Timestamp?
public var isActive: Bool

public init(payerId: String, receiverId: String, addedBy: String, updatedBy: String,
public init(payerId: String, receiverId: String, addedBy: String, updatedBy: String? = nil,
note: String? = nil, imageUrl: String? = nil, reason: String? = nil, amount: Double,
date: Timestamp, updatedAt: Timestamp = Timestamp(), isActive: Bool = true) {
date: Timestamp, updatedAt: Timestamp? = nil, isActive: Bool = true) {
self.payerId = payerId
self.receiverId = receiverId
self.addedBy = addedBy
Expand Down
6 changes: 2 additions & 4 deletions Data/Data/Repository/ActivityLogRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ public class ActivityLogRepository: ObservableObject {

@Inject private var store: ActivityLogStore

public func fetchLatestActivityLogs(userId: String, completion: @escaping ([ActivityLog]?) -> Void) {
store.fetchLatestActivityLogs(userId: userId) { activityLogs in
completion(activityLogs)
}
public func fetchLatestActivityLogs(userId: String) -> AsyncStream<[ActivityLog]?> {
store.fetchLatestActivityLogs(userId: userId)
}

public func addActivityLog(userId: String, activity: ActivityLog) async throws {
Expand Down
15 changes: 12 additions & 3 deletions Data/Data/Repository/ExpenseRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public class ExpenseRepository: ObservableObject {
private func hasExpenseChanged(_ expense: Expense, oldExpense: Expense) -> Bool {
return oldExpense.name != expense.name || oldExpense.amount != expense.amount ||
oldExpense.date.dateValue() != expense.date.dateValue() ||
oldExpense.updatedAt.dateValue() != expense.updatedAt.dateValue() ||
oldExpense.updatedAt?.dateValue() != expense.updatedAt?.dateValue() ||
oldExpense.paidBy != expense.paidBy || oldExpense.updatedBy != expense.updatedBy ||
oldExpense.note != expense.note || oldExpense.imageUrl != expense.imageUrl ||
oldExpense.splitTo != expense.splitTo || oldExpense.splitType != expense.splitType ||
Expand All @@ -91,8 +91,17 @@ public class ExpenseRepository: ObservableObject {
}

private func getInvolvedUserIds(oldExpense: Expense, expense: Expense, user: AppUser) -> Set<String> {
Set(oldExpense.splitTo + Array(oldExpense.paidBy.keys) + expense.splitTo + Array(expense.paidBy.keys) +
[user.id, expense.addedBy, expense.updatedBy])
var memberIds = Set<String>()
memberIds.formUnion(oldExpense.splitTo)
memberIds.formUnion(oldExpense.paidBy.keys)
memberIds.formUnion(expense.splitTo)
memberIds.formUnion(expense.paidBy.keys)
memberIds.insert(user.id)
memberIds.insert(expense.addedBy)
if let updatedBy = expense.updatedBy {
memberIds.insert(updatedBy)
}
return memberIds
}

private func addActivityLogsInParallel(for userIds: Set<String>, context: ActivityLogContext) async throws {
Expand Down
2 changes: 1 addition & 1 deletion Data/Data/Repository/GroupRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public class GroupRepository: ObservableObject {
// Change new admin if the old admin leaves the group
if removedMember.id == group.createdBy {
// Create another top member as a new admin
if let newAdmin = group.members.first {
if let newAdmin = group.members.first(where: { $0 != removedMember.id }) {
group.createdBy = newAdmin
}
}
Expand Down
3 changes: 2 additions & 1 deletion Data/Data/Repository/TransactionRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ public class TransactionRepository: ObservableObject {
oldTransaction.updatedBy != transaction.updatedBy || oldTransaction.note != transaction.note ||
oldTransaction.imageUrl != transaction.imageUrl || oldTransaction.reason != transaction.reason ||
oldTransaction.amount != transaction.amount || oldTransaction.date.dateValue() != transaction.date.dateValue() ||
oldTransaction.updatedAt.dateValue() != transaction.updatedAt.dateValue() || oldTransaction.isActive != transaction.isActive
oldTransaction.updatedAt?.dateValue() != transaction.updatedAt?.dateValue() ||
oldTransaction.isActive != transaction.isActive
}

public func updateTransaction(group: Groups, transaction: Transactions, oldTransaction: Transactions,
Expand Down
33 changes: 26 additions & 7 deletions Data/Data/Repository/UserRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class UserRepository: ObservableObject {
} else {
LogD("UserRepository: \(#function) User does not exist. Adding new user.")
try await store.addUser(user: user)
LogD("UserRepository: \(#function) User stored successfully.")
return user
}
}
Expand Down Expand Up @@ -75,17 +76,35 @@ public class UserRepository: ObservableObject {
}
}

public func deleteUser(id: String) async throws {
try await store.deactivateUserAfterDelete(userId: id)
public func deleteUser(user: AppUser) async throws {
do {
try await store.deactivateUserAfterDelete(userId: user.id)
try await deleteUserFromAuth()
} catch {
// Rollback deactivation if auth deletion fails
_ = try await store.updateUser(user: user)
throw error
}
}

private func deleteUserFromAuth() async throws {
guard let user = FirebaseProvider.auth.currentUser else {
throw NSError(domain: "AuthError", code: -1, userInfo: [NSLocalizedDescriptionKey: "No logged-in user found"])
}
try await user.delete()
}

public func updateDeviceFcmToken(retryCount: Int = 3) {
Task {
FirebaseProvider.auth.currentUser?.delete { error in
if let error {
LogE("UserRepository: \(#function) Deleting user from Auth failed with error: \(error).")
} else {
LogD("UserRepository: \(#function) User deactivated.")
guard let userId = preference.user?.id, let fcmToken = preference.fcmToken else { return }

do {
try await store.updateUserDeviceFcmToken(userId: userId, fcmToken: fcmToken)
LogI("AppDelegate: \(#function) Device fcm token updated successfully.")
} catch {
LogE("AppDelegate: \(#function) Failed to update device fcm token: \(error).")
if retryCount > 0 {
updateDeviceFcmToken(retryCount: retryCount - 1)
}
}
}
Expand Down
Loading

0 comments on commit 3a51c7b

Please sign in to comment.