Skip to content

Commit 3766ba5

Browse files
Arun-kushwaha007rootcoderabbitai[bot]CodeRabbit
authored
Fixes #2, Fixes #3, Fixes #5, Fixes #20: unify activity names, currency formatting, and web auth (#22)
* Fixes #2, Fixes #3, Fixes #5, Fixes #20: unify activity names, currency formatting, and web auth * 📝 Add docstrings to `issue-bundle-fixes` Docstrings generation was requested by @Arun-kushwaha007. The following files were modified: * `apps/mobile/app/components/ui/MoneyText.tsx` * `apps/mobile/app/screens/ActivityScreen.tsx` * `apps/mobile/app/screens/AddExpenseScreen.tsx` * `apps/mobile/app/screens/ExpenseDetailScreen.tsx` * `apps/mobile/app/screens/GroupDetailScreen.tsx` * `apps/mobile/app/screens/GuestGroupDetailScreen.tsx` * `apps/mobile/app/screens/HomeScreen.tsx` * `apps/web/app/dashboard/activity/page.tsx` * `apps/web/app/dashboard/groups/[groupId]/page.tsx` * `apps/web/app/dashboard/page.tsx` * `apps/web/app/share/[token]/page.tsx` * `apps/web/src/components/activity/ActivityFeed.tsx` * `apps/web/src/components/dashboard/ActivityList.tsx` * `apps/web/src/components/groups/ExpenseDetailCard.tsx` * `apps/web/src/components/groups/ExpenseRow.tsx` * `apps/web/src/components/groups/ExpenseTable.tsx` * `apps/web/src/components/groups/GroupSummaryPanel.tsx` * `apps/web/src/components/groups/RecurringExpenseList.tsx` * `apps/web/src/components/groups/SettlementList.tsx` * `packages/shared-types/src/index.ts` These files were ignored: * `apps/backend/src/activity/activity.service.spec.ts` These file types are not supported: * `README.md` * fix: improve type safety and input validation - Update activity type casting in backend service for better Prisma compatibility - Refine currency prop type in web component to prevent type mismatches - Add accessibility titles to select elements in recurring expense list - Enhance currency cents input normalization with integer validation in shared types Fixes #2, #3, #5, #20 * fix: apply CodeRabbit auto-fixes Fixed 7 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai> * fix: apply CodeRabbit auto-fixes (#23) Fixed 9 file(s) based on 3 unresolved review comments. Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <noreply@coderabbit.ai> Co-authored-by: Arun Kushwaha <121769639+Arun-kushwaha007@users.noreply.github.com> * 📝 Add docstrings to `issue-bundle-fixes` Docstrings generation was requested by @Arun-kushwaha007. The following files were modified: * `apps/mobile/app/components/ui/MoneyText.tsx` * `apps/mobile/app/screens/ActivityScreen.tsx` * `apps/mobile/app/screens/ExpenseDetailScreen.tsx` * `apps/mobile/app/screens/GuestGroupDetailScreen.tsx` * `apps/mobile/app/screens/HomeScreen.tsx` * `apps/web/app/dashboard/groups/[groupId]/page.tsx` * `apps/web/app/dashboard/page.tsx` * `apps/web/src/components/activity/ActivityFeed.tsx` * `apps/web/src/components/dashboard/ActivityList.tsx` * `apps/web/src/components/groups/GroupSummaryPanel.tsx` * `apps/web/src/components/groups/RecurringExpenseList.tsx` * `apps/web/src/components/groups/SettlementList.tsx` * `packages/shared-types/src/index.ts` These files were kept as they were: * `apps/mobile/app/screens/AddExpenseScreen.tsx` * `apps/mobile/app/screens/GroupDetailScreen.tsx` * `apps/web/app/dashboard/activity/page.tsx` * `apps/web/app/share/[token]/page.tsx` * `apps/web/src/components/groups/ExpenseDetailCard.tsx` * `apps/web/src/components/groups/ExpenseRow.tsx` * `apps/web/src/components/groups/ExpenseTable.tsx` These files were ignored: * `apps/backend/src/activity/activity.service.spec.ts` These file types are not supported: * `README.md` * `packages/shared-types/tsconfig.json` * `tsconfig.base.json` * Fix GitHub Actions build failures and configure gitignore --------- Co-authored-by: root <root@localhost.localdomain> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
1 parent 728aced commit 3766ba5

