Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 19 additions & 92 deletions src/app/api/quickbooks/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,103 +51,30 @@ export class AuthService extends BaseService {
return await Intuit.getInstance().authorizeUri(state)
}

async manageIncomeAccountRef(intuitApi: IntuitAPI): Promise<string> {
const existingIncomeAccRef = await intuitApi.getSingleIncomeAccount()
if (existingIncomeAccRef) {
return existingIncomeAccRef?.Id
}

console.info(
`IntuitAPI#manageIncomeAccountRef | No existing income account found. Creating new one.`,
)

const payload = {
Name: 'Assembly SOP Income',
Classification: 'Revenue',
AccountType: 'Income',
AccountSubType: 'SalesOfProductIncome',
Active: true,
}
const incomeAccRef = await intuitApi.createAccount(payload)
return incomeAccRef.Id
}

async manageExpenseAccountRef(intuitApi: IntuitAPI): Promise<string> {
const accName = 'Assembly Processing Fees'
const existingAccount = await intuitApi.getAnAccount(accName)
if (existingAccount) {
return existingAccount?.Id
}

// Docs: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account#the-account-object
const payload = {
Name: accName,
Classification: 'Expense',
AccountType: 'Expense', // Creating expense account.
AccountSubType: 'FinanceCosts',
Active: true,
}
const expenseAccRef = await intuitApi.createAccount(payload)
return expenseAccRef.Id
}

async manageAssetAccountRef(intuitApi: IntuitAPI): Promise<string> {
const accName = 'Assembly General Asset'
const existingAccount = await intuitApi.getAnAccount(accName)
if (existingAccount) {
return existingAccount?.Id
}

/**
* Need to create this account as the source of cash for the company. This account will be referenced while creating a purchase as Expense for absorbed fee.
* Doc: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/purchase#create-a-purchase
* */

// Docs: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account#the-account-object
const payload = {
Name: accName,
Classification: 'Asset',
AccountType: 'Bank', // Create Bank account. Default account subtype is "CashOnHand".
Active: true,
}
const assetAccRef = await intuitApi.createAccount(payload)
return assetAccRef.Id
}

async handleAccountReferences(
intuitApi: IntuitAPI,
payload: QBPortalConnectionCreateSchemaType,
tokenService: TokenService,
) {
// manage acc ref from intuit and store in qbPortalConnections table
let incomeAccountRef = await tokenService.checkAndUpdateAccountStatus(
AccountTypeObj.Income,
payload.intuitRealmId,
intuitApi,
payload.incomeAccountRef,
),
expenseAccountRef = await tokenService.checkAndUpdateAccountStatus(
AccountTypeObj.Expense,
payload.intuitRealmId,
intuitApi,
payload.expenseAccountRef,
),
assetAccountRef = await tokenService.checkAndUpdateAccountStatus(
AccountTypeObj.Asset,
payload.intuitRealmId,
intuitApi,
payload.assetAccountRef,
)
if (!incomeAccountRef) {
incomeAccountRef = await this.manageIncomeAccountRef(intuitApi)
}

if (!expenseAccountRef) {
expenseAccountRef = await this.manageExpenseAccountRef(intuitApi)
}
if (!assetAccountRef) {
assetAccountRef = await this.manageAssetAccountRef(intuitApi)
}
const incomeAccountRef = await tokenService.checkAndUpdateAccountStatus(
AccountTypeObj.Income,
payload.intuitRealmId,
intuitApi,
payload.incomeAccountRef,
)
const expenseAccountRef = await tokenService.checkAndUpdateAccountStatus(
AccountTypeObj.Expense,
payload.intuitRealmId,
intuitApi,
payload.expenseAccountRef,
)
const assetAccountRef = await tokenService.checkAndUpdateAccountStatus(
AccountTypeObj.Asset,
payload.intuitRealmId,
intuitApi,
payload.assetAccountRef,
)
return { ...payload, incomeAccountRef, expenseAccountRef, assetAccountRef }
}

