Skip to content

Commit e02c9e5

Browse files
authored
Merge pull request #309 from Jemiiah/feature/four-issues
feat: four issues
2 parents 889d1e6 + fd8a3b5 commit e02c9e5

4 files changed

Lines changed: 532 additions & 0 deletions

File tree

docs/CIRCLE_MANAGEMENT.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Circle Update and Detail API
2+
3+
## Overview
4+
5+
This document explains the circle update flow: how organizers can modify circle metadata and status after creation, which fields are editable, and what access control rules apply.
6+
7+
## PUT /api/circles/:id
8+
9+
Updates an existing circle. Only the organizer can call this endpoint.
10+
11+
Headers:
12+
13+
- Authorization: Bearer
14+
15+
Request body (validated by UpdateCircleSchema in lib/validations/circle.ts):
16+
17+
```json
18+
{
19+
"name": "Updated Circle Name",
20+
"description": "Updated description",
21+
"status": "COMPLETED"
22+
}
23+
```
24+
25+
All fields are optional. Only provided fields are updated. Omitted fields retain their current values.
26+
27+
### Validation Rules
28+
29+
| Field | Rule |
30+
|---|---|
31+
| name | Optional, 3-50 characters |
32+
| description | Optional, max 500 characters |
33+
| status | Optional, one of PENDING, ACTIVE, COMPLETED, CANCELLED |
34+
35+
### Access Control
36+
37+
Only the circle organizer can update the circle. Any other authenticated user receives 403 Forbidden.
38+
39+
### Success Response (200)
40+
41+
```json
42+
{
43+
"success": true,
44+
"circle": {
45+
"id": "...",
46+
"name": "Updated Circle Name",
47+
"description": "Updated description",
48+
"status": "COMPLETED",
49+
"organizerId": "...",
50+
"organizer": {
51+
"id": "...",
52+
"email": "...",
53+
"firstName": "...",
54+
"lastName": "..."
55+
},
56+
"members": [
57+
"..."
58+
]
59+
}
60+
}
61+
```
62+
63+
### Error Responses
64+
65+
| Status | Condition |
66+
|---|---|
67+
| 400 | Validation failed |
68+
| 401 | No or invalid token |
69+
| 403 | Authenticated user is not the organizer |
70+
| 404 | Circle not found |
71+
72+
## GET /api/circles/:id
73+
74+
Returns full circle details. Accessible to the organizer or any circle member.
75+
76+
Headers:
77+
78+
- Authorization: Bearer
79+
80+
Response includes:
81+
82+
- Full circle fields including contractAddress and contractDeployed.
83+
- organizer: id, email, firstName, lastName, walletAddress.
84+
- members: each with full user details and walletAddress.
85+
- contributions: ordered by createdAt descending, each with user name.
86+
- payments: payment schedule ordered by dueDate ascending.
87+
88+
Access control: returns 403 Forbidden if the authenticated user is neither the organizer nor a member.
89+
90+
## What Cannot Be Changed
91+
92+
The following fields are set at creation and cannot be updated via this API:
93+
94+
| Field | Reason |
95+
|---|---|
96+
| contributionAmount | Changing this mid-circle breaks payout math and member expectations |
97+
| contributionFrequencyDays | Affects round schedule and requires governance proposal |
98+
| maxRounds | Determines circle duration and cannot be shortened or extended |
99+
| organizerId | Circle ownership cannot be transferred via this endpoint |
100+
| contractAddress | Set when the smart contract is deployed |
101+
102+
Changes to contributionAmount or contributionFrequencyDays should go through governance using a CONTRIBUTION_ADJUSTMENT or RULE_CHANGE proposal.
103+
104+
## Status Transitions
105+
106+
The organizer can manually set circle status. Recommended transitions:
107+
108+
| From | To | When |
109+
|---|---|---|
110+
| PENDING | ACTIVE | When enough members have joined and the circle is ready to start |
111+
| ACTIVE | COMPLETED | When all rounds have finished |
112+
| ACTIVE | CANCELLED | When the circle needs to be shut down without completing |
113+
| PENDING | CANCELLED | When the circle is abandoned before starting |
114+
115+
Note: setting status to CANCELLED does not automatically trigger refunds. Refunds must be handled separately via smart contract functions dissolve_and_refund or emergency_refund.
116+
117+
## Example curl
118+
119+
```bash
120+
# Update circle name and description
121+
curl -X PUT http://localhost:3000/api/circles/<circle-id> \
122+
-H "Authorization: Bearer <token>" \
123+
-H "Content-Type: application/json" \
124+
-d '{"name": "New Name", "description": "Updated description"}'
125+
126+
# Mark circle as completed
127+
curl -X PUT http://localhost:3000/api/circles/<circle-id> \
128+
-H "Authorization: Bearer <token>" \
129+
-H "Content-Type: application/json" \
130+
-d '{"status": "COMPLETED"}'
131+
132+
# Get circle details
133+
curl http://localhost:3000/api/circles/<circle-id> \
134+
-H "Authorization: Bearer <token>"
135+
```

