Skip to content
Open
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
102 changes: 102 additions & 0 deletions .claude/commands/add-endpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Add New Endpoint

Add a new API endpoint to the Pluggy Java SDK.

## Required Information

- **Endpoint path**: e.g. `/models`, `/models/{id}`
- **HTTP method**: `GET`, `POST`, `PATCH`, `DELETE`
- **Query / path / body parameters**: from the OAS
- **Response schema**: from the OAS
- **Request schema** (if any): from the OAS

## Steps

### 1. Define request and response DTOs

Create DTOs under:
- `src/main/java/ai/pluggy/client/response/` — response payloads (`@Data @Builder`)
- `src/main/java/ai/pluggy/client/request/` — request bodies (`@Value @AllArgsConstructor @Builder`) or query maps

See `add-model-fields` for field patterns. **Field names must match the OAS byte-for-byte** (Gson `IDENTITY`).

Naming conventions:
- Single item response: bare entity name — `Account`, `Bill`, `Transaction`
- Collection / paginated response: `<Entity>sResponse` — `AccountsResponse`, `BillsResponse`
- Request body: `<Verb><Entity>Request` — `CreateItemRequest`, `UpdateItemRequest`
- Query map: `<Entity>SearchRequest` or `<Entity>Request` — `TransactionsSearchRequest`, `AccountsRequest`

### 2. Add the method to `PluggyApiService`

`src/main/java/ai/pluggy/client/PluggyApiService.java` is the single Retrofit interface for the entire SDK. Add the new method there with the appropriate annotations.

**Retrofit patterns:**

```java
// GET with no params
@GET("/categories")
Call<CategoriesResponse> getCategories();

// GET with path param
@GET("/categories/{id}")
Call<Category> getCategory(@Path("id") String categoryId);

// GET with single query param
@GET("/accounts")
Call<AccountsResponse> getAccounts(@Query("itemId") String itemId);

// GET with query map (filters / pagination)
@GET("/accounts")
Call<AccountsResponse> getAccounts(@QueryMap AccountsRequest accountsRequest);

// POST with body
@POST("/items")
Call<ItemResponse> createItem(@Body CreateItemRequest createItemRequest);

// PATCH with path param + body
@PATCH("/items/{id}")
Call<ItemResponse> updateItem(@Path("id") String itemId,
@Body UpdateItemRequest updateItemRequest);

// DELETE with path param
@DELETE("/items/{id}")
Call<DeleteItemResponse> deleteItem(@Path("id") String existingItemId);
```

Provide overloads (e.g. a no-body `updateItem` and a body-accepting one) only if the API genuinely supports both shapes — don't add convenience overloads speculatively.

### 3. Build and verify compilation

```bash
mvn -B package
```

### 4. Add an integration test

Put the test under `src/test/java/ai/pluggy/client/integration/` extending `BaseApiIntegrationTest`. The base class provides an authenticated `client` and cleans up items registered in `itemsIdCreated` after each test.

```java
class GetModelTest extends BaseApiIntegrationTest {

@Test
void getModel_validId_responseOk() throws IOException {
Response<Model> response = client.service().getModel("some-id").execute();
assertSuccessful(response); // from integration/util/AssertionsUtils
Model model = response.body();
assertNotNull(model);
assertEquals("some-id", model.getId());
}
}
```

If the test creates items, **register them** in `itemsIdCreated` immediately after creation so the `@AfterEach` cleanup deletes them — otherwise they leak into production.

### 5. Run the integration test

```bash
doppler run -- mvn -B verify -Dit.test=GetModelTest -DfailIfNoTests=false
```

### 6. Update CLAUDE.md (optional)

If the new endpoint is a notable addition (e.g. a whole new resource family), mention it under "Architecture".
124 changes: 124 additions & 0 deletions .claude/commands/add-model-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Add Model Fields

Add missing fields to existing DTOs in the Pluggy Java SDK.

## Required Information

