diff --git a/README.md b/README.md index 7d3b111..c34026f 100644 --- a/README.md +++ b/README.md @@ -53,3 +53,48 @@ if(connectorsResponse.isSuccessful()) { ErrorResponse errorResponse = pluggyClient.parseError(connectorsResponse) } ``` + +### Transactions (cursor pagination) + +`getTransactions` (`GET /transactions`) is deprecated and returns `410` for newer applications. Use `getTransactionsV2` (`GET /v2/transactions`) and follow the cursor until there is no next page: + +```java +List transactions = new ArrayList<>(); +TransactionsCursorSearchRequest request = new TransactionsCursorSearchRequest().dateFrom("2026-01-01"); +while (true) { + TransactionsCursorResponse page = pluggyClient.service() + .getTransactionsV2(accountId, request) + .execute() + .body(); + transactions.addAll(page.getResults()); + if (!page.hasNext()) { + break; + } + request = new TransactionsCursorSearchRequest().dateFrom("2026-01-01").after(page.getNextCursor()); +} +``` + +### Listing items (opt-in) + +> **Opt-in, paid plans only.** Listing items is disabled by default and is only available to paid-plan teams that have explicitly requested it from Pluggy support. Teams without it enabled get `403` with `codeDescription` `LIST_ITEMS_FEATURE_NOT_ENABLED`. For most integrations, store each `itemId` when it is created (Pluggy Connect `onSuccess` or the `item/created` webhook) and use `getItem(id)` instead. + +`getItems` (`GET /v2/items`) returns items newest first, optionally filtered by `clientUserId` and/or `connectorId`, and is cursor-paginated like `getTransactionsV2`: + +```java +List items = new ArrayList<>(); +ItemsCursorSearchRequest request = new ItemsCursorSearchRequest().clientUserId("user-123"); +while (true) { + Response response = pluggyClient.service().getItems(request).execute(); + if (!response.isSuccessful()) { + ErrorResponse error = pluggyClient.parseError(response); + // "LIST_ITEMS_FEATURE_NOT_ENABLED" when listing items is not enabled for your team + throw new IllegalStateException(error.getCodeDescription() + ": " + error.getMessage()); + } + ItemsCursorResponse page = response.body(); + items.addAll(page.getResults()); + if (!page.hasNext()) { + break; + } + request = new ItemsCursorSearchRequest().clientUserId("user-123").after(page.getNextCursor()); +} +``` diff --git a/pom.xml b/pom.xml index 004fe82..aaec26f 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ ai.pluggy pluggy-java - 1.14.0 + 1.15.0 jar diff --git a/src/main/java/ai/pluggy/client/PluggyApiService.java b/src/main/java/ai/pluggy/client/PluggyApiService.java index 7db5435..3357fac 100644 --- a/src/main/java/ai/pluggy/client/PluggyApiService.java +++ b/src/main/java/ai/pluggy/client/PluggyApiService.java @@ -6,6 +6,8 @@ import ai.pluggy.client.request.CreateItemRequest; import ai.pluggy.client.request.InvestmentTransactionsSearchRequest; import ai.pluggy.client.request.ItemResourcesSearchRequest; +import ai.pluggy.client.request.ItemsCursorSearchRequest; +import ai.pluggy.client.request.TransactionsCursorSearchRequest; import ai.pluggy.client.request.TransactionsSearchRequest; import ai.pluggy.client.request.UpdateItemMfaRequest; import ai.pluggy.client.request.UpdateItemRequest; @@ -56,6 +58,33 @@ Call updateItemSendMfa(@Path("id") String itemId, @DELETE("/items/{id}") Call deleteItem(@Path("id") String existingItemId); + /** + * First page of the team's items, newest first, cursor-paginated. Fetch the following pages by + * passing {@link ItemsCursorResponse#getNextCursor()} as + * {@link ItemsCursorSearchRequest#after(String)} until it returns null. + * + *

Availability: opt-in, paid plans only. Listing items is disabled by default and is + * only available to paid-plan teams that have explicitly requested it from Pluggy support. Teams + * without it enabled get {@code 403 LIST_ITEMS_FEATURE_NOT_ENABLED}. For most integrations, + * store each itemId when it is created (Pluggy Connect {@code onSuccess} or the + * {@code item/created} webhook) and use {@link #getItem(String)} instead. + */ + @GET("/v2/items") + Call getItems(); + + /** + * Items filtered by {@code clientUserId} and/or {@code connectorId}, newest first, + * cursor-paginated. See {@link #getItems()}. + * + *

Availability: opt-in, paid plans only. Listing items is disabled by default and is + * only available to paid-plan teams that have explicitly requested it from Pluggy support. Teams + * without it enabled get {@code 403 LIST_ITEMS_FEATURE_NOT_ENABLED}. For most integrations, + * store each itemId when it is created (Pluggy Connect {@code onSuccess} or the + * {@code item/created} webhook) and use {@link #getItem(String)} instead. + */ + @GET("/v2/items") + Call getItems(@QueryMap ItemsCursorSearchRequest itemsCursorSearchRequest); + /** * Open Finance only: the resources the financial institution declared for the item's consent. * Items on other connectors return an empty page. @@ -67,6 +96,38 @@ Call updateItemSendMfa(@Path("id") String itemId, Call getItemResources(@Path("id") String itemId, @QueryMap ItemResourcesSearchRequest itemResourcesSearchRequest); + /** + * Retrieve the SCR (Bacen's Sistema de Informações de Crédito) for the document behind an item, + * for the last 4 available base dates. The response is Bacen's own payload, forwarded unchanged. + * + *

Opt-in: requires the SCR feature to be enabled for your team (ask Pluggy support); otherwise + * 403 SCR_FEATURE_NOT_ENABLED (see {@link ErrorResponse#getCodeDescription()}). Only available for + * Open Finance items with a known CPF/CNPJ (otherwise 422 SCR_ITEM_NOT_SUPPORTED). + * + * @param itemId item primary identifier + */ + @GET("/items/{id}/scr") + Call getItemScr(@Path("id") String itemId); + + /** + * Retrieve the SCR (Bacen's Sistema de Informações de Crédito) for the document behind an item, + * for a range of base dates. Base dates are months, and Bacen consolidates each one with a few + * months of delay, so the current month returns nothing. + * + *

Opt-in: requires the SCR feature to be enabled for your team (ask Pluggy support); otherwise + * 403 SCR_FEATURE_NOT_ENABLED (see {@link ErrorResponse#getCodeDescription()}). Only available for + * Open Finance items with a known CPF/CNPJ (otherwise 422 SCR_ITEM_NOT_SUPPORTED). + * + * @param itemId item primary identifier + * @param from first base date to consult, as YYYYMM (e.g. {@code 202604}); null to default to 3 + * base dates before {@code to} + * @param to last base date to consult, as YYYYMM (e.g. {@code 202607}); null to default to 2 + * months before the current one + */ + @GET("/items/{id}/scr") + Call getItemScr(@Path("id") String itemId, @Query("from") String from, + @Query("to") String to); + @GET("/accounts") Call getAccounts(@Query("itemId") String itemId); @@ -76,19 +137,49 @@ Call getItemResources(@Path("id") String itemId, @GET("/accounts/{id}") Call getAccount(@Path("id") String accountId); + /** + * Real-time balance fetched from the institution. Only available for Open Finance connectors; + * accounts on other connectors get an error response. + */ + @GET("/accounts/{id}/balance") + Call getAccountBalance(@Path("id") String accountId); + @GET("/bills") Call getBills(@Query("accountId") String accountId); @GET("/bills/{id}") Call getBill(@Path("id") String billId); + /** + * @deprecated {@code GET /transactions} is deprecated and returns 410 for newer applications. + * Use {@link #getTransactionsV2(String)} (cursor pagination) instead. + */ + @Deprecated @GET("/transactions") Call getTransactions(@Query("accountId") String accountId); + /** + * @deprecated {@code GET /transactions} is deprecated and returns 410 for newer applications. + * Use {@link #getTransactionsV2(String, TransactionsCursorSearchRequest)} (cursor pagination) + * instead. + */ + @Deprecated @GET("/transactions") Call getTransactions(@Query("accountId") String accountId, @QueryMap TransactionsSearchRequest transactionsSearchRequest); + /** + * First page of an account's transactions, cursor-paginated. Fetch the following pages by + * passing {@link TransactionsCursorResponse#getNextCursor()} as + * {@link TransactionsCursorSearchRequest#after(String)} until it returns null. + */ + @GET("/v2/transactions") + Call getTransactionsV2(@Query("accountId") String accountId); + + @GET("/v2/transactions") + Call getTransactionsV2(@Query("accountId") String accountId, + @QueryMap TransactionsCursorSearchRequest transactionsCursorSearchRequest); + @GET("/transactions/{id}") Call getTransaction(@Path("id") String transactionId); diff --git a/src/main/java/ai/pluggy/client/request/ItemsCursorSearchRequest.java b/src/main/java/ai/pluggy/client/request/ItemsCursorSearchRequest.java new file mode 100644 index 0000000..2f93c95 --- /dev/null +++ b/src/main/java/ai/pluggy/client/request/ItemsCursorSearchRequest.java @@ -0,0 +1,70 @@ +package ai.pluggy.client.request; + +import static ai.pluggy.utils.Asserts.assertNotNull; + +import java.util.HashMap; + +/** + * Query params for GET /v2/items (cursor pagination). All filters are optional. The API rejects + * unknown query params, so only the filters below can be sent. + * + *

Availability: opt-in, paid plans only. Listing items is disabled by default and is + * only available to paid-plan teams that have explicitly requested it from Pluggy support. Teams + * without it enabled get {@code 403 LIST_ITEMS_FEATURE_NOT_ENABLED}. For most integrations, store + * each itemId when it is created (Pluggy Connect {@code onSuccess} or the {@code item/created} + * webhook) and use {@code getItem(id)} instead. + */ +public class ItemsCursorSearchRequest extends HashMap { + + private static final int CLIENT_USER_ID_MAX_LENGTH = 255; + + /** + * @param clientUserId String - only items created with this clientUserId (max 255 chars) + * @return this instance, useful to continue adding params + */ + public ItemsCursorSearchRequest clientUserId(String clientUserId) { + assertNotNull(clientUserId, "clientUserId"); + if (clientUserId.length() > CLIENT_USER_ID_MAX_LENGTH) { + throw new IllegalArgumentException( + "'clientUserId' cannot be longer than " + CLIENT_USER_ID_MAX_LENGTH + " characters"); + } + put("clientUserId", clientUserId); + return this; + } + + /** + * @param connectorId Integer - only items of this connector (>= 0) + * @return this instance, useful to continue adding params + */ + public ItemsCursorSearchRequest connectorId(Integer connectorId) { + assertNotNull(connectorId, "connectorId"); + if (connectorId < 0) { + throw new IllegalArgumentException("'connectorId' cannot be negative"); + } + put("connectorId", connectorId); + return this; + } + + /** + * @param after String - opaque cursor from a previous response + * ({@code ItemsCursorResponse#getNextCursor()}). + * @return this instance, useful to continue adding params + */ + public ItemsCursorSearchRequest after(String after) { + assertNotNull(after, "after"); + put("after", after); + return this; + } + + public String getClientUserId() { + return (String) get("clientUserId"); + } + + public Integer getConnectorId() { + return (Integer) get("connectorId"); + } + + public String getAfter() { + return (String) get("after"); + } +} diff --git a/src/main/java/ai/pluggy/client/request/Options.java b/src/main/java/ai/pluggy/client/request/Options.java index c7b4a1d..6c30fad 100644 --- a/src/main/java/ai/pluggy/client/request/Options.java +++ b/src/main/java/ai/pluggy/client/request/Options.java @@ -2,8 +2,10 @@ import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.NoArgsConstructor; @Builder +@NoArgsConstructor @AllArgsConstructor public class Options { diff --git a/src/main/java/ai/pluggy/client/request/TransactionsCursorSearchRequest.java b/src/main/java/ai/pluggy/client/request/TransactionsCursorSearchRequest.java new file mode 100644 index 0000000..122b187 --- /dev/null +++ b/src/main/java/ai/pluggy/client/request/TransactionsCursorSearchRequest.java @@ -0,0 +1,97 @@ +package ai.pluggy.client.request; + +import static ai.pluggy.utils.Asserts.assertNotEmpty; +import static ai.pluggy.utils.Asserts.assertNotNull; + +import java.util.HashMap; +import java.util.List; + +/** + * Query params for GET /v2/transactions (cursor pagination). {@code accountId} is passed + * separately to the service method. The API rejects unknown query params, so only the filters + * below can be sent. + */ +public class TransactionsCursorSearchRequest extends HashMap { + + /** + * @param ids List - list of transaction ids to return + * @return this instance, useful to continue adding params + */ + public TransactionsCursorSearchRequest ids(List ids) { + assertNotEmpty(ids, "ids"); + put("ids", String.join(",", ids)); + return this; + } + + /** + * @param dateFrom String - transaction date lower bound, 'YYYY-MM-DD' or ISO-8601 date. + * Cannot be combined with {@link #createdAtFrom(String)}. + * @return this instance, useful to continue adding params + */ + public TransactionsCursorSearchRequest dateFrom(String dateFrom) { + assertNotNull(dateFrom, "dateFrom"); + assertNotCombined("createdAtFrom", "dateFrom"); + put("dateFrom", dateFrom); + return this; + } + + /** + * @param dateTo String - transaction date upper bound, 'YYYY-MM-DD' or ISO-8601 date. + * @return this instance, useful to continue adding params + */ + public TransactionsCursorSearchRequest dateTo(String dateTo) { + assertNotNull(dateTo, "dateTo"); + put("dateTo", dateTo); + return this; + } + + /** + * @param createdAtFrom String - ISO-8601 timestamp; only transactions created in Pluggy from this + * moment on. Cannot be combined with {@link #dateFrom(String)}. + * @return this instance, useful to continue adding params + */ + public TransactionsCursorSearchRequest createdAtFrom(String createdAtFrom) { + assertNotNull(createdAtFrom, "createdAtFrom"); + assertNotCombined("dateFrom", "createdAtFrom"); + put("createdAtFrom", createdAtFrom); + return this; + } + + /** + * @param after String - opaque cursor from a previous response + * ({@code TransactionsCursorResponse#getNextCursor()}). + * @return this instance, useful to continue adding params + */ + public TransactionsCursorSearchRequest after(String after) { + assertNotNull(after, "after"); + put("after", after); + return this; + } + + private void assertNotCombined(String existing, String adding) { + if (containsKey(existing)) { + throw new IllegalArgumentException( + "'" + adding + "' cannot be combined with '" + existing + "'"); + } + } + + public String getIds() { + return (String) get("ids"); + } + + public String getDateFrom() { + return (String) get("dateFrom"); + } + + public String getDateTo() { + return (String) get("dateTo"); + } + + public String getCreatedAtFrom() { + return (String) get("createdAtFrom"); + } + + public String getAfter() { + return (String) get("after"); + } +} diff --git a/src/main/java/ai/pluggy/client/response/Account.java b/src/main/java/ai/pluggy/client/response/Account.java index 368f549..0a3de90 100644 --- a/src/main/java/ai/pluggy/client/response/Account.java +++ b/src/main/java/ai/pluggy/client/response/Account.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class Account { String id; diff --git a/src/main/java/ai/pluggy/client/response/AccountBalance.java b/src/main/java/ai/pluggy/client/response/AccountBalance.java new file mode 100644 index 0000000..d49eee2 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/AccountBalance.java @@ -0,0 +1,22 @@ +package ai.pluggy.client.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * GET /accounts/{id}/balance response: the account balance fetched in real time from the + * institution. Only available for Open Finance connectors. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AccountBalance { + + Double balance; + String currencyCode; + /** ISO-8601 timestamp of when the institution last updated the balance. */ + String updateDateTime; +} diff --git a/src/main/java/ai/pluggy/client/response/AccountsResponse.java b/src/main/java/ai/pluggy/client/response/AccountsResponse.java index 889553e..c46890f 100644 --- a/src/main/java/ai/pluggy/client/response/AccountsResponse.java +++ b/src/main/java/ai/pluggy/client/response/AccountsResponse.java @@ -2,11 +2,15 @@ import java.util.List; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class AccountsResponse { List results; diff --git a/src/main/java/ai/pluggy/client/response/AdditionalCard.java b/src/main/java/ai/pluggy/client/response/AdditionalCard.java new file mode 100644 index 0000000..4042b3d --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/AdditionalCard.java @@ -0,0 +1,19 @@ +package ai.pluggy.client.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Additional credit card associated with the main one. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AdditionalCard { + + /** Number of the additional credit card. */ + String number; +} diff --git a/src/main/java/ai/pluggy/client/response/Address.java b/src/main/java/ai/pluggy/client/response/Address.java index cd0322d..1000da5 100644 --- a/src/main/java/ai/pluggy/client/response/Address.java +++ b/src/main/java/ai/pluggy/client/response/Address.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class Address { String fullAddress; diff --git a/src/main/java/ai/pluggy/client/response/AuthResponse.java b/src/main/java/ai/pluggy/client/response/AuthResponse.java index a87f658..b244e15 100644 --- a/src/main/java/ai/pluggy/client/response/AuthResponse.java +++ b/src/main/java/ai/pluggy/client/response/AuthResponse.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class AuthResponse { String apiKey; diff --git a/src/main/java/ai/pluggy/client/response/BankData.java b/src/main/java/ai/pluggy/client/response/BankData.java index 537ed26..0c90f85 100644 --- a/src/main/java/ai/pluggy/client/response/BankData.java +++ b/src/main/java/ai/pluggy/client/response/BankData.java @@ -2,11 +2,15 @@ import java.util.List; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class BankData { String transferNumber; diff --git a/src/main/java/ai/pluggy/client/response/Bill.java b/src/main/java/ai/pluggy/client/response/Bill.java index 8ca13ad..192b146 100644 --- a/src/main/java/ai/pluggy/client/response/Bill.java +++ b/src/main/java/ai/pluggy/client/response/Bill.java @@ -1,13 +1,17 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.Date; import java.util.List; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class Bill { String id; @@ -20,4 +24,6 @@ public class Bill { List financeCharges; Date createdAt; Date updatedAt; + /** Payments associated to the bill. */ + List payments; } diff --git a/src/main/java/ai/pluggy/client/response/BillPayment.java b/src/main/java/ai/pluggy/client/response/BillPayment.java new file mode 100644 index 0000000..bbbf972 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/BillPayment.java @@ -0,0 +1,36 @@ +package ai.pluggy.client.response; + +import java.util.Date; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Payment associated to a credit card bill. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BillPayment { + + /** Primary identifier. */ + String id; + + /** Payment value type: {@code INSTALLMENT_PAYMENT}, {@code FULL_PAYMENT} or {@code OTHER_PAYMENT}. */ + String valueType; + + /** Date when the payment was made. */ + Date paymentDate; + + /** Payment mode used: {@code DEBIT_ACCOUNT}, {@code BANK_SLIP}, {@code PAYROLL_DEDUCTION}, {@code PIX}, or null. */ + String paymentMode; + + /** Payment amount. */ + Double amount; + + /** Currency code of the payment (e.g. {@code BRL}). */ + String currencyCode; +} diff --git a/src/main/java/ai/pluggy/client/response/BillsResponse.java b/src/main/java/ai/pluggy/client/response/BillsResponse.java index 2e3a68d..dd0183b 100644 --- a/src/main/java/ai/pluggy/client/response/BillsResponse.java +++ b/src/main/java/ai/pluggy/client/response/BillsResponse.java @@ -1,12 +1,16 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.List; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class BillsResponse { List results; diff --git a/src/main/java/ai/pluggy/client/response/BusinessOtherDocument.java b/src/main/java/ai/pluggy/client/response/BusinessOtherDocument.java index e1ee7d0..ea125e4 100644 --- a/src/main/java/ai/pluggy/client/response/BusinessOtherDocument.java +++ b/src/main/java/ai/pluggy/client/response/BusinessOtherDocument.java @@ -2,14 +2,18 @@ import java.util.Date; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; /** * Additional document for businesses headquartered abroad and not required to register a CNPJ. */ @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class BusinessOtherDocument { /** Type of the document (e.g. 'EIN'). */ diff --git a/src/main/java/ai/pluggy/client/response/BusinessParty.java b/src/main/java/ai/pluggy/client/response/BusinessParty.java index 2654497..4435012 100644 --- a/src/main/java/ai/pluggy/client/response/BusinessParty.java +++ b/src/main/java/ai/pluggy/client/response/BusinessParty.java @@ -2,8 +2,10 @@ import java.util.Date; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; /** * Partner or administrator of a business. @@ -12,6 +14,8 @@ */ @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class BusinessParty { BusinessPartyType type; diff --git a/src/main/java/ai/pluggy/client/response/CategoriesResponse.java b/src/main/java/ai/pluggy/client/response/CategoriesResponse.java index 5dc2bc8..2bc97e6 100644 --- a/src/main/java/ai/pluggy/client/response/CategoriesResponse.java +++ b/src/main/java/ai/pluggy/client/response/CategoriesResponse.java @@ -2,11 +2,15 @@ import java.util.List; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class CategoriesResponse { List results; diff --git a/src/main/java/ai/pluggy/client/response/Category.java b/src/main/java/ai/pluggy/client/response/Category.java index 17741c3..0704ea5 100644 --- a/src/main/java/ai/pluggy/client/response/Category.java +++ b/src/main/java/ai/pluggy/client/response/Category.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class Category { String id; diff --git a/src/main/java/ai/pluggy/client/response/ConnectTokenResponse.java b/src/main/java/ai/pluggy/client/response/ConnectTokenResponse.java index fb9e25f..ba96668 100644 --- a/src/main/java/ai/pluggy/client/response/ConnectTokenResponse.java +++ b/src/main/java/ai/pluggy/client/response/ConnectTokenResponse.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class ConnectTokenResponse { String accessToken; diff --git a/src/main/java/ai/pluggy/client/response/Connector.java b/src/main/java/ai/pluggy/client/response/Connector.java index 4157974..9f0f69c 100644 --- a/src/main/java/ai/pluggy/client/response/Connector.java +++ b/src/main/java/ai/pluggy/client/response/Connector.java @@ -3,11 +3,15 @@ import java.util.Date; import java.util.List; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class Connector { Integer id; String name; @@ -31,4 +35,11 @@ public class Connector { Boolean supportsScheduledPayments; Boolean supportsSmartTransfers; Boolean supportsBoletoManagement; + /** Whether the connector supports automatic Pix. */ + Boolean supportsAutomaticPix; + /** + * Which sub-products the institution serves, in the Open Finance directory's vocabulary (e.g. + * {@code INVESTMENTS:TREASURE_TITLES}). Absent for direct connectors. + */ + List productCoverage; } \ No newline at end of file diff --git a/src/main/java/ai/pluggy/client/response/ConnectorIncident.java b/src/main/java/ai/pluggy/client/response/ConnectorIncident.java index 276c990..98148d7 100644 --- a/src/main/java/ai/pluggy/client/response/ConnectorIncident.java +++ b/src/main/java/ai/pluggy/client/response/ConnectorIncident.java @@ -2,8 +2,10 @@ import java.util.Date; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; /** * An incident affecting a connector right now, as published on @@ -14,6 +16,8 @@ */ @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class ConnectorIncident { String id; diff --git a/src/main/java/ai/pluggy/client/response/ConnectorsResponse.java b/src/main/java/ai/pluggy/client/response/ConnectorsResponse.java index 1f6f188..d117d79 100644 --- a/src/main/java/ai/pluggy/client/response/ConnectorsResponse.java +++ b/src/main/java/ai/pluggy/client/response/ConnectorsResponse.java @@ -2,14 +2,18 @@ import java.util.List; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; /** * GET /connectors response entity */ @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class ConnectorsResponse { List results; diff --git a/src/main/java/ai/pluggy/client/response/CredentialSelectOption.java b/src/main/java/ai/pluggy/client/response/CredentialSelectOption.java index b166f2c..d3f3460 100644 --- a/src/main/java/ai/pluggy/client/response/CredentialSelectOption.java +++ b/src/main/java/ai/pluggy/client/response/CredentialSelectOption.java @@ -3,10 +3,12 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @AllArgsConstructor @Builder +@NoArgsConstructor public class CredentialSelectOption { String label; diff --git a/src/main/java/ai/pluggy/client/response/CreditCardAccountPaymentType.java b/src/main/java/ai/pluggy/client/response/CreditCardAccountPaymentType.java new file mode 100644 index 0000000..eb77a08 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/CreditCardAccountPaymentType.java @@ -0,0 +1,19 @@ +package ai.pluggy.client.response; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Whether a credit card transaction was paid in a single charge or in installments. + */ +@AllArgsConstructor +public enum CreditCardAccountPaymentType { + @SerializedName("SINGLE") + SINGLE("SINGLE"), + @SerializedName("INSTALLMENT") + INSTALLMENT("INSTALLMENT"); + + @Getter + private String value; +} diff --git a/src/main/java/ai/pluggy/client/response/CreditData.java b/src/main/java/ai/pluggy/client/response/CreditData.java index 4168fa9..3cabadc 100644 --- a/src/main/java/ai/pluggy/client/response/CreditData.java +++ b/src/main/java/ai/pluggy/client/response/CreditData.java @@ -1,12 +1,16 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.List; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class CreditData { String level; @@ -21,4 +25,11 @@ public class CreditData { HolderType holderType; CreditCardStatus status; List disaggregatedCreditLimits; + /** Additional credit cards associated with the main one. */ + List additionalCards; + /** + * Whether the credit limit can be adjusted (e.g. the customer can request an increase). Only + * returned when reported by the institution. + */ + Boolean isLimitFlexible; } diff --git a/src/main/java/ai/pluggy/client/response/DeleteItemResponse.java b/src/main/java/ai/pluggy/client/response/DeleteItemResponse.java index 6b2a212..661be51 100644 --- a/src/main/java/ai/pluggy/client/response/DeleteItemResponse.java +++ b/src/main/java/ai/pluggy/client/response/DeleteItemResponse.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class DeleteItemResponse { Integer count; diff --git a/src/main/java/ai/pluggy/client/response/DisaggregatedCreditLimit.java b/src/main/java/ai/pluggy/client/response/DisaggregatedCreditLimit.java index 5c0cb41..b95274b 100644 --- a/src/main/java/ai/pluggy/client/response/DisaggregatedCreditLimit.java +++ b/src/main/java/ai/pluggy/client/response/DisaggregatedCreditLimit.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class DisaggregatedCreditLimit { CreditCardLimitLineName lineName; diff --git a/src/main/java/ai/pluggy/client/response/DocumentNumber.java b/src/main/java/ai/pluggy/client/response/DocumentNumber.java index 74f5630..86248cb 100644 --- a/src/main/java/ai/pluggy/client/response/DocumentNumber.java +++ b/src/main/java/ai/pluggy/client/response/DocumentNumber.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class DocumentNumber { String type; String value; diff --git a/src/main/java/ai/pluggy/client/response/EconomicActivity.java b/src/main/java/ai/pluggy/client/response/EconomicActivity.java new file mode 100644 index 0000000..5c21786 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/EconomicActivity.java @@ -0,0 +1,22 @@ +package ai.pluggy.client.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * CNAE code describing one of the business's economic activities. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EconomicActivity { + + /** CNAE code (7 digits, including leading zeros). */ + String code; + + /** Whether this is the main economic activity. */ + Boolean isMain; +} diff --git a/src/main/java/ai/pluggy/client/response/Email.java b/src/main/java/ai/pluggy/client/response/Email.java index 0a3068a..2d4823d 100644 --- a/src/main/java/ai/pluggy/client/response/Email.java +++ b/src/main/java/ai/pluggy/client/response/Email.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class Email { EmailType type; diff --git a/src/main/java/ai/pluggy/client/response/ErrorDetail.java b/src/main/java/ai/pluggy/client/response/ErrorDetail.java index de17cf9..ff3b9f4 100644 --- a/src/main/java/ai/pluggy/client/response/ErrorDetail.java +++ b/src/main/java/ai/pluggy/client/response/ErrorDetail.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class ErrorDetail { String code; diff --git a/src/main/java/ai/pluggy/client/response/ErrorResponse.java b/src/main/java/ai/pluggy/client/response/ErrorResponse.java index d1ce3ca..4d36324 100644 --- a/src/main/java/ai/pluggy/client/response/ErrorResponse.java +++ b/src/main/java/ai/pluggy/client/response/ErrorResponse.java @@ -11,5 +11,10 @@ public class ErrorResponse { String message; Integer code; + /** + * Machine-readable error identifier, when the API provides one (e.g. + * {@code LIST_ITEMS_FEATURE_NOT_ENABLED}). Null for errors that don't carry it. + */ + String codeDescription; List details; } diff --git a/src/main/java/ai/pluggy/client/response/Expenses.java b/src/main/java/ai/pluggy/client/response/Expenses.java index 0ab8384..dfb06fb 100644 --- a/src/main/java/ai/pluggy/client/response/Expenses.java +++ b/src/main/java/ai/pluggy/client/response/Expenses.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class Expenses { String id; String transactionId; diff --git a/src/main/java/ai/pluggy/client/response/FinancialCharge.java b/src/main/java/ai/pluggy/client/response/FinancialCharge.java index bcf8db0..c05aff8 100644 --- a/src/main/java/ai/pluggy/client/response/FinancialCharge.java +++ b/src/main/java/ai/pluggy/client/response/FinancialCharge.java @@ -1,12 +1,16 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.Date; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class FinancialCharge { String id; String type; diff --git a/src/main/java/ai/pluggy/client/response/GeographicCoordinates.java b/src/main/java/ai/pluggy/client/response/GeographicCoordinates.java index 8b16286..52eabef 100644 --- a/src/main/java/ai/pluggy/client/response/GeographicCoordinates.java +++ b/src/main/java/ai/pluggy/client/response/GeographicCoordinates.java @@ -1,13 +1,17 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; /** * Geographic coordinates in decimal degrees, WGS84 reference system. */ @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class GeographicCoordinates { /** Between -90 and 90. */ diff --git a/src/main/java/ai/pluggy/client/response/Health.java b/src/main/java/ai/pluggy/client/response/Health.java index 1d069e9..b3150f3 100644 --- a/src/main/java/ai/pluggy/client/response/Health.java +++ b/src/main/java/ai/pluggy/client/response/Health.java @@ -2,11 +2,15 @@ import java.util.List; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class Health { ConnectorStatus status; diff --git a/src/main/java/ai/pluggy/client/response/IdentityFinancialRelationshipAccount.java b/src/main/java/ai/pluggy/client/response/IdentityFinancialRelationshipAccount.java new file mode 100644 index 0000000..72fa455 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/IdentityFinancialRelationshipAccount.java @@ -0,0 +1,34 @@ +package ai.pluggy.client.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Account of the client with valid consent. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IdentityFinancialRelationshipAccount { + + /** COMPE code of the account. */ + String compeCode; + + /** Branch code of the account. */ + String branchCode; + + /** Number of the account. */ + String number; + + /** Check digit of the account. */ + String checkDigit; + + /** Type of the account. */ + String type; + + /** Subtype of the account. */ + String subtype; +} diff --git a/src/main/java/ai/pluggy/client/response/IdentityFinancialRelationships.java b/src/main/java/ai/pluggy/client/response/IdentityFinancialRelationships.java new file mode 100644 index 0000000..c8132a5 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/IdentityFinancialRelationships.java @@ -0,0 +1,40 @@ +package ai.pluggy.client.response; + +import java.util.Date; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Information that allows institutions to assess the client's risk profile and economic-financial capacity. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IdentityFinancialRelationships { + + /** Date when the relationship with the institution started. */ + Date startDate; + + /** Products and services the client consumes. */ + List productsServicesType; + + /** Additional info about the products and services, when {@code productsServicesType} includes {@code OUTROS}. */ + String productsServicesTypeAdditionalInfo; + + /** Procurators of the client. */ + List procurators; + + /** Accounts of the client with valid consent. Only accounts with explicit user consent are returned. */ + List accounts; + + /** Salary portabilities received from the client's previous paycheck banks (PF-only). */ + List portabilitiesReceived; + + /** Paycheck-bank links to employers, active or formerly active (PF-only). */ + List paychecksBankLink; +} diff --git a/src/main/java/ai/pluggy/client/response/IdentityProcurator.java b/src/main/java/ai/pluggy/client/response/IdentityProcurator.java new file mode 100644 index 0000000..376e160 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/IdentityProcurator.java @@ -0,0 +1,34 @@ +package ai.pluggy.client.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Legal representative or procurator of the client. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IdentityProcurator { + + /** Type of relationship: {@code REPRESENTANTE_LEGAL} or {@code PROCURADOR}. */ + String type; + + /** CPF of the procurator. For business links it may hold a CPF or a CNPJ; prefer {@link #documentNumber}. */ + String cpfNumber; + + /** Document number of the procurator (CPF or CNPJ). */ + String documentNumber; + + /** Type of document carried by {@link #documentNumber}: {@code CPF} or {@code CNPJ}. */ + String documentType; + + /** Civil name of the procurator (company name for business procurators). */ + String civilName; + + /** Social name of the procurator, if any. */ + String socialName; +} diff --git a/src/main/java/ai/pluggy/client/response/IdentityQualifications.java b/src/main/java/ai/pluggy/client/response/IdentityQualifications.java new file mode 100644 index 0000000..da0e6b2 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/IdentityQualifications.java @@ -0,0 +1,39 @@ +package ai.pluggy.client.response; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Qualification data of the client: occupation, informed income/patrimony and, for businesses, economic activities and revenue. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IdentityQualifications { + + /** CNPJ of the company. */ + String companyCnpj; + + /** Occupation code: {@code RECEITA_FEDERAL}, {@code CBO} or {@code OUTRO}. */ + String occupationCode; + + /** Occupation description (list code for {@code RECEITA_FEDERAL}/{@code CBO}, free text for {@code OUTRO}). */ + String occupationDescription; + + /** Informed income. */ + InformedIncome informedIncome; + + /** Informed patrimony. */ + InformedPatrimony informedPatrimony; + + /** CNAE codes describing the business's economic activities. */ + List economicActivities; + + /** Revenue informed by the business. */ + InformedRevenue informedRevenue; +} diff --git a/src/main/java/ai/pluggy/client/response/IdentityRelation.java b/src/main/java/ai/pluggy/client/response/IdentityRelation.java index 3b2e17c..fa7111f 100644 --- a/src/main/java/ai/pluggy/client/response/IdentityRelation.java +++ b/src/main/java/ai/pluggy/client/response/IdentityRelation.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class IdentityRelation { // 'Mother' | 'Father' | 'Spouse' diff --git a/src/main/java/ai/pluggy/client/response/IdentityResponse.java b/src/main/java/ai/pluggy/client/response/IdentityResponse.java index 2b8d475..857b1d7 100644 --- a/src/main/java/ai/pluggy/client/response/IdentityResponse.java +++ b/src/main/java/ai/pluggy/client/response/IdentityResponse.java @@ -3,14 +3,18 @@ import java.util.Date; import java.util.List; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; /** * GET /identity */ @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class IdentityResponse { String id; @@ -24,6 +28,11 @@ public class IdentityResponse { List

addresses; List phoneNumbers; List emails; + /** + * @deprecated the API sends this list as {@code relations}, so this field is never filled. Use + * {@link #getRelations()}. + */ + @Deprecated List identityRelations; Date createdAt; Date updatedAt; @@ -51,4 +60,17 @@ public class IdentityResponse { List businessOtherDocuments; /** CNPJs of the financial institutions responsible for the customer cadastro. */ List companiesCnpj; + /** Establishment code (only for PAYMENT_ACCOUNT connectors). */ + String establishmentCode; + /** Name of the establishment (only for PAYMENT_ACCOUNT connectors). */ + String establishmentName; + /** Names related to the account owner (mother, father, spouse). */ + List relations; + /** + * Information to assess the client's risk profile and economic-financial capacity: relationship + * start date, products consumed, procurators, consented accounts and paycheck-bank links. + */ + IdentityFinancialRelationships financialRelationships; + /** Occupation, informed income/patrimony and, for businesses, economic activities and revenue. */ + IdentityQualifications qualifications; } diff --git a/src/main/java/ai/pluggy/client/response/InformedIncome.java b/src/main/java/ai/pluggy/client/response/InformedIncome.java new file mode 100644 index 0000000..89a450f --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/InformedIncome.java @@ -0,0 +1,27 @@ +package ai.pluggy.client.response; + +import java.util.Date; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Income informed by the client. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InformedIncome { + + /** Frequency: {@code DIARIA}, {@code SEMANAL}, {@code QUINZENAL}, {@code MENSAL}, {@code BIMESTRAL}, {@code TRIMESTRAL}, {@code SEMESTRAL}, {@code ANUAL} or {@code OUTROS}. */ + String frequency; + + /** Amount of the informed income. */ + Double amount; + + /** Date when the income was informed. */ + Date date; +} diff --git a/src/main/java/ai/pluggy/client/response/InformedPatrimony.java b/src/main/java/ai/pluggy/client/response/InformedPatrimony.java new file mode 100644 index 0000000..5456fb4 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/InformedPatrimony.java @@ -0,0 +1,27 @@ +package ai.pluggy.client.response; + +import java.util.Date; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Patrimony informed by the client. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InformedPatrimony { + + /** Amount of the informed patrimony. */ + Double amount; + + /** Year of the patrimony. */ + Integer year; + + /** Reference date of the patrimony (business path only). */ + Date date; +} diff --git a/src/main/java/ai/pluggy/client/response/InformedRevenue.java b/src/main/java/ai/pluggy/client/response/InformedRevenue.java new file mode 100644 index 0000000..55bd01b --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/InformedRevenue.java @@ -0,0 +1,28 @@ +package ai.pluggy.client.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Revenue (faturamento) informed by the business. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InformedRevenue { + + /** Amount of the informed revenue. */ + Double amount; + + /** Frequency: {@code DAILY}, {@code WEEKLY}, {@code BIWEEKLY}, {@code MONTHLY}, {@code BIMONTHLY}, {@code QUARTERLY}, {@code SEMIANNUAL}, {@code ANNUAL} or {@code OTHER}. */ + String frequency; + + /** Free-text complement to the frequency, when it is {@code OTHER}. */ + String frequencyAdditionalInfo; + + /** Reference year of the revenue. */ + Integer year; +} diff --git a/src/main/java/ai/pluggy/client/response/Investment.java b/src/main/java/ai/pluggy/client/response/Investment.java index 61a628f..b0e8f8c 100644 --- a/src/main/java/ai/pluggy/client/response/Investment.java +++ b/src/main/java/ai/pluggy/client/response/Investment.java @@ -2,11 +2,15 @@ import java.util.Date; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class Investment { String id; String itemId; @@ -33,6 +37,10 @@ public class Investment { Double taxes; Double taxes2; Double amountOriginal; + /** + * Profit/Loss to date over the investment. The API sends a number; kept as String (Gson reads + * the number into it) so existing callers don't break. Parse with {@code Double.valueOf}. + */ String amountProfit = null; Double amountWithdrawal; String issuer; @@ -59,4 +67,14 @@ public class Investment { InvestmentMetadata metadata; String providerId; InvestmentInstitution institution; + /** The date when the grace period ends (fixed-income investments only). */ + Date gracePeriodDate; + /** B3 lot/price conversion factor (variable income). */ + Double priceFactor; + /** Whether the product is tax-exempt (LCI, LCA, CRI, CRA, incentivized debentures). */ + Boolean taxExempt; + /** Coupon-payment schedule for coupon-bearing fixed income / Treasury bonds. */ + InvestmentCouponPayment couponPayment; + /** Underlying debtor of receivables-backed paper (CRI / CRA). */ + InvestmentDebtor debtor; } diff --git a/src/main/java/ai/pluggy/client/response/InvestmentCouponPayment.java b/src/main/java/ai/pluggy/client/response/InvestmentCouponPayment.java new file mode 100644 index 0000000..b599db5 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/InvestmentCouponPayment.java @@ -0,0 +1,25 @@ +package ai.pluggy.client.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Coupon-payment schedule for coupon-bearing fixed income / Treasury bonds. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InvestmentCouponPayment { + + /** Whether the paper pays periodic coupons. */ + Boolean hasCoupon; + + /** Frequency of coupon payments: {@code MONTHLY}, {@code QUARTERLY}, {@code SEMESTERLY}, {@code YEARLY} or {@code IRREGULAR}. */ + String periodicity; + + /** Free-text detail when periodicity is {@code IRREGULAR}. */ + String additionalInfo; +} diff --git a/src/main/java/ai/pluggy/client/response/InvestmentDebtor.java b/src/main/java/ai/pluggy/client/response/InvestmentDebtor.java new file mode 100644 index 0000000..c0a6ce9 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/InvestmentDebtor.java @@ -0,0 +1,19 @@ +package ai.pluggy.client.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Underlying debtor of receivables-backed paper (CRI / CRA). + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InvestmentDebtor { + + /** Name of the underlying debtor. */ + String name; +} diff --git a/src/main/java/ai/pluggy/client/response/InvestmentInstitution.java b/src/main/java/ai/pluggy/client/response/InvestmentInstitution.java index edf7c32..f6716e5 100644 --- a/src/main/java/ai/pluggy/client/response/InvestmentInstitution.java +++ b/src/main/java/ai/pluggy/client/response/InvestmentInstitution.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class InvestmentInstitution { String name; String number; diff --git a/src/main/java/ai/pluggy/client/response/InvestmentMetadata.java b/src/main/java/ai/pluggy/client/response/InvestmentMetadata.java index 9c382e4..12e59fa 100644 --- a/src/main/java/ai/pluggy/client/response/InvestmentMetadata.java +++ b/src/main/java/ai/pluggy/client/response/InvestmentMetadata.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class InvestmentMetadata { String taxRegime; String proposalNumber; diff --git a/src/main/java/ai/pluggy/client/response/InvestmentTransaction.java b/src/main/java/ai/pluggy/client/response/InvestmentTransaction.java index af86bb5..34f0498 100644 --- a/src/main/java/ai/pluggy/client/response/InvestmentTransaction.java +++ b/src/main/java/ai/pluggy/client/response/InvestmentTransaction.java @@ -2,11 +2,15 @@ import java.util.Date; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class InvestmentTransaction { String id; InvestmentTransactionType type; diff --git a/src/main/java/ai/pluggy/client/response/InvestmentsResponse.java b/src/main/java/ai/pluggy/client/response/InvestmentsResponse.java index f1c3876..eebc1b5 100644 --- a/src/main/java/ai/pluggy/client/response/InvestmentsResponse.java +++ b/src/main/java/ai/pluggy/client/response/InvestmentsResponse.java @@ -1,11 +1,15 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.List; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class InvestmentsResponse { List results; diff --git a/src/main/java/ai/pluggy/client/response/ItemError.java b/src/main/java/ai/pluggy/client/response/ItemError.java index 5e7589c..3dfcb7c 100644 --- a/src/main/java/ai/pluggy/client/response/ItemError.java +++ b/src/main/java/ai/pluggy/client/response/ItemError.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class ItemError { String message; diff --git a/src/main/java/ai/pluggy/client/response/ItemProductState.java b/src/main/java/ai/pluggy/client/response/ItemProductState.java index cde3205..0abfecb 100644 --- a/src/main/java/ai/pluggy/client/response/ItemProductState.java +++ b/src/main/java/ai/pluggy/client/response/ItemProductState.java @@ -2,11 +2,15 @@ import java.util.Date; import java.util.List; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class ItemProductState { /** Whether product was collected in this last execution or not */ boolean isUpdated; diff --git a/src/main/java/ai/pluggy/client/response/ItemProductStepWarning.java b/src/main/java/ai/pluggy/client/response/ItemProductStepWarning.java index 6f0a35c..39ae7fc 100644 --- a/src/main/java/ai/pluggy/client/response/ItemProductStepWarning.java +++ b/src/main/java/ai/pluggy/client/response/ItemProductStepWarning.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class ItemProductStepWarning { /** The specific warning code */ String code; diff --git a/src/main/java/ai/pluggy/client/response/ItemResource.java b/src/main/java/ai/pluggy/client/response/ItemResource.java index 3e60201..f927d42 100644 --- a/src/main/java/ai/pluggy/client/response/ItemResource.java +++ b/src/main/java/ai/pluggy/client/response/ItemResource.java @@ -1,7 +1,9 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; /** * One resource the financial institution declared for an item's Open Finance consent, reported @@ -9,6 +11,8 @@ */ @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class ItemResource { /** The institution's identifier for the resource. */ diff --git a/src/main/java/ai/pluggy/client/response/ItemResponse.java b/src/main/java/ai/pluggy/client/response/ItemResponse.java index 17f4fae..ace5626 100644 --- a/src/main/java/ai/pluggy/client/response/ItemResponse.java +++ b/src/main/java/ai/pluggy/client/response/ItemResponse.java @@ -2,11 +2,15 @@ import java.util.Date; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class ItemResponse { String id; @@ -38,4 +42,13 @@ public class ItemResponse { * while the resource list has not been read yet ({@code resourcesCollectedAt} is null). */ Boolean hasResourcesPendingAuthorization; + + /** Consent expiration date (Open Finance items). */ + Date consentExpiresAt; + + /** Date of the next auto-sync, or null if auto-sync is disabled for this item. */ + Date nextAutoSyncAt; + + /** User action the item is waiting on (e.g. a QR code to scan), when there is one. */ + ItemUserAction userAction; } diff --git a/src/main/java/ai/pluggy/client/response/ItemStatusDetail.java b/src/main/java/ai/pluggy/client/response/ItemStatusDetail.java index d7d64d6..1c5b02d 100644 --- a/src/main/java/ai/pluggy/client/response/ItemStatusDetail.java +++ b/src/main/java/ai/pluggy/client/response/ItemStatusDetail.java @@ -1,7 +1,9 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; /** * Only available when item.status is 'PARTIAL_SUCCESS'. @@ -17,6 +19,8 @@ */ @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class ItemStatusDetail { /** * Collection details for 'ACCOUNTS' product, or null if it was not requested at diff --git a/src/main/java/ai/pluggy/client/response/ItemUserAction.java b/src/main/java/ai/pluggy/client/response/ItemUserAction.java new file mode 100644 index 0000000..631cd09 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/ItemUserAction.java @@ -0,0 +1,31 @@ +package ai.pluggy.client.response; + +import java.util.Date; +import java.util.Map; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * User action the item is waiting on (for example, scanning a QR code or authorizing access in the institution's app). + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ItemUserAction { + + /** Instructions related to the user action. */ + String instructions; + + /** Additional information related to the user action, for example in a device authorization flow. */ + Map attributes; + + /** User action expiration date. */ + Date expiresAt; + + /** Kind of action the user has to take: {@code qr} or {@code authorize-access}. */ + String type; +} diff --git a/src/main/java/ai/pluggy/client/response/ItemsCursorResponse.java b/src/main/java/ai/pluggy/client/response/ItemsCursorResponse.java new file mode 100644 index 0000000..ddec49f --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/ItemsCursorResponse.java @@ -0,0 +1,46 @@ +package ai.pluggy.client.response; + +import ai.pluggy.utils.Cursors; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * GET /v2/items response: one cursor-paginated page of items, newest first. + * + *

Availability: opt-in, paid plans only. Listing items is disabled by default and is + * only available to paid-plan teams that have explicitly requested it from Pluggy support. Teams + * without it enabled get {@code 403 LIST_ITEMS_FEATURE_NOT_ENABLED}. For most integrations, store + * each itemId when it is created (Pluggy Connect {@code onSuccess} or the {@code item/created} + * webhook) and use {@code getItem(id)} instead. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ItemsCursorResponse { + + List results; + /** + * Query string to fetch the next page (e.g. {@code ?clientUserId=...&after=}), or null + * on the last page. Use {@link #getNextCursor()} to extract the cursor. + */ + String next; + + /** + * @return the URL-decoded {@code after} cursor to request the next page with, or null when this + * is the last page. + */ + public String getNextCursor() { + return Cursors.parseAfterCursor(next); + } + + /** + * @return true when there is another page to fetch + */ + public boolean hasNext() { + return getNextCursor() != null; + } +} diff --git a/src/main/java/ai/pluggy/client/response/MaritalStatus.java b/src/main/java/ai/pluggy/client/response/MaritalStatus.java index e0e5377..39ef7ee 100644 --- a/src/main/java/ai/pluggy/client/response/MaritalStatus.java +++ b/src/main/java/ai/pluggy/client/response/MaritalStatus.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class MaritalStatus { MaritalStatusCode code; diff --git a/src/main/java/ai/pluggy/client/response/Nationality.java b/src/main/java/ai/pluggy/client/response/Nationality.java index aa8c8fd..3f72795 100644 --- a/src/main/java/ai/pluggy/client/response/Nationality.java +++ b/src/main/java/ai/pluggy/client/response/Nationality.java @@ -2,11 +2,15 @@ import java.util.List; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class Nationality { Boolean hasBrazilianNationality; diff --git a/src/main/java/ai/pluggy/client/response/NationalityDocument.java b/src/main/java/ai/pluggy/client/response/NationalityDocument.java index 74c716e..e421342 100644 --- a/src/main/java/ai/pluggy/client/response/NationalityDocument.java +++ b/src/main/java/ai/pluggy/client/response/NationalityDocument.java @@ -2,11 +2,15 @@ import java.util.Date; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class NationalityDocument { String type; diff --git a/src/main/java/ai/pluggy/client/response/OtherDocument.java b/src/main/java/ai/pluggy/client/response/OtherDocument.java index c7a9138..9364ed6 100644 --- a/src/main/java/ai/pluggy/client/response/OtherDocument.java +++ b/src/main/java/ai/pluggy/client/response/OtherDocument.java @@ -2,11 +2,15 @@ import java.util.Date; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class OtherDocument { OtherDocumentType type; diff --git a/src/main/java/ai/pluggy/client/response/OtherNationality.java b/src/main/java/ai/pluggy/client/response/OtherNationality.java index 09acb6e..79c0635 100644 --- a/src/main/java/ai/pluggy/client/response/OtherNationality.java +++ b/src/main/java/ai/pluggy/client/response/OtherNationality.java @@ -2,11 +2,15 @@ import java.util.List; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class OtherNationality { /** Country code in alpha3 ISO-3166 format. */ diff --git a/src/main/java/ai/pluggy/client/response/Passport.java b/src/main/java/ai/pluggy/client/response/Passport.java index a8b43d7..15f8169 100644 --- a/src/main/java/ai/pluggy/client/response/Passport.java +++ b/src/main/java/ai/pluggy/client/response/Passport.java @@ -2,11 +2,15 @@ import java.util.Date; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class Passport { String number; diff --git a/src/main/java/ai/pluggy/client/response/PaycheckBankLink.java b/src/main/java/ai/pluggy/client/response/PaycheckBankLink.java new file mode 100644 index 0000000..b2de7b4 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/PaycheckBankLink.java @@ -0,0 +1,33 @@ +package ai.pluggy.client.response; + +import java.util.Date; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Paycheck-bank (banco-folha) link to an employer, active or formerly active. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PaycheckBankLink { + + /** Employer name as registered when the paycheck account was opened. */ + String employerName; + + /** Employer document (CPF or CNPJ). */ + String employerDocument; + + /** CNPJ of the institution providing the paycheck service. */ + String paycheckBankCnpj; + + /** ISPB of the institution providing the paycheck service. */ + String paycheckBankIspb; + + /** Date the paycheck account was opened. */ + Date accountOpeningDate; +} diff --git a/src/main/java/ai/pluggy/client/response/PaymentData.java b/src/main/java/ai/pluggy/client/response/PaymentData.java index c95f2c3..e39cd97 100644 --- a/src/main/java/ai/pluggy/client/response/PaymentData.java +++ b/src/main/java/ai/pluggy/client/response/PaymentData.java @@ -10,4 +10,11 @@ public class PaymentData { String referenceNumber; String reason; BoletoMetadata boletoMetadata; + /** + * Authentication code of the payment receipt, as printed by the institution on the proof of + * payment. Can be present for any payment method. + */ + String authenticationCode; + /** String submitted by the receiver when generating the payment request. */ + String receiverReferenceId; } diff --git a/src/main/java/ai/pluggy/client/response/PortabilityReceived.java b/src/main/java/ai/pluggy/client/response/PortabilityReceived.java new file mode 100644 index 0000000..e40f3bb --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/PortabilityReceived.java @@ -0,0 +1,33 @@ +package ai.pluggy.client.response; + +import java.util.Date; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Salary portability received by the institution from a previous paycheck bank (banco-folha). Approved requests only; an entry does not guarantee an active link. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PortabilityReceived { + + /** Employer name as received in the portability message. */ + String employerName; + + /** Employer document (CPF or CNPJ). */ + String employerDocument; + + /** CNPJ of the bank that holds the paycheck account. */ + String paycheckBankDetainerCnpj; + + /** ISPB of the bank that holds the paycheck account. */ + String paycheckBankDetainerIspb; + + /** Date the portability was approved. */ + Date portabilityApprovalDate; +} diff --git a/src/main/java/ai/pluggy/client/response/ReservedBalance.java b/src/main/java/ai/pluggy/client/response/ReservedBalance.java index 220a0ae..b84281c 100644 --- a/src/main/java/ai/pluggy/client/response/ReservedBalance.java +++ b/src/main/java/ai/pluggy/client/response/ReservedBalance.java @@ -2,11 +2,15 @@ import java.util.List; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class ReservedBalance { String name; diff --git a/src/main/java/ai/pluggy/client/response/ReservedBalanceAmount.java b/src/main/java/ai/pluggy/client/response/ReservedBalanceAmount.java index 95273a8..14d480c 100644 --- a/src/main/java/ai/pluggy/client/response/ReservedBalanceAmount.java +++ b/src/main/java/ai/pluggy/client/response/ReservedBalanceAmount.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class ReservedBalanceAmount { Double amount; diff --git a/src/main/java/ai/pluggy/client/response/ReservedBalanceRemuneration.java b/src/main/java/ai/pluggy/client/response/ReservedBalanceRemuneration.java index 7aeb3ad..81bee79 100644 --- a/src/main/java/ai/pluggy/client/response/ReservedBalanceRemuneration.java +++ b/src/main/java/ai/pluggy/client/response/ReservedBalanceRemuneration.java @@ -1,10 +1,14 @@ package ai.pluggy.client.response; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder +@NoArgsConstructor +@AllArgsConstructor public class ReservedBalanceRemuneration { Double preFixedRate; diff --git a/src/main/java/ai/pluggy/client/response/ScrAdditionalInfo.java b/src/main/java/ai/pluggy/client/response/ScrAdditionalInfo.java new file mode 100644 index 0000000..ba944b5 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/ScrAdditionalInfo.java @@ -0,0 +1,25 @@ +package ai.pluggy.client.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Complementary information reported for an SCR operation group. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ScrAdditionalInfo { + + /** Type of the complementary information. */ + String tp; + + /** Code of the complementary information. */ + String cd; + + /** Number of operations grouped under this entry. */ + Integer qtd; +} diff --git a/src/main/java/ai/pluggy/client/response/ScrDatabase.java b/src/main/java/ai/pluggy/client/response/ScrDatabase.java new file mode 100644 index 0000000..34fa078 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/ScrDatabase.java @@ -0,0 +1,48 @@ +package ai.pluggy.client.response; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * One consulted SCR base date, and what the SCR holds for the client in it. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ScrDatabase { + + /** The base date, as YYYYMM (numeric). */ + Integer dtb; + + /** Bacen's message for this base date, when it has one. */ + String msg; + + /** Percentage of the expected 3040 documents already incorporated by Bacen for this base date. */ + String docProc; + + /** Percentage of the expected operation volume already accepted for this base date. */ + String volProc; + + /** Number of financial institutions where the client has operations. */ + Integer qtdIfs; + + /** Number of financial conglomerates where the client has operations. */ + Integer qtdCongFinc; + + /** Start of the client's relationship with the national financial system. */ + String dtbIniRel; + + /** Co-obligation assumed by the client in credit assignments, in BRL. */ + Double coobAss; + + /** Co-obligation received in credit assignments, in BRL. */ + Double coobRec; + + /** Operation groups reported for this base date. */ + List lsOp; +} diff --git a/src/main/java/ai/pluggy/client/response/ScrGuarantee.java b/src/main/java/ai/pluggy/client/response/ScrGuarantee.java new file mode 100644 index 0000000..153c7a9 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/ScrGuarantee.java @@ -0,0 +1,22 @@ +package ai.pluggy.client.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Guarantee backing an SCR operation group. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ScrGuarantee { + + /** Bacen's code for the guarantee type. */ + String tp; + + /** Number of operations grouped under this type. */ + Integer qtd; +} diff --git a/src/main/java/ai/pluggy/client/response/ScrMaturityBalances.java b/src/main/java/ai/pluggy/client/response/ScrMaturityBalances.java new file mode 100644 index 0000000..2a1785b --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/ScrMaturityBalances.java @@ -0,0 +1,76 @@ +package ai.pluggy.client.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Balance of an SCR operation group split across Bacen's maturity vertices, in BRL. Each field is a vertex code as defined by Bacen's DOC3040 reference; only the vertices that carry a value are present (others are null). + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ScrMaturityBalances { + + Double v20; + + Double v40; + + Double v60; + + Double v80; + + Double v110; + + Double v120; + + Double v130; + + Double v140; + + Double v150; + + Double v160; + + Double v165; + + Double v170; + + Double v175; + + Double v180; + + Double v190; + + Double v199; + + Double v205; + + Double v210; + + Double v220; + + Double v230; + + Double v240; + + Double v245; + + Double v250; + + Double v255; + + Double v260; + + Double v270; + + Double v280; + + Double v290; + + Double v310; + + Double v320; +} diff --git a/src/main/java/ai/pluggy/client/response/ScrOperation.java b/src/main/java/ai/pluggy/client/response/ScrOperation.java new file mode 100644 index 0000000..ab4c643 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/ScrOperation.java @@ -0,0 +1,42 @@ +package ai.pluggy.client.response; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A group of SCR credit operations, aggregated by modality, source of funds, index and exchange variation. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ScrOperation { + + /** Bacen's code for the operation modality. */ + String mod; + + /** Bacen's code for the source of funds. */ + String oriRec; + + /** Bacen's code for the reference rate or index. */ + String indx; + + /** Bacen's code for the exchange rate variation. */ + String varCamb; + + /** Present when the operation is under dispute: {@code D}, {@code J} or {@code JD}. */ + String subJDisc; + + /** Balance of the group split across Bacen's maturity vertices. */ + ScrMaturityBalances resVenc; + + /** Guarantees backing the operations in this group. */ + List lsGar; + + /** Complementary information reported for this group. */ + List lsInfAd; +} diff --git a/src/main/java/ai/pluggy/client/response/ScrResponse.java b/src/main/java/ai/pluggy/client/response/ScrResponse.java new file mode 100644 index 0000000..eef8179 --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/ScrResponse.java @@ -0,0 +1,33 @@ +package ai.pluggy.client.response; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Bacen's SCR (Sistema de Informações de Crédito) response, forwarded exactly as the Banco Central returns it: field names and codes are not renamed or normalised. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ScrResponse { + + /** The base dates consulted (e.g. {@code 202604 a 202607}). */ + String dtbConsult; + + /** The consulted document: the CPF for an individual, or the 8-digit CNPJ root for a company. */ + String cdCli; + + /** Type of client: {@code "1"} for an individual, {@code "2"} for a legal entity. */ + String tpCli; + + /** One entry per consulted base date. A base date with no data is still listed, with no operations. */ + List lsDtb; + + /** Validation messages raised by Bacen for this request. */ + List listaDeMensagensDeValidacao; +} diff --git a/src/main/java/ai/pluggy/client/response/ScrValidationMessage.java b/src/main/java/ai/pluggy/client/response/ScrValidationMessage.java new file mode 100644 index 0000000..24b7a3f --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/ScrValidationMessage.java @@ -0,0 +1,22 @@ +package ai.pluggy.client.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Validation message raised by Bacen for an SCR request. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ScrValidationMessage { + + /** Message code. */ + String codigo; + + /** Message text. */ + String mensagem; +} diff --git a/src/main/java/ai/pluggy/client/response/Transaction.java b/src/main/java/ai/pluggy/client/response/Transaction.java index 57ac323..0a0f9af 100644 --- a/src/main/java/ai/pluggy/client/response/Transaction.java +++ b/src/main/java/ai/pluggy/client/response/Transaction.java @@ -24,4 +24,9 @@ public class Transaction { String operationType; String operationTypeAdditionalInfo; String providerId; + /** + * Sequential position of the transaction within the same day, used to preserve ordering when + * multiple transactions share the same date. + */ + Integer order; } diff --git a/src/main/java/ai/pluggy/client/response/TransactionCreditCardMetadata.java b/src/main/java/ai/pluggy/client/response/TransactionCreditCardMetadata.java index 654f36d..c2ebdaa 100644 --- a/src/main/java/ai/pluggy/client/response/TransactionCreditCardMetadata.java +++ b/src/main/java/ai/pluggy/client/response/TransactionCreditCardMetadata.java @@ -18,4 +18,21 @@ public class TransactionCreditCardMetadata { CreditCardAccountOtherCreditType otherCreditsType; String otherCreditsAdditionalInfo; String billForecastDate; + /** + * Whether the transaction was a single charge or an installment. + * Only returned for Open Finance connectors; not retroactive. + */ + CreditCardAccountPaymentType paymentType; + /** + * Date the institution posted the transaction to a bill, in 'YYYY-MM-DD' format, as reported + * by the institution. Null when the transaction was not posted to a bill yet or the institution + * does not report it. Kept as a String to avoid timezone shifts on a date-only value. + * Only returned for Open Finance connectors; not retroactive. + */ + String billPostDate; + /** + * ISO-8601 timestamp of the transaction, as reported by the institution. + * Only returned for Open Finance connectors; not retroactive. + */ + String transactionDateTime; } diff --git a/src/main/java/ai/pluggy/client/response/TransactionsCursorResponse.java b/src/main/java/ai/pluggy/client/response/TransactionsCursorResponse.java new file mode 100644 index 0000000..00aa9ae --- /dev/null +++ b/src/main/java/ai/pluggy/client/response/TransactionsCursorResponse.java @@ -0,0 +1,50 @@ +package ai.pluggy.client.response; + +import ai.pluggy.utils.Cursors; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * GET /v2/transactions response: one cursor-paginated page of transactions. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TransactionsCursorResponse { + + List results; + /** + * Query string to fetch the next page (e.g. {@code ?accountId=...&after=}), or null on + * the last page. Use {@link #getNextCursor()} to extract the cursor. + */ + String next; + + /** + * @return the URL-decoded {@code after} cursor to request the next page with, or null when this + * is the last page. + */ + public String getNextCursor() { + return parseAfterCursor(next); + } + + /** + * @return true when there is another page to fetch + */ + public boolean hasNext() { + return getNextCursor() != null; + } + + /** + * Extracts the URL-decoded {@code after} parameter from a {@code next} query string. + * + * @param next String - the {@code next} value of a /v2/transactions response + * @return the cursor, or null if {@code next} is null or has no {@code after} parameter + */ + public static String parseAfterCursor(String next) { + return Cursors.parseAfterCursor(next); + } +} diff --git a/src/main/java/ai/pluggy/utils/Cursors.java b/src/main/java/ai/pluggy/utils/Cursors.java new file mode 100644 index 0000000..2ad5bc1 --- /dev/null +++ b/src/main/java/ai/pluggy/utils/Cursors.java @@ -0,0 +1,51 @@ +package ai.pluggy.utils; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +/** + * Helpers for the cursor-paginated endpoints ({@code GET /v2/transactions}, + * {@code GET /v2/items}), whose responses carry a {@code next} query string. + */ +public final class Cursors { + + private Cursors() { + } + + /** + * Extracts the URL-decoded {@code after} parameter from a {@code next} query string. + * + * @param next String - the {@code next} value of a cursor-paginated response + * @return the cursor, or null if {@code next} is null or has no {@code after} parameter + */ + public static String parseAfterCursor(String next) { + if (next == null || next.isEmpty()) { + return null; + } + String query = next; + int questionMark = query.indexOf('?'); + if (questionMark >= 0) { + query = query.substring(questionMark + 1); + } + int fragment = query.indexOf('#'); + if (fragment >= 0) { + query = query.substring(0, fragment); + } + for (String pair : query.split("&")) { + int eq = pair.indexOf('='); + String key = eq >= 0 ? pair.substring(0, eq) : pair; + if ("after".equals(key)) { + String value = eq >= 0 ? pair.substring(eq + 1) : ""; + if (value.isEmpty()) { + return null; + } + try { + return URLDecoder.decode(value, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } + } + return null; + } +} diff --git a/src/test/java/ai/pluggy/client/unit/BuilderNoArgsConstructorTest.java b/src/test/java/ai/pluggy/client/unit/BuilderNoArgsConstructorTest.java new file mode 100644 index 0000000..71bd345 --- /dev/null +++ b/src/test/java/ai/pluggy/client/unit/BuilderNoArgsConstructorTest.java @@ -0,0 +1,51 @@ +package ai.pluggy.client.unit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ai.pluggy.client.response.Account; +import ai.pluggy.client.response.AccountBalance; +import ai.pluggy.client.response.AccountsResponse; +import ai.pluggy.client.response.Bill; +import ai.pluggy.client.response.Connector; +import ai.pluggy.client.response.CredentialSelectOption; +import ai.pluggy.client.response.IdentityResponse; +import ai.pluggy.client.response.Investment; +import ai.pluggy.client.response.ItemResponse; +import ai.pluggy.client.response.TransactionsCursorResponse; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * Response classes built with Lombok's @Builder must keep a public no-args constructor, so that + * JSON mappers that require one (e.g. Jackson) can deserialize them, not only Gson. + */ +public class BuilderNoArgsConstructorTest { + + private static final List> RESPONSE_CLASSES = Arrays.asList( + Account.class, AccountBalance.class, AccountsResponse.class, Bill.class, Connector.class, + CredentialSelectOption.class, IdentityResponse.class, Investment.class, ItemResponse.class, + TransactionsCursorResponse.class); + + @Test + void builderResponseClasses_havePublicNoArgsConstructor() throws Exception { + for (Class clazz : RESPONSE_CLASSES) { + Constructor constructor = clazz.getDeclaredConstructor(); + assertTrue(Modifier.isPublic(constructor.getModifiers()), clazz.getSimpleName()); + assertNotNull(constructor.newInstance(), clazz.getSimpleName()); + } + } + + @Test + void builderStillWorksAlongsideNoArgsConstructor() { + Account account = Account.builder().id("acc-1").balance(10.0).build(); + assertEquals("acc-1", account.getId()); + Account empty = new Account(); + empty.setId("acc-2"); + assertEquals("acc-2", empty.getId()); + } +} diff --git a/src/test/java/ai/pluggy/client/unit/ItemScrTest.java b/src/test/java/ai/pluggy/client/unit/ItemScrTest.java new file mode 100644 index 0000000..939d393 --- /dev/null +++ b/src/test/java/ai/pluggy/client/unit/ItemScrTest.java @@ -0,0 +1,161 @@ +package ai.pluggy.client.unit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import ai.pluggy.client.PluggyClient; +import ai.pluggy.client.response.ErrorResponse; +import ai.pluggy.client.response.ScrDatabase; +import ai.pluggy.client.response.ScrOperation; +import ai.pluggy.client.response.ScrResponse; +import java.io.IOException; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.ResponseBody; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import retrofit2.Response; + +public class ItemScrTest { + + private static final String SCR_JSON = "{\"dtbConsult\":\"202604 a 202607\"," + + "\"cdCli\":\"12345678901\",\"tpCli\":\"1\"," + + "\"lsDtb\":[{\"dtb\":202607,\"docProc\":\"99.5\",\"volProc\":\"98.1\",\"qtdIfs\":2," + + "\"qtdCongFinc\":1,\"dtbIniRel\":\"2010-03-01\",\"coobAss\":0,\"coobRec\":150.25," + + "\"lsOp\":[{\"mod\":\"0203\",\"oriRec\":\"0100\",\"indx\":\"11\",\"varCamb\":\"790\"," + + "\"subJDisc\":\"D\",\"resVenc\":{\"v110\":1000.5,\"v120\":250,\"v320\":10}," + + "\"lsGar\":[{\"tp\":\"0101\",\"qtd\":1}]," + + "\"lsInfAd\":[{\"tp\":\"01\",\"cd\":\"0001\",\"qtd\":3}]}]}," + + "{\"dtb\":202604,\"msg\":\"Base date not available for consultation\"}]," + + "\"listaDeMensagensDeValidacao\":[{\"codigo\":\"W01\",\"mensagem\":\"Partial data\"}]}"; + + private PluggyClient client; + private Request lastRequest; + private int responseCode; + private String responseJson; + + @BeforeEach + void setUp() { + responseCode = 200; + PluggyClient.PluggyClientBuilder builder = PluggyClient.builder() + .clientIdAndSecret("client-id", "client-secret") + .noAuthInterceptor(); + // answer every request with a canned body, so no request leaves the process + builder.okHttpClientBuilder().addInterceptor(chain -> { + lastRequest = chain.request(); + return new okhttp3.Response.Builder() + .request(chain.request()) + .protocol(Protocol.HTTP_1_1) + .code(responseCode) + .message(responseCode == 200 ? "OK" : "Error") + .body(ResponseBody.create(responseJson, MediaType.parse("application/json"))) + .build(); + }); + client = builder.build(); + } + + @Test + void getItemScr_withoutRange_sendsNoQueryAndParsesPayload() throws IOException { + responseJson = SCR_JSON; + + Response response = client.service().getItemScr("item-1").execute(); + + assertEquals("GET", lastRequest.method()); + assertEquals("/items/item-1/scr", lastRequest.url().encodedPath()); + assertEquals(0, lastRequest.url().querySize()); + + ScrResponse scr = response.body(); + assertNotNull(scr); + assertEquals("202604 a 202607", scr.getDtbConsult()); + assertEquals("12345678901", scr.getCdCli()); + assertEquals("1", scr.getTpCli()); + assertEquals(2, scr.getLsDtb().size()); + + ScrDatabase base = scr.getLsDtb().get(0); + assertEquals(202607, base.getDtb()); + assertEquals("99.5", base.getDocProc()); + assertEquals("98.1", base.getVolProc()); + assertEquals(2, base.getQtdIfs()); + assertEquals(1, base.getQtdCongFinc()); + assertEquals("2010-03-01", base.getDtbIniRel()); + assertEquals(0.0, base.getCoobAss()); + assertEquals(150.25, base.getCoobRec()); + + ScrOperation op = base.getLsOp().get(0); + assertEquals("0203", op.getMod()); + assertEquals("0100", op.getOriRec()); + assertEquals("11", op.getIndx()); + assertEquals("790", op.getVarCamb()); + assertEquals("D", op.getSubJDisc()); + assertEquals(1000.5, op.getResVenc().getV110()); + assertEquals(250.0, op.getResVenc().getV120()); + assertEquals(10.0, op.getResVenc().getV320()); + assertNull(op.getResVenc().getV20()); + assertEquals("0101", op.getLsGar().get(0).getTp()); + assertEquals(1, op.getLsGar().get(0).getQtd()); + assertEquals("01", op.getLsInfAd().get(0).getTp()); + assertEquals("0001", op.getLsInfAd().get(0).getCd()); + assertEquals(3, op.getLsInfAd().get(0).getQtd()); + + ScrDatabase empty = scr.getLsDtb().get(1); + assertEquals("Base date not available for consultation", empty.getMsg()); + assertNull(empty.getLsOp()); + + assertEquals("W01", scr.getListaDeMensagensDeValidacao().get(0).getCodigo()); + assertEquals("Partial data", scr.getListaDeMensagensDeValidacao().get(0).getMensagem()); + } + + @Test + void getItemScr_withRange_sendsFromAndTo() throws IOException { + responseJson = SCR_JSON; + + client.service().getItemScr("item-1", "202604", "202607").execute(); + + assertEquals("/items/item-1/scr", lastRequest.url().encodedPath()); + assertEquals(2, lastRequest.url().querySize()); + assertEquals("202604", lastRequest.url().queryParameter("from")); + assertEquals("202607", lastRequest.url().queryParameter("to")); + } + + @Test + void getItemScr_withNullBounds_omitsThem() throws IOException { + responseJson = SCR_JSON; + + client.service().getItemScr("item-1", null, "202607").execute(); + + assertEquals(1, lastRequest.url().querySize()); + assertNull(lastRequest.url().queryParameter("from")); + assertEquals("202607", lastRequest.url().queryParameter("to")); + } + + @Test + void getItemScr_featureNotEnabled_parsesErrorCode() throws IOException { + responseCode = 403; + responseJson = "{\"code\":403,\"codeDescription\":\"SCR_FEATURE_NOT_ENABLED\"," + + "\"message\":\"This client is not enabled to query SCR data.\"}"; + + Response response = client.service().getItemScr("item-1").execute(); + + assertFalse(response.isSuccessful()); + assertEquals(403, response.code()); + ErrorResponse error = client.parseError(response); + assertEquals(403, error.getCode()); + assertEquals("SCR_FEATURE_NOT_ENABLED", error.getCodeDescription()); + assertEquals("This client is not enabled to query SCR data.", error.getMessage()); + } + + @Test + void getItemScr_itemNotSupported_parsesErrorCode() throws IOException { + responseCode = 422; + responseJson = "{\"code\":422,\"codeDescription\":\"SCR_ITEM_NOT_SUPPORTED\"," + + "\"message\":\"SCR is only available for Open Finance items that have a known CPF or CNPJ.\"}"; + + Response response = client.service().getItemScr("item-1").execute(); + + assertEquals(422, response.code()); + assertEquals("SCR_ITEM_NOT_SUPPORTED", client.parseError(response).getCodeDescription()); + } +} diff --git a/src/test/java/ai/pluggy/client/unit/ItemsV2Test.java b/src/test/java/ai/pluggy/client/unit/ItemsV2Test.java new file mode 100644 index 0000000..8d9d224 --- /dev/null +++ b/src/test/java/ai/pluggy/client/unit/ItemsV2Test.java @@ -0,0 +1,139 @@ +package ai.pluggy.client.unit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ai.pluggy.client.PluggyClient; +import ai.pluggy.client.request.ItemsCursorSearchRequest; +import ai.pluggy.client.response.ErrorResponse; +import ai.pluggy.client.response.ItemResponse; +import ai.pluggy.client.response.ItemsCursorResponse; +import java.io.IOException; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.ResponseBody; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import retrofit2.Response; + +public class ItemsV2Test { + + private PluggyClient client; + private Request lastRequest; + private int responseCode; + private String responseJson; + + @BeforeEach + void setUp() { + responseCode = 200; + PluggyClient.PluggyClientBuilder builder = PluggyClient.builder() + .clientIdAndSecret("client-id", "client-secret") + .noAuthInterceptor(); + // answer every request with a canned body, so no request leaves the process + builder.okHttpClientBuilder().addInterceptor(chain -> { + lastRequest = chain.request(); + return new okhttp3.Response.Builder() + .request(chain.request()) + .protocol(Protocol.HTTP_1_1) + .code(responseCode) + .message(responseCode == 200 ? "OK" : "Error") + .body(ResponseBody.create(responseJson, MediaType.parse("application/json"))) + .build(); + }); + client = builder.build(); + } + + @Test + void getItems_parsesPageAndCursor() throws IOException { + responseJson = "{\"results\":[" + + "{\"id\":\"item-2\",\"clientUserId\":\"user-1\",\"status\":\"UPDATED\"," + + "\"connector\":{\"id\":201,\"name\":\"Bank\"}}," + + "{\"id\":\"item-1\",\"clientUserId\":\"user-1\",\"status\":\"LOGIN_ERROR\"}]," + + "\"next\":\"?clientUserId=user-1&after=abc%2B123%3D%3D\"}"; + + Response response = client.service().getItems().execute(); + + assertEquals("GET", lastRequest.method()); + assertEquals("/v2/items", lastRequest.url().encodedPath()); + assertEquals(0, lastRequest.url().querySize()); + + ItemsCursorResponse page = response.body(); + assertNotNull(page); + assertEquals(2, page.getResults().size()); + ItemResponse newest = page.getResults().get(0); + assertEquals("item-2", newest.getId()); + assertEquals("user-1", newest.getClientUserId()); + assertEquals(201, newest.getConnector().getId()); + assertEquals("abc+123==", page.getNextCursor()); + assertTrue(page.hasNext()); + } + + @Test + void getItems_withFilters_sendsOnlyWhitelistedParams() throws IOException { + responseJson = "{\"results\":[],\"next\":null}"; + + ItemsCursorSearchRequest request = new ItemsCursorSearchRequest() + .clientUserId("user 1") + .connectorId(201) + .after("abc+123=="); + client.service().getItems(request).execute(); + + assertEquals("/v2/items", lastRequest.url().encodedPath()); + assertEquals("user 1", lastRequest.url().queryParameter("clientUserId")); + assertEquals("201", lastRequest.url().queryParameter("connectorId")); + // the decoded cursor is re-encoded on the wire and round-trips intact + assertEquals("abc+123==", lastRequest.url().queryParameter("after")); + assertEquals(3, lastRequest.url().querySize()); + assertNull(lastRequest.url().queryParameter("page")); + assertNull(lastRequest.url().queryParameter("pageSize")); + } + + @Test + void getItems_lastPage_hasNoCursor() throws IOException { + responseJson = "{\"results\":[{\"id\":\"item-1\"}],\"next\":null}"; + + ItemsCursorResponse page = client.service().getItems().execute().body(); + + assertNotNull(page); + assertEquals(1, page.getResults().size()); + assertNull(page.getNext()); + assertNull(page.getNextCursor()); + assertFalse(page.hasNext()); + } + + @Test + void getItems_featureNotEnabled_parsesErrorCode() throws IOException { + responseCode = 403; + responseJson = "{\"message\":\"This client is not enabled to list its items.\"," + + "\"code\":403,\"codeDescription\":\"LIST_ITEMS_FEATURE_NOT_ENABLED\"}"; + + Response response = client.service().getItems().execute(); + + assertFalse(response.isSuccessful()); + assertEquals(403, response.code()); + ErrorResponse error = client.parseError(response); + assertEquals(403, error.getCode()); + assertEquals("LIST_ITEMS_FEATURE_NOT_ENABLED", error.getCodeDescription()); + assertEquals("This client is not enabled to list its items.", error.getMessage()); + } + + @Test + void itemsCursorSearchRequest_validatesInput() { + StringBuilder tooLong = new StringBuilder(); + for (int i = 0; i < 256; i++) { + tooLong.append('x'); + } + assertThrows(IllegalArgumentException.class, + () -> new ItemsCursorSearchRequest().clientUserId(tooLong.toString())); + assertThrows(IllegalArgumentException.class, + () -> new ItemsCursorSearchRequest().connectorId(-1)); + assertThrows(IllegalArgumentException.class, + () -> new ItemsCursorSearchRequest().after(null)); + assertEquals(0, new ItemsCursorSearchRequest().connectorId(0).getConnectorId()); + } +} diff --git a/src/test/java/ai/pluggy/client/unit/ResponseFieldsTest.java b/src/test/java/ai/pluggy/client/unit/ResponseFieldsTest.java new file mode 100644 index 0000000..0ee94fd --- /dev/null +++ b/src/test/java/ai/pluggy/client/unit/ResponseFieldsTest.java @@ -0,0 +1,251 @@ +package ai.pluggy.client.unit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ai.pluggy.client.PluggyClient; +import ai.pluggy.client.response.Account; +import ai.pluggy.client.response.Bill; +import ai.pluggy.client.response.BillPayment; +import ai.pluggy.client.response.Connector; +import ai.pluggy.client.response.CreditData; +import ai.pluggy.client.response.IdentityFinancialRelationships; +import ai.pluggy.client.response.IdentityProcurator; +import ai.pluggy.client.response.IdentityQualifications; +import ai.pluggy.client.response.IdentityResponse; +import ai.pluggy.client.response.Investment; +import ai.pluggy.client.response.ItemResponse; +import ai.pluggy.client.response.Transaction; +import java.io.IOException; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.ResponseBody; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Deserialization of response fields that map 1:1 to the API's JSON keys (Gson IDENTITY naming), so + * a casing mismatch would surface here as a null. + */ +public class ResponseFieldsTest { + + private PluggyClient client; + private String responseJson; + + @BeforeEach + void setUp() { + PluggyClient.PluggyClientBuilder builder = PluggyClient.builder() + .clientIdAndSecret("client-id", "client-secret") + .noAuthInterceptor(); + // answer every request with a canned body, so no request leaves the process + builder.okHttpClientBuilder().addInterceptor(chain -> new okhttp3.Response.Builder() + .request(chain.request()) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(ResponseBody.create(responseJson, MediaType.parse("application/json"))) + .build()); + client = builder.build(); + } + + @Test + void item_parsesConsentAutoSyncAndUserAction() throws IOException { + responseJson = "{\"id\":\"item-1\",\"consentExpiresAt\":\"2027-01-01T00:00:00.000Z\"," + + "\"nextAutoSyncAt\":\"2026-10-06T08:00:00.000Z\"," + + "\"userAction\":{\"instructions\":\"Scan the QR code\",\"type\":\"qr\"," + + "\"expiresAt\":\"2026-10-05T12:00:00.000Z\",\"attributes\":{\"qr\":\"data:image/png\"}}}"; + + ItemResponse item = client.service().getItem("item-1").execute().body(); + + assertNotNull(item); + assertNotNull(item.getConsentExpiresAt()); + assertNotNull(item.getNextAutoSyncAt()); + assertEquals("Scan the QR code", item.getUserAction().getInstructions()); + assertEquals("qr", item.getUserAction().getType()); + assertNotNull(item.getUserAction().getExpiresAt()); + assertEquals("data:image/png", item.getUserAction().getAttributes().get("qr")); + } + + @Test + void item_withoutAutoSync_hasNullNextAutoSyncAt() throws IOException { + responseJson = "{\"id\":\"item-1\",\"nextAutoSyncAt\":null}"; + + ItemResponse item = client.service().getItem("item-1").execute().body(); + + assertNotNull(item); + assertNull(item.getNextAutoSyncAt()); + assertNull(item.getUserAction()); + } + + @Test + void creditData_parsesAdditionalCardsAndLimitFlexibility() throws IOException { + responseJson = "{\"id\":\"acc-1\",\"creditData\":{\"level\":\"BLACK\"," + + "\"additionalCards\":[{\"number\":\"1234\"},{\"number\":\"5678\"}]," + + "\"isLimitFlexible\":true}}"; + + Account account = client.service().getAccount("acc-1").execute().body(); + + assertNotNull(account); + CreditData creditData = account.getCreditData(); + assertEquals(2, creditData.getAdditionalCards().size()); + assertEquals("5678", creditData.getAdditionalCards().get(1).getNumber()); + assertTrue(creditData.getIsLimitFlexible()); + } + + @Test + void transaction_parsesOrderAndPaymentDataCodes() throws IOException { + responseJson = "{\"id\":\"tx-1\",\"order\":3,\"paymentData\":{\"paymentMethod\":\"PIX\"," + + "\"authenticationCode\":\"E1234567820261005\",\"receiverReferenceId\":\"ref-42\"}}"; + + Transaction transaction = client.service().getTransaction("tx-1").execute().body(); + + assertNotNull(transaction); + assertEquals(3, transaction.getOrder()); + assertEquals("E1234567820261005", transaction.getPaymentData().getAuthenticationCode()); + assertEquals("ref-42", transaction.getPaymentData().getReceiverReferenceId()); + } + + @Test + void connector_parsesProductCoverageAndAutomaticPix() throws IOException { + responseJson = "{\"id\":601,\"name\":\"Bank\",\"supportsAutomaticPix\":true," + + "\"productCoverage\":[\"INVESTMENTS:TREASURE_TITLES\",\"CREDIT_OPERATIONS:INVOICE_FINANCINGS\"]}"; + + Connector connector = client.service().getConnector(601).execute().body(); + + assertNotNull(connector); + assertTrue(connector.getSupportsAutomaticPix()); + assertEquals(2, connector.getProductCoverage().size()); + assertEquals("INVESTMENTS:TREASURE_TITLES", connector.getProductCoverage().get(0)); + } + + @Test + void investment_parsesNewFieldsAndNestedObjects() throws IOException { + responseJson = "{\"id\":\"inv-1\",\"amountProfit\":123.45,\"priceFactor\":1000," + + "\"taxExempt\":true,\"gracePeriodDate\":\"2027-03-01T00:00:00.000Z\"," + + "\"couponPayment\":{\"hasCoupon\":true,\"periodicity\":\"IRREGULAR\"," + + "\"additionalInfo\":\"See prospectus\"}," + + "\"debtor\":{\"name\":\"Agro Company SA\"}}"; + + Investment investment = client.service().getInvestment("inv-1").execute().body(); + + assertNotNull(investment); + assertEquals(123.45, Double.valueOf(investment.getAmountProfit())); + assertEquals(1000.0, investment.getPriceFactor()); + assertTrue(investment.getTaxExempt()); + assertNotNull(investment.getGracePeriodDate()); + assertTrue(investment.getCouponPayment().getHasCoupon()); + assertEquals("IRREGULAR", investment.getCouponPayment().getPeriodicity()); + assertEquals("See prospectus", investment.getCouponPayment().getAdditionalInfo()); + assertEquals("Agro Company SA", investment.getDebtor().getName()); + } + + @Test + void investment_withNullNestedObjects() throws IOException { + responseJson = "{\"id\":\"inv-1\",\"amountProfit\":null,\"couponPayment\":null,\"debtor\":null}"; + + Investment investment = client.service().getInvestment("inv-1").execute().body(); + + assertNotNull(investment); + assertNull(investment.getAmountProfit()); + assertNull(investment.getCouponPayment()); + assertNull(investment.getDebtor()); + } + + @Test + void identity_parsesEstablishmentRelationsAndNestedObjects() throws IOException { + responseJson = "{\"id\":\"id-1\",\"establishmentCode\":\"EST-1\",\"establishmentName\":\"Store\"," + + "\"relations\":[{\"type\":\"Mother\",\"name\":\"Maria Silva\",\"document\":\"00000000000\"}]," + + "\"financialRelationships\":{\"startDate\":\"2015-01-01T00:00:00.000Z\"," + + "\"productsServicesType\":[\"CONTA_DEPOSITO_A_VISTA\",\"OUTROS\"]," + + "\"productsServicesTypeAdditionalInfo\":\"Other product\"," + + "\"procurators\":[{\"type\":\"PROCURADOR\",\"cpfNumber\":\"11111111111\"," + + "\"documentNumber\":\"11111111111\",\"documentType\":\"CPF\",\"civilName\":\"Joao\"," + + "\"socialName\":\"Jo\"}]," + + "\"accounts\":[{\"compeCode\":\"001\",\"branchCode\":\"1234\",\"number\":\"56789\"," + + "\"checkDigit\":\"0\",\"type\":\"CONTA_DEPOSITO_A_VISTA\",\"subtype\":\"INDIVIDUAL\"}]," + + "\"portabilitiesReceived\":[{\"employerName\":\"Acme\",\"employerDocument\":\"22222222000100\"," + + "\"paycheckBankDetainerCnpj\":\"33333333000100\",\"paycheckBankDetainerIspb\":\"12345678\"," + + "\"portabilityApprovalDate\":\"2024-05-01T00:00:00.000Z\"}]," + + "\"paychecksBankLink\":[{\"employerName\":\"Acme\",\"employerDocument\":\"22222222000100\"," + + "\"paycheckBankCnpj\":\"44444444000100\",\"paycheckBankIspb\":\"87654321\"," + + "\"accountOpeningDate\":\"2020-02-01T00:00:00.000Z\"}]}," + + "\"qualifications\":{\"companyCnpj\":\"55555555000100\",\"occupationCode\":\"OUTRO\"," + + "\"occupationDescription\":\"Engineer\"," + + "\"informedIncome\":{\"frequency\":\"MENSAL\",\"amount\":10000.5," + + "\"date\":\"2026-01-01T00:00:00.000Z\"}," + + "\"informedPatrimony\":{\"amount\":500000,\"year\":2025," + + "\"date\":\"2025-12-31T00:00:00.000Z\"}," + + "\"economicActivities\":[{\"code\":\"0111301\",\"isMain\":true}," + + "{\"code\":\"4711302\",\"isMain\":false}]," + + "\"informedRevenue\":{\"amount\":1200000,\"frequency\":\"OTHER\"," + + "\"frequencyAdditionalInfo\":\"Seasonal\",\"year\":2025}}}"; + + IdentityResponse identity = client.service().getIdentityById("id-1").execute().body(); + + assertNotNull(identity); + assertEquals("EST-1", identity.getEstablishmentCode()); + assertEquals("Store", identity.getEstablishmentName()); + assertEquals("Mother", identity.getRelations().get(0).getType()); + assertEquals("Maria Silva", identity.getRelations().get(0).getName()); + + IdentityFinancialRelationships fr = identity.getFinancialRelationships(); + assertNotNull(fr.getStartDate()); + assertEquals(2, fr.getProductsServicesType().size()); + assertEquals("Other product", fr.getProductsServicesTypeAdditionalInfo()); + IdentityProcurator procurator = fr.getProcurators().get(0); + assertEquals("PROCURADOR", procurator.getType()); + assertEquals("11111111111", procurator.getDocumentNumber()); + assertEquals("CPF", procurator.getDocumentType()); + assertEquals("Joao", procurator.getCivilName()); + assertEquals("Jo", procurator.getSocialName()); + assertEquals("001", fr.getAccounts().get(0).getCompeCode()); + assertEquals("0", fr.getAccounts().get(0).getCheckDigit()); + assertEquals("INDIVIDUAL", fr.getAccounts().get(0).getSubtype()); + assertEquals("12345678", fr.getPortabilitiesReceived().get(0).getPaycheckBankDetainerIspb()); + assertNotNull(fr.getPortabilitiesReceived().get(0).getPortabilityApprovalDate()); + assertEquals("87654321", fr.getPaychecksBankLink().get(0).getPaycheckBankIspb()); + assertNotNull(fr.getPaychecksBankLink().get(0).getAccountOpeningDate()); + + IdentityQualifications q = identity.getQualifications(); + assertEquals("55555555000100", q.getCompanyCnpj()); + assertEquals("OUTRO", q.getOccupationCode()); + assertEquals("Engineer", q.getOccupationDescription()); + assertEquals("MENSAL", q.getInformedIncome().getFrequency()); + assertEquals(10000.5, q.getInformedIncome().getAmount()); + assertNotNull(q.getInformedIncome().getDate()); + assertEquals(2025, q.getInformedPatrimony().getYear()); + assertEquals(500000.0, q.getInformedPatrimony().getAmount()); + assertEquals(2, q.getEconomicActivities().size()); + assertTrue(q.getEconomicActivities().get(0).getIsMain()); + assertFalse(q.getEconomicActivities().get(1).getIsMain()); + assertEquals("0111301", q.getEconomicActivities().get(0).getCode()); + assertEquals("OTHER", q.getInformedRevenue().getFrequency()); + assertEquals("Seasonal", q.getInformedRevenue().getFrequencyAdditionalInfo()); + assertEquals(2025, q.getInformedRevenue().getYear()); + } + + @Test + void bill_parsesPayments() throws IOException { + responseJson = "{\"id\":\"bill-1\",\"payments\":[{\"id\":\"pay-1\",\"valueType\":\"FULL_PAYMENT\"," + + "\"paymentDate\":\"2026-09-10T00:00:00.000Z\",\"paymentMode\":\"PIX\",\"amount\":1500.75," + + "\"currencyCode\":\"BRL\"},{\"id\":\"pay-2\",\"valueType\":\"OTHER_PAYMENT\"," + + "\"paymentDate\":\"2026-09-11T00:00:00.000Z\",\"paymentMode\":null,\"amount\":10," + + "\"currencyCode\":\"BRL\"}]}"; + + Bill bill = client.service().getBill("bill-1").execute().body(); + + assertNotNull(bill); + assertEquals(2, bill.getPayments().size()); + BillPayment payment = bill.getPayments().get(0); + assertEquals("pay-1", payment.getId()); + assertEquals("FULL_PAYMENT", payment.getValueType()); + assertNotNull(payment.getPaymentDate()); + assertEquals("PIX", payment.getPaymentMode()); + assertEquals(1500.75, payment.getAmount()); + assertEquals("BRL", payment.getCurrencyCode()); + assertNull(bill.getPayments().get(1).getPaymentMode()); + } +} diff --git a/src/test/java/ai/pluggy/client/unit/TransactionsV2Test.java b/src/test/java/ai/pluggy/client/unit/TransactionsV2Test.java new file mode 100644 index 0000000..6ed9da5 --- /dev/null +++ b/src/test/java/ai/pluggy/client/unit/TransactionsV2Test.java @@ -0,0 +1,170 @@ +package ai.pluggy.client.unit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ai.pluggy.client.PluggyClient; +import ai.pluggy.client.request.TransactionsCursorSearchRequest; +import ai.pluggy.client.response.AccountBalance; +import ai.pluggy.client.response.CreditCardAccountPaymentType; +import ai.pluggy.client.response.Transaction; +import ai.pluggy.client.response.TransactionCreditCardMetadata; +import ai.pluggy.client.response.TransactionsCursorResponse; +import java.io.IOException; +import java.util.Arrays; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.ResponseBody; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import retrofit2.Response; + +public class TransactionsV2Test { + + private static final String ACCOUNT_ID = "03fc3a6f-2b5e-4e8e-9f4c-1a2b3c4d5e6f"; + + private PluggyClient client; + private Request lastRequest; + private String responseJson; + + @BeforeEach + void setUp() { + PluggyClient.PluggyClientBuilder builder = PluggyClient.builder() + .clientIdAndSecret("client-id", "client-secret") + .noAuthInterceptor(); + // answer every request with a canned body, so no request leaves the process + builder.okHttpClientBuilder().addInterceptor(chain -> { + lastRequest = chain.request(); + return new okhttp3.Response.Builder() + .request(chain.request()) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body(ResponseBody.create(responseJson, MediaType.parse("application/json"))) + .build(); + }); + client = builder.build(); + } + + @Test + void getTransactionsV2_parsesPageAndCreditCardMetadata() throws IOException { + responseJson = "{\"results\":[{\"id\":\"t-1\",\"accountId\":\"" + ACCOUNT_ID + "\"," + + "\"amount\":-120.5,\"date\":\"2026-09-10T00:00:00.000Z\"," + + "\"creditCardMetadata\":{\"installmentNumber\":1,\"totalInstallments\":3," + + "\"paymentType\":\"INSTALLMENT\",\"billPostDate\":\"2026-09-15\"," + + "\"transactionDateTime\":\"2026-09-10T14:32:11.000Z\"," + + "\"billForecastDate\":\"2026-10-05\"}}]," + + "\"next\":\"?accountId=" + ACCOUNT_ID + "&dateFrom=2026-09-01&after=abc%2B123%3D%3D\"}"; + + Response response = client.service() + .getTransactionsV2(ACCOUNT_ID) + .execute(); + + assertEquals("GET", lastRequest.method()); + assertEquals("/v2/transactions", lastRequest.url().encodedPath()); + assertEquals(ACCOUNT_ID, lastRequest.url().queryParameter("accountId")); + assertEquals(1, lastRequest.url().querySize()); + + TransactionsCursorResponse page = response.body(); + assertNotNull(page); + assertEquals(1, page.getResults().size()); + assertEquals("abc+123==", page.getNextCursor()); + assertTrue(page.hasNext()); + + Transaction transaction = page.getResults().get(0); + TransactionCreditCardMetadata metadata = transaction.getCreditCardMetadata(); + assertEquals(CreditCardAccountPaymentType.INSTALLMENT, metadata.getPaymentType()); + assertEquals("2026-09-15", metadata.getBillPostDate()); + assertEquals("2026-09-10T14:32:11.000Z", metadata.getTransactionDateTime()); + assertEquals("2026-10-05", metadata.getBillForecastDate()); + assertEquals(3, metadata.getTotalInstallments()); + } + + @Test + void getTransactionsV2_lastPage_hasNoCursor_andMissingFieldsAreNull() throws IOException { + responseJson = "{\"results\":[{\"id\":\"t-2\",\"creditCardMetadata\":" + + "{\"paymentType\":\"SOME_FUTURE_TYPE\",\"billPostDate\":null}}],\"next\":null}"; + + TransactionsCursorResponse page = client.service() + .getTransactionsV2(ACCOUNT_ID) + .execute() + .body(); + + assertNotNull(page); + assertNull(page.getNext()); + assertNull(page.getNextCursor()); + assertFalse(page.hasNext()); + TransactionCreditCardMetadata metadata = page.getResults().get(0).getCreditCardMetadata(); + // a payment type added after this SDK release deserializes as null, like every SDK enum + assertNull(metadata.getPaymentType()); + assertNull(metadata.getBillPostDate()); + assertNull(metadata.getTransactionDateTime()); + } + + @Test + void getTransactionsV2_withFilters_sendsOnlyWhitelistedParams() throws IOException { + responseJson = "{\"results\":[],\"next\":null}"; + + TransactionsCursorSearchRequest request = new TransactionsCursorSearchRequest() + .ids(Arrays.asList("a", "b")) + .dateFrom("2026-09-01") + .dateTo("2026-09-30") + .after("abc+123=="); + client.service().getTransactionsV2(ACCOUNT_ID, request).execute(); + + assertEquals("/v2/transactions", lastRequest.url().encodedPath()); + assertEquals(ACCOUNT_ID, lastRequest.url().queryParameter("accountId")); + assertEquals("a,b", lastRequest.url().queryParameter("ids")); + assertEquals("2026-09-01", lastRequest.url().queryParameter("dateFrom")); + assertEquals("2026-09-30", lastRequest.url().queryParameter("dateTo")); + // the decoded cursor is re-encoded on the wire and round-trips intact + assertEquals("abc+123==", lastRequest.url().queryParameter("after")); + assertEquals(5, lastRequest.url().querySize()); + assertNull(lastRequest.url().queryParameter("page")); + assertNull(lastRequest.url().queryParameter("pageSize")); + } + + @Test + void cursorSearchRequest_dateFromAndCreatedAtFrom_areMutuallyExclusive() { + assertThrows(IllegalArgumentException.class, () -> new TransactionsCursorSearchRequest() + .dateFrom("2026-09-01") + .createdAtFrom("2026-09-01T00:00:00.000Z")); + assertThrows(IllegalArgumentException.class, () -> new TransactionsCursorSearchRequest() + .createdAtFrom("2026-09-01T00:00:00.000Z") + .dateFrom("2026-09-01")); + } + + @Test + void parseAfterCursor_handlesEdgeCases() { + assertNull(TransactionsCursorResponse.parseAfterCursor(null)); + assertNull(TransactionsCursorResponse.parseAfterCursor("")); + assertNull(TransactionsCursorResponse.parseAfterCursor("?accountId=x")); + assertNull(TransactionsCursorResponse.parseAfterCursor("?accountId=x&after=")); + assertEquals("c1", TransactionsCursorResponse.parseAfterCursor("?after=c1")); + assertEquals("c1", TransactionsCursorResponse.parseAfterCursor("after=c1&accountId=x")); + assertEquals("c1", + TransactionsCursorResponse.parseAfterCursor("https://api.pluggy.ai/v2/transactions?a=1&after=c1")); + // a param that merely ends in "after" is not the cursor + assertNull(TransactionsCursorResponse.parseAfterCursor("?notafter=c1")); + } + + @Test + void getAccountBalance_parsesBalance() throws IOException { + responseJson = "{\"balance\":1523.47,\"currencyCode\":\"BRL\"," + + "\"updateDateTime\":\"2026-10-01T10:15:00.000Z\"}"; + + AccountBalance balance = client.service().getAccountBalance(ACCOUNT_ID).execute().body(); + + assertEquals("GET", lastRequest.method()); + assertEquals("/accounts/" + ACCOUNT_ID + "/balance", lastRequest.url().encodedPath()); + assertNotNull(balance); + assertEquals(1523.47, balance.getBalance()); + assertEquals("BRL", balance.getCurrencyCode()); + assertEquals("2026-10-01T10:15:00.000Z", balance.getUpdateDateTime()); + } +}