Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { notificationsRouter } from './modules/notifications/notifications.route
import { searchRouter } from './modules/search/search.routes.js';
import { webhooksRouter } from './modules/webhooks/webhooks.routes.js';
import { analyticsRouter } from './modules/analytics/analytics.routes.js';
import { goalsRouter } from './modules/goals/goals.routes.js';
import { registerGoalsDocs } from './modules/goals/goals.openapi.js';

/** Builds and configures the Express application without starting a listener. */
export function createApp(): Express {
Expand Down Expand Up @@ -68,6 +70,10 @@ export function createApp(): Express {
app.use(`${env.API_BASE_PATH}/search`, searchRouter);
app.use(`${env.API_BASE_PATH}/webhooks`, webhooksRouter);
app.use(`${env.API_BASE_PATH}/analytics`, analyticsRouter);
app.use(`${env.API_BASE_PATH}/goals`, goalsRouter);

// Register OpenAPI path docs for feature modules.
registerGoalsDocs();

app.use(notFoundHandler);
app.use(errorHandler);
Expand Down
92 changes: 92 additions & 0 deletions backend/src/modules/goals/DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Goals Module — Design Document

## Purpose

Enable creators to set funding targets, track progress toward those goals, and receive automatic notifications when goals are fully funded.

## Architecture

### Layer Separation

```
┌─────────────────────────────────────────────────────────┐
│ Routes (goals.routes.ts) │
│ • HTTP method + path mapping │
│ • Auth middleware (requireAuth) │
├─────────────────────────────────────────────────────────┤
│ Controller (goals.controller.ts) │
│ • Request parsing and validation (Zod) │
│ • Ownership verification │
│ • Response formatting │
├─────────────────────────────────────────────────────────┤
│ Service (goals.service.ts) │
│ • Business logic (progress, completion) │
│ • Database operations (Prisma) │
│ • Notification creation │
├─────────────────────────────────────────────────────────┤
│ Database (Prisma/PostgreSQL) │
│ • Goal, Notification models │
└─────────────────────────────────────────────────────────┘
```

### Key Design Decisions

1. **Pure Progress Calculation**: `calculateProgress()` is a pure function with no I/O, making it easily testable and deterministic.

2. **Atomic Completion**: Goal status update and notification creation happen in a single `Promise.all()` transaction to ensure consistency.

3. **Ownership at Controller Level**: Ownership checks happen in the controller (before service calls) to keep service functions reusable for internal/system operations.

4. **BigInt String Serialization**: Stellar stroops use BigInt for precision, but the API serializes them as decimal strings for JSON compatibility.

## Data Flow

### Goal Creation
```
Client → POST /goals → Controller validates → Service creates → Prisma → Response
```

### Progress Check
```
Client → GET /goals/:id/progress → Controller validates → Service fetches + calculates → Response
```

### Completion Detection
```
Payment webhook → Service updates raisedStroops → checkAndNotifyCompletion()
→ If raised >= target: Transition to COMPLETED + Create notification
```

## Error Handling

- **NotFoundError** (404): Goal not found
- **BadRequestError** (400): Validation errors, ownership violations
- **ZodError** → Converted to BadRequestError with issue details

All errors propagate through Express error handler middleware.

## Security Considerations

1. **Authentication**: All endpoints require valid JWT via `requireAuth` middleware
2. **Ownership Enforcement**: Update/delete operations verify `goal.userId === auth.userId`
3. **Input Validation**: All inputs validated with Zod schemas before processing
4. **No SQL Injection**: Prisma ORM uses parameterized queries

## Performance

- **Pagination**: List endpoint supports page/limit with database-level skip/take
- **No N+1 Queries**: Single queries for list operations with count
- **Index Usage**: Prisma indexes on `userId` and `id` for fast lookups

## Testing Strategy

- **Unit Tests**: Pure functions tested without mocks
- **Integration Tests**: DB operations tested with Vitest mocks
- **Coverage**: 23 tests covering happy paths, edge cases, and error scenarios

## Future Considerations

1. **Goal Expiration**: Could add scheduled job to auto-expire past-deadline goals
2. **Progress Events**: Could emit events for real-time UI updates via WebSocket
3. **Goal Templates**: Could support reusable goal templates for common use cases
4. **Multi-currency**: Could extend to support multiple currency types beyond stroops
149 changes: 149 additions & 0 deletions backend/src/modules/goals/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Goals Module

Creator funding goals with CRUD operations, progress tracking, and completion notifications.

## Overview

The Goals module allows creators to set funding targets and track progress toward those goals. When a goal is fully funded, the system automatically transitions it to COMPLETED status and creates a notification for the goal creator.

## Features

- **CRUD Operations**: Create, read, update, and delete funding goals
- **Progress Tracking**: Real-time progress calculation with percentage, completion status, and days remaining
- **Completion Detection**: Automatic detection when a goal reaches its target
- **Notifications**: Automatic notification creation when a goal is completed
- **Ownership Enforcement**: Users can only modify/delete their own goals

## API Endpoints

All endpoints require authentication via Bearer token.

| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/goals` | Create a new funding goal |
| GET | `/goals` | List goals for a user (paginated) |
| GET | `/goals/:goalId` | Get a specific goal by ID |
| PATCH | `/goals/:goalId` | Update a goal (owner only) |
| DELETE | `/goals/:goalId` | Delete a goal (owner only) |
| GET | `/goals/:goalId/progress` | Get goal with computed progress fields |

## Request/Response Examples

### Create Goal

```http
POST /api/v1/goals
Authorization: Bearer <token>
Content-Type: application/json

{
"title": "New streaming setup",
"targetStroops": "10000000",
"deadline": "2026-12-31T23:59:59Z"
}
```

**Response (201):**
```json
{
"data": {
"id": "goal_abc123",
"userId": "user_xyz789",
"title": "New streaming setup",
"targetStroops": "10000000",
"raisedStroops": "0",
"deadline": "2026-12-31T23:59:59Z",
"status": "ACTIVE",
"createdAt": "2026-01-15T10:30:00Z",
"updatedAt": "2026-01-15T10:30:00Z"
}
}
```

### Get Goal Progress

```http
GET /api/v1/goals/goal_abc123/progress
Authorization: Bearer <token>
```

**Response (200):**
```json
{
"data": {
"id": "goal_abc123",
"userId": "user_xyz789",
"title": "New streaming setup",
"targetStroops": "10000000",
"raisedStroops": "5000000",
"deadline": "2026-12-31T23:59:59Z",
"status": "ACTIVE",
"createdAt": "2026-01-15T10:30:00Z",
"updatedAt": "2026-01-15T12:00:00Z",
"raisedPercentage": 50,
"isComplete": false,
"daysRemaining": 350
}
}
```

## Data Model

### Goal

| Field | Type | Description |
|-------|------|-------------|
| id | string | Unique identifier (CUID) |
| userId | string | Owner's user ID |
| title | string | Goal title (1-200 chars) |
| targetStroops | string | Target amount in stroops (decimal string) |
| raisedStroops | string | Amount raised so far in stroops |
| deadline | string \| null | Optional ISO-8601 deadline |
| status | GoalStatus | ACTIVE, COMPLETED, CANCELLED, or EXPIRED |
| createdAt | string | Creation timestamp |
| updatedAt | string | Last update timestamp |

### GoalProgress

Extends Goal with computed fields:

| Field | Type | Description |
|-------|------|-------------|
| raisedPercentage | number | Percentage raised (0-100) |
| isComplete | boolean | True when raised >= target |
| daysRemaining | number \| null | Days until deadline (null if no deadline) |

## Business Rules

1. **Ownership**: Only the goal owner can update or delete their goals
2. **Status Transitions**: Goals can be transitioned between ACTIVE, COMPLETED, CANCELLED, and EXPIRED
3. **Completion Detection**: When `raisedStroops >= targetStroops` and status is ACTIVE, the goal automatically transitions to COMPLETED
4. **Notification**: A GOAL_COMPLETED notification is created when a goal is completed
5. **Deadline Handling**: If a deadline passes, the goal does not automatically expire (manual status change required)

## Testing

Run tests with:
```bash
npm test -- --run goals
```

All 23 tests cover:
- Progress calculation (pure function)
- CRUD operations (DB-backed with mocks)
- Completion detection and notification
- Edge cases (not found, ownership validation)

## Architecture

```
goals/
├── goals.types.ts # TypeScript interfaces
├── goals.schema.ts # Zod validation schemas
├── goals.service.ts # Business logic and DB operations
├── goals.controller.ts # Express request handlers
├── goals.routes.ts # Route definitions with auth middleware
├── goals.openapi.ts # OpenAPI documentation
├── goals.test.ts # Unit tests
└── README.md # This file
```
Loading
Loading