Skip to content

Commit e0ed4a2

Browse files
authored
Merge pull request #1545 from Stanley-Owoh/document-bulk-import-endpoint-1509
feat: add document bulk import endpoint
2 parents 0bbbef4 + 474c99f commit e0ed4a2

1 file changed

Lines changed: 153 additions & 0 deletions

File tree

API.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,159 @@ Validation:
247247
- `slippageTolerance`: 0.1–5% (optional, default: 1)
248248
- `strategy`: `threshold` | `periodic` | `volatility` | `custom` (optional, default: `threshold`)
249249
250+
### Bulk Import Portfolio
251+
252+
Create a new portfolio by uploading allocations from a CSV or JSON file. This is the primary endpoint consumed by the frontend [`BulkPortfolioImport.tsx`](frontend/src/components/BulkPortfolioImport.tsx) component.
253+
254+
```bash
255+
POST /api/v1/portfolio/import
256+
Content-Type: application/json | text/csv
257+
```
258+
259+
#### Accepted Formats
260+
261+
**JSON — wrapped object:**
262+
```json
263+
{
264+
"allocations": [
265+
{ "asset": "XLM", "allocation_pct": 40 },
266+
{ "asset": "USDC", "allocation_pct": 35 },
267+
{ "asset": "BTC", "allocation_pct": 25 }
268+
],
269+
"userAddress": "GALPHABET...",
270+
"name": "My Portfolio",
271+
"description": "Optional description"
272+
}
273+
```
274+
275+
**JSON — bare array:**
276+
```json
277+
[
278+
{ "asset": "XLM", "allocation_pct": 40 },
279+
{ "asset": "USDC", "allocation_pct": 35 },
280+
{ "asset": "BTC", "allocation_pct": 25 }
281+
]
282+
```
283+
284+
> When using a bare array, `userAddress` must be included as a top-level field of the request or authenticated via JWT.
285+
286+
**CSV — raw text with required headers:**
287+
```csv
288+
asset,allocation_pct
289+
XLM,40
290+
USDC,35
291+
BTC,25
292+
```
293+
294+
> Send with `Content-Type: text/csv`. Headers must include `asset` and `allocation_pct`.
295+
296+
#### Request Schema
297+
298+
| Field | Type | Required | Notes |
299+
|-------|------|----------|-------|
300+
| `allocations` | `AllocationInputRow[]` | Yes | Array of `{ asset, allocation_pct }`. May also be the top-level body for JSON. |
301+
| `userAddress` | `string` | Yes | Stellar public key. Required in body or via JWT auth. |
302+
| `name` | `string` | No | Portfolio display name. |
303+
| `description` | `string` | No | Portfolio description. |
304+
305+
Each `AllocationInputRow`:
306+
307+
| Field | Type | Constraints |
308+
|-------|------|-------------|
309+
| `asset` | `string` | Required, normalized to uppercase. Must exist in the asset registry, be enabled, and not quarantined. |
310+
| `allocation_pct` | `number` | Required, finite, `0–100`. Duplicate assets are merged by summing percentages. |
311+
312+
#### Validation Rules
313+
314+
- Allocations must sum to **100%** (tolerance: 0.01%).
315+
- Maximum **10 distinct assets**.
316+
- Maximum **5 000 rows**.
317+
- `allocation_pct` must be a finite number between 0 and 100 inclusive.
318+
- Asset codes are validated against the internal asset registry; unknown, disabled, or quarantined assets are rejected.
319+
- Duplicate asset rows are merged (percentages summed) before validation.
320+
321+
#### Sample cURL — JSON
322+
323+
```bash
324+
curl -X POST http://localhost:3001/api/v1/portfolio/import \
325+
-H "Content-Type: application/json" \
326+
-d '{
327+
"allocations": [
328+
{ "asset": "XLM", "allocation_pct": 60 },
329+
{ "asset": "USDC", "allocation_pct": 40 }
330+
],
331+
"userAddress": "GALPHABET...",
332+
"name": "Imported Portfolio"
333+
}'
334+
```
335+
336+
#### Sample cURL — CSV
337+
338+
```bash
339+
curl -X POST http://localhost:3001/api/v1/portfolio/import \
340+
-H "Content-Type: text/csv" \
341+
--data-binary "asset,allocation_pct
342+
XLM,60
343+
USDC,40"
344+
```
345+
346+
#### Success Response (201)
347+
348+
```json
349+
{
350+
"success": true,
351+
"data": {
352+
"portfolioId": "portfolio-abc123",
353+
"status": "created"
354+
},
355+
"error": null,
356+
"timestamp": "2025-01-01T00:00:00.000Z"
357+
}
358+
```
359+
360+
#### Validation Error Response (400)
361+
362+
Returned when the payload fails validation. The `errors` array contains per-row details; `meta` summarises totals.
363+
364+
```json
365+
{
366+
"error": "VALIDATION_ERROR",
367+
"message": "Bulk import validation failed",
368+
"code": "VALIDATION_ERROR",
369+
"errors": [
370+
{ "row": 2, "field": "asset", "message": "Invalid or unknown asset code: FAKE" },
371+
{ "row": 3, "field": "allocation_pct", "message": "allocation_pct must be a number" },
372+
{ "row": 0, "field": "allocation_pct", "message": "Allocations must sum to 100% (received 85%)" }
373+
],
374+
"meta": {
375+
"totalRows": 3,
376+
"validRows": 1
377+
}
378+
}
379+
```
380+
381+
Each error object:
382+
383+
| Field | Type | Description |
384+
|-------|------|-------------|
385+
| `row` | `number` | 1-based row index (header = 1 for CSV). `0` indicates a payload-level error (e.g., sum check). |
386+
| `field` | `string` | Field that failed (`asset`, `allocation_pct`, `header`, `csv_or_json`, `rows`, `json`). |
387+
| `message` | `string` | Human-readable description of the failure. |
388+
389+
`meta` fields:
390+
391+
| Field | Type | Description |
392+
|-------|------|-------------|
393+
| `totalRows` | `number` | Total data rows received. |
394+
| `validRows` | `number` | Rows that passed all validation checks. |
395+
396+
#### Other Error Responses
397+
398+
| HTTP Status | Code | Condition |
399+
|-------------|------|-----------|
400+
| 400 | `VALIDATION_ERROR` | Missing `userAddress` in body or JWT. |
401+
| 500 | `INTERNAL_ERROR` | Unexpected server error during portfolio creation. |
402+
250403
### Get Portfolio
251404
252405
```bash

0 commit comments

Comments
 (0)