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
2 changes: 1 addition & 1 deletion src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const getSettings = async (user: User, connection: XeroConnection) => {
if (connection.tenantId) {
// Using tenantID even though tokenSet might be expired because the sync-settings feature don't need to perform Xero API calls
const settingsService = new SettingsService(user, connection as XeroConnectionWithTokenSet)
settings = await settingsService.getSettings()
settings = await settingsService.getOrCreateSettings()
} else {
settings = defaultSettings
}
Expand Down
3 changes: 2 additions & 1 deletion src/features/failed-syncs/lib/RetryFailedSyncs.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AuthService from '@auth/lib/Auth.service'
import { MAX_RETRY_ATTEMPTS } from '@failed-syncs/lib/constants'
import WebhookService from '@webhook/lib/webhook.service'
import { eq, lte } from 'drizzle-orm'
import env from '@/config/server.env'
Expand All @@ -13,7 +14,7 @@ class RetryFailedSyncsService {
const failedSyncRecords = await db
.select()
.from(failedSyncs)
.where(lte(failedSyncs.attempts, 3))
.where(lte(failedSyncs.attempts, MAX_RETRY_ATTEMPTS))

const tokenMap: Record<string, string> = {}

Expand Down
1 change: 1 addition & 0 deletions src/features/failed-syncs/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MAX_RETRY_ATTEMPTS = 3
2 changes: 1 addition & 1 deletion src/features/invoice-sync/lib/SyncedContacts.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class SyncedContactsService extends AuthenticatedXeroService {
logger.info('SyncedContactsService#getSyncedContact :: Getting synced contact for', clientId)

const settingsService = new SettingsService(this.user, this.connection)
const { useCompanyName } = await settingsService.getSettings()
const { useCompanyName } = await settingsService.getOrCreateSettings()

const client = clientId ? await this.copilot.getClient(clientId) : undefined

Expand Down
17 changes: 5 additions & 12 deletions src/features/settings/hooks/useAppMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,32 @@
import { useAuthContext } from '@auth/hooks/useAuth'
import { disconnectApp } from '@settings/actions/disconnectApp'
import { useSettingsContext } from '@settings/hooks/useSettings'
import { useState } from 'react'
import { useCallback } from 'react'
import { useActionsMenu } from '@/lib/copilot/hooks/app-bridge'
import { Icons } from '@/lib/copilot/hooks/app-bridge/types'

export const useAppBridge = ({ token }: { token: string }) => {
const { connectionStatus } = useAuthContext()
const { isSyncEnabled, updateSettings, initialSettings } = useSettingsContext()

const disconnectAppAction = async () => {
const disconnectAppAction = useCallback(async () => {
await disconnectApp(token)
updateSettings({
isSyncEnabled: false,
initialSettings: { ...initialSettings, isSyncEnabled: false },
})
}
}, [token, updateSettings, initialSettings])

// biome-ignore lint/suspicious/useAwait: there is no async action being done here but the type signature requires it
const _downloadCsvAction = async () => {
const downloadCsvAction = useCallback(async () => {
const url = `/api/sync-logs?token=${token}`
const link = document.createElement('a')
link.href = url
link.download = 'sync-history.csv'
document.body.appendChild(link)
link.click()
link.remove()
}

// Quickfix for now (it will probably stay like this for the end of time)
const [downloadCsvAction, setDownloadCsvAction] = useState(() => _downloadCsvAction)

setTimeout(() => {
setDownloadCsvAction(() => _downloadCsvAction)
}, 1000)
}, [token])

let actions: { label: string; icon?: Icons; onClick: () => Promise<void> }[] = []
if (connectionStatus) {
Expand Down
61 changes: 39 additions & 22 deletions src/features/settings/lib/Settings.service.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,59 @@
import { defaultSettings } from '@settings/constants/defaults'
import { and, eq } from 'drizzle-orm'
import status from 'http-status'
import { getTableFields } from '@/db/db.helpers'
import { type SettingsFields, settings } from '@/db/schema/settings.schema'
import APIError from '@/errors/APIError'
import logger from '@/lib/logger'
import AuthenticatedXeroService from '@/lib/xero/AuthenticatedXero.service'

class SettingsService extends AuthenticatedXeroService {
async getSettings(): Promise<SettingsFields> {
private readonly settingsFields = getTableFields(settings, [
'syncProductsAutomatically',
'addAbsorbedFees',
'useCompanyName',
'isSyncEnabled',
'initialInvoiceSettingsMapping',
'initialProductSettingsMapping',
])

private readonly MAX_RETRY_ATTEMPTS = 3

async getOrCreateSettings(attempt = 0): Promise<SettingsFields> {
const syncSettings = await this.getSettings()
if (syncSettings) return syncSettings

const [newSyncSettings] = await this.db
.insert(settings)
.values({
portalId: this.user.portalId,
tenantId: this.connection.tenantId,
// Default sync settings
...defaultSettings,
})
.onConflictDoNothing()
.returning(this.settingsFields)

if (newSyncSettings) return newSyncSettings

if (attempt > this.MAX_RETRY_ATTEMPTS)
throw new APIError('Failed to query settings for user', status.INTERNAL_SERVER_ERROR)

return await this.getOrCreateSettings(attempt + 1)
}

async getSettings(): Promise<SettingsFields | undefined> {
logger.info('SettingsService#getSettings :: Getting settings for portalId', this.user.portalId)
const [syncSettings] = await this.db
.select(
getTableFields(settings, [
'syncProductsAutomatically',
'addAbsorbedFees',
'useCompanyName',
'isSyncEnabled',
'initialInvoiceSettingsMapping',
'initialProductSettingsMapping',
]),
)
.select(this.settingsFields)
.from(settings)
.where(
and(
eq(settings.portalId, this.user.portalId),
eq(settings.tenantId, this.connection.tenantId),
),
)
if (syncSettings) {
return syncSettings
}

const [newSyncSettings] = await this.db.insert(settings).values({
portalId: this.user.portalId,
tenantId: this.connection.tenantId,
// Default sync settings
...defaultSettings,
})
return newSyncSettings
return syncSettings
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/features/webhook/api/webhook.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const handleCopilotWebhook = async (req: NextRequest) => {
const connection = await authService.authorizeXeroForCopilotWorkspace()

const settingsService = new SettingsService(user, connection)
const settings = await settingsService.getSettings()
const settings = await settingsService.getOrCreateSettings()
if (!settings.isSyncEnabled) {
logger.info(
'webhook/api/webhook.controller#handleCopilotWebhook :: Sync is disabled for this workspace. Skipping...',
Expand Down
4 changes: 2 additions & 2 deletions src/features/webhook/lib/webhook.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ class WebhookService extends AuthenticatedXeroService {

const data = PaymentSucceededEventSchema.parse(eventData)
const settingsService = new SettingsService(this.user, this.connection)
const { addAbsorbedFees } = await settingsService.getSettings()
const { addAbsorbedFees } = await settingsService.getOrCreateSettings()
if (!addAbsorbedFees) {
logger.info(
'WebhookService#handlePaymentSucceeded :: addAbsorbedFees is disabled, skipping fee addition',
Expand All @@ -200,7 +200,7 @@ class WebhookService extends AuthenticatedXeroService {

private checkAutomaticProductSyncEnabled = async (): Promise<boolean> => {
const settingsService = new SettingsService(this.user, this.connection)
const settings = await settingsService.getSettings()
const settings = await settingsService.getOrCreateSettings()
logger.info(
'WebhookService#checkAutomaticProductSyncEnabled :: Sync Products Automatically is set to',
settings.syncProductsAutomatically,
Expand Down