f6ee0c2a82
* fix(supplier-invoices): self-assess reverse-charge VAT + link payments to vouchers Reverse-charge supplier invoices now carry a per-item reverse_charge_rate (0.06/0.12/0.25). Under omvänd skattskyldighet the supplier charges 0% VAT, so the line vat_rate stays 0 and the buyer self-assesses fiktiv moms at the statutory rate. Centralizes rate resolution (resolveReverseChargeRate) and the ruta 20-24 basis-account guard (isReverseChargeBasisAccount) in vat-entries so the booking engine and review-dialog preview can no longer drift. Adds the link_supplier_invoice_voucher pending operation: mark a leverantorsfaktura paid by linking an existing posted verifikat that debits 2440, with no new journal entry. Exposes find-candidates/link MCP tools and the bulk-reconcile helper, scoped under suppliers:read/write. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(vat): report yearly VAT over the rakenskapsar, not the calendar year Annual VAT (helarsmoms) is filed per beskattningsar/rakenskapsar (SFL 26 kap), which can be extended or shortened up to 18 months. The previous Jan-Dec calendar span silently dropped part of an extended first year. calculateVatDeclaration now accepts a fiscalPeriodId and resolves the period's actual bounds for yearly; monthly/quarterly stay calendar. The reports UI passes the selected fiscal period, defaults the periodicity from the company's moms_period setting, and carries the period into the ruta drill-down. full-archive export threads the period id through too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(migration): resolve supplier invoice status from payment amounts The provider's lifecycle status and its payment status are computed independently upstream and can contradict each other (e.g. a Fortnox invoice marked booked but fully paid). Both the arcim entity-mapper and the Fortnox mapper now let payment state win: fully paid -> paid, partial -> partially_paid, otherwise the mapped lifecycle status, with credit notes forced terminal. Balance is compared numerically (never strict === 0) so float drift or a residual ore resolves cleanly, and an absent Balance is treated as unpaid. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(enable-banking): only ingest booked transactions to stop re-import drift Pending entries are skipped during sync: a pending row is unstable across syncs (a later 'synka nu' returns it still pending or finally booked, often with a different effective date). Because both the dedup external_id and the content-dedup key are date-derived, that drift minted a new id and re-imported a transaction that already existed - observed in production as the same amount+description landing twice with different dates. Gating the import set on a stable booking_date removes the drift at the source and leaves booked rows' ids byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(gitignore): ignore local SIE test fixtures tests/fixtures/sie/ may contain real or scrubbed company data and must never be committed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoice): handle errors during registration journal entry creation and ensure invoice rollback feat(tests): add test for reverse charge rate handling on supplier invoice line items feat(fortnox): ensure paid status reflects zero balance for fully paid invoices chore(migrations): add reverse_charge_rate to supplier_invoice_items and backfill link_supplier_invoice_voucher --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
540 lines
19 KiB
TypeScript
540 lines
19 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
// Mock api-client
|
|
const mockGetAllTransactionsWithRaw = vi.fn()
|
|
const mockConvertTransaction = vi.fn()
|
|
const mockGetAccountBalance = vi.fn()
|
|
vi.mock('../api-client', () => ({
|
|
getAllTransactionsWithRaw: (...args: unknown[]) => mockGetAllTransactionsWithRaw(...args),
|
|
convertTransaction: (...args: unknown[]) => mockConvertTransaction(...args),
|
|
getAccountBalance: (...args: unknown[]) => mockGetAccountBalance(...args),
|
|
}))
|
|
|
|
// Mock document service
|
|
const mockUploadDocument = vi.fn()
|
|
vi.mock('@/lib/core/documents/document-service', () => ({
|
|
uploadDocument: (...args: unknown[]) => mockUploadDocument(...args),
|
|
}))
|
|
|
|
// Mock ingest
|
|
const mockIngest = vi.fn()
|
|
|
|
import { syncAccountTransactions } from '../sync'
|
|
import type { StoredAccount } from '../../types'
|
|
|
|
const USER_ID = 'user-1'
|
|
const COMPANY_ID = 'company-1'
|
|
const CONNECTION_ID = 'conn-1'
|
|
|
|
function makeAccount(overrides: Partial<StoredAccount> = {}): StoredAccount {
|
|
return {
|
|
uid: 'acc-uid-1',
|
|
currency: 'SEK',
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
describe('syncAccountTransactions', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockGetAccountBalance.mockRejectedValue(new Error('skip'))
|
|
mockIngest.mockResolvedValue({ imported: 1, duplicates: 0, errors: 0, reconciled: 0, auto_categorized: 0, auto_matched_invoices: 0, transaction_ids: ['tx-1'] })
|
|
})
|
|
|
|
it('calls uploadDocument for each raw page with correct filename pattern', async () => {
|
|
const rawPage1 = JSON.stringify({ transactions: [{ transaction_amount: { amount: '100', currency: 'SEK' } }] })
|
|
const rawPage2 = JSON.stringify({ transactions: [{ transaction_amount: { amount: '200', currency: 'SEK' } }] })
|
|
|
|
mockGetAllTransactionsWithRaw.mockResolvedValue({
|
|
transactions: [
|
|
{ transaction_amount: { amount: '100', currency: 'SEK' } },
|
|
{ transaction_amount: { amount: '200', currency: 'SEK' } },
|
|
],
|
|
rawPages: [rawPage1, rawPage2],
|
|
})
|
|
|
|
mockConvertTransaction.mockImplementation((tx: { transaction_amount: { amount: string } }) => ({
|
|
id: `tx-${tx.transaction_amount.amount}`,
|
|
date: '2024-06-15',
|
|
booking_date: '2024-06-15',
|
|
amount: parseFloat(tx.transaction_amount.amount),
|
|
currency: 'SEK',
|
|
description: 'Test',
|
|
}))
|
|
|
|
mockUploadDocument.mockResolvedValue({ id: 'doc-1' })
|
|
|
|
const account = makeAccount()
|
|
await syncAccountTransactions(
|
|
{} as never,
|
|
COMPANY_ID,
|
|
USER_ID,
|
|
CONNECTION_ID,
|
|
account,
|
|
'2024-01-01',
|
|
'2024-12-31',
|
|
mockIngest
|
|
)
|
|
|
|
expect(mockUploadDocument).toHaveBeenCalledTimes(2)
|
|
|
|
// Verify filename pattern
|
|
const firstCall = mockUploadDocument.mock.calls[0]
|
|
expect(firstCall[3].name).toMatch(/^psd2-response_conn-1_acc-uid-1_.*_p1\.json$/)
|
|
expect(firstCall[3].type).toBe('application/json')
|
|
expect(firstCall[4]).toEqual({ upload_source: 'api' })
|
|
|
|
const secondCall = mockUploadDocument.mock.calls[1]
|
|
expect(secondCall[3].name).toMatch(/^psd2-response_conn-1_acc-uid-1_.*_p2\.json$/)
|
|
})
|
|
|
|
it('completes sync even if uploadDocument throws', async () => {
|
|
mockGetAllTransactionsWithRaw.mockResolvedValue({
|
|
transactions: [{ transaction_amount: { amount: '100', currency: 'SEK' } }],
|
|
rawPages: ['{"transactions":[]}'],
|
|
})
|
|
|
|
mockConvertTransaction.mockReturnValue({
|
|
id: 'tx-1',
|
|
date: '2024-06-15',
|
|
booking_date: '2024-06-15',
|
|
amount: 100,
|
|
currency: 'SEK',
|
|
description: 'Test',
|
|
})
|
|
|
|
mockUploadDocument.mockRejectedValue(new Error('Storage error'))
|
|
|
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
const account = makeAccount()
|
|
const result = await syncAccountTransactions(
|
|
{} as never,
|
|
COMPANY_ID,
|
|
USER_ID,
|
|
CONNECTION_ID,
|
|
account,
|
|
'2024-01-01',
|
|
'2024-12-31',
|
|
mockIngest
|
|
)
|
|
|
|
expect(result.imported).toBe(1)
|
|
expect(result.errors).toBe(0)
|
|
expect(errorSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining('Failed to archive raw response'),
|
|
expect.any(Error)
|
|
)
|
|
|
|
errorSpy.mockRestore()
|
|
})
|
|
|
|
it('forwards strategy from sync options to getAllTransactionsWithRaw', async () => {
|
|
mockGetAllTransactionsWithRaw.mockResolvedValue({
|
|
transactions: [],
|
|
rawPages: ['{}'],
|
|
})
|
|
mockUploadDocument.mockResolvedValue({ id: 'doc-1' })
|
|
|
|
await syncAccountTransactions(
|
|
{} as never,
|
|
COMPANY_ID,
|
|
USER_ID,
|
|
CONNECTION_ID,
|
|
makeAccount(),
|
|
'2024-01-01',
|
|
'2024-12-31',
|
|
mockIngest,
|
|
{ strategy: 'longest' }
|
|
)
|
|
|
|
expect(mockGetAllTransactionsWithRaw).toHaveBeenCalledWith(
|
|
'acc-uid-1',
|
|
'2024-01-01',
|
|
'2024-12-31',
|
|
'longest'
|
|
)
|
|
})
|
|
|
|
it('omits strategy when sync options do not include it', async () => {
|
|
mockGetAllTransactionsWithRaw.mockResolvedValue({
|
|
transactions: [],
|
|
rawPages: ['{}'],
|
|
})
|
|
mockUploadDocument.mockResolvedValue({ id: 'doc-1' })
|
|
|
|
await syncAccountTransactions(
|
|
{} as never,
|
|
COMPANY_ID,
|
|
USER_ID,
|
|
CONNECTION_ID,
|
|
makeAccount(),
|
|
'2024-01-01',
|
|
'2024-12-31',
|
|
mockIngest
|
|
)
|
|
|
|
expect(mockGetAllTransactionsWithRaw).toHaveBeenCalledWith(
|
|
'acc-uid-1',
|
|
'2024-01-01',
|
|
'2024-12-31',
|
|
undefined
|
|
)
|
|
})
|
|
|
|
it('passes raw transactions to ingest function', async () => {
|
|
mockGetAllTransactionsWithRaw.mockResolvedValue({
|
|
transactions: [{ transaction_amount: { amount: '500', currency: 'SEK' }, booking_date: '2024-06-15' }],
|
|
rawPages: ['{}'],
|
|
})
|
|
|
|
mockConvertTransaction.mockReturnValue({
|
|
id: 'tx-500',
|
|
date: '2024-06-15',
|
|
booking_date: '2024-06-15',
|
|
amount: -500,
|
|
currency: 'SEK',
|
|
description: 'Purchase',
|
|
counterparty_name: 'Store',
|
|
merchant_category_code: '5411',
|
|
})
|
|
|
|
mockUploadDocument.mockResolvedValue({ id: 'doc-1' })
|
|
|
|
const account = makeAccount()
|
|
await syncAccountTransactions(
|
|
{} as never,
|
|
COMPANY_ID,
|
|
USER_ID,
|
|
CONNECTION_ID,
|
|
account,
|
|
'2024-01-01',
|
|
'2024-12-31',
|
|
mockIngest
|
|
)
|
|
|
|
expect(mockIngest).toHaveBeenCalledTimes(1)
|
|
const rawTxns = mockIngest.mock.calls[0][3]
|
|
expect(rawTxns).toHaveLength(1)
|
|
// Content-derived external_id: eb_{accountScope}_{date}_{öre}_{occurrence}.
|
|
// Deliberately NOT keyed off the bank's (unstable) tx id.
|
|
expect(rawTxns[0].external_id).toBe('eb_acc-uid-1_2024-06-15_-50000_0')
|
|
expect(rawTxns[0].import_source).toBe('enable_banking')
|
|
})
|
|
|
|
it('skips pending entries (no booking_date) and ingests only booked ones', async () => {
|
|
// A pending PSD2 entry has no booking_date — only a value_date. Importing it
|
|
// is what produced the production duplicates: its date (hence its dedup id)
|
|
// differs from the same transaction's booked representation. Pending entries
|
|
// must be skipped; booked ones import as usual, keyed on booking_date.
|
|
mockConvertTransaction.mockImplementation((tx: { transaction_amount: { amount: string }, booking_date?: string, value_date?: string }) => ({
|
|
id: 'x',
|
|
date: tx.booking_date || tx.value_date,
|
|
booking_date: tx.booking_date || tx.value_date,
|
|
amount: -parseFloat(tx.transaction_amount.amount),
|
|
currency: 'SEK',
|
|
description: 'T',
|
|
}))
|
|
mockUploadDocument.mockResolvedValue({ id: 'doc-1' })
|
|
|
|
mockGetAllTransactionsWithRaw.mockResolvedValue({
|
|
transactions: [
|
|
{ transaction_amount: { amount: '50', currency: 'SEK' }, value_date: '2026-02-15' }, // pending → skipped
|
|
{ transaction_amount: { amount: '75', currency: 'SEK' }, booking_date: '2026-03-01' }, // booked → kept
|
|
],
|
|
rawPages: ['{}'],
|
|
})
|
|
|
|
await syncAccountTransactions(
|
|
{} as never, COMPANY_ID, USER_ID, CONNECTION_ID, makeAccount(),
|
|
'2026-01-01', '2026-06-01', mockIngest
|
|
)
|
|
|
|
const batch = mockIngest.mock.calls[0][3]
|
|
expect(batch).toHaveLength(1)
|
|
expect(batch[0].date).toBe('2026-03-01')
|
|
expect(batch[0].external_id).toBe('eb_acc-uid-1_2026-03-01_-7500_0')
|
|
})
|
|
|
|
it('does not re-import a booked transaction when a later sync returns it pending (no booking_date)', async () => {
|
|
// Regression for the 45→90 duplication. The same transaction is returned
|
|
// booked in sync 1 (booking_date 2026-04-21) and pending in sync 2 (only
|
|
// value_date 2026-02-15). Previously sync 2 ingested the pending copy with a
|
|
// value_date-derived id (different date → new id → duplicate). It must now
|
|
// be skipped, so sync 2 ingests nothing for it.
|
|
mockConvertTransaction.mockImplementation((tx: { transaction_amount: { amount: string }, booking_date?: string, value_date?: string }) => ({
|
|
id: 'x',
|
|
date: tx.booking_date || tx.value_date,
|
|
booking_date: tx.booking_date || tx.value_date,
|
|
amount: -parseFloat(tx.transaction_amount.amount),
|
|
currency: 'SEK',
|
|
description: 'AVI ÖVERDRAG',
|
|
}))
|
|
mockUploadDocument.mockResolvedValue({ id: 'doc-1' })
|
|
|
|
// Sync 1 — booked
|
|
mockGetAllTransactionsWithRaw.mockResolvedValueOnce({
|
|
transactions: [{ transaction_amount: { amount: '100', currency: 'SEK' }, booking_date: '2026-04-21', value_date: '2026-02-15' }],
|
|
rawPages: ['{}'],
|
|
})
|
|
await syncAccountTransactions(
|
|
{} as never, COMPANY_ID, USER_ID, CONNECTION_ID, makeAccount(),
|
|
'2026-01-01', '2026-06-01', mockIngest
|
|
)
|
|
const firstBatch = mockIngest.mock.calls[0][3]
|
|
expect(firstBatch).toHaveLength(1)
|
|
expect(firstBatch[0].date).toBe('2026-04-21')
|
|
expect(firstBatch[0].external_id).toBe('eb_acc-uid-1_2026-04-21_-10000_0')
|
|
|
|
// Sync 2 — same transaction now returned pending (booking_date dropped)
|
|
mockGetAllTransactionsWithRaw.mockResolvedValueOnce({
|
|
transactions: [{ transaction_amount: { amount: '100', currency: 'SEK' }, value_date: '2026-02-15' }],
|
|
rawPages: ['{}'],
|
|
})
|
|
await syncAccountTransactions(
|
|
{} as never, COMPANY_ID, USER_ID, CONNECTION_ID, makeAccount(),
|
|
'2026-01-01', '2026-06-01', mockIngest
|
|
)
|
|
const secondBatch = mockIngest.mock.calls[1][3]
|
|
expect(secondBatch).toHaveLength(0)
|
|
})
|
|
|
|
it('gives identical same-day same-amount transactions distinct, stable external_ids', async () => {
|
|
// Two genuinely distinct transactions that share date + amount must both be
|
|
// kept (distinct ids), and re-running the sync must reproduce the SAME set
|
|
// of ids so the second sync dedupes instead of duplicating.
|
|
const apiTxns = [
|
|
{ transaction_amount: { amount: '250', currency: 'SEK' }, booking_date: '2024-06-15' },
|
|
{ transaction_amount: { amount: '250', currency: 'SEK' }, booking_date: '2024-06-15' },
|
|
]
|
|
mockGetAllTransactionsWithRaw.mockResolvedValue({ transactions: apiTxns, rawPages: ['{}'] })
|
|
mockConvertTransaction.mockImplementation((tx: { transaction_amount: { amount: string }, booking_date: string }) => ({
|
|
id: `bank-id-${Math.random()}`, // unstable bank id — must NOT influence external_id
|
|
date: tx.booking_date,
|
|
booking_date: tx.booking_date,
|
|
amount: -parseFloat(tx.transaction_amount.amount),
|
|
currency: 'SEK',
|
|
description: 'Kaffe',
|
|
}))
|
|
mockUploadDocument.mockResolvedValue({ id: 'doc-1' })
|
|
|
|
await syncAccountTransactions(
|
|
{} as never, COMPANY_ID, USER_ID, CONNECTION_ID, makeAccount(),
|
|
'2024-06-01', '2024-06-30', mockIngest
|
|
)
|
|
|
|
const ids = mockIngest.mock.calls[0][3].map((t: { external_id: string }) => t.external_id)
|
|
expect(ids).toEqual([
|
|
'eb_acc-uid-1_2024-06-15_-25000_0',
|
|
'eb_acc-uid-1_2024-06-15_-25000_1',
|
|
])
|
|
})
|
|
|
|
it('prefers IBAN over uid for the external_id account scope', async () => {
|
|
mockGetAllTransactionsWithRaw.mockResolvedValue({
|
|
transactions: [{ transaction_amount: { amount: '100', currency: 'SEK' }, booking_date: '2024-06-15' }],
|
|
rawPages: ['{}'],
|
|
})
|
|
mockConvertTransaction.mockReturnValue({
|
|
id: 'tx-1', date: '2024-06-15', booking_date: '2024-06-15', amount: 100, currency: 'SEK', description: 'Test',
|
|
})
|
|
mockUploadDocument.mockResolvedValue({ id: 'doc-1' })
|
|
|
|
await syncAccountTransactions(
|
|
{} as never, COMPANY_ID, USER_ID, CONNECTION_ID,
|
|
makeAccount({ iban: 'SE4550000000058398257466' }),
|
|
'2024-06-01', '2024-06-30', mockIngest
|
|
)
|
|
|
|
const ids = mockIngest.mock.calls[0][3].map((t: { external_id: string }) => t.external_id)
|
|
expect(ids).toEqual(['eb_SE4550000000058398257466_2024-06-15_10000_0'])
|
|
})
|
|
|
|
it('normalizes IBAN whitespace/case so the account scope is stable across syncs', async () => {
|
|
mockGetAllTransactionsWithRaw.mockResolvedValue({
|
|
transactions: [{ transaction_amount: { amount: '100', currency: 'SEK' }, booking_date: '2024-06-15' }],
|
|
rawPages: ['{}'],
|
|
})
|
|
mockConvertTransaction.mockReturnValue({
|
|
id: 'tx-1', date: '2024-06-15', booking_date: '2024-06-15', amount: 100, currency: 'SEK', description: 'Test',
|
|
})
|
|
mockUploadDocument.mockResolvedValue({ id: 'doc-1' })
|
|
|
|
// ASPSP returns the IBAN in grouped, lowercased display form.
|
|
await syncAccountTransactions(
|
|
{} as never, COMPANY_ID, USER_ID, CONNECTION_ID,
|
|
makeAccount({ iban: 'se45 5000 0000 0583 9825 7466' }),
|
|
'2024-06-01', '2024-06-30', mockIngest
|
|
)
|
|
|
|
const ids = mockIngest.mock.calls[0][3].map((t: { external_id: string }) => t.external_id)
|
|
// Same scope as the spaced/cased variant above.
|
|
expect(ids).toEqual(['eb_SE4550000000058398257466_2024-06-15_10000_0'])
|
|
})
|
|
|
|
it('reproduces the same SET of external_ids when a re-sync returns transactions in a different order', async () => {
|
|
// Two genuinely distinct same-day/same-amount transactions. A later sync may
|
|
// return them in any order; the dedupe guarantee is that the id SET is
|
|
// identical, so the re-sync collides on (company_id, external_id).
|
|
const mk = (booking_date: string, amount: string) => ({ transaction_amount: { amount, currency: 'SEK' }, booking_date })
|
|
const convert = (tx: { transaction_amount: { amount: string }, booking_date: string }) => ({
|
|
id: `bank-${Math.random()}`, // unstable bank id — irrelevant to external_id
|
|
date: tx.booking_date,
|
|
booking_date: tx.booking_date,
|
|
amount: -parseFloat(tx.transaction_amount.amount),
|
|
currency: 'SEK',
|
|
description: 'Lunch',
|
|
})
|
|
mockConvertTransaction.mockImplementation(convert)
|
|
mockUploadDocument.mockResolvedValue({ id: 'doc-1' })
|
|
|
|
// First sync order.
|
|
mockGetAllTransactionsWithRaw.mockResolvedValueOnce({
|
|
transactions: [mk('2024-06-15', '250'), mk('2024-06-15', '250')],
|
|
rawPages: ['{}'],
|
|
})
|
|
await syncAccountTransactions(
|
|
{} as never, COMPANY_ID, USER_ID, CONNECTION_ID, makeAccount(),
|
|
'2024-06-01', '2024-06-30', mockIngest
|
|
)
|
|
const firstIds = mockIngest.mock.calls[0][3].map((t: { external_id: string }) => t.external_id)
|
|
|
|
// Re-sync, reversed order (and different lookback window does not matter).
|
|
mockGetAllTransactionsWithRaw.mockResolvedValueOnce({
|
|
transactions: [mk('2024-06-15', '250'), mk('2024-06-15', '250')],
|
|
rawPages: ['{}'],
|
|
})
|
|
await syncAccountTransactions(
|
|
{} as never, COMPANY_ID, USER_ID, CONNECTION_ID, makeAccount(),
|
|
'2024-03-01', '2024-06-30', mockIngest
|
|
)
|
|
const secondIds = mockIngest.mock.calls[1][3].map((t: { external_id: string }) => t.external_id)
|
|
|
|
expect(new Set(firstIds)).toEqual(new Set(secondIds))
|
|
expect(firstIds).toHaveLength(2)
|
|
})
|
|
|
|
it('returns the min/max booking date the ASPSP returned for the activation UI', async () => {
|
|
// The min/max loop reads booking_date from the *raw* transactions (sync.ts:75-82),
|
|
// before convertTransaction runs — so the dates need to be set here.
|
|
mockGetAllTransactionsWithRaw.mockResolvedValue({
|
|
transactions: [
|
|
{ transaction_amount: { amount: '100', currency: 'SEK' }, booking_date: '2026-04-15' },
|
|
{ transaction_amount: { amount: '200', currency: 'SEK' }, booking_date: '2026-02-20' },
|
|
{ transaction_amount: { amount: '300', currency: 'SEK' }, booking_date: '2026-05-10' },
|
|
],
|
|
rawPages: ['{}'],
|
|
})
|
|
|
|
mockConvertTransaction.mockImplementation((tx: { transaction_amount: { amount: string }, booking_date: string }) => ({
|
|
id: `tx-${tx.transaction_amount.amount}`,
|
|
date: tx.booking_date,
|
|
booking_date: tx.booking_date,
|
|
amount: parseFloat(tx.transaction_amount.amount),
|
|
currency: 'SEK',
|
|
description: 'Test',
|
|
}))
|
|
|
|
mockUploadDocument.mockResolvedValue({ id: 'doc-1' })
|
|
|
|
const account = makeAccount()
|
|
const result = await syncAccountTransactions(
|
|
{} as never,
|
|
COMPANY_ID,
|
|
USER_ID,
|
|
CONNECTION_ID,
|
|
account,
|
|
'2026-02-13',
|
|
'2026-05-13',
|
|
mockIngest
|
|
)
|
|
|
|
expect(result.returnedMinBookingDate).toBe('2026-02-20')
|
|
expect(result.returnedMaxBookingDate).toBe('2026-05-10')
|
|
})
|
|
|
|
it('returns undefined min/max when no transactions came back', async () => {
|
|
mockGetAllTransactionsWithRaw.mockResolvedValue({
|
|
transactions: [],
|
|
rawPages: [],
|
|
})
|
|
|
|
const account = makeAccount()
|
|
const result = await syncAccountTransactions(
|
|
{} as never,
|
|
COMPANY_ID,
|
|
USER_ID,
|
|
CONNECTION_ID,
|
|
account,
|
|
'2026-02-13',
|
|
'2026-05-13',
|
|
mockIngest
|
|
)
|
|
|
|
expect(result.returnedMinBookingDate).toBeUndefined()
|
|
expect(result.returnedMaxBookingDate).toBeUndefined()
|
|
})
|
|
|
|
it('passes account.ledger_account as IngestOptions.settlementAccount when set', async () => {
|
|
mockGetAllTransactionsWithRaw.mockResolvedValue({
|
|
transactions: [{ transaction_amount: { amount: '100', currency: 'EUR' }, booking_date: '2026-04-01' }],
|
|
rawPages: ['{}'],
|
|
})
|
|
mockConvertTransaction.mockReturnValue({
|
|
id: 'tx-1',
|
|
date: '2026-04-01',
|
|
booking_date: '2026-04-01',
|
|
amount: 100,
|
|
currency: 'EUR',
|
|
description: 'EUR purchase',
|
|
})
|
|
mockUploadDocument.mockResolvedValue({ id: 'doc-1' })
|
|
|
|
const account = makeAccount({ uid: 'eur-acc', currency: 'EUR', ledger_account: '1932' })
|
|
await syncAccountTransactions(
|
|
{} as never,
|
|
COMPANY_ID,
|
|
USER_ID,
|
|
CONNECTION_ID,
|
|
account,
|
|
'2026-02-13',
|
|
'2026-05-13',
|
|
mockIngest
|
|
)
|
|
|
|
const ingestOptions = mockIngest.mock.calls[0][4]
|
|
expect(ingestOptions).toMatchObject({ settlementAccount: '1932' })
|
|
})
|
|
|
|
it('omits settlementAccount when account.ledger_account is unset (mapping engine defaults to 1930)', async () => {
|
|
mockGetAllTransactionsWithRaw.mockResolvedValue({
|
|
transactions: [{ transaction_amount: { amount: '100', currency: 'SEK' }, booking_date: '2026-04-01' }],
|
|
rawPages: ['{}'],
|
|
})
|
|
mockConvertTransaction.mockReturnValue({
|
|
id: 'tx-1',
|
|
date: '2026-04-01',
|
|
booking_date: '2026-04-01',
|
|
amount: 100,
|
|
currency: 'SEK',
|
|
description: 'SEK purchase',
|
|
})
|
|
mockUploadDocument.mockResolvedValue({ id: 'doc-1' })
|
|
|
|
const account = makeAccount() // No ledger_account
|
|
await syncAccountTransactions(
|
|
{} as never,
|
|
COMPANY_ID,
|
|
USER_ID,
|
|
CONNECTION_ID,
|
|
account,
|
|
'2026-02-13',
|
|
'2026-05-13',
|
|
mockIngest
|
|
)
|
|
|
|
const ingestOptions = mockIngest.mock.calls[0][4]
|
|
expect(ingestOptions.settlementAccount).toBeUndefined()
|
|
})
|
|
})
|