Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
754d979
claude.md
antonio-ivanovski Jan 16, 2026
035a977
playwright skill
antonio-ivanovski Jan 16, 2026
464b39d
install playwright
antonio-ivanovski Jan 16, 2026
211b3f4
ci unit tests
antonio-ivanovski Jan 16, 2026
333bf5f
ci playwright
antonio-ivanovski Jan 16, 2026
8633a5f
add end-to-end test for group creation happy path
antonio-ivanovski Jan 16, 2026
eacdeb5
feat: implement calculateNextDate function for handling daily, weekly…
antonio-ivanovski Jan 17, 2026
941bc4a
remove serena
antonio-ivanovski Jan 19, 2026
169ad76
use arm for CI, better perf ~20%
antonio-ivanovski Jan 19, 2026
73d4453
replace claude with agents
antonio-ivanovski Jan 24, 2026
a9c5c8e
openspec
antonio-ivanovski Jan 24, 2026
1cf06fd
openspec proposal
antonio-ivanovski Jan 24, 2026
c8f3143
initial group sync impl
antonio-ivanovski Jan 24, 2026
33f6ac2
initial group sync impl + group managemnt refactor
antonio-ivanovski Jan 24, 2026
6c49d6c
split context
antonio-ivanovski Jan 24, 2026
c917215
fix e2e tests
antonio-ivanovski Jan 24, 2026
918b9f6
more e2e fixes
antonio-ivanovski Jan 24, 2026
aa79f5d
more e2e
antonio-ivanovski Jan 25, 2026
c0bafbf
rename visitor to syncProfile
antonio-ivanovski Jan 25, 2026
6c3a576
fix ssr issue
antonio-ivanovski Jan 25, 2026
40b2037
reorder
antonio-ivanovski Jan 25, 2026
9b7c2cf
cleanup archive sync proposal
antonio-ivanovski Jan 25, 2026
ab94c29
consolidate specs
antonio-ivanovski Jan 25, 2026
f3766ec
update readme
antonio-ivanovski Jan 25, 2026
f4aa544
update email template
antonio-ivanovski Jan 25, 2026
ae195c6
simplify creation of sync profile
antonio-ivanovski Jan 25, 2026
aea3885
remove syncExisting preference from group sync model and specifications
antonio-ivanovski Jan 25, 2026
9b1bdde
prisma auth adapter
antonio-ivanovski Jan 25, 2026
e6f21dd
remove unused
antonio-ivanovski Jan 25, 2026
1e2ceed
remove openspec
antonio-ivanovski Jan 25, 2026
53ae857
ignore openspec
antonio-ivanovski Jan 25, 2026
3d1e5fb
prettier
antonio-ivanovski Jan 25, 2026
a009f56
remove unused
antonio-ivanovski Jan 25, 2026
6fabe5c
remove
antonio-ivanovski Jan 25, 2026
9fdbc13
string text resources
antonio-ivanovski Jan 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .agent/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Architecture

## Data Model (Prisma)

```
Group (id, name, currency, currencyCode)
└── Participant (id, name)
└── Expense (id, title, amount, expenseDate, splitMode, isReimbursement)
├── paidBy → Participant
├── paidFor → ExpensePaidFor[] (participantId, shares)
├── Category (id, grouping, name)
├── ExpenseDocument[] (url, width, height)
└── RecurringExpenseLink (nextExpenseCreatedAt)
└── Activity (time, activityType, data) - audit log
```

### Split Modes

- `EVENLY`: Divide equally, `shares` = 1 per participant
- `BY_SHARES`: Proportional, e.g., shares 2:1:1 = 50%:25%:25%
- `BY_PERCENTAGE`: Basis points (10000 = 100%), e.g., 2500 = 25%
- `BY_AMOUNT`: Direct cents, `shares` = exact amount owed

### Calculations (src/lib/balances.ts)

```typescript
// BY_PERCENTAGE: (expense.amount * shares) / 10000
// BY_SHARES: (expense.amount * shares) / totalShares
// BY_AMOUNT: shares directly
// Rounding: Math.round() at the end
```

## Directory Details

### src/app/

Next.js App Router. Pages, layouts, Server Actions. Group pages under `groups/[groupId]/`.

### src/components/

Reusable components. shadcn/UI primitives in `ui/`. Feature components at root.

### src/trpc/

- `init.ts` - tRPC config, SuperJSON transformer
- `routers/_app.ts` - Root router composition
- `routers/groups/` - Group domain (expenses, balances, stats, activities)
- `routers/categories/` - Category CRUD

### src/lib/

- `api.ts` - Database operations (createExpense, updateExpense, etc.)
- `balances.ts` - Balance calculation logic
- `totals.ts` - Expense total calculations
- `schemas.ts` - Zod validation schemas
- `prisma.ts` - Prisma client singleton
- `featureFlags.ts` - Feature toggles (S3 docs, receipt scanning)

## tRPC Router Hierarchy

```
appRouter
├── groups
│ ├── get, getDetails, list, create, update
│ ├── expenses (list, get, create, update, delete)
│ ├── balances (list)
│ ├── stats (get)
│ └── activities (list)
└── categories
└── list
```

API calls: `trpc.groups.expenses.create()`, `trpc.groups.balances.list()`, etc.

## Feature Flags

Env vars for optional features:

- `NEXT_PUBLIC_ENABLE_EXPENSE_DOCUMENTS` - S3 image uploads
- `NEXT_PUBLIC_ENABLE_RECEIPT_EXTRACT` - GPT-4V receipt scanning
- `NEXT_PUBLIC_ENABLE_CATEGORY_EXTRACT` - AI category suggestions
119 changes: 119 additions & 0 deletions .agent/database.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Database

