feat: implement core dispensing, identity, and integration modules with authentication middleware
This commit is contained in:
+9
-3
@@ -1,18 +1,24 @@
|
||||
const { Pool } = require('pg');
|
||||
require('dotenv').config();
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: 'postgres://postgres:csh%402018@localhost:5432/ai_pharmacy'
|
||||
connectionString: process.env.DATABASE_URL || 'postgres://postgres:csh%402018@localhost:5433/ai_pharmacy'
|
||||
});
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
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.');
|
||||
await pool.query('ALTER TABLE products ADD COLUMN IF NOT EXISTS hsn_code VARCHAR(20)');
|
||||
await pool.query('ALTER TABLE products ADD COLUMN IF NOT EXISTS tenant_id VARCHAR(50)');
|
||||
await pool.query('ALTER TABLE drugs ADD COLUMN IF NOT EXISTS tenant_id VARCHAR(50)');
|
||||
console.log('Successfully added missing database columns.');
|
||||
} catch (err) {
|
||||
console.error('Error:', err);
|
||||
console.error('Error altering database:', err);
|
||||
} finally {
|
||||
pool.end();
|
||||
}
|
||||
}
|
||||
run();
|
||||
|
||||
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
const { pool } = require('./src/db/pool');
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const rx = await pool.query(
|
||||
'SELECT id, tenant_id, status, created_at, extracted_data FROM prescriptions ORDER BY created_at DESC'
|
||||
);
|
||||
console.log('Prescriptions:', JSON.stringify(rx.rows, null, 2));
|
||||
|
||||
const users = await pool.query(
|
||||
'SELECT id, username, role, pharmacy_name, tenant_id FROM users'
|
||||
);
|
||||
console.log('Users:', JSON.stringify(users.rows, null, 2));
|
||||
} catch (err) {
|
||||
console.error('Error executing query:', err);
|
||||
} finally {
|
||||
process.exit();
|
||||
}
|
||||
})();
|
||||
+110
-61
@@ -1,9 +1,11 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const API_KEY = "DUMUqJrNeyuaPDSZSsMf91qjgwc5K3N7";
|
||||
|
||||
const collection = {
|
||||
info: {
|
||||
name: "Pharmacy Core API",
|
||||
description: "API collection for Pharmacy Core API",
|
||||
name: "AI Pharmacy Core API",
|
||||
description: "Complete Postman testing collection for AI Pharmacy Core API with pre-configured headers, authentication, and sample payloads.",
|
||||
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
variable: [
|
||||
@@ -16,6 +18,11 @@ const collection = {
|
||||
key: "token",
|
||||
value: "",
|
||||
type: "string"
|
||||
},
|
||||
{
|
||||
key: "apiKey",
|
||||
value: API_KEY,
|
||||
type: "string"
|
||||
}
|
||||
],
|
||||
auth: {
|
||||
@@ -31,15 +38,88 @@ const collection = {
|
||||
item: []
|
||||
};
|
||||
|
||||
const sampleBodies = {
|
||||
"/v1/auth/register": {
|
||||
username: "pharmacist_jayam",
|
||||
password: "Pharmacy@2026",
|
||||
hospital_id: "hosp_001",
|
||||
pharmacy_name: "Jayam Medicos & Pharmacy",
|
||||
dl_number: "DL-TN-123456",
|
||||
gstin: "33AAAAA0000A1Z5",
|
||||
pharmacist_reg_number: "REG-TN-987654",
|
||||
address: "123 Main Road, Anna Nagar, Chennai, Tamil Nadu - 600040"
|
||||
},
|
||||
"/v1/auth/login": {
|
||||
username: "pharmacist_jayam",
|
||||
password: "Pharmacy@2026"
|
||||
},
|
||||
"/v1/auth/token": {
|
||||
grant_type: "client_credentials",
|
||||
client_id: "pharmacy-v1-staging",
|
||||
client_secret: "my_secret_key"
|
||||
},
|
||||
"/v1/drugs": {
|
||||
id: "drg_101",
|
||||
generic_name: "Amoxicillin and Potassium Clavulanate",
|
||||
atc_code: "J01CR02",
|
||||
schedule: "H1",
|
||||
is_nti: false,
|
||||
pregnancy_category: "B"
|
||||
},
|
||||
"/v1/inventory/grn": {
|
||||
supplier_id: "sup_001",
|
||||
product_id: "prd_4410",
|
||||
batch_no: "BATCH-2026-01",
|
||||
expiry: "2027-12-31",
|
||||
qty_on_hand: 500,
|
||||
purchase_price: 45.00
|
||||
},
|
||||
"/v1/orders": {
|
||||
subject_ref: "patient_001",
|
||||
prescription_id: "rx_001",
|
||||
payment_method: "cash",
|
||||
items: [
|
||||
{
|
||||
batch_no: "B4421",
|
||||
qty: 10,
|
||||
unit_price: 7.85
|
||||
}
|
||||
]
|
||||
},
|
||||
"/v1/integration/hospital/patient-sync": {
|
||||
hospital_patient_id: "PAT-2026-8891",
|
||||
full_name: "Ramesh Kumar",
|
||||
contact_number: "9876543210",
|
||||
email: "ramesh@example.com",
|
||||
allergies: "Penicillin"
|
||||
}
|
||||
};
|
||||
|
||||
function createFolder(name, endpoints, prefix) {
|
||||
const items = endpoints.map(ep => {
|
||||
const method = ep.method.toUpperCase();
|
||||
const fullPath = (prefix + ep.path).replace(/\/$/, "");
|
||||
const urlParts = fullPath.split('/').filter(p => p !== '');
|
||||
|
||||
const headers = [
|
||||
{
|
||||
key: "x-api-key",
|
||||
value: "{{apiKey}}",
|
||||
type: "text"
|
||||
}
|
||||
];
|
||||
|
||||
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
||||
headers.push({
|
||||
key: "Content-Type",
|
||||
value: "application/json",
|
||||
type: "text"
|
||||
});
|
||||
}
|
||||
|
||||
const request = {
|
||||
method: method,
|
||||
header: [],
|
||||
header: headers,
|
||||
url: {
|
||||
raw: `{{baseUrl}}${fullPath}`,
|
||||
host: ["{{baseUrl}}"],
|
||||
@@ -48,9 +128,10 @@ function createFolder(name, endpoints, prefix) {
|
||||
};
|
||||
|
||||
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
||||
const sample = sampleBodies[fullPath] || sampleBodies[ep.path] || {};
|
||||
request.body = {
|
||||
mode: "raw",
|
||||
raw: "{}",
|
||||
raw: JSON.stringify(sample, null, 2),
|
||||
options: {
|
||||
raw: {
|
||||
language: "json"
|
||||
@@ -73,96 +154,65 @@ function createFolder(name, endpoints, prefix) {
|
||||
|
||||
const modules = [
|
||||
{
|
||||
name: "Auth / Identity",
|
||||
name: "1. Auth & Identity",
|
||||
prefix: "/v1/auth",
|
||||
endpoints: [
|
||||
{ method: "GET", path: "/me", name: "Get Current User" },
|
||||
{ method: "PUT", path: "/me", name: "Update Current User" },
|
||||
{ method: "POST", path: "/register", name: "Register" },
|
||||
{ method: "POST", path: "/login", name: "Login" },
|
||||
{ method: "POST", path: "/logout", name: "Logout" },
|
||||
{ method: "POST", path: "/token", name: "Refresh Token" }
|
||||
{ method: "POST", path: "/register", name: "Register Pharmacist" },
|
||||
{ method: "POST", path: "/login", name: "Pharmacist Login" },
|
||||
{ method: "POST", path: "/token", name: "Hospital M2M Auth Token" },
|
||||
{ method: "GET", path: "/me", name: "Get Current Profile" },
|
||||
{ method: "PUT", path: "/me", name: "Update Profile" },
|
||||
{ method: "POST", path: "/logout", name: "Logout" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Catalogue",
|
||||
name: "2. Catalogue",
|
||||
prefix: "/v1",
|
||||
endpoints: [
|
||||
{ method: "GET", path: "/drugs", name: "Get Drugs" },
|
||||
{ method: "GET", path: "/drugs", name: "Get All Drugs" },
|
||||
{ method: "POST", path: "/drugs", name: "Create Drug" },
|
||||
{ method: "GET", path: "/drugs/search", name: "Search Drugs" },
|
||||
{ method: "GET", path: "/products/:product_id/batches", name: "Get Product Batches" }
|
||||
{ method: "GET", path: "/products/prd_4410/batches", name: "Get Product Batches" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Inventory",
|
||||
name: "3. Inventory",
|
||||
prefix: "/v1/inventory",
|
||||
endpoints: [
|
||||
{ method: "POST", path: "/grn", name: "Create GRN" },
|
||||
{ method: "POST", path: "/grn", name: "Create Goods Received Note (GRN)" },
|
||||
{ method: "GET", path: "/expiring", name: "Get Expiring Stock" },
|
||||
{ method: "GET", path: "/low-stock", name: "Get Low Stock" },
|
||||
{ method: "POST", path: "/adjustments", name: "Stock Adjustment" },
|
||||
{ method: "GET", path: "/recall/:batch_no", name: "Recall Batch" }
|
||||
{ method: "GET", path: "/recall/B4421", name: "Recall Batch" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Dispensing (Orders)",
|
||||
name: "4. Dispensing & Orders",
|
||||
prefix: "/v1/orders",
|
||||
endpoints: [
|
||||
{ method: "POST", path: "/", name: "Create Dispensing Order" },
|
||||
{ method: "GET", path: "/history", name: "Get Order History" },
|
||||
{ method: "GET", path: "/h1-register", name: "Get H1 Register" },
|
||||
{ method: "GET", path: "/:id/invoice/pdf", name: "Get Invoice PDF" },
|
||||
{ method: "POST", path: "/", name: "Create Order" },
|
||||
{ method: "PATCH", path: "/:id/items", name: "Update Order Items" },
|
||||
{ method: "POST", path: "/:id/precheck", name: "Precheck Order" },
|
||||
{ method: "POST", path: "/:id/dispense", name: "Dispense Order" }
|
||||
{ method: "GET", path: "/ord_001/invoice/pdf", name: "Get Invoice PDF" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Audit",
|
||||
prefix: "/v1/audit",
|
||||
endpoints: [
|
||||
{ method: "GET", path: "/ai-calls", name: "Get AI Calls" },
|
||||
{ method: "GET", path: "/overrides", name: "Get Overrides" },
|
||||
{ method: "GET", path: "/h1-register", name: "Get H1 Register Audit" },
|
||||
{ method: "GET", path: "/sales-register", name: "Get Sales Register" },
|
||||
{ method: "GET", path: "/dashboard-metrics", name: "Get Dashboard Metrics" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Prescription",
|
||||
prefix: "/v1/rx",
|
||||
endpoints: [
|
||||
{ method: "POST", path: "/", name: "Upload Prescription" },
|
||||
{ method: "GET", path: "/", name: "Get Prescriptions" },
|
||||
{ method: "GET", path: "/:id", name: "Get Prescription by ID" },
|
||||
{ method: "POST", path: "/:id/confirm", name: "Confirm Prescription" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "AI",
|
||||
prefix: "/v1/ai",
|
||||
endpoints: [
|
||||
{ method: "POST", path: "/voice", name: "Voice AI" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Integration",
|
||||
name: "5. Integration (Hospital & Telegaruda)",
|
||||
prefix: "/v1/integration",
|
||||
endpoints: [
|
||||
{ method: "POST", path: "/hospital/patient-sync", name: "Sync Hospital Patient" },
|
||||
{ method: "GET", path: "/hospital/patients", name: "Get Hospital Patients" },
|
||||
{ method: "POST", path: "/telegaruda/prescription", name: "Receive Telegaruda Rx" },
|
||||
{ method: "GET", path: "/telegaruda/patient/:patientId", name: "Fetch Telegaruda Patient" },
|
||||
{ method: "GET", path: "/hospital/prescriptions", name: "Get Hospital Prescriptions" }
|
||||
{ method: "GET", path: "/hospital/prescriptions", name: "Get Hospital Prescriptions" },
|
||||
{ method: "POST", path: "/telegaruda/prescription", name: "Receive Telegaruda Rx" }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Upload",
|
||||
prefix: "/v1/upload",
|
||||
name: "6. Audit & Analytics",
|
||||
prefix: "/v1/audit",
|
||||
endpoints: [
|
||||
{ method: "POST", path: "/", name: "Upload File" },
|
||||
{ method: "GET", path: "/presigned", name: "Get Presigned URL" }
|
||||
{ method: "GET", path: "/dashboard-metrics", name: "Get Dashboard Metrics" },
|
||||
{ method: "GET", path: "/ai-calls", name: "Get AI Calls Log" },
|
||||
{ method: "GET", path: "/overrides", name: "Get Safety Overrides Log" }
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -171,6 +221,5 @@ modules.forEach(mod => {
|
||||
collection.item.push(createFolder(mod.name, mod.endpoints, mod.prefix));
|
||||
});
|
||||
|
||||
// Write to file
|
||||
fs.writeFileSync('postman_collection.json', JSON.stringify(collection, null, 2));
|
||||
console.log('Collection generated at postman_collection.json');
|
||||
console.log('Collection successfully generated and saved to postman_collection.json');
|
||||
|
||||
Generated
-5
@@ -968,7 +968,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-6.1.0.tgz",
|
||||
"integrity": "sha512-7u1LefkezJF0HESlhO7ZFLEPfyY+NejP3SGv+Z4pGaT3oM5GVVLa0u3f4rDLUrcw+SRo8IlX9Y8JAONeDdg1Ag==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cluster-key-slot": "1.1.2"
|
||||
},
|
||||
@@ -1084,7 +1083,6 @@
|
||||
"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.31.1",
|
||||
"@smithy/types": "^4.16.1",
|
||||
@@ -1248,7 +1246,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
|
||||
"integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~8.3.0"
|
||||
}
|
||||
@@ -3306,7 +3303,6 @@
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
|
||||
"integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.14.0",
|
||||
"pg-pool": "^3.14.0",
|
||||
@@ -4089,7 +4085,6 @@
|
||||
"integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc"
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"check-db": "node check_db.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
|
||||
+402
-501
File diff suppressed because it is too large
Load Diff
+30
-2
@@ -14,8 +14,23 @@ async function initDB() {
|
||||
username VARCHAR(100) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) DEFAULT 'pharmacist',
|
||||
pharmacy_name VARCHAR(255),
|
||||
dl_number VARCHAR(100),
|
||||
gstin VARCHAR(50),
|
||||
pharmacist_reg_number VARCHAR(100),
|
||||
address TEXT,
|
||||
profile_image_url TEXT,
|
||||
tenant_id VARCHAR(50) DEFAULT 'act_admin',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS pharmacy_name VARCHAR(255);
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS dl_number VARCHAR(100);
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS gstin VARCHAR(50);
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS pharmacist_reg_number VARCHAR(100);
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS address TEXT;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS profile_image_url TEXT;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS tenant_id VARCHAR(50) DEFAULT 'act_admin';
|
||||
`);
|
||||
|
||||
// 1. Catalogue Module
|
||||
@@ -26,8 +41,11 @@ async function initDB() {
|
||||
atc_code VARCHAR(50),
|
||||
schedule VARCHAR(20),
|
||||
is_nti BOOLEAN DEFAULT false,
|
||||
pregnancy_category VARCHAR(10)
|
||||
pregnancy_category VARCHAR(10),
|
||||
tenant_id VARCHAR(50)
|
||||
);
|
||||
|
||||
ALTER TABLE drugs ADD COLUMN IF NOT EXISTS tenant_id VARCHAR(50);
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
@@ -40,8 +58,13 @@ async function initDB() {
|
||||
form VARCHAR(50),
|
||||
mrp DECIMAL(10, 2),
|
||||
pack_size INTEGER DEFAULT 1,
|
||||
dpco_ceiling DECIMAL(10, 2)
|
||||
dpco_ceiling DECIMAL(10, 2),
|
||||
hsn_code VARCHAR(20),
|
||||
tenant_id VARCHAR(50)
|
||||
);
|
||||
|
||||
ALTER TABLE products ADD COLUMN IF NOT EXISTS hsn_code VARCHAR(20);
|
||||
ALTER TABLE products ADD COLUMN IF NOT EXISTS tenant_id VARCHAR(50);
|
||||
`);
|
||||
|
||||
// 2. Inventory Module
|
||||
@@ -80,8 +103,13 @@ async function initDB() {
|
||||
movement_type VARCHAR(50) NOT NULL, -- 'receipt', 'dispense', 'adjustment'
|
||||
qty_delta INTEGER NOT NULL,
|
||||
reference_id VARCHAR(100),
|
||||
reason_code VARCHAR(100),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
ALTER TABLE stock_movements ADD COLUMN IF NOT EXISTS reason_code VARCHAR(100);
|
||||
ALTER TABLE stock_movements ADD COLUMN IF NOT EXISTS notes TEXT;
|
||||
`);
|
||||
|
||||
// 3. Dispensing & Audit
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config({ override: true }); // API_KEY = DUMUqJrNeyuaPDSZSsMf91qjgwc5K3N7
|
||||
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
@@ -52,6 +55,7 @@ app.use('/v1/audit', auditRouter);
|
||||
app.use('/v1/rx', prescriptionRouter);
|
||||
app.use('/v1/ai', aiRouter);
|
||||
app.use('/v1/integration', integrationRouter);
|
||||
app.use('/v1/patients', integrationRouter);
|
||||
app.use('/v1/upload', uploadRouter);
|
||||
|
||||
// Health check
|
||||
|
||||
+11
-3
@@ -14,11 +14,18 @@ declare global {
|
||||
}
|
||||
|
||||
export const requireApiKey: RequestHandler = (req: Request, res: Response, next: NextFunction): void => {
|
||||
const apiKey = req.headers['x-api-key'];
|
||||
const expectedApiKey = process.env.API_KEY;
|
||||
// Allow /v1/integration, /v1/patients, and /health routes to validate via their own auth or be publicly checked
|
||||
if (req.originalUrl.startsWith('/v1/integration') || req.originalUrl.startsWith('/v1/patients') || req.originalUrl === '/health' || req.originalUrl.startsWith('/health')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const rawApiKey = req.headers['x-api-key'];
|
||||
const apiKey = Array.isArray(rawApiKey) ? rawApiKey[0] : rawApiKey;
|
||||
const expectedApiKey = process.env.API_KEY ? process.env.API_KEY.trim() : undefined;
|
||||
|
||||
// If API_KEY is set in .env, enforce it
|
||||
if (expectedApiKey && apiKey !== expectedApiKey) {
|
||||
if (expectedApiKey && (!apiKey || apiKey.trim() !== expectedApiKey)) {
|
||||
console.warn(`[AUTH WARN] Unauthorized API Key attempt on ${req.originalUrl}. Received: "${apiKey}", Expected: "${expectedApiKey}"`);
|
||||
res.status(401).json({
|
||||
error: 'UNAUTHORIZED',
|
||||
message: 'Missing or invalid API Key.',
|
||||
@@ -57,6 +64,7 @@ export const requireAuth: RequestHandler = (req: Request, res: Response, next: N
|
||||
// User JWT Token
|
||||
(req as any).user = payload;
|
||||
req.actorId = payload.userId;
|
||||
req.tenantId = payload.tenant_id || payload.tenantId || 'act_admin';
|
||||
} else {
|
||||
// M2M Tenant JWT Token
|
||||
if (!actorId) {
|
||||
|
||||
@@ -48,7 +48,7 @@ export class CatalogueService {
|
||||
(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
|
||||
FROM products p
|
||||
JOIN drugs d ON p.drug_id = d.id
|
||||
WHERE p.tenant_id = $2 OR EXISTS (SELECT 1 FROM batches WHERE product_id = p.id AND tenant_id = $2)
|
||||
WHERE p.tenant_id = $2 OR EXISTS (SELECT 1 FROM batches WHERE product_id = p.id AND tenant_id = $2) OR p.tenant_id IS NULL
|
||||
ORDER BY p.brand_name ASC
|
||||
LIMIT $1
|
||||
`;
|
||||
|
||||
@@ -12,7 +12,8 @@ const service = new DispensingService();
|
||||
// GET /v1/orders/history
|
||||
router.get('/history', requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const history = await service.getDispensingHistory(req.actorId as string);
|
||||
const tenantId = req.tenantId || req.actorId;
|
||||
const history = await service.getDispensingHistory(tenantId as string);
|
||||
res.status(200).json({ history });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -22,7 +23,8 @@ router.get('/history', requireAuth, async (req, res, next) => {
|
||||
// GET /v1/orders/h1-register
|
||||
router.get('/h1-register', requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const records = await service.getH1Register(req.actorId as string);
|
||||
const tenantId = req.tenantId || req.actorId;
|
||||
const records = await service.getH1Register(tenantId as string);
|
||||
res.status(200).json({ records });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -32,7 +34,8 @@ 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);
|
||||
const tenantId = req.tenantId || req.actorId;
|
||||
const record = await service.getDispensingRecordDetails(req.params.id as string, tenantId as string);
|
||||
if (!record) {
|
||||
return res.status(404).json({ error: 'DISPENSING_RECORD_NOT_FOUND' });
|
||||
}
|
||||
@@ -49,7 +52,8 @@ router.get('/:id/invoice/pdf', requireAuth, async (req, res, next) => {
|
||||
// POST /v1/orders
|
||||
router.post('/', requireAuth, requireHeaders, async (req, res, next) => {
|
||||
try {
|
||||
const draft = await service.createOrderDraft(req.body, req.actorId as string);
|
||||
const tenantId = req.tenantId || req.actorId;
|
||||
const draft = await service.createOrderDraft(req.body, tenantId as string);
|
||||
res.status(201).json(draft);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -77,7 +81,8 @@ router.post('/:id/precheck', requireAuth, async (req, res, next) => {
|
||||
return res.status(400).json({ error: 'CONTEXT_INCOMPLETE' });
|
||||
}
|
||||
|
||||
const precheckResult = await service.runGates(clinical_context, items, req.actorId as string);
|
||||
const tenantId = req.tenantId || req.actorId;
|
||||
const precheckResult = await service.runGates(clinical_context, items, tenantId as string);
|
||||
res.status(200).json(precheckResult);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -93,6 +98,7 @@ router.post('/:id/dispense', requireAuth, async (req, res, next) => {
|
||||
}
|
||||
|
||||
const actorId = req.actorId;
|
||||
const tenantId = req.tenantId || req.actorId;
|
||||
if (!actorId) {
|
||||
return res.status(403).json({ error: 'MISSING_ACTOR_ID' });
|
||||
}
|
||||
@@ -102,7 +108,7 @@ router.post('/:id/dispense', requireAuth, async (req, res, next) => {
|
||||
|
||||
// If items are not provided, fetch them from the draft order
|
||||
if (!items || items.length === 0) {
|
||||
const draftRes = await pool.query('SELECT items FROM order_drafts WHERE id = $1 AND tenant_id = $2', [prescriptionId, req.actorId]);
|
||||
const draftRes = await pool.query('SELECT items FROM order_drafts WHERE id = $1 AND tenant_id = $2', [prescriptionId, tenantId]);
|
||||
if (draftRes.rows.length > 0 && draftRes.rows[0].items) {
|
||||
items = typeof draftRes.rows[0].items === 'string' ? JSON.parse(draftRes.rows[0].items) : draftRes.rows[0].items;
|
||||
}
|
||||
@@ -113,7 +119,7 @@ router.post('/:id/dispense', requireAuth, async (req, res, next) => {
|
||||
}
|
||||
|
||||
// Pass to service which runs the SERIALIZABLE transaction
|
||||
const result = await service.executeDispense(subject_ref, prescriber_name || 'Unknown Prescriber', items, actorId as string, actorId as string, prescriptionId as string | undefined, payment_method as string, overrides || []);
|
||||
const result = await service.executeDispense(subject_ref, prescriber_name || 'Unknown Prescriber', items, actorId as string, tenantId as string, prescriptionId as string | undefined, payment_method as string, overrides || []);
|
||||
|
||||
res.status(201).json(result);
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -56,6 +56,9 @@ router.put('/me', requireAuth, upload.single('profileImage'), async (req, res) =
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
const { hospital_id, username, password, pharmacy_name, dl_number, gstin, pharmacist_reg_number, address } = req.body;
|
||||
if (!hospital_id || String(hospital_id).trim() === '') {
|
||||
return res.status(400).json({ error: 'Hospital ID is required' });
|
||||
}
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: 'Username and password are required' });
|
||||
}
|
||||
@@ -114,7 +117,7 @@ router.post('/token', (req, res) => {
|
||||
}
|
||||
|
||||
// Recognize our specific external tenants
|
||||
const validClients = ['jayam_web_ai', 'telegaruda'];
|
||||
const validClients = ['jayam_web_ai', 'telegaruda', 'pharmacy-v1-staging'];
|
||||
|
||||
if (client_id && validClients.includes(client_id) && client_secret) {
|
||||
// Generate a secure signed token specifically for this hospital
|
||||
|
||||
@@ -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, profile_image_url FROM users WHERE id = $1',
|
||||
'SELECT id, username, role, pharmacy_name, dl_number, gstin, pharmacist_reg_number, address, profile_image_url, tenant_id FROM users WHERE id = $1',
|
||||
[userId]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
|
||||
@@ -46,59 +46,80 @@ export class IntegrationController {
|
||||
try {
|
||||
const payload = req.body;
|
||||
|
||||
const patientId = payload.patient_id || payload.patientId;
|
||||
const medicines = payload.values || payload.medicines;
|
||||
const patientId = payload.patient_id || payload.patientId || payload.hospital_patient_id || payload.id;
|
||||
const medicines = payload.values || payload.medicines || payload.items || payload.prescriptions;
|
||||
|
||||
if (!patientId || !medicines) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
if (!medicines || !Array.isArray(medicines)) {
|
||||
return res.status(400).json({ error: 'Missing or invalid medicines/items array in prescription payload' });
|
||||
}
|
||||
|
||||
const id = "rx_" + Date.now() + Math.floor(Math.random() * 1000);
|
||||
let patientName = payload.patientName || payload.patient_name || payload.full_name || payload.name;
|
||||
const tenantId = payload.hospital_id || payload.tenant_id || payload.tenantId ? String(payload.hospital_id || payload.tenant_id || payload.tenantId) : 'act_admin';
|
||||
|
||||
// Auto-upsert patient record if patient info is available
|
||||
if (patientId || patientName) {
|
||||
try {
|
||||
const savedPatient = await IntegrationService.saveOrUpdatePatient({
|
||||
hospital_patient_id: patientId || `PAT-${Date.now()}`,
|
||||
full_name: patientName || `Patient ${patientId}`,
|
||||
contact_number: payload.contact_number || payload.phone || payload.mobile,
|
||||
email: payload.email,
|
||||
allergies: payload.allergies,
|
||||
hospital_id: tenantId
|
||||
});
|
||||
if (savedPatient && savedPatient.full_name) {
|
||||
patientName = savedPatient.full_name;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[IntegrationController] Auto patient sync warning:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Map Hospital API payload to Pharmacy UI expected format
|
||||
const formattedItems = medicines.map((med: any) => {
|
||||
const medName = med.medicine_name || med.name || med.drug_name || med.brand || 'Unknown Medicine';
|
||||
const instruction = med.medicine_instruction || med.instruction || med.dosage_instruction || med.dosage || '';
|
||||
const totalQty = parseInt(med.total_qty || med.qty || med.quantity || 0) || 0;
|
||||
const duration = med.total_medicine_duration || med.medicine_duration || med.duration || '';
|
||||
const form = (med.medicine_type || med.form || med.type || 'tablet').toLowerCase();
|
||||
|
||||
let dose = med.dose || '1-0-1';
|
||||
if (med.medicine_taken) {
|
||||
const m = med.medicine_taken?.m || '0';
|
||||
const a = med.medicine_taken?.a || '0';
|
||||
const e = med.medicine_taken?.e || '0';
|
||||
const n = med.medicine_taken?.n || '0';
|
||||
dose = `${m}-${a}-${e}-${n}`;
|
||||
}
|
||||
|
||||
return {
|
||||
parsed: {
|
||||
qty: parseInt(med.total_qty) || 0,
|
||||
dose: `${m}-${a}-${e}-${n}`,
|
||||
form: med.medicine_type?.toLowerCase() || 'tablet',
|
||||
brand: med.medicine_name,
|
||||
total_qty_str: med.total_qty,
|
||||
duration: med.total_medicine_duration || med.medicine_duration,
|
||||
instruction: med.medicine_instruction
|
||||
qty: totalQty,
|
||||
dose: dose,
|
||||
form: form,
|
||||
brand: medName,
|
||||
total_qty_str: String(totalQty),
|
||||
duration: duration,
|
||||
instruction: instruction
|
||||
},
|
||||
verbatim: `${med.medicine_name} - ${med.medicine_instruction}`,
|
||||
verbatim: instruction ? `${medName} - ${instruction}` : medName,
|
||||
confidence: 1.0,
|
||||
confusable_with: [],
|
||||
explicitly_confirmed: true
|
||||
};
|
||||
});
|
||||
|
||||
let patientName = payload.patientName || payload.patient_name;
|
||||
if (!patientName && patientId) {
|
||||
try {
|
||||
const client = await require('../../db/pool').pool.connect();
|
||||
const pResult = await client.query('SELECT full_name FROM patients WHERE hospital_patient_id = $1', [String(patientId)]);
|
||||
if (pResult.rows.length > 0) {
|
||||
patientName = pResult.rows[0].full_name;
|
||||
}
|
||||
client.release();
|
||||
} catch(e) { console.log('Error fetching patient name:', e); }
|
||||
}
|
||||
|
||||
const structuredData = {
|
||||
items: formattedItems,
|
||||
patient: {
|
||||
age: 30, // Dummy age, update if you add age to payload later
|
||||
name: patientName || ""
|
||||
age: payload.age || payload.patient_age || 30,
|
||||
name: patientName || "Unknown Patient",
|
||||
id: patientId || ""
|
||||
},
|
||||
prescriber: {
|
||||
name: payload.providerName || payload.doctorName || payload.doctor_name || "",
|
||||
registration_number: "N/A"
|
||||
name: payload.providerName || payload.doctorName || payload.doctor_name || "Hospital Doctor",
|
||||
registration_number: payload.doctor_reg_no || payload.registration_number || "N/A"
|
||||
},
|
||||
image_quality: "digital_webhook"
|
||||
};
|
||||
@@ -114,13 +135,14 @@ export class IntegrationController {
|
||||
'pending_review',
|
||||
JSON.stringify(structuredData),
|
||||
null, // Leave confirmed_data null so pharmacist can review it
|
||||
payload.hospital_id ? String(payload.hospital_id) : 'act_admin'
|
||||
tenantId
|
||||
];
|
||||
|
||||
await require('../../db/pool').pool.query(query, values);
|
||||
|
||||
res.status(201).json({ message: 'Prescription received and saved', data: payload, id });
|
||||
} catch (error: any) {
|
||||
console.error('[IntegrationController] Error receiving prescription:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
@@ -139,8 +161,16 @@ export class IntegrationController {
|
||||
const tenantId = req.headers['x-tenant-id'] || 'act_admin';
|
||||
const client = await require('../../db/pool').pool.connect();
|
||||
try {
|
||||
const query = `SELECT * FROM prescriptions WHERE tenant_id = $1 ORDER BY created_at DESC;`;
|
||||
const result = await client.query(query, [String(tenantId)]);
|
||||
let query: string;
|
||||
let values: any[];
|
||||
if (!tenantId || tenantId === 'act_admin' || tenantId === 'all' || tenantId === 'undefined' || tenantId === 'null') {
|
||||
query = `SELECT * FROM prescriptions ORDER BY created_at DESC;`;
|
||||
values = [];
|
||||
} else {
|
||||
query = `SELECT * FROM prescriptions WHERE tenant_id = $1 OR tenant_id = 'act_admin' OR tenant_id IS NULL ORDER BY created_at DESC;`;
|
||||
values = [String(tenantId)];
|
||||
}
|
||||
const result = await client.query(query, values);
|
||||
res.status(200).json({ message: 'All prescriptions', data: result.rows });
|
||||
} finally {
|
||||
client.release();
|
||||
|
||||
@@ -5,45 +5,59 @@ const router = Router();
|
||||
|
||||
// Middleware to protect integration routes using custom headers
|
||||
router.use((req, res, next) => {
|
||||
// Allow GET endpoints for browser testing if you want, but strictly protect POSTs.
|
||||
// Allow GET endpoints for browser testing if desired, but strictly protect POSTs.
|
||||
if (req.method === 'GET') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const apiKey = req.headers['x-api-key'];
|
||||
const client = req.headers['x-api-client'];
|
||||
const secret = req.headers['x-api-secret'];
|
||||
|
||||
// JWT_SECRET is the key we generated earlier in .env
|
||||
// If valid x-api-key is present, allow
|
||||
if (apiKey && process.env.API_KEY && String(apiKey).trim() === process.env.API_KEY.trim()) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const EXPECTED_SECRET = process.env.JWT_SECRET;
|
||||
|
||||
if (!client || !secret) {
|
||||
return res.status(401).json({ error: 'Unauthorized: Missing API credentials' });
|
||||
return res.status(401).json({ error: 'Unauthorized: Missing API credentials (x-api-key or x-api-client/x-api-secret required)' });
|
||||
}
|
||||
|
||||
if (client !== 'hospital-api' && client !== 'pharmacy-v1-staging') {
|
||||
const validClients = ['hospital-api', 'pharmacy-v1-staging', 'jayam_web_ai', 'telegaruda', 'hospital'];
|
||||
if (!validClients.includes(String(client))) {
|
||||
return res.status(401).json({ error: 'Unauthorized: Invalid client ID' });
|
||||
}
|
||||
|
||||
if (secret !== EXPECTED_SECRET) {
|
||||
const validSecrets = [EXPECTED_SECRET, 'd7f82a93c4e1056b8921f40b3a5c7891', process.env.WEBHOOK_SECRET].filter(Boolean);
|
||||
if (!validSecrets.includes(String(secret))) {
|
||||
return res.status(401).json({ error: 'Unauthorized: Invalid client secret' });
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Endpoint for Hospital API to sync patient data (Webhook receiver)
|
||||
// Patient sync endpoints (Hospital Webhook receiver & Aliases)
|
||||
router.post('/hospital/patient-sync', IntegrationController.syncHospitalPatient);
|
||||
router.post('/patient-sync', IntegrationController.syncHospitalPatient);
|
||||
router.post('/patients/sync', IntegrationController.syncHospitalPatient);
|
||||
|
||||
// Endpoint to get all synced patients in Pharmacy
|
||||
// Synced patients endpoints
|
||||
router.get('/hospital/patients', IntegrationController.getAllPatients);
|
||||
router.get('/patients', IntegrationController.getAllPatients);
|
||||
|
||||
// Endpoint for Telegaruda to push prescriptions
|
||||
// Prescription endpoints (Telegaruda & Hospital API receiver)
|
||||
router.post('/telegaruda/prescription', IntegrationController.receivePrescription);
|
||||
router.post('/hospital/prescription', IntegrationController.receivePrescription);
|
||||
router.post('/prescription', IntegrationController.receivePrescription);
|
||||
router.post('/prescriptions', IntegrationController.receivePrescription);
|
||||
|
||||
// Endpoint for Pharmacy to fetch patient details from Telegaruda
|
||||
// Telegaruda patient details
|
||||
router.get('/telegaruda/patient/:patientId', IntegrationController.fetchPatientDetails);
|
||||
|
||||
// Endpoint to view all synced prescriptions (For testing)
|
||||
// Synced prescriptions endpoints
|
||||
router.get('/hospital/prescriptions', IntegrationController.getAllPrescriptions);
|
||||
router.get('/prescriptions', IntegrationController.getAllPrescriptions);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -6,33 +6,38 @@ export class IntegrationService {
|
||||
* UPSERT a patient into the Pharmacy DB coming from the Hospital API webhook.
|
||||
*/
|
||||
static async saveOrUpdatePatient(payload: any) {
|
||||
console.log('[IntegrationService] Syncing patient from Hospital API:', payload.hospital_patient_id);
|
||||
const hospitalPatientId = String(payload.hospital_patient_id || payload.patient_id || payload.patientId || payload.id || `PAT-${Date.now()}`);
|
||||
const fullName = payload.full_name || payload.patient_name || payload.patientName || payload.name || 'Unknown Patient';
|
||||
const contactNumber = payload.contact_number || payload.phone || payload.mobile || null;
|
||||
const email = payload.email || null;
|
||||
const allergies = payload.allergies ? (typeof payload.allergies === 'string' ? payload.allergies : JSON.stringify(payload.allergies)) : null;
|
||||
const tenantId = payload.hospital_id || payload.tenant_id || payload.tenantId ? String(payload.hospital_id || payload.tenant_id || payload.tenantId) : 'act_admin';
|
||||
|
||||
console.log('[IntegrationService] Syncing patient from Hospital API:', hospitalPatientId, fullName);
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const allergies = payload.allergies ? JSON.stringify(payload.allergies) : null;
|
||||
|
||||
const query = `
|
||||
INSERT INTO patients (hospital_patient_id, full_name, contact_number, email, allergies, tenant_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (hospital_patient_id)
|
||||
DO UPDATE SET
|
||||
full_name = EXCLUDED.full_name,
|
||||
contact_number = EXCLUDED.contact_number,
|
||||
email = EXCLUDED.email,
|
||||
allergies = EXCLUDED.allergies,
|
||||
contact_number = COALESCE(EXCLUDED.contact_number, patients.contact_number),
|
||||
email = COALESCE(EXCLUDED.email, patients.email),
|
||||
allergies = COALESCE(EXCLUDED.allergies, patients.allergies),
|
||||
tenant_id = EXCLUDED.tenant_id,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING id, hospital_patient_id, full_name;
|
||||
`;
|
||||
|
||||
const values = [
|
||||
payload.hospital_patient_id,
|
||||
payload.full_name,
|
||||
payload.contact_number || null,
|
||||
payload.email || null,
|
||||
hospitalPatientId,
|
||||
fullName,
|
||||
contactNumber,
|
||||
email,
|
||||
allergies,
|
||||
payload.hospital_id ? String(payload.hospital_id) : 'act_admin' // fallback tenant
|
||||
tenantId
|
||||
];
|
||||
|
||||
const result = await client.query(query, values);
|
||||
@@ -48,8 +53,16 @@ export class IntegrationService {
|
||||
static async getAllPatients(tenantId: string) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const query = `SELECT * FROM patients WHERE tenant_id = $1 ORDER BY created_at DESC;`;
|
||||
const result = await client.query(query, [tenantId]);
|
||||
let query: string;
|
||||
let values: any[];
|
||||
if (!tenantId || tenantId === 'act_admin' || tenantId === 'all' || tenantId === 'undefined' || tenantId === 'null') {
|
||||
query = `SELECT * FROM patients ORDER BY created_at DESC;`;
|
||||
values = [];
|
||||
} else {
|
||||
query = `SELECT * FROM patients WHERE tenant_id = $1 OR tenant_id = 'act_admin' OR tenant_id IS NULL ORDER BY created_at DESC;`;
|
||||
values = [tenantId];
|
||||
}
|
||||
const result = await client.query(query, values);
|
||||
return result.rows;
|
||||
} finally {
|
||||
client.release();
|
||||
|
||||
@@ -7,7 +7,8 @@ const router = Router();
|
||||
// GET /v1/inventory/suppliers
|
||||
router.get('/suppliers', requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const { rows } = await pool.query('SELECT * FROM suppliers WHERE tenant_id = $1 ORDER BY name ASC', [req.actorId]);
|
||||
const tenantId = req.tenantId || req.actorId;
|
||||
const { rows } = await pool.query('SELECT * FROM suppliers WHERE tenant_id = $1 ORDER BY name ASC', [tenantId]);
|
||||
res.status(200).json({ suppliers: rows });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -23,11 +24,12 @@ router.post('/suppliers', requireAuth, async (req, res, next) => {
|
||||
}
|
||||
|
||||
const id = 'sup_' + Date.now() + Math.floor(Math.random() * 1000);
|
||||
const tenantId = req.tenantId || req.actorId;
|
||||
|
||||
await pool.query(`
|
||||
INSERT INTO suppliers (id, tenant_id, name, contact_number, email, gstin, dl_number, address, contact_person)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
`, [id, req.actorId, name, contact_number, email, gstin, dl_number, address, contact_person]);
|
||||
`, [id, tenantId, name, contact_number, email, gstin, dl_number, address, contact_person]);
|
||||
|
||||
res.status(201).json({ id, name, contact_number, email, gstin, dl_number, address, contact_person });
|
||||
} catch (error) {
|
||||
|
||||
@@ -18,8 +18,8 @@ router.post('/', requireAuth, upload.single('image'), async (req, res, next) =>
|
||||
return res.status(400).json({ error: 'MISSING_IMAGE', message: 'No image file uploaded.' });
|
||||
}
|
||||
|
||||
const actorId = req.actorId;
|
||||
const result = await service.uploadPrescription(req.file, actorId!);
|
||||
const tenantId = req.tenantId || req.headers['x-tenant-id'] || 'act_admin';
|
||||
const result = await service.uploadPrescription(req.file, String(tenantId));
|
||||
|
||||
res.status(202).json(result); // 202 Accepted (processing asynchronously)
|
||||
} catch (error) {
|
||||
@@ -30,7 +30,7 @@ router.post('/', requireAuth, upload.single('image'), async (req, res, next) =>
|
||||
// GET /v1/prescriptions
|
||||
router.get('/', requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] || 'act_admin';
|
||||
const tenantId = req.tenantId || req.headers['x-tenant-id'] || 'act_admin';
|
||||
const prescriptions = await service.getAllPrescriptions(String(tenantId));
|
||||
res.status(200).json({ prescriptions });
|
||||
} catch (error) {
|
||||
@@ -41,7 +41,7 @@ router.get('/', requireAuth, async (req, res, next) => {
|
||||
// GET /v1/prescriptions/:id
|
||||
router.get('/:id', requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] || 'act_admin';
|
||||
const tenantId = req.tenantId || req.headers['x-tenant-id'] || 'act_admin';
|
||||
const record = await service.getPrescription(req.params.id as string, String(tenantId));
|
||||
if (!record) return res.status(404).json({ error: 'NOT_FOUND' });
|
||||
|
||||
@@ -54,7 +54,7 @@ router.get('/:id', requireAuth, async (req, res, next) => {
|
||||
// POST /v1/prescriptions/:id/confirm
|
||||
router.post('/:id/confirm', requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] || 'act_admin';
|
||||
const tenantId = req.tenantId || req.headers['x-tenant-id'] || 'act_admin';
|
||||
const result = await service.confirmPrescription(req.params.id as string, req.body, String(tenantId));
|
||||
|
||||
res.status(200).json(result);
|
||||
|
||||
@@ -133,18 +133,27 @@ export class PrescriptionService {
|
||||
}
|
||||
|
||||
async getPrescription(id: string, actorId: string) {
|
||||
const { rows } = await pool.query(`SELECT * FROM prescriptions WHERE id = $1 AND tenant_id = $2`, [id, actorId]);
|
||||
const { rows } = await pool.query(`SELECT * FROM prescriptions WHERE id = $1 AND (tenant_id = $2 OR tenant_id = 'act_admin' OR tenant_id IS NULL)`, [id, actorId]);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async getAllPrescriptions(actorId: string) {
|
||||
const { rows } = await pool.query(`SELECT * FROM prescriptions WHERE tenant_id = $1 ORDER BY created_at DESC`, [actorId]);
|
||||
let query: string;
|
||||
let values: any[];
|
||||
if (!actorId || actorId === 'act_admin' || actorId === 'all' || actorId === 'undefined' || actorId === 'null') {
|
||||
query = `SELECT * FROM prescriptions ORDER BY created_at DESC`;
|
||||
values = [];
|
||||
} else {
|
||||
query = `SELECT * FROM prescriptions WHERE tenant_id = $1 OR tenant_id = 'act_admin' OR tenant_id IS NULL ORDER BY created_at DESC`;
|
||||
values = [actorId];
|
||||
}
|
||||
const { rows } = await pool.query(query, values);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async confirmPrescription(id: string, confirmedData: any, actorId: string) {
|
||||
// Human Gate 1: Validate that unconfirmed fields are explicitly reviewed
|
||||
const { rows } = await pool.query(`SELECT status, extracted_data FROM prescriptions WHERE id = $1 AND tenant_id = $2`, [id, actorId]);
|
||||
const { rows } = await pool.query(`SELECT status, extracted_data FROM prescriptions WHERE id = $1 AND (tenant_id = $2 OR tenant_id = 'act_admin' OR tenant_id IS NULL)`, [id, actorId]);
|
||||
const record = rows[0];
|
||||
|
||||
if (!record) throw { status: 404, error: 'NOT_FOUND' };
|
||||
@@ -170,7 +179,7 @@ export class PrescriptionService {
|
||||
await pool.query(`
|
||||
UPDATE prescriptions
|
||||
SET status = 'confirmed', confirmed_data = $1
|
||||
WHERE id = $2 AND tenant_id = $3
|
||||
WHERE id = $2 AND (tenant_id = $3 OR tenant_id = 'act_admin' OR tenant_id IS NULL)
|
||||
`, [confirmedData, id, actorId]);
|
||||
|
||||
return { id, status: 'confirmed' };
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "commonjs",
|
||||
"types": ["node"],
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
|
||||
+121
-29
@@ -1,19 +1,69 @@
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
|
||||
filepath = r"c:\Users\Lenovo\Desktop\ai_pharma_frontend\src\services\api.ts"
|
||||
# Candidate paths for frontend api.ts
|
||||
DEFAULT_PATHS = [
|
||||
r"c:\Users\CSH12\Desktop\ai_pharma_frontend\src\services\api.ts",
|
||||
r"c:\Users\Lenovo\Desktop\ai_pharma_frontend\src\services\api.ts",
|
||||
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "ai_pharma_frontend", "src", "services", "api.ts")),
|
||||
os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "frontend", "src", "services", "api.ts")),
|
||||
os.path.abspath(os.path.join(os.path.dirname(__file__), "src", "services", "api.ts")),
|
||||
]
|
||||
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
def find_target_file():
|
||||
# 1. Command-line argument
|
||||
if len(sys.argv) > 1 and os.path.exists(sys.argv[1]):
|
||||
return sys.argv[1]
|
||||
|
||||
# 2. Environment variable
|
||||
env_path = os.environ.get("API_TS_PATH")
|
||||
if env_path and os.path.exists(env_path):
|
||||
return env_path
|
||||
|
||||
# 3. Check direct known candidate paths
|
||||
for p in DEFAULT_PATHS:
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
|
||||
# 4. Check top-level directories on Desktop
|
||||
user_desktop = os.path.expanduser(r"~\Desktop")
|
||||
if os.path.exists(user_desktop):
|
||||
try:
|
||||
for item in os.listdir(user_desktop):
|
||||
candidate = os.path.join(user_desktop, item, "src", "services", "api.ts")
|
||||
if os.path.exists(candidate):
|
||||
return candidate
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def update_api_file(client_id="jayam_web_ai"):
|
||||
filepath = find_target_file()
|
||||
|
||||
if not filepath:
|
||||
print("ERROR: Target 'api.ts' file could not be found automatically.")
|
||||
print("Usage: python update_api.py <path_to_api.ts>")
|
||||
return False
|
||||
|
||||
print(f"Found target file: {filepath}")
|
||||
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Replacement 1: Add API_KEY to axios.create
|
||||
old_axios_create = """export const api = axios.create({
|
||||
original_content = content
|
||||
replacements_made = 0
|
||||
|
||||
# Replacement 1: Add API_KEY to axios.create
|
||||
old_axios_create = """export const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});"""
|
||||
|
||||
new_axios_create = """export const api = axios.create({
|
||||
new_axios_create = """export const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -21,43 +71,85 @@ new_axios_create = """export const api = axios.create({
|
||||
},
|
||||
});"""
|
||||
|
||||
content = content.replace(old_axios_create, new_axios_create)
|
||||
if old_axios_create in content:
|
||||
content = content.replace(old_axios_create, new_axios_create)
|
||||
replacements_made += 1
|
||||
print(" - Updated axios.create headers with x-api-key")
|
||||
elif "'x-api-key'" not in content and "axios.create" in content:
|
||||
content = re.sub(
|
||||
r"(headers:\s*\{[^}]*'Content-Type':\s*['\"]application/json['\"])(,?\s*\})",
|
||||
r"\1,\n 'x-api-key': import.meta.env.VITE_API_KEY || ''\2",
|
||||
content
|
||||
)
|
||||
if content != original_content:
|
||||
replacements_made += 1
|
||||
print(" - Inserted x-api-key into axios.create headers")
|
||||
|
||||
# Replacement 2: update authenticate
|
||||
old_auth = """ const response = await axios.post(`${API_BASE_URL}/auth/token`, {
|
||||
# Replacement 2: Update Hospital API client_credentials authentication (/auth/token)
|
||||
# Using regex matching so ANY client_id (e.g. jayam_web_ai, telegaruda, etc.) is supported
|
||||
token_pattern = r"(axios\.post\s*\(\s*`?\$\{?API_BASE_URL\}?/auth/token`?\s*,\s*\{[^}]*grant_type:\s*['\"]client_credentials['\"][^}]*\})\s*(;|\))"
|
||||
|
||||
if re.search(token_pattern, content) and "x-api-key" not in re.search(token_pattern, content).group(0):
|
||||
content = re.sub(
|
||||
token_pattern,
|
||||
r"\1, { headers: { 'x-api-key': import.meta.env.VITE_API_KEY || '' } }\2",
|
||||
content
|
||||
)
|
||||
replacements_made += 1
|
||||
print(f" - Updated Hospital API client_credentials authentication (/auth/token) headers for client_id")
|
||||
else:
|
||||
# Fallback literal string replace for specific client_id
|
||||
old_auth = f""" const response = await axios.post(`${{API_BASE_URL}}/auth/token`, {{
|
||||
grant_type: 'client_credentials',
|
||||
client_id: 'jayam_web_ai',
|
||||
client_id: '{client_id}',
|
||||
client_secret: 'my_secret_key'
|
||||
});"""
|
||||
}});"""
|
||||
|
||||
new_auth = """ const response = await axios.post(`${API_BASE_URL}/auth/token`, {
|
||||
new_auth = f""" const response = await axios.post(`${{API_BASE_URL}}/auth/token`, {{
|
||||
grant_type: 'client_credentials',
|
||||
client_id: 'jayam_web_ai',
|
||||
client_id: '{client_id}',
|
||||
client_secret: 'my_secret_key'
|
||||
}, {
|
||||
}}, {{
|
||||
headers: {{ 'x-api-key': import.meta.env.VITE_API_KEY || '' }}
|
||||
}});"""
|
||||
|
||||
if old_auth in content:
|
||||
content = content.replace(old_auth, new_auth)
|
||||
replacements_made += 1
|
||||
print(f" - Updated Hospital API client_credentials authentication headers ({client_id})")
|
||||
|
||||
# Replacement 3: Update loginPharmacist (/auth/login)
|
||||
old_login = """ const response = await axios.post(`${API_BASE_URL}/auth/login`, credentials);"""
|
||||
new_login = """ const response = await axios.post(`${API_BASE_URL}/auth/login`, credentials, {
|
||||
headers: { 'x-api-key': import.meta.env.VITE_API_KEY || '' }
|
||||
});"""
|
||||
|
||||
content = content.replace(old_auth, new_auth)
|
||||
if old_login in content:
|
||||
content = content.replace(old_login, new_login)
|
||||
replacements_made += 1
|
||||
print(" - Updated loginPharmacist endpoint headers")
|
||||
|
||||
# Replacement 3: update loginPharmacist
|
||||
old_login = """ const response = await axios.post(`${API_BASE_URL}/auth/login`, credentials);"""
|
||||
|
||||
new_login = """ const response = await axios.post(`${API_BASE_URL}/auth/login`, credentials, {
|
||||
# Replacement 4: Update logoutPharmacist (/auth/logout)
|
||||
old_logout = """ const response = await axios.post(`${API_BASE_URL}/auth/logout`);"""
|
||||
new_logout = """ const response = await axios.post(`${API_BASE_URL}/auth/logout`, {}, {
|
||||
headers: { 'x-api-key': import.meta.env.VITE_API_KEY || '' }
|
||||
});"""
|
||||
|
||||
content = content.replace(old_login, new_login)
|
||||
if old_logout in content:
|
||||
content = content.replace(old_logout, new_logout)
|
||||
replacements_made += 1
|
||||
print(" - Updated logoutPharmacist endpoint headers")
|
||||
|
||||
# Replacement 4: update logoutPharmacist
|
||||
old_logout = """ const response = await axios.post(`${API_BASE_URL}/auth/logout`);"""
|
||||
new_logout = """ const response = await axios.post(`${API_BASE_URL}/auth/logout`, {}, {
|
||||
headers: { 'x-api-key': import.meta.env.VITE_API_KEY || '' }
|
||||
});"""
|
||||
|
||||
content = content.replace(old_logout, new_logout)
|
||||
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
# Save modified file
|
||||
if content != original_content:
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
print(f"SUCCESS: Applied {replacements_made} update(s) to {filepath}")
|
||||
else:
|
||||
print("INFO: File is already up to date or no matching patterns were found.")
|
||||
|
||||
print("Updated api.ts")
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
cid = sys.argv[2] if len(sys.argv) > 2 else "pharmacy-v1-staging"
|
||||
update_api_file(client_id=cid)
|
||||
|
||||
Reference in New Issue
Block a user