44 files changed

Lines changed: 654 additions & 829 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,15 @@ skills-lock.json
3535
.turbo
3636
.turbo/*
3737
.agent
38-
.agent/*
38+
.agent/*
39+
full_build.log
40+
backend_build.log
41+
backend_build2.log
42+
backend_build3.log
43+
web_build.log
44+
mobile_build.log
45+
lint.log
46+
.log
47+
.log*
48+
/infra/terraform/.turbo
49+
/infra/terraform/.terraform

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ pnpm dev:mobile
124124
### Web
125125
- Next.js dashboard and marketing pages
126126
- Theme is controlled via `data-theme` and local storage
127+
- Auth tokens for the web app are stored only in httpOnly cookies managed by `apps/web/app/api/auth/*`, `apps/web/src/lib/backend.ts`, and `apps/web/middleware.ts`
128+
- `localStorage` is not part of the web auth flow
127129
- Build: `pnpm --filter web build`
128130

129131
### Mobile
Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,86 @@
11
import { ActivityService } from './activity.service';
22

33
describe('ActivityService', () => {
4-
it('should be defined', () => {
5-
expect(ActivityService).toBeDefined();
4+
it('maps actor and group names for group activity', async () => {
5+
const prisma = {
6+
activity: {
7+
findMany: jest.fn().mockResolvedValue([
8+
{
9+
id: 'activity-1',
10+
groupId: 'group-1',
11+
actorUserId: 'user-1',
12+
type: 'expense_created',
13+
entityId: 'expense-1',
14+
metadata: { totalAmountCents: '1234', currency: 'USD' },
15+
createdAt: new Date('2026-04-07T00:00:00.000Z'),
16+
actor: { name: 'Ava' },
17+
group: { name: 'Trip Fund' },
18+
},
19+
]),
20+
},
21+
user: {
22+
findMany: jest.fn().mockResolvedValue([
23+
{ id: 'user-1', name: 'Ava' },
24+
]),
25+
},
26+
};
27+
28+
const service = new ActivityService(prisma as any);
29+
const result = await service.getGroupActivity('group-1');
30+
31+
expect(prisma.activity.findMany).toHaveBeenCalledWith(
32+
expect.objectContaining({
33+
where: { groupId: 'group-1' },
34+
include: {
35+
actor: { select: { name: true } },
36+
group: { select: { name: true } },
37+
},
38+
}),
39+
);
40+
expect(prisma.user.findMany).toHaveBeenCalledWith({
41+
where: { id: { in: ['user-1'] } },
42+
select: { id: true, name: true },
43+
});
44+
expect(result.items).toEqual([
45+
expect.objectContaining({
46+
actorUserId: 'user-1',
47+
actorName: 'Ava',
48+
groupName: 'Trip Fund',
49+
}),
50+
]);
51+
});
52+
53+
it('keeps responses backward compatible when names are missing', async () => {
54+
const prisma = {
55+
activity: {
56+
findMany: jest.fn().mockResolvedValue([
57+
{
58+
id: 'activity-2',
59+
groupId: 'group-2',
60+
actorUserId: 'user-2',
61+
type: 'member_joined',
62+
entityId: 'member-1',
63+
metadata: {},
64+
createdAt: new Date('2026-04-07T00:00:00.000Z'),
65+
actor: null,
66+
group: null,
67+
},
68+
]),
69+
},
70+
user: {
71+
findMany: jest.fn().mockResolvedValue([]),
72+
},
73+
};
74+
75+
const service = new ActivityService(prisma as any);
76+
const result = await service.getUserActivity('user-2');
77+
78+
expect(result.items[0]).toEqual(
79+
expect.objectContaining({
80+
actorUserId: 'user-2',
81+
actorName: undefined,
82+
groupName: undefined,
83+
}),
84+
);
685
});
786
});

apps/backend/src/activity/activity.service.ts

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,66 @@ import { PrismaService } from '../common/prisma.service';
77
export class ActivityService {
88
constructor(private readonly prisma: PrismaService) {}
99

10+
private enrichMetadata(metadata: Record<string, unknown>, userNamesById: Record<string, string>): Record<string, unknown> {
11+
const payerId = typeof metadata.payerId === 'string' ? metadata.payerId : null;
12+
const receiverId = typeof metadata.receiverId === 'string' ? metadata.receiverId : null;
13+
14+
return {
15+
...metadata,
16+
...(payerId && userNamesById[payerId] ? { payerName: userNamesById[payerId] } : {}),
17+
...(receiverId && userNamesById[receiverId] ? { receiverName: userNamesById[receiverId] } : {}),
18+
};
19+
}
20+
21+
private async mapActivities(events: Array<{
22+
id: string;
23+
groupId: string;
24+
actorUserId: string;
25+
type: ActivityType;
26+
entityId: string;
27+
metadata: Prisma.JsonValue;
28+
createdAt: Date;
29+
actor?: { name: string | null } | null;
30+
group?: { name: string | null } | null;
31+
}>): Promise<ActivityDto[]> {
32+
const userIds = new Set<string>();
33+
34+
events.forEach((event) => {
35+
userIds.add(event.actorUserId);
36+
const metadata = event.metadata as Record<string, unknown>;
37+
if (typeof metadata.payerId === 'string') {
38+
userIds.add(metadata.payerId);
39+
}
40+
if (typeof metadata.receiverId === 'string') {
41+
userIds.add(metadata.receiverId);
42+
}
43+
});
44+
45+
const users = userIds.size
46+
? await this.prisma.user.findMany({
47+
where: { id: { in: [...userIds] } },
48+
select: { id: true, name: true },
49+
})
50+
: [];
51+
const userNamesById = Object.fromEntries(users.map((user) => [user.id, user.name]));
52+
53+
return events.map((event) => {
54+
const metadata = event.metadata as Record<string, unknown>;
55+
56+
return {
57+
id: event.id,
58+
groupId: event.groupId,
59+
actorUserId: event.actorUserId,
60+
actorName: userNamesById[event.actorUserId] ?? event.actor?.name ?? undefined,
61+
groupName: event.group?.name ?? undefined,
62+
type: event.type,
63+
entityId: event.entityId,
64+
metadata: this.enrichMetadata(metadata, userNamesById),
65+
createdAt: event.createdAt.toISOString(),
66+
};
67+
});
68+
}
69+
1070
async log(params: {
1171
groupId: string;
1272
actorUserId: string;
@@ -18,7 +78,7 @@ export class ActivityService {
1878
data: {
1979
groupId: params.groupId,
2080
actorUserId: params.actorUserId,
21-
type: params.type,
81+
type: params.type as any,
2282
entityId: params.entityId,
2383
metadata: (params.metadata ?? {}) as Prisma.InputJsonValue,
2484
},
@@ -38,17 +98,13 @@ export class ActivityService {
3898
orderBy: { createdAt: 'desc' },
3999
skip: safeCursor,
40100
take: safeLimit,
101+
include: {
102+
actor: { select: { name: true } },
103+
group: { select: { name: true } },
104+
},
41105
});
42106

43-
const items = events.map((event) => ({
44-
id: event.id,
45-
groupId: event.groupId,
46-
actorUserId: event.actorUserId,
47-
type: event.type,
48-
entityId: event.entityId,
49-
metadata: event.metadata as Record<string, unknown>,
50-
createdAt: event.createdAt.toISOString(),
51-
}));
107+
const items = await this.mapActivities(events);
52108

53109
return {
54110
items,
@@ -75,21 +131,17 @@ export class ActivityService {
75131
orderBy: { createdAt: 'desc' },
76132
skip: safeCursor,
77133
take: safeLimit,
134+
include: {
135+
actor: { select: { name: true } },
136+
group: { select: { name: true } },
137+
},
78138
});
79139

80-
const items = events.map((event) => ({
81-
id: event.id,
82-
groupId: event.groupId,
83-
actorUserId: event.actorUserId,
84-
type: event.type,
85-
entityId: event.entityId,
86-
metadata: event.metadata as Record<string, unknown>,
87-
createdAt: event.createdAt.toISOString(),
88-
}));
140+
const items = await this.mapActivities(events);
89141

90142
return {
91143
items,
92144
nextCursor: events.length === safeLimit ? safeCursor + safeLimit : null,
93145
};
94146
}
95-
}
147+
}
Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { CurrencyCode, formatCurrencyFromCents } from '@fairshare/shared-types';
2+
13
export const sumMoney = (values: Array<bigint | number | string>): bigint =>
24
values.reduce<bigint>((acc, value) => acc + BigInt(value), 0n);
35

@@ -7,11 +9,5 @@ export const assertMoneyEquality = (left: bigint, right: bigint, message = 'Mone
79
}
810
};
911

10-
export const formatMoney = (amountCents: bigint | number | string, currency = 'USD'): string => {
11-
const value = Number(BigInt(amountCents)) / 100;
12-
return new Intl.NumberFormat('en-US', {
13-
style: 'currency',
14-
currency,
15-
maximumFractionDigits: 2,
16-
}).format(value);
17-
};
12+
export const formatMoney = (amountCents: bigint | number | string, currency: CurrencyCode = 'USD'): string =>
13+
formatCurrencyFromCents(amountCents, currency);

apps/backend/src/groups/groups.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ export class GroupsService {
125125
currency: group.currency as 'USD' | 'EUR' | 'INR',
126126
createdBy: group.createdBy,
127127
createdAt: group.createdAt.toISOString(),
128+
shareEnabled: group.shareEnabled,
129+
shareToken: group.shareToken,
128130
defaultSplitPreference: this.parseDefaultSplitPreference(
129131
group.defaultSplitType,
130132
group.defaultSplitConfig,
@@ -500,6 +502,8 @@ export class GroupsService {
500502
currency: group.currency as 'USD' | 'EUR' | 'INR',
501503
createdBy: group.createdBy,
502504
createdAt: group.createdAt.toISOString(),
505+
shareEnabled: group.shareEnabled,
506+
shareToken: group.shareToken,
503507
defaultSplitPreference: this.parseDefaultSplitPreference(
504508
group.defaultSplitType,
505509
group.defaultSplitConfig,

apps/backend/src/receipts/receipts.service.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Injectable, NotFoundException } from '@nestjs/common';
1+
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
22
import { randomUUID } from 'crypto';
33
import { PresignedReceiptUrlResponseDto } from '@fairshare/shared-types';
44
import { PrismaService } from '../common/prisma.service';
@@ -13,6 +13,27 @@ const RECEIPT_MIME_TYPES: Record<ReceiptExtension, string> = {
1313
webp: 'image/webp',
1414
};
1515

16+
@Injectable()
17+
export class ReceiptsService {
18+
constructor(
19+
private readonly prisma: PrismaService,
20+
private readonly jobsQueue: JobsQueueService,
21+
private readonly s3: S3Service,
22+
) {}
23+
24+
async createUploadUrl(
25+
expenseId: string,
26+
dto: CreateReceiptUrlDto,
27+
): Promise<PresignedReceiptUrlResponseDto> {
28+
const expense = await this.prisma.expense.findUnique({
29+
where: { id: expenseId },
30+
select: { groupId: true },
31+
});
32+
33+
if (!expense) {
34+
throw new NotFoundException('Expense not found');
35+
}
36+
1637
const extension = (dto.extension ?? 'jpg').toLowerCase() as ReceiptExtension;
1738
if (!RECEIPT_MIME_TYPES[extension]) {
1839
throw new BadRequestException(`Unsupported file extension: ${extension}`);

apps/mobile/.turbo/turbo-build.log

Lines changed: 0 additions & 4 deletions
This file was deleted.

apps/mobile/.turbo/turbo-lint.log

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)