diff --git a/connectors/connector-xero/def.ts b/connectors/connector-xero/def.ts index 985899a4..0de1430b 100644 --- a/connectors/connector-xero/def.ts +++ b/connectors/connector-xero/def.ts @@ -26,7 +26,7 @@ export const xeroSchemas = { sourceOutputEntities: R.mapValues(XERO_ENTITY_NAME, () => z.unknown()), } satisfies ConnectorSchemas -export const helpers = connHelpers(xeroSchemas) +export const xeroHelpers = connHelpers(xeroSchemas) export const xeroDef = { metadata: { diff --git a/connectors/connector-xero/index.ts b/connectors/connector-xero/index.ts index 81f31bd7..d180f7cb 100644 --- a/connectors/connector-xero/index.ts +++ b/connectors/connector-xero/index.ts @@ -1,4 +1,10 @@ +import type {initXeroSDK} from '@opensdks/sdk-xero' + // codegen:start {preset: barrel, include: "./{*.{ts,tsx},*/index.{ts,tsx}}", exclude: "./**/*.{d,spec,test,fixture,gen,node}.{ts,tsx}"} export * from './def' export * from './server' // codegen:end + +export * from '@opensdks/sdk-xero' + +export type XeroSDK = ReturnType diff --git a/connectors/connector-xero/server.ts b/connectors/connector-xero/server.ts index b8f75f8b..643ee032 100644 --- a/connectors/connector-xero/server.ts +++ b/connectors/connector-xero/server.ts @@ -1,8 +1,8 @@ import {initXeroSDK} from '@opensdks/sdk-xero' import type {ConnectorServer} from '@usevenice/cdk' import {nangoProxyLink} from '@usevenice/cdk' -import {rxjs} from '@usevenice/util' -import {type xeroSchemas} from './def' +import {Rx, rxjs} from '@usevenice/util' +import {xeroHelpers, type xeroSchemas} from './def' export const xeroServer = { // Would be good if this was async... @@ -34,7 +34,7 @@ export const xeroServer = { }, sourceSync: ({instance: xero}) => { console.log('[xero] Starting sync') - const getAll = async () => { + async function* iterateEntities() { // TODO: Should handle more than one tenant Id const tenantId = await xero.identity .GET('/Connections') @@ -44,18 +44,23 @@ export const xeroServer = { 'Missing access to any tenants. Check xero token permission', ) } + const result = await xero.accounting.GET('/Accounts', { params: {header: {'xero-tenant-id': tenantId}}, }) - console.log('result', result) - return result - } - getAll() - .then((res) => console.log('[data test]', res)) - .catch((err) => console.log('error', err)) + if (result.data.Accounts) { + yield result.data.Accounts?.map((a) => + xeroHelpers._opData('Accounts', a.AccountID!, a), + ) + } + } - return rxjs.empty() // TODO: replace with data above. + return rxjs + .from(iterateEntities()) + .pipe( + Rx.mergeMap((ops) => rxjs.from([...ops, xeroHelpers._op('commit')])), + ) }, } satisfies ConnectorServer> diff --git a/docs/samples/banking-test.ts b/docs/samples/banking-test.ts index cbc6ef83..51bdb90d 100644 --- a/docs/samples/banking-test.ts +++ b/docs/samples/banking-test.ts @@ -3,7 +3,8 @@ import {createVeniceClient} from '@usevenice/sdk' const venice = createVeniceClient({ apiKey: process.env['_VENICE_API_KEY'], apiHost: process.env['_VENICE_API_HOST'], - resourceId: process.env['_QBO_RESOURCE_ID'], + // resourceId: process.env['_QBO_RESOURCE_ID'], + resourceId: process.env['_XERO_RESOURCE_ID'], }) void venice.GET('/verticals/banking/category').then((r) => { diff --git a/packages/engine-backend/router/index.ts b/packages/engine-backend/router/index.ts index f0fec05b..01a46581 100644 --- a/packages/engine-backend/router/index.ts +++ b/packages/engine-backend/router/index.ts @@ -11,6 +11,7 @@ import { outreachAdapter, qboAdapter, salesloftAdapter, + xeroAdapter, } from '@usevenice/cdk/verticals' import {remoteProcedure, trpc} from './_base' import {adminRouter} from './adminRouter' @@ -26,7 +27,7 @@ import {systemRouter} from './systemRouter' const bankingRouter = createBankingRouter({ trpc, remoteProcedure, - adapterByName: {qbo: qboAdapter}, + adapterByName: {qbo: qboAdapter, xero: xeroAdapter}, }) const accountingRouter = createAccountingRouter({ trpc, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91da3c43..aed687c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1830,6 +1830,9 @@ importers: '@usevenice/connector-qbo': specifier: workspace:* version: link:../../connectors/connector-qbo + '@usevenice/connector-xero': + specifier: workspace:* + version: link:../../connectors/connector-xero '@usevenice/types': specifier: workspace:* version: link:../../utils/types diff --git a/verticals/vertical-banking/adapters/xero-adapter.ts b/verticals/vertical-banking/adapters/xero-adapter.ts new file mode 100644 index 00000000..4bcc17f0 --- /dev/null +++ b/verticals/vertical-banking/adapters/xero-adapter.ts @@ -0,0 +1,46 @@ +import type {Oas_accounting, XeroSDK} from 'connectors/connector-xero' +import type {StrictObj} from '@usevenice/vdk' +import {mapper, z, zCast} from '@usevenice/vdk' +import type {VerticalBanking} from '../banking' +import {zBanking} from '../banking' + +type Xero = Oas_accounting['components']['schemas'] + +const mappers = { + category: mapper( + zCast>(), + zBanking.category.extend({_raw: z.unknown().optional()}), + { + id: 'AccountID', + name: 'Name', + _raw: (a) => a, + }, + ), +} + +export const xeroAdapter = { + listCategories: async ({instance}) => { + // TODO: Abstract this away please... + const tenantId = await instance.identity + .GET('/Connections') + .then((r) => r.data?.[0]?.tenantId) + if (!tenantId) { + throw new Error( + 'Missing access to any tenants. Check xero token permission', + ) + } + + const res = await instance.accounting.GET('/Accounts', { + params: { + header: {'xero-tenant-id': tenantId}, + query: { + where: 'Class=="REVENUE"||Class=="EXPENSE"', + }, + }, + }) + return { + hasNextPage: false, + items: (res.data.Accounts ?? []).map(mappers.category), + } + }, +} satisfies VerticalBanking<{instance: XeroSDK}> diff --git a/verticals/vertical-banking/banking.ts b/verticals/vertical-banking/banking.ts index 01efb6e2..a19cdab1 100644 --- a/verticals/vertical-banking/banking.ts +++ b/verticals/vertical-banking/banking.ts @@ -4,6 +4,7 @@ import type {AnyEntityPayload, Id, Link} from '@usevenice/cdk' import type {PlaidSDKTypes} from '@usevenice/connector-plaid' import type {postgresHelpers} from '@usevenice/connector-postgres' import type {QBO} from '@usevenice/connector-qbo' +import type {Oas_accounting} from '@usevenice/connector-xero' import type {StrictObj} from '@usevenice/types' import type {RouterMap, RouterMeta, VerticalRouterOpts} from '@usevenice/vdk' import { @@ -16,6 +17,7 @@ import { } from '@usevenice/vdk' type Plaid = PlaidSDKTypes['oas']['components'] +type Xero = Oas_accounting['components']['schemas'] export const zBanking = { transaction: z @@ -83,6 +85,39 @@ export function bankingLink(ctx: { if (op.type !== 'data') { return rxjs.of(op) } + + if (ctx.source.connectorConfig.connectorName === 'xero') { + if (op.data.entityName === 'Accounts') { + const entity = op.data.entity as Xero['Account'] + if (entity.Class === 'REVENUE' || entity.Class === 'EXPENSE') { + const mapped = applyMapper( + mappers.xero.category, + op.data.entity as Xero['Account'], + ) + return rxjs.of({ + ...op, + data: { + id: mapped.id, + entityName: 'banking_category', + entity: {raw: op.data.entity, unified: mapped}, + } satisfies PostgresInputPayload, + }) + } else { + const mapped = applyMapper( + mappers.xero.accounts, + op.data.entity as Xero['Account'], + ) + return rxjs.of({ + ...op, + data: { + id: mapped.id, + entityName: 'banking_account', + entity: {raw: op.data.entity, unified: mapped}, + } satisfies PostgresInputPayload, + }) + } + } + } if (ctx.source.connectorConfig.connectorName === 'qbo') { if (op.data.entityName === 'purchase') { const mapped = applyMapper( @@ -203,6 +238,16 @@ export function bankingLink(ctx: { } const mappers = { + xero: { + accounts: mapper(zCast>(), zBanking.account, { + id: 'AccountID', + name: 'Name', + }), + category: mapper(zCast>(), zBanking.account, { + id: 'AccountID', + name: 'Name', + }), + }, // Should be able to have input and output entity types in here also. qbo: { purchase: mapper( diff --git a/verticals/vertical-banking/index.ts b/verticals/vertical-banking/index.ts index e066b5a6..e52470e8 100644 --- a/verticals/vertical-banking/index.ts +++ b/verticals/vertical-banking/index.ts @@ -2,3 +2,4 @@ export * from './banking' // codegen:end export * from './adapters/qbo-adapter' +export * from './adapters/xero-adapter' diff --git a/verticals/vertical-banking/package.json b/verticals/vertical-banking/package.json index e59bf92e..c1a634fe 100644 --- a/verticals/vertical-banking/package.json +++ b/verticals/vertical-banking/package.json @@ -9,6 +9,7 @@ "@usevenice/connector-plaid": "workspace:*", "@usevenice/connector-postgres": "workspace:*", "@usevenice/connector-qbo": "workspace:*", + "@usevenice/connector-xero": "workspace:*", "@usevenice/types": "workspace:*" } }