diff --git a/src/plugins/splitwise/ZenmoneyManifest.xml b/src/plugins/splitwise/ZenmoneyManifest.xml
new file mode 100644
index 00000000..2699c79b
--- /dev/null
+++ b/src/plugins/splitwise/ZenmoneyManifest.xml
@@ -0,0 +1,50 @@
+
+
+ splitwise
+ 15881
+
+ Synchronization plugin for Splitwise (https://splitwise.com).
+ Input your API token from https://secure.splitwise.com/apps.
+
+ This plugin synchronizes your Splitwise expenses with ZenMoney.
+ After first sync update Splitwise Account balances manually.
+
+ ## How it works
+
+ 1. The plugin creates separate accounts for each currency used in your Splitwise expenses
+ 2. All expenses where you owe money will be imported as transactions with negative amounts
+ 3. All expenses where others owe you money will be reflected in the account balance
+
+ ## Transaction handling
+
+ When you pay for a shared expense:
+ 1. The original payment will appear in ZenMoney from your payment method (card/cash)
+ 2. After syncing with Splitwise, your share of the expense will appear as a transaction
+ 3. You should convert the original payment to a transfer:
+ - Source: Your payment account (card/cash)
+ - Destination: The corresponding Splitwise currency account
+
+ This way, your Splitwise balance will accurately reflect what you owe and what others owe you.
+
+ ## Example
+
+ You paid $100 for dinner with a friend (split 50/50):
+ 1. Original $100 payment appears from your card
+ 2. Splitwise sync adds a -$50 expense (your share)
+ 3. Convert the $100 card payment to:
+ - Transfer from: Your card
+ - Transfer to: Splitwise USD account
+
+ Final result:
+ - Your card: -$100
+ - Splitwise USD: +$50 (friend owes you)
+
+ 1.0.0
+ 1
+ true
+
+ index.js
+ preferences.xml
+
+ false
+
\ No newline at end of file
diff --git a/src/plugins/splitwise/__tests__/converters.test.ts b/src/plugins/splitwise/__tests__/converters.test.ts
new file mode 100644
index 00000000..c3f33232
--- /dev/null
+++ b/src/plugins/splitwise/__tests__/converters.test.ts
@@ -0,0 +1,146 @@
+import { convertAccounts, convertTransaction } from '../converters'
+import { SplitwiseExpense } from '../models'
+import { AccountType } from '../../../types/zenmoney'
+
+describe('convertAccounts', () => {
+ it('should create accounts for all currencies with owed money', () => {
+ const expenses: SplitwiseExpense[] = [
+ {
+ id: 1,
+ description: 'Lunch',
+ details: '',
+ payment: false,
+ cost: '100.00',
+ date: '2024-01-08',
+ created_at: '2024-01-08T15:08:03Z',
+ updated_at: '2024-01-08T15:08:03Z',
+ currency_code: 'USD',
+ users: [
+ { user_id: 1, paid_share: '0', owed_share: '50.00' },
+ { user_id: 2, paid_share: '100.00', owed_share: '50.00' }
+ ]
+ },
+ {
+ id: 2,
+ description: 'Coffee',
+ details: '',
+ payment: false,
+ cost: '10.00',
+ date: '2024-01-08',
+ created_at: '2024-01-08T15:08:03Z',
+ updated_at: '2024-01-08T15:08:03Z',
+ currency_code: 'EUR',
+ users: [
+ { user_id: 1, paid_share: '10.00', owed_share: '0' },
+ { user_id: 2, paid_share: '0', owed_share: '10.00' }
+ ]
+ }
+ ]
+
+ const accounts = convertAccounts(expenses)
+ expect(accounts).toHaveLength(2)
+ expect(accounts).toEqual([
+ {
+ id: 'USD',
+ type: AccountType.checking,
+ title: 'Splitwise USD',
+ instrument: 'USD',
+ balance: -100,
+ syncIds: ['SWUSD0']
+ },
+ {
+ id: 'EUR',
+ type: AccountType.checking,
+ title: 'Splitwise EUR',
+ instrument: 'EUR',
+ balance: -10,
+ syncIds: ['SWEUR0']
+ }
+ ])
+ })
+
+ it('should skip deleted expenses when creating accounts', () => {
+ const expenses: SplitwiseExpense[] = [
+ {
+ id: 1,
+ description: 'Lunch',
+ details: '',
+ payment: false,
+ cost: '100.00',
+ date: '2024-01-08',
+ created_at: '2024-01-08T15:08:03Z',
+ updated_at: '2024-01-08T15:08:03Z',
+ deleted_at: '2024-01-09T10:00:00Z',
+ currency_code: 'USD',
+ users: [
+ { user_id: 1, paid_share: '0', owed_share: '50.00' },
+ { user_id: 2, paid_share: '100.00', owed_share: '50.00' }
+ ]
+ }
+ ]
+
+ const accounts = convertAccounts(expenses.filter(e => e.deleted_at == null))
+ expect(accounts).toHaveLength(0)
+ })
+})
+
+describe('convertTransaction', () => {
+ it('should convert expense to transaction when user owes money', () => {
+ const expense: SplitwiseExpense = {
+ id: 1,
+ description: 'Lunch',
+ details: '',
+ payment: false,
+ cost: '100.00',
+ date: '2024-01-08',
+ created_at: '2024-01-08T15:08:03Z',
+ updated_at: '2024-01-08T15:08:03Z',
+ currency_code: 'USD',
+ users: [
+ { user_id: 1, paid_share: '0', owed_share: '50.00' },
+ { user_id: 2, paid_share: '100.00', owed_share: '50.00' }
+ ]
+ }
+
+ const transaction = convertTransaction(expense, 1)
+ expect(transaction).toEqual({
+ hold: false,
+ date: new Date('2024-01-08'),
+ movements: [{
+ id: '1',
+ account: { id: 'USD' },
+ sum: -50,
+ fee: 0,
+ invoice: null
+ }],
+ merchant: {
+ fullTitle: 'Lunch',
+ mcc: null,
+ location: null
+ },
+ comment: 'Lunch'
+ })
+ })
+
+ it('should return null for deleted expenses', () => {
+ const expense: SplitwiseExpense = {
+ id: 1,
+ description: 'Lunch',
+ details: '',
+ payment: false,
+ cost: '100.00',
+ date: '2024-01-08',
+ created_at: '2024-01-08T15:08:03Z',
+ updated_at: '2024-01-08T15:08:03Z',
+ deleted_at: '2024-01-09T10:00:00Z',
+ currency_code: 'USD',
+ users: [
+ { user_id: 1, paid_share: '0', owed_share: '50.00' },
+ { user_id: 2, paid_share: '100.00', owed_share: '50.00' }
+ ]
+ }
+
+ const transaction = convertTransaction(expense, 1)
+ expect(transaction).toBeNull()
+ })
+})
diff --git a/src/plugins/splitwise/__tests__/index.test.ts b/src/plugins/splitwise/__tests__/index.test.ts
new file mode 100644
index 00000000..dbcb1df4
--- /dev/null
+++ b/src/plugins/splitwise/__tests__/index.test.ts
@@ -0,0 +1,67 @@
+import { scrape } from '../index'
+import { fetchCurrentUser, fetchExpenses, fetchBalances } from '../fetchApi'
+import { SplitwiseExpense, SplitwiseUser } from '../models'
+
+jest.mock('../fetchApi')
+
+describe('scrape', () => {
+ beforeEach(() => {
+ jest.resetAllMocks()
+ })
+
+ it('should fetch and convert expenses', async () => {
+ const mockUser: SplitwiseUser = {
+ id: 1,
+ first_name: 'Test',
+ last_name: 'User',
+ email: 'test@example.com',
+ registration_status: 'confirmed',
+ picture: {
+ small: '',
+ medium: '',
+ large: ''
+ },
+ default_currency: 'USD',
+ locale: 'en'
+ }
+
+ const mockExpenses: SplitwiseExpense[] = [{
+ id: 1,
+ description: 'Lunch',
+ details: '',
+ payment: false,
+ cost: '100.00',
+ date: '2024-01-08',
+ created_at: '2024-01-08T15:08:03Z',
+ updated_at: '2024-01-08T15:08:03Z',
+ currency_code: 'USD',
+ users: [
+ { user_id: 1, paid_share: '0', owed_share: '50.00' },
+ { user_id: 2, paid_share: '100.00', owed_share: '50.00' }
+ ]
+ }]
+
+ const mockBalances = {
+ USD: -50
+ }
+
+ ;(fetchCurrentUser as jest.Mock).mockResolvedValue(mockUser)
+ ;(fetchExpenses as jest.Mock).mockResolvedValue(mockExpenses)
+ ;(fetchBalances as jest.Mock).mockResolvedValue(mockBalances)
+
+ const result = await scrape({
+ preferences: {
+ token: 'test-token',
+ startDate: '2024-01-01'
+ },
+ fromDate: new Date('2024-01-01'),
+ toDate: new Date('2024-01-31'),
+ isFirstRun: true,
+ isInBackground: false
+ })
+
+ expect(result.accounts).toHaveLength(1)
+ expect(result.transactions).toHaveLength(1)
+ expect(fetchExpenses).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/src/plugins/splitwise/converters.ts b/src/plugins/splitwise/converters.ts
new file mode 100644
index 00000000..b537360c
--- /dev/null
+++ b/src/plugins/splitwise/converters.ts
@@ -0,0 +1,65 @@
+import { AccountType, Account, Transaction } from '../../types/zenmoney'
+import { SplitwiseExpense } from './models'
+
+export function convertAccounts (expenses: SplitwiseExpense[]): Account[] {
+ // Get currencies only from expenses where user was involved
+ const currencies = new Set(expenses
+ .filter((expense) => expense.users.some((user) => parseFloat(user.owed_share) > 0))
+ .map((expense) => expense.currency_code))
+
+ // Calculate balances from expenses
+ const balances: Record = {}
+ for (const expense of expenses) {
+ for (const user of expense.users) {
+ const owedShare = parseFloat(user.owed_share)
+ if (owedShare > 0) {
+ balances[expense.currency_code] = (balances[expense.currency_code] ?? 0) - owedShare
+ }
+ }
+ }
+
+ return Array.from(currencies).map((currency) => ({
+ id: currency,
+ type: AccountType.checking,
+ title: `Splitwise ${currency}`,
+ instrument: currency,
+ balance: balances[currency] ?? 0,
+ syncIds: [`SW${currency.padEnd(4, '0')}`]
+ }))
+}
+
+export function convertTransaction (expense: SplitwiseExpense, currentUserId: number): Transaction | null {
+ // Skip deleted expenses - ZenMoney will handle them automatically
+ if (expense.deleted_at != null) {
+ return null
+ }
+
+ const currentUser = expense.users.find((user) => user.user_id === currentUserId)
+ if (!currentUser) {
+ return null
+ }
+
+ const owedShare = parseFloat(currentUser.owed_share)
+ // Skip if user doesn't owe money
+ if (owedShare <= 0) {
+ return null
+ }
+
+ return {
+ hold: false,
+ date: new Date(expense.date),
+ movements: [{
+ id: expense.id.toString(),
+ account: { id: expense.currency_code },
+ sum: -owedShare, // Negative because it's an expense
+ fee: 0,
+ invoice: null
+ }],
+ merchant: {
+ fullTitle: expense.description,
+ mcc: null,
+ location: null
+ },
+ comment: expense.description
+ }
+}
diff --git a/src/plugins/splitwise/fetchApi.ts b/src/plugins/splitwise/fetchApi.ts
new file mode 100644
index 00000000..9d134474
--- /dev/null
+++ b/src/plugins/splitwise/fetchApi.ts
@@ -0,0 +1,68 @@
+import { Auth, SplitwiseExpense, SplitwiseUser } from './models'
+
+async function fetchApi (path: string, auth: Auth, params: Record = {}): Promise {
+ const queryString = new URLSearchParams(params as Record).toString()
+ const url = `https://secure.splitwise.com/api/v3.0${path}${(queryString !== '') ? '?' + queryString : ''}`
+
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${auth.token}`
+ }
+ })
+
+ if (response.status === 401) {
+ throw new Error('Unauthorized: Invalid token')
+ }
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`)
+ }
+
+ return await response.json()
+}
+
+function formatDate (date: Date): string {
+ // Ensure we have a valid date
+ if (!(date instanceof Date) || isNaN(date.getTime())) {
+ date = new Date()
+ }
+ const year = date.getFullYear()
+ const month = String(date.getMonth() + 1).padStart(2, '0')
+ const day = String(date.getDate()).padStart(2, '0')
+ return `${year}-${month}-${day}`
+}
+
+export async function fetchCurrentUser (auth: Auth): Promise {
+ const response = await fetchApi('/get_current_user', auth)
+ return (response as { user: SplitwiseUser }).user
+}
+
+export async function fetchExpenses (auth: Auth, fromDate: Date): Promise {
+ const params: Record = {
+ dated_after: formatDate(fromDate),
+ limit: 0
+ }
+
+ const response = await fetchApi('/get_expenses', auth, params)
+ const expenses = (response as { expenses: SplitwiseExpense[] }).expenses
+ return expenses.length > 0 ? expenses : []
+}
+
+export async function fetchBalances (auth: Auth): Promise> {
+ const response = await fetchApi('/get_groups', auth)
+ const groups = (response as { groups: Array<{ group_id: number, members: Array<{ balance: Array<{ currency_code: string, amount: string }> }> }> }).groups
+ const balances: Record = {}
+ for (const group of groups) {
+ for (const member of group.members) {
+ for (const balance of member.balance) {
+ if (balance.currency_code !== '') {
+ const amount = parseFloat(balance.amount)
+ balances[balance.currency_code] = (balances[balance.currency_code] ?? 0) + amount
+ }
+ }
+ }
+ }
+
+ return balances
+}
diff --git a/src/plugins/splitwise/index.ts b/src/plugins/splitwise/index.ts
new file mode 100644
index 00000000..b2e73958
--- /dev/null
+++ b/src/plugins/splitwise/index.ts
@@ -0,0 +1,48 @@
+import { ScrapeFunc, Transaction } from '../../types/zenmoney'
+import { fetchExpenses, fetchCurrentUser } from './fetchApi'
+import { convertAccounts, convertTransaction } from './converters'
+import { Auth, Preferences } from './models'
+import { InvalidPreferencesError } from '../../errors'
+
+export const scrape: ScrapeFunc = async ({ preferences, fromDate, toDate }) => {
+ if (preferences.token === '') {
+ throw new InvalidPreferencesError('Token is required')
+ }
+
+ const auth: Auth = {
+ token: preferences.token,
+ startDate: preferences.startDate ?? '2000-01-01'
+ }
+
+ // Ensure valid dates
+ toDate = toDate ?? new Date()
+ fromDate = fromDate ?? new Date(auth.startDate)
+
+ // Validate dates
+ if (isNaN(fromDate.getTime())) {
+ fromDate = new Date('2000-01-01')
+ }
+ if (isNaN(toDate.getTime())) {
+ toDate = new Date()
+ }
+
+ const currentUser = await fetchCurrentUser(auth)
+
+ // Get all expenses including deleted ones
+ const allExpenses = await fetchExpenses(auth, fromDate)
+ const accounts = convertAccounts(allExpenses.filter(e => e.deleted_at == null))
+
+ // Get expenses for the sync period
+ const apiExpenses = fromDate.getTime() === new Date(auth.startDate).getTime()
+ ? allExpenses
+ : await fetchExpenses(auth, fromDate)
+
+ const transactions = apiExpenses
+ .map((expense) => convertTransaction(expense, currentUser.id))
+ .filter((transaction): transaction is Transaction => transaction !== null)
+
+ return {
+ accounts,
+ transactions
+ }
+}
diff --git a/src/plugins/splitwise/models.ts b/src/plugins/splitwise/models.ts
new file mode 100644
index 00000000..f5b99566
--- /dev/null
+++ b/src/plugins/splitwise/models.ts
@@ -0,0 +1,51 @@
+export interface Preferences {
+ token: string
+ startDate: string
+}
+
+export interface Auth {
+ token: string
+ startDate: string
+}
+
+export interface SplitwiseExpense {
+ id: number
+ description: string
+ details: string
+ payment: boolean
+ cost: string
+ date: string
+ created_at: string
+ updated_at: string
+ deleted_at?: string
+ currency_code: string
+ users: Array<{
+ user_id: number
+ paid_share: string
+ owed_share: string
+ }>
+}
+
+export interface SplitwiseUser {
+ id: number
+ first_name: string
+ last_name: string
+ email: string
+ registration_status: string
+ picture: {
+ small: string
+ medium: string
+ large: string
+ }
+ default_currency: string
+ locale: string
+ balance?: Record
+}
+
+export interface SplitwiseGroup {
+ id: number
+ name: string
+ group_type: string
+ members: SplitwiseUser[]
+ currency_code: string
+}
diff --git a/src/plugins/splitwise/preferences.xml b/src/plugins/splitwise/preferences.xml
new file mode 100644
index 00000000..cc68345b
--- /dev/null
+++ b/src/plugins/splitwise/preferences.xml
@@ -0,0 +1,23 @@
+
+
+
+
+