Skip to content

Commit a40bc6a

Browse files
author
yahia008
committed
claim api
1 parent 0f2104a commit a40bc6a

16 files changed

Lines changed: 1248 additions & 43 deletions

backend/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ ADMIN_TOKEN=admin-token-for-cli
2424

2525
# Logging
2626
LOG_LEVEL=info
27+
28+
# Cache
29+
CACHE_TTL_SECONDS=60

backend/TODO-claims.md

Lines changed: 187 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,188 @@
1-
# Claims API (#41) TODO
2-
3-
## Progress
4-
- [ ] 1. Update package.json (Prisma, JWT, Redis)
5-
- [ ] 2. prisma/schema.prisma (Claim, Vote models)
6-
- [ ] 3. Expand auth/ (JwtStrategy, Guard)
7-
- [ ] 4. claims/ module (controller, service, DTOs)
8-
- [ ] 5. PrismaModule, RedisModule in app.module
9-
- [ ] 6. prisma generate & db push
10-
- [ ] 7. Tests, seed fixtures
11-
- [ ] 8. Uncomment other modules in app.module
12-
- [ ] 9. npm install & test /claims endpoints
1+
# Claims API Implementation
132

3+
## Overview
4+
Production-ready Claims REST API for the NiffyInsure Stellar insurance system.
5+
6+
## Endpoints
7+
8+
### GET /api/claims
9+
List all claims with aggregated data and pagination.
10+
11+
**Query Parameters:**
12+
- `page` (optional, default: 1) - Page number
13+
- `limit` (optional, default: 20, max: 100) - Items per page
14+
- `status` (optional) - Filter by status: `pending`, `approved`, `rejected`
15+
16+
**Response:**
17+
```json
18+
{
19+
"data": [
20+
{
21+
"metadata": {
22+
"id": 1,
23+
"policyId": "policy-123",
24+
"creatorAddress": "GXXXXXXXXXX",
25+
"status": "pending",
26+
"amount": "1000",
27+
"description": "Insurance claim...",
28+
"evidenceHash": "QmXXX...",
29+
"createdAtLedger": 12345678,
30+
"createdAt": "2024-01-01T00:00:00Z",
31+
"updatedAt": "2024-01-01T00:00:00Z"
32+
},
33+
"votes": {
34+
"yesVotes": 3,
35+
"noVotes": 1,
36+
"totalVotes": 4
37+
},
38+
"quorum": {
39+
"required": 5,
40+
"current": 4,
41+
"percentage": 80,
42+
"reached": false
43+
},
44+
"deadline": {
45+
"votingDeadlineLedger": 12350000,
46+
"votingDeadlineTime": "2024-01-02T00:00:00Z",
47+
"isOpen": true,
48+
"remainingSeconds": 3600
49+
},
50+
"evidence": {
51+
"gatewayUrl": "https://ipfs.io/ipfs/QmXXX...",
52+
"hash": "QmXXX..."
53+
},
54+
"consistency": {
55+
"isFinalized": false,
56+
"indexerLag": 2,
57+
"lastIndexedLedger": 12345680,
58+
"isStale": false
59+
}
60+
}
61+
],
62+
"pagination": {
63+
"page": 1,
64+
"limit": 20,
65+
"total": 100,
66+
"totalPages": 5,
67+
"hasNext": true
68+
}
69+
}
70+
```
71+
72+
### GET /api/claims/:id
73+
Get detailed claim view by ID.
74+
75+
### GET /api/claims/needs-my-vote (Authenticated)
76+
Get claims where the authenticated user has not voted yet.
77+
78+
**Headers:**
79+
- `Authorization: Bearer <jwt_token>`
80+
81+
## Features
82+
83+
### Aggregation
84+
- Optimized SQL queries with JOINs and GROUP BY
85+
- Vote tallies computed in single query (no N+1)
86+
- Pagination with total count
87+
88+
### Quorum Calculation
89+
```
90+
percentage = (totalVotes / requiredVotes) * 100
91+
reached = totalVotes >= requiredVotes
92+
```
93+
94+
### Deadline Handling
95+
- Deadlines based on ledger numbers, not timestamps
96+
- Stellar: ~5 seconds per ledger
97+
- Remaining time calculated from current indexed ledger
98+
99+
### Caching Strategy
100+
- Redis caching with configurable TTL (default: 60s)
101+
- Cache keys: `claims:list:{page}:{limit}:{status}` and `claims:detail:{id}`
102+
- Pattern-based invalidation on updates
103+
- Graceful degradation if Redis unavailable
104+
105+
### Security
106+
- XSS prevention via HTML entity encoding
107+
- IPFS hash validation (CID v0/v1 format)
108+
- Stellar address validation (G... format, 56 chars)
109+
- Whitelisted evidence URL domains
110+
- Authorization via JWT with wallet address
111+
112+
### Consistency Model
113+
- Indexer lag tracking (ledgers behind current)
114+
- `isStale` flag when lag > 5 ledgers
115+
- `isFinalized` for on-chain finality
116+
- Best-effort deadline accuracy based on indexer state
117+
118+
## Database Schema
119+
120+
### Claims
121+
```sql
122+
CREATE TABLE claims (
123+
id SERIAL PRIMARY KEY,
124+
policyId VARCHAR NOT NULL,
125+
creatorAddress VARCHAR NOT NULL,
126+
amount VARCHAR NOT NULL,
127+
description TEXT,
128+
evidenceHash VARCHAR,
129+
status VARCHAR DEFAULT 'PENDING',
130+
isFinalized BOOLEAN DEFAULT FALSE,
131+
createdAtLedger INT NOT NULL,
132+
createdAt TIMESTAMP DEFAULT NOW(),
133+
updatedAt TIMESTAMP,
134+
updatedAtLedger INT DEFAULT 0
135+
);
136+
CREATE INDEX idx_claims_status ON claims(status);
137+
CREATE INDEX idx_claims_createdAt ON claims(createdAt);
138+
CREATE INDEX idx_claims_policyId ON claims(policyId);
139+
```
140+
141+
### Votes
142+
```sql
143+
CREATE TABLE votes (
144+
id SERIAL PRIMARY KEY,
145+
claimId INT REFERENCES claims(id) ON DELETE CASCADE,
146+
voterAddress VARCHAR NOT NULL,
147+
vote VARCHAR NOT NULL, -- 'YES' or 'NO'
148+
votingPower VARCHAR DEFAULT '1',
149+
txHash VARCHAR,
150+
votedAtLedger INT NOT NULL,
151+
createdAt TIMESTAMP DEFAULT NOW(),
152+
UNIQUE(claimId, voterAddress)
153+
);
154+
CREATE INDEX idx_votes_voterAddress ON votes(voterAddress);
155+
CREATE INDEX idx_votes_claimId ON votes(claimId);
156+
```
157+
158+
### Policies
159+
```sql
160+
CREATE TABLE policies (
161+
id VARCHAR PRIMARY KEY,
162+
name VARCHAR NOT NULL,
163+
description TEXT,
164+
coverageAmount VARCHAR NOT NULL,
165+
premium VARCHAR NOT NULL,
166+
durationDays INT NOT NULL,
167+
requiredVotes INT DEFAULT 5,
168+
votingPeriodLedgers INT DEFAULT 720,
169+
votingDeadlineLedger INT NOT NULL,
170+
votingDeadlineTime TIMESTAMP NOT NULL,
171+
createdAt TIMESTAMP DEFAULT NOW(),
172+
updatedAt TIMESTAMP
173+
);
174+
```
175+
176+
### IndexerState
177+
```sql
178+
CREATE TABLE indexer_state (
179+
id SERIAL PRIMARY KEY,
180+
lastLedger INT NOT NULL,
181+
lastCursor VARCHAR,
182+
updatedAt TIMESTAMP
183+
);
184+
```
185+
186+
## Environment Variables
187+
- `CACHE_TTL_SECONDS` - Cache TTL in seconds (default: 60)
188+
- `IPFS_GATEWAY` - IPFS gateway URL (default: https://ipfs.io)

backend/prisma/schema.prisma

Lines changed: 64 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,50 +11,86 @@ datasource db {
1111
}
1212

1313
model Claim {
14-
id String @id @default(cuid())
15-
policyId String
16-
amount BigInt
17-
details String @db.VarChar(2000)
18-
evidenceUrls Json? // ["https://ipfs...", ...]
19-
status ClaimStatus @default(PENDING)
20-
createdAt DateTime @default(now())
21-
deadline DateTime?
22-
ledger BigInt?
23-
24-
votes Vote[]
25-
policy Policy? @relation(fields: [policyId], references: [id]) // FK to policies table (future)
14+
id Int @id @default(autoincrement())
15+
policyId String
16+
creatorAddress String
17+
amount String
18+
description String?
19+
evidenceHash String?
20+
status ClaimStatus @default(PENDING)
21+
isFinalized Boolean @default(false)
22+
createdAtLedger Int
23+
createdAt DateTime @default(now())
24+
updatedAt DateTime @updatedAt
25+
updatedAtLedger Int @default(0)
2626
27+
// Relations
28+
policy Policy @relation(fields: [policyId], references: [id])
29+
votes Vote[]
30+
31+
@@index([status])
32+
@@index([createdAt])
33+
@@index([policyId])
2734
@@map("claims")
2835
}
2936

3037
model Vote {
31-
id String @id @default(cuid())
32-
claimId String
33-
voter String // wallet G...
34-
vote Boolean // true=approve
35-
ledger BigInt
36-
timestamp DateTime @default(now())
38+
id Int @id @default(autoincrement())
39+
claimId Int
40+
voterAddress String
41+
vote VoteType
42+
votingPower String @default("1")
43+
txHash String?
44+
votedAtLedger Int
45+
createdAt DateTime @default(now())
3746
38-
claim Claim @relation(fields: [claimId], references: [id], onDelete: Cascade)
47+
// Relations
48+
claim Claim @relation(fields: [claimId], references: [id], onDelete: Cascade)
3949
40-
@@unique([claimId, voter])
50+
@@unique([claimId, voterAddress])
51+
@@index([voterAddress])
52+
@@index([claimId])
4153
@@map("votes")
4254
}
4355

56+
model Policy {
57+
id String @id
58+
name String
59+
description String?
60+
coverageAmount String
61+
premium String
62+
durationDays Int
63+
requiredVotes Int @default(5)
64+
votingPeriodLedgers Int @default(720) // ~1 hour at 5 sec/ledger
65+
votingDeadlineLedger Int
66+
votingDeadlineTime DateTime
67+
createdAt DateTime @default(now())
68+
updatedAt DateTime @updatedAt
69+
70+
// Relations
71+
claims Claim[]
72+
73+
@@map("policies")
74+
}
75+
76+
model IndexerState {
77+
id Int @id @default(autoincrement())
78+
lastLedger Int
79+
lastCursor String?
80+
updatedAt DateTime @updatedAt
81+
82+
@@map("indexer_state")
83+
}
84+
4485
enum ClaimStatus {
4586
PENDING
46-
VOTING
4787
APPROVED
4888
REJECTED
49-
SETTLED
5089
}
5190

52-
// Future
53-
model Policy {
54-
id String @id @default(cuid())
55-
// ...
56-
claims Claim[]
57-
@@map("policies")
91+
enum VoteType {
92+
YES
93+
NO
5894
}
5995

6096

backend/src/app.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { TerminusModule } from '@nestjs/terminus';
44
import { validationSchema } from './config/env.validation';
55
import { HealthModule } from './health/health.module';
66
import { PrismaModule } from './prisma/prisma.module';
7+
import { CacheModule } from './cache/cache.module';
78
import { RpcModule } from './rpc/rpc.module';
89
import { IndexerModule } from './indexer/indexer.module';
910
import { IpfsModule } from './ipfs/ipfs.module';
1011
import { AuthModule } from './auth/auth.module';
1112
import { AdminModule } from './admin/admin.module';
13+
import { ClaimsModule } from './claims/claims.module';
1214

1315
@Module({
1416
imports: [
@@ -22,12 +24,14 @@ import { AdminModule } from './admin/admin.module';
2224
}),
2325
TerminusModule,
2426
PrismaModule,
27+
CacheModule,
2528
HealthModule,
2629
RpcModule,
2730
IndexerModule,
2831
IpfsModule,
2932
AuthModule,
3033
AdminModule,
34+
ClaimsModule,
3135
],
3236
})
3337
export class AppModule {}

