Enhances expense submission with user wallet

Updates expense submission to require a user wallet.

Retrieves wallet information from the authentication context
and includes it in the expense submission request to the backend.
This ensures that expenses are correctly associated with the user's
wallet, enabling accurate tracking and management of expenses.
Also adds error handling and user feedback.
This commit is contained in:
padreug 2025-11-07 16:29:50 +01:00
parent 9ed674d0f3
commit 00a99995c9
3 changed files with 47 additions and 59 deletions

View file

@ -151,6 +151,7 @@ import {
Check
} from 'lucide-vue-next'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import { useAuth } from '@/composables/useAuthService'
import type { ExpensesAPI } from '../services/ExpensesAPI'
import type { Account, AccountNode } from '../types'
@ -167,8 +168,9 @@ interface Emits {
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
// Inject services
// Inject services and composables
const expensesAPI = injectService<ExpensesAPI>(SERVICE_TOKENS.EXPENSES_API)
const { user } = useAuth()
// Component state
const isLoading = ref(false)
@ -204,7 +206,14 @@ async function loadAccounts() {
error.value = null
try {
// Get wallet key from first wallet (invoice key for read operations)
const wallet = user.value?.wallets?.[0]
if (!wallet || !wallet.inkey) {
throw new Error('No wallet available. Please log in.')
}
accountHierarchy.value = await expensesAPI.getAccountHierarchy(
wallet.inkey,
props.rootAccount
)
} catch (err) {

View file

@ -197,6 +197,8 @@ import {
} from '@/components/ui/form'
import { DollarSign, X, ChevronLeft, Loader2 } from 'lucide-vue-next'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import { useAuth } from '@/composables/useAuthService'
import { useToast } from '@/core/composables/useToast'
import type { ExpensesAPI } from '../services/ExpensesAPI'
import type { Account } from '../types'
import AccountSelector from './AccountSelector.vue'
@ -209,8 +211,10 @@ interface Emits {
const emit = defineEmits<Emits>()
// Inject services
// Inject services and composables
const expensesAPI = injectService<ExpensesAPI>(SERVICE_TOKENS.EXPENSES_API)
const { user } = useAuth()
const toast = useToast()
// Component state
const currentStep = ref(1)
@ -255,22 +259,33 @@ function handleAccountSelected(account: Account) {
const onSubmit = form.handleSubmit(async (values) => {
if (!selectedAccount.value) {
console.error('[AddExpense] No account selected')
toast.error('No account selected', { description: 'Please select an account first' })
return
}
// Get wallet key from first wallet (invoice key for submission)
const wallet = user.value?.wallets?.[0]
if (!wallet || !wallet.inkey) {
toast.error('No wallet available', { description: 'Please log in to submit expenses' })
return
}
isSubmitting.value = true
try {
await expensesAPI.submitExpense({
await expensesAPI.submitExpense(wallet.inkey, {
description: values.description,
amount: values.amount,
expense_account: selectedAccount.value.name,
is_equity: values.isEquity,
user_wallet: '', // Will be filled by backend from auth
user_wallet: wallet.id,
reference: values.reference,
currency: 'sats'
})
// Show success message
toast.success('Expense submitted', { description: 'Your expense is pending admin approval' })
// Reset form and close
resetForm()
selectedAccount.value = null
@ -281,6 +296,8 @@ const onSubmit = form.handleSubmit(async (values) => {
emit('close')
} catch (error) {
console.error('[AddExpense] Error submitting expense:', error)
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
toast.error('Submission failed', { description: errorMessage })
} finally {
isSubmitting.value = false
}

View file

@ -15,7 +15,7 @@ export class ExpensesAPI extends BaseService {
protected readonly metadata = {
name: 'ExpensesAPI',
version: '1.0.0',
dependencies: ['AuthService', 'ToastService']
dependencies: [] // No dependencies - wallet key is passed as parameter
}
private get config() {
@ -34,18 +34,9 @@ export class ExpensesAPI extends BaseService {
}
/**
* Get authentication headers
* Get authentication headers with provided wallet key
*/
private getHeaders(): HeadersInit {
if (!this.authService) {
throw new Error('AuthService not available')
}
const walletKey = this.authService.walletKey
if (!walletKey) {
throw new Error('Wallet key not available. Please log in.')
}
private getHeaders(walletKey: string): HeadersInit {
return {
'Content-Type': 'application/json',
'X-Api-Key': walletKey
@ -58,11 +49,11 @@ export class ExpensesAPI extends BaseService {
* Note: Currently returns all accounts. Once castle API implements
* user permissions, use filter_by_user=true parameter.
*/
async getAccounts(): Promise<Account[]> {
async getAccounts(walletKey: string): Promise<Account[]> {
try {
const response = await fetch(`${this.baseUrl}/api/v1/accounts`, {
const response = await fetch(`${this.baseUrl}/castle/api/v1/accounts`, {
method: 'GET',
headers: this.getHeaders(),
headers: this.getHeaders(walletKey),
signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
})
@ -74,11 +65,6 @@ export class ExpensesAPI extends BaseService {
return accounts as Account[]
} catch (error) {
console.error('[ExpensesAPI] Error fetching accounts:', error)
if (this.toastService) {
this.toastService.error('Failed to load accounts', {
description: error instanceof Error ? error.message : 'Unknown error'
})
}
throw error
}
}
@ -89,8 +75,8 @@ export class ExpensesAPI extends BaseService {
* Converts flat account list to nested tree based on colon-separated names
* e.g., "Expenses:Groceries:Organic" becomes nested structure
*/
async getAccountHierarchy(rootAccount?: string): Promise<AccountNode[]> {
const accounts = await this.getAccounts()
async getAccountHierarchy(walletKey: string, rootAccount?: string): Promise<AccountNode[]> {
const accounts = await this.getAccounts(walletKey)
// Filter by root account if specified
let filteredAccounts = accounts
@ -151,11 +137,11 @@ export class ExpensesAPI extends BaseService {
/**
* Submit expense entry to castle
*/
async submitExpense(request: ExpenseEntryRequest): Promise<ExpenseEntry> {
async submitExpense(walletKey: string, request: ExpenseEntryRequest): Promise<ExpenseEntry> {
try {
const response = await fetch(`${this.baseUrl}/api/v1/entries/expense`, {
const response = await fetch(`${this.baseUrl}/castle/api/v1/entries/expense`, {
method: 'POST',
headers: this.getHeaders(),
headers: this.getHeaders(walletKey),
body: JSON.stringify(request),
signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
})
@ -168,28 +154,9 @@ export class ExpensesAPI extends BaseService {
}
const entry = await response.json()
if (this.toastService) {
this.toastService.success('Expense submitted', {
description: 'Your expense is pending admin approval'
})
}
return entry as ExpenseEntry
} catch (error) {
console.error('[ExpensesAPI] Error submitting expense:', error)
let errorMessage = 'Failed to submit expense'
if (error instanceof Error) {
errorMessage = error.message
}
if (this.toastService) {
this.toastService.error('Submission failed', {
description: errorMessage
})
}
throw error
}
}
@ -197,11 +164,11 @@ export class ExpensesAPI extends BaseService {
/**
* Get user's expense entries
*/
async getUserExpenses(): Promise<ExpenseEntry[]> {
async getUserExpenses(walletKey: string): Promise<ExpenseEntry[]> {
try {
const response = await fetch(`${this.baseUrl}/api/v1/entries/user`, {
const response = await fetch(`${this.baseUrl}/castle/api/v1/entries/user`, {
method: 'GET',
headers: this.getHeaders(),
headers: this.getHeaders(walletKey),
signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
})
@ -215,11 +182,6 @@ export class ExpensesAPI extends BaseService {
return entries as ExpenseEntry[]
} catch (error) {
console.error('[ExpensesAPI] Error fetching user expenses:', error)
if (this.toastService) {
this.toastService.error('Failed to load expenses', {
description: error instanceof Error ? error.message : 'Unknown error'
})
}
throw error
}
}
@ -227,11 +189,11 @@ export class ExpensesAPI extends BaseService {
/**
* Get user's balance with castle
*/
async getUserBalance(): Promise<{ balance: number; currency: string }> {
async getUserBalance(walletKey: string): Promise<{ balance: number; currency: string }> {
try {
const response = await fetch(`${this.baseUrl}/api/v1/balance`, {
const response = await fetch(`${this.baseUrl}/castle/api/v1/balance`, {
method: 'GET',
headers: this.getHeaders(),
headers: this.getHeaders(walletKey),
signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
})