Release v3.2.0 fulfillment and warranty tracking

This commit is contained in:
mfwadejr
2026-08-30 15:47:18 -04:00
committed by GitHub
parent 0ff468cdfd
commit 574e1244bf
5 changed files with 62 additions and 12 deletions
+26 -8
View File
@@ -11,7 +11,8 @@ const SESSION_IDLE_MS=12*60*60*1000,sessions=new Map(),loginFailures=new Map();
mkdirSync(dataDir,{recursive:true});mkdirSync(backupDir,{recursive:true});
let db=new DatabaseSync(databasePath);
const legacyProductSchema=()=>String(db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='products'").get()?.sql||"").includes("CHECK(model IN");
if(legacyProductSchema())await backup(db,join(backupDir,`pre-model-catalog-${new Date().toISOString().replace(/[:.]/g,"-")}.db`));
const needsSaleMigration=()=>{const exists=db.prepare("SELECT 1 found FROM sqlite_master WHERE type='table' AND name='products'").get();return exists&&!db.prepare("PRAGMA table_info(products)").all().some(c=>c.name==="fulfillment_method")};
if(legacyProductSchema()||needsSaleMigration())await backup(db,join(backupDir,`pre-v3-2-upgrade-${new Date().toISOString().replace(/[:.]/g,"-")}.db`));
function initializeDatabase(){
db.exec(`
@@ -23,18 +24,24 @@ function initializeDatabase(){
CREATE TABLE IF NOT EXISTS audit_log (id INTEGER PRIMARY KEY AUTOINCREMENT,user_id TEXT,username TEXT NOT NULL DEFAULT 'system',action TEXT NOT NULL,target TEXT NOT NULL DEFAULT '',details TEXT NOT NULL DEFAULT '',ip_address TEXT NOT NULL DEFAULT '',created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);
CREATE INDEX IF NOT EXISTS idx_products_status ON products(status); CREATE UNIQUE INDEX IF NOT EXISTS idx_products_uid ON products(uid) WHERE uid!=''; CREATE UNIQUE INDEX IF NOT EXISTS idx_products_sn ON products(sn) WHERE sn!=''; CREATE UNIQUE INDEX IF NOT EXISTS idx_products_mac ON products(mac) WHERE mac!=''; CREATE INDEX IF NOT EXISTS idx_customer_notes_customer_id ON customer_notes(customer_id); CREATE INDEX IF NOT EXISTS idx_audit_created_at ON audit_log(created_at DESC);`);
const cols=new Set(db.prepare("PRAGMA table_info(products)").all().map(c=>c.name));
for(const [name,definition] of [["customer_id","TEXT"],["ship_address1","TEXT NOT NULL DEFAULT ''"],["ship_address2","TEXT NOT NULL DEFAULT ''"],["ship_city","TEXT NOT NULL DEFAULT ''"],["ship_state","TEXT NOT NULL DEFAULT ''"],["ship_zip","TEXT NOT NULL DEFAULT ''"],["shipping_notes","TEXT NOT NULL DEFAULT ''"],["payment_method","TEXT NOT NULL DEFAULT ''"],["payment_reference","TEXT NOT NULL DEFAULT ''"],["sale_notes","TEXT NOT NULL DEFAULT ''"]])if(!cols.has(name))db.exec(`ALTER TABLE products ADD COLUMN ${name} ${definition}`);
for(const [name,definition] of [["customer_id","TEXT"],["ship_address1","TEXT NOT NULL DEFAULT ''"],["ship_address2","TEXT NOT NULL DEFAULT ''"],["ship_city","TEXT NOT NULL DEFAULT ''"],["ship_state","TEXT NOT NULL DEFAULT ''"],["ship_zip","TEXT NOT NULL DEFAULT ''"],["shipping_notes","TEXT NOT NULL DEFAULT ''"],["payment_method","TEXT NOT NULL DEFAULT ''"],["payment_reference","TEXT NOT NULL DEFAULT ''"],["sale_notes","TEXT NOT NULL DEFAULT ''"],["fulfillment_method","TEXT NOT NULL DEFAULT ''"],["fulfillment_name","TEXT NOT NULL DEFAULT ''"],["carrier","TEXT NOT NULL DEFAULT ''"],["tracking_number","TEXT NOT NULL DEFAULT ''"],["delivery_status","TEXT NOT NULL DEFAULT ''"],["delivery_status_updated_at","TEXT"],["warranty_preset_id","TEXT"],["warranty_name","TEXT NOT NULL DEFAULT ''"],["warranty_end_date","TEXT"]])if(!cols.has(name))db.exec(`ALTER TABLE products ADD COLUMN ${name} ${definition}`);
if(legacyProductSchema()){
db.exec("PRAGMA foreign_keys=OFF; BEGIN IMMEDIATE");
try{db.exec(`CREATE TABLE products_model_migration (id TEXT PRIMARY KEY,uid TEXT NOT NULL DEFAULT '',sn TEXT NOT NULL DEFAULT '',mac TEXT NOT NULL DEFAULT '',manufacturer TEXT NOT NULL DEFAULT 'vSeeBox' CHECK(manufacturer='vSeeBox'),model TEXT NOT NULL,condition TEXT NOT NULL CHECK(condition IN ('New','Used','Refurbished')),received_at TEXT NOT NULL,cost REAL NOT NULL DEFAULT 0,notes TEXT NOT NULL DEFAULT '',status TEXT NOT NULL DEFAULT 'available' CHECK(status IN ('available','sold')),sold_at TEXT,customer_name TEXT,phone TEXT,sale_price REAL,customer_id TEXT,ship_address1 TEXT NOT NULL DEFAULT '',ship_address2 TEXT NOT NULL DEFAULT '',ship_city TEXT NOT NULL DEFAULT '',ship_state TEXT NOT NULL DEFAULT '',ship_zip TEXT NOT NULL DEFAULT '',shipping_notes TEXT NOT NULL DEFAULT '',payment_method TEXT NOT NULL DEFAULT '',payment_reference TEXT NOT NULL DEFAULT '',sale_notes TEXT NOT NULL DEFAULT '');
INSERT INTO products_model_migration SELECT id,uid,sn,mac,manufacturer,model,condition,received_at,cost,notes,status,sold_at,customer_name,phone,sale_price,customer_id,ship_address1,ship_address2,ship_city,ship_state,ship_zip,shipping_notes,payment_method,payment_reference,sale_notes FROM products;
DROP TABLE products; ALTER TABLE products_model_migration RENAME TO products; COMMIT;`)}catch(error){db.exec("ROLLBACK");throw error}finally{db.exec("PRAGMA foreign_keys=ON")}
}
}
const finalCols=new Set(db.prepare("PRAGMA table_info(products)").all().map(c=>c.name));
for(const [name,definition] of [["fulfillment_method","TEXT NOT NULL DEFAULT ''"],["fulfillment_name","TEXT NOT NULL DEFAULT ''"],["carrier","TEXT NOT NULL DEFAULT ''"],["tracking_number","TEXT NOT NULL DEFAULT ''"],["delivery_status","TEXT NOT NULL DEFAULT ''"],["delivery_status_updated_at","TEXT"],["warranty_preset_id","TEXT"],["warranty_name","TEXT NOT NULL DEFAULT ''"],["warranty_end_date","TEXT"]])if(!finalCols.has(name))db.exec(`ALTER TABLE products ADD COLUMN ${name} ${definition}`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_products_status ON products(status); CREATE UNIQUE INDEX IF NOT EXISTS idx_products_uid ON products(uid) WHERE uid!=''; CREATE UNIQUE INDEX IF NOT EXISTS idx_products_sn ON products(sn) WHERE sn!=''; CREATE UNIQUE INDEX IF NOT EXISTS idx_products_mac ON products(mac) WHERE mac!=''; CREATE INDEX IF NOT EXISTS idx_products_customer_id ON products(customer_id);
CREATE TABLE IF NOT EXISTS product_models (id TEXT PRIMARY KEY,name TEXT NOT NULL COLLATE NOCASE UNIQUE,active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);`);
CREATE TABLE IF NOT EXISTS product_models (id TEXT PRIMARY KEY,name TEXT NOT NULL COLLATE NOCASE UNIQUE,active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);
CREATE TABLE IF NOT EXISTS warranty_presets (id TEXT PRIMARY KEY,name TEXT NOT NULL COLLATE NOCASE UNIQUE,duration_value INTEGER NOT NULL DEFAULT 0 CHECK(duration_value>=0),duration_unit TEXT NOT NULL DEFAULT 'days' CHECK(duration_unit IN ('days','months','years')),active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),is_default INTEGER NOT NULL DEFAULT 0 CHECK(is_default IN (0,1)),created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);`);
const addModel=db.prepare("INSERT OR IGNORE INTO product_models (id,name) VALUES (?,?)");
for(const row of db.prepare("SELECT DISTINCT model FROM products WHERE trim(model)!=''").all())addModel.run(crypto.randomUUID(),row.model);
for(const name of ["V3 Plus","V5 Pro","V6 Plus","V6 Pro"])addModel.run(crypto.randomUUID(),name);
const addWarranty=db.prepare("INSERT OR IGNORE INTO warranty_presets (id,name,duration_value,duration_unit,is_default) VALUES (?,?,?,?,?)");
for(const [name,value,unit,isDefault] of [["No Warranty",0,"days",1],["30 Days",30,"days",0],["60 Days",60,"days",0],["90 Days",90,"days",0],["1 Year",1,"years",0]])addWarranty.run(crypto.randomUUID(),name,value,unit,isDefault);
db.prepare("UPDATE products SET fulfillment_method='Shipped' WHERE fulfillment_method='' AND status='sold' AND (ship_address1!='' OR ship_city!='' OR ship_zip!='')").run();
}
const hashPassword=password=>{const salt=randomBytes(16).toString("hex");return `scrypt$${salt}$${scryptSync(password,salt,64).toString("hex")}`};
function verifyPassword(password,stored){try{const[kind,salt,hash]=stored.split("$");if(kind!=="scrypt")return false;const actual=scryptSync(password,salt,64),expected=Buffer.from(hash,"hex");return actual.length===expected.length&&timingSafeEqual(actual,expected)}catch{return false}}
@@ -48,7 +55,7 @@ for(const old of db.prepare("SELECT DISTINCT customer_name name,phone FROM produ
db.exec("PRAGMA optimize");
if(process.argv[2]==="reset-admin"){const username=cleanUsername(process.argv[3]||"admin"),password=validPassword(process.argv[4]||process.env.RESET_ADMIN_PASSWORD||""),existing=db.prepare("SELECT id FROM users WHERE username=? COLLATE NOCASE").get(username);if(existing)db.prepare("UPDATE users SET password_hash=?,role='admin',enabled=1,must_change_password=1,updated_at=CURRENT_TIMESTAMP WHERE id=?").run(hashPassword(password),existing.id);else db.prepare("INSERT INTO users (id,username,password_hash,role,enabled,must_change_password) VALUES (?,?,?,'admin',1,1)").run(crypto.randomUUID(),username,hashPassword(password));db.prepare("INSERT INTO audit_log (username,action,target,details) VALUES ('system','emergency_admin_reset',?,'Console reset; password change required')").run(username);console.log(`Administrator ${username} reset. Password change required at next login.`);db.close();process.exit(0)}
const columns="id,uid,sn,mac,manufacturer,model,condition,received_at AS receivedAt,cost,notes,status,sold_at AS soldAt,customer_id AS customerId,customer_name AS customerName,phone,sale_price AS salePrice,ship_address1 AS shipAddress1,ship_address2 AS shipAddress2,ship_city AS shipCity,ship_state AS shipState,ship_zip AS shipZip,shipping_notes AS shippingNotes,payment_method AS paymentMethod,payment_reference AS paymentReference,sale_notes AS saleNotes";
const columns="id,uid,sn,mac,manufacturer,model,condition,received_at AS receivedAt,cost,notes,status,sold_at AS soldAt,customer_id AS customerId,customer_name AS customerName,phone,sale_price AS salePrice,ship_address1 AS shipAddress1,ship_address2 AS shipAddress2,ship_city AS shipCity,ship_state AS shipState,ship_zip AS shipZip,shipping_notes AS fulfillmentNotes,shipping_notes AS shippingNotes,payment_method AS paymentMethod,payment_reference AS paymentReference,sale_notes AS saleNotes,fulfillment_method AS fulfillmentMethod,fulfillment_name AS fulfillmentName,carrier,tracking_number AS trackingNumber,delivery_status AS deliveryStatus,delivery_status_updated_at AS deliveryStatusUpdatedAt,warranty_preset_id AS warrantyPresetId,warranty_name AS warrantyName,warranty_end_date AS warrantyEndDate";
const allowedConditions=new Set(["New","Used","Refurbished"]);
const getProduct=()=>db.prepare(`SELECT ${columns} FROM products WHERE id=?`),listProducts=()=>db.prepare(`SELECT ${columns} FROM products ORDER BY CASE WHEN status='available' THEN received_at ELSE sold_at END DESC,rowid DESC`);
function json(res,status,value,headers={}){const b=JSON.stringify(value);res.writeHead(status,{"content-type":"application/json","content-length":Buffer.byteLength(b),...headers});res.end(b)}
@@ -73,6 +80,12 @@ function cleanModelName(value){const name=String(value||"").trim().replace(/\s+/
function productInput(v){const requested=String(v.model||"").trim(),modelRecord=db.prepare("SELECT name FROM product_models WHERE name=? COLLATE NOCASE AND active=1").get(requested),condition=String(v.condition||"");if(!modelRecord||!allowedConditions.has(condition)||!v.receivedAt)throw new Error("An active model, condition, and received date are required.");return{uid:String(v.uid||"").trim(),sn:String(v.sn||"").trim(),mac:String(v.mac||"").trim(),model:modelRecord.name,condition,receivedAt:String(v.receivedAt),cost:Number(v.cost)||0,notes:String(v.notes||"").trim()}}
function modelUsage(id){return db.prepare("SELECT m.id,m.name,m.active,COUNT(p.id) totalCount,COALESCE(SUM(CASE WHEN p.status='available' THEN 1 ELSE 0 END),0) availableCount,COALESCE(SUM(CASE WHEN p.status='sold' THEN 1 ELSE 0 END),0) soldCount FROM product_models m LEFT JOIN products p ON p.model=m.name COLLATE NOCASE WHERE m.id=? GROUP BY m.id").get(id)}
function allModels(){return db.prepare("SELECT m.id,m.name,m.active,m.created_at AS createdAt,m.updated_at AS updatedAt,COUNT(p.id) totalCount,COALESCE(SUM(CASE WHEN p.status='available' THEN 1 ELSE 0 END),0) availableCount,COALESCE(SUM(CASE WHEN p.status='sold' THEN 1 ELSE 0 END),0) soldCount FROM product_models m LEFT JOIN products p ON p.model=m.name COLLATE NOCASE GROUP BY m.id ORDER BY lower(m.name)").all().map(x=>({...x,active:Boolean(x.active)}))}
function cleanWarrantyName(value){const name=String(value||"").trim().replace(/\s+/g," ");if(!name||name.length>60)throw new Error("Warranty name must contain 160 characters.");return name}
function warrantyUsage(id){return db.prepare("SELECT w.*,COUNT(p.id) usageCount FROM warranty_presets w LEFT JOIN products p ON p.warranty_preset_id=w.id WHERE w.id=? GROUP BY w.id").get(id)}
function allWarranties(){return db.prepare("SELECT w.id,w.name,w.duration_value AS durationValue,w.duration_unit AS durationUnit,w.active,w.is_default AS isDefault,COUNT(p.id) usageCount FROM warranty_presets w LEFT JOIN products p ON p.warranty_preset_id=w.id GROUP BY w.id ORDER BY w.duration_value,w.name").all().map(x=>({...x,active:Boolean(x.active),isDefault:Boolean(x.isDefault)}))}
function warrantyEnd(start,preset){if(!preset||!preset.duration_value)return null;const [y,m,d]=String(start).split("-").map(Number),date=new Date(Date.UTC(y,m-1,d));if(preset.duration_unit==="years")date.setUTCFullYear(date.getUTCFullYear()+preset.duration_value);else if(preset.duration_unit==="months")date.setUTCMonth(date.getUTCMonth()+preset.duration_value);else date.setUTCDate(date.getUTCDate()+preset.duration_value);return date.toISOString().slice(0,10)}
const fulfillmentMethods=new Set(["Shipped","Dropped Off","Installed At","Meet"]),carriers=new Set(["UPS","FedEx","USPS","Other"]),deliveryStatuses=new Set(["Awaiting Tracking","Label Created","In Transit","Out for Delivery","Delivered","Delivery Exception","Returned","Unknown"]);
function saleDetails(v,current={}){const method=String(v.fulfillmentMethod??current.fulfillmentMethod??"").trim();if(!fulfillmentMethods.has(method))throw new Error("Delivery method is required.");const address1=String(v.shipAddress1??current.shipAddress1??"").trim(),address2=String(v.shipAddress2??current.shipAddress2??"").trim(),city=String(v.shipCity??current.shipCity??"").trim(),state=String(v.shipState??current.shipState??"").trim(),zip=String(v.shipZip??current.shipZip??"").trim(),name=String(v.fulfillmentName??current.fulfillmentName??"").trim(),notes=String(v.fulfillmentNotes??current.fulfillmentNotes??"").trim();if(new Set(["Shipped","Installed At"]).has(method)&&(!address1||!city||!state||!zip))throw new Error(`${method} requires a street address, city, state, and ZIP code.`);if(new Set(["Dropped Off","Meet"]).has(method)&&!name&&!address1&&!notes)throw new Error(`${method} requires a venue, address, or fulfillment detail.`);let carrier="",trackingNumber="",deliveryStatus="";if(method==="Shipped"){carrier=String(v.carrier??current.carrier??"").trim();if(!carriers.has(carrier))throw new Error("Carrier is required for shipped products.");trackingNumber=String(v.trackingNumber??current.trackingNumber??"").trim();deliveryStatus=String(v.deliveryStatus??(current.deliveryStatus||(trackingNumber?"Label Created":"Awaiting Tracking")));if(!deliveryStatuses.has(deliveryStatus))throw new Error("Invalid delivery status.")}return{method,name,address1,address2,city,state,zip,notes,carrier,trackingNumber,deliveryStatus}}
async function authApi(req,res,url,session){
if(url.pathname==="/api/auth/login"&&req.method==="POST"){const v=await body(req),username=String(v.username||"").trim(),key=`${clientIp(req)}|${username.toLowerCase()}`,failure=loginFailures.get(key);if(failure&&failure.count>=5&&failure.until>Date.now())throw Object.assign(new Error("Too many failed attempts. Try again in 15 minutes."),{status:429});const user=db.prepare("SELECT id,username,password_hash,role,enabled,must_change_password AS mustChangePassword FROM users WHERE username=? COLLATE NOCASE").get(username);if(!user?.enabled||!verifyPassword(String(v.password||""),user.password_hash)){loginFailures.set(key,{count:(failure?.count||0)+1,until:Date.now()+15*60*1000});audit(req,user,"login_failed",username);throw Object.assign(new Error("Invalid username or password."),{status:401})}loginFailures.delete(key);const token=randomBytes(32).toString("base64url");sessions.set(tokenKey(token),{userId:user.id,lastSeen:Date.now()});db.prepare("UPDATE users SET last_login_at=CURRENT_TIMESTAMP WHERE id=?").run(user.id);audit(req,user,"login_success");return json(res,200,{username:user.username,role:user.role,mustChangePassword:Boolean(user.mustChangePassword)},{"set-cookie":sessionCookie(req,token)})}
@@ -84,6 +97,10 @@ async function authApi(req,res,url,session){
async function adminApi(req,res,url,session){
const user=session.user;
if(url.pathname==="/api/admin/warranties"&&req.method==="GET")return json(res,200,allWarranties());
if(url.pathname==="/api/admin/warranties"&&req.method==="POST"){const v=await body(req),name=cleanWarrantyName(v.name),durationValue=Number(v.durationValue),durationUnit=String(v.durationUnit||"days");if(!Number.isInteger(durationValue)||durationValue<0||durationValue>3650||!new Set(["days","months","years"]).has(durationUnit))throw new Error("Enter a valid warranty duration.");const id=crypto.randomUUID();db.prepare("INSERT INTO warranty_presets (id,name,duration_value,duration_unit) VALUES (?,?,?,?)").run(id,name,durationValue,durationUnit);audit(req,user,"warranty_created",name,`${durationValue} ${durationUnit}`);return json(res,201,{id,name,durationValue,durationUnit,active:true,isDefault:false,usageCount:0})}
const warrantyMatch=url.pathname.match(/^\/api\/admin\/warranties\/([^/]+)$/);
if(warrantyMatch){const id=decodeURIComponent(warrantyMatch[1]),target=warrantyUsage(id);if(!target)return json(res,404,{error:"Warranty period not found"});if(req.method==="PATCH"){const v=await body(req);if(Object.hasOwn(v,"isDefault")&&v.isDefault){db.exec("UPDATE warranty_presets SET is_default=0");db.prepare("UPDATE warranty_presets SET is_default=1,active=1,updated_at=CURRENT_TIMESTAMP WHERE id=?").run(id);audit(req,user,"warranty_default_changed",target.name);return json(res,200,{ok:true})}if(Object.hasOwn(v,"active")){const active=Boolean(v.active);if(target.is_default&&!active)throw new Error("Choose another default warranty before archiving this one.");db.prepare("UPDATE warranty_presets SET active=?,updated_at=CURRENT_TIMESTAMP WHERE id=?").run(active?1:0,id);audit(req,user,active?"warranty_reactivated":"warranty_archived",target.name);return json(res,200,{ok:true})}if(target.usageCount)throw new Error("A warranty period used by a sale cannot be changed. Archive it and create a new period instead.");const name=cleanWarrantyName(v.name??target.name),durationValue=Number(v.durationValue??target.duration_value),durationUnit=String(v.durationUnit??target.duration_unit);if(!Number.isInteger(durationValue)||durationValue<0||durationValue>3650||!new Set(["days","months","years"]).has(durationUnit))throw new Error("Enter a valid warranty duration.");db.prepare("UPDATE warranty_presets SET name=?,duration_value=?,duration_unit=?,updated_at=CURRENT_TIMESTAMP WHERE id=?").run(name,durationValue,durationUnit,id);audit(req,user,"warranty_updated",target.name,`${name}; ${durationValue} ${durationUnit}`);return json(res,200,{ok:true})}if(req.method==="DELETE"){if(target.usageCount)throw new Error("A warranty period used by a sale cannot be deleted. Archive it instead.");if(target.is_default)throw new Error("The default warranty cannot be deleted.");db.prepare("DELETE FROM warranty_presets WHERE id=?").run(id);audit(req,user,"warranty_deleted",target.name);return json(res,204,null)}}
if(url.pathname==="/api/admin/models"&&req.method==="GET")return json(res,200,allModels());
if(url.pathname==="/api/admin/models"&&req.method==="POST"){const v=await body(req),name=cleanModelName(v.name),id=crypto.randomUUID();db.prepare("INSERT INTO product_models (id,name) VALUES (?,?)").run(id,name);audit(req,user,"model_created",name);return json(res,201,{id,name,active:true,totalCount:0,availableCount:0,soldCount:0})}
const modelMatch=url.pathname.match(/^\/api\/admin\/models\/([^/]+)$/);
@@ -110,6 +127,7 @@ async function api(req,res,url){
if(url.pathname.startsWith("/api/admin/")){const handled=await adminApi(req,res,url,session);if(handled!==false)return handled}
const user=session.user;
if(url.pathname==="/api/models"&&req.method==="GET")return json(res,200,db.prepare("SELECT id,name FROM product_models WHERE active=1 ORDER BY lower(name)").all());
if(url.pathname==="/api/warranties"&&req.method==="GET")return json(res,200,db.prepare("SELECT id,name,duration_value AS durationValue,duration_unit AS durationUnit,is_default AS isDefault FROM warranty_presets WHERE active=1 ORDER BY duration_value,name").all().map(x=>({...x,isDefault:Boolean(x.isDefault)})));
if(url.pathname==="/api/products"&&req.method==="GET")return json(res,200,listProducts().all());
if(url.pathname==="/api/products"&&req.method==="POST"){const p=productInput(await body(req)),id=crypto.randomUUID();db.prepare("INSERT INTO products (id,uid,sn,mac,model,condition,received_at,cost,notes) VALUES (?,?,?,?,?,?,?,?,?)").run(id,p.uid,p.sn,p.mac,p.model,p.condition,p.receivedAt,p.cost,p.notes);audit(req,user,"product_received",id,p.model);return json(res,201,getProduct().get(id))}
const customerMatch=url.pathname.match(/^\/api\/customers\/([^/]+)$/);
@@ -118,9 +136,9 @@ async function api(req,res,url){
if(notesMatch){const customerId=decodeURIComponent(notesMatch[1]),noteId=notesMatch[2]?decodeURIComponent(notesMatch[2]):null;if(!db.prepare("SELECT id FROM customers WHERE id=?").get(customerId))return json(res,404,{error:"Customer not found"});if(req.method==="POST"&&!noteId){const v=await body(req),note=String(v.note||"").trim(),category=String(v.category||"General");if(!note)throw new Error("Note text is required.");if(!new Set(["General","Support","Follow-up"]).has(category))throw new Error("Invalid note category.");const id=crypto.randomUUID();db.prepare("INSERT INTO customer_notes (id,customer_id,category,note) VALUES (?,?,?,?)").run(id,customerId,category,note);audit(req,user,"customer_note_added",customerId,category);return json(res,201,{id,category,note})}if(req.method==="DELETE"&&noteId){db.prepare("DELETE FROM customer_notes WHERE id=? AND customer_id=?").run(noteId,customerId);audit(req,user,"customer_note_deleted",customerId,noteId);return json(res,204,null)}}
const match=url.pathname.match(/^\/api\/products\/([^/]+)(?:\/(sell|restock))?$/);if(!match)return json(res,404,{error:"Not found"});const id=decodeURIComponent(match[1]),action=match[2],current=getProduct().get(id);if(!current)return json(res,404,{error:"Product not found"});
if(req.method==="DELETE"&&!action){db.prepare("DELETE FROM products WHERE id=?").run(id);audit(req,user,"record_deleted",id,current.status);return json(res,204,null)}
if(req.method==="PATCH"&&!action){if(current.status!=="sold")return json(res,400,{error:"Transaction notes can only be added to sales."});const v=await body(req);db.prepare("UPDATE products SET sale_notes=? WHERE id=?").run(String(v.saleNotes||"").trim(),id);audit(req,user,"sale_notes_updated",id);return json(res,200,getProduct().get(id))}
if(req.method==="POST"&&action==="sell"){const v=await body(req);if(!String(v.customerName||"").trim()||!v.soldAt)throw new Error("Customer name and sale date are required.");const paymentMethod=String(v.paymentMethod||"").trim();if(!new Set(["Cash","Venmo","PayPal"]).has(paymentMethod))throw new Error("Payment method must be Cash, Venmo, or PayPal.");const name=String(v.customerName).trim(),phone=String(v.phone||"").trim(),address1=String(v.shipAddress1||"").trim(),address2=String(v.shipAddress2||"").trim(),city=String(v.shipCity||"").trim(),state=String(v.shipState||"").trim(),zip=String(v.shipZip||"").trim(),shippingNotes=String(v.shippingNotes||"").trim();let customer=v.customerId?db.prepare("SELECT id FROM customers WHERE id=?").get(String(v.customerId)):findCustomerByName.get(name);if(!customer){customer={id:crypto.randomUUID()};addCustomer.run(customer.id,name,phone,address1,address2,city,state,zip,shippingNotes)}else db.prepare("UPDATE customers SET name=?,phone=?,address1=?,address2=?,city=?,state=?,zip=?,shipping_notes=?,updated_at=CURRENT_TIMESTAMP WHERE id=?").run(name,phone,address1,address2,city,state,zip,shippingNotes,customer.id);db.prepare("UPDATE products SET status='sold',sold_at=?,customer_id=?,customer_name=?,phone=?,sale_price=?,ship_address1=?,ship_address2=?,ship_city=?,ship_state=?,ship_zip=?,shipping_notes=?,payment_method=?,payment_reference=?,sale_notes=? WHERE id=? AND status='available'").run(String(v.soldAt),customer.id,name,phone,Number(v.salePrice)||0,address1,address2,city,state,zip,shippingNotes,paymentMethod,String(v.paymentReference||"").trim(),String(v.saleNotes||"").trim(),id);audit(req,user,"sale_recorded",id,name);return json(res,200,getProduct().get(id))}
if(req.method==="POST"&&action==="restock"){const v=await body(req);if(!allowedConditions.has(v.condition)||!v.receivedAt)throw new Error("Condition and return date are required.");db.prepare("UPDATE products SET status='available',condition=?,received_at=?,sold_at=NULL,customer_id=NULL,customer_name=NULL,phone=NULL,sale_price=NULL,ship_address1='',ship_address2='',ship_city='',ship_state='',ship_zip='',shipping_notes='',payment_method='',payment_reference='',sale_notes='' WHERE id=? AND status='sold'").run(v.condition,String(v.receivedAt),id);audit(req,user,"sale_voided_restocked",id);return json(res,200,getProduct().get(id))}
if(req.method==="PATCH"&&!action){if(current.status!=="sold")return json(res,400,{error:"Only sale records can be updated."});const v=await body(req),fulfillmentKeys=["fulfillmentMethod","fulfillmentName","shipAddress1","shipAddress2","shipCity","shipState","shipZip","fulfillmentNotes","carrier","trackingNumber","deliveryStatus"],hasFulfillmentUpdate=fulfillmentKeys.some(key=>Object.hasOwn(v,key)),f=hasFulfillmentUpdate?saleDetails(v,current):{method:current.fulfillmentMethod||"",name:current.fulfillmentName||"",address1:current.shipAddress1||"",address2:current.shipAddress2||"",city:current.shipCity||"",state:current.shipState||"",zip:current.shipZip||"",notes:current.fulfillmentNotes||current.shippingNotes||"",carrier:current.carrier||"",trackingNumber:current.trackingNumber||"",deliveryStatus:current.deliveryStatus||""};let warrantyPresetId=current.warrantyPresetId,warrantyName=current.warrantyName,warrantyEndDate=current.warrantyEndDate;if(Object.hasOwn(v,"warrantyPresetId")){const selectedId=String(v.warrantyPresetId),preset=db.prepare("SELECT id,name,duration_value,duration_unit,active FROM warranty_presets WHERE id=?").get(selectedId);if(!preset||(!preset.active&&selectedId!==current.warrantyPresetId))throw new Error("Select an active warranty period.");warrantyPresetId=preset.id;warrantyName=preset.name;warrantyEndDate=warrantyEnd(current.soldAt,preset)}db.prepare("UPDATE products SET sale_notes=?,fulfillment_method=?,fulfillment_name=?,ship_address1=?,ship_address2=?,ship_city=?,ship_state=?,ship_zip=?,shipping_notes=?,carrier=?,tracking_number=?,delivery_status=?,delivery_status_updated_at=CURRENT_TIMESTAMP,warranty_preset_id=?,warranty_name=?,warranty_end_date=? WHERE id=?").run(String(v.saleNotes??current.saleNotes??"").trim(),f.method,f.name,f.address1,f.address2,f.city,f.state,f.zip,f.notes,f.carrier,f.trackingNumber,f.deliveryStatus,warrantyPresetId,warrantyName,warrantyEndDate,id);audit(req,user,"sale_details_updated",id,`${f.method||"not recorded"}; ${f.deliveryStatus||"completed"}; ${warrantyName||"not recorded"}`);return json(res,200,getProduct().get(id))}
if(req.method==="POST"&&action==="sell"){const v=await body(req);if(!String(v.customerName||"").trim()||!v.soldAt)throw new Error("Customer name and sale date are required.");const paymentMethod=String(v.paymentMethod||"").trim();if(!new Set(["Cash","Venmo","PayPal"]).has(paymentMethod))throw new Error("Payment method must be Cash, Venmo, or PayPal.");const f=saleDetails(v),preset=v.warrantyPresetId?db.prepare("SELECT id,name,duration_value,duration_unit FROM warranty_presets WHERE id=? AND active=1").get(String(v.warrantyPresetId)):db.prepare("SELECT id,name,duration_value,duration_unit FROM warranty_presets WHERE active=1 AND is_default=1").get();if(!preset)throw new Error("Select an active warranty period.");const name=String(v.customerName).trim(),phone=String(v.phone||"").trim(),storeAddress=new Set(["Shipped","Installed At"]).has(f.method);let customer=v.customerId?db.prepare("SELECT id FROM customers WHERE id=?").get(String(v.customerId)):findCustomerByName.get(name);if(!customer){customer={id:crypto.randomUUID()};addCustomer.run(customer.id,name,phone,storeAddress?f.address1:"",storeAddress?f.address2:"",storeAddress?f.city:"",storeAddress?f.state:"",storeAddress?f.zip:"",storeAddress?f.notes:"")}else if(storeAddress)db.prepare("UPDATE customers SET name=?,phone=?,address1=?,address2=?,city=?,state=?,zip=?,shipping_notes=?,updated_at=CURRENT_TIMESTAMP WHERE id=?").run(name,phone,f.address1,f.address2,f.city,f.state,f.zip,f.notes,customer.id);else db.prepare("UPDATE customers SET name=?,phone=?,updated_at=CURRENT_TIMESTAMP WHERE id=?").run(name,phone,customer.id);db.prepare("UPDATE products SET status='sold',sold_at=?,customer_id=?,customer_name=?,phone=?,sale_price=?,ship_address1=?,ship_address2=?,ship_city=?,ship_state=?,ship_zip=?,shipping_notes=?,payment_method=?,payment_reference=?,sale_notes=?,fulfillment_method=?,fulfillment_name=?,carrier=?,tracking_number=?,delivery_status=?,delivery_status_updated_at=CURRENT_TIMESTAMP,warranty_preset_id=?,warranty_name=?,warranty_end_date=? WHERE id=? AND status='available'").run(String(v.soldAt),customer.id,name,phone,Number(v.salePrice)||0,f.address1,f.address2,f.city,f.state,f.zip,f.notes,paymentMethod,String(v.paymentReference||"").trim(),String(v.saleNotes||"").trim(),f.method,f.name,f.carrier,f.trackingNumber,f.deliveryStatus,preset.id,preset.name,warrantyEnd(String(v.soldAt),preset),id);audit(req,user,"sale_recorded",id,`${name}; ${f.method}; ${preset.name}`);return json(res,200,getProduct().get(id))}
if(req.method==="POST"&&action==="restock"){const v=await body(req);if(!allowedConditions.has(v.condition)||!v.receivedAt)throw new Error("Condition and return date are required.");db.prepare("UPDATE products SET status='available',condition=?,received_at=?,sold_at=NULL,customer_id=NULL,customer_name=NULL,phone=NULL,sale_price=NULL,ship_address1='',ship_address2='',ship_city='',ship_state='',ship_zip='',shipping_notes='',payment_method='',payment_reference='',sale_notes='',fulfillment_method='',fulfillment_name='',carrier='',tracking_number='',delivery_status='',delivery_status_updated_at=NULL,warranty_preset_id=NULL,warranty_name='',warranty_end_date=NULL WHERE id=? AND status='sold'").run(v.condition,String(v.receivedAt),id);audit(req,user,"sale_voided_restocked",id);return json(res,200,getProduct().get(id))}
return json(res,405,{error:"Method not allowed"});
}
const mime={".html":"text/html; charset=utf-8",".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".svg":"image/svg+xml",".png":"image/png",".webmanifest":"application/manifest+json; charset=utf-8"};