- **DTO name**: e.g. `Transaction`, `Investment`, `Account`
- **Field name (OAS)**: exact JSON property name from the OpenAPI spec — **case-sensitive**
- **Field type**: Java type (`String`, `Integer`, `Double`, `Boolean`, `Date`, custom DTO, array/list)
- **Nullability**: nullable by default in this SDK. Use boxed types (`Integer`, `Double`) for nullable numerics; primitives only when guaranteed non-null.
- **Description**: brief comment if the field is non-obvious (avoid otherwise)

## Critical: Gson IDENTITY casing

The Gson instance is configured with `FieldNamingPolicy.IDENTITY`. Java field names must match the OAS JSON property name **byte-for-byte**. `payeeMCC` ≠ `payeeMcc` — a casing mismatch silently produces `null` with no warning. **Always copy the OAS field name exactly, including unusual casing.**

## Steps

### 1. Locate the DTO

DTOs live in `src/main/java/ai/pluggy/client/response/` (response payloads) and `src/main/java/ai/pluggy/client/request/` (request bodies and query maps).

### 2. Verify the OAS field name

Fetch the OAS (see `sync-api`) and inspect the schema:

```bash
jq '.components.schemas.<SchemaName>.properties' /tmp/pluggy-oas.json
```

Copy the property key **exactly** as it appears.

### 3. Add the field

Response DTO pattern (Lombok `@Data @Builder`, package-private fields):

```java
@Data
@Builder
public class Transaction {
String id;
String accountId;
Double amount;
// ... existing fields ...

// New field — name must match OAS exactly
String operationType;
}
```

Request DTO pattern (Lombok `@Value @AllArgsConstructor @Builder` with convenience constructors):

```java
@Value
@AllArgsConstructor
@Builder
public class CreateItemRequest {
Integer connectorId;
ParametersMap parameters;
// ... existing fields ...

// New optional field
String newField;

// If you add a field, update each convenience constructor to set it to null
public CreateItemRequest(Integer connectorId, ParametersMap parameters) {
this.connectorId = connectorId;
this.parameters = parameters;
// ... null-out the rest, including newField ...
this.newField = null;
}
}
```

### 4. Type patterns

```java
// Nullable string
String name;

// Non-null primitive (only if API guarantees it)
int count;

// Nullable number (default — boxed)
Integer count;
Double balance;

// Boolean
Boolean isActive;

// Date — usually serialized as ISO 8601 string in the API; prefer String
// unless an existing pattern in a sibling DTO uses java.util.Date
String createdAt;

// Nested DTO
CreditData creditData;

// Array
String[] tags;
SubModel[] items;

// Enum — declared as a separate file under response/ or request/
ProductType[] products;
```

### 5. Add nested DTOs if needed

If the field is a complex type new to the SDK, create a sibling file in the same package and follow the `@Data @Builder` pattern (for response) or `@Value @Builder` (for request).

### 6. Build and test

```bash
mvn -B package # compile + unit tests
doppler run -- mvn -B verify # integration tests
```

For deserialization changes, integration tests are the real signal — they exercise real API responses.

### 7. Verification checklist

- [ ] Field name matches OAS byte-for-byte (no case smoothing)
- [ ] Nullable fields use boxed types
- [ ] Existing convenience constructors updated (for request DTOs)
- [ ] `mvn -B verify` passes
72 changes: 72 additions & 0 deletions .claude/commands/sync-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Sync SDK with API

Synchronize the Pluggy Java SDK with the current production API.

## Steps

### 1. Fetch Current API Specification

Fetch the OpenAPI 3.1 spec. The endpoint requires authentication via `x-api-key`:

```bash
doppler run -- bash -c '
apikey=$(curl -s -X POST https://api.pluggy.ai/auth \
-H "Content-Type: application/json" \
-d "{\"clientId\":\"$PLUGGY_CLIENT_ID\",\"clientSecret\":\"$PLUGGY_CLIENT_SECRET\"}" \
| jq -r .apiKey)
curl -s https://api.pluggy.ai/oas3.json -H "x-api-key: $apikey" -o /tmp/pluggy-oas.json
'
```

