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 1884f7c..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 = { @@ -113,6 +118,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 +150,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 & { @@ -205,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() + }) +}) 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() + }) +})