feat: make extension system packageable via enriched ExtensionContext
Enrich ExtensionContext with supabase, emit(), settings, storage, log, and services so extensions can receive everything through dependency injection instead of importing core modules directly. - Add context factory and inject context into event handlers via registry - Move supplier invoice journal entry creation to core event handler - Add services.ingestTransactions to ExtensionContext for enable-banking - Create catch-all API route for extension-declared apiRoutes - Migrate 5 extensions to accept context with dynamic import fallbacks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
59d935f2cc
commit
ef5a84a5d5
@@ -1,16 +1,9 @@
|
||||
import type { Extension } from '@/lib/extensions/types'
|
||||
import { createClient, createServiceClient } from '@/lib/supabase/server'
|
||||
import type { Extension, ExtensionContext } from '@/lib/extensions/types'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { eventBus } from '@/lib/events'
|
||||
import {
|
||||
startAuthorization,
|
||||
getASPSPs,
|
||||
createSession,
|
||||
getAccountBalance,
|
||||
isConsentExpiringSoon,
|
||||
getDaysUntilExpiry,
|
||||
type ASPSP,
|
||||
type AccountInfo,
|
||||
} from './lib/api-client'
|
||||
import { syncAccountTransactions } from './lib/sync'
|
||||
import type { StoredAccount } from './types'
|
||||
@@ -41,7 +34,8 @@ export const enableBankingExtension: Extension = {
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/banks',
|
||||
handler: async () => {
|
||||
handler: async (_request: Request, ctx?: ExtensionContext) => {
|
||||
const log = ctx?.log ?? console
|
||||
try {
|
||||
const aspsps = await getASPSPs('SE')
|
||||
const banks = aspsps.map((aspsp: ASPSP) => ({
|
||||
@@ -52,7 +46,7 @@ export const enableBankingExtension: Extension = {
|
||||
}))
|
||||
return NextResponse.json({ banks })
|
||||
} catch (error) {
|
||||
console.error('Error fetching banks:', error)
|
||||
log.error('Error fetching banks:', error)
|
||||
return NextResponse.json({
|
||||
banks: [
|
||||
{ name: 'Nordea', country: 'SE', bic: 'NDEASESS' },
|
||||
@@ -67,8 +61,9 @@ export const enableBankingExtension: Extension = {
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/connect',
|
||||
handler: async (request: Request) => {
|
||||
const supabase = await createClient()
|
||||
handler: async (request: Request, ctx?: ExtensionContext) => {
|
||||
const log = ctx?.log ?? console
|
||||
const supabase = ctx?.supabase ?? await (await import('@/lib/supabase/server')).createClient()
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
@@ -108,7 +103,7 @@ export const enableBankingExtension: Extension = {
|
||||
.single()
|
||||
|
||||
if (error) {
|
||||
console.error('Database error:', error)
|
||||
log.error('Database error:', error)
|
||||
throw new Error('Failed to store connection')
|
||||
}
|
||||
|
||||
@@ -117,7 +112,7 @@ export const enableBankingExtension: Extension = {
|
||||
authorization_url: url,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Bank connection error:', error)
|
||||
log.error('Bank connection error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Connection failed' },
|
||||
{ status: 500 }
|
||||
@@ -128,8 +123,9 @@ export const enableBankingExtension: Extension = {
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/sync',
|
||||
handler: async (request: Request) => {
|
||||
const supabase = await createClient()
|
||||
handler: async (request: Request, ctx?: ExtensionContext) => {
|
||||
const log = ctx?.log ?? console
|
||||
const supabase = ctx?.supabase ?? await (await import('@/lib/supabase/server')).createClient()
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
@@ -161,6 +157,9 @@ export const enableBankingExtension: Extension = {
|
||||
.toISOString()
|
||||
.split('T')[0]
|
||||
|
||||
// Use ctx.services.ingestTransactions when available
|
||||
const ingestFn = ctx?.services.ingestTransactions
|
||||
|
||||
let totalImported = 0
|
||||
let totalDuplicates = 0
|
||||
|
||||
@@ -171,7 +170,8 @@ export const enableBankingExtension: Extension = {
|
||||
connection.id,
|
||||
account,
|
||||
fromDate,
|
||||
toDate
|
||||
toDate,
|
||||
ingestFn
|
||||
)
|
||||
|
||||
totalImported += result.imported
|
||||
@@ -198,7 +198,8 @@ export const enableBankingExtension: Extension = {
|
||||
.limit(totalImported)
|
||||
|
||||
if (syncedTransactions && syncedTransactions.length > 0) {
|
||||
await eventBus.emit({
|
||||
const emit = ctx?.emit ?? (await import('@/lib/events/bus')).eventBus.emit.bind((await import('@/lib/events/bus')).eventBus)
|
||||
await emit({
|
||||
type: 'transaction.synced',
|
||||
payload: { transactions: syncedTransactions as Transaction[], userId: user.id },
|
||||
})
|
||||
@@ -211,7 +212,7 @@ export const enableBankingExtension: Extension = {
|
||||
last_synced_at: syncedAt,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Sync error:', error)
|
||||
log.error('Sync error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Sync failed' },
|
||||
{ status: 500 }
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js'
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { getTransactions, getAccountBalance } from './api-client'
|
||||
import { ingestTransactions, type RawTransaction } from '@/lib/transactions/ingest'
|
||||
import { ingestTransactions as defaultIngest } from '@/lib/transactions/ingest'
|
||||
import type { RawTransaction, IngestResult } from '@/types'
|
||||
import type { StoredAccount } from '../types'
|
||||
|
||||
/** Ingest function signature — matches lib/transactions/ingest */
|
||||
export type IngestFn = (
|
||||
supabase: SupabaseClient,
|
||||
userId: string,
|
||||
raw: RawTransaction[]
|
||||
) => Promise<IngestResult>
|
||||
|
||||
export interface SyncResult {
|
||||
imported: number
|
||||
duplicates: number
|
||||
@@ -14,6 +22,10 @@ export interface SyncResult {
|
||||
*
|
||||
* Fetches transactions from the Enable Banking API, converts to RawTransaction
|
||||
* format, and delegates to the shared ingestion pipeline.
|
||||
*
|
||||
* @param ingest - Optional ingest function override (defaults to core ingestTransactions).
|
||||
* When called from an extension handler with ctx.services.ingestTransactions,
|
||||
* pass that function to avoid direct @/lib imports.
|
||||
*/
|
||||
export async function syncAccountTransactions(
|
||||
supabase: SupabaseClient,
|
||||
@@ -21,7 +33,8 @@ export async function syncAccountTransactions(
|
||||
connectionId: string,
|
||||
account: StoredAccount,
|
||||
fromDate: string,
|
||||
toDate: string
|
||||
toDate: string,
|
||||
ingest: IngestFn = defaultIngest
|
||||
): Promise<SyncResult> {
|
||||
const bankTransactions = await getTransactions(
|
||||
account.uid,
|
||||
@@ -44,7 +57,7 @@ export async function syncAccountTransactions(
|
||||
import_source: 'enable_banking',
|
||||
}))
|
||||
|
||||
const ingestResult = await ingestTransactions(supabase, userId, rawTransactions)
|
||||
const ingestResult = await ingest(supabase, userId, rawTransactions)
|
||||
|
||||
// Update account balance
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user