backend/src/auth/auth.module.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
import { Module } from '@nestjs/common';
2+
import { PassportModule } from '@nestjs/passport';
3+
import { JwtModule } from '@nestjs/jwt';
4+
import { ConfigModule, ConfigService } from '@nestjs/config';
5+
import { JwtStrategy } from './strategies/jwt.strategy';
26

37
@Module({
4-
// controllers: [AuthController],
5-
// providers: [AuthService],
6-
// exports: [AuthService],
8+
imports: [
9+
PassportModule.register({ defaultStrategy: 'jwt' }),
10+
JwtModule.registerAsync({
11+
imports: [ConfigModule],
12+
useFactory: (configService: ConfigService) => ({
13+
secret: configService.get<string>('JWT_SECRET'),
14+
signOptions: { expiresIn: '7d' },
15+
}),
16+
inject: [ConfigService],
17+
}),
18+
],
19+
providers: [JwtStrategy],
20+
exports: [PassportModule, JwtModule],
721
})
822
export class AuthModule {}
923

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2+
3+
export const WalletAddress = createParamDecorator(
4+
(data: unknown, ctx: ExecutionContext): string => {
5+
const request = ctx.switchToHttp().getRequest();
6+
return request.user?.walletAddress;
7+
},
8+
);

0 commit comments

Comments
 (0)