From ea49fe800fd0f048a0c25eb15bd23d1c965f4e93 Mon Sep 17 00:00:00 2001 From: KavyaCSH Date: Wed, 19 Aug 2026 11:05:04 +0530 Subject: [PATCH] feat: implement dispensing module with transaction-safe stock management, FEFO allocation, and H1 compliance reporting --- .env | 4 +- AI_Pharmacy_Postman_Collection.json | 54 +++++ add_profile_image.js | 21 ++ add_profile_image.ts | 18 ++ alter_db.js | 5 +- check_novamox.js | 7 + fix.js | 8 + fix_history.js | 30 +++ fix_pack_size.js | 15 ++ fix_stock.js | 26 ++ migrate_multitenant.js | 35 +++ package-lock.json | 358 +++++++++++++++++++++++++--- package.json | 6 + src/db/pool.js | 20 ++ src/index.ts | 2 + src/modules/ai/service.ts | 29 ++- src/modules/audit/router.ts | 2 +- src/modules/catalogue/service.ts | 2 + src/modules/dispensing/pdf.ts | 155 ++++++++++++ src/modules/dispensing/router.ts | 18 ++ src/modules/dispensing/service.ts | 32 +++ src/modules/identity/router.ts | 27 ++- src/modules/identity/service.ts | 10 +- src/modules/integration/router.ts | 2 +- src/modules/inventory/service.ts | 6 +- src/modules/upload/controller.ts | 60 +++++ src/modules/upload/router.ts | 18 ++ src/modules/upload/service.ts | 129 ++++++++++ test_billing.ts | 53 ++++ test_hist.js | 30 +++ update_batch.js | 7 + update_old_adjs.js | 14 ++ 32 files changed, 1150 insertions(+), 53 deletions(-) create mode 100644 add_profile_image.js create mode 100644 add_profile_image.ts create mode 100644 check_novamox.js create mode 100644 fix.js create mode 100644 fix_history.js create mode 100644 fix_pack_size.js create mode 100644 fix_stock.js create mode 100644 migrate_multitenant.js create mode 100644 src/db/pool.js create mode 100644 src/modules/dispensing/pdf.ts create mode 100644 src/modules/upload/controller.ts create mode 100644 src/modules/upload/router.ts create mode 100644 src/modules/upload/service.ts create mode 100644 test_billing.ts create mode 100644 test_hist.js create mode 100644 update_batch.js create mode 100644 update_old_adjs.js diff --git a/.env b/.env index 287c49b..99f886f 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ DATABASE_URL=postgres://postgres:csh%402018@localhost:5432/ai_pharmacy PORT=8080 -JWT_SECRET=super_secret_pharmacy_key_2026 +JWT_SECRET=caa4e8c1e05f10194b15ac3064df6289bb431b4daa3d1db6a469821212d8973f # S3 CONFIGURATION S3_KEY=AKIA3GRU7BB33WCCTN4V @@ -11,5 +11,5 @@ S3_BASE_PATH=temp/ S3_PUBLIC_BASE_PATH=https://a2zschoolfolio.s3.ap-south-1.amazonaws.com/ -GEMINI_API_KEY=AQ.Ab8RN6KkBPhZo9kmwV-mFBtriT3LF8vPWenXNfA2jSyVWWwMMQ +OPENAI_API_KEY=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 \ No newline at end of file diff --git a/AI_Pharmacy_Postman_Collection.json b/AI_Pharmacy_Postman_Collection.json index 1d95247..0a51a42 100644 --- a/AI_Pharmacy_Postman_Collection.json +++ b/AI_Pharmacy_Postman_Collection.json @@ -77,6 +77,60 @@ ] } } + }, + { + "name": "Register Pharmacist", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"test_pharma\",\n \"password\": \"secret123\"\n}" + }, + "url": { + "raw": "{{base_url}}/v1/auth/register", + "host": [ + "{{base_url}}" + ], + "path": [ + "v1", + "auth", + "register" + ] + } + } + }, + { + "name": "Pharmacist Login", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"test_pharma\",\n \"password\": \"secret123\"\n}" + }, + "url": { + "raw": "{{base_url}}/v1/auth/login", + "host": [ + "{{base_url}}" + ], + "path": [ + "v1", + "auth", + "login" + ] + } + } } ] }, diff --git a/add_profile_image.js b/add_profile_image.js new file mode 100644 index 0000000..ba57643 --- /dev/null +++ b/add_profile_image.js @@ -0,0 +1,21 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const pool_1 = require("./src/db/pool"); +async function migrate() { + try { + await pool_1.pool.query('ALTER TABLE users ADD COLUMN profile_image_url TEXT'); + console.log("Migration successful"); + } + catch (err) { + if (err.message.includes('already exists')) { + console.log("Column already exists"); + } + else { + console.error(err); + } + } + finally { + process.exit(0); + } +} +migrate(); diff --git a/add_profile_image.ts b/add_profile_image.ts new file mode 100644 index 0000000..00a0026 --- /dev/null +++ b/add_profile_image.ts @@ -0,0 +1,18 @@ +import { pool } from './src/db/pool'; + +async function migrate() { + try { + await pool.query('ALTER TABLE users ADD COLUMN profile_image_url TEXT'); + console.log("Migration successful"); + } catch (err: any) { + if (err.message.includes('already exists')) { + console.log("Column already exists"); + } else { + console.error(err); + } + } finally { + process.exit(0); + } +} + +migrate(); diff --git a/alter_db.js b/alter_db.js index ba9f2db..a2a1dc8 100644 --- a/alter_db.js +++ b/alter_db.js @@ -6,8 +6,9 @@ const pool = new Pool({ async function run() { try { - await pool.query('ALTER TABLE dispensing_records ADD COLUMN IF NOT EXISTS prescription_id VARCHAR(50)'); - console.log('Successfully added prescription_id column.'); + await pool.query('ALTER TABLE stock_movements ADD COLUMN IF NOT EXISTS reason_code VARCHAR(100)'); + await pool.query('ALTER TABLE stock_movements ADD COLUMN IF NOT EXISTS notes TEXT'); + console.log('Successfully added columns.'); } catch (err) { console.error('Error:', err); } finally { diff --git a/check_novamox.js b/check_novamox.js new file mode 100644 index 0000000..a6619df --- /dev/null +++ b/check_novamox.js @@ -0,0 +1,7 @@ +const { Client } = require('pg'); +const client = new Client('postgres://postgres:csh%402018@localhost:5432/ai_pharmacy'); +client.connect().then(async () => { + const res = await client.query("SELECT brand_name, mrp, pack_size FROM products WHERE brand_name LIKE '%Novamox%'"); + console.log(res.rows); + process.exit(0); +}).catch(console.error); diff --git a/fix.js b/fix.js new file mode 100644 index 0000000..2b4a39b --- /dev/null +++ b/fix.js @@ -0,0 +1,8 @@ +const { Client } = require('pg'); +const client = new Client('postgres://postgres:csh%402018@localhost:5432/ai_pharmacy'); +client.connect().then(async () => { + await client.query("UPDATE products SET mrp = 30.00 WHERE brand_name = 'Dolo'"); + await client.query("UPDATE products SET mrp = 115.00 WHERE brand_name = 'Benadryl DR'"); + console.log('Prices updated'); + process.exit(0); +}).catch(console.error); diff --git a/fix_history.js b/fix_history.js new file mode 100644 index 0000000..7fe3c06 --- /dev/null +++ b/fix_history.js @@ -0,0 +1,30 @@ +const { Client } = require('pg'); +const client = new Client('postgres://postgres:csh%402018@localhost:5432/ai_pharmacy'); + +client.connect().then(async () => { + try { + // 1. Update the pack_size of Losar 50 to 10 + await client.query("UPDATE products SET pack_size = 10 WHERE id = 'prd_4410'"); + + // 2. Fix all old dispensing records that were calculated with pack_size=1 + // For every dispensing record, we recalculate the total amount. + const res = await client.query(` + SELECT d.id, + SUM((p.mrp / COALESCE(NULLIF(p.pack_size, 0), 1)) * ABS(sm.qty_delta)) as new_total + FROM dispensing_records d + JOIN stock_movements sm ON sm.reference_id = d.id + JOIN batches b ON b.batch_no = sm.batch_no + JOIN products p ON p.id = b.product_id + GROUP BY d.id + `); + + for (const row of res.rows) { + await client.query("UPDATE dispensing_records SET total_amount = $1 WHERE id = $2", [row.new_total, row.id]); + } + + console.log("Updated pack_size to 10 for prd_4410 and corrected all historical total amounts."); + } catch (err) { + console.error('Error fixing history:', err); + } + process.exit(0); +}).catch(console.error); diff --git a/fix_pack_size.js b/fix_pack_size.js new file mode 100644 index 0000000..61f872b --- /dev/null +++ b/fix_pack_size.js @@ -0,0 +1,15 @@ +const { Client } = require('pg'); +const client = new Client('postgres://postgres:csh%402018@localhost:5432/ai_pharmacy'); +client.connect().then(async () => { + try { + await client.query("ALTER TABLE products ADD COLUMN pack_size INTEGER DEFAULT 1;"); + console.log('Added pack_size column to products table.'); + } catch (err) { + if (err.code === '42701') { + console.log('pack_size column already exists.'); + } else { + console.error(err); + } + } + process.exit(0); +}).catch(console.error); diff --git a/fix_stock.js b/fix_stock.js new file mode 100644 index 0000000..fe7e948 --- /dev/null +++ b/fix_stock.js @@ -0,0 +1,26 @@ +const { Client } = require('pg'); +const client = new Client('postgres://postgres:csh%402018@localhost:5432/ai_pharmacy'); + +client.connect().then(async () => { + try { + const res = await client.query('SELECT DISTINCT tenant_id FROM products WHERE tenant_id IS NOT NULL'); + const tenants = res.rows.map(r => r.tenant_id); + if (!tenants.includes('act_admin')) tenants.push('act_admin'); + + // Expiry date 2 years from now + const expiry = new Date(Date.now() + 2 * 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + + for (const t of tenants) { + const batchNo = 'B_FIX_' + t.substring(0,8) + '_' + Date.now(); + await client.query(` + INSERT INTO batches (batch_no, product_id, expiry, qty_on_hand, purchase_price, tenant_id) + VALUES ($1, 'prd_4410', $2, 1000, 50.00, $3) + `, [batchNo, expiry, t]); + console.log(`Added stock for tenant ${t}`); + } + + } catch (err) { + console.error('Error adding stock:', err); + } + process.exit(0); +}).catch(console.error); diff --git a/migrate_multitenant.js b/migrate_multitenant.js new file mode 100644 index 0000000..182c65e --- /dev/null +++ b/migrate_multitenant.js @@ -0,0 +1,35 @@ +const { Pool } = require('pg'); +require('dotenv').config(); + +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); + +async function migrate() { + try { + const DEFAULT_TENANT = 'act_admin'; // Fallback tenant for existing data + + await pool.query(` + ALTER TABLE prescriptions ADD COLUMN IF NOT EXISTS tenant_id VARCHAR(50); + UPDATE prescriptions SET tenant_id = '${DEFAULT_TENANT}' WHERE tenant_id IS NULL; + + ALTER TABLE batches ADD COLUMN IF NOT EXISTS tenant_id VARCHAR(50); + UPDATE batches SET tenant_id = '${DEFAULT_TENANT}' WHERE tenant_id IS NULL; + + ALTER TABLE patients ADD COLUMN IF NOT EXISTS tenant_id VARCHAR(50); + UPDATE patients SET tenant_id = '${DEFAULT_TENANT}' WHERE tenant_id IS NULL; + + ALTER TABLE suppliers ADD COLUMN IF NOT EXISTS tenant_id VARCHAR(50); + UPDATE suppliers SET tenant_id = '${DEFAULT_TENANT}' WHERE tenant_id IS NULL; + + ALTER TABLE order_drafts ADD COLUMN IF NOT EXISTS tenant_id VARCHAR(50); + UPDATE order_drafts SET tenant_id = '${DEFAULT_TENANT}' WHERE tenant_id IS NULL; + `); + + console.log('Multi-tenant migration completed successfully'); + } catch (err) { + console.error('Error during migration:', err); + } finally { + pool.end(); + } +} + +migrate(); diff --git a/package-lock.json b/package-lock.json index 6644a73..2be1a4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,18 @@ "license": "ISC", "dependencies": { "@aws-sdk/client-s3": "^3.1094.0", + "@aws-sdk/s3-request-presigner": "^3.1101.0", "@google/genai": "^2.13.0", + "@types/bcrypt": "^6.0.0", + "bcrypt": "^6.0.0", "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", "helmet": "^8.3.0", "jsonwebtoken": "^9.0.3", "multer": "^2.2.0", + "openai": "^7.1.0", + "pdfkit": "^0.19.1", "pg": "^8.22.0", "redis": "^6.1.0" }, @@ -26,6 +31,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/multer": "^2.2.0", "@types/node": "^26.1.1", + "@types/pdfkit": "^0.17.6", "@types/pg": "^8.20.0", "ts-node-dev": "^2.0.0", "tsx": "^4.23.1", @@ -71,16 +77,16 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.976.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.976.0.tgz", - "integrity": "sha512-0cjRaEdlVoOrsNb9pP5q1Syyc8pXw5xSj2Np2ryReRTr9FppIIRVSdZK4lbnfmc2Hvgux/xBOUU6baB7z8//uA==", + "version": "3.977.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.4.tgz", + "integrity": "sha512-CEkcQlMOQJCvul60U7wdAOACjtdgFWDsfJI+6wUOGdhGNV2lGbuJpi/R50QLpFG3Tp+sQxa/RmzC3X7KHbhuTA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.974.2", - "@aws-sdk/xml-builder": "^3.972.36", + "@aws-sdk/xml-builder": "^3.972.37", "@aws/lambda-invoke-store": "^0.3.0", - "@smithy/core": "^3.29.4", - "@smithy/signature-v4": "^5.6.5", + "@smithy/core": "^3.31.1", + "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" @@ -273,14 +279,31 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.1101.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1101.0.tgz", + "integrity": "sha512-2raRfiex9qYrdQ60ATeEmOVVC7Do2GEDGSTnoPbgv+xyN3oB48tZeREVw0brI7AlRRvm9fygXTHmyRR0I/LskA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/signature-v4-multi-region": "^3.996.43", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.41.tgz", - "integrity": "sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng==", + "version": "3.996.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.43.tgz", + "integrity": "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.974.2", - "@smithy/signature-v4": "^5.6.5", + "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, @@ -319,9 +342,9 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.36", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.36.tgz", - "integrity": "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.37.tgz", + "integrity": "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.16.1", @@ -847,6 +870,30 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -978,9 +1025,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.29.8", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.29.8.tgz", - "integrity": "sha512-rpCbCV+TimOBi3VLNBMmtTvgfOWcFIEAru3+TFlG87SL2F+te4jOnnNR+cf3uR4eJ5Qf4LnT80fqnBKgPRS6zA==", + "version": "3.31.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.31.1.tgz", + "integrity": "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.16.1", @@ -1033,12 +1080,13 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.6.9", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.9.tgz", - "integrity": "sha512-g5rnEii/mkT0mjVJmlsaOfyNBtHNTecD9Lo4NP8D5HzMUEnZNpz7/FbvBCjNcV4vteHFAxOGiLUYNxPkDZZAPw==", + "version": "5.6.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.12.tgz", + "integrity": "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { - "@smithy/core": "^3.29.8", + "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, @@ -1058,6 +1106,15 @@ "node": ">=18.0.0" } }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", @@ -1086,6 +1143,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -1187,6 +1253,16 @@ "undici-types": "~8.3.0" } }, + "node_modules/@types/pdfkit": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.6.tgz", + "integrity": "sha512-tIwzxk2uWKp0Cq9JIluQXJid77lYhF52EsIOwhsMF4iWLA6YneoBR1xVKYYdAysHuepUB0OX4tdwMiUDdGKmig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/pg": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", @@ -1696,6 +1772,20 @@ ], "license": "MIT" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -1785,6 +1875,24 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, "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", @@ -1871,6 +1979,15 @@ "fsevents": "~2.3.2" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -2001,6 +2118,12 @@ "node": ">= 0.8" } }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, "node_modules/diff": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", @@ -2207,6 +2330,12 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -2264,6 +2393,23 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -2339,20 +2485,6 @@ "node": ">=18" } }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "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", @@ -2442,6 +2574,20 @@ "node": ">=18" } }, + "node_modules/google-auth-library/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "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/google-logging-utils": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", @@ -2643,6 +2789,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/js-md5": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz", + "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==", + "license": "MIT" + }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -2695,6 +2847,25 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2918,6 +3089,15 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.0.tgz", + "integrity": "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -2956,6 +3136,17 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3008,6 +3199,39 @@ "wrappy": "1" } }, + "node_modules/openai": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-7.1.0.tgz", + "integrity": "sha512-7xWJ9iO5z5u1dnIUGwoUmZHkSyrUYXX2cUxo2E/26iKFrSC8IdEak7z94d5UntU7z+S/Cid33hYymwMSab2fZQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": ">=3.972.0 <4", + "@smithy/hash-node": ">=4.3.0 <5", + "@smithy/signature-v4": ">=5.4.0 <6", + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-provider-node": { + "optional": true + }, + "@smithy/hash-node": { + "optional": true + }, + "@smithy/signature-v4": { + "optional": true + }, + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -3021,6 +3245,12 @@ "node": ">=8" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3057,6 +3287,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pdfkit": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.19.1.tgz", + "integrity": "sha512-6Gzk+wDwTs4VSxsR5rCMTnIl5nlmkye1oWB0l2hDB1EX6ZNSIBroKQEv+2+fPPn+stVjyqzmsqRJVDfB9fo5DA==", + "license": "MIT", + "dependencies": { + "@noble/ciphers": "^1.0.0", + "@noble/hashes": "^1.6.0", + "fontkit": "^2.0.4", + "js-md5": "^0.8.3", + "linebreak": "^1.1.0", + "png-js": "^1.1.0" + } + }, "node_modules/pg": { "version": "8.22.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", @@ -3160,6 +3404,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/png-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.1.0.tgz", + "integrity": "sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==", + "dependencies": { + "browserify-zlib": "^0.2.0" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -3344,6 +3596,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -3633,6 +3891,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3861,6 +4125,32 @@ "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "license": "MIT" }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 0824f7f..6893f5d 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,18 @@ "type": "commonjs", "dependencies": { "@aws-sdk/client-s3": "^3.1094.0", + "@aws-sdk/s3-request-presigner": "^3.1101.0", "@google/genai": "^2.13.0", + "@types/bcrypt": "^6.0.0", + "bcrypt": "^6.0.0", "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", "helmet": "^8.3.0", "jsonwebtoken": "^9.0.3", "multer": "^2.2.0", + "openai": "^7.1.0", + "pdfkit": "^0.19.1", "pg": "^8.22.0", "redis": "^6.1.0" }, @@ -29,6 +34,7 @@ "@types/jsonwebtoken": "^9.0.10", "@types/multer": "^2.2.0", "@types/node": "^26.1.1", + "@types/pdfkit": "^0.17.6", "@types/pg": "^8.20.0", "ts-node-dev": "^2.0.0", "tsx": "^4.23.1", diff --git a/src/db/pool.js b/src/db/pool.js new file mode 100644 index 0000000..36c8220 --- /dev/null +++ b/src/db/pool.js @@ -0,0 +1,20 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.pool = void 0; +const pg_1 = require("pg"); +const dotenv_1 = __importDefault(require("dotenv")); +dotenv_1.default.config(); +exports.pool = new pg_1.Pool({ + connectionString: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/ai_pharmacy', +}); +// Test connection +exports.pool.on('connect', () => { + console.log('Connected to the PostgreSQL database.'); +}); +exports.pool.on('error', (err) => { + console.error('Unexpected error on idle client', err); + process.exit(-1); +}); diff --git a/src/index.ts b/src/index.ts index 820375d..4ba1fa6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import auditRouter from './modules/audit/router'; import prescriptionRouter from './modules/prescription/router'; import aiRouter from './modules/ai/router'; import integrationRouter from './modules/integration/router'; +import uploadRouter from './modules/upload/router'; const app = express(); const PORT = process.env.PORT || 8080; @@ -47,6 +48,7 @@ app.use('/v1/audit', auditRouter); app.use('/v1/rx', prescriptionRouter); app.use('/v1/ai', aiRouter); app.use('/v1/integration', integrationRouter); +app.use('/v1/upload', uploadRouter); // Health check app.get('/health', (req, res) => { diff --git a/src/modules/ai/service.ts b/src/modules/ai/service.ts index 2544273..1464b5e 100644 --- a/src/modules/ai/service.ts +++ b/src/modules/ai/service.ts @@ -18,6 +18,7 @@ If the user asks about suppliers or distributors, use get_total_suppliers. If the user asks to open or navigate to a page (e.g. "open prescriptions", "take me to suppliers", "show patients"), use navigate_to with the appropriate target. If the user asks to search for a specific medicine or asks if a medicine is available (e.g., "search privika", "do we have calpol"), use search_medicine. If the user asks to search for a supplier, use search_supplier. +If the user asks to search for a patient, use search_patient. If the user asks to add a new medicine to the system, use add_medicine. If the user asks to add stock for a medicine, use add_stock. If the user asks to add a new supplier, use add_supplier. @@ -41,6 +42,11 @@ If the user asks to add a new patient, use add_patient.`; description: "Search for a wholesale supplier by name.", parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] } }}, + { type: "function", function: { + name: "search_patient", + description: "Search for a patient by name.", + parameters: { type: "object", properties: { query: { type: "string" } }, required: ["query"] } + }}, { type: "function", function: { name: "add_medicine", description: "Add a new medicine to the product catalog.", @@ -137,7 +143,7 @@ If the user asks to add a new patient, use add_patient.`; const { rows } = await pool.query(`SELECT sum(total_amount) as total FROM dispensing_records WHERE DATE(created_at) = CURRENT_DATE AND tenant_id = $1`, [actorId]); toolData = { todays_revenue_rupees: rows[0].total || 0 }; } else if (call.name === 'get_expiring_medicines') { - const { rows } = await pool.query(`SELECT count(*) as count FROM batches WHERE expiry <= CURRENT_DATE + interval '30 days' AND qty_on_hand > 0 AND tenant_id = $1`, [actorId]); + const { rows } = await pool.query(`SELECT count(*) as count FROM batches WHERE expiry >= CURRENT_DATE AND expiry <= CURRENT_DATE + interval '30 days' AND qty_on_hand > 0 AND tenant_id = $1`, [actorId]); toolData = { batches_expiring_in_30_days: rows[0].count }; actionCommand = 'VIEW_EXPIRING_STOCK'; } else if (call.name === 'get_total_patients') { @@ -151,13 +157,14 @@ If the user asks to add a new patient, use add_patient.`; toolData = { todays_prescriptions: rows[0].count }; } else if (call.name === 'search_medicine') { const query = args.query || ''; + const searchStr = query.replace(/\s+/g, ''); const { rows } = await pool.query( `SELECT p.brand_name, COALESCE(SUM(b.qty_on_hand), 0) as total_stock FROM products p LEFT JOIN batches b ON p.id = b.product_id - WHERE p.brand_name ILIKE $1 + WHERE REPLACE(p.brand_name, ' ', '') ILIKE $1 GROUP BY p.brand_name`, - [`%${query}%`] + [`%${searchStr}%`] ); toolData = rows.length > 0 ? { search_results: rows } : { message: `No medicine found matching '${query}'.` }; actionCommand = 'NAVIGATE_INVENTORY'; @@ -166,13 +173,27 @@ If the user asks to add a new patient, use add_patient.`; const { rows } = await pool.query('SELECT name, contact_number FROM suppliers WHERE name ILIKE $1', [`%${query}%`]); toolData = rows.length > 0 ? { search_results: rows } : { message: `No supplier found matching '${query}'.` }; actionCommand = 'NAVIGATE_SUPPLIERS'; + } else if (call.name === 'search_patient') { + const query = args.query || ''; + const searchStr = query.replace(/\s+/g, ''); + let rows = []; + try { + const res = await pool.query('SELECT id, name, phone FROM patients WHERE REPLACE(name, \' \', \'\') ILIKE $1', [`%${searchStr}%`]); + rows = res.rows; + } catch(e) { + const res = await pool.query('SELECT id, full_name, contact_number FROM patients WHERE REPLACE(full_name, \' \', \'\') ILIKE $1', [`%${searchStr}%`]); + rows = res.rows; + } + toolData = rows.length > 0 ? { search_results: rows } : { message: `No patient found matching '${query}'.` }; + actionCommand = 'NAVIGATE_PATIENTS'; } else if (call.name === 'add_medicine') { const id = `prd_${Date.now()}`; await pool.query('INSERT INTO products (id, brand_name, form, mrp, pack_size) VALUES ($1, $2, $3, $4, $5)', [id, args.brand_name, args.form || 'tablet', args.mrp || 0, args.pack_size || 1]); toolData = { success: true, message: `Added ${args.brand_name} successfully.` }; actionCommand = 'NAVIGATE_CATALOGUE'; } else if (call.name === 'add_stock') { - const pRes = await pool.query('SELECT id, brand_name FROM products WHERE brand_name ILIKE $1 LIMIT 1', [`%${args.product_name}%`]); + const searchStr = (args.product_name || '').replace(/\s+/g, ''); + const pRes = await pool.query('SELECT id, brand_name FROM products WHERE REPLACE(brand_name, \' \', \'\') ILIKE $1 LIMIT 1', [`%${searchStr}%`]); if (pRes.rows.length > 0) { const batchNo = args.batch_no || `B${Date.now()}`; const expiry = args.expiry || new Date(Date.now() + 31536000000).toISOString().split('T')[0]; diff --git a/src/modules/audit/router.ts b/src/modules/audit/router.ts index 55db901..b18fae8 100644 --- a/src/modules/audit/router.ts +++ b/src/modules/audit/router.ts @@ -33,7 +33,7 @@ router.get('/ai-calls', requireAuth, async (req, res, next) => { // Also grab the stock movements for full audit visibility const { rows: stockMovements } = await pool.query(` - SELECT sm.id, sm.batch_no, sm.movement_type, sm.qty_delta, sm.reference_id, sm.created_at + SELECT sm.id, sm.batch_no, sm.movement_type, sm.qty_delta, sm.reference_id, sm.created_at, sm.reason_code, sm.notes FROM stock_movements sm JOIN batches b ON sm.batch_no = b.batch_no WHERE b.tenant_id = $1 diff --git a/src/modules/catalogue/service.ts b/src/modules/catalogue/service.ts index fdd662b..2dd2a6f 100644 --- a/src/modules/catalogue/service.ts +++ b/src/modules/catalogue/service.ts @@ -14,6 +14,7 @@ export class CatalogueService { d.is_nti, d.pregnancy_category, p.mrp, + p.pack_size, p.dpco_ceiling, (SELECT COALESCE(SUM(qty_on_hand), 0) FROM batches WHERE product_id = p.id AND expiry > CURRENT_DATE AND tenant_id = $3) as total_stock FROM products p @@ -41,6 +42,7 @@ export class CatalogueService { d.is_nti, d.pregnancy_category, p.mrp, + p.pack_size, p.dpco_ceiling, (SELECT purchase_price FROM batches WHERE product_id = p.id AND tenant_id = $2 ORDER BY expiry DESC LIMIT 1) as purchase_price, (SELECT COALESCE(SUM(qty_on_hand), 0) FROM batches WHERE product_id = p.id AND expiry > CURRENT_DATE AND tenant_id = $2) as total_stock diff --git a/src/modules/dispensing/pdf.ts b/src/modules/dispensing/pdf.ts new file mode 100644 index 0000000..3814923 --- /dev/null +++ b/src/modules/dispensing/pdf.ts @@ -0,0 +1,155 @@ +import PDFDocument from 'pdfkit'; +import { Writable } from 'stream'; + +const BRAND_COLOR = '#4F46E5'; // Indigo-600 +const TEXT_COLOR = '#334155'; // Slate-700 +const LIGHT_TEXT = '#64748B'; // Slate-500 +const BORDER_COLOR = '#E2E8F0'; // Slate-200 + +export function generateInvoicePDF(record: any, stream: Writable) { + const doc = new PDFDocument({ margin: 50, size: 'A4' }); + + doc.pipe(stream); + + // --- Header Background --- + doc + .rect(0, 0, 600, 110) + .fill(BRAND_COLOR); + + // --- Header Text --- + doc + .fillColor('#FFFFFF') + .font('Helvetica-Bold') + .fontSize(28) + .text('INVOICE', 50, 40, { characterSpacing: 2 }) + .fontSize(10) + .font('Helvetica') + .text(`Date: ${new Date(record.created_at).toLocaleDateString()}`, 50, 75) + .text(`Invoice #: ${record.dispensing_id.substring(0, 8).toUpperCase()}`, 50, 90); + + // --- Pharmacy Details (Right Aligned in Header) --- + doc + .font('Helvetica-Bold') + .fontSize(16) + .text('AI Pharmacy Clinic', 300, 40, { align: 'right' }) + .font('Helvetica') + .fontSize(10) + .text('123 Health Avenue, Med City', 300, 60, { align: 'right' }) + .text('Phone: (555) 123-4567', 300, 75, { align: 'right' }) + .text('Email: hello@aipharmacy.com', 300, 90, { align: 'right' }); + + doc.moveDown(4); + + // --- Patient & Dispensing Details --- + const currentY = 150; + + // Left Box + doc.fillColor(TEXT_COLOR).font('Helvetica-Bold').fontSize(12).text('Billed To:', 50, currentY); + doc.font('Helvetica').fontSize(10).fillColor(LIGHT_TEXT) + .text(record.patient_name || 'Walk-in Patient', 50, currentY + 15) + .text(`Payment: ${record.payment_method || 'Cash'}`, 50, currentY + 30); + + // Right Box + doc.fillColor(TEXT_COLOR).font('Helvetica-Bold').fontSize(12).text('Dispensed By:', 350, currentY); + doc.font('Helvetica').fontSize(10).fillColor(LIGHT_TEXT) + .text(record.dispensed_by || 'Pharmacist', 350, currentY + 15); + + doc.moveDown(3); + + // --- Table Header --- + const tableTop = 240; + + doc + .rect(50, tableTop, 500, 25) + .fill(BRAND_COLOR); + + doc + .fillColor('#FFFFFF') + .font('Helvetica-Bold') + .fontSize(10); + + generateTableRow(doc, tableTop + 7, 'Item Description', 'Batch', 'Qty', 'Unit Price', 'Total'); + + // --- Table Rows --- + let position = tableTop + 25; + doc.font('Helvetica').fillColor(TEXT_COLOR).fontSize(10); + + for (let i = 0; i < record.items.length; i++) { + const item = record.items[i]; + const qty = item.qty; + const packSize = parseInt(item.pack_size, 10) || 1; + const unitPrice = parseFloat(item.mrp) / packSize; + const total = qty * unitPrice; + + // Alternating row colors + if (i % 2 === 1) { + doc.rect(50, position, 500, 25).fill('#F8FAFC'); // Slate-50 + } + doc.fillColor(TEXT_COLOR); + + generateTableRow( + doc, + position + 7, + item.brand_name, + item.batch_no || '-', + qty.toString(), + `$${unitPrice.toFixed(2)}`, + `$${total.toFixed(2)}` + ); + + position += 25; + + if (position > 700) { + doc.addPage(); + position = 50; + } + } + + // Draw bottom table border + doc.strokeColor(BORDER_COLOR).lineWidth(1).moveTo(50, position).lineTo(550, position).stroke(); + + // --- Totals Section --- + const subtotal = parseFloat(record.total_amount); + const grandTotal = subtotal; + + position += 20; + + doc.font('Helvetica-Bold').fillColor(TEXT_COLOR).fontSize(12); + doc.text('Total Amount:', 350, position, { width: 100, align: 'right' }); + doc.fillColor(BRAND_COLOR).fontSize(14); + doc.text(`$${grandTotal.toFixed(2)}`, 450, position - 2, { width: 90, align: 'right' }); + + // --- Footer --- + const footerY = 750; + doc.strokeColor(BORDER_COLOR).lineWidth(1).moveTo(50, footerY - 20).lineTo(550, footerY - 20).stroke(); + + doc + .font('Helvetica') + .fontSize(9) + .fillColor(LIGHT_TEXT) + .text( + 'Thank you for your business. For any questions concerning this invoice, please contact us.', + 50, + footerY, + { align: 'center', width: 500 } + ); + + doc.end(); +} + +function generateTableRow( + doc: PDFKit.PDFDocument, + y: number, + item: string, + batch: string, + qty: string, + unitCost: string, + lineTotal: string +) { + doc + .text(item, 60, y, { width: 180 }) + .text(batch, 250, y, { width: 80 }) + .text(qty, 330, y, { width: 50, align: 'center' }) + .text(unitCost, 390, y, { width: 70, align: 'right' }) + .text(lineTotal, 460, y, { width: 80, align: 'right' }); +} diff --git a/src/modules/dispensing/router.ts b/src/modules/dispensing/router.ts index e5a5973..c4d1ea1 100644 --- a/src/modules/dispensing/router.ts +++ b/src/modules/dispensing/router.ts @@ -4,6 +4,7 @@ import { requireHeaders } from '../../middleware/headers'; import { requireIdempotency } from '../../middleware/idempotency'; import { DispensingService } from './service'; import { pool } from '../../db/pool'; +import { generateInvoicePDF } from './pdf'; const router = Router(); const service = new DispensingService(); @@ -28,6 +29,23 @@ router.get('/h1-register', requireAuth, async (req, res, next) => { } }); +// GET /v1/orders/:id/invoice/pdf +router.get('/:id/invoice/pdf', requireAuth, async (req, res, next) => { + try { + const record = await service.getDispensingRecordDetails(req.params.id as string, req.actorId as string); + if (!record) { + return res.status(404).json({ error: 'DISPENSING_RECORD_NOT_FOUND' }); + } + + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('Content-Disposition', `attachment; filename=invoice_${req.params.id}.pdf`); + + generateInvoicePDF(record, res); + } catch (error) { + next(error); + } +}); + // POST /v1/orders router.post('/', requireAuth, requireHeaders, async (req, res, next) => { try { diff --git a/src/modules/dispensing/service.ts b/src/modules/dispensing/service.ts index f03c29b..a98e147 100644 --- a/src/modules/dispensing/service.ts +++ b/src/modules/dispensing/service.ts @@ -306,4 +306,36 @@ export class DispensingService { const { rows } = await pool.query(query, [tenantId]); return rows; } + + async getDispensingRecordDetails(id: string, tenantId: string) { + const query = ` + SELECT + d.id as dispensing_id, + COALESCE(pat.full_name, d.subject_ref) as patient_name, + d.dispensed_by, + d.created_at, + d.prescription_id, + d.total_amount, + d.payment_method, + json_agg( + json_build_object( + 'batch_no', sm.batch_no, + 'qty', ABS(sm.qty_delta), + 'brand_name', p.brand_name, + 'strength', p.strength, + 'mrp', p.mrp, + 'pack_size', p.pack_size + ) + ) as items + FROM dispensing_records d + LEFT JOIN patients pat ON pat.hospital_patient_id = d.subject_ref OR pat.id::text = d.subject_ref + JOIN stock_movements sm ON sm.reference_id = d.id + JOIN batches b ON b.batch_no = sm.batch_no + JOIN products p ON p.id = b.product_id + WHERE d.id = $1 AND d.tenant_id = $2 + GROUP BY d.id, pat.full_name + `; + const { rows } = await pool.query(query, [id, tenantId]); + return rows[0] || null; + } } diff --git a/src/modules/identity/router.ts b/src/modules/identity/router.ts index fc17005..40dd628 100644 --- a/src/modules/identity/router.ts +++ b/src/modules/identity/router.ts @@ -1,9 +1,13 @@ import { Router } from 'express'; import jwt from 'jsonwebtoken'; +import multer from 'multer'; import { IdentityService } from './service'; import { requireAuth } from '../../middleware/auth'; +import { S3Service } from '../upload/service'; const router = Router(); +const upload = multer({ storage: multer.memoryStorage() }); +const s3Service = new S3Service(); const identityService = new IdentityService(); const JWT_SECRET = process.env.JWT_SECRET || 'super_secret_pharmacy_key_2026'; @@ -19,10 +23,29 @@ router.get('/me', requireAuth, async (req, res) => { }); // PUT /v1/auth/me (Update Profile) -router.put('/me', requireAuth, async (req, res) => { +router.put('/me', requireAuth, upload.single('profileImage'), async (req, res) => { try { const userId = req.actorId as string; - const profile = await identityService.updateUserProfile(userId, req.body); + const fieldsToUpdate = { ...req.body }; + + // Handle profile image upload + if (req.file) { + const file = req.file; + let extension = ''; + const parts = file.originalname.split('.'); + if (parts.length > 1) { + extension = parts[parts.length - 1]; + } else { + extension = file.mimetype.split('/')[1] || 'bin'; + } + + const s3Res = await s3Service.diskStorage(file.buffer, file.mimetype, extension, 'profiles/'); + if (s3Res.success && s3Res.url) { + fieldsToUpdate.profile_image_url = s3Res.url; + } + } + + const profile = await identityService.updateUserProfile(userId, fieldsToUpdate); res.status(200).json({ success: true, profile }); } catch (error: any) { res.status(400).json({ error: error.message }); diff --git a/src/modules/identity/service.ts b/src/modules/identity/service.ts index 661a198..64fe953 100644 --- a/src/modules/identity/service.ts +++ b/src/modules/identity/service.ts @@ -80,7 +80,7 @@ export class IdentityService { async getUserProfile(userId: string) { const result = await pool.query( - 'SELECT id, username, role, pharmacy_name, dl_number, gstin, pharmacist_reg_number, address FROM users WHERE id = $1', + 'SELECT id, username, role, pharmacy_name, dl_number, gstin, pharmacist_reg_number, address, profile_image_url FROM users WHERE id = $1', [userId] ); if (result.rows.length === 0) { @@ -95,6 +95,7 @@ export class IdentityService { gstin?: string; pharmacist_reg_number?: string; address?: string; + profile_image_url?: string; }) { // Only update allowed fields await pool.query( @@ -103,9 +104,10 @@ export class IdentityService { dl_number = COALESCE($2, dl_number), gstin = COALESCE($3, gstin), pharmacist_reg_number = COALESCE($4, pharmacist_reg_number), - address = COALESCE($5, address) - WHERE id = $6`, - [data.pharmacy_name, data.dl_number, data.gstin, data.pharmacist_reg_number, data.address, userId] + address = COALESCE($5, address), + profile_image_url = COALESCE($6, profile_image_url) + WHERE id = $7`, + [data.pharmacy_name, data.dl_number, data.gstin, data.pharmacist_reg_number, data.address, data.profile_image_url, userId] ); return this.getUserProfile(userId); diff --git a/src/modules/integration/router.ts b/src/modules/integration/router.ts index 1e518d4..ae9f487 100644 --- a/src/modules/integration/router.ts +++ b/src/modules/integration/router.ts @@ -20,7 +20,7 @@ router.use((req, res, next) => { return res.status(401).json({ error: 'Unauthorized: Missing API credentials' }); } - if (client !== 'hospital-api' && client !== 'telegaruda') { + if (client !== 'hospital-api' && client !== 'pharmacy-v1-staging') { return res.status(401).json({ error: 'Unauthorized: Invalid client ID' }); } diff --git a/src/modules/inventory/service.ts b/src/modules/inventory/service.ts index ace89b1..d29e3e0 100644 --- a/src/modules/inventory/service.ts +++ b/src/modules/inventory/service.ts @@ -19,9 +19,9 @@ export class InventoryService { `, [batch_no, qty_delta, reason_code, notes, actorId]); await client.query(` - INSERT INTO stock_movements (batch_no, movement_type, qty_delta, reference_id) - VALUES ($1, 'adjustment', $2, 'adj_' || extract(epoch from now())) - `, [batch_no, qty_delta]); + INSERT INTO stock_movements (batch_no, movement_type, qty_delta, reference_id, reason_code, notes) + VALUES ($1, 'adjustment', $2, 'adj_' || extract(epoch from now()), $3, $4) + `, [batch_no, qty_delta, reason_code, notes]); await client.query('COMMIT'); return { status: 'success', batch_no, adjusted: qty_delta }; diff --git a/src/modules/upload/controller.ts b/src/modules/upload/controller.ts new file mode 100644 index 0000000..bf59742 --- /dev/null +++ b/src/modules/upload/controller.ts @@ -0,0 +1,60 @@ +import { Request, Response } from 'express'; +import { S3Service } from './service'; + +const s3Service = new S3Service(); + +export class UploadController { + + static async handleUpload(req: Request, res: Response) { + try { + if (!req.file) { + return res.status(400).json({ success: false, error: 'No file provided' }); + } + + const file = req.file; + + // Determine extension from originalname or mimetype + let extension = ''; + const parts = file.originalname.split('.'); + if (parts.length > 1) { + extension = parts[parts.length - 1]; + } else { + // Fallback to mime type split + extension = file.mimetype.split('/')[1] || 'bin'; + } + + // Optional prefix for organizing files (e.g. 'general/', 'prescriptions/') + const prefix = req.body.prefix || 'uploads/'; + + const result = await s3Service.diskStorage(file.buffer, file.mimetype, extension, prefix); + + if (!result.success) { + return res.status(500).json(result); + } + + res.status(200).json(result); + } catch (error: any) { + console.error("[UploadController] Error:", error); + res.status(500).json({ success: false, error: error.message }); + } + } + + static async getTemporaryUrl(req: Request, res: Response) { + try { + const { path, expiresIn } = req.query; + + if (!path) { + return res.status(400).json({ success: false, error: 'Missing path parameter' }); + } + + const time = expiresIn ? parseInt(expiresIn as string, 10) : 3600; + + const url = await s3Service.getAwsTemporaryUrl(path as string, time); + + res.status(200).json({ success: true, url }); + } catch (error: any) { + console.error("[UploadController] Error:", error); + res.status(500).json({ success: false, error: error.message }); + } + } +} diff --git a/src/modules/upload/router.ts b/src/modules/upload/router.ts new file mode 100644 index 0000000..a7a838d --- /dev/null +++ b/src/modules/upload/router.ts @@ -0,0 +1,18 @@ +import { Router } from 'express'; +import multer from 'multer'; +import { UploadController } from './controller'; +import { requireAuth } from '../../middleware/auth'; + +const router = Router(); + +// Setup Multer for memory storage +const storage = multer.memoryStorage(); +const upload = multer({ storage: storage }); + +// POST /v1/upload +router.post('/', requireAuth, upload.single('file'), UploadController.handleUpload); + +// GET /v1/upload/presigned +router.get('/presigned', requireAuth, UploadController.getTemporaryUrl); + +export default router; diff --git a/src/modules/upload/service.ts b/src/modules/upload/service.ts new file mode 100644 index 0000000..abc48fe --- /dev/null +++ b/src/modules/upload/service.ts @@ -0,0 +1,129 @@ +import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import crypto from 'crypto'; + +let s3Client: S3Client | null = null; +if (process.env.S3_KEY && process.env.S3_SECRET) { + s3Client = new S3Client({ + region: process.env.S3_REGION || 'us-east-1', + credentials: { + accessKeyId: process.env.S3_KEY, + secretAccessKey: process.env.S3_SECRET + } + }); +} + +export class S3Service { + + /** + * Uploads a file buffer to S3 (equivalent to Api_Mentalhealth's diskStorage) + * @param buffer The file buffer + * @param mimetype The file mimetype + * @param extension The file extension (e.g. 'png', 'pdf') + * @param pathPrefix Optional path prefix (e.g. 'uploads/') + * @returns Object containing filename, fullPath, and success status + */ + async diskStorage(buffer: Buffer, mimetype: string, extension: string, pathPrefix: string = '') { + if (!s3Client) { + console.warn("S3 Credentials not found, mocking S3 upload."); + const filename = `${crypto.createHash('md5').update(Date.now().toString() + Math.random().toString()).digest('hex')}.${extension}`; + const fullPath = `${pathPrefix}${filename}`; + return { success: true, filename, fullPath, url: `https://mock-s3-bucket.s3.amazonaws.com/${fullPath}` }; + } + + try { + // Generate unique MD5 filename like Api_Mentalhealth does + const filename = `${crypto.createHash('md5').update(Date.now().toString() + Math.random().toString()).digest('hex')}.${extension}`; + const fullPath = `${pathPrefix}${filename}`; + const bucket = process.env.S3_BUCKET || 'default-bucket'; + const region = process.env.S3_REGION || 'us-east-1'; + + const command = new PutObjectCommand({ + Bucket: bucket, + Key: fullPath, + Body: buffer, + ContentType: mimetype, + // Set object to be public readable + ACL: 'public-read', + }); + + await s3Client.send(command); + + // Use the provided public base path or construct the correct regional URL + const basePath = process.env.S3_PUBLIC_BASE_PATH + ? (process.env.S3_PUBLIC_BASE_PATH.endsWith('/') ? process.env.S3_PUBLIC_BASE_PATH : process.env.S3_PUBLIC_BASE_PATH + '/') + : `https://${bucket}.s3.${region}.amazonaws.com/`; + + return { + success: true, + filename, + fullPath, + url: `${basePath}${fullPath}` + }; + } catch (error: any) { + console.error("[S3Service] Error uploading file to S3:", error); + return { success: false, error: error.message }; + } + } + + /** + * Generates an AWS temporary signed URL (equivalent to Api_Mentalhealth's getAwsTemporaryUrl) + * @param path The S3 object key + * @param expiresIn Expiration time in seconds + * @returns The presigned URL + */ + async getAwsTemporaryUrl(path: string, expiresIn: number = 3600) { + if (!s3Client) { + return `https://mock-s3-bucket.s3.amazonaws.com/${path}?mock_token=123`; + } + + try { + const bucket = process.env.S3_BUCKET || 'default-bucket'; + const command = new GetObjectCommand({ + Bucket: bucket, + Key: path, + }); + + const signedUrl = await getSignedUrl(s3Client, command, { expiresIn }); + return signedUrl; + } catch (error: any) { + console.error("[S3Service] Error generating presigned URL:", error); + throw error; + } + } + + /** + * Fetches data from S3 (equivalent to Api_Mentalhealth's getDataFromDisk) + * @param path The S3 object key + * @returns The file content as a Buffer + */ + async getDataFromDisk(path: string): Promise { + if (!s3Client) { + return false; + } + + try { + const bucket = process.env.S3_BUCKET || 'default-bucket'; + const command = new GetObjectCommand({ + Bucket: bucket, + Key: path, + }); + + const response = await s3Client.send(command); + + if (response.Body) { + // stream to buffer + const chunks: any[] = []; + for await (let chunk of response.Body as any) { + chunks.push(chunk); + } + return Buffer.concat(chunks); + } + + return false; + } catch (error: any) { + console.error("[S3Service] Error fetching data from disk:", error); + return false; + } + } +} diff --git a/test_billing.ts b/test_billing.ts new file mode 100644 index 0000000..edd711c --- /dev/null +++ b/test_billing.ts @@ -0,0 +1,53 @@ +import { pool } from './src/db/pool'; +import { DispensingService } from './src/modules/dispensing/service'; + +async function runTest() { + const dispensingService = new DispensingService(); + const tenantId = 't1'; + + // 1. Create a fake product with MRP = 20, pack_size = 10 + const productId = 'prd_test_' + Date.now(); + await pool.query(` + INSERT INTO products (id, brand_name, mrp, pack_size, tenant_id) + VALUES ($1, 'Test Medicine Strip', 20.00, 10, $2) + `, [productId, tenantId]); + + console.log(`Created product: ${productId} with MRP 20 and pack_size 10`); + + // 2. Add some stock for it so it doesn't fail expiry/stock checks + const batchNo = 'B_TEST_' + Date.now(); + const nextYear = new Date(Date.now() + 365*24*60*60*1000).toISOString(); + await pool.query(` + INSERT INTO batches (batch_no, product_id, expiry, qty_on_hand, tenant_id) + VALUES ($1, $2, $3, 100, $4) + `, [batchNo, productId, nextYear, tenantId]); + console.log(`Added batch ${batchNo} with 100 qty`); + + // 3. Test Dispensing Flow (Dispense 10 tablets) + const payload = { + subject_ref: 'Patient_123', + items: [ + { product_id: productId, qty: 10 } + ] + }; + + try { + console.log(`\nAttempting to dispense 10 tablets...`); + const result = await dispensingService.executeDispensing(payload, 'user1', tenantId); + console.log(`\nDispensing Successful!`); + console.log(`Transaction ID: ${result.dispensing_id}`); + console.log(`Calculated Total Amount: ₹${result.total_amount}`); + + if (parseFloat(result.total_amount) === 20) { + console.log('✅ TEST PASSED: 10 tablets at (20 MRP / 10 pack_size) correctly calculated as 20 rupees!'); + } else { + console.log('❌ TEST FAILED: Total amount is not 20 rupees.'); + } + } catch (err) { + console.error('Dispensing Error:', err); + } + + process.exit(0); +} + +runTest(); diff --git a/test_hist.js b/test_hist.js new file mode 100644 index 0000000..b33a235 --- /dev/null +++ b/test_hist.js @@ -0,0 +1,30 @@ +const { Client } = require('pg'); +const client = new Client('postgres://postgres:csh%402018@localhost:5432/ai_pharmacy'); + +client.connect().then(async () => { + const query = ` + SELECT + d.id as dispensing_id, + COALESCE(pat.full_name, d.subject_ref) as patient_name, + d.total_amount, + json_agg( + json_build_object( + 'batch_no', sm.batch_no, + 'qty', ABS(sm.qty_delta), + 'brand_name', p.brand_name + ) + ) as items + FROM dispensing_records d + LEFT JOIN patients pat ON pat.hospital_patient_id = d.subject_ref OR pat.id::text = d.subject_ref + JOIN stock_movements sm ON sm.reference_id = d.id + JOIN batches b ON b.batch_no = sm.batch_no + JOIN products p ON p.id = b.product_id + WHERE d.tenant_id = 'usr_1785482612908' + GROUP BY d.id, pat.full_name + ORDER BY d.created_at DESC + LIMIT 5 + `; + const res = await client.query(query); + console.log(JSON.stringify(res.rows, null, 2)); + process.exit(0); +}).catch(console.error); diff --git a/update_batch.js b/update_batch.js new file mode 100644 index 0000000..2d02043 --- /dev/null +++ b/update_batch.js @@ -0,0 +1,7 @@ +const {Client} = require('pg'); +const client = new Client('postgres://postgres:csh%402018@localhost:5432/ai_pharmacy'); +client.connect().then(async () => { + await client.query("UPDATE batches SET qty_on_hand = 10"); + console.log('Updated all batches to 10 for testing'); + process.exit(0); +}); diff --git a/update_old_adjs.js b/update_old_adjs.js new file mode 100644 index 0000000..9a15759 --- /dev/null +++ b/update_old_adjs.js @@ -0,0 +1,14 @@ +const { Pool } = require('pg'); +const pool = new Pool({ connectionString: 'postgres://postgres:csh%402018@localhost:5432/ai_pharmacy' }); + +async function run() { + try { + await pool.query("UPDATE stock_movements SET reason_code = 'system_recovery', notes = 'Recovered past adjustment' WHERE movement_type = 'adjustment' AND notes IS NULL"); + console.log('Successfully updated old adjustments!'); + } catch(err) { + console.error(err); + } finally { + pool.end(); + } +} +run();