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
1 change: 1 addition & 0 deletions packages/core/src/ksef/ksef.adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export class KsefAdapterImpl implements KsefAdapter {
subjectType,
pageSize: opts.pageSize,
pageOffset: opts.pageOffset,
includeXml: opts.includeXml,
})

return result.invoices.map((raw) => ({
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/ksef/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface KsefClient {
subjectType?: KsefSubjectType
pageSize?: number
pageOffset?: number
includeXml?: boolean
}): Promise<{ invoices: KsefRawInvoice[]; total: number }>
sendInvoice(params: {
token: string
Expand Down
22 changes: 21 additions & 1 deletion packages/http/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
shouldRefresh,
type ActiveSession,
} from './session.js'
import { fetchInvoices as fetchInvoicesHttp } from './invoices.js'
import { fetchInvoices as fetchInvoicesHttp, fetchInvoiceXml as fetchInvoiceXmlHttp } from './invoices.js'
import { fetchUpoXml } from './upo.js'
import { KsefApiError } from './errors.js'
import { fetchKsefTokenEncryptionKey } from './public-key.js'
Expand Down Expand Up @@ -129,6 +129,7 @@ export class KsefHttpClient implements KsefClient {
subjectType?: 'Subject1' | 'Subject2' | 'Subject3'
pageSize?: number
pageOffset?: number
includeXml?: boolean
}): Promise<{ invoices: KsefRawInvoice[]; total: number }> {
let session = decodeSessionToken(params.token)
if (!session) {
Expand All @@ -151,12 +152,31 @@ export class KsefHttpClient implements KsefClient {
pageSize: params.pageSize,
pageOffset: params.pageOffset,
subjectType: params.subjectType ?? 'Subject2',
includeXml: params.includeXml,
}),
this.retryOpts,
)
return result
}

async fetchInvoiceXml(params: { token: string; ksefNumber: string }): Promise<string> {
let session = decodeSessionToken(params.token)
if (!session) {
throw new Error('KsefHttpClient.fetchInvoiceXml: invalid session token')
}
if (shouldRefresh(session)) {
session = await withHttpRetry(
() => refreshAccessToken(this.http, session as ActiveSession),
this.retryOpts,
)
}
return withHttpRetry(
() =>
fetchInvoiceXmlHttp(this.http, (session as ActiveSession).accessToken, params.ksefNumber),
this.retryOpts,
)
}

async sendInvoice(): Promise<{ ksefReferenceNumber: string; timestamp: string }> {
throw new Error(
'KsefHttpClient.sendInvoice: not implemented in HTTP client MVP — see http_plan.md §H05.5',
Expand Down
35 changes: 34 additions & 1 deletion packages/mcp/src/tools/sync-invoices.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { Ksefnik } from '@ksefnik/core'
import { writeFile, mkdir } from 'node:fs/promises'
import { resolve, isAbsolute, join } from 'node:path'
import { z } from 'zod'

export const syncInvoicesSchema = z.object({
Expand All @@ -11,17 +13,48 @@ export const syncInvoicesSchema = z.object({
* Defaults to `cost` for backwards compatibility with the MVP.
*/
subject: z.enum(['sales', 'cost']).optional(),
/**
* When provided, downloads the full FA(2)/FA(3) XML for each invoice and
* saves it to `${saveDir}/${ksefReference}.xml`. Path must be absolute.
* Creates the directory if it doesn't exist.
*/
saveDir: z.string().optional(),
})

export type SyncInvoicesInput = z.infer<typeof syncInvoicesSchema>

export async function syncInvoices(ksef: Ksefnik, input: SyncInvoicesInput) {
const subjectType = input.subject === 'sales' ? 'Subject1' : 'Subject2'
const includeXml = Boolean(input.saveDir)

const invoices = await ksef.invoices.fetch({
from: input.dateFrom,
to: input.dateTo,
nip: input.nip,
subjectType,
includeXml,
})
return { invoices, count: invoices.length, subject: input.subject ?? 'cost' }

let savedFiles: string[] = []
if (input.saveDir) {
const dir = isAbsolute(input.saveDir) ? input.saveDir : resolve(input.saveDir)
await mkdir(dir, { recursive: true })
savedFiles = await Promise.all(
invoices
.filter((inv) => inv.ksefReference && inv.rawXml)
.map(async (inv) => {
const path = join(dir, `${inv.ksefReference}.xml`)
await writeFile(path, inv.rawXml ?? '', 'utf8')
return path
}),
)
}

return {
invoices,
count: invoices.length,
subject: input.subject ?? 'cost',
savedFiles: input.saveDir ? savedFiles : undefined,
savedCount: input.saveDir ? savedFiles.length : undefined,
}
}
6 changes: 6 additions & 0 deletions packages/shared/src/types/ksef-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export interface FetchInvoicesOpts {
subjectType?: InvoiceSubjectType
pageSize?: number
pageOffset?: number
/**
* When true, also fetch the full FA(2)/FA(3) XML body for each invoice via
* `GET /invoices/ksef/{ksefNumber}` and put it on `Invoice.rawXml`. Off by
* default — metadata alone covers grossAmount/NIPs/dates.
*/
includeXml?: boolean
}

export interface SendInvoiceInput {
Expand Down
Loading