From 865d622f23db4c5fa7ef67ea86448e6386617f15 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 28 Sep 2025 22:39:14 +0000 Subject: [PATCH] feat: Add integrations for Supabase, Notion, and Google Drive Co-authored-by: info --- .env.example | 14 ++ README.md | 43 ++++ config/integrations.js | 155 +++++++++++++ package-lock.json | 512 +++++++++++++++++++++++++++++++++++++---- package.json | 21 +- public/index.html | 7 + public/script.js | 33 ++- server.js | 132 ++++++++++- 8 files changed, 860 insertions(+), 57 deletions(-) create mode 100644 .env.example create mode 100644 config/integrations.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..05628af --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +SUPABASE_URL= +SUPABASE_ANON_KEY= +SUPABASE_SERVICE_ROLE_KEY= +SUPABASE_BUCKET=financial-documents + +NOTION_TOKEN= +NOTION_DATABASE_ID= + +GOOGLE_PROJECT_ID= +GOOGLE_CLIENT_EMAIL= +GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" +GOOGLE_DRIVE_FOLDER_ID= + +PORT=3000 \ No newline at end of file diff --git a/README.md b/README.md index cae1c61..76fbbe9 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,49 @@ A comprehensive workspace application that combines credit analysis tools for co - **File Upload**: Multer middleware - **OCR**: Framework ready for integration with tesseract.js or similar +## Integrations + +The app can integrate with Supabase Storage, Notion, and Google Drive. Use the checkboxes in the UI to opt-in per action. Availability is shown by enabling/disabling those checkboxes based on server configuration. + +### Environment Variables + +Copy `.env.example` to `.env` and fill values: + +```bash +cp .env.example .env +``` + +- `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY` (or `SUPABASE_ANON_KEY`): Supabase project credentials +- `SUPABASE_BUCKET`: Storage bucket name (default: `financial-documents`) +- `NOTION_TOKEN`, `NOTION_DATABASE_ID`: Notion internal integration token and database ID +- `GOOGLE_CLIENT_EMAIL`, `GOOGLE_PRIVATE_KEY`, `GOOGLE_DRIVE_FOLDER_ID`: Google service account and a target folder ID + +Notes: +- For `GOOGLE_PRIVATE_KEY`, keep it on one line with `\n` for newlines, e.g. `"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"`. +- The app automatically converts `\n` to real newlines at runtime. + +### Supabase Setup +- Create a Supabase project and a Storage bucket (e.g. `financial-documents`). +- Optionally make the bucket public if you need public links, or keep private and serve via signed URLs (current code uses public URLs). +- Get the Service Role key for server-side uploads and set `SUPABASE_SERVICE_ROLE_KEY`. + +### Notion Setup +- Create a Notion internal integration and get the token. +- Create a database (or use an existing one) and share it with the integration. +- Copy the database ID to `NOTION_DATABASE_ID`. + +### Google Drive Setup +- Create a Google Cloud project and enable the Drive API. +- Create a Service Account and generate a JSON key. +- Use the service account email to share a target Drive folder (or its parent) with at least `Writer` access, and set that folder's ID in `GOOGLE_DRIVE_FOLDER_ID`. +- Put the JSON key fields into `.env`: + - `GOOGLE_CLIENT_EMAIL` + - `GOOGLE_PRIVATE_KEY` (escaped with `\n` as above) + +### Runtime +- Start the server and visit `/api/integrations` to see booleans for configured services. +- In the UI, checkboxes will be disabled if a service is not configured. + ## Installation 1. Clone the repository: diff --git a/config/integrations.js b/config/integrations.js new file mode 100644 index 0000000..a26cfee --- /dev/null +++ b/config/integrations.js @@ -0,0 +1,155 @@ +'use strict'; + +require('dotenv').config(); + +const { createClient } = require('@supabase/supabase-js'); +const { Client: NotionClient } = require('@notionhq/client'); +const { google } = require('googleapis'); + +const supabaseUrl = process.env.SUPABASE_URL; +const supabaseAnonKey = process.env.SUPABASE_ANON_KEY; +const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; +const supabaseBucket = process.env.SUPABASE_BUCKET || 'financial-documents'; + +const notionToken = process.env.NOTION_TOKEN; +const notionDatabaseId = process.env.NOTION_DATABASE_ID; + +const googleProjectId = process.env.GOOGLE_PROJECT_ID; +const googleClientEmail = process.env.GOOGLE_CLIENT_EMAIL; +let googlePrivateKey = process.env.GOOGLE_PRIVATE_KEY; +if (googlePrivateKey && googlePrivateKey.includes('\\n')) { + googlePrivateKey = googlePrivateKey.replace(/\\n/g, '\n'); +} +const googleDriveFolderId = process.env.GOOGLE_DRIVE_FOLDER_ID; + +const supabaseKeyToUse = supabaseServiceKey || supabaseAnonKey; +const supabase = (supabaseUrl && supabaseKeyToUse) ? createClient(supabaseUrl, supabaseKeyToUse) : null; + +const notion = notionToken ? new NotionClient({ auth: notionToken }) : null; + +let drive = null; +if (googleClientEmail && googlePrivateKey) { + const jwt = new google.auth.JWT({ + email: googleClientEmail, + key: googlePrivateKey, + scopes: ['https://www.googleapis.com/auth/drive.file'], + subject: undefined, + }); + drive = google.drive({ version: 'v3', auth: jwt }); +} + +const integrations = { + supabase, + supabaseBucket, + notion, + notionDatabaseId, + drive, + googleDriveFolderId, + isSupabaseConfigured: Boolean(supabase), + isNotionConfigured: Boolean(notion), + isGoogleDriveConfigured: Boolean(drive), +}; + +async function uploadFileToSupabase(localFilePath, destinationPath, mimeType) { + if (!supabase) { + throw new Error('Supabase is not configured'); + } + const fs = require('fs').promises; + const buffer = await fs.readFile(localFilePath); + const { data, error } = await supabase.storage.from(supabaseBucket).upload(destinationPath, buffer, { + contentType: mimeType || 'application/octet-stream', + upsert: true, + }); + if (error) { + throw error; + } + const { data: publicUrlData } = supabase.storage.from(supabaseBucket).getPublicUrl(destinationPath); + return { path: data.path, publicUrl: publicUrlData.publicUrl }; +} + +async function uploadFileToGoogleDrive(localFilePath, destinationName, mimeType) { + if (!drive) { + throw new Error('Google Drive is not configured'); + } + const fs = require('fs'); + const fileMetadata = { + name: destinationName, + parents: googleDriveFolderId ? [googleDriveFolderId] : undefined, + }; + const media = { + mimeType: mimeType || 'application/octet-stream', + body: fs.createReadStream(localFilePath), + }; + const res = await drive.files.create({ + requestBody: fileMetadata, + media, + fields: 'id, name, webViewLink, webContentLink', + }); + const fileId = res.data.id; + try { + await drive.permissions.create({ + fileId, + requestBody: { role: 'reader', type: 'anyone' }, + }); + } catch (e) {} + const linkRes = await drive.files.get({ + fileId, + fields: 'id, name, webViewLink, webContentLink', + }); + return linkRes.data; +} + +async function exportCreditMemoToNotion(memo) { + if (!notion) { + throw new Error('Notion is not configured'); + } + if (!notionDatabaseId) { + throw new Error('NOTION_DATABASE_ID is not set'); + } + const page = await notion.pages.create({ + parent: { database_id: notionDatabaseId }, + properties: { + Title: { + title: [{ text: { content: memo.title || 'Credit Memo' } }], + }, + MemoType: { + rich_text: [{ text: { content: memo.memo_type || '' } }], + }, + CompanyId: { + number: Number(memo.company_id) || null, + }, + CreatedAt: { + date: { start: new Date().toISOString() }, + }, + }, + children: [ + { + object: 'block', + type: 'paragraph', + paragraph: { + rich_text: [{ type: 'text', text: { content: memo.content || '' } }], + }, + }, + ...(memo.financial_metrics + ? [ + { + object: 'block', + type: 'code', + code: { + language: 'json', + rich_text: [{ type: 'text', text: { content: typeof memo.financial_metrics === 'string' ? memo.financial_metrics : JSON.stringify(memo.financial_metrics, null, 2) } }], + }, + }, + ] + : []), + ], + }); + return page; +} + +module.exports = { + integrations, + uploadFileToSupabase, + uploadFileToGoogleDrive, + exportCreditMemoToNotion, +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fc93824..2520a2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,14 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@notionhq/client": "^5.1.0", + "@supabase/storage-js": "^2.12.2", + "@supabase/supabase-js": "^2.58.0", "body-parser": "^1.20.2", + "dotenv": "^17.2.2", "express": "^4.18.2", - "fs": "^0.0.1-security", + "googleapis": "^160.0.0", "multer": "^1.4.5-lts.1", - "path": "^0.12.7", "sqlite3": "^5.1.6" } }, @@ -24,6 +27,15 @@ "license": "MIT", "optional": true }, + "node_modules/@notionhq/client": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@notionhq/client/-/client-5.1.0.tgz", + "integrity": "sha512-YYVjXYk1XwKQ4XIh+iGjaaXOGHxaDgB3UaGnDMyrZ3X9UiYQsZpzPIvTuhvp97os8a5W5kTQFsyq77+I+COOVQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -63,6 +75,80 @@ "node": ">=10" } }, + "node_modules/@supabase/auth-js": { + "version": "2.72.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.72.0.tgz", + "integrity": "sha512-4+bnUrtTDK1YD0/FCx2YtMiQH5FGu9Jlf4IQi5kcqRwRwqp2ey39V61nHNdH86jm3DIzz0aZKiWfTW8qXk1swQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.5.0.tgz", + "integrity": "sha512-SXBx6Jvp+MOBekeKFu+G11YLYPeVeGQl23eYyAG9+Ro0pQ1aIP0UZNIBxHKNHqxzR0L0n6gysNr2KT3841NATw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.21.4", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.21.4.tgz", + "integrity": "sha512-TxZCIjxk6/dP9abAi89VQbWWMBbybpGWyvmIzTd79OeravM13OjR/YEYeyUOPcM1C3QyvXkvPZhUfItvmhY1IQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.15.5", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.15.5.tgz", + "integrity": "sha512-/Rs5Vqu9jejRD8ZeuaWXebdkH+J7V6VySbCZ/zQM93Ta5y3mAmocjioa/nzlB6qvFmyylUgKVS1KpE212t30OA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.13", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.12.2.tgz", + "integrity": "sha512-SiySHxi3q7gia7NBYpsYRu8gyI0NhFwSORMxbZIxJ/zAVkN6QpwDRan158CJ+UdzD4WB/rQMAGRqIJQP+7ccAQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.58.0.tgz", + "integrity": "sha512-Tm1RmQpoAKdQr4/8wiayGti/no+If7RtveVZjHR8zbO7hhQjmPW2Ok5ZBPf1MGkt5c+9R85AVMsTfSaqAP1sUg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.72.0", + "@supabase/functions-js": "2.5.0", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.21.4", + "@supabase/realtime-js": "2.15.5", + "@supabase/storage-js": "2.12.2" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -73,6 +159,30 @@ "node": ">= 6" } }, + "node_modules/@types/node": { + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.12.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -244,6 +354,15 @@ ], "license": "MIT" }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -337,6 +456,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -535,6 +660,15 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -603,6 +737,18 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -617,6 +763,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -788,6 +943,35 @@ "url": "https://opencollective.com/express" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -812,6 +996,18 @@ "node": ">= 0.8" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -830,12 +1026,6 @@ "node": ">= 0.6" } }, - "node_modules/fs": { - "version": "0.0.1-security", - "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", - "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==", - "license": "ISC" - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -891,6 +1081,79 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/gaxios": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.2.tgz", + "integrity": "sha512-/Szrn8nr+2TsQT1Gp8iIe/BEytJmbyfrbFh419DfGQSkEgNEhbPi7JRJuughjkTzPWgU9gBQf5AVu3DbHt0OXA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/gcp-metadata": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", + "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -956,6 +1219,62 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/google-auth-library": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.3.0.tgz", + "integrity": "sha512-ylSE3RlCRZfZB56PFJSfUCuiuPq83Fx8hqu1KPWGK8FVdSaxlp/qkeMMX/DT/18xkwXIHvXEXkZsljRwfrdEfQ==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^7.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.1.tgz", + "integrity": "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "160.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-160.0.0.tgz", + "integrity": "sha512-lAGFQVSeYvWKCxeCQKo20xWFFDgnWuJYbLn92IgLrT4UTYVOGdrZ9XTqgWJf316isE9KdfuDY5X8Tu4ZrXSFig==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.2.0", + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/googleapis-common": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.0.tgz", + "integrity": "sha512-66if47It7y+Sab3HMkwEXx1kCq9qUC9px8ZXoj1CMrmLmUw81GpbnsNlXnlyZyGbGPGcj+tDD9XsZ23m7GLaJQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^7.0.0-rc.4", + "google-auth-library": "^10.1.0", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -975,6 +1294,19 @@ "license": "ISC", "optional": true }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1250,6 +1582,36 @@ "license": "ISC", "optional": true }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1565,6 +1927,44 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", @@ -1690,16 +2090,6 @@ "node": ">= 0.8" } }, - "node_modules/path": { - "version": "0.12.7", - "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", - "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", - "license": "MIT", - "dependencies": { - "process": "^0.11.1", - "util": "^0.10.3" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -1742,15 +2132,6 @@ "node": ">=10" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -2399,6 +2780,12 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2430,6 +2817,12 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "license": "MIT" + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -2459,14 +2852,11 @@ "node": ">= 0.8" } }, - "node_modules/util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "license": "MIT", - "dependencies": { - "inherits": "2.0.3" - } + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2474,12 +2864,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/util/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "license": "ISC" - }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -2498,6 +2882,31 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2530,6 +2939,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 3a686b9..685acb4 100644 --- a/package.json +++ b/package.json @@ -8,15 +8,24 @@ "dev": "node server.js", "test": "echo \"No tests specified\" && exit 0" }, - "keywords": ["credit-analysis", "commodities", "trading", "novel-planning", "workspace"], + "keywords": [ + "credit-analysis", + "commodities", + "trading", + "novel-planning", + "workspace" + ], "author": "", "license": "MIT", "dependencies": { + "@notionhq/client": "^5.1.0", + "@supabase/storage-js": "^2.12.2", + "@supabase/supabase-js": "^2.58.0", + "body-parser": "^1.20.2", + "dotenv": "^17.2.2", "express": "^4.18.2", - "sqlite3": "^5.1.6", + "googleapis": "^160.0.0", "multer": "^1.4.5-lts.1", - "body-parser": "^1.20.2", - "path": "^0.12.7", - "fs": "^0.0.1-security" + "sqlite3": "^5.1.6" } -} \ No newline at end of file +} diff --git a/public/index.html b/public/index.html index 4e35965..9c66402 100644 --- a/public/index.html +++ b/public/index.html @@ -44,6 +44,10 @@

