Files
sap-erp/services/sales/samples.js
T
John eead8f5ffd
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s
sale order
2026-10-05 18:45:17 +05:30

261 lines
20 KiB
JavaScript

// services/sales/samples.js
// Sample Requests — msale's two sample workflows:
//
// Domestic (sales_sample): raised by Sales (SM/BUH) for a customer
// 1 Submitted ──buh_review──▶ 2 Approved | 3 Modification | 4 Rejected
// 3 ──creator edits──▶ 5 Resubmitted ──buh_review──▶ 2/3/4
// 2/5→ Logistics: stock available? yes ▶ 6 QA pending (batch nos, due +2d) / no ▶ 7 In production
// 7 ──logistics──▶ 6 ; 6 ──logistics (QA verified + challan/courier)──▶ 8 Shipped
// 8 ──feedback (Sales)──▶ 9 Completed
// Export (sales_export_sample): raised by Export team
// 1 Submitted ──logistics──▶ 2 In production | 3 QA pending ; 2 ──▶ 3 ; 3 ──QA verified + dispatch docs──▶ 4 Dispatched
'use strict';
const { query, one, tx } = require('./db');
const { SAMPLE, EXPORT_SAMPLE, sampleStatusLabel, exportSampleStatusLabel } = require('./constants');
const masters = require('./masters');
const notify = require('./notify');
const { hasStepPerm } = require('../../middleware/auth');
const { appendRemark, refNo, log, getDocs } = require('./orders');
function fail(msg, code = 400) { const e = new Error(msg); e.status = code; throw e; }
const canS = (actor, key, perm) => hasStepPerm(actor.user, `sales_sample:${key}`, perm);
const canE = (actor, key, perm) => hasStepPerm(actor.user, `sales_export_sample:${key}`, perm);
function addDays(d, n) { const x = new Date(d); x.setDate(x.getDate() + n); return x; }
async function itemsFor(id, kind) {
return (await query(`SELECT * FROM dbo.ZSO_SAMPLE_ITEMS WHERE SAMPLE_ID=? AND KIND=? AND ACTIVE=1 ORDER BY ID`, [parseInt(id), kind]))
.map(r => ({ id: r.ID, productId: r.PRODUCT_ID, itemCode: r.ITEM_CODE, displayCode: r.DISPLAY_CODE || r.ITEM_CODE, description: r.DESCRIPTION || '',
qty: Number(r.QTY), nop: r.NOP, batchNo: r.BATCH_NO || '' }));
}
async function priceSampleLines(lines, divisionId, catalog) {
if (!Array.isArray(lines) || !lines.length) fail('Please add at least one product');
const out = [];
for (const [i, l] of lines.entries()) {
const qty = Number(l.qty);
if (!(qty > 0)) fail(`Line ${i + 1}: quantity must be greater than 0`);
const p = await masters.resolveProduct(l.key, divisionId, catalog);
if (!p) fail(`Line ${i + 1}: product not found`);
out.push({ productId: p.id, itemCode: p.itemCode, displayCode: p.displayCode, description: p.description, qty, nop: Math.max(1, Math.ceil(qty / (p.packSize || 1))) });
}
return out;
}
async function writeItems(id, kind, items, r) {
await query(`UPDATE dbo.ZSO_SAMPLE_ITEMS SET ACTIVE=0 WHERE SAMPLE_ID=? AND KIND=?`, [id, kind], r);
for (const it of items) {
await query(`INSERT INTO dbo.ZSO_SAMPLE_ITEMS (SAMPLE_ID,KIND,PRODUCT_ID,ITEM_CODE,DISPLAY_CODE,DESCRIPTION,QTY,NOP) VALUES (?,?,?,?,?,?,?,?)`,
[id, kind, it.productId || null, it.itemCode, it.displayCode, it.description, it.qty, it.nop], r);
}
}
// ── Domestic ────────────────────────────────────────────────────────────────
function sFromRow(r) {
return { id: r.ID, refNo: r.REF_NO, cardCode: r.CARD_CODE, cardName: r.CARD_NAME, address: r.ADDRESS || '', bbLicenceNo: r.BB_LICENCE_NO || '', bbName: r.BB_NAME || '',
divisionId: r.DIVISION_ID, reqDate: r.REQ_DATE, deliveryDays: r.DELIVERY_DAYS, status: r.STATUS, statusLabel: sampleStatusLabel(r.STATUS),
internalRemarks: r.INTERNAL_REMARKS || '', modificationRemarks: r.MODIFICATION_REMARKS || '', rejectionRemarks: r.REJECTION_REMARKS || '',
stockAvailable: r.STOCK_AVAILABLE == null ? null : !!r.STOCK_AVAILABLE, qaDueDate: r.QA_DUE_DATE, qaVerified: r.QA_VERIFIED == null ? null : !!r.QA_VERIFIED,
challanNo: r.CHALLAN_NO || '', challanDate: r.CHALLAN_DATE, courierNo: r.COURIER_NO || '', dispatchInfo: r.DISPATCH_INFO || '', feedback: r.FEEDBACK || '',
createdBy: r.CREATED_BY, createdByName: r.CREATED_BY_NAME || '', createdAt: r.CREATED_AT, updatedAt: r.UPDATED_AT, updatedByName: r.UPDATED_BY_NAME || '', legacy: !!r.LEGACY };
}
async function sampleScope(actor) {
const prof = await masters.getProfile(actor.user);
return prof;
}
async function listSamples(actor, f = {}) {
if (!canS(actor, 'view', 'view')) fail('You are not assigned "view" on Sample Requests', 403);
const prof = await sampleScope(actor);
if (!prof.divisions.length) return [];
const w = [`DIVISION_ID IN (${prof.divisions.map(Number).join(',')})`]; const p = [];
// A Sales Manager sees only the requests they raised (msale behaviour).
if (prof.scope !== 'all') { w.push('CREATED_BY=?'); p.push(parseInt(actor.user.id)); }
if (f.statuses) { const st = String(f.statuses).split(',').map(Number).filter(Boolean); if (st.length) w.push(`STATUS IN (${st.join(',')})`); }
if (f.search) { w.push('(REF_NO LIKE ? OR CARD_NAME LIKE ? OR CARD_CODE LIKE ?)'); const s = `%${f.search}%`; p.push(s, s, s); }
return (await query(`SELECT TOP 2000 * FROM dbo.ZSO_SAMPLES WHERE ${w.join(' AND ')} ORDER BY ID DESC`, p)).map(sFromRow);
}
async function getSample(actor, id) {
const r = await one(`SELECT * FROM dbo.ZSO_SAMPLES WHERE ID=?`, [parseInt(id)]);
if (!r || !canS(actor, 'view', 'view')) fail('Sample request not found', 404);
const prof = await sampleScope(actor);
if (!prof.divisions.includes(Number(r.DIVISION_ID)) || (prof.scope !== 'all' && r.CREATED_BY !== parseInt(actor.user.id))) fail('Sample request not found', 404);
const s = sFromRow(r);
s.items = await itemsFor(s.id, 'domestic');
s.log = (await query(`SELECT * FROM dbo.ZSO_LOG WHERE ENTITY='sample' AND ENTITY_ID=? ORDER BY ID`, [s.id])).map(l => ({ action: l.ACTION, fromStatus: l.FROM_STATUS, toStatus: l.TO_STATUS, remarks: l.REMARKS || '', actorName: l.ACTOR_NAME || '', at: l.AT }));
s.actions = sampleActions(actor, s);
return s;
}
function sampleActions(actor, s) {
const a = [];
if ([SAMPLE.SUBMITTED, SAMPLE.RESUBMITTED].includes(s.status) && canS(actor, 'buh_review', 'approve')) a.push('buh_approve', 'buh_modify', 'buh_reject');
if (s.status === SAMPLE.MODIFICATION && canS(actor, 'create', 'edit') && s.createdBy === parseInt(actor.user.id)) a.push('resubmit');
if (s.status === SAMPLE.APPROVED && canS(actor, 'logistics', 'approve')) a.push('stock');
if (s.status === SAMPLE.IN_PRODUCTION && canS(actor, 'logistics', 'approve')) a.push('stock');
if (s.status === SAMPLE.QA_PENDING && canS(actor, 'logistics', 'approve')) a.push('dispatch', 'qa_extend');
if (s.status === SAMPLE.SHIPPED && canS(actor, 'feedback', 'approve')) a.push('feedback');
return a;
}
async function createSample(actor, d) {
if (!canS(actor, 'create', 'add')) fail('You are not assigned "add" on sales_sample:create', 403);
const prof = await masters.getProfile(actor.user);
const divisionId = parseInt(d.divisionId);
if (!prof.divisions.includes(divisionId)) fail('This product category is not in your divisions', 403);
const div = await masters.getDivision(divisionId);
if (!div || !div.active) fail('This product category is disabled');
if (!d.cardCode) fail('Customer is required');
const items = await priceSampleLines(d.lines, divisionId, 'domestic');
const id = await tx(async (r) => {
const row = await one(`INSERT INTO dbo.ZSO_SAMPLES (CARD_CODE,CARD_NAME,ADDRESS,BB_LICENCE_NO,BB_NAME,DIVISION_ID,REQ_DATE,DELIVERY_DAYS,STATUS,CREATED_BY,CREATED_BY_NAME)
OUTPUT INSERTED.ID, INSERTED.CREATED_AT VALUES (?,?,?,?,?,?,?,?,1,?,?)`,
[d.cardCode, d.cardName || '', d.address || '', d.bbLicenceNo || '', d.bbName || '', divisionId, d.reqDate || new Date(), parseInt(d.deliveryDays) || null,
parseInt(actor.user.id), actor.name], r);
await query(`UPDATE dbo.ZSO_SAMPLES SET REF_NO=? WHERE ID=?`, [refNo(row.ID, row.CREATED_AT), row.ID], r);
await writeItems(row.ID, 'domestic', items, r);
await log('sample', row.ID, 'created', null, 1, d.remarks, actor, r);
return row.ID;
});
notify.sampleEvent('domestic', `Sample request submitted`, [`${actor.name} raised a sample request for ${d.cardName || d.cardCode}.`], 'sales_sample:buh_review', divisionId);
return id;
}
// data per action:
// buh_approve {internalRemarks} · buh_modify {modificationRemarks} · buh_reject {rejectionRemarks}
// resubmit {lines, ...header} · stock {stockAvailable, batchNos:{itemId:batch}, outOfStockDate}
// dispatch {challanNo, challanDate, courierNo, dispatchInfo} · qa_extend {} · feedback {feedback}
async function sampleAct(actor, id, action, d = {}) {
const s = await getSample(actor, id);
if (!s.actions.includes(action)) fail('This action is not available for you at the current status', 403);
let to = s.status; const sets = []; const vals = []; let notifyArgs = null;
switch (action) {
case 'buh_approve': to = SAMPLE.APPROVED; sets.push('INTERNAL_REMARKS=?'); vals.push(appendRemark(s.internalRemarks, actor.name, d.internalRemarks));
notifyArgs = ['Sample request approved', [`Sample ${s.refNo} for ${s.cardName} approved by ${actor.name}.`], 'sales_sample:logistics']; break;
case 'buh_modify': if (!String(d.modificationRemarks || '').trim()) fail('Modification remarks are required');
to = SAMPLE.MODIFICATION; sets.push('MODIFICATION_REMARKS=?'); vals.push(d.modificationRemarks); break;
case 'buh_reject': if (!String(d.rejectionRemarks || '').trim()) fail('Rejection remarks are required');
to = SAMPLE.REJECTED; sets.push('REJECTION_REMARKS=?'); vals.push(d.rejectionRemarks); break;
case 'resubmit': {
const items = await priceSampleLines(d.lines, s.divisionId, 'domestic');
to = SAMPLE.RESUBMITTED;
sets.push('BB_LICENCE_NO=?', 'BB_NAME=?', 'DELIVERY_DAYS=?', 'ADDRESS=?'); vals.push(d.bbLicenceNo ?? s.bbLicenceNo, d.bbName ?? s.bbName, parseInt(d.deliveryDays) || s.deliveryDays, d.address ?? s.address);
await tx(r => writeItems(s.id, 'domestic', items, r));
notifyArgs = ['Sample request resubmitted', [`Sample ${s.refNo} was modified and resubmitted.`], 'sales_sample:buh_review']; break;
}
case 'stock': {
if (d.stockAvailable === undefined || d.stockAvailable === null || d.stockAvailable === '') fail('Select whether stock is available');
const yes = d.stockAvailable === true || d.stockAvailable === 'true' || d.stockAvailable === 1 || d.stockAvailable === '1';
sets.push('STOCK_AVAILABLE=?'); vals.push(yes);
if (yes) {
const batches = d.batchNos || {};
if (s.items.some(it => !String(batches[it.id] || '').trim())) fail('Enter the batch no. for every item');
to = SAMPLE.QA_PENDING; sets.push('QA_DUE_DATE=?'); vals.push(addDays(new Date(), 2));
await tx(async (r) => { for (const it of s.items) await query(`UPDATE dbo.ZSO_SAMPLE_ITEMS SET BATCH_NO=? WHERE ID=?`, [String(batches[it.id]).trim(), it.id], r); });
notifyArgs = ['Sample — QA verification pending', [`Sample ${s.refNo}: stock available, QA verification due in 2 days.`], null];
} else { to = SAMPLE.IN_PRODUCTION; sets.push('OUT_OF_STOCK_DATE=?'); vals.push(d.outOfStockDate || new Date()); }
break;
}
case 'qa_extend': sets.push('QA_DUE_DATE=?'); vals.push(addDays(new Date(), 2)); break;
case 'dispatch':
if (!String(d.challanNo || '').trim() || !d.challanDate || !String(d.courierNo || '').trim()) fail('Challan no., challan date and courier no. are required');
to = SAMPLE.SHIPPED; sets.push('QA_VERIFIED=1', 'CHALLAN_NO=?', 'CHALLAN_DATE=?', 'COURIER_NO=?', 'DISPATCH_INFO=?'); vals.push(d.challanNo, d.challanDate, d.courierNo, d.dispatchInfo || '');
notifyArgs = ['Sample shipped — feedback awaited', [`Sample ${s.refNo} for ${s.cardName} shipped. Courier: ${d.courierNo}.`], null]; break;
case 'feedback': if (!String(d.feedback || '').trim()) fail('Feedback is required');
to = SAMPLE.COMPLETED; sets.push('FEEDBACK=?'); vals.push(d.feedback); break;
default: fail('Unknown action');
}
await tx(async (r) => {
await query(`UPDATE dbo.ZSO_SAMPLES SET STATUS=?, ${sets.length ? sets.join(', ') + ',' : ''} UPDATED_AT=SYSDATETIME(), UPDATED_BY_NAME=? WHERE ID=?`, [to, ...vals, actor.name, s.id], r);
await log('sample', s.id, action, s.status, to, d.internalRemarks || d.modificationRemarks || d.rejectionRemarks || d.feedback || d.dispatchInfo || '', actor, r);
});
const creator = await one(`SELECT EMAIL FROM dbo.ZCUST_USERS WHERE ID=?`, [s.createdBy]).catch(() => null);
if (notifyArgs) notify.sampleEvent('domestic', notifyArgs[0], notifyArgs[1], notifyArgs[2], s.divisionId, creator && creator.EMAIL ? [creator.EMAIL] : []);
else if (creator && creator.EMAIL) notify.sampleEvent('domestic', `Sample ${s.refNo}: ${sampleStatusLabel(to)}`, [`Sample request ${s.refNo} is now "${sampleStatusLabel(to)}".`], null, s.divisionId, [creator.EMAIL]);
return getSample(actor, s.id);
}
// ── Export ──────────────────────────────────────────────────────────────────
function eFromRow(r) {
return { id: r.ID, refNo: r.REF_NO, region: r.REGION || '', sampleAgainst: r.SAMPLE_AGAINST || '', custName: r.CUST_NAME, country: r.COUNTRY || '',
shippingAddress: r.SHIPPING_ADDRESS || '', contactPerson: r.CONTACT_PERSON || '', contactEmail: r.CONTACT_EMAIL || '', contactNo: r.CONTACT_NO || '',
accountDetails: r.ACCOUNT_DETAILS || '', courierByCustomer: !!r.COURIER_BY_CUSTOMER, divisionId: r.DIVISION_ID, reqDate: r.REQ_DATE, deliveryDate: r.DELIVERY_DATE,
deliveryDays: r.DELIVERY_DAYS, remarksQa: r.REMARKS_QA || '', remarksLogistics: r.REMARKS_LOGISTICS || '', status: r.STATUS, statusLabel: exportSampleStatusLabel(r.STATUS),
stockAvailable: r.STOCK_AVAILABLE == null ? null : !!r.STOCK_AVAILABLE, qaDueDate: r.QA_DUE_DATE, qaVerified: r.QA_VERIFIED == null ? null : !!r.QA_VERIFIED,
challanNo: r.CHALLAN_NO || '', challanDate: r.CHALLAN_DATE, courierNo: r.COURIER_NO || '', dispatchDetails: r.DISPATCH_DETAILS || '',
createdBy: r.CREATED_BY, createdByName: r.CREATED_BY_NAME || '', createdAt: r.CREATED_AT, updatedAt: r.UPDATED_AT, legacy: !!r.LEGACY };
}
async function listExport(actor, f = {}) {
if (!canE(actor, 'view', 'view')) fail('You are not assigned "view" on Export Sample Requests', 403);
const w = ['1=1']; const p = [];
if (f.statuses) { const st = String(f.statuses).split(',').map(Number).filter(Boolean); if (st.length) w.push(`STATUS IN (${st.join(',')})`); }
if (f.search) { w.push('(REF_NO LIKE ? OR CUST_NAME LIKE ? OR COUNTRY LIKE ?)'); const s = `%${f.search}%`; p.push(s, s, s); }
return (await query(`SELECT TOP 2000 * FROM dbo.ZSO_EXPORT_SAMPLES WHERE ${w.join(' AND ')} ORDER BY ID DESC`, p)).map(eFromRow);
}
async function getExport(actor, id) {
if (!canE(actor, 'view', 'view')) fail('Not found', 404);
const r = await one(`SELECT * FROM dbo.ZSO_EXPORT_SAMPLES WHERE ID=?`, [parseInt(id)]);
if (!r) fail('Export sample request not found', 404);
const e = eFromRow(r);
e.items = await itemsFor(e.id, 'export');
e.docs = await getDocs('export_sample', e.id);
e.log = (await query(`SELECT * FROM dbo.ZSO_LOG WHERE ENTITY='export_sample' AND ENTITY_ID=? ORDER BY ID`, [e.id])).map(l => ({ action: l.ACTION, fromStatus: l.FROM_STATUS, toStatus: l.TO_STATUS, remarks: l.REMARKS || '', actorName: l.ACTOR_NAME || '', at: l.AT }));
e.actions = [];
if (canE(actor, 'logistics', 'approve') && [EXPORT_SAMPLE.SUBMITTED, EXPORT_SAMPLE.IN_PRODUCTION].includes(e.status)) e.actions.push('stock');
if (canE(actor, 'logistics', 'approve') && e.status === EXPORT_SAMPLE.QA_PENDING) e.actions.push('dispatch');
if (canE(actor, 'create', 'edit') && e.status === EXPORT_SAMPLE.SUBMITTED) e.actions.push('edit');
return e;
}
const EXPORT_FIELDS = ['region', 'sampleAgainst', 'custName', 'country', 'shippingAddress', 'contactPerson', 'contactEmail', 'contactNo', 'accountDetails'];
async function saveExport(actor, d) {
const editing = !!d.id;
if (!canE(actor, 'create', editing ? 'edit' : 'add')) fail(`You are not assigned "${editing ? 'edit' : 'add'}" on sales_export_sample:create`, 403);
for (const f of ['custName', 'country', 'shippingAddress', 'contactPerson', 'contactEmail', 'contactNo']) if (!String(d[f] || '').trim()) fail(`${f} is required`);
const items = await priceSampleLines(d.lines, null, 'export');
const vals = [...EXPORT_FIELDS.map(f => d[f] || ''), !!d.courierByCustomer, d.reqDate || new Date(), parseInt(d.deliveryDays) || null, d.remarksQa || '', d.remarksLogistics || ''];
return tx(async (r) => {
let id;
if (editing) {
const cur = await one(`SELECT STATUS FROM dbo.ZSO_EXPORT_SAMPLES WHERE ID=?`, [parseInt(d.id)], r);
if (!cur || cur.STATUS !== EXPORT_SAMPLE.SUBMITTED) fail('Only submitted requests can be edited');
await query(`UPDATE dbo.ZSO_EXPORT_SAMPLES SET REGION=?, SAMPLE_AGAINST=?, CUST_NAME=?, COUNTRY=?, SHIPPING_ADDRESS=?, CONTACT_PERSON=?, CONTACT_EMAIL=?, CONTACT_NO=?,
ACCOUNT_DETAILS=?, COURIER_BY_CUSTOMER=?, REQ_DATE=?, DELIVERY_DAYS=?, REMARKS_QA=?, REMARKS_LOGISTICS=?, UPDATED_AT=SYSDATETIME(), UPDATED_BY_NAME=? WHERE ID=?`,
[...vals, actor.name, parseInt(d.id)], r);
id = parseInt(d.id);
await log('export_sample', id, 'edited', 1, 1, '', actor, r);
} else {
const row = await one(`INSERT INTO dbo.ZSO_EXPORT_SAMPLES (REGION,SAMPLE_AGAINST,CUST_NAME,COUNTRY,SHIPPING_ADDRESS,CONTACT_PERSON,CONTACT_EMAIL,CONTACT_NO,ACCOUNT_DETAILS,
COURIER_BY_CUSTOMER,REQ_DATE,DELIVERY_DAYS,REMARKS_QA,REMARKS_LOGISTICS,STATUS,CREATED_BY,CREATED_BY_NAME) OUTPUT INSERTED.ID, INSERTED.CREATED_AT
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?)`, [...vals, parseInt(actor.user.id), actor.name], r);
id = row.ID;
await query(`UPDATE dbo.ZSO_EXPORT_SAMPLES SET REF_NO=? WHERE ID=?`, [refNo(id, row.CREATED_AT), id], r);
await log('export_sample', id, 'created', null, 1, '', actor, r);
}
await writeItems(id, 'export', items, r);
if (!editing) notify.sampleEvent('export', 'Export sample request submitted', [`${actor.name} raised an export sample request for ${d.custName} (${d.country}).`], 'sales_export_sample:logistics', null);
return id;
});
}
// stock {stockAvailable, batchNos, deliveryDate} · dispatch {challanNo, challanDate, courierNo, dispatchDetails}
async function exportAct(actor, id, action, d = {}) {
const e = await getExport(actor, id);
if (!e.actions.includes(action)) fail('This action is not available for you at the current status', 403);
let to = e.status; const sets = []; const vals = [];
if (action === 'stock') {
const yes = d.stockAvailable === true || d.stockAvailable === 'true' || d.stockAvailable === 1 || d.stockAvailable === '1';
sets.push('STOCK_AVAILABLE=?', 'DELIVERY_DATE=?'); vals.push(yes, d.deliveryDate || null);
if (yes) {
const b = d.batchNos || {};
if (e.items.some(it => !String(b[it.id] || '').trim())) fail('Enter the batch no. for every item');
to = EXPORT_SAMPLE.QA_PENDING; sets.push('QA_DUE_DATE=?'); vals.push(addDays(new Date(), 2));
await tx(async (r) => { for (const it of e.items) await query(`UPDATE dbo.ZSO_SAMPLE_ITEMS SET BATCH_NO=? WHERE ID=?`, [String(b[it.id]).trim(), it.id], r); });
} else to = EXPORT_SAMPLE.IN_PRODUCTION;
} else if (action === 'dispatch') {
if (!String(d.courierNo || '').trim()) fail('Courier / AWB no. is required');
to = EXPORT_SAMPLE.DISPATCHED; sets.push('QA_VERIFIED=1', 'CHALLAN_NO=?', 'CHALLAN_DATE=?', 'COURIER_NO=?', 'DISPATCH_DETAILS=?');
vals.push(d.challanNo || '', d.challanDate || null, d.courierNo, d.dispatchDetails || '');
} else fail('Unknown action');
await tx(async (r) => {
await query(`UPDATE dbo.ZSO_EXPORT_SAMPLES SET STATUS=?, ${sets.join(', ')}, UPDATED_AT=SYSDATETIME(), UPDATED_BY_NAME=? WHERE ID=?`, [to, ...vals, actor.name, e.id], r);
await log('export_sample', e.id, action, e.status, to, d.dispatchDetails || '', actor, r);
});
const creator = await one(`SELECT EMAIL FROM dbo.ZCUST_USERS WHERE ID=?`, [e.createdBy]).catch(() => null);
if (creator && creator.EMAIL) notify.sampleEvent('export', `Export sample ${e.refNo}: ${exportSampleStatusLabel(to)}`, [`Export sample ${e.refNo} for ${e.custName} is now "${exportSampleStatusLabel(to)}".`], null, null, [creator.EMAIL]);
return getExport(actor, e.id);
}
module.exports = { listSamples, getSample, createSample, sampleAct, listExport, getExport, saveExport, exportAct };