diff --git a/package.json b/package.json index 21a1c45..2d1ced7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@blindpay/cli", "type": "module", - "version": "0.1.1", + "version": "0.2.0", "description": "Blindpay CLI - manage receivers, bank accounts, payouts, payins, and more from the terminal", "license": "MIT", "author": "Blindpay (https://blindpay.com/)", diff --git a/src/__tests__/resources.test.ts b/src/__tests__/resources.test.ts index 1441522..53bd2fa 100644 --- a/src/__tests__/resources.test.ts +++ b/src/__tests__/resources.test.ts @@ -112,31 +112,32 @@ function lastJsonLog(): Record { const BASE = 'https://api.blindpay.com/v1/instances/in_testInstance' -describe('Receivers', () => { +describe('Customers', () => { beforeEach(setupTestEnv) afterEach(teardownTestEnv) - test('lists receivers', async () => { - mockResponse.body = { data: [{ id: 're_1', type: 'individual', email: 'a@b.com' }] } - await resources.listReceivers({ json: true }) + test('lists customers', async () => { + mockResponse.body = { data: [{ id: 'cu_1', type: 'individual', email: 'a@b.com' }] } + await resources.listCustomers({ json: true }) expect(lastCall().method).toBe('GET') - expect(lastCall().url).toBe(`${BASE}/receivers`) + expect(lastCall().url).toBe(`${BASE}/customers`) }) - test('fetches a receiver by id', async () => { - mockResponse.body = { id: 're_xyz' } - await resources.getReceiver('re_xyz', { json: true }) + test('fetches a customer by id', async () => { + mockResponse.body = { id: 'cu_xyz' } + await resources.getCustomer('cu_xyz', { json: true }) expect(lastCall().method).toBe('GET') - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz`) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz`) }) - test('creates a receiver, splitting --name into first_name and last_name and filling defaults', async () => { - mockResponse.body = { id: 're_new', type: 'individual' } - await resources.createReceiver({ email: 'a@b.com', name: 'Jane Doe', json: true }) + test('creates a customer with kyc_type, splitting --name into first_name and last_name', async () => { + mockResponse.body = { id: 'cu_new', customer_id: 'cu_new' } + await resources.createCustomer({ email: 'a@b.com', kycType: 'standard', name: 'Jane Doe', json: true }) expect(lastCall().method).toBe('POST') - expect(lastCall().url).toBe(`${BASE}/receivers`) + expect(lastCall().url).toBe(`${BASE}/customers`) expect(lastCall().body).toEqual({ type: 'individual', + kyc_type: 'standard', email: 'a@b.com', tax_id: null, first_name: 'Jane', @@ -144,40 +145,39 @@ describe('Receivers', () => { legal_name: null, country: 'US', external_id: null, - kyc_status: 'approved', }) }) test('updates only the fields explicitly passed via flags', async () => { - mockResponse.body = { id: 're_xyz' } - await resources.updateReceiver('re_xyz', { email: 'new@b.com', json: true }) + mockResponse.body = { id: 'cu_xyz' } + await resources.updateCustomer('cu_xyz', { email: 'new@b.com', json: true }) expect(lastCall().method).toBe('PUT') - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz`) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz`) expect(lastCall().body).toEqual({ email: 'new@b.com' }) }) - test('deletes a receiver by id', async () => { + test('deletes a customer by id', async () => { mockResponse.body = { success: true } - await resources.deleteReceiver('re_xyz', { json: true }) + await resources.deleteCustomer('cu_xyz', { json: true }) expect(lastCall().method).toBe('DELETE') - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz`) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz`) }) - test('fetches receiver limits', async () => { + test('fetches customer limits', async () => { mockResponse.body = { per_transaction: 100 } - await resources.getReceiverLimits('re_xyz', { json: true }) - expect(lastCall().url).toBe(`${BASE}/limits/receivers/re_xyz`) + await resources.getCustomerLimits('cu_xyz', { json: true }) + expect(lastCall().url).toBe(`${BASE}/limits/customers/cu_xyz`) }) - test('fetches receiver limit-increase requests', async () => { + test('fetches customer limit-increase requests', async () => { mockResponse.body = [] - await resources.getReceiverLimitsIncreaseRequests('re_xyz', { json: true }) - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/limit-increase`) + await resources.getCustomerLimitsIncreaseRequests('cu_xyz', { json: true }) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/limit-increase`) }) test('creates a limit-increase request with monetary fields parsed as integers', async () => { mockResponse.body = { id: 'rl_new' } - await resources.createReceiverLimitIncrease('re_xyz', { + await resources.createCustomerLimitIncrease('cu_xyz', { perTransaction: '100000', daily: '200000', monthly: '1000000', @@ -186,7 +186,7 @@ describe('Receivers', () => { json: true, }) expect(lastCall().method).toBe('POST') - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/limit-increase`) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/limit-increase`) expect(lastCall().body).toEqual({ per_transaction: 100000, daily: 200000, @@ -195,28 +195,43 @@ describe('Receivers', () => { supporting_document_file: 'https://example.com/doc.pdf', }) }) + + test('fetches the open RFI for a customer', async () => { + mockResponse.body = { id: 'rfi_1', status: 'pending' } + await resources.getCustomerRfi('cu_xyz', { json: true }) + expect(lastCall().method).toBe('GET') + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/rfi`) + }) + + test('submits an RFI response with --body JSON', async () => { + mockResponse.body = { success: true } + await resources.submitCustomerRfi('cu_xyz', { body: '{"address":"123 Main St"}', json: true }) + expect(lastCall().method).toBe('POST') + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/rfi`) + expect(lastCall().body).toEqual({ address: '123 Main St' }) + }) }) describe('Bank Accounts', () => { beforeEach(setupTestEnv) afterEach(teardownTestEnv) - test('lists bank accounts for a receiver', async () => { + test('lists bank accounts for a customer', async () => { mockResponse.body = [] - await resources.listBankAccounts({ receiverId: 're_xyz', json: true }) - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/bank-accounts`) + await resources.listBankAccounts({ customerId: 'cu_xyz', json: true }) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/bank-accounts`) }) test('fetches a bank account by id', async () => { mockResponse.body = { id: 'ba_1' } - await resources.getBankAccount('ba_1', { receiverId: 're_xyz', json: true }) - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/bank-accounts/ba_1`) + await resources.getBankAccount('ba_1', { customerId: 'cu_xyz', json: true }) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/bank-accounts/ba_1`) }) test('creates a bank account, snake_casing keys and defaulting unset fields to null', async () => { mockResponse.body = { id: 'ba_new', type: 'ach' } await resources.createBankAccount({ - receiverId: 're_xyz', + customerId: 'cu_xyz', type: 'ach', beneficiaryName: 'Jane', routingNumber: '021000021', @@ -224,7 +239,7 @@ describe('Bank Accounts', () => { json: true, }) expect(lastCall().method).toBe('POST') - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/bank-accounts`) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/bank-accounts`) expect(lastCall().body).toEqual({ type: 'ach', name: 'CLI Bank Account', @@ -242,9 +257,9 @@ describe('Bank Accounts', () => { test('deletes a bank account', async () => { mockResponse.body = { success: true } - await resources.deleteBankAccount('ba_1', { receiverId: 're_xyz', json: true }) + await resources.deleteBankAccount('ba_1', { customerId: 'cu_xyz', json: true }) expect(lastCall().method).toBe('DELETE') - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/bank-accounts/ba_1`) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/bank-accounts/ba_1`) }) }) @@ -252,41 +267,53 @@ describe('Blockchain Wallets', () => { beforeEach(setupTestEnv) afterEach(teardownTestEnv) - test('lists blockchain wallets for a receiver', async () => { + test('lists blockchain wallets for a customer', async () => { mockResponse.body = [] - await resources.listBlockchainWallets({ receiverId: 're_xyz', json: true }) - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/blockchain-wallets`) + await resources.listBlockchainWallets({ customerId: 'cu_xyz', json: true }) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/blockchain-wallets`) }) test('fetches a blockchain wallet by id', async () => { mockResponse.body = { id: 'bw_1' } - await resources.getBlockchainWallet('bw_1', { receiverId: 're_xyz', json: true }) - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/blockchain-wallets/bw_1`) + await resources.getBlockchainWallet('bw_1', { customerId: 'cu_xyz', json: true }) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/blockchain-wallets/bw_1`) }) - test('creates a blockchain wallet for a receiver', async () => { + test('creates a blockchain wallet for a customer', async () => { mockResponse.body = { id: 'bw_new', network: 'base' } await resources.createBlockchainWallet({ - receiverId: 're_xyz', + customerId: 'cu_xyz', address: '0xabc', network: 'base', json: true, }) expect(lastCall().method).toBe('POST') - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/blockchain-wallets`) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/blockchain-wallets`) expect(lastCall().body).toEqual({ - address: '0xabc', network: 'base', name: 'CLI Blockchain Wallet', - external_id: null, + address: '0xabc', }) }) + test('omits address from body when not provided', async () => { + mockResponse.body = { id: 'bw_new', network: 'base' } + await resources.createBlockchainWallet({ customerId: 'cu_xyz', network: 'base', json: true }) + expect(lastCall().body).toEqual({ network: 'base', name: 'CLI Blockchain Wallet' }) + }) + test('deletes a blockchain wallet', async () => { mockResponse.body = { success: true } - await resources.deleteBlockchainWallet('bw_1', { receiverId: 're_xyz', json: true }) + await resources.deleteBlockchainWallet('bw_1', { customerId: 'cu_xyz', json: true }) expect(lastCall().method).toBe('DELETE') - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/blockchain-wallets/bw_1`) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/blockchain-wallets/bw_1`) + }) + + test('fetches the sign-message for a customer blockchain wallet', async () => { + mockResponse.body = { message: 'Sign this: abc123' } + await resources.getBlockchainWalletSignMessage({ customerId: 'cu_xyz', json: true }) + expect(lastCall().method).toBe('GET') + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/blockchain-wallets/sign-message`) }) }) @@ -515,18 +542,48 @@ describe('Virtual Accounts', () => { beforeEach(setupTestEnv) afterEach(teardownTestEnv) - test('lists virtual accounts for a receiver', async () => { + test('lists virtual accounts for a customer', async () => { mockResponse.body = [] - await resources.listVirtualAccounts({ receiverId: 're_xyz', json: true }) - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/virtual-accounts`) + await resources.listVirtualAccounts({ customerId: 'cu_xyz', json: true }) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/virtual-accounts`) }) - test('creates a virtual account for a receiver', async () => { + test('creates a virtual account for a customer', async () => { mockResponse.body = { id: 'va_new' } - await resources.createVirtualAccount({ receiverId: 're_xyz', blockchainWalletId: 'bw_1', json: true }) + await resources.createVirtualAccount({ + customerId: 'cu_xyz', + bankingPartner: 'jpmorgan', + token: 'USDC', + blockchainWalletId: 'bw_1', + json: true, + }) expect(lastCall().method).toBe('POST') - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/virtual-accounts`) - expect(lastCall().body).toEqual({ blockchain_wallet_id: 'bw_1' }) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/virtual-accounts`) + expect(lastCall().body).toEqual({ + banking_partner: 'jpmorgan', + token: 'USDC', + blockchain_wallet_id: 'bw_1', + }) + }) + + test('fetches a virtual account by id', async () => { + mockResponse.body = { id: 'va_1' } + await resources.getVirtualAccount('va_1', { customerId: 'cu_xyz', json: true }) + expect(lastCall().method).toBe('GET') + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/virtual-accounts/va_1`) + }) + + test('updates a virtual account', async () => { + mockResponse.body = { success: true } + await resources.updateVirtualAccount('va_1', { + customerId: 'cu_xyz', + token: 'USDT', + blockchainWalletId: 'bw_2', + json: true, + }) + expect(lastCall().method).toBe('PUT') + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/virtual-accounts/va_1`) + expect(lastCall().body).toEqual({ token: 'USDT', blockchain_wallet_id: 'bw_2' }) }) }) @@ -536,8 +593,40 @@ describe('Offramp Wallets', () => { test('lists offramp wallets for a bank account', async () => { mockResponse.body = [] - await resources.listOfframpWallets({ receiverId: 're_xyz', bankAccountId: 'ba_1', json: true }) - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/bank-accounts/ba_1/offramp-wallets`) + await resources.listOfframpWallets({ customerId: 'cu_xyz', bankAccountId: 'ba_1', json: true }) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/bank-accounts/ba_1/offramp-wallets`) + }) + + test('creates an offramp wallet', async () => { + mockResponse.body = { id: 'ow_new', network: 'base', address: '0xabc' } + await resources.createOfframpWallet({ + customerId: 'cu_xyz', + bankAccountId: 'ba_1', + network: 'base', + json: true, + }) + expect(lastCall().method).toBe('POST') + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/bank-accounts/ba_1/offramp-wallets`) + expect(lastCall().body).toEqual({ network: 'base' }) + }) + + test('creates an offramp wallet with external_id', async () => { + mockResponse.body = { id: 'ow_new', network: 'base', address: '0xabc' } + await resources.createOfframpWallet({ + customerId: 'cu_xyz', + bankAccountId: 'ba_1', + network: 'base', + externalId: 'ext-1', + json: true, + }) + expect(lastCall().body).toEqual({ network: 'base', external_id: 'ext-1' }) + }) + + test('fetches an offramp wallet by id', async () => { + mockResponse.body = { id: 'ow_1' } + await resources.getOfframpWallet('ow_1', { customerId: 'cu_xyz', bankAccountId: 'ba_1', json: true }) + expect(lastCall().method).toBe('GET') + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/bank-accounts/ba_1/offramp-wallets/ow_1`) }) }) @@ -564,6 +653,14 @@ describe('Instances', () => { expect(lastCall().url).toBe(`${BASE}`) expect(lastCall().body).toEqual({ name: 'New' }) }) + + test('migrates instance ownership to a new user', async () => { + mockResponse.body = { success: true } + await resources.migrateInstanceOwnership({ userId: 'usr_123', json: true }) + expect(lastCall().method).toBe('POST') + expect(lastCall().url).toBe(`${BASE}/ownership`) + expect(lastCall().body).toEqual({ user_id: 'usr_123' }) + }) }) describe('Available', () => { @@ -587,49 +684,49 @@ describe('Wallets (custodial)', () => { beforeEach(setupTestEnv) afterEach(teardownTestEnv) - test('lists custodial wallets for a receiver', async () => { + test('lists custodial wallets for a customer', async () => { mockResponse.body = [] - await resources.listWallets({ receiverId: 're_xyz', json: true }) - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/wallets`) + await resources.listWallets({ customerId: 'cu_xyz', json: true }) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/wallets`) }) test('fetches a custodial wallet by id', async () => { mockResponse.body = { id: 'bl_1' } - await resources.getWallet('bl_1', { receiverId: 're_xyz', json: true }) - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/wallets/bl_1`) + await resources.getWallet('bl_1', { customerId: 'cu_xyz', json: true }) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/wallets/bl_1`) }) test('fetches a custodial wallet balance', async () => { mockResponse.body = { USDC: { amount: 0 } } - await resources.getWalletBalance('bl_1', { receiverId: 're_xyz', json: true }) - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/wallets/bl_1/balance`) + await resources.getWalletBalance('bl_1', { customerId: 'cu_xyz', json: true }) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/wallets/bl_1/balance`) }) test('creates a custodial wallet with --external-id', async () => { mockResponse.body = { id: 'bl_new', network: 'polygon' } await resources.createWallet({ - receiverId: 're_xyz', + customerId: 'cu_xyz', network: 'polygon', name: 'Main', externalId: 'ext-1', json: true, }) expect(lastCall().method).toBe('POST') - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/wallets`) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/wallets`) expect(lastCall().body).toEqual({ network: 'polygon', name: 'Main', external_id: 'ext-1' }) }) test('omits external_id from the body when --external-id is not passed', async () => { mockResponse.body = { id: 'bl_new', network: 'polygon' } - await resources.createWallet({ receiverId: 're_xyz', network: 'polygon', name: 'Main', json: true }) + await resources.createWallet({ customerId: 'cu_xyz', network: 'polygon', name: 'Main', json: true }) expect(lastCall().body).toEqual({ network: 'polygon', name: 'Main' }) }) test('deletes a custodial wallet', async () => { mockResponse.body = { success: true } - await resources.deleteWallet('bl_1', { receiverId: 're_xyz', json: true }) + await resources.deleteWallet('bl_1', { customerId: 'cu_xyz', json: true }) expect(lastCall().method).toBe('DELETE') - expect(lastCall().url).toBe(`${BASE}/receivers/re_xyz/wallets/bl_1`) + expect(lastCall().url).toBe(`${BASE}/customers/cu_xyz/wallets/bl_1`) }) }) @@ -778,6 +875,14 @@ describe('Upload', () => { expect(lastCall().url).toBe('https://api.blindpay.com/v1/upload?instance_id=in_override') }) + + test('analyzes a document via POST /v1/upload/analyze', async () => { + mockResponse.body = { approval_rate: 'high', description: 'Clear document' } + await resources.analyzeDocument({ body: '{"file_url":"https://example.com/doc.pdf"}', json: true }) + expect(lastCall().method).toBe('POST') + expect(lastCall().url).toBe('https://api.blindpay.com/v1/upload/analyze') + expect(lastCall().body).toEqual({ file_url: 'https://example.com/doc.pdf' }) + }) }) describe('Error handling', () => { @@ -785,13 +890,13 @@ describe('Error handling', () => { afterEach(teardownTestEnv) test('exits with code 2 and emits a JSON error payload on a non-2xx response', async () => { - mockResponse = { status: 404, body: { message: 'receiver not found', errors: [] } } - await expect(resources.getReceiver('re_xyz', { json: true })).rejects.toThrow('__test_exit__2') + mockResponse = { status: 404, body: { message: 'customer not found', errors: [] } } + await expect(resources.getCustomer('cu_xyz', { json: true })).rejects.toThrow('__test_exit__2') expect(lastJsonLog()).toMatchObject({ error: true, exitCode: 2, statusCode: 404, - message: 'receiver not found', + message: 'customer not found', }) }) @@ -803,7 +908,7 @@ describe('Error handling', () => { errors: [{ path: ['email'], message: 'required' }], }, } - await expect(resources.createReceiver({ email: 'x', json: true })).rejects.toThrow('__test_exit__2') + await expect(resources.createCustomer({ email: 'x', kycType: 'standard', json: true })).rejects.toThrow('__test_exit__2') expect(lastJsonLog()).toMatchObject({ error: true, exitCode: 2, diff --git a/src/commands/resources.ts b/src/commands/resources.ts index 148b934..2d5a8a9 100644 --- a/src/commands/resources.ts +++ b/src/commands/resources.ts @@ -67,11 +67,11 @@ function extractList(res: any): any[] { return [] } -// Receivers -export async function listReceivers(options: { json: boolean }) { +// Customers +export async function listCustomers(options: { json: boolean }) { try { const ctx = resolveContext() - const res = await apiGet(ctx, `${instancePath(ctx)}/receivers`) + const res = await apiGet(ctx, `${instancePath(ctx)}/customers`) const list = extractList(res) const display = list.map((r: any) => ({ id: r.id, @@ -88,20 +88,21 @@ export async function listReceivers(options: { json: boolean }) { } } -export async function getReceiver(id: string, options: { json: boolean }) { +export async function getCustomer(id: string, options: { json: boolean }) { try { const ctx = resolveContext() - const receiver = await apiGet(ctx, `${instancePath(ctx)}/receivers/${id}`) - printResult(receiver, options.json) + const customer = await apiGet(ctx, `${instancePath(ctx)}/customers/${id}`) + printResult(customer, options.json) } catch (e) { handleApiError(e, options.json) } } -export async function createReceiver(options: { +export async function createCustomer(options: { email: string type?: string + kycType: string name?: string firstName?: string lastName?: string @@ -109,7 +110,6 @@ export async function createReceiver(options: { country?: string taxId?: string externalId?: string - kycStatus?: string json: boolean }) { try { @@ -129,6 +129,7 @@ export async function createReceiver(options: { } const body = { type: options.type || 'individual', + kyc_type: options.kycType, email: options.email, tax_id: options.taxId ?? null, first_name: first_name ?? null, @@ -136,22 +137,21 @@ export async function createReceiver(options: { legal_name: options.legalName ?? null, country: options.country || 'US', external_id: options.externalId ?? null, - kyc_status: options.kycStatus ?? 'approved', } - const receiver = await apiPost<{ id: string, type: string }>(ctx, `${instancePath(ctx)}/receivers`, body) + const customer = await apiPost<{ id: string, customer_id: string }>(ctx, `${instancePath(ctx)}/customers`, body) const displayName = body.type === 'business' ? (body.legal_name || '—') : [body.first_name, body.last_name].filter(Boolean).join(' ').trim() || '—' - clack.log.success(`Created receiver ${receiver.id} (${receiver.type}, ${displayName})`) + clack.log.success(`Created customer ${customer.id} (${body.type}, ${displayName})`) if (options.json) - console.log(formatOutput(receiver, true)) + console.log(formatOutput(customer, true)) } catch (e) { handleApiError(e, options.json) } } -export async function updateReceiver( +export async function updateCustomer( id: string, options: { name?: string @@ -160,7 +160,6 @@ export async function updateReceiver( legalName?: string email?: string country?: string - kycStatus?: string json?: boolean }, ) { @@ -177,8 +176,6 @@ export async function updateReceiver( body.email = options.email if (options.country !== undefined) body.country = options.country - if (options.kycStatus !== undefined) - body.kyc_status = options.kycStatus if (options.name !== undefined && options.name.trim()) { const parts = options.name.trim().split(/\s+/) if (parts.length >= 2) { @@ -191,25 +188,25 @@ export async function updateReceiver( } } if (Object.keys(body).length === 0) { - exitWithError('Provide at least one field to update (e.g. --name, --kyc-status)', 1, options.json) + exitWithError('Provide at least one field to update (e.g. --name, --email)', 1, options.json) } - const receiver = await apiPut>(ctx, `${instancePath(ctx)}/receivers/${id}`, body) - clack.log.success(`Updated receiver ${id}`) + const customer = await apiPut>(ctx, `${instancePath(ctx)}/customers/${id}`, body) + clack.log.success(`Updated customer ${id}`) if (options.json) - console.log(formatOutput(receiver, true)) + console.log(formatOutput(customer, true)) else - console.log(formatOutput(receiver, false)) + console.log(formatOutput(customer, false)) } catch (e) { handleApiError(e, options.json) } } -export async function deleteReceiver(id: string, options: { json?: boolean } = {}) { +export async function deleteCustomer(id: string, options: { json?: boolean } = {}) { try { const ctx = resolveContext() - await apiDelete(ctx, `${instancePath(ctx)}/receivers/${id}`) - clack.log.success(`Deleted receiver ${id}`) + await apiDelete(ctx, `${instancePath(ctx)}/customers/${id}`) + clack.log.success(`Deleted customer ${id}`) } catch (e) { handleApiError(e, options.json) @@ -217,10 +214,10 @@ export async function deleteReceiver(id: string, options: { json?: boolean } = { } // Bank Accounts -export async function listBankAccounts(options: { receiverId: string, json: boolean }) { +export async function listBankAccounts(options: { customerId: string, json: boolean }) { try { const ctx = resolveContext() - const res = await apiGet(ctx, `${instancePath(ctx)}/receivers/${options.receiverId}/bank-accounts`) + const res = await apiGet(ctx, `${instancePath(ctx)}/customers/${options.customerId}/bank-accounts`) const list = extractList(res) const display = list.map((a: any) => ({ id: a.id, type: a.type, name: a.name, status: a.status, country: a.country })) printResult(options.json ? list : display, options.json, ['id', 'type', 'name', 'status', 'country']) @@ -230,10 +227,10 @@ export async function listBankAccounts(options: { receiverId: string, json: bool } } -export async function getBankAccount(id: string, options: { receiverId: string, json: boolean }) { +export async function getBankAccount(id: string, options: { customerId: string, json: boolean }) { try { const ctx = resolveContext() - const account = await apiGet(ctx, `${instancePath(ctx)}/receivers/${options.receiverId}/bank-accounts/${id}`) + const account = await apiGet(ctx, `${instancePath(ctx)}/customers/${options.customerId}/bank-accounts/${id}`) printResult(account, options.json) } catch (e) { @@ -242,7 +239,7 @@ export async function getBankAccount(id: string, options: { receiverId: string, } export async function createBankAccount(options: { - receiverId: string + customerId: string type?: string name?: string recipientRelationship?: string @@ -271,7 +268,7 @@ export async function createBankAccount(options: { country: options.country ?? null, swift_ifsc_branch_code: options.swiftIfscBranchCode ?? null, } - const ba = await apiPost<{ id: string, type: string }>(ctx, `${instancePath(ctx)}/receivers/${options.receiverId}/bank-accounts`, body) + const ba = await apiPost<{ id: string, type: string }>(ctx, `${instancePath(ctx)}/customers/${options.customerId}/bank-accounts`, body) clack.log.success(`Created bank account ${ba.id} (${ba.type})`) if (options.json) console.log(formatOutput(ba, true)) @@ -281,10 +278,10 @@ export async function createBankAccount(options: { } } -export async function deleteBankAccount(id: string, options: { receiverId: string, json?: boolean }) { +export async function deleteBankAccount(id: string, options: { customerId: string, json?: boolean }) { try { const ctx = resolveContext() - await apiDelete(ctx, `${instancePath(ctx)}/receivers/${options.receiverId}/bank-accounts/${id}`) + await apiDelete(ctx, `${instancePath(ctx)}/customers/${options.customerId}/bank-accounts/${id}`) clack.log.success(`Deleted bank account ${id}`) } catch (e) { @@ -293,10 +290,10 @@ export async function deleteBankAccount(id: string, options: { receiverId: strin } // Blockchain Wallets -export async function listBlockchainWallets(options: { receiverId: string, json: boolean }) { +export async function listBlockchainWallets(options: { customerId: string, json: boolean }) { try { const ctx = resolveContext() - const res = await apiGet(ctx, `${instancePath(ctx)}/receivers/${options.receiverId}/blockchain-wallets`) + const res = await apiGet(ctx, `${instancePath(ctx)}/customers/${options.customerId}/blockchain-wallets`) const list = extractList(res) const display = list.map((w: any) => ({ id: w.id, address: truncate(w.address, 20), network: w.network })) printResult(options.json ? list : display, options.json, ['id', 'address', 'network']) @@ -306,10 +303,10 @@ export async function listBlockchainWallets(options: { receiverId: string, json: } } -export async function getBlockchainWallet(id: string, options: { receiverId: string, json: boolean }) { +export async function getBlockchainWallet(id: string, options: { customerId: string, json: boolean }) { try { const ctx = resolveContext() - const wallet = await apiGet(ctx, `${instancePath(ctx)}/receivers/${options.receiverId}/blockchain-wallets/${id}`) + const wallet = await apiGet(ctx, `${instancePath(ctx)}/customers/${options.customerId}/blockchain-wallets/${id}`) printResult(wallet, options.json) } catch (e) { @@ -318,22 +315,24 @@ export async function getBlockchainWallet(id: string, options: { receiverId: str } export async function createBlockchainWallet(options: { - receiverId: string - address: string + customerId: string + address?: string network?: string name?: string - externalId?: string + signatureTxHash?: string + isAccountAbstraction?: boolean json: boolean }) { try { const ctx = resolveContext() - const body = { - address: options.address, + const body: Record = { network: options.network || 'base', name: options.name || 'CLI Blockchain Wallet', - external_id: options.externalId ?? null, } - const wallet = await apiPost<{ id: string, network: string }>(ctx, `${instancePath(ctx)}/receivers/${options.receiverId}/blockchain-wallets`, body) + if (options.address !== undefined) body.address = options.address + if (options.signatureTxHash !== undefined) body.signature_tx_hash = options.signatureTxHash + if (options.isAccountAbstraction !== undefined) body.is_account_abstraction = options.isAccountAbstraction + const wallet = await apiPost<{ id: string, network: string }>(ctx, `${instancePath(ctx)}/customers/${options.customerId}/blockchain-wallets`, body) clack.log.success(`Created blockchain wallet ${wallet.id} (${wallet.network})`) if (options.json) console.log(formatOutput(wallet, true)) @@ -343,10 +342,10 @@ export async function createBlockchainWallet(options: { } } -export async function deleteBlockchainWallet(id: string, options: { receiverId: string, json?: boolean }) { +export async function deleteBlockchainWallet(id: string, options: { customerId: string, json?: boolean }) { try { const ctx = resolveContext() - await apiDelete(ctx, `${instancePath(ctx)}/receivers/${options.receiverId}/blockchain-wallets/${id}`) + await apiDelete(ctx, `${instancePath(ctx)}/customers/${options.customerId}/blockchain-wallets/${id}`) clack.log.success(`Deleted blockchain wallet ${id}`) } catch (e) { @@ -354,6 +353,17 @@ export async function deleteBlockchainWallet(id: string, options: { receiverId: } } +export async function getBlockchainWalletSignMessage(options: { customerId: string, json: boolean }) { + try { + const ctx = resolveContext() + const res = await apiGet<{ message: string }>(ctx, `${instancePath(ctx)}/customers/${options.customerId}/blockchain-wallets/sign-message`) + printResult(res, options.json) + } + catch (e) { + handleApiError(e, options.json) + } +} + // Payouts export async function listPayouts(options: { json: boolean, status?: string }) { try { @@ -661,23 +671,38 @@ export async function deleteApiKey(id: string, options: { json?: boolean } = {}) } // Virtual Accounts -export async function listVirtualAccounts(options: { receiverId: string, json: boolean }) { +export async function listVirtualAccounts(options: { customerId: string, json: boolean }) { try { const ctx = resolveContext() - const res = await apiGet(ctx, `${instancePath(ctx)}/receivers/${options.receiverId}/virtual-accounts`) + const res = await apiGet(ctx, `${instancePath(ctx)}/customers/${options.customerId}/virtual-accounts`) const list = extractList(res) - const display = list.map((a: any) => ({ id: a.id, account_number: a.account_number, routing_number: a.routing_number, kyc_status: a.kyc_status })) - printResult(options.json ? list : display, options.json, ['id', 'account_number', 'routing_number', 'kyc_status']) + const display = list.map((a: any) => ({ id: a.id, banking_partner: a.banking_partner, token: a.token, kyc_status: a.kyc_status })) + printResult(options.json ? list : display, options.json, ['id', 'banking_partner', 'token', 'kyc_status']) } catch (e) { handleApiError(e, options.json) } } -export async function createVirtualAccount(options: { receiverId: string, blockchainWalletId: string, json: boolean }) { +export async function createVirtualAccount(options: { + customerId: string + bankingPartner: string + token: string + blockchainWalletId: string + soleProprietorDocType?: string + soleProprietorDocFile?: string + json: boolean +}) { try { const ctx = resolveContext() - const account = await apiPost<{ id: string }>(ctx, `${instancePath(ctx)}/receivers/${options.receiverId}/virtual-accounts`, { blockchain_wallet_id: options.blockchainWalletId }) + const body: Record = { + banking_partner: options.bankingPartner, + token: options.token, + blockchain_wallet_id: options.blockchainWalletId, + } + if (options.soleProprietorDocType !== undefined) body.sole_proprietor_doc_type = options.soleProprietorDocType + if (options.soleProprietorDocFile !== undefined) body.sole_proprietor_doc_file = options.soleProprietorDocFile + const account = await apiPost<{ id: string }>(ctx, `${instancePath(ctx)}/customers/${options.customerId}/virtual-accounts`, body) clack.log.success(`Created virtual account ${account.id}`) if (options.json) console.log(formatOutput(account, true)) @@ -687,11 +712,45 @@ export async function createVirtualAccount(options: { receiverId: string, blockc } } +export async function getVirtualAccount(id: string, options: { customerId: string, json: boolean }) { + try { + const ctx = resolveContext() + const account = await apiGet(ctx, `${instancePath(ctx)}/customers/${options.customerId}/virtual-accounts/${id}`) + printResult(account, options.json) + } + catch (e) { + handleApiError(e, options.json) + } +} + +export async function updateVirtualAccount( + id: string, + options: { + customerId: string + token: string + blockchainWalletId: string + json: boolean + }, +) { + try { + const ctx = resolveContext() + const body = { + token: options.token, + blockchain_wallet_id: options.blockchainWalletId, + } + await apiPut(ctx, `${instancePath(ctx)}/customers/${options.customerId}/virtual-accounts/${id}`, body) + clack.log.success(`Updated virtual account ${id}`) + } + catch (e) { + handleApiError(e, options.json) + } +} + // Offramp Wallets -export async function listOfframpWallets(options: { receiverId: string, bankAccountId: string, json: boolean }) { +export async function listOfframpWallets(options: { customerId: string, bankAccountId: string, json: boolean }) { try { const ctx = resolveContext() - const res = await apiGet(ctx, `${instancePath(ctx)}/receivers/${options.receiverId}/bank-accounts/${options.bankAccountId}/offramp-wallets`) + const res = await apiGet(ctx, `${instancePath(ctx)}/customers/${options.customerId}/bank-accounts/${options.bankAccountId}/offramp-wallets`) const list = extractList(res) const display = list.map((w: any) => ({ id: w.id, address: truncate(w.address, 20), network: w.network })) printResult(options.json ? list : display, options.json, ['id', 'address', 'network']) @@ -701,6 +760,42 @@ export async function listOfframpWallets(options: { receiverId: string, bankAcco } } +export async function createOfframpWallet(options: { + customerId: string + bankAccountId: string + network: string + externalId?: string + json: boolean +}) { + try { + const ctx = resolveContext() + const body: Record = { network: options.network } + if (options.externalId !== undefined) body.external_id = options.externalId + const wallet = await apiPost<{ id: string, network: string, address: string }>( + ctx, + `${instancePath(ctx)}/customers/${options.customerId}/bank-accounts/${options.bankAccountId}/offramp-wallets`, + body, + ) + clack.log.success(`Created offramp wallet ${wallet.id} (${wallet.network}) -> ${wallet.address}`) + if (options.json) + console.log(formatOutput(wallet, true)) + } + catch (e) { + handleApiError(e, options.json) + } +} + +export async function getOfframpWallet(id: string, options: { customerId: string, bankAccountId: string, json: boolean }) { + try { + const ctx = resolveContext() + const wallet = await apiGet(ctx, `${instancePath(ctx)}/customers/${options.customerId}/bank-accounts/${options.bankAccountId}/offramp-wallets/${id}`) + printResult(wallet, options.json) + } + catch (e) { + handleApiError(e, options.json) + } +} + // Instances export async function getInstance(options: { json: boolean }) { try { @@ -748,11 +843,23 @@ export async function updateInstance(options: { } } -// Receiver Limits -export async function getReceiverLimits(receiverId: string, options: { json: boolean }) { +export async function migrateInstanceOwnership(options: { userId: string, json: boolean }) { try { const ctx = resolveContext() - const limits = await apiGet(ctx, `${instancePath(ctx)}/limits/receivers/${receiverId}`) + const res = await apiPost<{ success: boolean }>(ctx, `${instancePath(ctx)}/ownership`, { user_id: options.userId }) + clack.log.success('Instance ownership migrated') + if (options.json) console.log(formatOutput(res, true)) + } + catch (e) { + handleApiError(e, options.json) + } +} + +// Customer Limits +export async function getCustomerLimits(customerId: string, options: { json: boolean }) { + try { + const ctx = resolveContext() + const limits = await apiGet(ctx, `${instancePath(ctx)}/limits/customers/${customerId}`) printResult(limits, options.json) } catch (e) { @@ -760,10 +867,10 @@ export async function getReceiverLimits(receiverId: string, options: { json: boo } } -export async function getReceiverLimitsIncreaseRequests(receiverId: string, options: { json: boolean }) { +export async function getCustomerLimitsIncreaseRequests(customerId: string, options: { json: boolean }) { try { const ctx = resolveContext() - const res = await apiGet(ctx, `${instancePath(ctx)}/receivers/${receiverId}/limit-increase`) + const res = await apiGet(ctx, `${instancePath(ctx)}/customers/${customerId}/limit-increase`) const list = extractList(res) printResult(list, options.json, ['id', 'status', 'per_transaction', 'daily', 'monthly', 'created_at']) } @@ -772,8 +879,8 @@ export async function getReceiverLimitsIncreaseRequests(receiverId: string, opti } } -export async function createReceiverLimitIncrease( - receiverId: string, +export async function createCustomerLimitIncrease( + customerId: string, options: { perTransaction: string daily: string @@ -792,7 +899,7 @@ export async function createReceiverLimitIncrease( supporting_document_type: options.supportingDocumentType, supporting_document_file: options.supportingDocumentFile, } - const res = await apiPost<{ id: string }>(ctx, `${instancePath(ctx)}/receivers/${receiverId}/limit-increase`, body) + const res = await apiPost<{ id: string }>(ctx, `${instancePath(ctx)}/customers/${customerId}/limit-increase`, body) clack.log.success(`Created limit increase request ${res.id}`) printResult(res, options.json) } @@ -801,6 +908,37 @@ export async function createReceiverLimitIncrease( } } +// Customer RFI +export async function getCustomerRfi(customerId: string, options: { json: boolean }) { + try { + const ctx = resolveContext() + const rfi = await apiGet(ctx, `${instancePath(ctx)}/customers/${customerId}/rfi`) + printResult(rfi, options.json, ['id', 'status', 'expires_at', 'created_at']) + } + catch (e) { + handleApiError(e, options.json) + } +} + +export async function submitCustomerRfi(customerId: string, options: { body: string, json: boolean }) { + let body: Record + try { + body = JSON.parse(options.body) + } + catch (e) { + exitWithError(`Invalid --body JSON: ${(e as Error).message}`, 1, options.json) + } + try { + const ctx = resolveContext() + const res = await apiPost<{ success: boolean }>(ctx, `${instancePath(ctx)}/customers/${customerId}/rfi`, body!) + clack.log.success('RFI response submitted') + if (options.json) console.log(formatOutput(res, true)) + } + catch (e) { + handleApiError(e, options.json) + } +} + // FX Rates export async function getQuoteFxRate(options: { from?: string, to?: string, json: boolean }) { try { @@ -842,10 +980,10 @@ export async function getAvailableBankDetails(options: { rail: string, json: boo } // Wallets (custodial, distinct from blockchain_wallets) -export async function listWallets(options: { receiverId: string, json: boolean }) { +export async function listWallets(options: { customerId: string, json: boolean }) { try { const ctx = resolveContext() - const res = await apiGet(ctx, `${instancePath(ctx)}/receivers/${options.receiverId}/wallets`) + const res = await apiGet(ctx, `${instancePath(ctx)}/customers/${options.customerId}/wallets`) const list = extractList(res) printResult(list, options.json, ['id', 'name', 'network', 'address']) } @@ -854,10 +992,10 @@ export async function listWallets(options: { receiverId: string, json: boolean } } } -export async function getWallet(id: string, options: { receiverId: string, json: boolean }) { +export async function getWallet(id: string, options: { customerId: string, json: boolean }) { try { const ctx = resolveContext() - const wallet = await apiGet(ctx, `${instancePath(ctx)}/receivers/${options.receiverId}/wallets/${id}`) + const wallet = await apiGet(ctx, `${instancePath(ctx)}/customers/${options.customerId}/wallets/${id}`) printResult(wallet, options.json) } catch (e) { @@ -865,10 +1003,10 @@ export async function getWallet(id: string, options: { receiverId: string, json: } } -export async function getWalletBalance(id: string, options: { receiverId: string, json: boolean }) { +export async function getWalletBalance(id: string, options: { customerId: string, json: boolean }) { try { const ctx = resolveContext() - const balance = await apiGet(ctx, `${instancePath(ctx)}/receivers/${options.receiverId}/wallets/${id}/balance`) + const balance = await apiGet(ctx, `${instancePath(ctx)}/customers/${options.customerId}/wallets/${id}/balance`) printResult(balance, options.json) } catch (e) { @@ -877,7 +1015,7 @@ export async function getWalletBalance(id: string, options: { receiverId: string } export async function createWallet(options: { - receiverId: string + customerId: string network: string name: string externalId?: string @@ -890,7 +1028,7 @@ export async function createWallet(options: { name: options.name, } if (options.externalId !== undefined) body.external_id = options.externalId - const wallet = await apiPost<{ id: string, network: string }>(ctx, `${instancePath(ctx)}/receivers/${options.receiverId}/wallets`, body) + const wallet = await apiPost<{ id: string, network: string }>(ctx, `${instancePath(ctx)}/customers/${options.customerId}/wallets`, body) clack.log.success(`Created wallet ${wallet.id} (${wallet.network})`) printResult(wallet, options.json) } @@ -899,10 +1037,10 @@ export async function createWallet(options: { } } -export async function deleteWallet(id: string, options: { receiverId: string, json?: boolean }) { +export async function deleteWallet(id: string, options: { customerId: string, json?: boolean }) { try { const ctx = resolveContext() - await apiDelete(ctx, `${instancePath(ctx)}/receivers/${options.receiverId}/wallets/${id}`) + await apiDelete(ctx, `${instancePath(ctx)}/customers/${options.customerId}/wallets/${id}`) clack.log.success(`Deleted wallet ${id}`) } catch (e) { @@ -1048,3 +1186,21 @@ export async function uploadFile(options: { file: string, bucket: string, instan handleApiError(e, options.json) } } + +export async function analyzeDocument(options: { body: string, json: boolean }) { + let body: Record + try { + body = JSON.parse(options.body) + } + catch (e) { + exitWithError(`Invalid --body JSON: ${(e as Error).message}`, 1, options.json) + } + try { + const ctx = resolveContext() + const res = await apiPost<{ approval_rate: string, description: string }>(ctx, `/v1/upload/analyze`, body!) + printResult(res, options.json, ['approval_rate', 'description']) + } + catch (e) { + handleApiError(e, options.json) + } +} diff --git a/src/index.ts b/src/index.ts index 735694d..04c74fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,13 +3,15 @@ import { Command } from 'commander' import * as clack from '@clack/prompts' import { listSchemas, getSchema } from './commands/schema' import { - listReceivers, - getReceiver, - createReceiver, - updateReceiver, - deleteReceiver, - getReceiverLimits, - getReceiverLimitsIncreaseRequests, + listCustomers, + getCustomer, + createCustomer, + updateCustomer, + deleteCustomer, + getCustomerLimits, + getCustomerLimitsIncreaseRequests, + getCustomerRfi, + submitCustomerRfi, listBankAccounts, getBankAccount, createBankAccount, @@ -18,6 +20,7 @@ import { getBlockchainWallet, createBlockchainWallet, deleteBlockchainWallet, + getBlockchainWalletSignMessage, listPayouts, getPayout, createPayout, @@ -38,13 +41,18 @@ import { deleteApiKey, listVirtualAccounts, createVirtualAccount, + getVirtualAccount, + updateVirtualAccount, listOfframpWallets, + createOfframpWallet, + getOfframpWallet, getInstance, listInstanceMembers, updateInstance, + migrateInstanceOwnership, listAvailableRails, getAvailableBankDetails, - createReceiverLimitIncrease, + createCustomerLimitIncrease, listWallets, getWallet, getWalletBalance, @@ -58,6 +66,7 @@ import { getInstanceFees, initiateTos, uploadFile, + analyzeDocument, } from './commands/resources' import { getConfig, setConfig, clearConfig, getConfigPath } from './utils/config' import { CLI_VERSION } from './utils/constants' @@ -66,12 +75,12 @@ const program = new Command() program .name('blindpay') - .description('Blindpay CLI — manage receivers, bank accounts, payouts, payins, and more from the terminal.') + .description('Blindpay CLI — manage customers, bank accounts, payouts, payins, and more from the terminal.') .version(CLI_VERSION) .addHelpText('after', ` Examples: $ blindpay config set --api-key --instance-id - $ blindpay receivers list --json + $ blindpay customers list --json $ blindpay payouts list --status processing $ blindpay available rails @@ -135,34 +144,37 @@ configCmd .description('Print config file path') .action(() => console.log(getConfigPath())) -// ── Receivers ─────────────────────────────────────────────────────────── -const receivers = program.command('receivers').description('Manage receivers') +// ── Customers ──────────────────────────────────────────────────────────── +const customers = program.command('customers').description('Manage customers') .addHelpText('after', ` Examples: - $ blindpay receivers list - $ blindpay receivers list --json - $ blindpay receivers get - $ blindpay receivers create --email user@example.com --name "John Doe" --country US - $ blindpay receivers create --type business --email biz@co.com --legal-name "Acme Inc" - $ blindpay receivers update --kyc-status approved - $ blindpay receivers delete `) - -receivers + $ blindpay customers list + $ blindpay customers list --json + $ blindpay customers get + $ blindpay customers create --email user@example.com --name "John Doe" --country US --kyc-type standard + $ blindpay customers create --type business --email biz@co.com --legal-name "Acme Inc" --kyc-type light + $ blindpay customers update --email new@example.com + $ blindpay customers delete + $ blindpay customers rfi_get + $ blindpay customers rfi_submit --body '{"key":"value"}'`) + +customers .command('list') - .description('List all receivers') + .description('List all customers') .option('--json', 'Output as JSON', false) - .action(opts => listReceivers(opts)) + .action(opts => listCustomers(opts)) -receivers +customers .command('get ') - .description('Get a receiver by ID') + .description('Get a customer by ID') .option('--json', 'Output as JSON', false) - .action((id, opts) => getReceiver(id, opts)) + .action((id, opts) => getCustomer(id, opts)) -receivers +customers .command('create') - .description('Create a new receiver') - .requiredOption('--email ', 'Receiver email') + .description('Create a new customer') + .requiredOption('--email ', 'Customer email') + .requiredOption('--kyc-type ', 'KYC type (light, standard, enhanced)') .option('--type ', 'individual or business', 'individual') .option('--name ', 'Full name (individual); splits into first_name and last_name') .option('--first-name ', 'First name (individual)') @@ -171,80 +183,91 @@ receivers .option('--country ', 'ISO 3166 country code', 'US') .option('--tax-id ', 'Tax ID') .option('--external-id ', 'External ID') - .option('--kyc-status ', 'KYC status (verifying, approved, rejected, deprecated)', 'approved') .option('--json', 'Output as JSON', false) - .action(opts => createReceiver(opts)) + .action(opts => createCustomer(opts)) -receivers +customers .command('update ') - .description('Update a receiver') + .description('Update a customer') .option('--name ', 'Full name (individual); splits into first_name and last_name') .option('--first-name ', 'First name (individual)') .option('--last-name ', 'Last name (individual)') .option('--legal-name ', 'Legal name (business)') - .option('--email ', 'Receiver email') + .option('--email ', 'Customer email') .option('--country ', 'ISO 3166 country code') - .option('--kyc-status ', 'KYC status (verifying, approved, rejected, deprecated)') .option('--json', 'Output as JSON', false) - .action((id, opts) => updateReceiver(id, opts)) + .action((id, opts) => updateCustomer(id, opts)) -receivers +customers .command('delete ') - .description('Delete a receiver') + .description('Delete a customer') .option('--json', 'Output as JSON', false) - .action((id, opts) => deleteReceiver(id, opts)) + .action((id, opts) => deleteCustomer(id, opts)) -receivers +customers .command('limits ') - .description('Get receiver limits') + .description('Get customer limits') .option('--json', 'Output as JSON', false) - .action((id, opts) => getReceiverLimits(id, opts)) + .action((id, opts) => getCustomerLimits(id, opts)) -receivers +customers .command('limits_increase_requests ') - .description('Get receiver limit-increase requests') + .description('Get customer limit-increase requests') .option('--json', 'Output as JSON', false) - .action((id, opts) => getReceiverLimitsIncreaseRequests(id, opts)) + .action((id, opts) => getCustomerLimitsIncreaseRequests(id, opts)) -receivers +customers .command('create_limit_increase ') - .description('Request a limit increase for a receiver') + .description('Request a limit increase for a customer') .requiredOption('--per-transaction ', 'Per-transaction limit in cents') .requiredOption('--daily ', 'Daily limit in cents') .requiredOption('--monthly ', 'Monthly limit in cents') .requiredOption('--supporting-document-type ', 'Supporting document type (individual_bank_statement, individual_tax_return, individual_proof_of_income, business_bank_statement, business_financial_statements, business_tax_return)') .requiredOption('--supporting-document-file ', 'Supporting document URL (upload via `blindpay upload` first)') .option('--json', 'Output as JSON', false) - .action((id, opts) => createReceiverLimitIncrease(id, opts)) + .action((id, opts) => createCustomerLimitIncrease(id, opts)) + +customers + .command('rfi_get ') + .description('Get the open RFI for a customer') + .option('--json', 'Output as JSON', false) + .action((id, opts) => getCustomerRfi(id, opts)) + +customers + .command('rfi_submit ') + .description('Submit an RFI response for a customer') + .requiredOption('--body ', 'JSON body (e.g. \'{"address":"..."}\')') + .option('--json', 'Output as JSON', false) + .action((id, opts) => submitCustomerRfi(id, opts)) // ── Bank Accounts ─────────────────────────────────────────────────────── const bankAccounts = program.command('bank_accounts').description('Manage bank accounts') .addHelpText('after', ` Examples: - $ blindpay bank_accounts list --receiver-id - $ blindpay bank_accounts get --receiver-id - $ blindpay bank_accounts create --receiver-id --type ach --routing-number 021000021 --account-number 123456789 - $ blindpay bank_accounts create --receiver-id --type pix --pix-key user@email.com - $ blindpay bank_accounts delete --receiver-id `) + $ blindpay bank_accounts list --customer-id + $ blindpay bank_accounts get --customer-id + $ blindpay bank_accounts create --customer-id --type ach --routing-number 021000021 --account-number 123456789 + $ blindpay bank_accounts create --customer-id --type pix --pix-key user@email.com + $ blindpay bank_accounts delete --customer-id `) bankAccounts .command('list') - .description('List bank accounts for a receiver') - .requiredOption('--receiver-id ', 'Receiver ID') + .description('List bank accounts for a customer') + .requiredOption('--customer-id ', 'Customer ID') .option('--json', 'Output as JSON', false) .action(opts => listBankAccounts(opts)) bankAccounts .command('get ') .description('Get a bank account by ID') - .requiredOption('--receiver-id ', 'Receiver ID') + .requiredOption('--customer-id ', 'Customer ID') .option('--json', 'Output as JSON', false) .action((id, opts) => getBankAccount(id, opts)) bankAccounts .command('create') .description('Create a new bank account') - .requiredOption('--receiver-id ', 'Receiver ID') + .requiredOption('--customer-id ', 'Customer ID') .option('--type ', 'Bank account type (ach, wire, pix, etc.)', 'ach') .option('--name ', 'Account name') .option('--beneficiary-name ', 'Beneficiary name') @@ -262,7 +285,7 @@ bankAccounts bankAccounts .command('delete ') .description('Delete a bank account') - .requiredOption('--receiver-id ', 'Receiver ID') + .requiredOption('--customer-id ', 'Customer ID') .option('--json', 'Output as JSON', false) .action((id, opts) => deleteBankAccount(id, opts)) @@ -270,40 +293,49 @@ bankAccounts const blockchainWallets = program.command('blockchain_wallets').description('Manage blockchain wallets') .addHelpText('after', ` Examples: - $ blindpay blockchain_wallets list --receiver-id - $ blindpay blockchain_wallets get --receiver-id - $ blindpay blockchain_wallets create --receiver-id --address 0x... --network base - $ blindpay blockchain_wallets delete --receiver-id `) + $ blindpay blockchain_wallets list --customer-id + $ blindpay blockchain_wallets get --customer-id + $ blindpay blockchain_wallets create --customer-id --address 0x... --network base + $ blindpay blockchain_wallets sign_message --customer-id + $ blindpay blockchain_wallets delete --customer-id `) blockchainWallets .command('list') - .description('List blockchain wallets for a receiver') - .requiredOption('--receiver-id ', 'Receiver ID') + .description('List blockchain wallets for a customer') + .requiredOption('--customer-id ', 'Customer ID') .option('--json', 'Output as JSON', false) .action(opts => listBlockchainWallets(opts)) blockchainWallets .command('get ') .description('Get a blockchain wallet by ID') - .requiredOption('--receiver-id ', 'Receiver ID') + .requiredOption('--customer-id ', 'Customer ID') .option('--json', 'Output as JSON', false) .action((id, opts) => getBlockchainWallet(id, opts)) blockchainWallets .command('create') .description('Create a new blockchain wallet') - .requiredOption('--receiver-id ', 'Receiver ID') - .requiredOption('--address
', 'Wallet address') + .requiredOption('--customer-id ', 'Customer ID') + .option('--address
', 'Wallet address') .option('--network ', 'Blockchain network', 'base') .option('--name ', 'Wallet name') - .option('--external-id ', 'External ID') + .option('--signature-tx-hash ', 'Signature transaction hash (for wallet ownership verification)') + .option('--is-account-abstraction', 'Mark wallet as account abstraction', false) .option('--json', 'Output as JSON', false) .action(opts => createBlockchainWallet(opts)) +blockchainWallets + .command('sign_message') + .description('Get the message to sign for blockchain wallet verification') + .requiredOption('--customer-id ', 'Customer ID') + .option('--json', 'Output as JSON', false) + .action(opts => getBlockchainWalletSignMessage(opts)) + blockchainWallets .command('delete ') .description('Delete a blockchain wallet') - .requiredOption('--receiver-id ', 'Receiver ID') + .requiredOption('--customer-id ', 'Customer ID') .option('--json', 'Output as JSON', false) .action((id, opts) => deleteBlockchainWallet(id, opts)) @@ -508,44 +540,87 @@ apiKeys const virtualAccounts = program.command('virtual_accounts').description('Manage virtual accounts') .addHelpText('after', ` Examples: - $ blindpay virtual_accounts list --receiver-id - $ blindpay virtual_accounts create --receiver-id --blockchain-wallet-id `) + $ blindpay virtual_accounts list --customer-id + $ blindpay virtual_accounts get --customer-id + $ blindpay virtual_accounts create --customer-id --banking-partner jpmorgan --token USDC --blockchain-wallet-id + $ blindpay virtual_accounts update --customer-id --token USDC --blockchain-wallet-id `) virtualAccounts .command('list') - .description('List virtual accounts for a receiver') - .requiredOption('--receiver-id ', 'Receiver ID') + .description('List virtual accounts for a customer') + .requiredOption('--customer-id ', 'Customer ID') .option('--json', 'Output as JSON', false) .action(opts => listVirtualAccounts(opts)) +virtualAccounts + .command('get ') + .description('Get a virtual account by ID') + .requiredOption('--customer-id ', 'Customer ID') + .option('--json', 'Output as JSON', false) + .action((id, opts) => getVirtualAccount(id, opts)) + virtualAccounts .command('create') .description('Create a virtual account') - .requiredOption('--receiver-id ', 'Receiver ID') + .requiredOption('--customer-id ', 'Customer ID') + .requiredOption('--banking-partner ', 'Banking partner (jpmorgan, citi, hsbc, cfsb)') + .requiredOption('--token ', 'Token (USDC, USDT, USDB)') .requiredOption('--blockchain-wallet-id ', 'Blockchain wallet ID') + .option('--sole-proprietor-doc-type ', 'Sole proprietor document type (master_service_agreement, salary_slip, bank_statement)') + .option('--sole-proprietor-doc-file ', 'Sole proprietor document file URL') .option('--json', 'Output as JSON', false) .action(opts => createVirtualAccount(opts)) +virtualAccounts + .command('update ') + .description('Update a virtual account') + .requiredOption('--customer-id ', 'Customer ID') + .requiredOption('--token ', 'Token (USDC, USDT, USDB)') + .requiredOption('--blockchain-wallet-id ', 'Blockchain wallet ID') + .option('--json', 'Output as JSON', false) + .action((id, opts) => updateVirtualAccount(id, opts)) + // ── Offramp Wallets ───────────────────────────────────────────────────── const offrampWallets = program.command('offramp_wallets').description('Manage offramp wallets') .addHelpText('after', ` Examples: - $ blindpay offramp_wallets list --receiver-id --bank-account-id `) + $ blindpay offramp_wallets list --customer-id --bank-account-id + $ blindpay offramp_wallets get --customer-id --bank-account-id + $ blindpay offramp_wallets create --customer-id --bank-account-id --network base`) offrampWallets .command('list') .description('List offramp wallets') - .requiredOption('--receiver-id ', 'Receiver ID') + .requiredOption('--customer-id ', 'Customer ID') .requiredOption('--bank-account-id ', 'Bank account ID') .option('--json', 'Output as JSON', false) .action(opts => listOfframpWallets(opts)) +offrampWallets + .command('create') + .description('Create an offramp wallet') + .requiredOption('--customer-id ', 'Customer ID') + .requiredOption('--bank-account-id ', 'Bank account ID') + .requiredOption('--network ', 'Blockchain network') + .option('--external-id ', 'External ID') + .option('--json', 'Output as JSON', false) + .action(opts => createOfframpWallet(opts)) + +offrampWallets + .command('get ') + .description('Get an offramp wallet by ID') + .requiredOption('--customer-id ', 'Customer ID') + .requiredOption('--bank-account-id ', 'Bank account ID') + .option('--json', 'Output as JSON', false) + .action((id, opts) => getOfframpWallet(id, opts)) + // ── Instances ────────────────────────────────────────────────────────── const instances = program.command('instances').description('Manage your instance') .addHelpText('after', ` Examples: $ blindpay instances get $ blindpay instances update --name "My Instance" + $ blindpay instances migrate_ownership --user-id $ blindpay instances members list`) instances @@ -562,6 +637,13 @@ instances .option('--json', 'Output as JSON', false) .action(opts => updateInstance(opts)) +instances + .command('migrate_ownership') + .description('Migrate instance ownership to another user') + .requiredOption('--user-id ', 'User ID to transfer ownership to') + .option('--json', 'Output as JSON', false) + .action(opts => migrateInstanceOwnership(opts)) + const instanceMembers = instances.command('members').description('Manage instance members') instanceMembers @@ -596,37 +678,37 @@ available const wallets = program.command('wallets').description('Manage custodial wallets (BlindPay-managed, with balance)') .addHelpText('after', ` Examples: - $ blindpay wallets list --receiver-id - $ blindpay wallets get --receiver-id - $ blindpay wallets balance --receiver-id - $ blindpay wallets create --receiver-id --network polygon --name "Main Wallet" - $ blindpay wallets delete --receiver-id `) + $ blindpay wallets list --customer-id + $ blindpay wallets get --customer-id + $ blindpay wallets balance --customer-id + $ blindpay wallets create --customer-id --network polygon --name "Main Wallet" + $ blindpay wallets delete --customer-id `) wallets .command('list') - .description('List custodial wallets for a receiver') - .requiredOption('--receiver-id ', 'Receiver ID') + .description('List custodial wallets for a customer') + .requiredOption('--customer-id ', 'Customer ID') .option('--json', 'Output as JSON', false) .action(opts => listWallets(opts)) wallets .command('get ') .description('Get a custodial wallet by ID') - .requiredOption('--receiver-id ', 'Receiver ID') + .requiredOption('--customer-id ', 'Customer ID') .option('--json', 'Output as JSON', false) .action((id, opts) => getWallet(id, opts)) wallets .command('balance ') .description('Get a custodial wallet balance (USDC/USDT/USDB)') - .requiredOption('--receiver-id ', 'Receiver ID') + .requiredOption('--customer-id ', 'Customer ID') .option('--json', 'Output as JSON', false) .action((id, opts) => getWalletBalance(id, opts)) wallets .command('create') .description('Create a new custodial wallet') - .requiredOption('--receiver-id ', 'Receiver ID') + .requiredOption('--customer-id ', 'Customer ID') .requiredOption('--network ', 'Network') .requiredOption('--name ', 'Wallet name') .option('--external-id ', 'External ID') @@ -636,7 +718,7 @@ wallets wallets .command('delete ') .description('Delete a custodial wallet') - .requiredOption('--receiver-id ', 'Receiver ID') + .requiredOption('--customer-id ', 'Customer ID') .option('--json', 'Output as JSON', false) .action((id, opts) => deleteWallet(id, opts)) @@ -727,11 +809,12 @@ tos .action(opts => initiateTos(opts)) // ── Upload ───────────────────────────────────────────────────────────── -const upload = program.command('upload').description('Upload a file to a BlindPay bucket') +const upload = program.command('upload').description('Upload files and analyze documents') .addHelpText('after', ` Examples: $ blindpay upload file --file ./document.pdf --bucket limit_increase - $ blindpay upload file --file ./avatar.png --bucket avatar --instance-id `) + $ blindpay upload file --file ./avatar.png --bucket avatar --instance-id + $ blindpay upload analyze --body '{"file_url":"https://..."}'`) upload .command('file') @@ -742,17 +825,24 @@ upload .option('--json', 'Output as JSON', false) .action(opts => uploadFile(opts)) +upload + .command('analyze') + .description('Analyze a document and get an approval rate prediction') + .requiredOption('--body ', 'JSON body with document details (e.g. \'{"file_url":"..."}\')') + .option('--json', 'Output as JSON', false) + .action(opts => analyzeDocument(opts)) + // ── Schema ───────────────────────────────────────────────────────────── const schema = program.command('schema').description('Introspect CLI resource schemas (JSON output for LLM/automation use)') .addHelpText('after', ` Examples: $ blindpay schema # list all resources - $ blindpay schema receivers # full schema for receivers + $ blindpay schema customers # full schema for customers $ blindpay schema bank_accounts # schema + available rails $ blindpay schema bank_accounts --rail ach # schema + rail-specific fields`) schema - .argument('[resource]', 'Resource name (e.g. receivers, payouts, bank_accounts)') + .argument('[resource]', 'Resource name (e.g. customers, payouts, bank_accounts)') .option('--rail ', 'Show rail-specific fields (bank_accounts only)') .action((resource, opts) => { if (!resource) {