Expand Down Expand Up @@ -205,7 +132,7 @@ export class AuthService extends BaseService {
incomeAccountRef: existingToken?.incomeAccountRef || '',
expenseAccountRef: existingToken?.expenseAccountRef || '',
assetAccountRef: existingToken?.assetAccountRef || '',
isSuspended: false, // default vaalue is false when created. Added this for the re-auth case.
isSuspended: false, // default value is false when created. Added this for the re-auth case.
}
const intuitApi = new IntuitAPI({
accessToken: tokenInfo.access_token,
Expand Down
161 changes: 142 additions & 19 deletions src/app/api/quickbooks/token/token.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,49 +140,168 @@ export class TokenService extends BaseService {
return portal
}

private async removeAccountMapping(
private async updateAccountMapping(
accountType: AccountType,
realmId: string,
accountRef: string,
) {
let payload = {}
let payload: QBPortalConnectionUpdateSchemaType = {}
switch (accountType) {
case AccountTypeObj.Income:
payload = {
incomeAccountRef: '',
}
payload = { incomeAccountRef: accountRef }
break
case AccountTypeObj.Expense:
payload = {
expenseAccountRef: '',
}
payload = { expenseAccountRef: accountRef }
break
case AccountTypeObj.Asset:
payload = {
assetAccountRef: '',
}
payload = { assetAccountRef: accountRef }
break
default:
throw new APIError(
httpStatus.BAD_REQUEST,
`Cannot remove account mapping for account type ${accountType}`,
`Cannot update account mapping for account type ${accountType}`,
)
}
await this.updateQBPortalConnection(
const updated = await this.updateQBPortalConnection(
payload,
and(
eq(QBPortalConnection.portalId, this.user.workspaceId),
eq(QBPortalConnection.intuitRealmId, realmId),
) as WhereClause,
)
if (!updated) {
console.error(
`TokenService#updateAccountMapping | No row updated for portalId=${this.user.workspaceId} realmId=${realmId}. Account ref may be stale in DB.`,
)
}
}

private async getOrCreateIncomeAccountRef(
intuitApi: IntuitAPI,
): Promise<string> {
const existingIncomeAccRef = await intuitApi.getSingleIncomeAccount()
if (existingIncomeAccRef?.Id) {
return existingIncomeAccRef.Id
}

console.info(
'TokenService#getOrCreateIncomeAccountRef | No existing income account found. Creating new one.',
)

const payload = {
Name: 'Assembly SOP Income',
Classification: 'Revenue',
AccountType: 'Income',
AccountSubType: 'SalesOfProductIncome',
Active: true,
}
const incomeAccRef = await intuitApi.createAccount(payload)
if (!incomeAccRef?.Id) {
throw new APIError(
httpStatus.INTERNAL_SERVER_ERROR,
'TokenService#getOrCreateIncomeAccountRef | Created account has no Id',
)
}
return incomeAccRef.Id
}

private async getOrCreateExpenseAccountRef(
intuitApi: IntuitAPI,
): Promise<string> {
const accName = 'Assembly Processing Fees'
const existingAccount = await intuitApi.getAnAccount(accName)
if (existingAccount?.Id) {
return existingAccount.Id
}

console.info(
'TokenService#getOrCreateExpenseAccountRef | No existing expense account found. Creating new one.',
)

const payload = {
Name: accName,
Classification: 'Expense',
AccountType: 'Expense',
AccountSubType: 'FinanceCosts',
Active: true,
}
const expenseAccRef = await intuitApi.createAccount(payload)
if (!expenseAccRef?.Id) {
throw new APIError(
httpStatus.INTERNAL_SERVER_ERROR,
'TokenService#getOrCreateExpenseAccountRef | Created account has no Id',
)
}
return expenseAccRef.Id
}

private async getOrCreateAssetAccountRef(
intuitApi: IntuitAPI,
): Promise<string> {
const accName = 'Assembly General Asset'
const existingAccount = await intuitApi.getAnAccount(accName)
if (existingAccount?.Id) {
return existingAccount.Id
}

console.info(
'TokenService#getOrCreateAssetAccountRef | No existing asset account found. Creating new one.',
)

// Need to create this account as the source of cash for the company. This account will be referenced while creating a purchase as Expense for absorbed fee.
// Docs: https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account#the-account-object
const payload = {
Name: accName,
Classification: 'Asset',
AccountType: 'Bank', // Create Bank account. Default account subtype is "CashOnHand".
Active: true,
}
const assetAccRef = await intuitApi.createAccount(payload)
if (!assetAccRef?.Id) {
throw new APIError(
httpStatus.INTERNAL_SERVER_ERROR,
'TokenService#getOrCreateAssetAccountRef | Created account has no Id',
)
}
return assetAccRef.Id
}

private async restoreAccountRef(
accountType: AccountType,
intuitApi: IntuitAPI,
): Promise<string> {
switch (accountType) {
case AccountTypeObj.Income:
return this.getOrCreateIncomeAccountRef(intuitApi)
case AccountTypeObj.Expense:
return this.getOrCreateExpenseAccountRef(intuitApi)
case AccountTypeObj.Asset:
return this.getOrCreateAssetAccountRef(intuitApi)
default:
throw new APIError(
httpStatus.BAD_REQUEST,
`Cannot restore account ref for account type ${accountType}`,
)
}
}

async checkAndUpdateAccountStatus(
accountType: AccountType,
realmId: string,
intuitApi: IntuitAPI,
accountId?: string,
) {
if (!accountId) return
): Promise<string> {
if (!accountId) {
console.info(
`TokenService#checkAndUpdateAccountStatus. No accountId provided for ${accountType}. Restoring account ref...`,
)
const restoredRef = await this.restoreAccountRef(accountType, intuitApi)
await this.updateAccountMapping(accountType, realmId, restoredRef)
console.info(
`TokenService#checkAndUpdateAccountStatus. Restored ${accountType} account ref to ${restoredRef}.`,
)
return restoredRef
}

console.info(
'TokenService#checkAndUpdateAccountStatus. Updating account status ...',
Expand All @@ -197,13 +316,17 @@ export class TokenService extends BaseService {
'TokenService#checkAndUpdateAccountStatus. Account query response',
})

// if no account found, remove mapping
// if no account found, restore account ref
if (!account) {
console.info(
`TokenService#checkAndUpdateAccountStatus. Account not found for Id ${accountId} in QuickBooks. Unmapping the account...`,
`TokenService#checkAndUpdateAccountStatus. Account not found for Id ${accountId} in QuickBooks. Restoring account ref...`,
)
const restoredRef = await this.restoreAccountRef(accountType, intuitApi)
await this.updateAccountMapping(accountType, realmId, restoredRef)
console.info(
`TokenService#checkAndUpdateAccountStatus. Restored ${accountType} account ref to ${restoredRef}.`,
)
await this.removeAccountMapping(accountType, realmId)
return
return restoredRef
} else if (!account.Active) {
console.info(
`TokenService#checkAndUpdateAccountStatus. Account with Id ${accountId} is inactive. Making it active...`,
Expand Down
Loading