Financial Document Upload (OCR)

+
+ + +
@@ -64,6 +68,9 @@

Credit Memo Generation

+
+ +
diff --git a/public/script.js b/public/script.js index aa37199..d08919b 100644 --- a/public/script.js +++ b/public/script.js @@ -22,6 +22,7 @@ document.addEventListener('DOMContentLoaded', function() { // Load initial data loadCompanies(); loadNovels(); + loadIntegrationAvailability(); }); // Navigation @@ -100,6 +101,8 @@ async function handleFileUpload(e) { formData.append('company_id', document.getElementById('upload-company').value); formData.append('document_type', document.getElementById('document-type').value); formData.append('document', document.getElementById('financial-document').files[0]); + formData.append('uploadToSupabase', document.getElementById('flag-supabase').checked); + formData.append('uploadToDrive', document.getElementById('flag-drive').checked); try { const response = await fetch('/api/upload-financial', { @@ -125,7 +128,8 @@ async function handleMemoSubmit(e) { memo_type: document.getElementById('memo-type').value, title: document.getElementById('memo-title').value, content: document.getElementById('memo-content').value, - financial_metrics: parseJSON(document.getElementById('memo-metrics').value) + financial_metrics: parseJSON(document.getElementById('memo-metrics').value), + exportToNotion: document.getElementById('flag-notion').checked }; try { @@ -359,4 +363,31 @@ function parseJSON(str) { } catch { return {}; } +} + +// Integrations availability +async function loadIntegrationAvailability() { + try { + const res = await fetch('/api/integrations'); + if (!res.ok) return; + const cfg = await res.json(); + const supabaseEl = document.getElementById('flag-supabase'); + const driveEl = document.getElementById('flag-drive'); + const notionEl = document.getElementById('flag-notion'); + + if (supabaseEl) { + supabaseEl.disabled = !cfg.supabase; + supabaseEl.title = cfg.supabase ? '' : 'Supabase not configured on server'; + } + if (driveEl) { + driveEl.disabled = !cfg.googleDrive; + driveEl.title = cfg.googleDrive ? '' : 'Google Drive not configured on server'; + } + if (notionEl) { + notionEl.disabled = !cfg.notion; + notionEl.title = cfg.notion ? '' : 'Notion not configured on server'; + } + } catch (e) { + // ignore; endpoint may be unavailable during startup + } } \ No newline at end of file diff --git a/server.js b/server.js index 3e60877..fe9d7a9 100644 --- a/server.js +++ b/server.js @@ -1,9 +1,11 @@ +require('dotenv').config(); const express = require('express'); const sqlite3 = require('sqlite3').verbose(); const multer = require('multer'); const bodyParser = require('body-parser'); const path = require('path'); const fs = require('fs'); +const { integrations, uploadFileToSupabase, uploadFileToGoogleDrive, exportCreditMemoToNotion } = require('./config/integrations'); const app = express(); const PORT = process.env.PORT || 3000; @@ -13,6 +15,15 @@ app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(express.static('public')); +// Ensure uploads directory exists +try { + if (!fs.existsSync(path.join(__dirname, 'uploads'))) { + fs.mkdirSync(path.join(__dirname, 'uploads'), { recursive: true }); + } +} catch (e) { + console.error('Failed to ensure uploads directory:', e.message); +} + // Multer setup for file uploads const upload = multer({ dest: 'uploads/' }); @@ -41,6 +52,10 @@ function initializeDatabase() { document_type TEXT, file_path TEXT, extracted_data TEXT, + supabase_path TEXT, + supabase_public_url TEXT, + gdrive_file_id TEXT, + gdrive_web_view_link TEXT, upload_date DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (company_id) REFERENCES companies (id) )`); @@ -52,6 +67,8 @@ function initializeDatabase() { title TEXT, content TEXT, financial_metrics TEXT, + notion_page_id TEXT, + notion_url TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (company_id) REFERENCES companies (id) )`); @@ -94,6 +111,35 @@ function initializeDatabase() { )`); } +// Utility to add a column if it does not exist (for existing DBs) +function ensureColumnExists(tableName, columnName, columnType) { + return new Promise((resolve, reject) => { + db.all(`PRAGMA table_info(${tableName})`, (err, rows) => { + if (err) return reject(err); + const exists = rows.some(r => r.name === columnName); + if (exists) return resolve(); + db.run(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnType}`, (alterErr) => { + if (alterErr) return reject(alterErr); + resolve(); + }); + }); + }); +} + +// Best-effort migrations for existing databases +(async () => { + try { + await ensureColumnExists('financial_data', 'supabase_path', 'TEXT'); + await ensureColumnExists('financial_data', 'supabase_public_url', 'TEXT'); + await ensureColumnExists('financial_data', 'gdrive_file_id', 'TEXT'); + await ensureColumnExists('financial_data', 'gdrive_web_view_link', 'TEXT'); + await ensureColumnExists('credit_memos', 'notion_page_id', 'TEXT'); + await ensureColumnExists('credit_memos', 'notion_url', 'TEXT'); + } catch (e) { + console.warn('DB migration warning:', e.message); + } +})(); + // Routes // Main dashboard @@ -102,6 +148,13 @@ app.get('/', (req, res) => { }); // Credit Analysis Routes +app.get('/api/integrations', (req, res) => { + res.json({ + supabase: integrations.isSupabaseConfigured, + notion: integrations.isNotionConfigured, + googleDrive: integrations.isGoogleDriveConfigured, + }); +}); app.get('/api/companies', (req, res) => { db.all('SELECT * FROM companies ORDER BY name', (err, rows) => { if (err) { @@ -125,30 +178,91 @@ app.post('/api/companies', (req, res) => { app.post('/api/upload-financial', upload.single('document'), (req, res) => { const { company_id, document_type } = req.body; + const uploadToSupabase = String(req.body.uploadToSupabase).toLowerCase() === 'true'; + const uploadToDrive = String(req.body.uploadToDrive).toLowerCase() === 'true'; const file_path = req.file.path; - + const mimeType = req.file.mimetype || 'application/octet-stream'; + // Basic OCR simulation - in a real implementation, you'd use tesseract.js or similar const extracted_data = `Extracted financial data from ${req.file.originalname}`; - + db.run('INSERT INTO financial_data (company_id, document_type, file_path, extracted_data) VALUES (?, ?, ?, ?)', [company_id, document_type, file_path, extracted_data], function(err) { if (err) { - res.status(500).json({ error: err.message }); - } else { - res.json({ id: this.lastID, message: 'Financial document uploaded and processed' }); + return res.status(500).json({ error: err.message }); } + + const recordId = this.lastID; + (async () => { + const updates = {}; + const originalName = (req.file.originalname || 'file').replace(/\s+/g, '_'); + const destinationPath = `company_${company_id || 'unknown'}/${Date.now()}_${originalName}`; + try { + if (uploadToSupabase && integrations.isSupabaseConfigured) { + const sb = await uploadFileToSupabase(file_path, destinationPath, mimeType); + updates.supabase_path = sb.path; + updates.supabase_public_url = sb.publicUrl; + } + } catch (e) { + console.warn('Supabase upload failed:', e.message); + } + try { + if (uploadToDrive && integrations.isGoogleDriveConfigured) { + const driveRes = await uploadFileToGoogleDrive(file_path, originalName, mimeType); + updates.gdrive_file_id = driveRes.id; + updates.gdrive_web_view_link = driveRes.webViewLink || driveRes.webContentLink || null; + } + } catch (e) { + console.warn('Google Drive upload failed:', e.message); + } + + if (Object.keys(updates).length > 0) { + const setClauses = Object.keys(updates).map(k => `${k} = ?`).join(', '); + const params = [...Object.values(updates), recordId]; + db.run(`UPDATE financial_data SET ${setClauses} WHERE id = ?`, params, (updErr) => { + if (updErr) { + console.warn('Failed to update financial_data with external links:', updErr.message); + } + res.json({ id: recordId, message: 'Financial document uploaded and processed', ...updates }); + }); + } else { + res.json({ id: recordId, message: 'Financial document uploaded and processed' }); + } + })(); }); }); app.post('/api/credit-memos', (req, res) => { - const { company_id, memo_type, title, content, financial_metrics } = req.body; + const { company_id, memo_type, title, content } = req.body; + const financial_metrics = req.body.financial_metrics || {}; + const exportToNotion = String(req.body.exportToNotion).toLowerCase() === 'true'; + db.run('INSERT INTO credit_memos (company_id, memo_type, title, content, financial_metrics) VALUES (?, ?, ?, ?, ?)', [company_id, memo_type, title, content, JSON.stringify(financial_metrics)], function(err) { if (err) { - res.status(500).json({ error: err.message }); - } else { - res.json({ id: this.lastID, message: 'Credit memo created' }); + return res.status(500).json({ error: err.message }); } + + const recordId = this.lastID; + (async () => { + if (exportToNotion && integrations.isNotionConfigured) { + try { + const page = await exportCreditMemoToNotion({ company_id, memo_type, title, content, financial_metrics }); + const notion_page_id = page.id; + const notion_url = page.url || null; + db.run('UPDATE credit_memos SET notion_page_id = ?, notion_url = ? WHERE id = ?', [notion_page_id, notion_url, recordId], (updErr) => { + if (updErr) { + console.warn('Failed to update credit memo with Notion info:', updErr.message); + } + res.json({ id: recordId, message: 'Credit memo created', notion_page_id, notion_url }); + }); + return; + } catch (e) { + console.warn('Notion export failed:', e.message); + } + } + res.json({ id: recordId, message: 'Credit memo created' }); + })(); }); });