docs/PAYMENT_SCHEDULE.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Payment Schedule System
2+
3+
## Overview
4+
5+
This document explains the payment schedule system: what PaymentSchedule records represent, how they are created, how statuses work, and how they map to round progression and on-chain payouts.
6+
7+
## What Is a Payment Schedule?
8+
9+
A PaymentSchedule record represents a planned payout for a specific round of a circle. It tracks:
10+
11+
- Which round the payout is for.
12+
- Which member is the payee (by rotation index).
13+
- How much they are expected to receive.
14+
- When the payout is due.
15+
- Whether it has been paid.
16+
17+
## PaymentSchedule Model
18+
19+
| Field | Type | Description |
20+
|---|---|---|
21+
| id | string | CUID primary key |
22+
| circleId | string | FK to Circle |
23+
| round | int | Round number covered by this schedule |
24+
| payeeIndex | int | 1-based index into the rotation order |
25+
| expectedAmount | float | contributionAmount x memberCount |
26+
| dueDate | datetime | When contributions for this round are due |
27+
| status | PaymentStatus | Current payout state |
28+
| paidAt | datetime? | Set when status becomes COMPLETED |
29+
| createdAt | datetime | Auto-set |
30+
| updatedAt | datetime | Auto-updated |
31+
32+
## Payment Statuses
33+
34+
| Status | Description |
35+
|---|---|
36+
| PENDING | Payout is scheduled but not yet initiated |
37+
| INITIATED | Payout process has started |
38+
| COMPLETED | Payout has been successfully delivered |
39+
| OVERDUE | Due date has passed without completion |
40+
| FAILED | Payout attempt failed |
41+
42+
## How Payment Schedules Are Created
43+
44+
Payment schedules are created by the seed script for test data. In production, they should be created when a circle is initialized or when a new round begins.
45+
46+
Seed data example:
47+
48+
```ts
49+
await prisma.paymentSchedule.create({
50+
data: {
51+
circleId: circle1.id,
52+
round: 1,
53+
payeeIndex: 1,
54+
expectedAmount: 300, // 100 XLM x 3 members
55+
dueDate: nextWeek,
56+
status: 'COMPLETED',
57+
},
58+
});
59+
60+
await prisma.paymentSchedule.create({
61+
data: {
62+
circleId: circle1.id,
63+
round: 2,
64+
payeeIndex: 2,
65+
expectedAmount: 300,
66+
dueDate: nextTwoWeeks,
67+
status: 'PENDING',
68+
},
69+
});
70+
```
71+
72+
## Relationship to Circle Rounds
73+
74+
Each circle has maxRounds rounds. Ideally, one PaymentSchedule record should exist per round and be created upfront when the circle starts:
75+
76+
- Round 1 -> PaymentSchedule { round: 1, payeeIndex: 1, dueDate: startDate + frequency }
77+
- Round 2 -> PaymentSchedule { round: 2, payeeIndex: 2, dueDate: startDate + 2 x frequency }
78+
- ...
79+
- Round N -> PaymentSchedule { round: N, payeeIndex: N, dueDate: startDate + N x frequency }
80+
81+
expectedAmount = circle.contributionAmount x circle.members.length
82+
83+
## Relationship to On-Chain State
84+
85+
The PaymentSchedule table is the off-chain record of planned payouts. The actual payout is executed on-chain via claim_payout(member) on the Soroban contract.
86+
87+
When a member successfully calls claim_payout on-chain, the corresponding PaymentSchedule record should be updated to:
88+
89+
- status: COMPLETED
90+
- paidAt: now
91+
92+
This synchronization step is not yet automated and is a known gap.
93+
94+
## Overdue Detection
95+
96+
A cron job or scheduled task should periodically check for PaymentSchedule records where:
97+
98+
- status = PENDING
99+
- dueDate < now
100+
101+
Then update them to status = OVERDUE and notify the circle organizer.
102+
103+
This is referenced in the cron jobs issue as a task to implement.
104+
105+
## Indexes
106+
107+
The PaymentSchedule table has these indexes for query performance:
108+
109+
- circleId
110+
- status
111+
- composite: [circleId, status]
112+
113+
These support efficient queries such as finding all pending payment schedules for a circle.
114+
115+
## Example Query
116+
117+
```ts
118+
// Find all overdue payment schedules
119+
const overdue = await prisma.paymentSchedule.findMany({
120+
where: {
121+
status: 'PENDING',
122+
dueDate: { lt: new Date() },
123+
},
124+
include: {
125+
circle: { select: { name: true, organizerId: true } },
126+
},
127+
});
128+
```

0 commit comments

Comments
 (0)