Skip to content

Commit

Permalink
feat(xero): bank transactions ync
Browse files Browse the repository at this point in the history
  • Loading branch information
tonyxiao committed Apr 2, 2024
1 parent 9839bf3 commit e036a0d
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 16 deletions.
6 changes: 3 additions & 3 deletions connectors/connector-xero/def.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export const zSettings = oReso.extend({
export type XERO = components['schemas']

export const XERO_ENTITY_NAME = {
Accounts: 'Accounts',
PurchaseOrders: 'PurchaseOrders',
}
Account: 'Account',
BankTransaction: 'BankTransaction',
} as const

export const xeroSchemas = {
name: z.literal('xero'),
Expand Down
36 changes: 26 additions & 10 deletions connectors/connector-xero/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {initXeroSDK} from '@opensdks/sdk-xero'
import type {ConnectorServer} from '@usevenice/cdk'
import {nangoProxyLink} from '@usevenice/cdk'
import {Rx, rxjs} from '@usevenice/util'
import {xeroHelpers, type xeroSchemas} from './def'
import {XERO_ENTITY_NAME, xeroHelpers, type xeroSchemas} from './def'

export const xeroServer = {
// Would be good if this was async...
Expand Down Expand Up @@ -32,7 +32,7 @@ export const xeroServer = {
})
return xero
},
sourceSync: ({instance: xero}) => {
sourceSync: ({instance: xero, streams}) => {
console.log('[xero] Starting sync')
async function* iterateEntities() {
// TODO: Should handle more than one tenant Id
Expand All @@ -44,15 +44,31 @@ export const xeroServer = {
'Missing access to any tenants. Check xero token permission',
)
}
for (const type of Object.values(XERO_ENTITY_NAME)) {
if (!streams[type]) {
continue
}

const result = await xero.accounting.GET('/Accounts', {
params: {header: {'xero-tenant-id': tenantId}},
})

if (result.data.Accounts) {
yield result.data.Accounts?.map((a) =>
xeroHelpers._opData('Accounts', a.AccountID!, a),
)
const singular = type as 'BankTransaction'
const plural = `${singular}s` as const
const kId = `${singular}ID` as const
let page = 1
while (true) {
const result = await xero.accounting.GET(`/${plural}`, {
params: {header: {'xero-tenant-id': tenantId}, query: {page}},
})
if (result.data[plural]?.length) {
yield result.data[plural]?.map((a) =>
xeroHelpers._opData(singular, a[kId]!, a),
)
// Account does not support pagination, all or nothing...
if (type !== 'Account') {
page++
continue
}
}
break
}
}
}

Expand Down
60 changes: 57 additions & 3 deletions verticals/vertical-banking/banking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function bankingLink(ctx: {
}

if (ctx.source.connectorConfig.connectorName === 'xero') {
if (op.data.entityName === 'Accounts') {
if (op.data.entityName === 'Account') {
const entity = op.data.entity as Xero['Account']
if (entity.Class === 'REVENUE' || entity.Class === 'EXPENSE') {
const mapped = applyMapper(
Expand All @@ -104,7 +104,7 @@ export function bankingLink(ctx: {
})
} else {
const mapped = applyMapper(
mappers.xero.accounts,
mappers.xero.account,
op.data.entity as Xero['Account'],
)
return rxjs.of({
Expand All @@ -117,6 +117,42 @@ export function bankingLink(ctx: {
})
}
}
if (op.data.entityName === 'BankTransaction') {
// TODO: Dedupe from qbo.purchase later
const mapped = applyMapper(
mappers.xero.bank_transaction,
op.data.entity as Xero['BankTransaction'],
)
// TODO: Make this better, should at the minimum apply to both Plaid & QBO, options are
// 1) Banking link needs to take input parameters to determine if by default
// transactions should go through if metadata is missing or not
// 2) Banking vertical should include abstraction for account / category selection UI etc.
// 3) Extract this into a more generic filtering link that works for ANY entity.
// In addition, will need to handle incremental sync state reset when we change stream filtering
// parameter like this, as well as deleting the no longer relevant entities in destination
if (
// Support both name and ID
!categories[mapped.category_name ?? ''] &&
!categories[mapped.category_id ?? '']
) {
console.log(
`[banking] skip txn ${mapped.id} in ${mapped.category_id}: ${mapped.category_name}`,
)
return rxjs.EMPTY
} else {
console.log(
`[banking] allow txn ${mapped.id} in ${mapped.category_id}: ${mapped.category_name}`,
)
}
return rxjs.of({
...op,
data: {
id: mapped.id,
entityName: 'banking_transaction',
entity: {raw: op.data.entity, unified: mapped},
} satisfies PostgresInputPayload,
})
}
}
if (ctx.source.connectorConfig.connectorName === 'qbo') {
if (op.data.entityName === 'purchase') {
Expand Down Expand Up @@ -239,14 +275,32 @@ export function bankingLink(ctx: {

const mappers = {
xero: {
accounts: mapper(zCast<StrictObj<Xero['Account']>>(), zBanking.account, {
account: mapper(zCast<StrictObj<Xero['Account']>>(), zBanking.account, {
id: 'AccountID',
name: 'Name',
}),
category: mapper(zCast<StrictObj<Xero['Account']>>(), zBanking.account, {
id: 'AccountID',
name: 'Name',
}),
bank_transaction: mapper(
zCast<StrictObj<Xero['BankTransaction']>>(),
zBanking.transaction,
{
id: 'BankTransactionID',
amount: 'Total',
currency: 'CurrencyCode',
date: 'DateString' as 'Date', // empirically works https://share.cleanshot.com/0c6dlNsF
account_id: 'BankAccount.AccountID',
account_name: 'BankAccount.Name',
merchant_id: 'Contact.ContactID',
merchant_name: 'Contact.Name',
category_id: (t) => t.LineItems[0]?.AccountID ?? '',
description: (t) => t.LineItems[0]?.Description ?? '',
// Don't have data readily available for these...
// category_name is not readily available, only ID is provided
},
),
},
// Should be able to have input and output entity types in here also.
qbo: {
Expand Down

0 comments on commit e036a0d

Please sign in to comment.