Files
sap-erp/services/batchIntimationStore.js
T
John 69b4e68baf
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s
first commit
2026-09-23 17:31:02 +05:30

233 lines
8.1 KiB
JavaScript

// services/batchIntimationStore.js
// Batch Issuance Intimation records in SQL: ZBATCH_INTIMATIONS
// Doc No format: BII-{MM}-{YY}-{NNNN}. References a Requirement's Ref No.
const sql = require('mssql');
const TABLE = `[dbo].[ZBATCH_INTIMATIONS]`;
let _conn = null;
async function getConn() {
if (_conn) return _conn;
const config = {
server: process.env.APP_SQL_HOST,
port: parseInt(process.env.APP_SQL_PORT),
user: process.env.APP_SQL_USER,
password: process.env.APP_SQL_PASSWORD,
database: process.env.APP_SQL_DATABASE,
options: { encrypt: true, trustServerCertificate: true },
};
_conn = await sql.connect(config);
return _conn;
}
async function exec(sqlQuery, params = []) {
const conn = await getConn();
const request = conn.request();
params.forEach((param, index) => { request.input(`param${index}`, param); });
const replacedSql = sqlQuery.replace(/\?/g, (match, offset, string) => {
const paramIndex = (string.slice(0, offset).match(/\?/g) || []).length;
return `@param${paramIndex}`;
});
const result = await request.query(replacedSql);
return result.recordset || [];
}
function isAlreadyExists(e) {
const m = (e.message || '').toLowerCase();
return m.includes('already exists') || m.includes('duplicate') || m.includes('existing object') || m.includes('there is already an object');
}
async function bootstrap() {
console.log('[BII-STORE] Checking table', TABLE, '...');
await exec(`
CREATE TABLE ${TABLE} (
ID INT IDENTITY(1,1) PRIMARY KEY,
DOC_NO NVARCHAR(30) NOT NULL,
REF_NO NVARCHAR(30),
REQUIREMENT_ID INT,
DOC_DATE DATE,
ISSUE_DATE DATE,
PRODUCTS NVARCHAR(MAX),
REMARKS NVARCHAR(MAX),
STATUS NVARCHAR(30) DEFAULT 'SENT_TO_QA',
CREATED_BY NVARCHAR(50),
CREATED_NAME NVARCHAR(100),
CREATED_AT DATETIME2,
UPDATED_AT DATETIME2,
IS_DELETED BIT DEFAULT 0,
COMPANY NVARCHAR(60)
)
`).catch(e => {
if (isAlreadyExists(e)) { console.log('[BII-STORE] Table exists — OK'); }
else throw e;
});
console.log('[BII-STORE] ✅ Ready');
}
function toTs(isoStr) {
if (!isoStr) return null;
return isoStr.replace('T', ' ').replace('Z', '').substring(0, 23);
}
function safeJson(val, fallback) {
if (!val) return fallback;
try { return JSON.parse(val); } catch { return fallback; }
}
function fromRow(row) {
if (!row) return null;
return {
id: row.ID,
docNo: row.DOC_NO || '',
refNo: row.REF_NO || '',
requirementId: row.REQUIREMENT_ID || null,
date: row.DOC_DATE ? new Date(row.DOC_DATE).toISOString().slice(0, 10) : null,
issueDate: row.ISSUE_DATE ? new Date(row.ISSUE_DATE).toISOString().slice(0, 10) : null,
products: safeJson(row.PRODUCTS, []),
remarks: row.REMARKS || '',
status: row.STATUS || 'SENT_TO_QA',
createdBy: row.CREATED_BY || '',
createdByName: row.CREATED_NAME || '',
createdAt: row.CREATED_AT ? new Date(row.CREATED_AT).toISOString() : null,
updatedAt: row.UPDATED_AT ? new Date(row.UPDATED_AT).toISOString() : null,
isDeleted: !!row.IS_DELETED,
company: row.COMPANY || '',
};
}
async function generateDocNo(company) {
const now = new Date();
const mm = String(now.getMonth() + 1).padStart(2, '0');
const yy = String(now.getFullYear()).slice(-2);
const prefix = `BII-${mm}-${yy}-`;
const rows = await exec(
`SELECT DOC_NO FROM ${TABLE} WHERE DOC_NO LIKE ? ${company ? 'AND COMPANY = ?' : ''}`,
company ? [prefix + '%', company] : [prefix + '%']
);
let max = 0;
rows.forEach(r => {
const n = parseInt((r.DOC_NO || '').slice(prefix.length), 10);
if (!isNaN(n) && n > max) max = n;
});
return prefix + String(max + 1).padStart(4, '0');
}
async function insertIntimation(r) {
const now = toTs(new Date().toISOString());
const docNo = await generateDocNo(r.company);
// INSERT and SELECT SCOPE_IDENTITY() must be one batch — as two separate
// exec() calls, a pooled connection can route the second one to a
// DIFFERENT physical connection than the one that just inserted, where
// SCOPE_IDENTITY() correctly returns NULL (see workOrderStore.js's
// insertWorkOrder() for the full write-up of this bug class).
const idRows = await exec(`
INSERT INTO ${TABLE} (
DOC_NO, REF_NO, REQUIREMENT_ID, DOC_DATE, ISSUE_DATE, PRODUCTS, REMARKS,
STATUS, CREATED_BY, CREATED_NAME, CREATED_AT, COMPANY
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?);
SELECT SCOPE_IDENTITY() AS ID;
`, [
docNo,
r.refNo || '',
r.requirementId ? parseInt(r.requirementId) : null,
r.date || null,
r.issueDate || null,
JSON.stringify(r.products || []),
r.remarks || '',
'SENT_TO_QA',
r.createdBy,
r.createdByName || r.createdBy,
now,
r.company || '',
]);
return { id: idRows[0].ID, docNo, ...r, status: 'SENT_TO_QA', createdAt: new Date().toISOString() };
}
async function listIntimations({ mine, company, status, refNo, includeDeleted } = {}) {
const all = await exec(`SELECT * FROM ${TABLE} ORDER BY CREATED_AT DESC`);
return all.map(fromRow).filter(r => {
if (!includeDeleted && r.isDeleted) return false;
if (status && status !== 'ALL' && r.status !== status.toUpperCase()) return false;
if (refNo && r.refNo !== refNo) return false;
if (mine && r.createdBy !== mine) return false;
if (company && r.company && r.company !== company) return false;
return true;
});
}
async function findById(id) {
const rows = await exec(`SELECT * FROM ${TABLE} WHERE ID = ?`, [parseInt(id)]);
return rows.length ? fromRow(rows[0]) : null;
}
async function updateIntimation(id, patch) {
const now = toTs(new Date().toISOString());
await exec(`
UPDATE ${TABLE}
SET DOC_DATE = ?, ISSUE_DATE = ?, PRODUCTS = ?, REMARKS = ?, UPDATED_AT = ?
WHERE ID = ? AND (IS_DELETED = 0 OR IS_DELETED IS NULL)
`, [
patch.date || null,
patch.issueDate || null,
JSON.stringify(patch.products || []),
patch.remarks || '',
now,
parseInt(id),
]);
return findById(id);
}
async function softDelete(id) {
await exec(`UPDATE ${TABLE} SET IS_DELETED = 1, UPDATED_AT = ? WHERE ID = ?`,
[toTs(new Date().toISOString()), parseInt(id)]);
}
// Sum of intimated Batch Size per requirement, per item.
// Returns { [requirementId||refNo]: { docs: <count>, byItem: { itemCode: qty } } }
// `excludeId` lets an edit exclude its own current record from the totals.
async function intimatedByRequirement({ company, excludeId } = {}) {
const list = await listIntimations({ company });
const map = {};
list.forEach(bi => {
if (excludeId && String(bi.id) === String(excludeId)) return;
const keys = [];
if (bi.requirementId != null) keys.push('rid:' + bi.requirementId);
if (bi.refNo) keys.push('ref:' + bi.refNo);
keys.forEach(k => {
if (!map[k]) map[k] = { docs: 0, byItem: {} };
map[k].docs += 1;
(bi.products || []).forEach(p => {
const qty = (p.batches || []).reduce((s, b) => s + (Number(b.batchSize) || 0), 0);
map[k].byItem[p.itemCode] = (map[k].byItem[p.itemCode] || 0) + qty;
});
});
});
return map;
}
// Does this batch number already appear in ANY other (non-deleted) saved
// Intimation's products/batches? Used by the "Batch No. Required" uniqueness
// check — `excludeId` lets an edit ignore its own current record.
async function findBatchNoUsage(batchNo, excludeId) {
const needle = (batchNo || '').trim().toUpperCase();
if (!needle) return null;
const list = await listIntimations({});
for (const bi of list) {
if (excludeId && String(bi.id) === String(excludeId)) continue;
for (const p of (bi.products || [])) {
for (const b of (p.batches || [])) {
if ((b.batchNo || '').trim().toUpperCase() === needle) {
return { docNo: bi.docNo, itemCode: p.itemCode };
}
}
}
}
return null;
}
module.exports = {
bootstrap, generateDocNo, insertIntimation,
listIntimations, findById, updateIntimation, softDelete,
intimatedByRequirement, findBatchNoUsage,
};