From c1d95a7f4a8bf8cf36ee250ec8490308dc5cc05d Mon Sep 17 00:00:00 2001 From: Gabriel Pan Gantes Date: Sun, 4 Oct 2026 23:03:02 +0200 Subject: [PATCH 1/2] feat(transaction): add paymentType, billPostDate and transactionDateTime to creditCardMetadata The API returns three more credit card metadata fields for Open Finance connectors. transactionDateTime is passed through as the institution sends it, so it is typed Date | string: the JSON reviver only turns the exact YYYY-MM-DDTHH:mm:ss.sssZ shape into a Date. --- src/types/transaction.ts | 17 ++++++++++++++ tests/transactions.test.ts | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/types/transaction.ts b/src/types/transaction.ts index 1884f7c..05f192d 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -113,6 +113,13 @@ export const CREDIT_CARD_ACCOUNT_OTHER_CREDIT_TYPES = [ */ export type CreditCardAccountOtherCreditType = typeof CREDIT_CARD_ACCOUNT_OTHER_CREDIT_TYPES[number] +export const CREDIT_CARD_ACCOUNT_PAYMENT_TYPES = ['SINGLE', 'INSTALLMENT'] as const +/** + * @typedef CreditCardAccountPaymentType + * Whether a credit card purchase is charged in full on a single bill or split into installments. + */ +export type CreditCardAccountPaymentType = typeof CREDIT_CARD_ACCOUNT_PAYMENT_TYPES[number] + export type CreditCardMetadata = { /** The number of the installment */ installmentNumber?: number @@ -138,6 +145,16 @@ export type CreditCardMetadata = { otherCreditsAdditionalInfo?: string /** Forecasted bill period (formatted as YYYY-MM) in which this transaction is expected to be charged. Unlike billId, it is provided for pending and future transactions too. Only returned for Open Finance connectors */ billForecastDate?: string + /** Whether the purchase is charged in full on a single bill (SINGLE) or split into installments (INSTALLMENT). Only returned for Open Finance connectors */ + paymentType?: CreditCardAccountPaymentType + /** Date (YYYY-MM-DD) the institution posted the transaction to a bill, as reported by the institution and never adjusted by Pluggy. Null when the transaction is not posted to a bill yet or the institution does not report it. Only returned for Open Finance connectors */ + billPostDate?: string | null + /** + * Date and time of the transaction as reported by the institution, normalized to ISO-8601. Only returned for Open Finance connectors. + * The value is passed through as the institution sends it, so it is parsed into a Date only when it has the exact + * `YYYY-MM-DDTHH:mm:ss.sssZ` shape; any other valid ISO-8601 form (no milliseconds, an offset instead of `Z`) stays a string. + */ + transactionDateTime?: Date | string } export type TransactionFilters = PageFilters & { diff --git a/tests/transactions.test.ts b/tests/transactions.test.ts index 505c80f..98f3025 100644 --- a/tests/transactions.test.ts +++ b/tests/transactions.test.ts @@ -233,3 +233,49 @@ describe('fetchAllTransactions (cursor-based)', () => { expect(mock.isDone()).toBeTruthy() }) }) + +describe('creditCardMetadata deserialization', () => { + beforeEach(() => { + nock.cleanAll() + setupAuth() + }) + + it('exposes paymentType, billPostDate and transactionDateTime as the institution reports them', async () => { + const withMetadata = ( + id: string, + transactionDateTime: string, + billPostDate: string | null + ) => ({ + ...mockTransaction(id), + creditCardMetadata: { + installmentNumber: 2, + totalInstallments: 10, + paymentType: 'INSTALLMENT', + billPostDate, + transactionDateTime, + }, + }) + nock(API_URL) + .get('/v2/transactions') + .query({ accountId: ACCOUNT_ID }) + .reply(200, { + results: [ + withMetadata('tx-1', '2026-04-09T16:43:35.203Z', '2026-10-12'), + withMetadata('tx-2', '2026-04-09T16:43:35-03:00', null), + ], + next: null, + }) + + const client = new PluggyClient({ clientId: '123', clientSecret: '456' }) + const { results } = await client.fetchTransactionsCursor(ACCOUNT_ID) + + const [first, second] = results.map(tx => tx.creditCardMetadata!) + expect(first.paymentType).toBe('INSTALLMENT') + // a date-only value is never turned into a Date, so it cannot shift by timezone + expect(first.billPostDate).toBe('2026-10-12') + // the exact YYYY-MM-DDTHH:mm:ss.sssZ shape is revived into a Date; any other ISO form stays a string + expect(first.transactionDateTime).toEqual(new Date('2026-04-09T16:43:35.203Z')) + expect(second.transactionDateTime).toBe('2026-04-09T16:43:35-03:00') + expect(second.billPostDate).toBeNull() + }) +}) From e9bde9f38fb85ee0989063514fc49b7c5c2e8f36 Mon Sep 17 00:00:00 2001 From: Gabriel Pan Gantes Date: Mon, 5 Oct 2026 08:07:59 +0200 Subject: [PATCH 2/2] feat: sync with the API spec (SCR, merchants, category rules, pix-qr, boletos, model fields) Adds the fields the API returns that the types were missing, plus fetchItemScr (opt-in, SCR feature required), fetchMerchants, category rules (list/create/delete), createPixQrPaymentRequest, fetchSmartTransferPreauthorizationPayments and the beta boleto endpoints. Mutation requests now accept any 2xx: DELETE /categories/rules/{id} answers 204, which was treated as a failure. --- CLAUDE.md | 12 +- src/baseApi.ts | 2 +- src/client.ts | 119 +++++++++++++++ src/paymentsClient.ts | 26 ++++ src/types/account.ts | 7 + src/types/boleto.ts | 142 ++++++++++++++++++ src/types/category.ts | 28 ++++ src/types/connector.ts | 6 + src/types/index.ts | 3 + src/types/investment.ts | 17 +++ src/types/merchant.ts | 10 ++ src/types/payments/paymentFilters.ts | 7 + src/types/payments/paymentRequest.ts | 11 ++ src/types/scr.ts | 100 ++++++++++++ src/types/transaction.ts | 7 + tests/boletos.test.ts | 130 ++++++++++++++++ tests/categoryRules.test.ts | 70 +++++++++ tests/merchants.test.ts | 40 +++++ tests/payments/pixQrPaymentRequest.test.ts | 28 ++++ ...rtTransferPreauthorizationPayments.test.ts | 44 ++++++ tests/scr.test.ts | 71 +++++++++ 21 files changed, 875 insertions(+), 5 deletions(-) create mode 100644 src/types/boleto.ts create mode 100644 src/types/merchant.ts create mode 100644 src/types/scr.ts create mode 100644 tests/boletos.test.ts create mode 100644 tests/categoryRules.test.ts create mode 100644 tests/merchants.test.ts create mode 100644 tests/payments/pixQrPaymentRequest.test.ts create mode 100644 tests/payments/smartTransferPreauthorizationPayments.test.ts create mode 100644 tests/scr.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index f491853..e084380 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,6 +68,7 @@ Current SDK methods in `client.ts` (PluggyClient): | Items | `createItem()`, `fetchItem()`, `updateItem()`, `deleteItem()` | /items | | Item MFA | `updateItemMFA()` | POST /items/{id}/mfa | | Item Resources | `fetchItemResources()` | GET /items/{id}/resources | +| SCR (opt-in) | `fetchItemScr()` | GET /items/{id}/scr | | Accounts | `fetchAccounts()`, `fetchAccount()` | /accounts | | Account Statements | `fetchAccountStatements()` | GET /accounts/{id}/statements | | Transactions | `fetchTransactionsCursor()` (single page) + `fetchAllTransactions()` (full sweep) for the v2 cursor endpoint; `fetchTransactions()` page-based is `@deprecated`; `fetchTransaction()` by id | GET /v2/transactions, GET /transactions/{id} | @@ -79,6 +80,9 @@ Current SDK methods in `client.ts` (PluggyClient): | Identity | `fetchIdentity()`, `fetchIdentityByItemId()` | /identity | | Credit Card Bills | `fetchCreditCardBills()`, `fetchCreditCardBill()` | /bills | | Categories | `fetchCategories()`, `fetchCategory()` | /categories | +| Category Rules | `fetchCategoryRules()`, `createCategoryRule()`, `deleteCategoryRule()` | /categories/rules | +| Merchants | `fetchMerchants()` | GET /merchants | +| Boleto Management (beta) | `createBoletoConnection()`, `createBoletoConnectionFromItem()`, `createBoleto()`, `fetchBoleto()`, `cancelBoleto()` | /boleto-connections, /boletos | | Webhooks | Full CRUD | /webhooks | | Connect Token | `createConnectToken()` | POST /connect_token | @@ -87,7 +91,7 @@ Current SDK methods in `paymentsClient.ts` (PluggyPaymentsClient): | Resource | SDK Methods | |----------|-------------| | Payment Recipients | `createPaymentRecipient()`, `fetchPaymentRecipient()`, `fetchPaymentRecipients()`, `updatePaymentRecipient()`, `deletePaymentRecipient()` | -| Payment Requests | `createPaymentRequest()`, `fetchPaymentRequest()`, `fetchPaymentRequests()`, `deletePaymentRequest()` | +| Payment Requests | `createPaymentRequest()`, `createPixQrPaymentRequest()`, `fetchPaymentRequest()`, `fetchPaymentRequests()`, `deletePaymentRequest()` | | Payment Intents | `createPaymentIntent()`, `fetchPaymentIntent()`, `fetchPaymentIntents()` | | Payment Customers | Full CRUD | | Payment Institutions | `fetchPaymentInstitution()`, `fetchPaymentInstitutions()` | @@ -95,7 +99,7 @@ Current SDK methods in `paymentsClient.ts` (PluggyPaymentsClient): | Smart Accounts | `createSmartAccount()`, `fetchSmartAccount()`, `fetchSmartAccountBalance()`, `fetchSmartAccounts()` | | Automatic PIX | Multiple methods for PIX automático | | Scheduled Payments | Multiple methods | -| Smart Transfers | Multiple methods | +| Smart Transfers | Multiple methods, incl. `fetchSmartTransferPreauthorizationPayments()` | ### 3. Type Definition Patterns @@ -226,5 +230,5 @@ Type-only gap fixes from comparing `src/types/` against the OpenAPI spec (`oas3. - **CreditCardBills**: `payments` (new `CreditCardBillPayment` type + `valueType`/`paymentMode` enums) - Re-exported `creditCardBills` types from `src/types/index.ts` (were public via `fetchCreditCardBill()` return type but not importable) -### Not Implemented (Intentional) -- Boleto Management: Beta feature, not added to SDK (the `Connector.supportsBoletoManagement` flag is exposed as read-only metadata, but no boleto endpoints/types are implemented) +### Boleto Management (beta) +Boleto endpoints (`/boleto-connections`, `/boletos`) are implemented on `PluggyClient` and marked BETA in their JSDoc, since the API still flags them as beta. diff --git a/src/baseApi.ts b/src/baseApi.ts index 364d4bc..a2e36d2 100644 --- a/src/baseApi.ts +++ b/src/baseApi.ts @@ -172,7 +172,7 @@ export class BaseApi { json: body, }) - if (statusCode !== 200) { + if (statusCode < 200 || statusCode >= 300) { return Promise.reject(body) } diff --git a/src/client.ts b/src/client.ts index 203ed2d..932168c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -30,6 +30,16 @@ import { PageFilters, InvestmentsFilters, AccountStatement, + ScrFilters, + ScrResponse, + MerchantsResponse, + ClientCategoryRule, + CreateClientCategoryRule, + BoletoConnection, + CreateBoletoConnection, + CreateBoletoConnectionFromItem, + CreateBoleto, + IssuedBoleto, } from './types' import { CreditCardBills } from './types/creditCardBills' import { ValidationResult } from './types/validation' @@ -155,6 +165,24 @@ export class PluggyClient extends BaseApi { return await this.createGetRequest(`items/${itemId}/resources`, options) } + /** + * Fetch the SCR (Bacen's Sistema de Informações de Crédito) for the document behind an Item. + * + * Opt-in: requires the SCR feature to be enabled for your team (ask Pluggy support); otherwise + * 403 SCR_FEATURE_NOT_ENABLED. Only available for Open Finance items with a known CPF/CNPJ + * (otherwise 422 SCR_ITEM_NOT_SUPPORTED). + * + * The response is Bacen's own payload, forwarded unchanged. Base dates are months (YYYYMM) and + * Bacen consolidates each one with a few months of delay: when `from` and `to` are omitted the + * last 4 available base dates are consulted, ending 2 months before the current one. + * @param itemId The Item id + * @param {ScrFilters} options - optional `from` / `to` base dates, as YYYYMM + * @returns {ScrResponse} the SCR data for the Item's document + */ + async fetchItemScr(itemId: string, options: ScrFilters = {}): Promise { + return await this.createGetRequest(`items/${itemId}/scr`, { ...options }) + } + /** * Fetch accounts from an Item * @param itemId The Item id @@ -434,6 +462,97 @@ export class PluggyClient extends BaseApi { return await this.createGetRequest(`categories/${id}`) } + /** + * Fetch the category rules of your client + * @returns {PageResponse} paged response of category rules + */ + async fetchCategoryRules(): Promise> { + return await this.createGetRequest('categories/rules') + } + + /** + * Create a category rule: transactions matching the description are assigned the given category + * @param rule - the rule to create + * @returns {ClientCategoryRule} the created category rule + */ + async createCategoryRule(rule: CreateClientCategoryRule): Promise { + return await this.createPostRequest('categories/rules', null, rule) + } + + /** + * Delete a category rule. Clients can only delete their own rules. + * @param id - the category rule id + */ + async deleteCategoryRule(id: string): Promise { + await this.createDeleteRequest(`categories/rules/${id}`) + } + + /** + * Fetch merchant information for a list of CNPJs + * @param cnpjs - list of CNPJs to look up (sent as a comma-separated list) + * @returns {MerchantsResponse} found merchants, valid CNPJs that were not found, and invalid CNPJs + */ + async fetchMerchants(cnpjs: string[]): Promise { + return await this.createGetRequest('merchants', { cnpjs: cnpjs.join(',') }) + } + + /** + * BETA: Boleto Management is in beta and may change. + * + * Create a boleto connection from the institution's credentials + * @param payload - the connector id (with `supportsBoletoManagement`) and its credentials + * @returns {BoletoConnection} the created boleto connection + */ + async createBoletoConnection(payload: CreateBoletoConnection): Promise { + return await this.createPostRequest('boleto-connections', null, payload) + } + + /** + * BETA: Boleto Management is in beta and may change. + * + * Create a boleto connection from an existing Item + * @param payload - the Item id + * @returns {BoletoConnection} the created boleto connection + */ + async createBoletoConnectionFromItem( + payload: CreateBoletoConnectionFromItem + ): Promise { + return await this.createPostRequest('boleto-connections/from-item', null, payload) + } + + /** + * BETA: Boleto Management is in beta and may change. + * + * Issue a boleto through a boleto connection + * @param payload - the boleto connection id and the boleto data + * @returns {IssuedBoleto} the issued boleto + */ + async createBoleto(payload: CreateBoleto): Promise { + return await this.createPostRequest('boletos', null, payload) + } + + /** + * BETA: Boleto Management is in beta and may change. + * + * Fetch a single issued boleto + * @param id - the boleto id + * @returns {IssuedBoleto} the issued boleto + */ + async fetchBoleto(id: string): Promise { + return await this.createGetRequest(`boletos/${id}`) + } + + /** + * BETA: Boleto Management is in beta and may change. + * + * Cancel an issued boleto + * @param id - the boleto id + * @returns {IssuedBoleto} the cancelled boleto + */ + async cancelBoleto(id: string): Promise { + return await this.createPostRequest(`boletos/${id}/cancel`) + } + /** * Fetch a single webhook * @returns {Webhook} a webhook object diff --git a/src/paymentsClient.ts b/src/paymentsClient.ts index 04ab5de..837c245 100644 --- a/src/paymentsClient.ts +++ b/src/paymentsClient.ts @@ -34,6 +34,8 @@ import { SmartTransferPayment, CreateSmartTransferPayment, SmartTransferPreauthorizationsFilters, + SmartTransferPreauthorizationPaymentsFilters, + CreatePixQrPaymentRequest, } from './types' /** @@ -51,6 +53,15 @@ export class PluggyPaymentsClient extends BaseApi { return await this.createPostRequest(`payments/requests`, null, paymentRequest) } + /** + * Creates a payment request from a PIX QR code + * @param payload CreatePixQrPaymentRequest with the PIX QR code + * @returns {PaymentRequest} PaymentRequest object + */ + async createPixQrPaymentRequest(payload: CreatePixQrPaymentRequest): Promise { + return await this.createPostRequest('payments/requests/pix-qr', null, payload) + } + /** * Fetch a single payment request * @returns {PaymentRequest} PaymentRequest object @@ -447,6 +458,21 @@ export class PluggyPaymentsClient extends BaseApi { return await this.createGetRequest(`smart-transfers/preauthorizations/${id}`) } + /** + * Fetch the payments of a smart transfer preauthorization, ordered by date descending + * @param id ID of the smart transfer preauthorization + * @param options SmartTransferPreauthorizationPaymentsFilters with from, to, page and pageSize + * @returns {PageResponse} paged response of smart transfer payments + */ + async fetchSmartTransferPreauthorizationPayments( + id: string, + options: SmartTransferPreauthorizationPaymentsFilters = {} + ): Promise> { + return await this.createGetRequest(`smart-transfers/preauthorizations/${id}/payments`, { + ...options, + }) + } + /** * Creates a smart transfer payment * @param payload CreateSmartTransferPayment diff --git a/src/types/account.ts b/src/types/account.ts index a03a108..dfddcee 100644 --- a/src/types/account.ts +++ b/src/types/account.ts @@ -173,4 +173,11 @@ export type CreditData = { holderType: 'MAIN' | 'ADDITIONAL' | null /** Detailed credit limit information, broken down by credit line. Only returned for Open Finance connectors */ disaggregatedCreditLimits?: DisaggregatedCreditLimit[] + /** Additional credit cards associated with the main one */ + additionalCards?: AdditionalCard[] | null +} + +export type AdditionalCard = { + /** Number of the additional credit card */ + number: string } diff --git a/src/types/boleto.ts b/src/types/boleto.ts new file mode 100644 index 0000000..71de961 --- /dev/null +++ b/src/types/boleto.ts @@ -0,0 +1,142 @@ +export type CreateBoletoConnection = { + /** Connector identifier. The connector must have `supportsBoletoManagement` set to true */ + connectorId: number + /** Credentials required for the connection (for Inter: clientId, clientSecret, certificate and privateKey) */ + credentials: Record +} + +export type CreateBoletoConnectionFromItem = { + /** Item ID */ + itemId: string +} + +export type BoletoConnection = { + /** Primary identifier */ + id: string + /** Primary identifier of the connector associated with this connection */ + connectorId: number + /** Date when the connection was created */ + createdAt: Date + /** Date when the connection was last updated */ + updatedAt: Date +} + +export const BOLETO_FINE_TYPES = ['PERCENTAGE', 'FIXED'] as const +export type BoletoFineType = typeof BOLETO_FINE_TYPES[number] + +export const BOLETO_INTEREST_TYPES = ['PERCENTAGE'] as const +export type BoletoInterestType = typeof BOLETO_INTEREST_TYPES[number] + +export type BoletoFine = { + /** Fine value */ + value: number + /** Type of fine calculation */ + type: BoletoFineType +} + +export type BoletoInterest = { + /** Interest value */ + value: number + /** Type of interest calculation */ + type: BoletoInterestType +} + +export type CreateBoletoPayer = { + /** Payer tax number (CPF/CNPJ) */ + taxNumber: string + /** Payer name */ + name: string + /** Payer street address */ + addressStreet?: string + /** Payer city */ + addressCity?: string + /** Payer state */ + addressState: string + /** Payer ZIP code */ + addressZipCode: string +} + +export type CreateBoleto = { + /** Primary identifier of the boleto connection */ + boletoConnectionId: string + boleto: { + /** Your identifier for this boleto (max 10 characters) */ + seuNumero: string + /** Boleto amount (minimum 2.5) */ + amount: number + /** Due date for the boleto (ISO date-time). Must be today or in the future */ + dueDate: string + payer: CreateBoletoPayer + /** Fine information for late payment */ + fine?: BoletoFine + /** Interest information for late payment */ + interest?: BoletoInterest + } +} + +export const BOLETO_STATUSES = ['OPEN', 'PAID', 'OVERDUE', 'CANCELLED', 'PROTESTED'] as const +/** + * @typedef BoletoStatus + * - OPEN: issued and awaiting payment + * - PAID: paid (see `paidAt`, `amountPaid`, `paymentOrigin`) + * - OVERDUE: the due date has passed and the boleto has not been paid + * - CANCELLED: canceled by the issuer + * - PROTESTED: sent to a protesto/credit-bureau process + */ +export type BoletoStatus = typeof BOLETO_STATUSES[number] + +export const BOLETO_PAYMENT_ORIGINS = ['PIX', 'BOLETO'] as const +export type BoletoPaymentOrigin = typeof BOLETO_PAYMENT_ORIGINS[number] + +export type IssuedBoletoPayer = CreateBoletoPayer & { + /** Type of person (individual or business) */ + personType?: string + /** Payer address number */ + addressNumber?: string + /** Additional address information */ + addressComplement?: string + /** Payer neighborhood */ + addressNeighborhood?: string + /** Payer email */ + email?: string + /** Payer area code */ + ddd?: string + /** Payer phone number */ + phoneNumber?: string +} + +export type IssuedBoleto = { + /** Primary identifier */ + id: string + /** Boleto amount */ + amount: number + /** Current status of the boleto */ + status: BoletoStatus + /** Your identifier for this boleto */ + seuNumero: string + /** Due date of the boleto */ + dueDate: Date + payer: IssuedBoletoPayer + /** PIX QR code for payment */ + pixQr?: string + /** Boleto digitable line */ + digitableLine: string + /** Bank's internal identifier for the boleto */ + nossoNumero?: string + /** Boleto barcode */ + barcode: string + /** ID of the boleto connection used to create this boleto */ + boletoConnectionId: string + /** Date when the boleto was created */ + createdAt: Date + /** Amount that was paid for this boleto */ + amountPaid?: number | null + /** Origin of the payment when the boleto is paid */ + paymentOrigin?: BoletoPaymentOrigin | null + /** Fine information for late payment */ + fine?: BoletoFine | null + /** Interest information for late payment */ + interest?: BoletoInterest + /** Date when the boleto was paid */ + paidAt?: Date +} diff --git a/src/types/category.ts b/src/types/category.ts index 9b2b308..994b2be 100644 --- a/src/types/category.ts +++ b/src/types/category.ts @@ -8,3 +8,31 @@ export type Category = { /** Parent category hierachy name or description */ parentDescription?: string } + +export type ClientCategoryRule = { + /** Description of the transaction rule */ + description: string + /** Identifier of the category */ + categoryId?: string + /** Description of the category */ + category: string + /** Identifier of the client */ + clientId?: string + /** Transaction type (DEBIT/CREDIT) */ + transactionType?: string + /** Account type (CHECKING_ACCOUNT/CREDIT_CARD) */ + accountType?: string +} + +export type CreateClientCategoryRule = { + /** Description of the transaction rule */ + description: string + /** Identifier of the category */ + categoryId: string + /** Transaction type (DEBIT/CREDIT) */ + transactionType?: string + /** Account type (CHECKING_ACCOUNT/CREDIT_CARD) */ + accountType?: string + /** Type of match used to identify the rule (exact/contains/startsWith/endsWith). Defaults to 'exact' */ + matchType?: string +} diff --git a/src/types/connector.ts b/src/types/connector.ts index fa21d6f..2cad5cb 100644 --- a/src/types/connector.ts +++ b/src/types/connector.ts @@ -259,6 +259,12 @@ export type Connector = { resetPasswordUrl?: string /** list of products supported by the institution */ products: ProductType[] + /** + * Which sub-products the institution serves, in the Open Finance directory's own vocabulary + * (for example `INVESTMENTS:TREASURE_TITLES`, `CREDIT_OPERATIONS:INVOICE_FINANCINGS`). + * Absent for direct connectors, which have no Open Finance participant. + */ + productCoverage?: string[] /** Connector creation date */ createdAt: Date /** Date of last modification of the connector */ diff --git a/src/types/index.ts b/src/types/index.ts index 507322b..a4fb423 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -17,3 +17,6 @@ export * from './payments' export * from './transaction' export * from './webhook' export * from './accountStatement' +export * from './scr' +export * from './merchant' +export * from './boleto' diff --git a/src/types/investment.ts b/src/types/investment.ts index 1e470ac..e54fc72 100644 --- a/src/types/investment.ts +++ b/src/types/investment.ts @@ -270,6 +270,23 @@ export type Investment = { owner: string | null /** Financial institution holder of the investment */ institution: InvestmentInstitution | null + /** Coupon-payment schedule for coupon-bearing fixed income / Treasury bonds */ + couponPayment?: InvestmentCouponPayment | null + /** The date when the grace period ends (fixed-income investments only) */ + gracePeriodDate?: Date | null + /** B3 lot/price conversion factor (variable income) */ + priceFactor?: number | null + /** Whether the product is tax-exempt (LCI, LCA, CRI, CRA, debêntures incentivadas) */ + taxExempt?: boolean | null +} + +export type InvestmentCouponPayment = { + /** Whether the paper pays periodic coupons */ + hasCoupon?: boolean + /** Frequency of coupon payments (MONTHLY, QUARTERLY, SEMESTERLY, YEARLY, IRREGULAR) */ + periodicity?: string + /** Free-text detail when periodicity is IRREGULAR */ + additionalInfo?: string } export type InvestmentsFilters = PageFilters diff --git a/src/types/merchant.ts b/src/types/merchant.ts new file mode 100644 index 0000000..3dff288 --- /dev/null +++ b/src/types/merchant.ts @@ -0,0 +1,10 @@ +import { TransactionMerchantData } from './transaction' + +export type MerchantsResponse = { + /** List of merchants found for the provided CNPJs */ + foundMerchants: TransactionMerchantData[] + /** List of valid CNPJs that were not found in the merchant database */ + notFoundMerchants: string[] + /** List of invalid CNPJ values provided */ + invalidCnpjs: string[] +} diff --git a/src/types/payments/paymentFilters.ts b/src/types/payments/paymentFilters.ts index ac6422a..1b44fbb 100644 --- a/src/types/payments/paymentFilters.ts +++ b/src/types/payments/paymentFilters.ts @@ -11,3 +11,10 @@ export type PaymentCustomersFilters = PageFilters export type PaymentInstitutionsFilters = PageFilters & { name?: string } export type SmartTransferPreauthorizationsFilters = PageFilters + +export type SmartTransferPreauthorizationPaymentsFilters = PageFilters & { + /** Filter payments created from this date ('YYYY-MM-DD') */ + from?: string + /** Filter payments created until this date ('YYYY-MM-DD') */ + to?: string +} diff --git a/src/types/payments/paymentRequest.ts b/src/types/payments/paymentRequest.ts index 6541649..a39b7f8 100644 --- a/src/types/payments/paymentRequest.ts +++ b/src/types/payments/paymentRequest.ts @@ -51,6 +51,17 @@ export type CreatePaymentRequest = { isSandbox: boolean } +export type CreatePixQrPaymentRequest = { + /** Pix QR code */ + pixQrCode: string + /** Redirect urls after the payment was completed or ended in error status */ + callbackUrls?: CallbackUrls | null + /** Customer identifier associated to the payment */ + customerId?: string + /** Indicates if this payment request should be created in sandbox mode. Default: false */ + isSandbox?: boolean +} + export type PaymentRequestSchedule = | SingleSchedule | DailySchedule diff --git a/src/types/scr.ts b/src/types/scr.ts new file mode 100644 index 0000000..1554648 --- /dev/null +++ b/src/types/scr.ts @@ -0,0 +1,100 @@ +export type ScrFilters = { + /** First base date to consult, as YYYYMM. Defaults to 3 base dates before `to` */ + from?: string + /** Last base date to consult, as YYYYMM. Defaults to 2 months before the current one, the most recent base date Bacen has consolidated */ + to?: string +} + +/** + * Bacen's SCR response, forwarded exactly as the Banco Central returns it: field names, + * codes and all. Nothing is renamed or normalised. + */ +export type ScrResponse = { + /** The base dates consulted, ie. "202604 a 202607" */ + dtbConsult: string + /** The consulted document: the CPF for an individual, or the 8-digit CNPJ root for a company */ + cdCli: string + /** Type of client: "1" for an individual, "2" for a legal entity */ + tpCli: '1' | '2' + /** One entry per consulted base date. A base date with no data for the client is still listed, with no operations */ + lsDtb?: ScrDatabase[] + /** Validation messages raised by Bacen for this request */ + listaDeMensagensDeValidacao?: ScrValidationMessage[] +} + +/** One consulted base date, and what the SCR holds for the client in it */ +export type ScrDatabase = { + /** The base date, as YYYYMM. Numeric, not a string */ + dtb?: number + /** Bacen's message for this base date, when it has one */ + msg?: string + /** Percentage of the expected 3040 documents already incorporated by Bacen for this base date */ + docProc?: string + /** Percentage of the expected operation volume already accepted for this base date */ + volProc?: string + /** Number of financial institutions where the client has operations */ + qtdIfs?: number + /** Number of financial conglomerates where the client has operations */ + qtdCongFinc?: number + /** Start of the client's relationship with the national financial system */ + dtbIniRel?: string + /** Co-obligation assumed by the client in credit assignments, in BRL */ + coobAss?: number + /** Co-obligation received in credit assignments, in BRL */ + coobRec?: number + /** Operation groups reported for this base date */ + lsOp?: ScrOperation[] +} + +/** + * A group of credit operations. The SCR aggregates contracts by the combination of + * modality, source of funds, index and exchange variation. + */ +export type ScrOperation = { + /** Bacen's code for the operation modality */ + mod?: string + /** Bacen's code for the source of funds */ + oriRec?: string + /** Bacen's code for the reference rate or index */ + indx?: string + /** Bacen's code for the exchange rate variation */ + varCamb?: string + /** Present when the operation is under dispute: "D" for disagreement, "J" for sub judice, "JD" for both */ + subJDisc?: string + /** Balance of the operation group split across Bacen's maturity vertices */ + resVenc?: ScrMaturityBalances + /** Guarantees backing the operations in this group */ + lsGar?: ScrGuarantee[] + /** Complementary information reported for this group */ + lsInfAd?: ScrAdditionalInfo[] +} + +/** + * Balance of the operation group split across Bacen's maturity vertices, in BRL. + * Each key is a vertex code (`v20`, `v40`, ..., `v320`) as defined by Bacen's DOC3040 + * reference. Only the vertices that carry a value are present. + */ +export type ScrMaturityBalances = { + [vertex: string]: number +} + +export type ScrGuarantee = { + /** Bacen's code for the guarantee type */ + tp?: string + /** Number of operations grouped under this type */ + qtd?: number +} + +export type ScrAdditionalInfo = { + /** Type of the complementary information */ + tp?: string + /** Code of the complementary information */ + cd?: string + /** Number of operations grouped under this entry */ + qtd?: number +} + +export type ScrValidationMessage = { + codigo?: string + mensagem?: string +} diff --git a/src/types/transaction.ts b/src/types/transaction.ts index 05f192d..5582ed3 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -70,6 +70,11 @@ export type TransactionPaymentData = { reason?: string /** Additional data related to boleto transaction */ boletoMetadata: TransactionBoletoMetadataResponse | null + /** + * Authentication code of the payment receipt, as printed by the institution on the proof of payment. + * It identifies the operation itself, so it can be present for any payment method (PIX, TED, DOC, BOLETO). + */ + authenticationCode?: string } export type TransactionMerchantData = { @@ -222,6 +227,8 @@ export type Transaction = { operationTypeAdditionalInfo: string | null /** Provider ID of the transaction. Only returned for Open Finance connectors */ providerId: string | null + /** Sequential position of the transaction within the same day, used to preserve ordering when multiple transactions share the same date. */ + order?: number /** Date when the transaction was created in Pluggy */ createdAt: Date /** Date when the transaction was last updated in Pluggy */ diff --git a/tests/boletos.test.ts b/tests/boletos.test.ts new file mode 100644 index 0000000..0ff6a8d --- /dev/null +++ b/tests/boletos.test.ts @@ -0,0 +1,130 @@ +import nock from 'nock' +import { setupAuth } from './utils' +import { PluggyClient } from '../src/client' +import { BoletoConnection, CreateBoleto, IssuedBoleto } from '../src/types' + +const API_URL = process.env.PLUGGY_API_URL! +const BOLETO_ID = '82da0d63-fbc0-4e20-b191-50e6df030875' +const CONNECTION_ID = 'dc3537ad-13b4-4770-b248-e4578983899c' + +const connectionBody = { + id: CONNECTION_ID, + connectorId: 225, + createdAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-01-01T00:00:00.000Z', +} +const expectedConnection: BoletoConnection = { + id: CONNECTION_ID, + connectorId: 225, + createdAt: new Date('2026-01-01T00:00:00.000Z'), + updatedAt: new Date('2026-01-01T00:00:00.000Z'), +} + +const boletoBody = { + id: BOLETO_ID, + amount: 100, + status: 'OPEN', + seuNumero: 'INV-1', + dueDate: '2026-12-01T00:00:00.000Z', + payer: { + taxNumber: '41679495000100', + name: 'NOME LEGAL EMPRESA', + addressState: 'SP', + addressZipCode: '01000000', + }, + digitableLine: '00190000090000000000000000000000000000000000000', + barcode: '00190000000000000000000000000000000000000000', + boletoConnectionId: CONNECTION_ID, + createdAt: '2026-10-01T00:00:00.000Z', + amountPaid: null, + paymentOrigin: null, +} + +function client(): PluggyClient { + return new PluggyClient({ clientId: '123', clientSecret: '456' }) +} + +describe('boletos (beta)', () => { + beforeEach(() => { + nock.cleanAll() + nock.disableNetConnect() + setupAuth() + }) + + it('createBoletoConnection POSTs boleto-connections with the payload', async () => { + const payload = { connectorId: 225, credentials: { clientId: 'id', clientSecret: 'secret' } } + const mock = nock(API_URL) + .post('/boleto-connections', payload) + .reply(200, connectionBody) + + const result = await client().createBoletoConnection(payload) + + expect(result).toEqual(expectedConnection) + expect(mock.isDone()).toBe(true) + }) + + it('createBoletoConnectionFromItem POSTs boleto-connections/from-item with the item id', async () => { + const payload = { itemId: '0303c07b-fef0-4903-af9a-007fa086ca8c' } + const mock = nock(API_URL) + .post('/boleto-connections/from-item', payload) + .reply(200, connectionBody) + + const result = await client().createBoletoConnectionFromItem(payload) + + expect(result).toEqual(expectedConnection) + expect(mock.isDone()).toBe(true) + }) + + it('createBoleto POSTs boletos with the payload and deserializes dates', async () => { + const payload: CreateBoleto = { + boletoConnectionId: CONNECTION_ID, + boleto: { + seuNumero: 'INV-1', + amount: 100, + dueDate: '2026-12-01T00:00:00.000Z', + payer: { + taxNumber: '41679495000100', + name: 'NOME LEGAL EMPRESA', + addressState: 'SP', + addressZipCode: '01000000', + }, + fine: { value: 2, type: 'PERCENTAGE' }, + interest: { value: 1, type: 'PERCENTAGE' }, + }, + } + const mock = nock(API_URL) + .post('/boletos', payload) + .reply(200, boletoBody) + + const result: IssuedBoleto = await client().createBoleto(payload) + + expect(result.id).toBe(BOLETO_ID) + expect(result.status).toBe('OPEN') + expect(result.dueDate).toEqual(new Date('2026-12-01T00:00:00.000Z')) + expect(result.amountPaid).toBeNull() + expect(mock.isDone()).toBe(true) + }) + + it('fetchBoleto GETs boletos/:id', async () => { + const mock = nock(API_URL) + .get(`/boletos/${BOLETO_ID}`) + .reply(200, boletoBody) + + const result = await client().fetchBoleto(BOLETO_ID) + + expect(result.id).toBe(BOLETO_ID) + expect(result.digitableLine).toBe(boletoBody.digitableLine) + expect(mock.isDone()).toBe(true) + }) + + it('cancelBoleto POSTs boletos/:id/cancel', async () => { + const mock = nock(API_URL) + .post(`/boletos/${BOLETO_ID}/cancel`) + .reply(200, { ...boletoBody, status: 'CANCELLED' }) + + const result = await client().cancelBoleto(BOLETO_ID) + + expect(result.status).toBe('CANCELLED') + expect(mock.isDone()).toBe(true) + }) +}) diff --git a/tests/categoryRules.test.ts b/tests/categoryRules.test.ts new file mode 100644 index 0000000..452084d --- /dev/null +++ b/tests/categoryRules.test.ts @@ -0,0 +1,70 @@ +import nock from 'nock' +import { setupAuth } from './utils' +import { PluggyClient } from '../src/client' +import { ClientCategoryRule, CreateClientCategoryRule, PageResponse } from '../src/types' + +const API_URL = process.env.PLUGGY_API_URL! + +const rule: ClientCategoryRule = { + description: 'uber payment', + category: 'Taxi and ride-hailing', + categoryId: '19050000', + transactionType: 'DEBIT', + accountType: 'CHECKING_ACCOUNT', +} + +describe('category rules', () => { + beforeEach(() => { + nock.cleanAll() + nock.disableNetConnect() + setupAuth() + }) + + it('fetchCategoryRules GETs categories/rules', async () => { + const page: PageResponse = { + page: 1, + total: 1, + totalPages: 1, + results: [rule], + } + const mock = nock(API_URL) + .get('/categories/rules') + .reply(200, page) + + const client = new PluggyClient({ clientId: '123', clientSecret: '456' }) + const result = await client.fetchCategoryRules() + + expect(result).toEqual(page) + expect(mock.isDone()).toBe(true) + }) + + it('createCategoryRule POSTs categories/rules with the rule as body', async () => { + const payload: CreateClientCategoryRule = { + description: 'uber payment', + categoryId: '19050000', + transactionType: 'DEBIT', + accountType: 'CHECKING_ACCOUNT', + matchType: 'contains', + } + const mock = nock(API_URL) + .post('/categories/rules', payload) + .reply(200, rule) + + const client = new PluggyClient({ clientId: '123', clientSecret: '456' }) + const result = await client.createCategoryRule(payload) + + expect(result).toEqual(rule) + expect(mock.isDone()).toBe(true) + }) + + it('deleteCategoryRule DELETEs categories/rules/:id and resolves on 204', async () => { + const mock = nock(API_URL) + .delete('/categories/rules/rule-1') + .reply(204) + + const client = new PluggyClient({ clientId: '123', clientSecret: '456' }) + await expect(client.deleteCategoryRule('rule-1')).resolves.toBeUndefined() + + expect(mock.isDone()).toBe(true) + }) +}) diff --git a/tests/merchants.test.ts b/tests/merchants.test.ts new file mode 100644 index 0000000..d36b56a --- /dev/null +++ b/tests/merchants.test.ts @@ -0,0 +1,40 @@ +import nock from 'nock' +import { setupAuth } from './utils' +import { PluggyClient } from '../src/client' +import { MerchantsResponse } from '../src/types' + +const API_URL = process.env.PLUGGY_API_URL! + +describe('fetchMerchants', () => { + beforeEach(() => { + nock.cleanAll() + nock.disableNetConnect() + setupAuth() + }) + + it('GETs merchants with the CNPJs as a comma-separated list', async () => { + const response: MerchantsResponse = { + foundMerchants: [ + { + businessName: 'BANCO DO BRASIL SA', + cnpj: '00000000000191', + name: 'BANCO DO BRASIL', + cnae: '6422100', + category: 'Banking', + }, + ], + notFoundMerchants: ['60701190000104'], + invalidCnpjs: ['123'], + } + const mock = nock(API_URL) + .get('/merchants') + .query({ cnpjs: '00000000000191,60701190000104,123' }) + .reply(200, response) + + const client = new PluggyClient({ clientId: '123', clientSecret: '456' }) + const result = await client.fetchMerchants(['00000000000191', '60701190000104', '123']) + + expect(result).toEqual(response) + expect(mock.isDone()).toBe(true) + }) +}) diff --git a/tests/payments/pixQrPaymentRequest.test.ts b/tests/payments/pixQrPaymentRequest.test.ts new file mode 100644 index 0000000..797168c --- /dev/null +++ b/tests/payments/pixQrPaymentRequest.test.ts @@ -0,0 +1,28 @@ +import { CreatePixQrPaymentRequest, PaymentRequest } from '../../src/types' +import { API_URL, createPaymentsClient, mockAs, nock } from './utils' + +describe('PluggyPaymentsClient — PIX QR payment requests', () => { + beforeEach(() => { + nock.cleanAll() + }) + + it('createPixQrPaymentRequest POSTs payments/requests/pix-qr with payload', async () => { + const payload: CreatePixQrPaymentRequest = { + pixQrCode: '00020126490014br.gov.bcb.pix0108dict-key5204000053039865802BR6304ABCD', + callbackUrls: null, + isSandbox: true, + } + const mock = nock(API_URL) + .post('/payments/requests/pix-qr', payload) + .reply( + 200, + mockAs({ id: 'pr-1', amount: 100.5, status: 'CREATED' }) + ) + + const client = createPaymentsClient() + const result = await client.createPixQrPaymentRequest(payload) + + expect(result).toEqual({ id: 'pr-1', amount: 100.5, status: 'CREATED' }) + expect(mock.isDone()).toBe(true) + }) +}) diff --git a/tests/payments/smartTransferPreauthorizationPayments.test.ts b/tests/payments/smartTransferPreauthorizationPayments.test.ts new file mode 100644 index 0000000..fa6dd7a --- /dev/null +++ b/tests/payments/smartTransferPreauthorizationPayments.test.ts @@ -0,0 +1,44 @@ +import { PageResponse, SmartTransferPayment } from '../../src/types' +import { API_URL, createPaymentsClient, mockAs, nock } from './utils' + +describe('PluggyPaymentsClient — smart transfer preauthorization payments', () => { + beforeEach(() => { + nock.cleanAll() + }) + + it('fetchSmartTransferPreauthorizationPayments GETs smart-transfers/preauthorizations/:id/payments', async () => { + const page: PageResponse = { + page: 1, + total: 1, + totalPages: 1, + results: [mockAs({ id: 'pay-1', preauthorizationId: 'pre-1' })], + } + const mock = nock(API_URL) + .get('/smart-transfers/preauthorizations/pre-1/payments') + .reply(200, page) + + const client = createPaymentsClient() + const result = await client.fetchSmartTransferPreauthorizationPayments('pre-1') + + expect(result).toEqual(page) + expect(mock.isDone()).toBe(true) + }) + + it('passes from, to, page and pageSize as query params', async () => { + const mock = nock(API_URL) + .get('/smart-transfers/preauthorizations/pre-1/payments') + .query({ from: '2026-01-01', to: '2026-12-31', page: '2', pageSize: '50' }) + .reply(200, { page: 2, total: 0, totalPages: 0, results: [] }) + + const client = createPaymentsClient() + const result = await client.fetchSmartTransferPreauthorizationPayments('pre-1', { + from: '2026-01-01', + to: '2026-12-31', + page: 2, + pageSize: 50, + }) + + expect(result.results).toEqual([]) + expect(mock.isDone()).toBe(true) + }) +}) diff --git a/tests/scr.test.ts b/tests/scr.test.ts new file mode 100644 index 0000000..7365c56 --- /dev/null +++ b/tests/scr.test.ts @@ -0,0 +1,71 @@ +import nock from 'nock' +import { setupAuth } from './utils' +import { PluggyClient } from '../src/client' +import { ScrResponse } from '../src/types' + +const ITEM_ID = 'item-abc' +const API_URL = process.env.PLUGGY_API_URL! + +const scrResponse: ScrResponse = { + dtbConsult: '202604 a 202607', + cdCli: '12345678900', + tpCli: '1', + lsDtb: [ + { + dtb: 202607, + qtdIfs: 2, + lsOp: [{ mod: '0203', resVenc: { v20: 1500.5, v110: 120 }, lsGar: [{ tp: '0101', qtd: 1 }] }], + }, + ], +} + +describe('fetchItemScr', () => { + beforeEach(() => { + nock.cleanAll() + nock.disableNetConnect() + setupAuth() + }) + + it('GETs items/:id/scr with no query when no range is given', async () => { + const mock = nock(API_URL) + .get(`/items/${ITEM_ID}/scr`) + .reply(200, scrResponse) + + const client = new PluggyClient({ clientId: '123', clientSecret: '456' }) + const result = await client.fetchItemScr(ITEM_ID) + + expect(result).toEqual(scrResponse) + expect(mock.isDone()).toBe(true) + }) + + it('passes from and to as query params', async () => { + const mock = nock(API_URL) + .get(`/items/${ITEM_ID}/scr`) + .query({ from: '202604', to: '202607' }) + .reply(200, scrResponse) + + const client = new PluggyClient({ clientId: '123', clientSecret: '456' }) + const result = await client.fetchItemScr(ITEM_ID, { from: '202604', to: '202607' }) + + expect(result).toEqual(scrResponse) + expect(mock.isDone()).toBe(true) + }) + + it('rejects with the API error body when the SCR feature is not enabled (403)', async () => { + const errorBody = { + code: 403, + codeDescription: 'SCR_FEATURE_NOT_ENABLED', + message: 'This client is not enabled to query SCR data.', + } + const mock = nock(API_URL) + .get(`/items/${ITEM_ID}/scr`) + .reply(403, errorBody) + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => undefined) + + const client = new PluggyClient({ clientId: '123', clientSecret: '456' }) + await expect(client.fetchItemScr(ITEM_ID)).rejects.toEqual(errorBody) + + expect(mock.isDone()).toBe(true) + consoleError.mockRestore() + }) +})