## Setup

```bash
./scripts/start-local-db.sh # Start PostgreSQL container
npx prisma migrate dev # Run migrations
npx prisma studio # GUI for database
npx prisma generate # Regenerate client after schema changes
```

## Prisma Client Singleton

```typescript
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client'

const globalForPrisma = global as unknown as { prisma: PrismaClient }

export const prisma = globalForPrisma.prisma || new PrismaClient()

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
```

Dev mode uses global singleton to survive hot reload.

## Schema Changes

1. Edit `prisma/schema.prisma`
2. Run `npx prisma migrate dev --name descriptive_name`
3. Commit migration file + schema changes together

## Query Patterns

### Create with Relations

```typescript
// src/lib/api.ts - createExpense
await prisma.expense.create({
data: {
groupId,
title,
amount,
paidById: paidBy,
splitMode,
expenseDate,
paidFor: {
createMany: {
data: paidFor.map(({ participant, shares }) => ({
participantId: participant,
shares,
})),
},
},
},
})
```

### Query with Includes

```typescript
// Expenses with payer and split details
await prisma.expense.findMany({
where: { groupId },
include: {
paidBy: true,
paidFor: { include: { participant: true } },
category: true,
},
orderBy: [{ expenseDate: 'desc' }, { createdAt: 'desc' }],
})
```

### Update with Nested Operations

```typescript
// Update expense and replace paidFor entries
await prisma.expense.update({
where: { id: expenseId },
data: {
title,
amount,
paidFor: {
deleteMany: {}, // Remove all existing
createMany: { data: newPaidFor },
},
},
})
```

## Transactions

Used for atomic operations:

```typescript
// src/lib/api.ts - createRecurringExpenses
await prisma.$transaction(async (tx) => {
const expense = await tx.expense.create({ data: expenseData })
await tx.recurringExpenseLink.update({
where: { id: linkId },
data: { nextExpenseCreatedAt: nextDate },
})
return expense
})
```

## Amount Storage

All monetary values stored as **integers in cents**:

- `100` = $1.00
- `15050` = $150.50

Split shares vary by mode:

- `EVENLY`: 1 per participant
- `BY_SHARES`: Weight integers (1, 2, 3...)
- `BY_PERCENTAGE`: Basis points (2500 = 25%)
- `BY_AMOUNT`: Cents directly
110 changes: 110 additions & 0 deletions .agent/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Testing

## Jest Unit Tests

```bash
npm test # Run all tests
npm test -- --watch # Watch mode
npm test -- path/to/file.test.ts # Specific file
```

Tests in `src/**/*.test.ts` alongside implementation.

### Test Data Factory Pattern

```typescript
// src/lib/balances.test.ts
const makeExpense = (overrides: Partial<BalancesExpense>): BalancesExpense =>
({
id: 'e1',
expenseDate: new Date('2025-01-01T00:00:00.000Z'),
title: 'Dinner',
amount: 0,
isReimbursement: false,
splitMode: 'EVENLY',
paidBy: { id: 'p0', name: 'P0' },
paidFor: [{ participant: { id: 'p0', name: 'P0' }, shares: 1 }],
...overrides,
}) as BalancesExpense

// Usage
const expenses = [
makeExpense({
amount: 100,
paidBy: { id: 'p0', name: 'P0' },
paidFor: [
{ participant: { id: 'p0', name: 'P0' }, shares: 1 },
{ participant: { id: 'p1', name: 'P1' }, shares: 1 },
],
}),
]
```

### Focus Areas

- `balances.test.ts` - Balance calculations, split modes, edge cases
- `totals.test.ts` - Expense totals, user shares
- `currency.test.ts` - Currency formatting

## Playwright E2E Tests

```bash
npm run test-e2e # Runs against local dev server
```

Tests in `tests/e2e/*.spec.ts` and `tests/*.spec.ts`.

### Test Helpers (`tests/helpers/`)

| Helper | Purpose |
| ----------------------------------------------- | ------------------------ |
| `createGroupViaAPI(page, name, participants)` | Fast group setup via API |
| `createExpense(page, { title, amount, payer })` | Fill expense form |
| `navigateToExpenseCreate(page, groupId)` | Go to expense creation |
| `fillParticipants(page, names)` | Add participants to form |
| `selectComboboxOption(page, label, value)` | Select dropdown value |

### Stability Patterns

```typescript
// Wait after navigation
await page.goto(`/groups/${groupId}`)
await page.waitForLoadState()

// Wait for URL after form submission
await page.getByRole('button', { name: 'Create' }).click()
await page.waitForURL(/\/groups\/[^/]+\/expenses/)

// Use API for fast setup
const groupId = await createGroupViaAPI(page, 'Test Group', ['Alice', 'Bob'])
```

### Example Test

```typescript
import { createExpense } from '../helpers'
import { createGroupViaAPI } from '../helpers/batch-api'

test('creates expense with correct values', async ({ page }) => {
const groupId = await createGroupViaAPI(page, `Test ${randomId(4)}`, [
'Alice',
'Bob',
])
await page.goto(`/groups/${groupId}/expenses`)

await createExpense(page, {
title: 'Dinner',
amount: '150.00',
payer: 'Alice',
})

await expect(page.getByText('Dinner')).toBeVisible()
await expect(page.getByText('$150.00')).toBeVisible()
})
```

### Config Notes

- `fullyParallel: false` in playwright.config.ts prevents DB conflicts
- Runs Chromium, Firefox, WebKit
- `json` reporter when `CLAUDE_CODE` env var detected
Loading