From d6575ce2e1917db0842e02873c6970cabba0e1ef Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 18:03:03 +0600 Subject: [PATCH 01/18] feat(payments): add capture, sandboxPaid, sandboxSettle methods and fix capture_method type --- .../integration/paymentServiceCapture.test.ts | 181 ++++++++++++++++++ .../payments/src/services/paymentService.ts | 48 +++++ packages/payments/src/types/payment.ts | 2 +- 3 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 packages/payments/__tests__/integration/paymentServiceCapture.test.ts diff --git a/packages/payments/__tests__/integration/paymentServiceCapture.test.ts b/packages/payments/__tests__/integration/paymentServiceCapture.test.ts new file mode 100644 index 00000000..b7185d6b --- /dev/null +++ b/packages/payments/__tests__/integration/paymentServiceCapture.test.ts @@ -0,0 +1,181 @@ +import { + createOakClient, + Payment, + createPaymentService, + createCustomerService, +} from '../../src'; +import { PaymentService } from '../../src/services/paymentService'; +import { CustomerService } from '../../src/services/customerService'; +import { getConfigFromEnv } from '../config'; + +const INTEGRATION_TEST_TIMEOUT = 30000; + +/** + * Build a Stripe card payment request with manual capture. + */ +const buildStripeManualCaptureRequest = ( + customerId: string, +): Payment.Request => { + const request: Record = { + provider: 'stripe', + source: { + amount: 1500, + currency: 'usd', + payment_method: { type: 'card' }, + capture_method: 'manual', + customer: { id: customerId }, + }, + confirm: true, + metadata: { + order_id: `test-capture-${Date.now()}`, + }, + }; + return request as unknown as Payment.Request; +}; + +/** + * Build a PagarMe PIX payment request (no capture_method). + */ +const buildPagarMePixPaymentRequest = ( + customerId: string, +): Payment.Request => + ({ + provider: 'pagar_me', + source: { + amount: 100, + currency: 'brl', + customer: { id: customerId }, + payment_method: { type: 'pix', expiry_date: '2030-01-01' }, + }, + confirm: true, + }) as unknown as Payment.Request; + +describe('PaymentService - Capture & Sandbox', () => { + let payments: PaymentService; + let customers: CustomerService; + let approvedCustomerId: string | undefined; + + beforeAll(async () => { + const client = createOakClient({ + ...getConfigFromEnv(), + retryOptions: { + maxNumberOfRetries: 2, + delay: 500, + backoffFactor: 2, + }, + }); + customers = createCustomerService(client); + payments = createPaymentService(client); + + // Find an approved customer + const listRes = await customers.list({ + target_role: 'customer', + provider_registration_status: 'approved', + provider: 'stripe', + }); + + if (listRes.ok && listRes.value.data.customer_list.length > 0) { + approvedCustomerId = (listRes.value.data.customer_list[0].id ?? + listRes.value.data.customer_list[0].customer_id) as string; + } + }, INTEGRATION_TEST_TIMEOUT); + + // --------------------------------------------------------------- + // capture() + // --------------------------------------------------------------- + describe('capture', () => { + it( + 'should have a capture method on the service', + () => { + expect(typeof payments.capture).toBe('function'); + }, + ); + + it( + 'should return an error for an invalid payment ID', + async () => { + const response = await payments.capture('non-existent-id'); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // sandboxPaid() + // --------------------------------------------------------------- + describe('sandboxPaid', () => { + it( + 'should have a sandboxPaid method on the service', + () => { + expect(typeof payments.sandboxPaid).toBe('function'); + }, + ); + + it( + 'should mark a sandbox payment as paid', + async () => { + if (!approvedCustomerId) { + throw new Error('No approved customer available'); + } + + // Create a Stripe payment (confirmed=false so it stays in a sandbox-actionable state) + const createRes = await payments.create({ + provider: 'stripe', + source: { + amount: 1500, + currency: 'usd', + payment_method: { type: 'card' }, + capture_method: 'automatic', + customer: { id: approvedCustomerId }, + }, + confirm: false, + metadata: { order_id: `test-sandbox-paid-${Date.now()}` }, + } as unknown as Payment.Request); + expect(createRes.ok).toBe(true); + if (!createRes.ok) return; + + const paymentId = createRes.value.data.id; + const response = await payments.sandboxPaid(paymentId); + + // sandboxPaid may succeed or return error depending on payment state; + // we verify the method exists and hits the right endpoint + expect(response).toBeDefined(); + if (response.ok) { + expect(response.value.data.id).toEqual(paymentId); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); + + it( + 'should return an error for an invalid payment ID', + async () => { + const response = await payments.sandboxPaid('non-existent-id'); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // sandboxSettle() + // --------------------------------------------------------------- + describe('sandboxSettle', () => { + it( + 'should have a sandboxSettle method on the service', + () => { + expect(typeof payments.sandboxSettle).toBe('function'); + }, + ); + + it( + 'should return an error for an invalid payment ID', + async () => { + const response = await payments.sandboxSettle('non-existent-id'); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); +}); diff --git a/packages/payments/src/services/paymentService.ts b/packages/payments/src/services/paymentService.ts index d8b0cf90..3fbf0646 100644 --- a/packages/payments/src/services/paymentService.ts +++ b/packages/payments/src/services/paymentService.ts @@ -7,6 +7,9 @@ export interface PaymentService { create(payment: Payment.Request): Promise>; confirm(paymentId: string): Promise>; cancel(paymentId: string): Promise>; + capture(paymentId: string): Promise>; + sandboxPaid(paymentId: string): Promise>; + sandboxSettle(paymentId: string): Promise>; } /** @@ -58,4 +61,49 @@ export const createPaymentService = (client: OakClient): PaymentService => ({ ), ); }, + + async capture(paymentId: string): Promise> { + return withAuth(client, (token) => + httpClient.post( + buildUrl(client.config.baseUrl, "api/v1/payments", paymentId, "capture"), + {}, + { + headers: { + Authorization: `Bearer ${token}`, + }, + retryOptions: client.retryOptions, + }, + ), + ); + }, + + async sandboxPaid(paymentId: string): Promise> { + return withAuth(client, (token) => + httpClient.post( + buildUrl(client.config.baseUrl, "api/v1/payments", paymentId, "sandbox", "paid"), + {}, + { + headers: { + Authorization: `Bearer ${token}`, + }, + retryOptions: client.retryOptions, + }, + ), + ); + }, + + async sandboxSettle(paymentId: string): Promise> { + return withAuth(client, (token) => + httpClient.post( + buildUrl(client.config.baseUrl, "api/v1/payments", paymentId, "sandbox", "settle"), + {}, + { + headers: { + Authorization: `Bearer ${token}`, + }, + retryOptions: client.retryOptions, + }, + ), + ); + }, }); diff --git a/packages/payments/src/types/payment.ts b/packages/payments/src/types/payment.ts index 0dacfec4..4b227c4b 100644 --- a/packages/payments/src/types/payment.ts +++ b/packages/payments/src/types/payment.ts @@ -48,7 +48,7 @@ export namespace Payment { payment_method: PaymentMethod; installments?: number; float_rate?: number; - capture_method: "automatic"; + capture_method: "automatic" | "manual"; fraud_check?: FraudCheck; } From 70b4dd44e155cd6aa63a2808cccfb6c31a432047 Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 18:08:35 +0600 Subject: [PATCH 02/18] feat: add subscription service and plan type --- .../integration/subscriptionService.test.ts | 131 ++++++++++++++++++ packages/payments/src/services/index.ts | 3 + .../src/services/subscriptionService.ts | 124 +++++++++++++++++ packages/payments/src/types/index.ts | 1 + packages/payments/src/types/plan.ts | 6 + packages/payments/src/types/subscription.ts | 83 +++++++++++ packages/payments/src/utils/httpClient.ts | 7 +- 7 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 packages/payments/__tests__/integration/subscriptionService.test.ts create mode 100644 packages/payments/src/services/subscriptionService.ts create mode 100644 packages/payments/src/types/subscription.ts diff --git a/packages/payments/__tests__/integration/subscriptionService.test.ts b/packages/payments/__tests__/integration/subscriptionService.test.ts new file mode 100644 index 00000000..b48451c9 --- /dev/null +++ b/packages/payments/__tests__/integration/subscriptionService.test.ts @@ -0,0 +1,131 @@ +import { + createOakClient, + createSubscriptionService, + createPlanService, +} from '../../src'; +import { SubscriptionService } from '../../src/services/subscriptionService'; +import { PlanService } from '../../src/services/planService'; +import { getConfigFromEnv } from '../config'; + +const INTEGRATION_TEST_TIMEOUT = 30000; + +describe('SubscriptionService - Integration', () => { + let subscriptions: SubscriptionService; + let plans: PlanService; + + /** Plan hash_id created during setup. */ + let testPlanId: string | undefined; + + beforeAll(async () => { + const client = createOakClient({ + ...getConfigFromEnv(), + retryOptions: { + maxNumberOfRetries: 2, + delay: 500, + backoffFactor: 2, + }, + }); + subscriptions = createSubscriptionService(client); + plans = createPlanService(client); + + // Try to get an existing plan, or note that none exist + const listRes = await plans.list({ per_page: 1 }); + if (listRes.ok && listRes.value.data.data.length > 0) { + testPlanId = listRes.value.data.data[0].hash_id; + } + }, INTEGRATION_TEST_TIMEOUT); + + // --------------------------------------------------------------- + // Service shape + // --------------------------------------------------------------- + describe('service interface', () => { + it('should expose subscribe method', () => { + expect(typeof subscriptions.subscribe).toBe('function'); + }); + + it('should expose cancel method', () => { + expect(typeof subscriptions.cancel).toBe('function'); + }); + + it('should expose list method', () => { + expect(typeof subscriptions.list).toBe('function'); + }); + + it('should expose get method', () => { + expect(typeof subscriptions.get).toBe('function'); + }); + + it('should expose pay method', () => { + expect(typeof subscriptions.pay).toBe('function'); + }); + }); + + // --------------------------------------------------------------- + // get() - error path + // --------------------------------------------------------------- + describe('get', () => { + it( + 'should return an error for a non-existent subscription', + async () => { + const response = await subscriptions.get('non-existent-id'); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // cancel() - error path + // --------------------------------------------------------------- + describe('cancel', () => { + it( + 'should return an error for a non-existent subscription', + async () => { + const response = await subscriptions.cancel('non-existent-id'); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // pay() - error path + // --------------------------------------------------------------- + describe('pay', () => { + it( + 'should return an error for a non-existent subscription', + async () => { + const response = await subscriptions.pay('non-existent-id', { + customer_id: 'fake-customer-id', + payment_method_id: 'fake-pm-id', + payment_method_type: 'CARD', + payment_method_provider: 'PAGAR_ME', + }); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // subscribe() - error path (invalid plan) + // --------------------------------------------------------------- + describe('subscribe', () => { + it( + 'should return an error for an invalid plan', + async () => { + const response = await subscriptions.subscribe({ + plan_id: 'non-existent-plan', + source_customer_id: 'fake-source', + destination_customer_id: 'fake-dest', + payment_method_id: 'fake-pm', + payment_method_type: 'CARD', + payment_method_provider: 'PAGAR_ME', + fee_bearer: 'connected_account', + }); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); +}); diff --git a/packages/payments/src/services/index.ts b/packages/payments/src/services/index.ts index 47eeb926..8a1c1354 100644 --- a/packages/payments/src/services/index.ts +++ b/packages/payments/src/services/index.ts @@ -30,3 +30,6 @@ export type { WebhookService } from "./webhookService"; export { createRefundService } from "./refundService"; export type { RefundService } from "./refundService"; + +export { createSubscriptionService } from "./subscriptionService"; +export type { SubscriptionService } from "./subscriptionService"; diff --git a/packages/payments/src/services/subscriptionService.ts b/packages/payments/src/services/subscriptionService.ts new file mode 100644 index 00000000..e512646c --- /dev/null +++ b/packages/payments/src/services/subscriptionService.ts @@ -0,0 +1,124 @@ +import type { Subscription, OakClient, Result } from "../types"; +import { httpClient } from "../utils/httpClient"; +import { buildQueryString } from "./helpers"; +import { withAuth } from "../utils/withAuth"; +import { buildUrl } from "../utils/buildUrl"; + +export interface SubscriptionService { + subscribe( + request: Subscription.SubscribeRequest, + ): Promise>; + cancel(subscriptionId: string): Promise>; + list( + params: Subscription.ListQuery, + ): Promise>; + get(subscriptionId: string): Promise>; + pay( + subscriptionId: string, + request: Subscription.PaymentRequest, + ): Promise>; +} + +/** + * @param client - Configured OakClient instance + * @returns SubscriptionService instance + */ +export const createSubscriptionService = ( + client: OakClient, +): SubscriptionService => ({ + async subscribe( + request: Subscription.SubscribeRequest, + ): Promise> { + return withAuth(client, (token) => + httpClient.post( + buildUrl(client.config.baseUrl, "api/v1/subscription/subscribe"), + request, + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, + + async cancel( + subscriptionId: string, + ): Promise> { + return withAuth(client, (token) => + httpClient.patch( + buildUrl( + client.config.baseUrl, + "api/v1/subscription/subscriptions", + subscriptionId, + "cancel", + ), + undefined, + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, + + async list( + params: Subscription.ListQuery, + ): Promise> { + const { customer_id, ...queryParams } = params; + const queryString = buildQueryString( + Object.keys(queryParams).length > 0 ? queryParams : undefined, + ); + return withAuth(client, (token) => + httpClient.get( + `${buildUrl(client.config.baseUrl, "api/v1/subscription/list")}${queryString}`, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + retryOptions: client.retryOptions, + body: JSON.stringify({ customer_id }), + }, + ), + ); + }, + + async get( + subscriptionId: string, + ): Promise> { + return withAuth(client, (token) => + httpClient.get( + buildUrl( + client.config.baseUrl, + "api/v1/subscription", + subscriptionId, + ), + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, + + async pay( + subscriptionId: string, + request: Subscription.PaymentRequest, + ): Promise> { + return withAuth(client, (token) => + httpClient.post( + buildUrl( + client.config.baseUrl, + "api/v1/subscription", + subscriptionId, + "payment", + ), + request, + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, +}); diff --git a/packages/payments/src/types/index.ts b/packages/payments/src/types/index.ts index 7cd3553e..bd9d5216 100644 --- a/packages/payments/src/types/index.ts +++ b/packages/payments/src/types/index.ts @@ -14,3 +14,4 @@ export * from "./plan"; export * from "./buy"; export * from "./result"; export * from "./refund"; +export * from "./subscription"; diff --git a/packages/payments/src/types/plan.ts b/packages/payments/src/types/plan.ts index a3aea98b..02e044dc 100644 --- a/packages/payments/src/types/plan.ts +++ b/packages/payments/src/types/plan.ts @@ -9,11 +9,14 @@ export namespace Plan { description: string; frequency: number; // in days price: number; + overridden_price?: number; start_date: string; // ISO date format (YYYY-MM-DD) end_date?: string; // Optional ISO date format is_auto_renewable: boolean; currency: string; // e.g. "BRL" allow_amount_override: boolean; + campaign_id?: string; + campaign_name?: string; created_by: string; } @@ -28,6 +31,7 @@ export namespace Plan { description: string; frequency: number; // in days price: number; + overridden_price?: number; is_active: boolean; start_time: string; // ISO datetime end_time: string; // ISO datetime @@ -36,6 +40,8 @@ export namespace Plan { updated_by: string; currency: string; // lowercase like "brl" allow_amount_override: boolean; + campaign_id?: string; + campaign_name?: string; created_at: string; // ISO datetime updated_at: string; // ISO datetime deleted_at: string | null; diff --git a/packages/payments/src/types/subscription.ts b/packages/payments/src/types/subscription.ts new file mode 100644 index 00000000..55d2db6c --- /dev/null +++ b/packages/payments/src/types/subscription.ts @@ -0,0 +1,83 @@ +import { ApiResponse } from "./common"; + +export namespace Subscription { + // ---------------------- + // Subscribe request + // ---------------------- + export interface SubscribeRequest { + plan_id: string; + source_customer_id: string; + destination_customer_id: string; + payment_method_id: string; + payment_method_type: "CARD" | "PIX" | "BOLETO"; + payment_method_provider: "PAGAR_ME" | "FACILITA_PAY" | "BRLA"; + fee_bearer: "connected_account"; + } + + // ---------------------- + // Payment request + // ---------------------- + export interface PaymentRequest { + customer_id: string; + payment_method_id: string; + payment_method_type: "CARD" | "PIX" | "BOLETO"; + payment_method_provider: "PAGAR_ME" | "FACILITA_PAY" | "BRLA"; + } + + // ---------------------- + // List query + // ---------------------- + export interface ListQuery { + customer_id: string; + status?: string; + per_page?: number; + page_no?: number; + } + + // ---------------------- + // Data + // ---------------------- + export type Status = + | "active" + | "pending_activation" + | "canceled" + | "expired" + | "queued"; + + export interface Details { + hash_id: string; + start_time: string; + end_time: string; + plan_hash_id: string; + auto_renew: boolean; + status: Status; + created_at: string; + updated_at: string; + } + + export interface SubscribeData { + id: string; + status: string; + sub_status: string; + } + + export interface Pagination { + per_page: number; + page_no: number; + total: number; + } + + export interface ListData { + data: Details[]; + pagination: Pagination; + } + + // ---------------------- + // Responses + // ---------------------- + export type SubscribeResponse = ApiResponse; + export type DetailsResponse = ApiResponse
; + export type ListResponse = ApiResponse; + export type CancelResponse = ApiResponse>; + export type PaymentResponse = ApiResponse; +} diff --git a/packages/payments/src/utils/httpClient.ts b/packages/payments/src/utils/httpClient.ts index 6a2483f1..e407dbac 100644 --- a/packages/payments/src/utils/httpClient.ts +++ b/packages/payments/src/utils/httpClient.ts @@ -168,8 +168,11 @@ export const httpClient = { * @param config - HTTP client configuration * @returns Result containing parsed response or error */ - async get(url: string, config: HttpClientConfig): Promise> { - return request(url, config, { method: "GET" }); + async get(url: string, config: HttpClientConfig & { body?: string }): Promise> { + return request(url, config, { + method: "GET", + ...(config.body ? { body: config.body } : {}), + }); }, /** * @typeParam T - Expected response body type From 965080bc27f74a02a0da4e1dd9136ec541510487 Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 18:10:52 +0600 Subject: [PATCH 03/18] feat: add dispute service for dispute management --- .../integration/disputeService.test.ts | 106 ++++++++++++++++++ .../payments/src/services/disputeService.ts | 74 ++++++++++++ packages/payments/src/services/index.ts | 3 + packages/payments/src/types/dispute.ts | 56 +++++++++ packages/payments/src/types/index.ts | 1 + 5 files changed, 240 insertions(+) create mode 100644 packages/payments/__tests__/integration/disputeService.test.ts create mode 100644 packages/payments/src/services/disputeService.ts create mode 100644 packages/payments/src/types/dispute.ts diff --git a/packages/payments/__tests__/integration/disputeService.test.ts b/packages/payments/__tests__/integration/disputeService.test.ts new file mode 100644 index 00000000..f45c27c6 --- /dev/null +++ b/packages/payments/__tests__/integration/disputeService.test.ts @@ -0,0 +1,106 @@ +import { createOakClient, createDisputeService } from '../../src'; +import { DisputeService } from '../../src/services/disputeService'; +import { getConfigFromEnv } from '../config'; + +const INTEGRATION_TEST_TIMEOUT = 30000; + +describe('DisputeService - Integration', () => { + let disputes: DisputeService; + + beforeAll(() => { + const client = createOakClient({ + ...getConfigFromEnv(), + retryOptions: { + maxNumberOfRetries: 2, + delay: 500, + backoffFactor: 2, + }, + }); + disputes = createDisputeService(client); + }); + + // --------------------------------------------------------------- + // Service shape + // --------------------------------------------------------------- + describe('service interface', () => { + it('should expose list method', () => { + expect(typeof disputes.list).toBe('function'); + }); + + it('should expose updateEvidence method', () => { + expect(typeof disputes.updateEvidence).toBe('function'); + }); + + it('should expose submit method', () => { + expect(typeof disputes.submit).toBe('function'); + }); + + it('should expose close method', () => { + expect(typeof disputes.close).toBe('function'); + }); + }); + + // --------------------------------------------------------------- + // list() + // --------------------------------------------------------------- + describe('list', () => { + it( + 'should return a response when listing disputes', + async () => { + const response = await disputes.list(); + // May return empty list or data - both are valid + expect(response).toBeDefined(); + if (response.ok) { + expect(response.value).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // updateEvidence() - error path + // --------------------------------------------------------------- + describe('updateEvidence', () => { + it( + 'should return an error for a non-existent dispute', + async () => { + const response = await disputes.updateEvidence('non-existent-id', { + text_evidences: [ + { key: 'customer_name', value: 'Test Customer' }, + ], + }); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // submit() - error path + // --------------------------------------------------------------- + describe('submit', () => { + it( + 'should return an error for a non-existent dispute', + async () => { + const response = await disputes.submit('non-existent-id'); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // close() - error path + // --------------------------------------------------------------- + describe('close', () => { + it( + 'should return an error for a non-existent dispute', + async () => { + const response = await disputes.close('non-existent-id'); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); +}); diff --git a/packages/payments/src/services/disputeService.ts b/packages/payments/src/services/disputeService.ts new file mode 100644 index 00000000..18fa73a8 --- /dev/null +++ b/packages/payments/src/services/disputeService.ts @@ -0,0 +1,74 @@ +import type { Dispute, OakClient, Result } from "../types"; +import { httpClient } from "../utils/httpClient"; +import { withAuth } from "../utils/withAuth"; +import { buildUrl } from "../utils/buildUrl"; + +export interface DisputeService { + list(): Promise>; + updateEvidence( + disputeId: string, + evidence: Dispute.EvidenceRequest, + ): Promise>; + submit(disputeId: string): Promise>; + close(disputeId: string): Promise>; +} + +/** + * @param client - Configured OakClient instance + * @returns DisputeService instance + */ +export const createDisputeService = (client: OakClient): DisputeService => ({ + async list(): Promise> { + return withAuth(client, (token) => + httpClient.get( + buildUrl(client.config.baseUrl, "api/v1/disputes"), + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, + + async updateEvidence( + disputeId: string, + evidence: Dispute.EvidenceRequest, + ): Promise> { + return withAuth(client, (token) => + httpClient.put( + buildUrl(client.config.baseUrl, "api/v1/disputes", disputeId, "evidence"), + evidence, + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, + + async submit(disputeId: string): Promise> { + return withAuth(client, (token) => + httpClient.put( + buildUrl(client.config.baseUrl, "api/v1/disputes", disputeId, "submit"), + undefined, + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, + + async close(disputeId: string): Promise> { + return withAuth(client, (token) => + httpClient.put( + buildUrl(client.config.baseUrl, "api/v1/disputes", disputeId, "close"), + undefined, + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, +}); diff --git a/packages/payments/src/services/index.ts b/packages/payments/src/services/index.ts index 8a1c1354..be0b88e6 100644 --- a/packages/payments/src/services/index.ts +++ b/packages/payments/src/services/index.ts @@ -33,3 +33,6 @@ export type { RefundService } from "./refundService"; export { createSubscriptionService } from "./subscriptionService"; export type { SubscriptionService } from "./subscriptionService"; + +export { createDisputeService } from "./disputeService"; +export type { DisputeService } from "./disputeService"; diff --git a/packages/payments/src/types/dispute.ts b/packages/payments/src/types/dispute.ts new file mode 100644 index 00000000..960f0ee2 --- /dev/null +++ b/packages/payments/src/types/dispute.ts @@ -0,0 +1,56 @@ +import { ApiResponse } from "./common"; + +export namespace Dispute { + // ---------------------- + // Evidence request + // ---------------------- + export interface FileEvidence { + id: string; + type: + | "receipt" + | "customer_signature" + | "shipping_documentation" + | "service_documentation" + | "refund_policy" + | "cancellation_policy" + | "uncategorized_file"; + } + + export interface TextEvidence { + key: + | "customer_name" + | "customer_email_address" + | "customer_purchase_ip" + | "product_description" + | "duplicate_charge_id" + | "enhanced_evidence" + | "customer_communication" + | "refund_policy" + | "refund_policy_disclosure" + | "refund_refusal_explanation" + | "service_date" + | "shipping_address" + | "shipping_carrier" + | "shipping_date" + | "shipping_tracking_number" + | "shipping_tracking_url" + | "duplicate_charge_explanation" + | "cancellation_policy" + | "cancellation_rebuttal" + | "uncategorized_text"; + value: string; + } + + export interface EvidenceRequest { + file_evidences?: FileEvidence[]; + text_evidences?: TextEvidence[]; + } + + // ---------------------- + // Responses + // ---------------------- + export type ListResponse = ApiResponse; + export type UpdateResponse = ApiResponse; + export type SubmitResponse = ApiResponse; + export type CloseResponse = ApiResponse; +} diff --git a/packages/payments/src/types/index.ts b/packages/payments/src/types/index.ts index bd9d5216..ad895a5b 100644 --- a/packages/payments/src/types/index.ts +++ b/packages/payments/src/types/index.ts @@ -15,3 +15,4 @@ export * from "./buy"; export * from "./result"; export * from "./refund"; export * from "./subscription"; +export * from "./dispute"; From 21e13dd2798e513fed751ee5c75a317fff74c8b5 Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 18:13:17 +0600 Subject: [PATCH 04/18] feat: add payout service --- .../integration/payoutService.test.ts | 66 +++++++++++++++++++ packages/payments/src/services/index.ts | 3 + .../payments/src/services/payoutService.ts | 27 ++++++++ packages/payments/src/types/index.ts | 1 + packages/payments/src/types/payout.ts | 13 ++++ 5 files changed, 110 insertions(+) create mode 100644 packages/payments/__tests__/integration/payoutService.test.ts create mode 100644 packages/payments/src/services/payoutService.ts create mode 100644 packages/payments/src/types/payout.ts diff --git a/packages/payments/__tests__/integration/payoutService.test.ts b/packages/payments/__tests__/integration/payoutService.test.ts new file mode 100644 index 00000000..2aba01c1 --- /dev/null +++ b/packages/payments/__tests__/integration/payoutService.test.ts @@ -0,0 +1,66 @@ +import { createOakClient, createPayoutService } from '../../src'; +import { PayoutService } from '../../src/services/payoutService'; +import { getConfigFromEnv } from '../config'; + +const INTEGRATION_TEST_TIMEOUT = 30000; + +describe('PayoutService - Integration', () => { + let payouts: PayoutService; + + beforeAll(() => { + const client = createOakClient({ + ...getConfigFromEnv(), + retryOptions: { + maxNumberOfRetries: 2, + delay: 500, + backoffFactor: 2, + }, + }); + payouts = createPayoutService(client); + }); + + // --------------------------------------------------------------- + // Service shape + // --------------------------------------------------------------- + describe('service interface', () => { + it('should expose create method', () => { + expect(typeof payouts.create).toBe('function'); + }); + }); + + // --------------------------------------------------------------- + // create() - error path (invalid customer) + // --------------------------------------------------------------- + describe('create', () => { + it( + 'should return an error for an invalid customer', + async () => { + const response = await payouts.create({ + payment_method_id: 'non-existent-pm', + amount: 1000, + currency: 'USD', + customer_id: 'non-existent-customer', + }); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + + it( + 'should return an error for invalid currency', + async () => { + const response = await payouts.create({ + payment_method_id: 'non-existent-pm', + amount: 1000, + currency: 'USD', + customer_id: 'non-existent-customer', + }); + expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); +}); diff --git a/packages/payments/src/services/index.ts b/packages/payments/src/services/index.ts index be0b88e6..3d8db4b7 100644 --- a/packages/payments/src/services/index.ts +++ b/packages/payments/src/services/index.ts @@ -36,3 +36,6 @@ export type { SubscriptionService } from "./subscriptionService"; export { createDisputeService } from "./disputeService"; export type { DisputeService } from "./disputeService"; + +export { createPayoutService } from "./payoutService"; +export type { PayoutService } from "./payoutService"; diff --git a/packages/payments/src/services/payoutService.ts b/packages/payments/src/services/payoutService.ts new file mode 100644 index 00000000..d6fff2c8 --- /dev/null +++ b/packages/payments/src/services/payoutService.ts @@ -0,0 +1,27 @@ +import type { Payout, OakClient, Result } from "../types"; +import { httpClient } from "../utils/httpClient"; +import { withAuth } from "../utils/withAuth"; +import { buildUrl } from "../utils/buildUrl"; + +export interface PayoutService { + create(request: Payout.Request): Promise>; +} + +/** + * @param client - Configured OakClient instance + * @returns PayoutService instance + */ +export const createPayoutService = (client: OakClient): PayoutService => ({ + async create(request: Payout.Request): Promise> { + return withAuth(client, (token) => + httpClient.post( + buildUrl(client.config.baseUrl, "api/v1/outbound_payments"), + request, + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, +}); diff --git a/packages/payments/src/types/index.ts b/packages/payments/src/types/index.ts index ad895a5b..d3bc1ec5 100644 --- a/packages/payments/src/types/index.ts +++ b/packages/payments/src/types/index.ts @@ -16,3 +16,4 @@ export * from "./result"; export * from "./refund"; export * from "./subscription"; export * from "./dispute"; +export * from "./payout"; diff --git a/packages/payments/src/types/payout.ts b/packages/payments/src/types/payout.ts new file mode 100644 index 00000000..e2f93a21 --- /dev/null +++ b/packages/payments/src/types/payout.ts @@ -0,0 +1,13 @@ +import { ApiResponse } from "./common"; + +export namespace Payout { + export interface Request { + payment_method_id: string; + amount: number; + currency: "BRL" | "USD" | "COP" | "BRLA" | "JPY"; + customer_id: string; + metadata?: Record; + } + + export type Response = ApiResponse; +} From 12a45fb70dff56a24d404bc261991e7f9e86b6e1 Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 18:22:24 +0600 Subject: [PATCH 05/18] fix: align all sdk types with Crowdsplit staging --- .../payments/__tests__/unit/services.test.ts | 2 +- .../payments/src/services/customerService.ts | 2 +- .../src/services/paymentMethodService.ts | 24 +++++++++++++++++++ packages/payments/src/types/buy.ts | 5 +++- packages/payments/src/types/payment.ts | 2 +- packages/payments/src/types/provider.ts | 5 +++- packages/payments/src/types/transactions.ts | 14 ++++++----- 7 files changed, 43 insertions(+), 11 deletions(-) diff --git a/packages/payments/__tests__/unit/services.test.ts b/packages/payments/__tests__/unit/services.test.ts index 9d11e858..9dab03b7 100644 --- a/packages/payments/__tests__/unit/services.test.ts +++ b/packages/payments/__tests__/unit/services.test.ts @@ -204,7 +204,7 @@ describe("Oak services (Unit)", () => { service.balance("cust-1", { provider: "stripe", role: "customer" }), httpMethod: "get", expectedArgs: [ - `${SANDBOX_URL}/api/v1/customers/cust-1/balance?provider=stripe&role=customer`, + `${SANDBOX_URL}/api/v1/customers/cust-1/balances?provider=stripe&role=customer`, authConfig, ], }); diff --git a/packages/payments/src/services/customerService.ts b/packages/payments/src/services/customerService.ts index 8ecaf1c0..0127576f 100644 --- a/packages/payments/src/services/customerService.ts +++ b/packages/payments/src/services/customerService.ts @@ -127,7 +127,7 @@ export const createCustomerService = (client: OakClient): CustomerService => ({ const queryString = buildQueryString(filter); return httpClient.get( - `${client.config.baseUrl}/api/v1/customers/${customer_id}/balance${queryString}`, + `${client.config.baseUrl}/api/v1/customers/${customer_id}/balances${queryString}`, { headers: { Authorization: `Bearer ${token.value}`, diff --git a/packages/payments/src/services/paymentMethodService.ts b/packages/payments/src/services/paymentMethodService.ts index 90cc2566..cca95a7b 100644 --- a/packages/payments/src/services/paymentMethodService.ts +++ b/packages/payments/src/services/paymentMethodService.ts @@ -17,6 +17,11 @@ export interface PaymentMethodService { customerId: string, query?: PaymentMethod.ListQuery, ): Promise>; + update( + customerId: string, + paymentMethodId: string, + paymentMethod: PaymentMethod.Request, + ): Promise>; delete( customerId: string, paymentMethodId: string, @@ -82,6 +87,25 @@ export const createPaymentMethodService = ( ); }, + async update( + customerId: string, + paymentMethodId: string, + paymentMethod: PaymentMethod.Request, + ): Promise> { + return withAuth(client, (token) => + httpClient.put( + buildUrl(client.config.baseUrl, "api/v1/customers", customerId, "payment_methods", paymentMethodId), + paymentMethod, + { + headers: { + Authorization: `Bearer ${token}`, + }, + retryOptions: client.retryOptions, + }, + ), + ); + }, + async delete( customerId: string, paymentMethodId: string, diff --git a/packages/payments/src/types/buy.ts b/packages/payments/src/types/buy.ts index d947870d..8724e6fd 100644 --- a/packages/payments/src/types/buy.ts +++ b/packages/payments/src/types/buy.ts @@ -3,7 +3,7 @@ import { ApiResponse } from "./common"; export namespace Buy { export interface PaymentMethod { type: "customer_wallet"; - chain?: "ethereum" | "polygon" | "arbitrum" | "solana"; + chain?: "ethereum" | "polygon" | "arbitrum" | "solana" | "celo"; evm_address: string; } @@ -48,6 +48,9 @@ export namespace Buy { provider: "bridge"; source: Source; destination: Destination; + provider_data?: { + developer_fee_percent?: number; + }; metadata?: Metadata; } diff --git a/packages/payments/src/types/payment.ts b/packages/payments/src/types/payment.ts index 4b227c4b..3e195740 100644 --- a/packages/payments/src/types/payment.ts +++ b/packages/payments/src/types/payment.ts @@ -22,7 +22,7 @@ export namespace Payment { export interface FraudCheckConfig { threshold?: "low" | "medium" | "high"; sequence?: "fraud_before_auth" | "fraud_after_auth"; - action_on_fail?: "reject" | "review"; + action_on_fail?: "retain_auth" | "cancel_auth"; } export interface FraudCheck { diff --git a/packages/payments/src/types/provider.ts b/packages/payments/src/types/provider.ts index 757c11c7..6d967e1a 100644 --- a/packages/payments/src/types/provider.ts +++ b/packages/payments/src/types/provider.ts @@ -9,7 +9,10 @@ export namespace Provider { | "mercado_pago" | "bridge" | "stripe" - | "pagar_me"; + | "pagar_me" + | "brla" + | "facilita_pay" + | "inter_bank"; export type TargetRole = "subaccount" | "customer" | "connected_account"; diff --git a/packages/payments/src/types/transactions.ts b/packages/payments/src/types/transactions.ts index 65547d59..d01e0254 100644 --- a/packages/payments/src/types/transactions.ts +++ b/packages/payments/src/types/transactions.ts @@ -22,12 +22,14 @@ export namespace Transaction { // Status // ---------------------- export type Status = - | "INITIATED" - | "PENDING" - | "COMPLETED" - | "SETTLED" - | "FAILED" - | "CANCELED_AFTER_COMPLETION"; + | "created" + | "awaiting_confirmation" + | "processing" + | "captured" + | "succeeded" + | "failed" + | "canceled_after_completion" + | "canceled"; // ---------------------- // Model From 04ac7961d9ea44b6a058aa8b1d4919a4ba43f7c8 Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 18:27:12 +0600 Subject: [PATCH 06/18] feat: tighten transfer types for chain and payment method --- packages/payments/src/types/transfer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/payments/src/types/transfer.ts b/packages/payments/src/types/transfer.ts index a49e8de8..978e92da 100644 --- a/packages/payments/src/types/transfer.ts +++ b/packages/payments/src/types/transfer.ts @@ -16,8 +16,8 @@ export namespace Transfer { }; payment_method?: { id?: string; // if present, chain and evm_address are forbidden - type: string; // from TRANSFER_PAYMENT_METHOD_TYPE keys - chain?: string; // from WALLET_CHAIN values, required when id is absent + type: "customer_wallet" | "bank"; + chain?: "polygon" | "celo"; // required when id is absent evm_address?: string; // required when id is absent, validated as checksummed address }; }; From 69aacd7914983f3dfd1abd7ec5f85ef252b5974f Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 18:30:13 +0600 Subject: [PATCH 07/18] chore: add changelog.md --- .changeset/many-parrots-give.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/many-parrots-give.md diff --git a/.changeset/many-parrots-give.md b/.changeset/many-parrots-give.md new file mode 100644 index 00000000..6851d33d --- /dev/null +++ b/.changeset/many-parrots-give.md @@ -0,0 +1,5 @@ +--- +'@oaknetwork/payments-sdk': minor +--- + +Update payment sdk with the recent changes from Crowdsplit From 4f9f62fb3214460113f17403b00a284c3eeb8676 Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 18:37:54 +0600 Subject: [PATCH 08/18] feat: add customer files, kyc platforms and other missing services --- .../integration/missingServices.test.ts | 167 ++++++++++++++++++ .../payments/src/services/customerService.ts | 56 ++++++ packages/payments/src/services/fileService.ts | 66 +++++++ packages/payments/src/services/index.ts | 9 + .../payments/src/services/merchantService.ts | 33 ++++ .../payments/src/services/walletService.ts | 28 +++ packages/payments/src/types/customer.ts | 8 + packages/payments/src/types/file.ts | 8 + packages/payments/src/types/index.ts | 3 + packages/payments/src/types/merchant.ts | 14 ++ packages/payments/src/types/wallet.ts | 5 + 11 files changed, 397 insertions(+) create mode 100644 packages/payments/__tests__/integration/missingServices.test.ts create mode 100644 packages/payments/src/services/fileService.ts create mode 100644 packages/payments/src/services/merchantService.ts create mode 100644 packages/payments/src/services/walletService.ts create mode 100644 packages/payments/src/types/file.ts create mode 100644 packages/payments/src/types/merchant.ts create mode 100644 packages/payments/src/types/wallet.ts diff --git a/packages/payments/__tests__/integration/missingServices.test.ts b/packages/payments/__tests__/integration/missingServices.test.ts new file mode 100644 index 00000000..a12628ea --- /dev/null +++ b/packages/payments/__tests__/integration/missingServices.test.ts @@ -0,0 +1,167 @@ +import { + createOakClient, + createCustomerService, + createWalletService, + createMerchantService, + createFileService, +} from '../../src'; +import { CustomerService } from '../../src/services/customerService'; +import { WalletService } from '../../src/services/walletService'; +import { MerchantService } from '../../src/services/merchantService'; +import { FileService } from '../../src/services/fileService'; +import { getConfigFromEnv } from '../config'; + +const INTEGRATION_TEST_TIMEOUT = 30000; + +describe('Missing Services - Integration', () => { + let customers: CustomerService; + let wallets: WalletService; + let merchant: MerchantService; + let files: FileService; + let existingCustomerId: string | undefined; + + beforeAll(async () => { + const client = createOakClient({ + ...getConfigFromEnv(), + retryOptions: { + maxNumberOfRetries: 2, + delay: 500, + backoffFactor: 2, + }, + }); + customers = createCustomerService(client); + wallets = createWalletService(client); + merchant = createMerchantService(client); + files = createFileService(client); + + const listRes = await customers.list({ limit: 1 }); + if (listRes.ok && listRes.value.data.customer_list.length > 0) { + const first = listRes.value.data.customer_list[0]; + existingCustomerId = (first.id ?? first.customer_id) as string; + } + }, INTEGRATION_TEST_TIMEOUT); + + // --------------------------------------------------------------- + // CustomerService - file methods + // --------------------------------------------------------------- + describe('CustomerService - files', () => { + it('should expose uploadFiles method', () => { + expect(typeof customers.uploadFiles).toBe('function'); + }); + + it('should expose getFiles method', () => { + expect(typeof customers.getFiles).toBe('function'); + }); + + it( + 'should get files for a customer', + async () => { + if (!existingCustomerId) { + throw new Error('No customer available'); + } + const response = await customers.getFiles(existingCustomerId); + expect(response).toBeDefined(); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // CustomerService - populatePlatform (KYC) + // --------------------------------------------------------------- + describe('CustomerService - populatePlatform', () => { + it('should expose populatePlatform method', () => { + expect(typeof customers.populatePlatform).toBe('function'); + }); + + it( + 'should return an error for an invalid customer', + async () => { + const response = await customers.populatePlatform('non-existent-id', { + provider: 'stripe', + }); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // WalletService + // --------------------------------------------------------------- + describe('WalletService', () => { + it('should expose getBalance method', () => { + expect(typeof wallets.getBalance).toBe('function'); + }); + + it( + 'should return an error for a non-existent customer', + async () => { + const response = await wallets.getBalance('non-existent-id'); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // MerchantService + // --------------------------------------------------------------- + describe('MerchantService', () => { + it('should expose calculateTransferDate method', () => { + expect(typeof merchant.calculateTransferDate).toBe('function'); + }); + + it( + 'should calculate a transfer date', + async () => { + const response = await merchant.calculateTransferDate({ + settlementDate: '2026-05-01', + region: 'US', + }); + // May succeed or fail depending on backend config + expect(response).toBeDefined(); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // FileService + // --------------------------------------------------------------- + describe('FileService', () => { + it('should expose upload method', () => { + expect(typeof files.upload).toBe('function'); + }); + + it('should expose list method', () => { + expect(typeof files.list).toBe('function'); + }); + + it('should expose get method', () => { + expect(typeof files.get).toBe('function'); + }); + + it('should expose delete method', () => { + expect(typeof files.delete).toBe('function'); + }); + + it( + 'should list files', + async () => { + const response = await files.list(); + expect(response).toBeDefined(); + }, + INTEGRATION_TEST_TIMEOUT, + ); + + it( + 'should return an error for a non-existent file', + async () => { + const response = await files.get('non-existent-id'); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); +}); diff --git a/packages/payments/src/services/customerService.ts b/packages/payments/src/services/customerService.ts index 0127576f..e89f1bd8 100644 --- a/packages/payments/src/services/customerService.ts +++ b/packages/payments/src/services/customerService.ts @@ -22,6 +22,18 @@ export interface CustomerService { customer_id: string, filter: Customer.BalanceFilter, ): Promise>; + + uploadFiles( + customerId: string, + files: unknown, + ): Promise>; + + getFiles(customerId: string): Promise>; + + populatePlatform( + customerId: string, + data: Customer.PlatformRequest, + ): Promise>; } /** @@ -115,6 +127,50 @@ export const createCustomerService = (client: OakClient): CustomerService => ({ ); }, + async uploadFiles( + customerId: string, + files: unknown, + ): Promise> { + return withAuth(client, (token) => + httpClient.post( + buildUrl(client.config.baseUrl, "api/v1/customers", customerId, "files"), + files, + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, + + async getFiles(customerId: string): Promise> { + return withAuth(client, (token) => + httpClient.get( + buildUrl(client.config.baseUrl, "api/v1/customers", customerId, "files"), + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, + + async populatePlatform( + customerId: string, + data: Customer.PlatformRequest, + ): Promise> { + return withAuth(client, (token) => + httpClient.post( + buildUrl(client.config.baseUrl, "api/v1/customers", customerId, "platforms"), + data, + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, + async balance( customer_id: string, filter: Customer.BalanceFilter, diff --git a/packages/payments/src/services/fileService.ts b/packages/payments/src/services/fileService.ts new file mode 100644 index 00000000..9528178c --- /dev/null +++ b/packages/payments/src/services/fileService.ts @@ -0,0 +1,66 @@ +import type { File, OakClient, Result } from "../types"; +import { httpClient } from "../utils/httpClient"; +import { withAuth } from "../utils/withAuth"; +import { buildUrl } from "../utils/buildUrl"; + +export interface FileService { + upload(files: unknown): Promise>; + list(): Promise>; + get(fileId: string): Promise>; + delete(fileId: string): Promise>; +} + +/** + * @param client - Configured OakClient instance + * @returns FileService instance + */ +export const createFileService = (client: OakClient): FileService => ({ + async upload(files: unknown): Promise> { + return withAuth(client, (token) => + httpClient.post( + buildUrl(client.config.baseUrl, "api/v1/files"), + files, + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, + + async list(): Promise> { + return withAuth(client, (token) => + httpClient.get( + buildUrl(client.config.baseUrl, "api/v1/files"), + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, + + async get(fileId: string): Promise> { + return withAuth(client, (token) => + httpClient.get( + buildUrl(client.config.baseUrl, "api/v1/files", fileId), + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, + + async delete(fileId: string): Promise> { + return withAuth(client, (token) => + httpClient.delete( + buildUrl(client.config.baseUrl, "api/v1/files", fileId), + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, +}); diff --git a/packages/payments/src/services/index.ts b/packages/payments/src/services/index.ts index 3d8db4b7..163c11ea 100644 --- a/packages/payments/src/services/index.ts +++ b/packages/payments/src/services/index.ts @@ -39,3 +39,12 @@ export type { DisputeService } from "./disputeService"; export { createPayoutService } from "./payoutService"; export type { PayoutService } from "./payoutService"; + +export { createWalletService } from "./walletService"; +export type { WalletService } from "./walletService"; + +export { createMerchantService } from "./merchantService"; +export type { MerchantService } from "./merchantService"; + +export { createFileService } from "./fileService"; +export type { FileService } from "./fileService"; diff --git a/packages/payments/src/services/merchantService.ts b/packages/payments/src/services/merchantService.ts new file mode 100644 index 00000000..5a0b04dd --- /dev/null +++ b/packages/payments/src/services/merchantService.ts @@ -0,0 +1,33 @@ +import type { Merchant, OakClient, Result } from "../types"; +import { httpClient } from "../utils/httpClient"; +import { withAuth } from "../utils/withAuth"; +import { buildUrl } from "../utils/buildUrl"; + +export interface MerchantService { + calculateTransferDate( + request: Merchant.TransferDateRequest, + ): Promise>; +} + +/** + * @param client - Configured OakClient instance + * @returns MerchantService instance + */ +export const createMerchantService = ( + client: OakClient, +): MerchantService => ({ + async calculateTransferDate( + request: Merchant.TransferDateRequest, + ): Promise> { + return withAuth(client, (token) => + httpClient.post( + buildUrl(client.config.baseUrl, "api/v1/merchant/util/transfer-date"), + request, + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, +}); diff --git a/packages/payments/src/services/walletService.ts b/packages/payments/src/services/walletService.ts new file mode 100644 index 00000000..7226de99 --- /dev/null +++ b/packages/payments/src/services/walletService.ts @@ -0,0 +1,28 @@ +import type { Wallet, OakClient, Result } from "../types"; +import { httpClient } from "../utils/httpClient"; +import { withAuth } from "../utils/withAuth"; +import { buildUrl } from "../utils/buildUrl"; + +export interface WalletService { + getBalance(customerId: string): Promise>; +} + +/** + * @param client - Configured OakClient instance + * @returns WalletService instance + */ +export const createWalletService = (client: OakClient): WalletService => ({ + async getBalance( + customerId: string, + ): Promise> { + return withAuth(client, (token) => + httpClient.get( + buildUrl(client.config.baseUrl, "api/v1/wallets", customerId, "balance"), + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, +}); diff --git a/packages/payments/src/types/customer.ts b/packages/payments/src/types/customer.ts index 6cdb73bf..173af7ef 100644 --- a/packages/payments/src/types/customer.ts +++ b/packages/payments/src/types/customer.ts @@ -89,6 +89,14 @@ export namespace Customer { document_type?: string; country_code?: string; } + export type FilesResponse = ApiResponse; + export type PlatformResponse = ApiResponse; + + export interface PlatformRequest { + provider: string; + [key: string]: unknown; + } + export interface BalanceFilter { provider: string; role: string; diff --git a/packages/payments/src/types/file.ts b/packages/payments/src/types/file.ts new file mode 100644 index 00000000..35a5a153 --- /dev/null +++ b/packages/payments/src/types/file.ts @@ -0,0 +1,8 @@ +import { ApiResponse } from "./common"; + +export namespace File { + export type UploadResponse = ApiResponse; + export type ListResponse = ApiResponse; + export type GetResponse = ApiResponse; + export type DeleteResponse = ApiResponse; +} diff --git a/packages/payments/src/types/index.ts b/packages/payments/src/types/index.ts index d3bc1ec5..3cce82f7 100644 --- a/packages/payments/src/types/index.ts +++ b/packages/payments/src/types/index.ts @@ -17,3 +17,6 @@ export * from "./refund"; export * from "./subscription"; export * from "./dispute"; export * from "./payout"; +export * from "./wallet"; +export * from "./merchant"; +export * from "./file"; diff --git a/packages/payments/src/types/merchant.ts b/packages/payments/src/types/merchant.ts new file mode 100644 index 00000000..0a991c22 --- /dev/null +++ b/packages/payments/src/types/merchant.ts @@ -0,0 +1,14 @@ +import { ApiResponse } from "./common"; + +export namespace Merchant { + export interface TransferDateRequest { + settlementDate: string; + region: string; + config?: { + weekends?: number[]; + holidays?: string[]; + }; + } + + export type TransferDateResponse = ApiResponse; +} diff --git a/packages/payments/src/types/wallet.ts b/packages/payments/src/types/wallet.ts new file mode 100644 index 00000000..33c660fb --- /dev/null +++ b/packages/payments/src/types/wallet.ts @@ -0,0 +1,5 @@ +import { ApiResponse } from "./common"; + +export namespace Wallet { + export type BalanceResponse = ApiResponse; +} From 7f690c4aa76b899415052f66f8094da018c61aab Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 18:44:28 +0600 Subject: [PATCH 09/18] feat: close all remaining gaps with Crowdsplit staging --- .../integration/remainingGaps.test.ts | 141 ++++++++++++++++++ packages/payments/src/services/index.ts | 12 ++ packages/payments/src/services/pixService.ts | 29 ++++ .../src/services/providerProxyService.ts | 35 +++++ .../payments/src/services/sandboxService.ts | 38 +++++ packages/payments/src/services/taxService.ts | 29 ++++ .../payments/src/services/transferService.ts | 17 +++ packages/payments/src/types/index.ts | 4 + packages/payments/src/types/pix.ts | 11 ++ packages/payments/src/types/providerProxy.ts | 12 ++ packages/payments/src/types/sandbox.ts | 15 ++ packages/payments/src/types/tax.ts | 10 ++ packages/payments/src/types/webhook.ts | 62 +++++++- 13 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 packages/payments/__tests__/integration/remainingGaps.test.ts create mode 100644 packages/payments/src/services/pixService.ts create mode 100644 packages/payments/src/services/providerProxyService.ts create mode 100644 packages/payments/src/services/sandboxService.ts create mode 100644 packages/payments/src/services/taxService.ts create mode 100644 packages/payments/src/types/pix.ts create mode 100644 packages/payments/src/types/providerProxy.ts create mode 100644 packages/payments/src/types/sandbox.ts create mode 100644 packages/payments/src/types/tax.ts diff --git a/packages/payments/__tests__/integration/remainingGaps.test.ts b/packages/payments/__tests__/integration/remainingGaps.test.ts new file mode 100644 index 00000000..375374a5 --- /dev/null +++ b/packages/payments/__tests__/integration/remainingGaps.test.ts @@ -0,0 +1,141 @@ +import { + createOakClient, + createTaxService, + createProviderProxyService, + createPixService, + createSandboxService, + createTransferService, +} from '../../src'; +import { TaxService } from '../../src/services/taxService'; +import { ProviderProxyService } from '../../src/services/providerProxyService'; +import { PixService } from '../../src/services/pixService'; +import { SandboxService } from '../../src/services/sandboxService'; +import { TransferService } from '../../src/services/transferService'; +import { getConfigFromEnv } from '../config'; + +const INTEGRATION_TEST_TIMEOUT = 30000; + +describe('Remaining Gaps - Integration', () => { + let taxes: TaxService; + let providerProxy: ProviderProxyService; + let pix: PixService; + let sandbox: SandboxService; + let transfers: TransferService; + + beforeAll(() => { + const client = createOakClient({ + ...getConfigFromEnv(), + retryOptions: { + maxNumberOfRetries: 2, + delay: 500, + backoffFactor: 2, + }, + }); + taxes = createTaxService(client); + providerProxy = createProviderProxyService(client); + pix = createPixService(client); + sandbox = createSandboxService(client); + transfers = createTransferService(client); + }); + + // --------------------------------------------------------------- + // Gap 6: TaxService + // --------------------------------------------------------------- + describe('TaxService', () => { + it('should expose calculate method', () => { + expect(typeof taxes.calculate).toBe('function'); + }); + + it( + 'should call the tax calculation endpoint', + async () => { + const response = await taxes.calculate({ + provider: 'stripe', + }); + expect(response).toBeDefined(); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // Gap 7: ProviderProxyService + // --------------------------------------------------------------- + describe('ProviderProxyService', () => { + it('should expose proxy method', () => { + expect(typeof providerProxy.proxy).toBe('function'); + }); + + it( + 'should return an error for an invalid proxy request', + async () => { + const response = await providerProxy.proxy('stripe', { + method: 'GET', + url: 'https://invalid-url.example.com', + }); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // Gap 8: PixService + // --------------------------------------------------------------- + describe('PixService', () => { + it('should expose createPaid method', () => { + expect(typeof pix.createPaid).toBe('function'); + }); + + it( + 'should call the pix paid endpoint', + async () => { + const response = await pix.createPaid({ + pix_string: 'test-pix-string', + pix_string_type: 'BR_CODE', + amount: 100, + }); + expect(response).toBeDefined(); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // Gap 9: SandboxService + // --------------------------------------------------------------- + describe('SandboxService', () => { + it('should expose simulateWebhook method', () => { + expect(typeof sandbox.simulateWebhook).toBe('function'); + }); + + it( + 'should return an error for an invalid simulation', + async () => { + const response = await sandbox.simulateWebhook('stripe', { + category: 'invalid_category', + }); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // Gap 10: TransferService.sendWebhook + // --------------------------------------------------------------- + describe('TransferService - sendWebhook', () => { + it('should expose sendWebhook method', () => { + expect(typeof transfers.sendWebhook).toBe('function'); + }); + + it( + 'should return a response when sending a transfer webhook', + async () => { + const response = await transfers.sendWebhook({}); + expect(response).toBeDefined(); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); +}); diff --git a/packages/payments/src/services/index.ts b/packages/payments/src/services/index.ts index 163c11ea..6788c6fa 100644 --- a/packages/payments/src/services/index.ts +++ b/packages/payments/src/services/index.ts @@ -48,3 +48,15 @@ export type { MerchantService } from "./merchantService"; export { createFileService } from "./fileService"; export type { FileService } from "./fileService"; + +export { createTaxService } from "./taxService"; +export type { TaxService } from "./taxService"; + +export { createProviderProxyService } from "./providerProxyService"; +export type { ProviderProxyService } from "./providerProxyService"; + +export { createPixService } from "./pixService"; +export type { PixService } from "./pixService"; + +export { createSandboxService } from "./sandboxService"; +export type { SandboxService } from "./sandboxService"; diff --git a/packages/payments/src/services/pixService.ts b/packages/payments/src/services/pixService.ts new file mode 100644 index 00000000..f723d701 --- /dev/null +++ b/packages/payments/src/services/pixService.ts @@ -0,0 +1,29 @@ +import type { Pix, OakClient, Result } from "../types"; +import { httpClient } from "../utils/httpClient"; +import { withAuth } from "../utils/withAuth"; +import { buildUrl } from "../utils/buildUrl"; + +export interface PixService { + createPaid(request: Pix.PaidRequest): Promise>; +} + +/** + * @param client - Configured OakClient instance + * @returns PixService instance + */ +export const createPixService = (client: OakClient): PixService => ({ + async createPaid( + request: Pix.PaidRequest, + ): Promise> { + return withAuth(client, (token) => + httpClient.post( + buildUrl(client.config.baseUrl, "api/v1/pix/paid"), + request, + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, +}); diff --git a/packages/payments/src/services/providerProxyService.ts b/packages/payments/src/services/providerProxyService.ts new file mode 100644 index 00000000..567066b3 --- /dev/null +++ b/packages/payments/src/services/providerProxyService.ts @@ -0,0 +1,35 @@ +import type { ProviderProxy, OakClient, Result } from "../types"; +import { httpClient } from "../utils/httpClient"; +import { withAuth } from "../utils/withAuth"; +import { buildUrl } from "../utils/buildUrl"; + +export interface ProviderProxyService { + proxy( + provider: string, + request: ProviderProxy.Request, + ): Promise>; +} + +/** + * @param client - Configured OakClient instance + * @returns ProviderProxyService instance + */ +export const createProviderProxyService = ( + client: OakClient, +): ProviderProxyService => ({ + async proxy( + provider: string, + request: ProviderProxy.Request, + ): Promise> { + return withAuth(client, (token) => + httpClient.post( + buildUrl(client.config.baseUrl, "api/v1/providers", provider, "proxy"), + request, + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, +}); diff --git a/packages/payments/src/services/sandboxService.ts b/packages/payments/src/services/sandboxService.ts new file mode 100644 index 00000000..b45689e1 --- /dev/null +++ b/packages/payments/src/services/sandboxService.ts @@ -0,0 +1,38 @@ +import type { Sandbox, OakClient, Result } from "../types"; +import { httpClient } from "../utils/httpClient"; +import { withAuth } from "../utils/withAuth"; +import { buildUrl } from "../utils/buildUrl"; + +export interface SandboxService { + simulateWebhook( + provider: string, + request: Sandbox.WebhookSimulationRequest, + ): Promise>; +} + +/** + * @param client - Configured OakClient instance + * @returns SandboxService instance + */ +export const createSandboxService = (client: OakClient): SandboxService => ({ + async simulateWebhook( + provider: string, + request: Sandbox.WebhookSimulationRequest, + ): Promise> { + return withAuth(client, (token) => + httpClient.post( + buildUrl( + client.config.baseUrl, + "api/v1/sandbox/webhooks", + provider, + "simulate", + ), + request, + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, +}); diff --git a/packages/payments/src/services/taxService.ts b/packages/payments/src/services/taxService.ts new file mode 100644 index 00000000..ee0f0c61 --- /dev/null +++ b/packages/payments/src/services/taxService.ts @@ -0,0 +1,29 @@ +import type { Tax, OakClient, Result } from "../types"; +import { httpClient } from "../utils/httpClient"; +import { withAuth } from "../utils/withAuth"; +import { buildUrl } from "../utils/buildUrl"; + +export interface TaxService { + calculate(request: Tax.CalculateRequest): Promise>; +} + +/** + * @param client - Configured OakClient instance + * @returns TaxService instance + */ +export const createTaxService = (client: OakClient): TaxService => ({ + async calculate( + request: Tax.CalculateRequest, + ): Promise> { + return withAuth(client, (token) => + httpClient.post( + buildUrl(client.config.baseUrl, "api/v1/taxes/calculate"), + request, + { + headers: { Authorization: `Bearer ${token}` }, + retryOptions: client.retryOptions, + }, + ), + ); + }, +}); diff --git a/packages/payments/src/services/transferService.ts b/packages/payments/src/services/transferService.ts index 99d8c585..e3a61c28 100644 --- a/packages/payments/src/services/transferService.ts +++ b/packages/payments/src/services/transferService.ts @@ -1,10 +1,12 @@ import type { Transfer, OakClient, Result } from "../types"; +import { ApiResponse } from "../types"; import { httpClient } from "../utils/httpClient"; import { withAuth } from "../utils/withAuth"; import { buildUrl } from "../utils/buildUrl"; export interface TransferService { create(transfer: Transfer.Request): Promise>; + sendWebhook(data: unknown): Promise>>; } /** @@ -26,4 +28,19 @@ export const createTransferService = (client: OakClient): TransferService => ({ ), ); }, + + async sendWebhook(data: unknown): Promise>> { + return withAuth(client, (token) => + httpClient.post>( + buildUrl(client.config.baseUrl, "api/v1/transfer/webhook"), + data, + { + headers: { + Authorization: `Bearer ${token}`, + }, + retryOptions: client.retryOptions, + }, + ), + ); + }, }); diff --git a/packages/payments/src/types/index.ts b/packages/payments/src/types/index.ts index 3cce82f7..8a67828a 100644 --- a/packages/payments/src/types/index.ts +++ b/packages/payments/src/types/index.ts @@ -20,3 +20,7 @@ export * from "./payout"; export * from "./wallet"; export * from "./merchant"; export * from "./file"; +export * from "./tax"; +export * from "./providerProxy"; +export * from "./pix"; +export * from "./sandbox"; diff --git a/packages/payments/src/types/pix.ts b/packages/payments/src/types/pix.ts new file mode 100644 index 00000000..36245343 --- /dev/null +++ b/packages/payments/src/types/pix.ts @@ -0,0 +1,11 @@ +import { ApiResponse } from "./common"; + +export namespace Pix { + export interface PaidRequest { + pix_string: string; + pix_string_type: "BR_CODE" | "PIX_KEY"; + amount: number; + } + + export type PaidResponse = ApiResponse; +} diff --git a/packages/payments/src/types/providerProxy.ts b/packages/payments/src/types/providerProxy.ts new file mode 100644 index 00000000..fbbd3b3d --- /dev/null +++ b/packages/payments/src/types/providerProxy.ts @@ -0,0 +1,12 @@ +import { ApiResponse } from "./common"; + +export namespace ProviderProxy { + export interface Request { + method: string; + url: string; + body?: unknown; + headers?: Record; + } + + export type Response = ApiResponse; +} diff --git a/packages/payments/src/types/sandbox.ts b/packages/payments/src/types/sandbox.ts new file mode 100644 index 00000000..26db6829 --- /dev/null +++ b/packages/payments/src/types/sandbox.ts @@ -0,0 +1,15 @@ +import { ApiResponse } from "./common"; + +export namespace Sandbox { + export interface WebhookSimulationRequest { + category: string; + provider?: string; + provider_data?: { + status?: string; + [key: string]: unknown; + }; + [key: string]: unknown; + } + + export type WebhookSimulationResponse = ApiResponse; +} diff --git a/packages/payments/src/types/tax.ts b/packages/payments/src/types/tax.ts new file mode 100644 index 00000000..5b26d884 --- /dev/null +++ b/packages/payments/src/types/tax.ts @@ -0,0 +1,10 @@ +import { ApiResponse } from "./common"; + +export namespace Tax { + export interface CalculateRequest { + provider: string; + [key: string]: unknown; + } + + export type CalculateResponse = ApiResponse; +} diff --git a/packages/payments/src/types/webhook.ts b/packages/payments/src/types/webhook.ts index 4ad44c49..c8cfb0f1 100644 --- a/packages/payments/src/types/webhook.ts +++ b/packages/payments/src/types/webhook.ts @@ -30,10 +30,70 @@ export namespace Webhook { // ---------------------- // Notifications // ---------------------- + export type EventType = + | "payment.processing" + | "payment.succeeded" + | "payment.captured" + | "payment.failed" + | "payment.awaiting_confirmation" + | "payment.cancelled" + | "payment.refunded" + | "payment.updated" + | "refund.created" + | "refund.updated" + | "refund.failed" + | "refund.succeeded" + | "customer.verified" + | "customer.processing" + | "customer.action_required" + | "customer.rejected" + | "customer.created" + | "customer.updated" + | "customer.sync" + | "provider_registration.submitted" + | "provider_registration.approved" + | "provider_registration.restricted" + | "provider_registration.rejected" + | "provider_registration.awaiting_confirmation" + | "provider_registration.processing" + | "provider_registration.action_required" + | "provider_registration.documents_uploaded" + | "provider_registration.verification_expired" + | "payment_method.verified" + | "payment_method.approved" + | "payment_method.rejected" + | "payment_method.created" + | "payment_method.updated" + | "transaction.updated" + | "transaction.sc_updated" + | "dispute.created" + | "dispute.updated" + | "dispute.closed" + | "dispute.funds_reinstated" + | "dispute.funds_withdrawn" + | "payout.succeeded" + | "kyc.approved" + | "kyc.rejected" + | "kyc.action_required" + | "external_deposit.received" + | "external_deposit.pending" + | "external_deposit.completed" + | "external_deposit.succeeded" + | "external_deposit.virtual_account_funded" + | "transfer.succeeded" + | "transfer.failed" + | "sell.succeeded" + | "sell.failed" + | "buy.succeeded" + | "buy.completed" + | "buy.awaiting_confirmation" + | "installment_payment.succeeded" + | "installment_payment.failed"; + export interface Notification { id: string; is_acknowledged: boolean; - event: string | null; + event: EventType | string | null; category: string | null; data: any; } From 7ebedf335c485f7d0f98b9188b96904e99543824 Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 18:53:11 +0600 Subject: [PATCH 10/18] fix: address all field level and enum audit findings --- packages/payments/src/types/payment.ts | 4 +- packages/payments/src/types/paymentMethod.ts | 41 ++++++--- packages/payments/src/types/provider.ts | 7 +- packages/payments/src/types/subscription.ts | 2 +- packages/payments/src/types/transactions.ts | 58 ++++++++++++- packages/payments/src/types/webhook.ts | 90 ++++++++++++++------ 6 files changed, 159 insertions(+), 43 deletions(-) diff --git a/packages/payments/src/types/payment.ts b/packages/payments/src/types/payment.ts index 3e195740..32ff4d1b 100644 --- a/packages/payments/src/types/payment.ts +++ b/packages/payments/src/types/payment.ts @@ -22,7 +22,7 @@ export namespace Payment { export interface FraudCheckConfig { threshold?: "low" | "medium" | "high"; sequence?: "fraud_before_auth" | "fraud_after_auth"; - action_on_fail?: "retain_auth" | "cancel_auth"; + action_on_fail?: "RETAIN_AUTH" | "CANCEL_AUTH"; } export interface FraudCheck { @@ -163,7 +163,7 @@ export namespace Payment { metadata?: Record; id: string; status: string; - type: "payment"; + type: string; created_at: string; updated_at: string; provider_response?: ProviderResponse; diff --git a/packages/payments/src/types/paymentMethod.ts b/packages/payments/src/types/paymentMethod.ts index 82041073..5b410dd5 100644 --- a/packages/payments/src/types/paymentMethod.ts +++ b/packages/payments/src/types/paymentMethod.ts @@ -1,8 +1,27 @@ import { ApiResponse } from "./common"; export namespace PaymentMethod { + export type MethodType = + | "BANK" + | "CARD" + | "PLAID" + | "VIRTUAL_ACCOUNT" + | "LIQUIDATION_ADDRESS" + | "TRADING_WALLET" + | "CUSTOMER_WALLET" + | "PIX" + | "EVM_ADDRESS"; + + export type PaymentMethodKind = + | "PIX" + | "BOLETO" + | "CARD" + | "BANK_TRANSFER" + | "BANK" + | "CASH_PAYMENT"; + export interface BridgeBankAccount { - type: string; // from SUBJECT_PAYMENT_METHOD_TYPE keys + type: MethodType; provider?: string; // from PLATFORMS keys currency?: string; // from CURRENCY keys (lowercase) bank_name: string; @@ -22,7 +41,7 @@ export namespace PaymentMethod { } export interface OakBankAccount { - type: string; // from SUBJECT_PAYMENT_METHOD_TYPE keys + type: MethodType; provider?: string; // from PLATFORMS keys bank_branch_code: string; bank_account_number: string; // pattern: digits only @@ -34,7 +53,7 @@ export namespace PaymentMethod { } export interface StripeBankAccount { - type: string; // from SUBJECT_PAYMENT_METHOD_TYPE keys + type: MethodType; provider?: string; // from PLATFORMS keys currency?: string; // from CURRENCY keys bank_name: string; @@ -47,7 +66,7 @@ export namespace PaymentMethod { } export interface MercadoPagoCard { - type: string; // from SUBJECT_PAYMENT_METHOD_TYPE keys + type: MethodType; provider: string; // from PLATFORMS keys card_details: { card_token: string; @@ -56,7 +75,7 @@ export namespace PaymentMethod { } export interface PagarMeCard { - type: string; // from SUBJECT_PAYMENT_METHOD_TYPE keys + type: MethodType; provider: string; // from PLATFORMS keys card_token: string; billing_address: { @@ -72,13 +91,13 @@ export namespace PaymentMethod { } export interface StripeCard { - type: string; // from SUBJECT_PAYMENT_METHOD_TYPE keys + type: MethodType; provider: string; // from PLATFORMS keys metadata?: Record; } export interface OakCustomerWallet { - type: string; // from SUBJECT_PAYMENT_METHOD_TYPE keys + type: MethodType; provider?: string; // from PLATFORMS keys evm_address: string; // validated as checksummed Ethereum address chain: string; // from WALLET_CHAIN keys @@ -87,7 +106,7 @@ export namespace PaymentMethod { } export interface BridgeLiquidationAddress { - type: string; // from SUBJECT_PAYMENT_METHOD_TYPE keys + type: MethodType; provider: string; // from PLATFORMS keys source_currency: string; // from ASSET_TYPE keys destination_currency: string; // from CURRENCY keys @@ -101,20 +120,20 @@ export namespace PaymentMethod { } export interface OakPix { - type: string; // from SUBJECT_PAYMENT_METHOD_TYPE keys + type: MethodType; provider?: string; // from PLATFORMS keys pix_string: string; metadata?: Record; } export interface BridgePlaid { - type: string; // from SUBJECT_PAYMENT_METHOD_TYPE keys + type: MethodType; provider: string; // from PLATFORMS keys metadata?: Record; } export interface BridgeVirtualAccount { - type: string; // from SUBJECT_PAYMENT_METHOD_TYPE keys + type: MethodType; provider: string; // from PLATFORMS keys source_currency: string; // from CURRENCY keys destination_currency: string; // from ASSET_TYPE keys diff --git a/packages/payments/src/types/provider.ts b/packages/payments/src/types/provider.ts index 6d967e1a..8f8fed81 100644 --- a/packages/payments/src/types/provider.ts +++ b/packages/payments/src/types/provider.ts @@ -12,7 +12,12 @@ export namespace Provider { | "pagar_me" | "brla" | "facilita_pay" - | "inter_bank"; + | "inter_bank" + | "wallet_service" + | "crowd_split" + | "konduto" + | "cel_coin" + | "cel_baas"; export type TargetRole = "subaccount" | "customer" | "connected_account"; diff --git a/packages/payments/src/types/subscription.ts b/packages/payments/src/types/subscription.ts index 55d2db6c..30c2973e 100644 --- a/packages/payments/src/types/subscription.ts +++ b/packages/payments/src/types/subscription.ts @@ -11,7 +11,7 @@ export namespace Subscription { payment_method_id: string; payment_method_type: "CARD" | "PIX" | "BOLETO"; payment_method_provider: "PAGAR_ME" | "FACILITA_PAY" | "BRLA"; - fee_bearer: "connected_account"; + fee_bearer: "connected_account" | "platform"; } // ---------------------- diff --git a/packages/payments/src/types/transactions.ts b/packages/payments/src/types/transactions.ts index d01e0254..e7d897b1 100644 --- a/packages/payments/src/types/transactions.ts +++ b/packages/payments/src/types/transactions.ts @@ -31,13 +31,69 @@ export namespace Transaction { | "canceled_after_completion" | "canceled"; + // ---------------------- + // Type + // ---------------------- + export type TransactionType = + | "pledge" + | "payment" + | "pay_out" + | "batch_transfer" + | "transfer" + | "buy" + | "sell" + | "cancel" + | "refund" + | "payment_method_verification" + | "pledge_with_installment" + | "recurring_payment" + | "installment_payment" + | "external"; + + // ---------------------- + // Sub-status + // ---------------------- + export type SubStatus = + | "payment_intent_created" + | "awaiting_user_initiation" + | "instant_payment_completed" + | "on_ramp_successful" + | "awaiting_capture" + | "captured" + | "authorized" + | "collateral_account" + | "transfer_initiated" + | "transfer_completed" + | "collateral_added" + | "payment_completed" + | "fraud_check_approved" + | "fraud_check_pending" + | "fraud_check_cancelled" + | "capture_failed" + | "on_ramp_failed" + | "transfer_failed" + | "instant_payment_failed" + | "payment_failed" + | "collateral_add_failed" + | "authorization_failed" + | "fraud_check_failed" + | "capture_cancelled" + | "capture_cancellation_failed" + | "funds_scheduled" + | "funds_received" + | "payment_submitted" + | "payment_processed" + | "requires_action" + | "off_ramp_failed"; + // ---------------------- // Model // ---------------------- export interface Item { id: string; status: Status | string; - type: string; + sub_status?: SubStatus | string | null; + type: TransactionType | string; source: Payment.Source; confirm: boolean; metadata?: Payment.Metadata; diff --git a/packages/payments/src/types/webhook.ts b/packages/payments/src/types/webhook.ts index c8cfb0f1..88311bb0 100644 --- a/packages/payments/src/types/webhook.ts +++ b/packages/payments/src/types/webhook.ts @@ -1,6 +1,23 @@ import { ApiResponse } from "./common"; export namespace Webhook { + // ---------------------- + // Categories + // ---------------------- + export type Category = + | "payment_lifecycle" + | "provider_registration_lifecycle" + | "payment_method_lifecycle" + | "transfer_lifecycle" + | "buy_lifecycle" + | "sell_lifecycle" + | "payout_lifecycle" + | "dispute_lifecycle" + | "refund_lifecycle" + | "OTHER" + | "externally_lifecycle" + | "customer_sync_lifecycle"; + // ---------------------- // Data // ---------------------- @@ -31,25 +48,18 @@ export namespace Webhook { // Notifications // ---------------------- export type EventType = - | "payment.processing" - | "payment.succeeded" - | "payment.captured" - | "payment.failed" - | "payment.awaiting_confirmation" - | "payment.cancelled" - | "payment.refunded" - | "payment.updated" - | "refund.created" - | "refund.updated" - | "refund.failed" - | "refund.succeeded" + // Customer lifecycle | "customer.verified" | "customer.processing" | "customer.action_required" | "customer.rejected" | "customer.created" | "customer.updated" - | "customer.sync" + // Customer sync lifecycle + | "customer.sync.started" + | "customer.sync.succeeded" + | "customer.sync.failed" + // Provider registration lifecycle | "provider_registration.submitted" | "provider_registration.approved" | "provider_registration.restricted" @@ -59,42 +69,68 @@ export namespace Webhook { | "provider_registration.action_required" | "provider_registration.documents_uploaded" | "provider_registration.verification_expired" + // Payment lifecycle + | "payment.processing" + | "payment.succeeded" + | "payment.captured" + | "payment.failed" + | "payment.awaiting_confirmation" + | "payment.cancelled" + | "payment.refunded" + | "payment.updated" + // Payment refund sub-events + | "payment.refund.created" + | "payment.refund.updated" + | "payment.refund.failed" + // Refund lifecycle + | "refund.failed" + | "refund.succeeded" + // Payment method lifecycle | "payment_method.verified" | "payment_method.approved" | "payment_method.rejected" | "payment_method.created" | "payment_method.updated" + // Transaction lifecycle | "transaction.updated" | "transaction.sc_updated" - | "dispute.created" - | "dispute.updated" - | "dispute.closed" - | "dispute.funds_reinstated" - | "dispute.funds_withdrawn" + // Dispute lifecycle + | "payment.disputed" + | "payment.dispute.updated" + | "payment.dispute.closed" + | "payment.dispute.funds_reinstated" + | "payment.dispute.funds_withdrawn" + // Payout lifecycle | "payout.succeeded" + // Installment payment lifecycle + | "payment.installment.succeeded" + | "payment.installment.failed" + // KYC lifecycle | "kyc.approved" | "kyc.rejected" | "kyc.action_required" - | "external_deposit.received" - | "external_deposit.pending" - | "external_deposit.completed" - | "external_deposit.succeeded" - | "external_deposit.virtual_account_funded" + // External deposit lifecycle + | "external.deposit.received" + | "external.deposit.awaiting_confirmation" + | "external.deposit.completed" + | "external.deposit.succeeded" + | "external.virtual_account.funded" + // Transfer lifecycle | "transfer.succeeded" | "transfer.failed" + // Sell lifecycle | "sell.succeeded" | "sell.failed" + // Buy lifecycle | "buy.succeeded" | "buy.completed" - | "buy.awaiting_confirmation" - | "installment_payment.succeeded" - | "installment_payment.failed"; + | "buy.awaiting_confirmation"; export interface Notification { id: string; is_acknowledged: boolean; event: EventType | string | null; - category: string | null; + category: Category | string | null; data: any; } From 2d2667325a987024e6bc39828ee1f847f60cbc49 Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 18:57:03 +0600 Subject: [PATCH 11/18] feat: improve support for brazil clients --- packages/payments/src/types/buy.ts | 25 ++++++++++++++++++++- packages/payments/src/types/customer.ts | 3 +++ packages/payments/src/types/payout.ts | 24 +++++++++++++++++++- packages/payments/src/types/transactions.ts | 9 ++++++++ packages/payments/src/types/wallet.ts | 11 ++++++++- 5 files changed, 69 insertions(+), 3 deletions(-) diff --git a/packages/payments/src/types/buy.ts b/packages/payments/src/types/buy.ts index 8724e6fd..ed01772b 100644 --- a/packages/payments/src/types/buy.ts +++ b/packages/payments/src/types/buy.ts @@ -54,7 +54,30 @@ export namespace Buy { metadata?: Metadata; } - export type Request = Bridge; + export interface BrlaPaymentMethod { + id?: string; + type?: "WALLET"; + wallet_details?: { + address: string; + }; + } + + export interface Brla { + provider: "brla"; + source: { + amount: number; + currency: "brl"; + customer_id?: string; + }; + destination: { + currency: "brla"; + customer_id?: string; + payment_method: BrlaPaymentMethod; + }; + metadata?: Metadata; + } + + export type Request = Bridge | Brla; export type Response = ApiResponse; } diff --git a/packages/payments/src/types/customer.ts b/packages/payments/src/types/customer.ts index 173af7ef..4941650b 100644 --- a/packages/payments/src/types/customer.ts +++ b/packages/payments/src/types/customer.ts @@ -51,6 +51,9 @@ export namespace Customer { customer_wallet?: string | null; trading_wallet?: string | null; account_type?: string | null; + additional_info?: Record | null; + synced?: boolean | null; + synced_at?: string | null; } type Provider = diff --git a/packages/payments/src/types/payout.ts b/packages/payments/src/types/payout.ts index e2f93a21..0c751be1 100644 --- a/packages/payments/src/types/payout.ts +++ b/packages/payments/src/types/payout.ts @@ -9,5 +9,27 @@ export namespace Payout { metadata?: Record; } - export type Response = ApiResponse; + export interface PayoutData { + id: string; + status: string; + type: string; + provider: string; + source: { + amount: number; + currency: string; + customer?: { id: string }; + }; + destination?: { + customer?: { id: string }; + payment_method?: { + id: string; + type: string; + }; + }; + metadata?: Record; + created_at: string; + updated_at: string; + } + + export type Response = ApiResponse; } diff --git a/packages/payments/src/types/transactions.ts b/packages/payments/src/types/transactions.ts index e7d897b1..e236d9b8 100644 --- a/packages/payments/src/types/transactions.ts +++ b/packages/payments/src/types/transactions.ts @@ -89,6 +89,14 @@ export namespace Transaction { // ---------------------- // Model // ---------------------- + export interface Installment { + uid: string; + amount: number; + settlement_date: string; + status: string; + sequence: number; + } + export interface Item { id: string; status: Status | string; @@ -98,6 +106,7 @@ export namespace Transaction { confirm: boolean; metadata?: Payment.Metadata; provider: string; + installments?: Installment[]; created_at: string; updated_at: string; } diff --git a/packages/payments/src/types/wallet.ts b/packages/payments/src/types/wallet.ts index 33c660fb..85b81e99 100644 --- a/packages/payments/src/types/wallet.ts +++ b/packages/payments/src/types/wallet.ts @@ -1,5 +1,14 @@ import { ApiResponse } from "./common"; export namespace Wallet { - export type BalanceResponse = ApiResponse; + export interface TokenBalance { + tokenName: string; + amount: number; + } + + export interface BalanceData { + balance: TokenBalance[]; + } + + export type BalanceResponse = ApiResponse; } From 79f41240c3d64ebcf9c08b8ba4cac1f4fb325f10 Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 19:06:47 +0600 Subject: [PATCH 12/18] fix: fix query params and missing enums --- .../payments/src/services/customerService.ts | 47 ++++++++----------- packages/payments/src/services/fileService.ts | 2 +- packages/payments/src/types/customer.ts | 14 ++++++ packages/payments/src/types/paymentMethod.ts | 36 ++++++++------ packages/payments/src/types/plan.ts | 2 + packages/payments/src/types/provider.ts | 13 +++++ packages/payments/src/types/transactions.ts | 6 ++- packages/payments/src/types/webhook.ts | 2 + packages/payments/src/utils/httpClient.ts | 35 +++++++++++--- 9 files changed, 106 insertions(+), 51 deletions(-) diff --git a/packages/payments/src/services/customerService.ts b/packages/payments/src/services/customerService.ts index e89f1bd8..f6e2d22b 100644 --- a/packages/payments/src/services/customerService.ts +++ b/packages/payments/src/services/customerService.ts @@ -1,5 +1,4 @@ import type { Customer, OakClient, Result } from "../types"; -import { err } from "../types"; import { httpClient } from "../utils/httpClient"; import { buildQueryString } from "./helpers"; import { withAuth } from "../utils/withAuth"; @@ -110,20 +109,17 @@ export const createCustomerService = (client: OakClient): CustomerService => ({ id: string, sync: Customer.Sync, ): Promise> { - const token = await client.getAccessToken(); - if (!token.ok) { - return err(token.error); - } - - return httpClient.post( - `${client.config.baseUrl}/api/v1/customers/${id}/sync`, - sync, - { - headers: { - Authorization: `Bearer ${token.value}`, + return withAuth(client, (token) => + httpClient.post( + buildUrl(client.config.baseUrl, "api/v1/customers", id, "sync"), + sync, + { + headers: { + Authorization: `Bearer ${token}`, + }, + retryOptions: client.retryOptions, }, - retryOptions: client.retryOptions, - }, + ), ); }, @@ -132,7 +128,7 @@ export const createCustomerService = (client: OakClient): CustomerService => ({ files: unknown, ): Promise> { return withAuth(client, (token) => - httpClient.post( + httpClient.postMultipart( buildUrl(client.config.baseUrl, "api/v1/customers", customerId, "files"), files, { @@ -175,21 +171,18 @@ export const createCustomerService = (client: OakClient): CustomerService => ({ customer_id: string, filter: Customer.BalanceFilter, ): Promise> { - const token = await client.getAccessToken(); - if (!token.ok) { - return err(token.error); - } - const queryString = buildQueryString(filter); - return httpClient.get( - `${client.config.baseUrl}/api/v1/customers/${customer_id}/balances${queryString}`, - { - headers: { - Authorization: `Bearer ${token.value}`, + return withAuth(client, (token) => + httpClient.get( + `${buildUrl(client.config.baseUrl, "api/v1/customers", customer_id, "balances")}${queryString}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + retryOptions: client.retryOptions, }, - retryOptions: client.retryOptions, - }, + ), ); }, }); diff --git a/packages/payments/src/services/fileService.ts b/packages/payments/src/services/fileService.ts index 9528178c..04cf9180 100644 --- a/packages/payments/src/services/fileService.ts +++ b/packages/payments/src/services/fileService.ts @@ -17,7 +17,7 @@ export interface FileService { export const createFileService = (client: OakClient): FileService => ({ async upload(files: unknown): Promise> { return withAuth(client, (token) => - httpClient.post( + httpClient.postMultipart( buildUrl(client.config.baseUrl, "api/v1/files"), files, { diff --git a/packages/payments/src/types/customer.ts b/packages/payments/src/types/customer.ts index 4941650b..c2d0ddba 100644 --- a/packages/payments/src/types/customer.ts +++ b/packages/payments/src/types/customer.ts @@ -3,6 +3,20 @@ import { ApiResponse } from "./common"; export namespace Customer { export type DocumentType = "personal_tax_id" | "company_tax_id"; + export type Status = + | "INITIATED" + | "APPROVED" + | "REJECTED" + | "IN_REVIEW" + | "INCOMPLETE" + | "NOT_STARTED" + | "DOCUMENT_REQUIRED" + | "PENDING" + | "PROCESSING" + | "DOCUMENT_PENDING" + | "DOCUMENT_APPROVED" + | "AWAITING_CONFIRMATION"; + export interface Base { email: string; document_number?: string; diff --git a/packages/payments/src/types/paymentMethod.ts b/packages/payments/src/types/paymentMethod.ts index 5b410dd5..832925e5 100644 --- a/packages/payments/src/types/paymentMethod.ts +++ b/packages/payments/src/types/paymentMethod.ts @@ -2,23 +2,29 @@ import { ApiResponse } from "./common"; export namespace PaymentMethod { export type MethodType = - | "BANK" - | "CARD" - | "PLAID" - | "VIRTUAL_ACCOUNT" - | "LIQUIDATION_ADDRESS" - | "TRADING_WALLET" - | "CUSTOMER_WALLET" - | "PIX" - | "EVM_ADDRESS"; + | "bank" + | "card" + | "plaid" + | "virtual_account" + | "liquidation_address" + | "trading_wallet" + | "customer_wallet" + | "pix" + | "evm_address"; export type PaymentMethodKind = - | "PIX" - | "BOLETO" - | "CARD" - | "BANK_TRANSFER" - | "BANK" - | "CASH_PAYMENT"; + | "pix" + | "boleto" + | "card" + | "bank_transfer" + | "bank" + | "cash_payment"; + + export type BankAccountType = + | "payment" + | "checking" + | "savings" + | "virtual_account"; export interface BridgeBankAccount { type: MethodType; diff --git a/packages/payments/src/types/plan.ts b/packages/payments/src/types/plan.ts index 02e044dc..9d340d8c 100644 --- a/packages/payments/src/types/plan.ts +++ b/packages/payments/src/types/plan.ts @@ -72,5 +72,7 @@ export namespace Plan { export interface ListQuery { page_no?: number; per_page?: number; + active?: boolean; + expired?: boolean; } } diff --git a/packages/payments/src/types/provider.ts b/packages/payments/src/types/provider.ts index 8f8fed81..b9bcfe91 100644 --- a/packages/payments/src/types/provider.ts +++ b/packages/payments/src/types/provider.ts @@ -21,6 +21,19 @@ export namespace Provider { export type TargetRole = "subaccount" | "customer" | "connected_account"; + export type PlatformStatus = + | "NOT_SUBMITTED" + | "SUBMITTED" + | "AWAITING_CONFIRMATION" + | "PROCESSING" + | "APPROVED" + | "REJECTED" + | "RESTRICTED" + | "CANCELLED" + | "ERROR"; + + export type KycLevel = "1" | "2"; + // ---------------------- // Schema // ---------------------- diff --git a/packages/payments/src/types/transactions.ts b/packages/payments/src/types/transactions.ts index e236d9b8..ba95014c 100644 --- a/packages/payments/src/types/transactions.ts +++ b/packages/payments/src/types/transactions.ts @@ -12,8 +12,10 @@ export namespace Transaction { type_list?: string; // e.g. "installment_payment" status?: string; // comma-separated, e.g. "created,processing" payment_method?: string; // e.g. "pix" - dateFrom?: string; // e.g. "2025-07-02" - dateTo?: string; // e.g. "2025-07-02" + date_from?: string; // e.g. "2025-07-02" + date_to?: string; // e.g. "2025-07-02" + provider?: string; // comma-separated, e.g. "stripe,pagar_me" + strict?: boolean; source_currency?: string; // e.g. "brla" destination_currency?: string; // e.g. "brl" } diff --git a/packages/payments/src/types/webhook.ts b/packages/payments/src/types/webhook.ts index 88311bb0..d5504373 100644 --- a/packages/payments/src/types/webhook.ts +++ b/packages/payments/src/types/webhook.ts @@ -137,6 +137,8 @@ export namespace Webhook { export interface ListNotificationsQuery { limit?: number; offset?: number; + reference_id?: string; + reference_module?: string; } export interface ListNotificationsData { diff --git a/packages/payments/src/utils/httpClient.ts b/packages/payments/src/utils/httpClient.ts index e407dbac..9093cea4 100644 --- a/packages/payments/src/utils/httpClient.ts +++ b/packages/payments/src/utils/httpClient.ts @@ -7,6 +7,7 @@ export interface HttpClientConfig { headers?: Record; retryOptions: RetryOptions; signal?: AbortSignal; + isMultipart?: boolean; } /** @@ -28,11 +29,16 @@ const oakVersion = process.env.OAK_VERSION ?? getPackageVersion() ?? "unknown"; * @param headers - Optional custom headers to merge * @returns Merged headers with defaults */ -const mergeHeaders = (headers?: Record) => ({ - "Content-Type": "application/json", - "Oak-Version": oakVersion, - ...(headers ?? {}), -}); +const mergeHeaders = (headers?: Record, isMultipart?: boolean) => { + const base: Record = { + "Oak-Version": oakVersion, + ...(headers ?? {}), + }; + if (!isMultipart) { + base["Content-Type"] = "application/json"; + } + return base; +}; type ParseResult = | { success: true; data: unknown; error?: undefined } @@ -110,7 +116,7 @@ const request = async ( try { response = await fetch(url, { ...init, - headers: mergeHeaders(config.headers), + headers: mergeHeaders(config.headers, config.isMultipart), signal: config.signal, }); } catch (error) { @@ -162,6 +168,23 @@ export const httpClient = { body: JSON.stringify(data), }); }, + /** + * @typeParam T - Expected response body type + * @param url - Request URL + * @param data - FormData or other multipart body + * @param config - HTTP client configuration (isMultipart should be true) + * @returns Result containing parsed response or error + */ + async postMultipart( + url: string, + data: unknown, + config: HttpClientConfig + ): Promise> { + return request(url, { ...config, isMultipart: true }, { + method: "POST", + body: data as BodyInit, + }); + }, /** * @typeParam T - Expected response body type * @param url - Request URL From f48820314cc052d77b408df6d390144efac4c09b Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 19:17:34 +0600 Subject: [PATCH 13/18] feat: add pix payment type, trading wallet and plaid response fields --- packages/payments/src/types/buy.ts | 2 +- packages/payments/src/types/payment.ts | 23 +++++++++++++++- packages/payments/src/types/paymentMethod.ts | 28 +++++++++++++++++++- packages/payments/src/types/sell.ts | 5 ++-- 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/packages/payments/src/types/buy.ts b/packages/payments/src/types/buy.ts index ed01772b..ab1a7101 100644 --- a/packages/payments/src/types/buy.ts +++ b/packages/payments/src/types/buy.ts @@ -56,7 +56,7 @@ export namespace Buy { export interface BrlaPaymentMethod { id?: string; - type?: "WALLET"; + type?: "wallet"; wallet_details?: { address: string; }; diff --git a/packages/payments/src/types/payment.ts b/packages/payments/src/types/payment.ts index 32ff4d1b..27eef691 100644 --- a/packages/payments/src/types/payment.ts +++ b/packages/payments/src/types/payment.ts @@ -151,7 +151,28 @@ export namespace Payment { metadata?: Record; } - export type Request = MercadoPagoRequest | PagarMeRequest | StripeRequest; + export interface PagarMePixRequest { + provider: "pagar_me"; + source: { + amount: number; + currency: "brl"; + customer: { + id: string; + }; + payment_method: { + type: "pix"; + expiry_date: string; // ISO date, must be in the future + }; + }; + confirm?: boolean; + metadata?: Record; + } + + export type Request = + | MercadoPagoRequest + | PagarMeRequest + | PagarMePixRequest + | StripeRequest; // ---------------------------------------- // Payment responses (create / confirm / cancel) diff --git a/packages/payments/src/types/paymentMethod.ts b/packages/payments/src/types/paymentMethod.ts index 832925e5..8c859ebb 100644 --- a/packages/payments/src/types/paymentMethod.ts +++ b/packages/payments/src/types/paymentMethod.ts @@ -138,6 +138,31 @@ export namespace PaymentMethod { metadata?: Record; } + export interface PlaidResponseData { + id: string; + type: MethodType; + link_token: string; + callback_url: string; + link_token_expires_at: string; + metadata?: Record; + } + + export interface TradingWallet { + type: MethodType; + provider: string; // from PLATFORMS keys + metadata?: Record; + } + + export interface TradingWalletResponseData { + id: string; + type: MethodType; + status: string; + wallet_details: { + address: string; + }; + metadata?: Record; + } + export interface BridgeVirtualAccount { type: MethodType; provider: string; // from PLATFORMS keys @@ -171,7 +196,8 @@ export namespace PaymentMethod { | BridgeLiquidationAddress | OakPix | BridgePlaid - | BridgeVirtualAccount; + | BridgeVirtualAccount + | TradingWallet; export type ResponseData = Request & { id: string; diff --git a/packages/payments/src/types/sell.ts b/packages/payments/src/types/sell.ts index 7083e6e7..1bdd049f 100644 --- a/packages/payments/src/types/sell.ts +++ b/packages/payments/src/types/sell.ts @@ -6,7 +6,8 @@ export namespace Sell { // ---------------------- export type PaymentMethod = | { type: "pix"; id: string } // saved payment method - | { type: "pix"; pix_string: string }; // direct PIX string + | { type: "pix"; pix_string: string } // direct PIX string + | { type: "bank"; id: string }; // saved bank payment method // ---------------------- // Request @@ -42,7 +43,7 @@ export namespace Sell { currency: string; customer: { id: string }; payment_method: { - type: "pix"; + type: "pix" | "bank"; id?: string; pix_string?: string; }; From 13407f0b0426c0398b5fd6d0742bb155a38b1898 Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 19:25:46 +0600 Subject: [PATCH 14/18] fix: fix webhook signature verification and balance response field --- packages/payments/src/types/customer.ts | 2 +- .../payments/src/utils/webhookVerification.ts | 21 ++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/payments/src/types/customer.ts b/packages/payments/src/types/customer.ts index c2d0ddba..232b32f0 100644 --- a/packages/payments/src/types/customer.ts +++ b/packages/payments/src/types/customer.ts @@ -138,7 +138,7 @@ export namespace Customer { as_of: string; totals: { currency: string; - amount: number; + available: number; pending: number; reserved: number; instant_payouts: number; diff --git a/packages/payments/src/utils/webhookVerification.ts b/packages/payments/src/utils/webhookVerification.ts index 521c7887..df9dbc77 100644 --- a/packages/payments/src/utils/webhookVerification.ts +++ b/packages/payments/src/utils/webhookVerification.ts @@ -29,13 +29,28 @@ export function verifyWebhookSignature( secret: string, ): boolean { try { - // Generate expected signature + // Parse the CrowdSplit-Signature header: "t=,v1=" + let timestamp: string; + let v1Signature: string; + + if (signature.startsWith("t=")) { + const parts = signature.split(","); + timestamp = parts[0]?.replace("t=", "") ?? ""; + v1Signature = parts[1]?.replace("v1=", "") ?? ""; + } else { + // Fallback: treat as raw signature (legacy) + timestamp = ""; + v1Signature = signature; + } + + // Crowdsplit signs: timestamp + "." + payload + const base = timestamp ? `${timestamp}.${payload}` : payload; const hmac = createHmac("sha256", secret); - hmac.update(payload); + hmac.update(base, "utf8"); const expectedSignature = hmac.digest("hex"); // Convert both signatures to buffers for timing-safe comparison - const signatureBuffer = Buffer.from(signature, "utf-8"); + const signatureBuffer = Buffer.from(v1Signature, "utf-8"); const expectedBuffer = Buffer.from(expectedSignature, "utf-8"); // Ensure buffers are same length to prevent timing attacks From 7184b18a605f51a45b9c2157d3f46ae08886329e Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 19:53:25 +0600 Subject: [PATCH 15/18] test: rewrite integration tests for all new services --- .../integration/missingServices.test.ts | 474 ++++++++- .../integration/remainingGaps.test.ts | 141 --- .../__tests__/unit/newServices.test.ts | 925 ++++++++++++++++++ .../unit/webhookVerification.test.ts | 74 +- 4 files changed, 1407 insertions(+), 207 deletions(-) delete mode 100644 packages/payments/__tests__/integration/remainingGaps.test.ts create mode 100644 packages/payments/__tests__/unit/newServices.test.ts diff --git a/packages/payments/__tests__/integration/missingServices.test.ts b/packages/payments/__tests__/integration/missingServices.test.ts index a12628ea..b1ec8278 100644 --- a/packages/payments/__tests__/integration/missingServices.test.ts +++ b/packages/payments/__tests__/integration/missingServices.test.ts @@ -4,21 +4,53 @@ import { createWalletService, createMerchantService, createFileService, + createTaxService, + createProviderProxyService, + createPixService, + createSandboxService, + createTransferService, + createPlanService, + createSubscriptionService, + createDisputeService, + createPayoutService, + createPaymentService, } from '../../src'; import { CustomerService } from '../../src/services/customerService'; import { WalletService } from '../../src/services/walletService'; import { MerchantService } from '../../src/services/merchantService'; import { FileService } from '../../src/services/fileService'; +import { TaxService } from '../../src/services/taxService'; +import { ProviderProxyService } from '../../src/services/providerProxyService'; +import { PixService } from '../../src/services/pixService'; +import { SandboxService } from '../../src/services/sandboxService'; +import { TransferService } from '../../src/services/transferService'; +import { PlanService } from '../../src/services/planService'; +import { SubscriptionService } from '../../src/services/subscriptionService'; +import { DisputeService } from '../../src/services/disputeService'; +import { PayoutService } from '../../src/services/payoutService'; +import { PaymentService } from '../../src/services/paymentService'; import { getConfigFromEnv } from '../config'; const INTEGRATION_TEST_TIMEOUT = 30000; -describe('Missing Services - Integration', () => { +describe('All New Services - Integration', () => { let customers: CustomerService; let wallets: WalletService; let merchant: MerchantService; let files: FileService; + let taxes: TaxService; + let providerProxy: ProviderProxyService; + let pix: PixService; + let sandbox: SandboxService; + let transfers: TransferService; + let plans: PlanService; + let subscriptions: SubscriptionService; + let disputes: DisputeService; + let payouts: PayoutService; + let payments: PaymentService; + let existingCustomerId: string | undefined; + let approvedCustomerId: string | undefined; beforeAll(async () => { const client = createOakClient({ @@ -33,54 +65,291 @@ describe('Missing Services - Integration', () => { wallets = createWalletService(client); merchant = createMerchantService(client); files = createFileService(client); + taxes = createTaxService(client); + providerProxy = createProviderProxyService(client); + pix = createPixService(client); + sandbox = createSandboxService(client); + transfers = createTransferService(client); + plans = createPlanService(client); + subscriptions = createSubscriptionService(client); + disputes = createDisputeService(client); + payouts = createPayoutService(client); + payments = createPaymentService(client); + // Find existing customer const listRes = await customers.list({ limit: 1 }); if (listRes.ok && listRes.value.data.customer_list.length > 0) { const first = listRes.value.data.customer_list[0]; existingCustomerId = (first.id ?? first.customer_id) as string; } + + // Find approved customer for payment tests + const approvedRes = await customers.list({ + target_role: 'customer', + provider_registration_status: 'approved', + provider: 'stripe', + }); + if (approvedRes.ok && approvedRes.value.data.customer_list.length > 0) { + approvedCustomerId = (approvedRes.value.data.customer_list[0].id ?? + approvedRes.value.data.customer_list[0].customer_id) as string; + } }, INTEGRATION_TEST_TIMEOUT); // --------------------------------------------------------------- - // CustomerService - file methods + // PaymentService - capture, sandboxPaid, sandboxSettle // --------------------------------------------------------------- - describe('CustomerService - files', () => { - it('should expose uploadFiles method', () => { - expect(typeof customers.uploadFiles).toBe('function'); - }); + describe('PaymentService - capture/sandbox', () => { + it( + 'should return error for capture with invalid ID', + async () => { + const response = await payments.capture('non-existent-id'); + expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); - it('should expose getFiles method', () => { - expect(typeof customers.getFiles).toBe('function'); - }); + it( + 'should create payment then sandbox paid it', + async () => { + if (!approvedCustomerId) { + throw new Error('No approved customer — create one with Stripe KYC'); + } + + const createRes = await payments.create({ + provider: 'stripe', + source: { + amount: 1500, + currency: 'usd', + payment_method: { type: 'card' }, + capture_method: 'automatic', + customer: { id: approvedCustomerId }, + }, + confirm: false, + metadata: { order_id: `test-sandbox-${Date.now()}` }, + } as any); + + expect(createRes.ok).toBe(true); + if (!createRes.ok) return; + + const paymentId = createRes.value.data.id; + expect(paymentId).toBeDefined(); + expect(typeof paymentId).toBe('string'); + + const sandboxRes = await payments.sandboxPaid(paymentId); + expect(sandboxRes).toBeDefined(); + if (sandboxRes.ok) { + expect(sandboxRes.value.data.id).toEqual(paymentId); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); + + it( + 'should return error for sandboxSettle with invalid ID', + async () => { + const response = await payments.sandboxSettle('non-existent-id'); + expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + // --------------------------------------------------------------- + // CustomerService - files + populatePlatform + // --------------------------------------------------------------- + describe('CustomerService - files & platform', () => { it( - 'should get files for a customer', + 'should get files for an existing customer and verify response shape', async () => { if (!existingCustomerId) { throw new Error('No customer available'); } const response = await customers.getFiles(existingCustomerId); expect(response).toBeDefined(); + if (response.ok) { + expect(response.value).toBeDefined(); + expect(response.value.data).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); + + it( + 'should return error for getFiles with non-existent customer', + async () => { + const response = await customers.getFiles('non-existent-id'); + expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); + + it( + 'should return error for populatePlatform with invalid customer', + async () => { + const response = await customers.populatePlatform('non-existent-id', { + provider: 'stripe', + }); + expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error).toBeDefined(); + } }, INTEGRATION_TEST_TIMEOUT, ); }); // --------------------------------------------------------------- - // CustomerService - populatePlatform (KYC) + // TransferService - sendWebhook // --------------------------------------------------------------- - describe('CustomerService - populatePlatform', () => { - it('should expose populatePlatform method', () => { - expect(typeof customers.populatePlatform).toBe('function'); - }); + describe('TransferService - sendWebhook', () => { + it( + 'should call the transfer webhook endpoint', + async () => { + const response = await transfers.sendWebhook({}); + expect(response).toBeDefined(); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + // --------------------------------------------------------------- + // SubscriptionService + // --------------------------------------------------------------- + describe('SubscriptionService', () => { it( - 'should return an error for an invalid customer', + 'should return error for subscribe with invalid plan', async () => { - const response = await customers.populatePlatform('non-existent-id', { - provider: 'stripe', + const response = await subscriptions.subscribe({ + plan_id: 'non-existent-plan', + source_customer_id: 'fake-source', + destination_customer_id: 'fake-dest', + payment_method_id: 'fake-pm', + payment_method_type: 'CARD', + payment_method_provider: 'PAGAR_ME', + fee_bearer: 'connected_account', + }); + expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); + + it( + 'should return error for get with non-existent subscription', + async () => { + const response = await subscriptions.get('non-existent-id'); + expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); + + it( + 'should return error for cancel with non-existent subscription', + async () => { + const response = await subscriptions.cancel('non-existent-id'); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + + it( + 'should return error for pay with non-existent subscription', + async () => { + const response = await subscriptions.pay('non-existent-id', { + customer_id: 'fake-customer-id', + payment_method_id: 'fake-pm-id', + payment_method_type: 'CARD', + payment_method_provider: 'PAGAR_ME', + }); + expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // DisputeService + // --------------------------------------------------------------- + describe('DisputeService', () => { + it( + 'should list disputes', + async () => { + const response = await disputes.list(); + expect(response).toBeDefined(); + if (response.ok) { + expect(response.value).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); + + it( + 'should return error for updateEvidence with non-existent dispute', + async () => { + const response = await disputes.updateEvidence('non-existent-id', { + text_evidences: [{ key: 'customer_name', value: 'Test Customer' }], + }); + expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); + + it( + 'should return error for submit with non-existent dispute', + async () => { + const response = await disputes.submit('non-existent-id'); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + + it( + 'should return error for close with non-existent dispute', + async () => { + const response = await disputes.close('non-existent-id'); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // PayoutService + // --------------------------------------------------------------- + describe('PayoutService', () => { + it( + 'should return error for payout with non-existent customer', + async () => { + const response = await payouts.create({ + payment_method_id: 'non-existent-pm', + amount: 1000, + currency: 'USD', + customer_id: 'non-existent-customer', }); expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error).toBeDefined(); + } }, INTEGRATION_TEST_TIMEOUT, ); @@ -90,15 +359,27 @@ describe('Missing Services - Integration', () => { // WalletService // --------------------------------------------------------------- describe('WalletService', () => { - it('should expose getBalance method', () => { - expect(typeof wallets.getBalance).toBe('function'); - }); - it( - 'should return an error for a non-existent customer', + 'should return error for getBalance with non-existent customer', async () => { const response = await wallets.getBalance('non-existent-id'); expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); + + it( + 'should attempt getBalance for existing customer', + async () => { + if (!existingCustomerId) { + throw new Error('No customer available'); + } + const response = await wallets.getBalance(existingCustomerId); + // May return 404 if no trading wallet — both outcomes valid + expect(response).toBeDefined(); }, INTEGRATION_TEST_TIMEOUT, ); @@ -108,19 +389,33 @@ describe('Missing Services - Integration', () => { // MerchantService // --------------------------------------------------------------- describe('MerchantService', () => { - it('should expose calculateTransferDate method', () => { - expect(typeof merchant.calculateTransferDate).toBe('function'); - }); - it( - 'should calculate a transfer date', + 'should calculate transfer date with valid params', async () => { const response = await merchant.calculateTransferDate({ - settlementDate: '2026-05-01', + settlementDate: '2026-06-01', region: 'US', }); - // May succeed or fail depending on backend config expect(response).toBeDefined(); + if (response.ok) { + expect(response.value).toBeDefined(); + expect(response.value.data).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); + + it( + 'should return error for invalid region format', + async () => { + const response = await merchant.calculateTransferDate({ + settlementDate: '2026-06-01', + region: 'INVALID_TOO_LONG', + }); + expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error).toBeDefined(); + } }, INTEGRATION_TEST_TIMEOUT, ); @@ -130,36 +425,123 @@ describe('Missing Services - Integration', () => { // FileService // --------------------------------------------------------------- describe('FileService', () => { - it('should expose upload method', () => { - expect(typeof files.upload).toBe('function'); - }); + it( + 'should list merchant files', + async () => { + const response = await files.list(); + expect(response).toBeDefined(); + if (response.ok) { + expect(response.value).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); - it('should expose list method', () => { - expect(typeof files.list).toBe('function'); - }); + it( + 'should return error for get with non-existent file', + async () => { + const response = await files.get('non-existent-file-id'); + expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); - it('should expose get method', () => { - expect(typeof files.get).toBe('function'); - }); + it( + 'should return error for delete with non-existent file', + async () => { + const response = await files.delete('non-existent-file-id'); + expect(response.ok).toBe(false); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); - it('should expose delete method', () => { - expect(typeof files.delete).toBe('function'); - }); + // --------------------------------------------------------------- + // TaxService + // --------------------------------------------------------------- + describe('TaxService', () => { + it( + 'should call tax calculation endpoint', + async () => { + const response = await taxes.calculate({ provider: 'stripe' }); + expect(response).toBeDefined(); + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + // --------------------------------------------------------------- + // ProviderProxyService + // --------------------------------------------------------------- + describe('ProviderProxyService', () => { it( - 'should list files', + 'should return error for non-whitelisted URL', async () => { - const response = await files.list(); + const response = await providerProxy.proxy('stripe', { + method: 'GET', + url: 'https://invalid-not-whitelisted.example.com', + }); + expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // PixService + // --------------------------------------------------------------- + describe('PixService', () => { + it( + 'should call pix paid endpoint', + async () => { + const response = await pix.createPaid({ + pix_string: 'test-pix-string', + pix_string_type: 'BR_CODE', + amount: 100, + }); expect(response).toBeDefined(); }, INTEGRATION_TEST_TIMEOUT, ); + }); + // --------------------------------------------------------------- + // SandboxService + // --------------------------------------------------------------- + describe('SandboxService', () => { it( - 'should return an error for a non-existent file', + 'should return error for invalid simulation category', async () => { - const response = await files.get('non-existent-id'); + const response = await sandbox.simulateWebhook('stripe', { + category: 'invalid_category', + }); expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error).toBeDefined(); + } + }, + INTEGRATION_TEST_TIMEOUT, + ); + }); + + // --------------------------------------------------------------- + // PlanService - verify new query params + // --------------------------------------------------------------- + describe('PlanService - filters', () => { + it( + 'should list plans with active filter', + async () => { + const response = await plans.list({ active: true }); + expect(response).toBeDefined(); + if (response.ok) { + expect(response.value.data).toBeDefined(); + } }, INTEGRATION_TEST_TIMEOUT, ); diff --git a/packages/payments/__tests__/integration/remainingGaps.test.ts b/packages/payments/__tests__/integration/remainingGaps.test.ts deleted file mode 100644 index 375374a5..00000000 --- a/packages/payments/__tests__/integration/remainingGaps.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { - createOakClient, - createTaxService, - createProviderProxyService, - createPixService, - createSandboxService, - createTransferService, -} from '../../src'; -import { TaxService } from '../../src/services/taxService'; -import { ProviderProxyService } from '../../src/services/providerProxyService'; -import { PixService } from '../../src/services/pixService'; -import { SandboxService } from '../../src/services/sandboxService'; -import { TransferService } from '../../src/services/transferService'; -import { getConfigFromEnv } from '../config'; - -const INTEGRATION_TEST_TIMEOUT = 30000; - -describe('Remaining Gaps - Integration', () => { - let taxes: TaxService; - let providerProxy: ProviderProxyService; - let pix: PixService; - let sandbox: SandboxService; - let transfers: TransferService; - - beforeAll(() => { - const client = createOakClient({ - ...getConfigFromEnv(), - retryOptions: { - maxNumberOfRetries: 2, - delay: 500, - backoffFactor: 2, - }, - }); - taxes = createTaxService(client); - providerProxy = createProviderProxyService(client); - pix = createPixService(client); - sandbox = createSandboxService(client); - transfers = createTransferService(client); - }); - - // --------------------------------------------------------------- - // Gap 6: TaxService - // --------------------------------------------------------------- - describe('TaxService', () => { - it('should expose calculate method', () => { - expect(typeof taxes.calculate).toBe('function'); - }); - - it( - 'should call the tax calculation endpoint', - async () => { - const response = await taxes.calculate({ - provider: 'stripe', - }); - expect(response).toBeDefined(); - }, - INTEGRATION_TEST_TIMEOUT, - ); - }); - - // --------------------------------------------------------------- - // Gap 7: ProviderProxyService - // --------------------------------------------------------------- - describe('ProviderProxyService', () => { - it('should expose proxy method', () => { - expect(typeof providerProxy.proxy).toBe('function'); - }); - - it( - 'should return an error for an invalid proxy request', - async () => { - const response = await providerProxy.proxy('stripe', { - method: 'GET', - url: 'https://invalid-url.example.com', - }); - expect(response.ok).toBe(false); - }, - INTEGRATION_TEST_TIMEOUT, - ); - }); - - // --------------------------------------------------------------- - // Gap 8: PixService - // --------------------------------------------------------------- - describe('PixService', () => { - it('should expose createPaid method', () => { - expect(typeof pix.createPaid).toBe('function'); - }); - - it( - 'should call the pix paid endpoint', - async () => { - const response = await pix.createPaid({ - pix_string: 'test-pix-string', - pix_string_type: 'BR_CODE', - amount: 100, - }); - expect(response).toBeDefined(); - }, - INTEGRATION_TEST_TIMEOUT, - ); - }); - - // --------------------------------------------------------------- - // Gap 9: SandboxService - // --------------------------------------------------------------- - describe('SandboxService', () => { - it('should expose simulateWebhook method', () => { - expect(typeof sandbox.simulateWebhook).toBe('function'); - }); - - it( - 'should return an error for an invalid simulation', - async () => { - const response = await sandbox.simulateWebhook('stripe', { - category: 'invalid_category', - }); - expect(response.ok).toBe(false); - }, - INTEGRATION_TEST_TIMEOUT, - ); - }); - - // --------------------------------------------------------------- - // Gap 10: TransferService.sendWebhook - // --------------------------------------------------------------- - describe('TransferService - sendWebhook', () => { - it('should expose sendWebhook method', () => { - expect(typeof transfers.sendWebhook).toBe('function'); - }); - - it( - 'should return a response when sending a transfer webhook', - async () => { - const response = await transfers.sendWebhook({}); - expect(response).toBeDefined(); - }, - INTEGRATION_TEST_TIMEOUT, - ); - }); -}); diff --git a/packages/payments/__tests__/unit/newServices.test.ts b/packages/payments/__tests__/unit/newServices.test.ts new file mode 100644 index 00000000..1ac3d1b9 --- /dev/null +++ b/packages/payments/__tests__/unit/newServices.test.ts @@ -0,0 +1,925 @@ +import { + createSubscriptionService, + createDisputeService, + createPayoutService, + createWalletService, + createMerchantService, + createFileService, + createTaxService, + createProviderProxyService, + createPixService, + createSandboxService, + createPaymentService, + createCustomerService, + createTransferService, +} from "../../src/services"; +import { httpClient } from "../../src/utils/httpClient"; +import { ApiError, SDKError } from "../../src/utils/errorHandler"; +import type { OakClient } from "../../src/types"; +import { err, ok } from "../../src/types"; +import { ENVIRONMENT_URLS } from "../../src/types/environment"; + +const SANDBOX_URL = ENVIRONMENT_URLS.sandbox; + +jest.mock("../../src/utils/httpClient", () => ({ + httpClient: { + post: jest.fn(), + get: jest.fn(), + put: jest.fn(), + patch: jest.fn(), + delete: jest.fn(), + postMultipart: jest.fn(), + }, +})); + +const mockedHttpClient = httpClient as jest.Mocked; + +const retryOptions = { maxNumberOfRetries: 0, delay: 0, backoffFactor: 2 }; + +const makeClient = (): OakClient => ({ + config: { + environment: "sandbox", + clientId: "id", + baseUrl: SANDBOX_URL, + }, + retryOptions, + getAccessToken: jest.fn().mockResolvedValue(ok("token")), + grantToken: jest.fn(), +}); + +const makeClientWithTokenError = (): OakClient => { + const tokenError = new SDKError("Token error"); + return { + config: { + environment: "sandbox", + clientId: "id", + baseUrl: SANDBOX_URL, + }, + retryOptions, + getAccessToken: jest.fn().mockResolvedValue(err(tokenError)), + grantToken: jest.fn(), + }; +}; + +const getAuthConfig = (client: OakClient) => + expect.objectContaining({ + headers: { Authorization: "Bearer token" }, + retryOptions: client.retryOptions, + }); + +const expectSuccess = async (options: { + client: OakClient; + call: () => Promise; + httpMethod: keyof typeof mockedHttpClient; + expectedArgs: unknown[]; +}) => { + const response = { ok: true }; + mockedHttpClient[options.httpMethod].mockResolvedValue(ok(response) as never); + + const result = await options.call(); + + expect(result).toEqual(ok(response)); + expect(mockedHttpClient[options.httpMethod]).toHaveBeenCalledWith( + ...options.expectedArgs, + ); + expect(options.client.getAccessToken).toHaveBeenCalled(); +}; + +const expectFailure = async (options: { + call: () => Promise; + httpMethod: keyof typeof mockedHttpClient; + errorMessage: string; +}) => { + const apiError = new ApiError(options.errorMessage, 400, { + msg: options.errorMessage, + }); + mockedHttpClient[options.httpMethod].mockResolvedValue( + err(apiError) as never, + ); + + const result = await options.call(); + expect(result).toEqual(err(expect.any(ApiError))); +}; + +const expectTokenFailure = async (call: () => Promise) => { + const result = await call(); + expect(result).toEqual(err(expect.any(SDKError))); +}; + +describe("New services (Unit)", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + // --------------------------------------------------------------- + // PaymentService - capture, sandboxPaid, sandboxSettle + // --------------------------------------------------------------- + it("paymentService.capture success", async () => { + const client = makeClient(); + const service = createPaymentService(client); + await expectSuccess({ + client, + call: () => service.capture("pay-1"), + httpMethod: "post", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/payments/pay-1/capture`, + {}, + getAuthConfig(client), + ], + }); + }); + + it("paymentService.capture failure", async () => { + const client = makeClient(); + const service = createPaymentService(client); + await expectFailure({ + call: () => service.capture("pay-1"), + httpMethod: "post", + errorMessage: "Failed to capture payment", + }); + }); + + it("paymentService.capture token error", async () => { + const client = makeClientWithTokenError(); + const service = createPaymentService(client); + await expectTokenFailure(() => service.capture("pay-1")); + }); + + it("paymentService.sandboxPaid success", async () => { + const client = makeClient(); + const service = createPaymentService(client); + await expectSuccess({ + client, + call: () => service.sandboxPaid("pay-1"), + httpMethod: "post", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/payments/pay-1/sandbox/paid`, + {}, + getAuthConfig(client), + ], + }); + }); + + it("paymentService.sandboxPaid token error", async () => { + const client = makeClientWithTokenError(); + const service = createPaymentService(client); + await expectTokenFailure(() => service.sandboxPaid("pay-1")); + }); + + it("paymentService.sandboxSettle success", async () => { + const client = makeClient(); + const service = createPaymentService(client); + await expectSuccess({ + client, + call: () => service.sandboxSettle("pay-1"), + httpMethod: "post", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/payments/pay-1/sandbox/settle`, + {}, + getAuthConfig(client), + ], + }); + }); + + it("paymentService.sandboxSettle token error", async () => { + const client = makeClientWithTokenError(); + const service = createPaymentService(client); + await expectTokenFailure(() => service.sandboxSettle("pay-1")); + }); + + // --------------------------------------------------------------- + // CustomerService - uploadFiles, getFiles, populatePlatform + // --------------------------------------------------------------- + it("customerService.uploadFiles success", async () => { + const client = makeClient(); + const service = createCustomerService(client); + await expectSuccess({ + client, + call: () => service.uploadFiles("cust-1", {}), + httpMethod: "postMultipart", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/customers/cust-1/files`, + {}, + getAuthConfig(client), + ], + }); + }); + + it("customerService.uploadFiles token error", async () => { + const client = makeClientWithTokenError(); + const service = createCustomerService(client); + await expectTokenFailure(() => service.uploadFiles("cust-1", {})); + }); + + it("customerService.getFiles success", async () => { + const client = makeClient(); + const service = createCustomerService(client); + await expectSuccess({ + client, + call: () => service.getFiles("cust-1"), + httpMethod: "get", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/customers/cust-1/files`, + getAuthConfig(client), + ], + }); + }); + + it("customerService.getFiles failure", async () => { + const client = makeClient(); + const service = createCustomerService(client); + await expectFailure({ + call: () => service.getFiles("cust-1"), + httpMethod: "get", + errorMessage: "Failed to get files", + }); + }); + + it("customerService.populatePlatform success", async () => { + const client = makeClient(); + const service = createCustomerService(client); + await expectSuccess({ + client, + call: () => + service.populatePlatform("cust-1", { provider: "stripe" }), + httpMethod: "post", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/customers/cust-1/platforms`, + { provider: "stripe" }, + getAuthConfig(client), + ], + }); + }); + + it("customerService.populatePlatform token error", async () => { + const client = makeClientWithTokenError(); + const service = createCustomerService(client); + await expectTokenFailure(() => + service.populatePlatform("cust-1", { provider: "stripe" }), + ); + }); + + // --------------------------------------------------------------- + // TransferService - sendWebhook + // --------------------------------------------------------------- + it("transferService.sendWebhook success", async () => { + const client = makeClient(); + const service = createTransferService(client); + await expectSuccess({ + client, + call: () => service.sendWebhook({ test: true }), + httpMethod: "post", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/transfer/webhook`, + { test: true }, + getAuthConfig(client), + ], + }); + }); + + it("transferService.sendWebhook failure", async () => { + const client = makeClient(); + const service = createTransferService(client); + await expectFailure({ + call: () => service.sendWebhook({}), + httpMethod: "post", + errorMessage: "Failed to send webhook", + }); + }); + + it("transferService.sendWebhook token error", async () => { + const client = makeClientWithTokenError(); + const service = createTransferService(client); + await expectTokenFailure(() => service.sendWebhook({})); + }); + + // --------------------------------------------------------------- + // SubscriptionService + // --------------------------------------------------------------- + it("subscriptionService.subscribe success", async () => { + const client = makeClient(); + const service = createSubscriptionService(client); + await expectSuccess({ + client, + call: () => + service.subscribe({ + plan_id: "plan-1", + source_customer_id: "src-1", + destination_customer_id: "dst-1", + payment_method_id: "pm-1", + payment_method_type: "CARD", + payment_method_provider: "PAGAR_ME", + fee_bearer: "connected_account", + }), + httpMethod: "post", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/subscription/subscribe`, + expect.any(Object), + getAuthConfig(client), + ], + }); + }); + + it("subscriptionService.subscribe failure", async () => { + const client = makeClient(); + const service = createSubscriptionService(client); + await expectFailure({ + call: () => + service.subscribe({ + plan_id: "x", + source_customer_id: "x", + destination_customer_id: "x", + payment_method_id: "x", + payment_method_type: "CARD", + payment_method_provider: "PAGAR_ME", + fee_bearer: "connected_account", + }), + httpMethod: "post", + errorMessage: "Failed to subscribe", + }); + }); + + it("subscriptionService.subscribe token error", async () => { + const client = makeClientWithTokenError(); + const service = createSubscriptionService(client); + await expectTokenFailure(() => + service.subscribe({ + plan_id: "x", + source_customer_id: "x", + destination_customer_id: "x", + payment_method_id: "x", + payment_method_type: "CARD", + payment_method_provider: "PAGAR_ME", + fee_bearer: "connected_account", + }), + ); + }); + + it("subscriptionService.cancel success", async () => { + const client = makeClient(); + const service = createSubscriptionService(client); + await expectSuccess({ + client, + call: () => service.cancel("sub-1"), + httpMethod: "patch", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/subscription/subscriptions/sub-1/cancel`, + undefined, + getAuthConfig(client), + ], + }); + }); + + it("subscriptionService.cancel token error", async () => { + const client = makeClientWithTokenError(); + const service = createSubscriptionService(client); + await expectTokenFailure(() => service.cancel("sub-1")); + }); + + it("subscriptionService.get success", async () => { + const client = makeClient(); + const service = createSubscriptionService(client); + await expectSuccess({ + client, + call: () => service.get("sub-1"), + httpMethod: "get", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/subscription/sub-1`, + getAuthConfig(client), + ], + }); + }); + + it("subscriptionService.get token error", async () => { + const client = makeClientWithTokenError(); + const service = createSubscriptionService(client); + await expectTokenFailure(() => service.get("sub-1")); + }); + + it("subscriptionService.list success", async () => { + const client = makeClient(); + const service = createSubscriptionService(client); + const response = { ok: true }; + mockedHttpClient.get.mockResolvedValue(ok(response) as never); + const result = await service.list({ customer_id: "cust-1" }); + expect(result).toEqual(ok(response)); + expect(client.getAccessToken).toHaveBeenCalled(); + }); + + it("subscriptionService.list token error", async () => { + const client = makeClientWithTokenError(); + const service = createSubscriptionService(client); + await expectTokenFailure(() => + service.list({ customer_id: "cust-1" }), + ); + }); + + it("subscriptionService.pay success", async () => { + const client = makeClient(); + const service = createSubscriptionService(client); + await expectSuccess({ + client, + call: () => + service.pay("sub-1", { + customer_id: "cust-1", + payment_method_id: "pm-1", + payment_method_type: "CARD", + payment_method_provider: "PAGAR_ME", + }), + httpMethod: "post", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/subscription/sub-1/payment`, + expect.any(Object), + getAuthConfig(client), + ], + }); + }); + + it("subscriptionService.pay token error", async () => { + const client = makeClientWithTokenError(); + const service = createSubscriptionService(client); + await expectTokenFailure(() => + service.pay("sub-1", { + customer_id: "cust-1", + payment_method_id: "pm-1", + payment_method_type: "CARD", + payment_method_provider: "PAGAR_ME", + }), + ); + }); + + // --------------------------------------------------------------- + // DisputeService + // --------------------------------------------------------------- + it("disputeService.list success", async () => { + const client = makeClient(); + const service = createDisputeService(client); + await expectSuccess({ + client, + call: () => service.list(), + httpMethod: "get", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/disputes`, + getAuthConfig(client), + ], + }); + }); + + it("disputeService.list token error", async () => { + const client = makeClientWithTokenError(); + const service = createDisputeService(client); + await expectTokenFailure(() => service.list()); + }); + + it("disputeService.updateEvidence success", async () => { + const client = makeClient(); + const service = createDisputeService(client); + await expectSuccess({ + client, + call: () => + service.updateEvidence("d-1", { + text_evidences: [{ key: "customer_name", value: "Test" }], + }), + httpMethod: "put", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/disputes/d-1/evidence`, + expect.any(Object), + getAuthConfig(client), + ], + }); + }); + + it("disputeService.updateEvidence failure", async () => { + const client = makeClient(); + const service = createDisputeService(client); + await expectFailure({ + call: () => service.updateEvidence("d-1", {}), + httpMethod: "put", + errorMessage: "Failed to update evidence", + }); + }); + + it("disputeService.submit success", async () => { + const client = makeClient(); + const service = createDisputeService(client); + await expectSuccess({ + client, + call: () => service.submit("d-1"), + httpMethod: "put", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/disputes/d-1/submit`, + undefined, + getAuthConfig(client), + ], + }); + }); + + it("disputeService.close success", async () => { + const client = makeClient(); + const service = createDisputeService(client); + await expectSuccess({ + client, + call: () => service.close("d-1"), + httpMethod: "put", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/disputes/d-1/close`, + undefined, + getAuthConfig(client), + ], + }); + }); + + it("disputeService.close token error", async () => { + const client = makeClientWithTokenError(); + const service = createDisputeService(client); + await expectTokenFailure(() => service.close("d-1")); + }); + + // --------------------------------------------------------------- + // PayoutService + // --------------------------------------------------------------- + it("payoutService.create success", async () => { + const client = makeClient(); + const service = createPayoutService(client); + await expectSuccess({ + client, + call: () => + service.create({ + payment_method_id: "pm-1", + amount: 1000, + currency: "USD", + customer_id: "cust-1", + }), + httpMethod: "post", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/outbound_payments`, + expect.any(Object), + getAuthConfig(client), + ], + }); + }); + + it("payoutService.create failure", async () => { + const client = makeClient(); + const service = createPayoutService(client); + await expectFailure({ + call: () => + service.create({ + payment_method_id: "x", + amount: 1000, + currency: "USD", + customer_id: "x", + }), + httpMethod: "post", + errorMessage: "Failed to create payout", + }); + }); + + it("payoutService.create token error", async () => { + const client = makeClientWithTokenError(); + const service = createPayoutService(client); + await expectTokenFailure(() => + service.create({ + payment_method_id: "x", + amount: 1000, + currency: "USD", + customer_id: "x", + }), + ); + }); + + // --------------------------------------------------------------- + // WalletService + // --------------------------------------------------------------- + it("walletService.getBalance success", async () => { + const client = makeClient(); + const service = createWalletService(client); + await expectSuccess({ + client, + call: () => service.getBalance("cust-1"), + httpMethod: "get", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/wallets/cust-1/balance`, + getAuthConfig(client), + ], + }); + }); + + it("walletService.getBalance failure", async () => { + const client = makeClient(); + const service = createWalletService(client); + await expectFailure({ + call: () => service.getBalance("cust-1"), + httpMethod: "get", + errorMessage: "Balance not found", + }); + }); + + it("walletService.getBalance token error", async () => { + const client = makeClientWithTokenError(); + const service = createWalletService(client); + await expectTokenFailure(() => service.getBalance("cust-1")); + }); + + // --------------------------------------------------------------- + // MerchantService + // --------------------------------------------------------------- + it("merchantService.calculateTransferDate success", async () => { + const client = makeClient(); + const service = createMerchantService(client); + await expectSuccess({ + client, + call: () => + service.calculateTransferDate({ + settlementDate: "2026-05-01", + region: "US", + }), + httpMethod: "post", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/merchant/util/transfer-date`, + { settlementDate: "2026-05-01", region: "US" }, + getAuthConfig(client), + ], + }); + }); + + it("merchantService.calculateTransferDate failure", async () => { + const client = makeClient(); + const service = createMerchantService(client); + await expectFailure({ + call: () => + service.calculateTransferDate({ + settlementDate: "invalid", + region: "XX", + }), + httpMethod: "post", + errorMessage: "Failed to calculate transfer date", + }); + }); + + it("merchantService.calculateTransferDate token error", async () => { + const client = makeClientWithTokenError(); + const service = createMerchantService(client); + await expectTokenFailure(() => + service.calculateTransferDate({ + settlementDate: "2026-05-01", + region: "US", + }), + ); + }); + + // --------------------------------------------------------------- + // FileService + // --------------------------------------------------------------- + it("fileService.upload success", async () => { + const client = makeClient(); + const service = createFileService(client); + await expectSuccess({ + client, + call: () => service.upload({}), + httpMethod: "postMultipart", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/files`, + {}, + getAuthConfig(client), + ], + }); + }); + + it("fileService.upload token error", async () => { + const client = makeClientWithTokenError(); + const service = createFileService(client); + await expectTokenFailure(() => service.upload({})); + }); + + it("fileService.list success", async () => { + const client = makeClient(); + const service = createFileService(client); + await expectSuccess({ + client, + call: () => service.list(), + httpMethod: "get", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/files`, + getAuthConfig(client), + ], + }); + }); + + it("fileService.list token error", async () => { + const client = makeClientWithTokenError(); + const service = createFileService(client); + await expectTokenFailure(() => service.list()); + }); + + it("fileService.get success", async () => { + const client = makeClient(); + const service = createFileService(client); + await expectSuccess({ + client, + call: () => service.get("f-1"), + httpMethod: "get", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/files/f-1`, + getAuthConfig(client), + ], + }); + }); + + it("fileService.get failure", async () => { + const client = makeClient(); + const service = createFileService(client); + await expectFailure({ + call: () => service.get("f-1"), + httpMethod: "get", + errorMessage: "File not found", + }); + }); + + it("fileService.delete success", async () => { + const client = makeClient(); + const service = createFileService(client); + await expectSuccess({ + client, + call: () => service.delete("f-1"), + httpMethod: "delete", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/files/f-1`, + getAuthConfig(client), + ], + }); + }); + + it("fileService.delete token error", async () => { + const client = makeClientWithTokenError(); + const service = createFileService(client); + await expectTokenFailure(() => service.delete("f-1")); + }); + + // --------------------------------------------------------------- + // TaxService + // --------------------------------------------------------------- + it("taxService.calculate success", async () => { + const client = makeClient(); + const service = createTaxService(client); + await expectSuccess({ + client, + call: () => service.calculate({ provider: "stripe" }), + httpMethod: "post", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/taxes/calculate`, + { provider: "stripe" }, + getAuthConfig(client), + ], + }); + }); + + it("taxService.calculate failure", async () => { + const client = makeClient(); + const service = createTaxService(client); + await expectFailure({ + call: () => service.calculate({ provider: "stripe" }), + httpMethod: "post", + errorMessage: "Failed to calculate taxes", + }); + }); + + it("taxService.calculate token error", async () => { + const client = makeClientWithTokenError(); + const service = createTaxService(client); + await expectTokenFailure(() => + service.calculate({ provider: "stripe" }), + ); + }); + + // --------------------------------------------------------------- + // ProviderProxyService + // --------------------------------------------------------------- + it("providerProxyService.proxy success", async () => { + const client = makeClient(); + const service = createProviderProxyService(client); + await expectSuccess({ + client, + call: () => + service.proxy("stripe", { + method: "GET", + url: "https://example.com", + }), + httpMethod: "post", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/providers/stripe/proxy`, + { method: "GET", url: "https://example.com" }, + getAuthConfig(client), + ], + }); + }); + + it("providerProxyService.proxy failure", async () => { + const client = makeClient(); + const service = createProviderProxyService(client); + await expectFailure({ + call: () => + service.proxy("stripe", { method: "GET", url: "invalid" }), + httpMethod: "post", + errorMessage: "URL not whitelisted", + }); + }); + + it("providerProxyService.proxy token error", async () => { + const client = makeClientWithTokenError(); + const service = createProviderProxyService(client); + await expectTokenFailure(() => + service.proxy("stripe", { method: "GET", url: "https://example.com" }), + ); + }); + + // --------------------------------------------------------------- + // PixService + // --------------------------------------------------------------- + it("pixService.createPaid success", async () => { + const client = makeClient(); + const service = createPixService(client); + await expectSuccess({ + client, + call: () => + service.createPaid({ + pix_string: "test", + pix_string_type: "BR_CODE", + amount: 100, + }), + httpMethod: "post", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/pix/paid`, + { pix_string: "test", pix_string_type: "BR_CODE", amount: 100 }, + getAuthConfig(client), + ], + }); + }); + + it("pixService.createPaid failure", async () => { + const client = makeClient(); + const service = createPixService(client); + await expectFailure({ + call: () => + service.createPaid({ + pix_string: "test", + pix_string_type: "BR_CODE", + amount: 100, + }), + httpMethod: "post", + errorMessage: "Failed to create PIX payment", + }); + }); + + it("pixService.createPaid token error", async () => { + const client = makeClientWithTokenError(); + const service = createPixService(client); + await expectTokenFailure(() => + service.createPaid({ + pix_string: "test", + pix_string_type: "BR_CODE", + amount: 100, + }), + ); + }); + + // --------------------------------------------------------------- + // SandboxService + // --------------------------------------------------------------- + it("sandboxService.simulateWebhook success", async () => { + const client = makeClient(); + const service = createSandboxService(client); + await expectSuccess({ + client, + call: () => + service.simulateWebhook("stripe", { + category: "payment_lifecycle", + }), + httpMethod: "post", + expectedArgs: [ + `${SANDBOX_URL}/api/v1/sandbox/webhooks/stripe/simulate`, + { category: "payment_lifecycle" }, + getAuthConfig(client), + ], + }); + }); + + it("sandboxService.simulateWebhook failure", async () => { + const client = makeClient(); + const service = createSandboxService(client); + await expectFailure({ + call: () => + service.simulateWebhook("stripe", { category: "invalid" }), + httpMethod: "post", + errorMessage: "Failed to simulate webhook", + }); + }); + + it("sandboxService.simulateWebhook token error", async () => { + const client = makeClientWithTokenError(); + const service = createSandboxService(client); + await expectTokenFailure(() => + service.simulateWebhook("stripe", { category: "payment_lifecycle" }), + ); + }); +}); diff --git a/packages/payments/__tests__/unit/webhookVerification.test.ts b/packages/payments/__tests__/unit/webhookVerification.test.ts index d0db8ed2..d10850fb 100644 --- a/packages/payments/__tests__/unit/webhookVerification.test.ts +++ b/packages/payments/__tests__/unit/webhookVerification.test.ts @@ -15,42 +15,67 @@ describe("webhookVerification", () => { return hmac.digest("hex"); }; + // Helper to generate CrowdSplit-Signature header format + const generateCrowdSplitSignature = ( + data: string, + webhookSecret: string, + timestamp: string, + ): string => { + const base = `${timestamp}.${data}`; + const hmac = createHmac("sha256", webhookSecret); + hmac.update(base, "utf8"); + const sig = hmac.digest("hex"); + return `t=${timestamp},v1=${sig}`; + }; + describe("verifyWebhookSignature", () => { - it("should verify valid signature", () => { - const signature = generateSignature(payload, secret); + it("should verify valid CrowdSplit-Signature format (t=,v1=)", () => { + const timestamp = Math.floor(Date.now() / 1000).toString(); + const signature = generateCrowdSplitSignature(payload, secret, timestamp); const result = verifyWebhookSignature(payload, signature, secret); expect(result).toBe(true); }); - it("should reject invalid signature", () => { - const invalidSignature = "invalid-signature-12345"; - const result = verifyWebhookSignature(payload, invalidSignature, secret); + it("should reject invalid CrowdSplit-Signature", () => { + const signature = "t=12345,v1=invalidsignature"; + const result = verifyWebhookSignature(payload, signature, secret); expect(result).toBe(false); }); - it("should reject signature with wrong secret", () => { - const signature = generateSignature(payload, "wrong-secret"); + it("should reject CrowdSplit-Signature with wrong secret", () => { + const timestamp = Math.floor(Date.now() / 1000).toString(); + const signature = generateCrowdSplitSignature(payload, "wrong-secret", timestamp); const result = verifyWebhookSignature(payload, signature, secret); expect(result).toBe(false); }); - it("should reject signature with tampered payload", () => { - const signature = generateSignature(payload, secret); + it("should reject CrowdSplit-Signature with tampered payload", () => { + const timestamp = Math.floor(Date.now() / 1000).toString(); + const signature = generateCrowdSplitSignature(payload, secret, timestamp); const tamperedPayload = JSON.stringify({ event: "payment.created", id: "pay_999", }); - const result = verifyWebhookSignature( - tamperedPayload, - signature, - secret, - ); + const result = verifyWebhookSignature(tamperedPayload, signature, secret); expect(result).toBe(false); }); - it("should handle empty payload", () => { + it("should verify legacy raw signature format (fallback)", () => { + const signature = generateSignature(payload, secret); + const result = verifyWebhookSignature(payload, signature, secret); + expect(result).toBe(true); + }); + + it("should reject invalid legacy signature", () => { + const invalidSignature = "invalid-signature-12345"; + const result = verifyWebhookSignature(payload, invalidSignature, secret); + expect(result).toBe(false); + }); + + it("should handle empty payload with CrowdSplit-Signature", () => { const emptyPayload = ""; - const signature = generateSignature(emptyPayload, secret); + const timestamp = "1234567890"; + const signature = generateCrowdSplitSignature(emptyPayload, secret, timestamp); const result = verifyWebhookSignature(emptyPayload, signature, secret); expect(result).toBe(true); }); @@ -60,6 +85,12 @@ describe("webhookVerification", () => { const result = verifyWebhookSignature(payload, shortSignature, secret); expect(result).toBe(false); }); + + it("should handle CrowdSplit-Signature with empty timestamp", () => { + const signature = "t=,v1=somesig"; + const result = verifyWebhookSignature(payload, signature, secret); + expect(result).toBe(false); + }); }); describe("parseWebhookPayload", () => { @@ -68,8 +99,9 @@ describe("webhookVerification", () => { id: string; } - it("should parse and verify valid webhook", () => { - const signature = generateSignature(payload, secret); + it("should parse and verify valid webhook with CrowdSplit-Signature", () => { + const timestamp = Math.floor(Date.now() / 1000).toString(); + const signature = generateCrowdSplitSignature(payload, secret, timestamp); const result = parseWebhookPayload(payload, signature, secret); expect(result.ok).toBe(true); @@ -96,7 +128,8 @@ describe("webhookVerification", () => { it("should reject invalid JSON", () => { const invalidPayload = "{ invalid json }"; - const signature = generateSignature(invalidPayload, secret); + const timestamp = Math.floor(Date.now() / 1000).toString(); + const signature = generateCrowdSplitSignature(invalidPayload, secret, timestamp); const result = parseWebhookPayload( invalidPayload, signature, @@ -121,7 +154,8 @@ describe("webhookVerification", () => { }, }, }); - const signature = generateSignature(complexPayload, secret); + const timestamp = Math.floor(Date.now() / 1000).toString(); + const signature = generateCrowdSplitSignature(complexPayload, secret, timestamp); const result = parseWebhookPayload(complexPayload, signature, secret); expect(result.ok).toBe(true); From 0c078689c98a4d4a88db7ed6bb20a1f97e2d6f0e Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 20:02:08 +0600 Subject: [PATCH 16/18] fix: resolve PR review comments --- packages/payments/__tests__/config.ts | 2 ++ .../payments/__tests__/integration/disputeService.test.ts | 4 +--- .../payments/__tests__/integration/missingServices.test.ts | 4 +--- .../__tests__/integration/paymentServiceCapture.test.ts | 4 +--- .../payments/__tests__/integration/payoutService.test.ts | 4 +--- .../__tests__/integration/subscriptionService.test.ts | 4 +--- packages/payments/src/services/customerService.ts | 3 +++ packages/payments/src/services/disputeService.ts | 4 ++++ packages/payments/src/services/paymentService.ts | 3 +++ packages/payments/src/services/subscriptionService.ts | 7 +------ packages/payments/src/utils/httpClient.ts | 7 ++----- packages/payments/src/utils/webhookVerification.ts | 4 ++-- 12 files changed, 22 insertions(+), 28 deletions(-) diff --git a/packages/payments/__tests__/config.ts b/packages/payments/__tests__/config.ts index 2bc7bc1a..91a994a1 100644 --- a/packages/payments/__tests__/config.ts +++ b/packages/payments/__tests__/config.ts @@ -41,6 +41,8 @@ export function getConfigFromEnv(): OakClientConfig { }; } +export const INTEGRATION_TEST_TIMEOUT = 30000; + export function getTestEnvironment(): TestEnvironment { return { paymentCustomerId: process.env.PAYMENT_CUSTOMER_ID, diff --git a/packages/payments/__tests__/integration/disputeService.test.ts b/packages/payments/__tests__/integration/disputeService.test.ts index f45c27c6..5d018b4d 100644 --- a/packages/payments/__tests__/integration/disputeService.test.ts +++ b/packages/payments/__tests__/integration/disputeService.test.ts @@ -1,8 +1,6 @@ import { createOakClient, createDisputeService } from '../../src'; import { DisputeService } from '../../src/services/disputeService'; -import { getConfigFromEnv } from '../config'; - -const INTEGRATION_TEST_TIMEOUT = 30000; +import { getConfigFromEnv, INTEGRATION_TEST_TIMEOUT } from '../config'; describe('DisputeService - Integration', () => { let disputes: DisputeService; diff --git a/packages/payments/__tests__/integration/missingServices.test.ts b/packages/payments/__tests__/integration/missingServices.test.ts index b1ec8278..75bdfe90 100644 --- a/packages/payments/__tests__/integration/missingServices.test.ts +++ b/packages/payments/__tests__/integration/missingServices.test.ts @@ -29,9 +29,7 @@ import { SubscriptionService } from '../../src/services/subscriptionService'; import { DisputeService } from '../../src/services/disputeService'; import { PayoutService } from '../../src/services/payoutService'; import { PaymentService } from '../../src/services/paymentService'; -import { getConfigFromEnv } from '../config'; - -const INTEGRATION_TEST_TIMEOUT = 30000; +import { getConfigFromEnv, INTEGRATION_TEST_TIMEOUT } from '../config'; describe('All New Services - Integration', () => { let customers: CustomerService; diff --git a/packages/payments/__tests__/integration/paymentServiceCapture.test.ts b/packages/payments/__tests__/integration/paymentServiceCapture.test.ts index b7185d6b..86c639f1 100644 --- a/packages/payments/__tests__/integration/paymentServiceCapture.test.ts +++ b/packages/payments/__tests__/integration/paymentServiceCapture.test.ts @@ -6,9 +6,7 @@ import { } from '../../src'; import { PaymentService } from '../../src/services/paymentService'; import { CustomerService } from '../../src/services/customerService'; -import { getConfigFromEnv } from '../config'; - -const INTEGRATION_TEST_TIMEOUT = 30000; +import { getConfigFromEnv, INTEGRATION_TEST_TIMEOUT } from '../config'; /** * Build a Stripe card payment request with manual capture. diff --git a/packages/payments/__tests__/integration/payoutService.test.ts b/packages/payments/__tests__/integration/payoutService.test.ts index 2aba01c1..008c1ae1 100644 --- a/packages/payments/__tests__/integration/payoutService.test.ts +++ b/packages/payments/__tests__/integration/payoutService.test.ts @@ -1,8 +1,6 @@ import { createOakClient, createPayoutService } from '../../src'; import { PayoutService } from '../../src/services/payoutService'; -import { getConfigFromEnv } from '../config'; - -const INTEGRATION_TEST_TIMEOUT = 30000; +import { getConfigFromEnv, INTEGRATION_TEST_TIMEOUT } from '../config'; describe('PayoutService - Integration', () => { let payouts: PayoutService; diff --git a/packages/payments/__tests__/integration/subscriptionService.test.ts b/packages/payments/__tests__/integration/subscriptionService.test.ts index b48451c9..e02e321a 100644 --- a/packages/payments/__tests__/integration/subscriptionService.test.ts +++ b/packages/payments/__tests__/integration/subscriptionService.test.ts @@ -5,9 +5,7 @@ import { } from '../../src'; import { SubscriptionService } from '../../src/services/subscriptionService'; import { PlanService } from '../../src/services/planService'; -import { getConfigFromEnv } from '../config'; - -const INTEGRATION_TEST_TIMEOUT = 30000; +import { getConfigFromEnv, INTEGRATION_TEST_TIMEOUT } from '../config'; describe('SubscriptionService - Integration', () => { let subscriptions: SubscriptionService; diff --git a/packages/payments/src/services/customerService.ts b/packages/payments/src/services/customerService.ts index f6e2d22b..86dd5b2a 100644 --- a/packages/payments/src/services/customerService.ts +++ b/packages/payments/src/services/customerService.ts @@ -22,13 +22,16 @@ export interface CustomerService { filter: Customer.BalanceFilter, ): Promise>; + /** Upload files for a customer (multipart/form-data). */ uploadFiles( customerId: string, files: unknown, ): Promise>; + /** List files for a customer. */ getFiles(customerId: string): Promise>; + /** Populate KYC data for a customer on a specific provider platform. */ populatePlatform( customerId: string, data: Customer.PlatformRequest, diff --git a/packages/payments/src/services/disputeService.ts b/packages/payments/src/services/disputeService.ts index 18fa73a8..2931d973 100644 --- a/packages/payments/src/services/disputeService.ts +++ b/packages/payments/src/services/disputeService.ts @@ -4,12 +4,16 @@ import { withAuth } from "../utils/withAuth"; import { buildUrl } from "../utils/buildUrl"; export interface DisputeService { + /** List all disputes for the merchant. */ list(): Promise>; + /** Update evidence for a dispute. */ updateEvidence( disputeId: string, evidence: Dispute.EvidenceRequest, ): Promise>; + /** Submit a dispute for review. */ submit(disputeId: string): Promise>; + /** Close a dispute. */ close(disputeId: string): Promise>; } diff --git a/packages/payments/src/services/paymentService.ts b/packages/payments/src/services/paymentService.ts index 3fbf0646..cf71c0d0 100644 --- a/packages/payments/src/services/paymentService.ts +++ b/packages/payments/src/services/paymentService.ts @@ -7,8 +7,11 @@ export interface PaymentService { create(payment: Payment.Request): Promise>; confirm(paymentId: string): Promise>; cancel(paymentId: string): Promise>; + /** Capture an authorized payment (manual capture flow). */ capture(paymentId: string): Promise>; + /** Mark a payment as paid in sandbox environment. */ sandboxPaid(paymentId: string): Promise>; + /** Simulate settlement for a payment in sandbox environment. */ sandboxSettle(paymentId: string): Promise>; } diff --git a/packages/payments/src/services/subscriptionService.ts b/packages/payments/src/services/subscriptionService.ts index e512646c..4837b748 100644 --- a/packages/payments/src/services/subscriptionService.ts +++ b/packages/payments/src/services/subscriptionService.ts @@ -64,20 +64,15 @@ export const createSubscriptionService = ( async list( params: Subscription.ListQuery, ): Promise> { - const { customer_id, ...queryParams } = params; - const queryString = buildQueryString( - Object.keys(queryParams).length > 0 ? queryParams : undefined, - ); + const queryString = buildQueryString(params); return withAuth(client, (token) => httpClient.get( `${buildUrl(client.config.baseUrl, "api/v1/subscription/list")}${queryString}`, { headers: { Authorization: `Bearer ${token}`, - "Content-Type": "application/json", }, retryOptions: client.retryOptions, - body: JSON.stringify({ customer_id }), }, ), ); diff --git a/packages/payments/src/utils/httpClient.ts b/packages/payments/src/utils/httpClient.ts index 9093cea4..2d9c6e19 100644 --- a/packages/payments/src/utils/httpClient.ts +++ b/packages/payments/src/utils/httpClient.ts @@ -191,11 +191,8 @@ export const httpClient = { * @param config - HTTP client configuration * @returns Result containing parsed response or error */ - async get(url: string, config: HttpClientConfig & { body?: string }): Promise> { - return request(url, config, { - method: "GET", - ...(config.body ? { body: config.body } : {}), - }); + async get(url: string, config: HttpClientConfig): Promise> { + return request(url, config, { method: "GET" }); }, /** * @typeParam T - Expected response body type diff --git a/packages/payments/src/utils/webhookVerification.ts b/packages/payments/src/utils/webhookVerification.ts index df9dbc77..992b674d 100644 --- a/packages/payments/src/utils/webhookVerification.ts +++ b/packages/payments/src/utils/webhookVerification.ts @@ -35,8 +35,8 @@ export function verifyWebhookSignature( if (signature.startsWith("t=")) { const parts = signature.split(","); - timestamp = parts[0]?.replace("t=", "") ?? ""; - v1Signature = parts[1]?.replace("v1=", "") ?? ""; + timestamp = (parts[0]?.replace("t=", "") ?? "").trim(); + v1Signature = (parts[1]?.replace("v1=", "") ?? "").trim(); } else { // Fallback: treat as raw signature (legacy) timestamp = ""; From 756baa28ff752fa1c4b34147eee6652bdf7b8612 Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 20:42:15 +0600 Subject: [PATCH 17/18] fix: remove wallet, merchant, providerproxy,pix and sandbox service --- .../integration/missingServices.test.ts | 143 ------------ .../__tests__/unit/newServices.test.ts | 215 ------------------ packages/payments/src/services/index.ts | 15 -- .../payments/src/services/merchantService.ts | 33 --- packages/payments/src/services/pixService.ts | 29 --- .../src/services/providerProxyService.ts | 35 --- .../payments/src/services/sandboxService.ts | 38 ---- .../payments/src/services/walletService.ts | 28 --- packages/payments/src/types/index.ts | 5 - packages/payments/src/types/merchant.ts | 14 -- packages/payments/src/types/pix.ts | 11 - packages/payments/src/types/providerProxy.ts | 12 - packages/payments/src/types/sandbox.ts | 15 -- packages/payments/src/types/wallet.ts | 14 -- 14 files changed, 607 deletions(-) delete mode 100644 packages/payments/src/services/merchantService.ts delete mode 100644 packages/payments/src/services/pixService.ts delete mode 100644 packages/payments/src/services/providerProxyService.ts delete mode 100644 packages/payments/src/services/sandboxService.ts delete mode 100644 packages/payments/src/services/walletService.ts delete mode 100644 packages/payments/src/types/merchant.ts delete mode 100644 packages/payments/src/types/pix.ts delete mode 100644 packages/payments/src/types/providerProxy.ts delete mode 100644 packages/payments/src/types/sandbox.ts delete mode 100644 packages/payments/src/types/wallet.ts diff --git a/packages/payments/__tests__/integration/missingServices.test.ts b/packages/payments/__tests__/integration/missingServices.test.ts index 75bdfe90..7e5fc006 100644 --- a/packages/payments/__tests__/integration/missingServices.test.ts +++ b/packages/payments/__tests__/integration/missingServices.test.ts @@ -1,13 +1,8 @@ import { createOakClient, createCustomerService, - createWalletService, - createMerchantService, createFileService, createTaxService, - createProviderProxyService, - createPixService, - createSandboxService, createTransferService, createPlanService, createSubscriptionService, @@ -16,13 +11,8 @@ import { createPaymentService, } from '../../src'; import { CustomerService } from '../../src/services/customerService'; -import { WalletService } from '../../src/services/walletService'; -import { MerchantService } from '../../src/services/merchantService'; import { FileService } from '../../src/services/fileService'; import { TaxService } from '../../src/services/taxService'; -import { ProviderProxyService } from '../../src/services/providerProxyService'; -import { PixService } from '../../src/services/pixService'; -import { SandboxService } from '../../src/services/sandboxService'; import { TransferService } from '../../src/services/transferService'; import { PlanService } from '../../src/services/planService'; import { SubscriptionService } from '../../src/services/subscriptionService'; @@ -33,13 +23,8 @@ import { getConfigFromEnv, INTEGRATION_TEST_TIMEOUT } from '../config'; describe('All New Services - Integration', () => { let customers: CustomerService; - let wallets: WalletService; - let merchant: MerchantService; let files: FileService; let taxes: TaxService; - let providerProxy: ProviderProxyService; - let pix: PixService; - let sandbox: SandboxService; let transfers: TransferService; let plans: PlanService; let subscriptions: SubscriptionService; @@ -60,13 +45,8 @@ describe('All New Services - Integration', () => { }, }); customers = createCustomerService(client); - wallets = createWalletService(client); - merchant = createMerchantService(client); files = createFileService(client); taxes = createTaxService(client); - providerProxy = createProviderProxyService(client); - pix = createPixService(client); - sandbox = createSandboxService(client); transfers = createTransferService(client); plans = createPlanService(client); subscriptions = createSubscriptionService(client); @@ -353,72 +333,6 @@ describe('All New Services - Integration', () => { ); }); - // --------------------------------------------------------------- - // WalletService - // --------------------------------------------------------------- - describe('WalletService', () => { - it( - 'should return error for getBalance with non-existent customer', - async () => { - const response = await wallets.getBalance('non-existent-id'); - expect(response.ok).toBe(false); - if (!response.ok) { - expect(response.error).toBeDefined(); - } - }, - INTEGRATION_TEST_TIMEOUT, - ); - - it( - 'should attempt getBalance for existing customer', - async () => { - if (!existingCustomerId) { - throw new Error('No customer available'); - } - const response = await wallets.getBalance(existingCustomerId); - // May return 404 if no trading wallet — both outcomes valid - expect(response).toBeDefined(); - }, - INTEGRATION_TEST_TIMEOUT, - ); - }); - - // --------------------------------------------------------------- - // MerchantService - // --------------------------------------------------------------- - describe('MerchantService', () => { - it( - 'should calculate transfer date with valid params', - async () => { - const response = await merchant.calculateTransferDate({ - settlementDate: '2026-06-01', - region: 'US', - }); - expect(response).toBeDefined(); - if (response.ok) { - expect(response.value).toBeDefined(); - expect(response.value.data).toBeDefined(); - } - }, - INTEGRATION_TEST_TIMEOUT, - ); - - it( - 'should return error for invalid region format', - async () => { - const response = await merchant.calculateTransferDate({ - settlementDate: '2026-06-01', - region: 'INVALID_TOO_LONG', - }); - expect(response.ok).toBe(false); - if (!response.ok) { - expect(response.error).toBeDefined(); - } - }, - INTEGRATION_TEST_TIMEOUT, - ); - }); - // --------------------------------------------------------------- // FileService // --------------------------------------------------------------- @@ -471,63 +385,6 @@ describe('All New Services - Integration', () => { ); }); - // --------------------------------------------------------------- - // ProviderProxyService - // --------------------------------------------------------------- - describe('ProviderProxyService', () => { - it( - 'should return error for non-whitelisted URL', - async () => { - const response = await providerProxy.proxy('stripe', { - method: 'GET', - url: 'https://invalid-not-whitelisted.example.com', - }); - expect(response.ok).toBe(false); - if (!response.ok) { - expect(response.error).toBeDefined(); - } - }, - INTEGRATION_TEST_TIMEOUT, - ); - }); - - // --------------------------------------------------------------- - // PixService - // --------------------------------------------------------------- - describe('PixService', () => { - it( - 'should call pix paid endpoint', - async () => { - const response = await pix.createPaid({ - pix_string: 'test-pix-string', - pix_string_type: 'BR_CODE', - amount: 100, - }); - expect(response).toBeDefined(); - }, - INTEGRATION_TEST_TIMEOUT, - ); - }); - - // --------------------------------------------------------------- - // SandboxService - // --------------------------------------------------------------- - describe('SandboxService', () => { - it( - 'should return error for invalid simulation category', - async () => { - const response = await sandbox.simulateWebhook('stripe', { - category: 'invalid_category', - }); - expect(response.ok).toBe(false); - if (!response.ok) { - expect(response.error).toBeDefined(); - } - }, - INTEGRATION_TEST_TIMEOUT, - ); - }); - // --------------------------------------------------------------- // PlanService - verify new query params // --------------------------------------------------------------- diff --git a/packages/payments/__tests__/unit/newServices.test.ts b/packages/payments/__tests__/unit/newServices.test.ts index 1ac3d1b9..7eb75575 100644 --- a/packages/payments/__tests__/unit/newServices.test.ts +++ b/packages/payments/__tests__/unit/newServices.test.ts @@ -2,13 +2,8 @@ import { createSubscriptionService, createDisputeService, createPayoutService, - createWalletService, - createMerchantService, createFileService, createTaxService, - createProviderProxyService, - createPixService, - createSandboxService, createPaymentService, createCustomerService, createTransferService, @@ -588,86 +583,6 @@ describe("New services (Unit)", () => { ); }); - // --------------------------------------------------------------- - // WalletService - // --------------------------------------------------------------- - it("walletService.getBalance success", async () => { - const client = makeClient(); - const service = createWalletService(client); - await expectSuccess({ - client, - call: () => service.getBalance("cust-1"), - httpMethod: "get", - expectedArgs: [ - `${SANDBOX_URL}/api/v1/wallets/cust-1/balance`, - getAuthConfig(client), - ], - }); - }); - - it("walletService.getBalance failure", async () => { - const client = makeClient(); - const service = createWalletService(client); - await expectFailure({ - call: () => service.getBalance("cust-1"), - httpMethod: "get", - errorMessage: "Balance not found", - }); - }); - - it("walletService.getBalance token error", async () => { - const client = makeClientWithTokenError(); - const service = createWalletService(client); - await expectTokenFailure(() => service.getBalance("cust-1")); - }); - - // --------------------------------------------------------------- - // MerchantService - // --------------------------------------------------------------- - it("merchantService.calculateTransferDate success", async () => { - const client = makeClient(); - const service = createMerchantService(client); - await expectSuccess({ - client, - call: () => - service.calculateTransferDate({ - settlementDate: "2026-05-01", - region: "US", - }), - httpMethod: "post", - expectedArgs: [ - `${SANDBOX_URL}/api/v1/merchant/util/transfer-date`, - { settlementDate: "2026-05-01", region: "US" }, - getAuthConfig(client), - ], - }); - }); - - it("merchantService.calculateTransferDate failure", async () => { - const client = makeClient(); - const service = createMerchantService(client); - await expectFailure({ - call: () => - service.calculateTransferDate({ - settlementDate: "invalid", - region: "XX", - }), - httpMethod: "post", - errorMessage: "Failed to calculate transfer date", - }); - }); - - it("merchantService.calculateTransferDate token error", async () => { - const client = makeClientWithTokenError(); - const service = createMerchantService(client); - await expectTokenFailure(() => - service.calculateTransferDate({ - settlementDate: "2026-05-01", - region: "US", - }), - ); - }); - // --------------------------------------------------------------- // FileService // --------------------------------------------------------------- @@ -792,134 +707,4 @@ describe("New services (Unit)", () => { ); }); - // --------------------------------------------------------------- - // ProviderProxyService - // --------------------------------------------------------------- - it("providerProxyService.proxy success", async () => { - const client = makeClient(); - const service = createProviderProxyService(client); - await expectSuccess({ - client, - call: () => - service.proxy("stripe", { - method: "GET", - url: "https://example.com", - }), - httpMethod: "post", - expectedArgs: [ - `${SANDBOX_URL}/api/v1/providers/stripe/proxy`, - { method: "GET", url: "https://example.com" }, - getAuthConfig(client), - ], - }); - }); - - it("providerProxyService.proxy failure", async () => { - const client = makeClient(); - const service = createProviderProxyService(client); - await expectFailure({ - call: () => - service.proxy("stripe", { method: "GET", url: "invalid" }), - httpMethod: "post", - errorMessage: "URL not whitelisted", - }); - }); - - it("providerProxyService.proxy token error", async () => { - const client = makeClientWithTokenError(); - const service = createProviderProxyService(client); - await expectTokenFailure(() => - service.proxy("stripe", { method: "GET", url: "https://example.com" }), - ); - }); - - // --------------------------------------------------------------- - // PixService - // --------------------------------------------------------------- - it("pixService.createPaid success", async () => { - const client = makeClient(); - const service = createPixService(client); - await expectSuccess({ - client, - call: () => - service.createPaid({ - pix_string: "test", - pix_string_type: "BR_CODE", - amount: 100, - }), - httpMethod: "post", - expectedArgs: [ - `${SANDBOX_URL}/api/v1/pix/paid`, - { pix_string: "test", pix_string_type: "BR_CODE", amount: 100 }, - getAuthConfig(client), - ], - }); - }); - - it("pixService.createPaid failure", async () => { - const client = makeClient(); - const service = createPixService(client); - await expectFailure({ - call: () => - service.createPaid({ - pix_string: "test", - pix_string_type: "BR_CODE", - amount: 100, - }), - httpMethod: "post", - errorMessage: "Failed to create PIX payment", - }); - }); - - it("pixService.createPaid token error", async () => { - const client = makeClientWithTokenError(); - const service = createPixService(client); - await expectTokenFailure(() => - service.createPaid({ - pix_string: "test", - pix_string_type: "BR_CODE", - amount: 100, - }), - ); - }); - - // --------------------------------------------------------------- - // SandboxService - // --------------------------------------------------------------- - it("sandboxService.simulateWebhook success", async () => { - const client = makeClient(); - const service = createSandboxService(client); - await expectSuccess({ - client, - call: () => - service.simulateWebhook("stripe", { - category: "payment_lifecycle", - }), - httpMethod: "post", - expectedArgs: [ - `${SANDBOX_URL}/api/v1/sandbox/webhooks/stripe/simulate`, - { category: "payment_lifecycle" }, - getAuthConfig(client), - ], - }); - }); - - it("sandboxService.simulateWebhook failure", async () => { - const client = makeClient(); - const service = createSandboxService(client); - await expectFailure({ - call: () => - service.simulateWebhook("stripe", { category: "invalid" }), - httpMethod: "post", - errorMessage: "Failed to simulate webhook", - }); - }); - - it("sandboxService.simulateWebhook token error", async () => { - const client = makeClientWithTokenError(); - const service = createSandboxService(client); - await expectTokenFailure(() => - service.simulateWebhook("stripe", { category: "payment_lifecycle" }), - ); - }); }); diff --git a/packages/payments/src/services/index.ts b/packages/payments/src/services/index.ts index 6788c6fa..e328b1c9 100644 --- a/packages/payments/src/services/index.ts +++ b/packages/payments/src/services/index.ts @@ -40,23 +40,8 @@ export type { DisputeService } from "./disputeService"; export { createPayoutService } from "./payoutService"; export type { PayoutService } from "./payoutService"; -export { createWalletService } from "./walletService"; -export type { WalletService } from "./walletService"; - -export { createMerchantService } from "./merchantService"; -export type { MerchantService } from "./merchantService"; - export { createFileService } from "./fileService"; export type { FileService } from "./fileService"; export { createTaxService } from "./taxService"; export type { TaxService } from "./taxService"; - -export { createProviderProxyService } from "./providerProxyService"; -export type { ProviderProxyService } from "./providerProxyService"; - -export { createPixService } from "./pixService"; -export type { PixService } from "./pixService"; - -export { createSandboxService } from "./sandboxService"; -export type { SandboxService } from "./sandboxService"; diff --git a/packages/payments/src/services/merchantService.ts b/packages/payments/src/services/merchantService.ts deleted file mode 100644 index 5a0b04dd..00000000 --- a/packages/payments/src/services/merchantService.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Merchant, OakClient, Result } from "../types"; -import { httpClient } from "../utils/httpClient"; -import { withAuth } from "../utils/withAuth"; -import { buildUrl } from "../utils/buildUrl"; - -export interface MerchantService { - calculateTransferDate( - request: Merchant.TransferDateRequest, - ): Promise>; -} - -/** - * @param client - Configured OakClient instance - * @returns MerchantService instance - */ -export const createMerchantService = ( - client: OakClient, -): MerchantService => ({ - async calculateTransferDate( - request: Merchant.TransferDateRequest, - ): Promise> { - return withAuth(client, (token) => - httpClient.post( - buildUrl(client.config.baseUrl, "api/v1/merchant/util/transfer-date"), - request, - { - headers: { Authorization: `Bearer ${token}` }, - retryOptions: client.retryOptions, - }, - ), - ); - }, -}); diff --git a/packages/payments/src/services/pixService.ts b/packages/payments/src/services/pixService.ts deleted file mode 100644 index f723d701..00000000 --- a/packages/payments/src/services/pixService.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Pix, OakClient, Result } from "../types"; -import { httpClient } from "../utils/httpClient"; -import { withAuth } from "../utils/withAuth"; -import { buildUrl } from "../utils/buildUrl"; - -export interface PixService { - createPaid(request: Pix.PaidRequest): Promise>; -} - -/** - * @param client - Configured OakClient instance - * @returns PixService instance - */ -export const createPixService = (client: OakClient): PixService => ({ - async createPaid( - request: Pix.PaidRequest, - ): Promise> { - return withAuth(client, (token) => - httpClient.post( - buildUrl(client.config.baseUrl, "api/v1/pix/paid"), - request, - { - headers: { Authorization: `Bearer ${token}` }, - retryOptions: client.retryOptions, - }, - ), - ); - }, -}); diff --git a/packages/payments/src/services/providerProxyService.ts b/packages/payments/src/services/providerProxyService.ts deleted file mode 100644 index 567066b3..00000000 --- a/packages/payments/src/services/providerProxyService.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { ProviderProxy, OakClient, Result } from "../types"; -import { httpClient } from "../utils/httpClient"; -import { withAuth } from "../utils/withAuth"; -import { buildUrl } from "../utils/buildUrl"; - -export interface ProviderProxyService { - proxy( - provider: string, - request: ProviderProxy.Request, - ): Promise>; -} - -/** - * @param client - Configured OakClient instance - * @returns ProviderProxyService instance - */ -export const createProviderProxyService = ( - client: OakClient, -): ProviderProxyService => ({ - async proxy( - provider: string, - request: ProviderProxy.Request, - ): Promise> { - return withAuth(client, (token) => - httpClient.post( - buildUrl(client.config.baseUrl, "api/v1/providers", provider, "proxy"), - request, - { - headers: { Authorization: `Bearer ${token}` }, - retryOptions: client.retryOptions, - }, - ), - ); - }, -}); diff --git a/packages/payments/src/services/sandboxService.ts b/packages/payments/src/services/sandboxService.ts deleted file mode 100644 index b45689e1..00000000 --- a/packages/payments/src/services/sandboxService.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Sandbox, OakClient, Result } from "../types"; -import { httpClient } from "../utils/httpClient"; -import { withAuth } from "../utils/withAuth"; -import { buildUrl } from "../utils/buildUrl"; - -export interface SandboxService { - simulateWebhook( - provider: string, - request: Sandbox.WebhookSimulationRequest, - ): Promise>; -} - -/** - * @param client - Configured OakClient instance - * @returns SandboxService instance - */ -export const createSandboxService = (client: OakClient): SandboxService => ({ - async simulateWebhook( - provider: string, - request: Sandbox.WebhookSimulationRequest, - ): Promise> { - return withAuth(client, (token) => - httpClient.post( - buildUrl( - client.config.baseUrl, - "api/v1/sandbox/webhooks", - provider, - "simulate", - ), - request, - { - headers: { Authorization: `Bearer ${token}` }, - retryOptions: client.retryOptions, - }, - ), - ); - }, -}); diff --git a/packages/payments/src/services/walletService.ts b/packages/payments/src/services/walletService.ts deleted file mode 100644 index 7226de99..00000000 --- a/packages/payments/src/services/walletService.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Wallet, OakClient, Result } from "../types"; -import { httpClient } from "../utils/httpClient"; -import { withAuth } from "../utils/withAuth"; -import { buildUrl } from "../utils/buildUrl"; - -export interface WalletService { - getBalance(customerId: string): Promise>; -} - -/** - * @param client - Configured OakClient instance - * @returns WalletService instance - */ -export const createWalletService = (client: OakClient): WalletService => ({ - async getBalance( - customerId: string, - ): Promise> { - return withAuth(client, (token) => - httpClient.get( - buildUrl(client.config.baseUrl, "api/v1/wallets", customerId, "balance"), - { - headers: { Authorization: `Bearer ${token}` }, - retryOptions: client.retryOptions, - }, - ), - ); - }, -}); diff --git a/packages/payments/src/types/index.ts b/packages/payments/src/types/index.ts index 8a67828a..e8aa76f2 100644 --- a/packages/payments/src/types/index.ts +++ b/packages/payments/src/types/index.ts @@ -17,10 +17,5 @@ export * from "./refund"; export * from "./subscription"; export * from "./dispute"; export * from "./payout"; -export * from "./wallet"; -export * from "./merchant"; export * from "./file"; export * from "./tax"; -export * from "./providerProxy"; -export * from "./pix"; -export * from "./sandbox"; diff --git a/packages/payments/src/types/merchant.ts b/packages/payments/src/types/merchant.ts deleted file mode 100644 index 0a991c22..00000000 --- a/packages/payments/src/types/merchant.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiResponse } from "./common"; - -export namespace Merchant { - export interface TransferDateRequest { - settlementDate: string; - region: string; - config?: { - weekends?: number[]; - holidays?: string[]; - }; - } - - export type TransferDateResponse = ApiResponse; -} diff --git a/packages/payments/src/types/pix.ts b/packages/payments/src/types/pix.ts deleted file mode 100644 index 36245343..00000000 --- a/packages/payments/src/types/pix.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiResponse } from "./common"; - -export namespace Pix { - export interface PaidRequest { - pix_string: string; - pix_string_type: "BR_CODE" | "PIX_KEY"; - amount: number; - } - - export type PaidResponse = ApiResponse; -} diff --git a/packages/payments/src/types/providerProxy.ts b/packages/payments/src/types/providerProxy.ts deleted file mode 100644 index fbbd3b3d..00000000 --- a/packages/payments/src/types/providerProxy.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ApiResponse } from "./common"; - -export namespace ProviderProxy { - export interface Request { - method: string; - url: string; - body?: unknown; - headers?: Record; - } - - export type Response = ApiResponse; -} diff --git a/packages/payments/src/types/sandbox.ts b/packages/payments/src/types/sandbox.ts deleted file mode 100644 index 26db6829..00000000 --- a/packages/payments/src/types/sandbox.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiResponse } from "./common"; - -export namespace Sandbox { - export interface WebhookSimulationRequest { - category: string; - provider?: string; - provider_data?: { - status?: string; - [key: string]: unknown; - }; - [key: string]: unknown; - } - - export type WebhookSimulationResponse = ApiResponse; -} diff --git a/packages/payments/src/types/wallet.ts b/packages/payments/src/types/wallet.ts deleted file mode 100644 index 85b81e99..00000000 --- a/packages/payments/src/types/wallet.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiResponse } from "./common"; - -export namespace Wallet { - export interface TokenBalance { - tokenName: string; - amount: number; - } - - export interface BalanceData { - balance: TokenBalance[]; - } - - export type BalanceResponse = ApiResponse; -} From 32abcdb78ae6de23d1b207daaa793679addd2ac7 Mon Sep 17 00:00:00 2001 From: fahmidareem3 Date: Wed, 29 Apr 2026 20:48:17 +0600 Subject: [PATCH 18/18] fix: parse webhook signature fields by key prefix --- packages/payments/src/utils/webhookVerification.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/payments/src/utils/webhookVerification.ts b/packages/payments/src/utils/webhookVerification.ts index 992b674d..d721f782 100644 --- a/packages/payments/src/utils/webhookVerification.ts +++ b/packages/payments/src/utils/webhookVerification.ts @@ -33,10 +33,10 @@ export function verifyWebhookSignature( let timestamp: string; let v1Signature: string; - if (signature.startsWith("t=")) { + if (signature.includes("t=") && signature.includes("v1=")) { const parts = signature.split(","); - timestamp = (parts[0]?.replace("t=", "") ?? "").trim(); - v1Signature = (parts[1]?.replace("v1=", "") ?? "").trim(); + timestamp = (parts.find(p => p.trimStart().startsWith("t="))?.replace("t=", "") ?? "").trim(); + v1Signature = (parts.find(p => p.trimStart().startsWith("v1="))?.replace("v1=", "") ?? "").trim(); } else { // Fallback: treat as raw signature (legacy) timestamp = "";