Skip to content

Commit 4c808c4

Browse files
feat: Implement BullMQ job queueing and worker services for notifications, receipt processing, and payment webhooks, and integrate Expo push notifications.
1 parent 53a0d37 commit 4c808c4

6 files changed

Lines changed: 56 additions & 72 deletions

File tree

apps/backend/src/jobs/jobs-queue.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export class JobsQueueService implements OnModuleDestroy {
120120
url: this.config.redisUrl,
121121
lazyConnect: true,
122122
maxRetriesPerRequest: null,
123-
enableOfflineQueue: false,
123+
enableOfflineQueue: true,
124124
connectionName: 'fairshare:jobs-queue',
125125
retryStrategy: (times: number) => {
126126
if (times > MAX_REDIS_RETRIES) {
@@ -142,7 +142,7 @@ export class JobsQueueService implements OnModuleDestroy {
142142
const probe = new Redis(this.config.redisUrl, {
143143
lazyConnect: true,
144144
maxRetriesPerRequest: 1,
145-
enableOfflineQueue: false,
145+
enableOfflineQueue: true,
146146
connectTimeout: 750,
147147
retryStrategy: () => null,
148148
});

apps/backend/src/jobs/jobs-worker.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class JobsWorkerService implements OnModuleInit, OnModuleDestroy {
3333
url: this.config.redisUrl,
3434
lazyConnect: true,
3535
maxRetriesPerRequest: null,
36-
enableOfflineQueue: false,
36+
enableOfflineQueue: true,
3737
connectionName: 'fairshare:jobs-worker',
3838
retryStrategy: (times: number) => {
3939
if (times > MAX_REDIS_RETRIES) {
@@ -88,7 +88,7 @@ export class JobsWorkerService implements OnModuleInit, OnModuleDestroy {
8888
const probe = new Redis(this.config.redisUrl, {
8989
lazyConnect: true,
9090
maxRetriesPerRequest: 1,
91-
enableOfflineQueue: false,
91+
enableOfflineQueue: true,
9292
connectTimeout: 750,
9393
retryStrategy: () => null,
9494
});

apps/backend/src/notifications/notifications.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ export class NotificationsService implements OnModuleInit, OnModuleDestroy {
213213
const client = new Redis(this.config.redisUrl, {
214214
lazyConnect: true,
215215
maxRetriesPerRequest: 1,
216-
enableOfflineQueue: false,
216+
enableOfflineQueue: true,
217217
connectionName,
218218
retryStrategy: (times: number) => {
219219
if (times > MAX_REDIS_RETRIES) {
@@ -239,7 +239,7 @@ export class NotificationsService implements OnModuleInit, OnModuleDestroy {
239239
const probe = new Redis(this.config.redisUrl, {
240240
lazyConnect: true,
241241
maxRetriesPerRequest: 1,
242-
enableOfflineQueue: false,
242+
enableOfflineQueue: true,
243243
connectTimeout: 750,
244244
retryStrategy: () => null,
245245
});

apps/backend/src/redis/redis.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const MAX_RETRY_DELAY_MS = 1000;
2020
const client = new Redis(config.redisUrl, {
2121
lazyConnect: true,
2222
maxRetriesPerRequest: 1,
23-
enableOfflineQueue: false,
23+
enableOfflineQueue: true,
2424
connectionName: 'fairshare:cache',
2525
retryStrategy: (times) => {
2626
if (times > MAX_REDIS_RETRIES) {

apps/web/app/dashboard/page.tsx

Lines changed: 44 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ import { DashboardLayout } from '../../src/components/layout';
55
import { backendFetch } from '../../src/lib/backend';
66

77
type Group = { id: string; name: string; currency: string };
8-
type Balance = { id: string; userId: string; counterpartyUserId: string; amountCents: string };
98

10-
function formatMoney(cents: bigint, currency: string | null): string {
11-
if (!currency) {
12-
return '';
9+
function formatMoney(cents: string | null, currency: string | null): string {
10+
if (!cents || !currency) {
11+
return '$0.00';
1312
}
1413

1514
const amount = Number(cents) / 100;
@@ -18,72 +17,55 @@ function formatMoney(cents: bigint, currency: string | null): string {
1817

1918
export default async function DashboardPage() {
2019
const [me, groups] = await Promise.all([
21-
backendFetch<AuthUserDto>('/users/me').catch(() => null),
22-
backendFetch<Group[]>('/groups').catch(() => [] as Group[]),
20+
backendFetch<AuthUserDto>('/users/me'),
21+
backendFetch<Group[]>('/groups'),
2322
]);
2423

25-
const activeGroup = groups[0] ?? null;
26-
const activeGroupId = activeGroup?.id;
27-
const activeCurrency = activeGroup?.currency ?? null;
28-
29-
const [balances, activity] = await Promise.all([
30-
activeGroupId
31-
? backendFetch<Balance[]>(`/groups/${activeGroupId}/balances`).catch(() => [] as Balance[])
32-
: Promise.resolve([] as Balance[]),
33-
activeGroupId
34-
? backendFetch<ActivityDto[]>(`/groups/${activeGroupId}/activity?cursor=0&limit=5`).catch(() => [] as ActivityDto[])
35-
: Promise.resolve([] as ActivityDto[]),
24+
const [recentActivityData, summary] = await Promise.all([
25+
backendFetch<{ items: ActivityDto[] }>('/activity'),
26+
backendFetch<{ totalBalanceCents: string }>('/groups/summary'),
3627
]);
3728

38-
let oweCents = 0n;
39-
let owedToMeCents = 0n;
40-
if (me) {
41-
for (const bal of balances) {
42-
if (bal.userId !== me.id) continue;
43-
const amt = BigInt(bal.amountCents);
44-
if (amt < 0n) oweCents += -amt;
45-
else owedToMeCents += amt;
46-
}
47-
}
29+
const recentActivity = recentActivityData.items || [];
30+
const totalBalanceCents = summary.totalBalanceCents;
4831

4932
return (
50-
<DashboardLayout>
51-
<div className="space-y-12">
52-
<section className="space-y-4">
53-
<div className="flex items-center justify-between">
54-
<h2 className="text-lg font-bold text-[var(--fs-text-primary)]">Balance summary</h2>
55-
<span className="text-[11px] font-semibold uppercase tracking-[0.12em] text-[var(--fs-text-muted)]">
56-
Updated on refresh
57-
</span>
58-
</div>
59-
<div className="grid gap-6 md:grid-cols-3">
60-
<SummaryCard title="Active groups" value={groups.length} hint="Registered ensembles" />
61-
<SummaryCard
62-
title="Liabilities"
63-
value={formatMoney(oweCents, activeCurrency)}
64-
hint={activeGroupId ? `${activeGroup?.name} · ${activeCurrency}` : 'No active group selected'}
65-
/>
66-
<SummaryCard
67-
title="Assets"
68-
value={formatMoney(owedToMeCents, activeCurrency)}
69-
hint={activeGroupId ? `Shown in ${activeCurrency}` : 'Select a group to see currency-specific totals'}
70-
/>
71-
</div>
72-
</section>
33+
<DashboardLayout user={me}>
34+
<div className="space-y-6">
35+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
36+
<SummaryCard
37+
title="Total Balance"
38+
amount={formatMoney(totalBalanceCents, 'USD')}
39+
trend={Number(totalBalanceCents) >= 0 ? 'up' : 'down'}
40+
description={Number(totalBalanceCents) >= 0 ? 'You are owed' : 'You owe'}
41+
/>
42+
<SummaryCard
43+
title="Active Groups"
44+
amount={groups.length.toString()}
45+
description="Across all categories"
46+
/>
47+
<QuickActions />
48+
</div>
7349

74-
<section className="grid gap-12 lg:grid-cols-[1fr_350px]">
75-
{activeGroupId ? (
76-
<ActivityList groupId={activeGroupId} items={activity} />
77-
) : (
78-
<div className="rounded-3xl border border-[var(--fs-border)] bg-[var(--fs-card)] p-10 text-center shadow-[var(--fs-shadow-soft)]">
79-
<p className="text-xl font-bold text-[var(--fs-text-primary)] mb-2">No active group detected</p>
80-
<p className="text-sm font-medium text-[var(--fs-text-muted)]">
81-
Create or select a group to start tracking balances and activity.
82-
</p>
50+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
51+
<div className="lg:col-span-2">
52+
<ActivityList items={recentActivity} groupId="" />
53+
</div>
54+
<div>
55+
<h2 className="text-xl font-semibold mb-4">Group Balances</h2>
56+
<div className="bg-white rounded-xl shadow-sm p-6">
57+
{groups.map((group) => (
58+
<div key={group.id} className="flex justify-between items-center py-3 border-b last:border-0">
59+
<span className="font-medium">{group.name}</span>
60+
<span className="text-sm text-gray-500">{group.currency}</span>
61+
</div>
62+
))}
63+
{groups.length === 0 && (
64+
<p className="text-sm text-gray-500 text-center py-4">No active groups yet.</p>
65+
)}
8366
</div>
84-
)}
85-
<QuickActions groupId={activeGroupId} />
86-
</section>
67+
</div>
68+
</div>
8769
</div>
8870
</DashboardLayout>
8971
);

apps/web/src/components/dashboard/ActivityList.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ function labelForType(type: ActivityDto['type']): string {
2222
}
2323
}
2424

25-
export function ActivityList({ items, groupId }: { items: ActivityDto[]; groupId: string }) {
25+
export function ActivityList({ items = [], groupId = '' }: { items?: ActivityDto[]; groupId?: string }) {
26+
const safeItems = Array.isArray(items) ? items : [];
27+
2628
return (
2729
<div className={`${glassPanel} p-7`}>
2830
<div className="flex items-center justify-between gap-3 mb-6">
@@ -36,7 +38,7 @@ export function ActivityList({ items, groupId }: { items: ActivityDto[]; groupId
3638
</div>
3739

3840
<div className="space-y-3">
39-
{items.map((item) => (
41+
{safeItems.map((item) => (
4042
<div
4143
key={item.id}
4244
className="flex items-center justify-between gap-3 rounded-xl border border-[var(--fs-border)] bg-[var(--fs-background)]/70 px-4 py-3 hover:border-[var(--fs-primary)] transition-colors"
@@ -54,7 +56,7 @@ export function ActivityList({ items, groupId }: { items: ActivityDto[]; groupId
5456
</div>
5557
))}
5658

57-
{items.length === 0 ? (
59+
{safeItems.length === 0 ? (
5860
<div className="rounded-2xl border border-[var(--fs-border)] bg-[var(--fs-background)]/50 px-6 py-8 text-center">
5961
<p className="text-sm font-semibold text-[var(--fs-text-primary)] mb-1">No activity yet</p>
6062
<p className="text-[12px] font-medium text-[var(--fs-text-muted)]">

0 commit comments

Comments
 (0)