If Doppler is not configured, set `PLUGGY_CLIENT_ID` and `PLUGGY_CLIENT_SECRET` manually.

### 2. Compare Endpoints

Review the Retrofit interface `src/main/java/ai/pluggy/client/PluggyApiService.java` against the OpenAPI `paths`.

**Check for:**
- Missing endpoints (paths in OAS but not in `PluggyApiService`)
- Missing HTTP methods on existing paths
- Path parameters declared with `@Path` matching OAS `{param}` placeholders
- Query parameters: single `@Query("name")` vs. `@QueryMap SearchRequest` patterns
- Request body shape (`@Body` DTO) matches the OAS request schema
- Response type (`Call<ResponseType>`) matches the OAS response schema

### 3. Compare Types

Review types in `src/main/java/ai/pluggy/client/response/` and `src/main/java/ai/pluggy/client/request/` against the OAS `components.schemas`.

**Check for:**
- Missing fields in existing DTOs
- New schemas not yet defined as DTOs
- **Field name casing**: the Gson instance uses `FieldNamingPolicy.IDENTITY`, so Java field names must match OAS property names **byte-for-byte** (e.g. OAS `payeeMCC` → Java `payeeMCC`, not `payeeMcc`). A single-character mismatch silently deserializes to `null`. This is the single highest-impact gap class — audit casing carefully. See commit `d1befb0` for prior incident.
- Type mismatches (e.g. OAS `integer` vs Java `Integer`, OAS `array` vs `List<T>` / `T[]`)
- Nullability: Java has no `?` syntax; use boxed types (`Integer`, `Double`, `Boolean`) for nullable fields. Primitives (`int`, `double`, `boolean`) cannot represent null.

### 4. Document Findings

Produce a gap analysis grouped by:
- **Missing endpoints** — with priority and HTTP method
- **Missing fields** — by DTO, with API name, Java name, type, nullability
- **Casing mismatches** — highest priority because they fail silently
- **Type mismatches** — flag and propose fix
- **Breaking changes** (if the OAS removed a field the SDK still exposes)

### 5. Implementation Order

1. **Phase 1 — Casing fixes**: rename Java fields to match OAS exactly. Highest impact because failures are silent.
2. **Phase 2 — Missing fields**: add new fields to existing DTOs (see `add-model-fields`).
3. **Phase 3 — Missing endpoints**: implement new endpoints (see `add-endpoint`).
4. **Phase 4 — Type corrections**: fix any wrong types.

### 6. Validation

After implementation:
```bash
mvn -B package # compile + unit tests
doppler run -- mvn -B verify # integration tests against live API
```

### 7. Update CLAUDE.md

If significant gaps were closed, add a line under "Architecture" noting the sync date and what was added.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,6 @@ buildNumber.properties
!.vscode/launch.json
!.vscode/extensions.json
.history

### Claude Code ###
.claude/settings.local.json
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ mvn -B verify -Dit.test=GetConnectorsTest -DfailIfNoTests=false

No lint/format plugin is configured (no checkstyle, spotless, PMD, errorprone) — formatting is enforced only by `.editorconfig`. Don't go looking for one.

## Claude commands

Three slash commands under `.claude/commands/` codify the recurring SDK ↔ API workflows:

- `sync-api` — fetch the live OpenAPI spec from `api.pluggy.ai/oas3.json`, diff it against `PluggyApiService` and the DTOs under `client/request/` + `client/response/`, and produce a gap report. Use periodically to catch missing fields and casing mismatches before users do.
- `add-model-fields` — Java/Lombok templates for adding fields to existing DTOs, with the Gson `IDENTITY` casing gotcha called out.
- `add-endpoint` — templates for adding a new endpoint method to `PluggyApiService` with the right Retrofit annotations and DTO naming conventions.

Integration tests require the following env vars (loaded from Doppler in CI, see `.github/workflows/maven.yml`):

- `PLUGGY_CLIENT_ID`
Expand Down
Loading