Skip to content

Commit

Permalink
Improve Expense list (#246)
Browse files Browse the repository at this point in the history
* WIP: improve sorting

* Update emulator data

* Add progress indicator at the end of expense list

* Add expenses cubit tests

* Test loading number of expenses divisible by page size

* Improve expenses loading state

* WIP: implement expense filtering

* Refactor expenses cubit

* Style processing state

* Fix filter state after loading more

* Revert adding 15 expenses

* Add finalizable expense to import data

* Improve scroll behaviour

* Fix functions and add updater

* Undo multiadd

* Fix variable redeclaration

* Fix all tests

* Update emulator test data

* WIP: create userExpense

* Introduce UserExpense

* Simplify user expense definition

* Remove UserExpense

* Add data synchronizer

* Fix unit tests

* Fix integration tests

* Uncomment more tests

* Remove wrong testing data
  • Loading branch information
ANDREYDEN authored Nov 12, 2023
1 parent e641d89 commit 6e64cc3
Show file tree
Hide file tree
Showing 49 changed files with 869 additions and 453 deletions.
79 changes: 32 additions & 47 deletions admin/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,56 +7,27 @@ const db = admin.firestore();
const auth = admin.auth();

(async () => {
// const groupId = '0i9Ni8Bz5qUk7yBIj5Cu'
const groupId = 'Kc052CgJBU4VNyBxpxCQ'
const groupReference = await db.collection('groups').doc(groupId).get()
const group = groupReference.data()
const expenses = await db.collection('expenses').get()
console.log(`Found ${expenses.docs.length} expenses...`);

const expenseId = '6SMoDPBvcyNU6X8gJ7zf'
const expenseReference = await db.collection('expenses').doc(expenseId).get()
const expense = expenseReference.data()

const authorId = expense.authorUid
for (const assigneeId of expense.assigneeIds) {
if (assigneeId == authorId) continue

const payments = await getPayments(groupId, authorId, assigneeId)

const previousDebt = payments
.find(p => p.relatedExpense?.id === expenseId)
.value

const currantDebt = getTotalDebt(expense, assigneeId)

const paymentValueDifferenceByAuthor = currantDebt - previousDebt
const payerId = paymentValueDifferenceByAuthor > 0 ? authorId : assigneeId
const receiverId = paymentValueDifferenceByAuthor > 0 ? assigneeId : authorId

const payment = createPayment(
Math.abs(paymentValueDifferenceByAuthor),
`Debt adjustment for expense "${expense.name}"`,
payerId,
receiverId,
group,
groupId
)

group.balance[receiverId][payerId] += Math.abs(paymentValueDifferenceByAuthor)
group.balance[payerId][receiverId] -= Math.abs(paymentValueDifferenceByAuthor)
for (const expenseDoc of expenses.docs) {
console.log(`Updating expense ${expenseDoc.id}...`);
const expense = expenseDoc.data()
try {
const relatedUids = new Set([...expense.assigneeIds, expense.authorUid]).values()
for (const uid of relatedUids) {
const stage = calculateStage(expense, uid)
await db.collection('users').doc(uid)
.collection('expenses').doc(expenseDoc.id)
.set({ ...expense, stage })
}

console.log(payment);
await db.collection('payments').add(payment)
// console.log(expense);
// await expenseDoc.ref.set(expense)
} catch (error) {
console.log(`Could not update expense ${expenseDoc.id}: `, error);
}
}

console.log('new group: ', group);
await groupReference.ref.set(group)

// const userId = 'VVcVfnsRNqbZtvW4NG173yMfxd72'
// const userReference = await db.collection('users').doc(userId).get()
// const user = {
// uid: userId,
// ...userReference.data()
// }
})();

function addUserToGroup(group, user) {
Expand Down Expand Up @@ -207,4 +178,18 @@ async function getPayments(groupId, fromId, toId) {
.get()

return payments.docs.map(p => p.data())
}

function calculateStage(expense, assigneeId) {
if (expense.finalizedDate != null) return 2

if (expense.unmarkedAssigneeIds.includes(assigneeId)) return 0

return 1
}

function getHasItemsDeniedByAll(expense) {
return expense.items.some(item => {
return item.assignees.every(a => a.parts === 0)
})
}
2 changes: 1 addition & 1 deletion emulator_data/auth_export/accounts.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"7IE7Zua6OBqK4YEGiF61TMo9agHL","createdAt":"1654454042657","lastLoginAt":"1654454253238","displayName":"Third","photoUrl":"https://picsum.photos/300","passwordHash":"fakeHash:salt=fakeSaltESg0Rz1lsdgpiaOAJOL5:password=Qweqwe1!","salt":"fakeSaltESg0Rz1lsdgpiaOAJOL5","passwordUpdatedAt":1695944415918,"providerUserInfo":[{"providerId":"password","email":"third@example.com","federatedId":"third@example.com","rawId":"third@example.com","displayName":"Third","photoUrl":"https://picsum.photos/300"}],"validSince":"1695944415","email":"third@example.com","emailVerified":false,"disabled":false},{"localId":"92HncVCBiegd6rJP0Vt1gjVPz2cM","createdAt":"1650417583240","lastLoginAt":"1695944471554","displayName":"Tester","photoUrl":"https://picsum.photos/200","passwordHash":"fakeHash:salt=fakeSaltIiD2sStkbTThNgabfTRB:password=Qweqwe1!","salt":"fakeSaltIiD2sStkbTThNgabfTRB","passwordUpdatedAt":1695944415918,"providerUserInfo":[{"providerId":"password","email":"user@example.com","federatedId":"user@example.com","rawId":"user@example.com","displayName":"Tester","photoUrl":"https://picsum.photos/200"}],"validSince":"1695944415","email":"user@example.com","emailVerified":false,"disabled":false,"lastRefreshAt":"2023-09-28T23:41:11.554Z"},{"localId":"Sp0Vj5sxPqxhigfcEcMhLjBABKys","createdAt":"1650417747662","lastLoginAt":"1695696494701","displayName":"Admin","photoUrl":"https://picsum.photos/100","passwordHash":"fakeHash:salt=fakeSalteQRBxA7ZoPpuLT6a4FpJ:password=Qweqwe1!","salt":"fakeSalteQRBxA7ZoPpuLT6a4FpJ","passwordUpdatedAt":1695944415918,"providerUserInfo":[{"providerId":"password","email":"admin@example.com","federatedId":"admin@example.com","rawId":"admin@example.com","displayName":"Admin","photoUrl":"https://picsum.photos/100"}],"validSince":"1695944415","email":"admin@example.com","emailVerified":false,"disabled":false},{"localId":"bqWC1RRnBBncEDJjb0qqq9q7lQUr","createdAt":"1695696328310","lastLoginAt":"1695696444840","displayName":"John Doe","photoUrl":"https://picsum.photos/id/22/100","passwordHash":"fakeHash:salt=fakeSaltCIpVFXmFNFHBXP9yUoXk:password=Qweqwe1!","salt":"fakeSaltCIpVFXmFNFHBXP9yUoXk","passwordUpdatedAt":1695944415918,"providerUserInfo":[{"providerId":"password","email":"j.doe@example.com","federatedId":"j.doe@example.com","rawId":"j.doe@example.com","displayName":"John Doe","photoUrl":"https://picsum.photos/id/22/100"}],"validSince":"1695944415","email":"j.doe@example.com","emailVerified":false,"disabled":false}]}
{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"7IE7Zua6OBqK4YEGiF61TMo9agHL","createdAt":"1654454042657","lastLoginAt":"1654454253238","displayName":"Third","photoUrl":"https://picsum.photos/300","passwordHash":"fakeHash:salt=fakeSaltESg0Rz1lsdgpiaOAJOL5:password=Qweqwe1!","salt":"fakeSaltESg0Rz1lsdgpiaOAJOL5","passwordUpdatedAt":1698195790521,"providerUserInfo":[{"providerId":"password","email":"third@example.com","federatedId":"third@example.com","rawId":"third@example.com","displayName":"Third","photoUrl":"https://picsum.photos/300"}],"validSince":"1698195790","email":"third@example.com","emailVerified":false,"disabled":false},{"localId":"92HncVCBiegd6rJP0Vt1gjVPz2cM","createdAt":"1650417583240","lastLoginAt":"1698195914227","displayName":"Tester","photoUrl":"https://picsum.photos/200","passwordHash":"fakeHash:salt=fakeSaltIiD2sStkbTThNgabfTRB:password=Qweqwe1!","salt":"fakeSaltIiD2sStkbTThNgabfTRB","passwordUpdatedAt":1698195790521,"providerUserInfo":[{"providerId":"password","email":"user@example.com","federatedId":"user@example.com","rawId":"user@example.com","displayName":"Tester","photoUrl":"https://picsum.photos/200"}],"validSince":"1698195790","email":"user@example.com","emailVerified":false,"disabled":false,"lastRefreshAt":"2023-10-25T01:05:14.228Z"},{"localId":"Sp0Vj5sxPqxhigfcEcMhLjBABKys","createdAt":"1650417747662","lastLoginAt":"1698195933819","displayName":"Admin","photoUrl":"https://picsum.photos/100","passwordHash":"fakeHash:salt=fakeSalteQRBxA7ZoPpuLT6a4FpJ:password=Qweqwe1!","salt":"fakeSalteQRBxA7ZoPpuLT6a4FpJ","passwordUpdatedAt":1698195790521,"providerUserInfo":[{"providerId":"password","email":"admin@example.com","federatedId":"admin@example.com","rawId":"admin@example.com","displayName":"Admin","photoUrl":"https://picsum.photos/100"}],"validSince":"1698195790","email":"admin@example.com","emailVerified":false,"disabled":false,"lastRefreshAt":"2023-10-25T01:05:33.819Z"},{"localId":"bqWC1RRnBBncEDJjb0qqq9q7lQUr","createdAt":"1695696328310","lastLoginAt":"1695696444840","displayName":"John Doe","photoUrl":"https://picsum.photos/id/22/100","passwordHash":"fakeHash:salt=fakeSaltCIpVFXmFNFHBXP9yUoXk:password=Qweqwe1!","salt":"fakeSaltCIpVFXmFNFHBXP9yUoXk","passwordUpdatedAt":1698195790521,"providerUserInfo":[{"providerId":"password","email":"j.doe@example.com","federatedId":"j.doe@example.com","rawId":"j.doe@example.com","displayName":"John Doe","photoUrl":"https://picsum.photos/id/22/100"}],"validSince":"1698195790","email":"j.doe@example.com","emailVerified":false,"disabled":false}]}
Binary file not shown.
Binary file modified emulator_data/firestore_export/all_namespaces/all_kinds/output-0
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion emulator_data_testing/auth_export/accounts.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"7IE7Zua6OBqK4YEGiF61TMo9agHL","createdAt":"1654454042657","lastLoginAt":"1679538347325","displayName":"John","photoUrl":"https://picsum.photos/id/338/100","passwordHash":"fakeHash:salt=fakeSaltyUiyIWr4ZqQrp1nzxltk:password=Qweqwe1!","salt":"fakeSaltyUiyIWr4ZqQrp1nzxltk","passwordUpdatedAt":1679519351308,"providerUserInfo":[{"providerId":"password","email":"john@example.com","federatedId":"john@example.com","rawId":"john@example.com","displayName":"John","photoUrl":"https://picsum.photos/id/338/100"}],"validSince":"1679519351","email":"john@example.com","emailVerified":false,"disabled":false,"lastRefreshAt":"2023-03-23T02:25:47.326Z","initialEmail":"third@example.com","customAttributes":""},{"localId":"92HncVCBiegd6rJP0Vt1gjVPz2cM","createdAt":"1650417583240","lastLoginAt":"1679532068271","displayName":"Isabel","photoUrl":"https://picsum.photos/id/342/100","passwordHash":"fakeHash:salt=fakeSaltdQhWGRJk5dYL7OLmJKKa:password=Qweqwe1!","salt":"fakeSaltdQhWGRJk5dYL7OLmJKKa","passwordUpdatedAt":1679519360121,"providerUserInfo":[{"providerId":"password","email":"isabel@example.com","federatedId":"isabel@example.com","rawId":"isabel@example.com","displayName":"Isabel","photoUrl":"https://picsum.photos/id/342/100"}],"validSince":"1679519360","email":"isabel@example.com","emailVerified":false,"disabled":false,"initialEmail":"user@example.com","customAttributes":"","lastRefreshAt":"2023-03-23T00:41:08.272Z"},{"localId":"Sp0Vj5sxPqxhigfcEcMhLjBABKys","createdAt":"1650417747662","lastLoginAt":"1679532639532","displayName":"Alice","photoUrl":"https://picsum.photos/id/237/100","passwordHash":"fakeHash:salt=fakeSaltbedHQHO9MWHasS5t2S4E:password=Qweqwe1!","salt":"fakeSaltbedHQHO9MWHasS5t2S4E","passwordUpdatedAt":1679519366497,"providerUserInfo":[{"providerId":"password","email":"alice@example.com","federatedId":"alice@example.com","rawId":"alice@example.com","displayName":"Alice","photoUrl":"https://picsum.photos/id/237/100"}],"validSince":"1679519366","email":"alice@example.com","emailVerified":false,"disabled":false,"initialEmail":"admin@example.com","customAttributes":"","lastRefreshAt":"2023-03-23T00:50:39.533Z"}]}
{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"7IE7Zua6OBqK4YEGiF61TMo9agHL","createdAt":"1654454042657","lastLoginAt":"1679538347325","displayName":"John","photoUrl":"https://picsum.photos/id/338/100","passwordHash":"fakeHash:salt=fakeSaltyUiyIWr4ZqQrp1nzxltk:password=Qweqwe1!","salt":"fakeSaltyUiyIWr4ZqQrp1nzxltk","passwordUpdatedAt":1699408202871,"providerUserInfo":[{"providerId":"password","email":"john@example.com","federatedId":"john@example.com","rawId":"john@example.com","displayName":"John","photoUrl":"https://picsum.photos/id/338/100"}],"validSince":"1699408202","email":"john@example.com","emailVerified":false,"disabled":false},{"localId":"92HncVCBiegd6rJP0Vt1gjVPz2cM","createdAt":"1650417583240","lastLoginAt":"1679532068271","displayName":"Isabel","photoUrl":"https://picsum.photos/id/342/100","passwordHash":"fakeHash:salt=fakeSaltdQhWGRJk5dYL7OLmJKKa:password=Qweqwe1!","salt":"fakeSaltdQhWGRJk5dYL7OLmJKKa","passwordUpdatedAt":1699408202872,"providerUserInfo":[{"providerId":"password","email":"isabel@example.com","federatedId":"isabel@example.com","rawId":"isabel@example.com","displayName":"Isabel","photoUrl":"https://picsum.photos/id/342/100"}],"validSince":"1699408202","email":"isabel@example.com","emailVerified":false,"disabled":false},{"localId":"Sp0Vj5sxPqxhigfcEcMhLjBABKys","createdAt":"1650417747662","lastLoginAt":"1679532639532","displayName":"Alice","photoUrl":"https://picsum.photos/id/237/100","passwordHash":"fakeHash:salt=fakeSaltbedHQHO9MWHasS5t2S4E:password=Qweqwe1!","salt":"fakeSaltbedHQHO9MWHasS5t2S4E","passwordUpdatedAt":1699408202872,"providerUserInfo":[{"providerId":"password","email":"alice@example.com","federatedId":"alice@example.com","rawId":"alice@example.com","displayName":"Alice","photoUrl":"https://picsum.photos/id/237/100"}],"validSince":"1699408202","email":"alice@example.com","emailVerified":false,"disabled":false}]}
8 changes: 4 additions & 4 deletions emulator_data_testing/firebase-export-metadata.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"version": "11.15.0",
"version": "12.4.2",
"firestore": {
"version": "1.15.1",
"version": "1.18.1",
"path": "firestore_export",
"metadata_file": "firestore_export/firestore_export.overall_export_metadata"
},
"auth": {
"version": "11.15.0",
"version": "12.4.2",
"path": "auth_export"
},
"storage": {
"version": "11.15.0",
"version": "12.4.2",
"path": "storage_export"
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
4 changes: 4 additions & 0 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ service cloud.firestore {
allow read, write: if request.auth != null;
}

match /users/{userId}/expenses/{expenseId} {
allow read;
}

function fieldsChanged(fields) {
return debug(request.resource.data.diff(resource.data).changedKeys()).hasOnly(fields);
}
Expand Down
12 changes: 9 additions & 3 deletions functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { createUserDoc } from './src/functions/userManagement/createUserDoc'
import { removeUserFromGroups } from './src/functions/userManagement/removeUserFromGroups'
import { updateUser } from './src/functions/userManagement/updateUser'
import { UserData } from './src/types/userData'
import { Timestamp } from 'firebase-admin/firestore'
import { updateUserExpenses } from './src/functions/docSync/updateUserExpenses'

admin.initializeApp()

Expand All @@ -26,13 +28,18 @@ export const setTimestampOnPaymentCreation = functions.firestore

export const handleExpenseUpdate = functions.firestore
.document('expenses/{expenseId}')
.onUpdate(async (change, _) => {
.onWrite(async (change, _) => {
const oldExpense = change.before.data()
const newExpense = change.after.data()

await updateUserExpenses(change)

if (!newExpense || !oldExpense) return

if (oldExpense.finalizedDate !== newExpense.finalizedDate) {
const oldFinalizedTimestamp = oldExpense.finalizedDate as (Timestamp | null)
const newFinalizedTimestamp = newExpense.finalizedDate as (Timestamp | null)

if (oldFinalizedTimestamp?.toMillis != newFinalizedTimestamp?.toMillis) {
if (newExpense.finalizedDate) {
await notifyWhenExpenseFinalized(change.after)
} else {
Expand All @@ -53,7 +60,6 @@ export const getReceiptData = functions

return analyzeReceipt(
data.receiptUrl,
data.isWalmart,
data.storeName,
data.withNameImprovement
)
Expand Down
28 changes: 28 additions & 0 deletions functions/src/functions/docSync/updateUserExpenses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { firestore } from "firebase-admin";
import { DocumentSnapshot } from "firebase-admin/firestore";
import { Change } from "firebase-functions/v1";
import { calculateStage } from "../../utils/expenseUtils";

export async function updateUserExpenses(change: Change<DocumentSnapshot>) {
const expenseData = change.after.data() ?? change.before.data()
const expenseDeleted = !change.after.exists
if (!expenseData) return

const relatedUids = new Set([
...expenseData.assigneeIds,
expenseData.authorUid
]).values()

for (const uid of relatedUids) {
const userExpenseRef = firestore().collection('users').doc(uid).collection('expenses').doc(change.after.id)
if (expenseDeleted) {
await userExpenseRef.delete()
} else {
const stage = calculateStage(expenseData, uid)
await userExpenseRef.set({
...expenseData,
stage
})
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { messaging } from 'firebase-admin'
import { DocumentSnapshot } from 'firebase-admin/firestore'
import { getExpenseNotificationTokens } from './notificationUtils'
import { QueryDocumentSnapshot } from 'firebase-admin/firestore'

export async function notifyWhenExpenseFinalized(expenseSnap: QueryDocumentSnapshot) {
export async function notifyWhenExpenseFinalized(expenseSnap: DocumentSnapshot) {
if (!expenseSnap.exists) {
console.log(`Expense ${expenseSnap.id} no longer exists`)
return
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { messaging } from 'firebase-admin'
import { DocumentSnapshot } from 'firebase-admin/firestore'
import { getExpenseNotificationTokens } from './notificationUtils'
import { QueryDocumentSnapshot } from 'firebase-admin/firestore'

export async function notifyWhenExpenseReverted(expenseSnap: QueryDocumentSnapshot) {
export async function notifyWhenExpenseReverted(expenseSnap: DocumentSnapshot) {
if (!expenseSnap.exists) {
console.log(`Expense ${expenseSnap.id} no longer exists`)
return
Expand Down
7 changes: 7 additions & 0 deletions functions/src/utils/expenseUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function calculateStage(expense: any, assigneeId: string) {
if (expense.finalizedDate != null) return 2

if (expense.unmarkedAssigneeIds.includes(assigneeId)) return 0

return 1
}
Loading

0 comments on commit 6e64cc3

Please sign in to comment.