Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/config/db.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,7 @@ db.InvoiceJob = require('../models/invoicejob.model.js')(sequelize, Sequelize);
db.ProductEntry = require('../models/productentry.model.js')(sequelize, Sequelize);
db.VersionInfo = require('../models/versioninfo.model.js')(sequelize, Sequelize);
db.PurchaseOrderVendor = require('../models/purchaseordervendor.model.js')(sequelize, Sequelize);
db.PurchaseOrderHeader = require('../models/purchaseorderheader.model.js')(sequelize, Sequelize);
db.PurchaseOrderLine = require('../models/purchaseorderline.model.js')(sequelize, Sequelize);

module.exports = db;
67 changes: 67 additions & 0 deletions app/config/openapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,31 @@ const versionInfoSchema = {
},
};

const purchaseOrderHeaderSchema = {
type: 'object',
properties: {
pohId: { type: 'integer', readOnly: true },
pohDate: { type: 'string', format: 'date-time' },
pohReference: { type: 'string' },
pohTerms: { type: 'string' },
pohPovId: { type: 'integer' },
pohArch: { type: 'boolean', readOnly: true },
},
};

const purchaseOrderLineSchema = {
type: 'object',
properties: {
polId: { type: 'integer', readOnly: true },
polpoh: { type: 'integer' },
polItemDesc: { type: 'string' },
polQty: { type: 'number' },
polPrice: { type: 'number' },
polInvtId: { type: 'integer' },
polArch: { type: 'boolean', readOnly: true },
},
};

const purchaseOrderVendorSchema = {
type: 'object',
properties: {
Expand Down Expand Up @@ -245,6 +270,8 @@ const spec = {
ProductEntry: productEntrySchema,
VersionInfo: versionInfoSchema,
PurchaseOrderVendor: purchaseOrderVendorSchema,
PurchaseOrderHeader: purchaseOrderHeaderSchema,
PurchaseOrderLine: purchaseOrderLineSchema,
Error: errorResponse,
},
},
Expand Down Expand Up @@ -714,6 +741,46 @@ const spec = {
responses: { 200: { description: 'OK' }, 400: { description: 'Invalid company id' }, 403: { description: 'Auth failure' } },
},
},
'/v1/purchaseorderheader': {
post: { summary: 'Create a PO header', security: [{ authKey: [] }], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/PurchaseOrderHeader' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Auth failure' } } },
},
'/v1/purchaseorderheader/{id}': {
get: { summary: 'Get one PO header', security: [{ authKey: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }], responses: { 200: { description: 'Found' }, 404: { description: 'Not found' }, 403: { description: 'Auth failure' } } },
patch: { summary: 'Partial update of a PO header', security: [{ authKey: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }], requestBody: { content: { 'application/json': { schema: { $ref: '#/components/schemas/PurchaseOrderHeader' } } } }, responses: { 200: { description: 'Updated' }, 400: { description: 'No updatable fields supplied' }, 404: { description: 'Not found' }, 403: { description: 'Auth failure' } } },
delete: { summary: 'Soft-delete a PO header', security: [{ authKey: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }], responses: { 200: { description: 'Archived' }, 404: { description: 'Not found' }, 403: { description: 'Auth failure' } } },
},
'/v1/purchaseorderheader/byvendor/{id}': {
get: {
summary: 'List PO headers for a vendor (paginated, newest first)',
security: [{ authKey: [] }],
parameters: [
{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } },
{ name: 'limit', in: 'query', schema: { type: 'integer', default: 100, maximum: 500 } },
{ name: 'offset', in: 'query', schema: { type: 'integer', default: 0 } },
],
responses: { 200: { description: 'OK' }, 400: { description: 'Invalid vendor id' }, 403: { description: 'Auth failure' } },
},
},
'/v1/purchaseorderline': {
post: { summary: 'Create a PO line', security: [{ authKey: [] }], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/PurchaseOrderLine' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Auth failure' } } },
},
'/v1/purchaseorderline/{id}': {
get: { summary: 'Get one PO line', security: [{ authKey: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }], responses: { 200: { description: 'Found' }, 404: { description: 'Not found' }, 403: { description: 'Auth failure' } } },
patch: { summary: 'Partial update of a PO line', security: [{ authKey: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }], requestBody: { content: { 'application/json': { schema: { $ref: '#/components/schemas/PurchaseOrderLine' } } } }, responses: { 200: { description: 'Updated' }, 400: { description: 'No updatable fields supplied' }, 404: { description: 'Not found' }, 403: { description: 'Auth failure' } } },
delete: { summary: 'Soft-delete a PO line', security: [{ authKey: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }], responses: { 200: { description: 'Archived' }, 404: { description: 'Not found' }, 403: { description: 'Auth failure' } } },
},
'/v1/purchaseorderline/byheader/{id}': {
get: {
summary: 'List PO lines for a header (paginated)',
security: [{ authKey: [] }],
parameters: [
{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } },
{ name: 'limit', in: 'query', schema: { type: 'integer', default: 100, maximum: 500 } },
{ name: 'offset', in: 'query', schema: { type: 'integer', default: 0 } },
],
responses: { 200: { description: 'OK' }, 400: { description: 'Invalid header id' }, 403: { description: 'Auth failure' } },
},
},
},
};

Expand Down
209 changes: 209 additions & 0 deletions app/controllers/purchaseorderheadercontroller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Aaron K. Clark
"use strict";

/**
* PurchaseOrderHeader controller — vendor-scoped auth via
* pohPovId → PurchaseOrderVendor.povCompId.
*/

const db = require('../config/db.config.js');
const log = require('../config/logger.js');
const auth = require('../middleware/auth.js');
const PurchaseOrderHeader = db.PurchaseOrderHeader;

const IsMaster = auth.isMaster;
const GetCompanyId = auth.getCompanyId;
const GetCompanyIdByPovId = auth.getCompanyIdByPovId;

const ALLOWED_FIELDS_CREATE = ['pohDate', 'pohReference', 'pohTerms', 'pohPovId'];
const ALLOWED_FIELDS_UPDATE = ['pohDate', 'pohReference', 'pohTerms'];

exports.create = async (req, res) => {
const authKey = req.get('authKey');
if (!authKey) {
return res.status(403).json({ message: "Authorization key not sent." });
}

const body = req.body || {};
const payload = {};
for (const f of ALLOWED_FIELDS_CREATE) {
if (body[f] !== undefined) payload[f] = body[f];
}
if (!payload.pohPovId) {
return res.status(400).json({ message: "pohPovId is required." });
}

const isMaster = await IsMaster(authKey);
if (!isMaster) {
const authCompanyId = await GetCompanyId(authKey);
const vendorCompanyId = await GetCompanyIdByPovId(payload.pohPovId);
if (authCompanyId === -1 || vendorCompanyId === -1 || authCompanyId !== vendorCompanyId) {
return res.status(403).json({
message: "Cannot create a purchase order for a vendor in a company you do not belong to.",
});
}
}

payload.pohArch = false;

try {
const created = await PurchaseOrderHeader.create(payload);
return res.status(201).json({ message: "Purchase order header created.", purchaseOrderHeader: created });
} catch (error) {
log.error({ err: error }, 'PurchaseOrderHeader.create failed');
return res.status(500).json({ message: "Error!", error: String(error) });
}
};

exports.getById = async (req, res) => {
const authKey = req.get('authKey');
if (!authKey) {
return res.status(403).json({ message: "Authorization key not sent." });
}

let header;
try {
header = await PurchaseOrderHeader.findByPk(req.params.id);
} catch (error) {
log.error({ err: error }, 'PurchaseOrderHeader.findByPk failed');
return res.status(500).json({ message: "Error!", error: String(error) });
}
if (!header || header.pohArch) {
return res.status(404).json({ message: "Not found." });
}

const isMaster = await IsMaster(authKey);
if (!isMaster) {
const authCompanyId = await GetCompanyId(authKey);
const vendorCompanyId = await GetCompanyIdByPovId(header.pohPovId);
if (authCompanyId === -1 || vendorCompanyId === -1 || authCompanyId !== vendorCompanyId) {
return res.status(403).json({ message: "Invalid Authorization Key." });
}
}
return res.status(200).json({ message: "Found.", purchaseOrderHeader: header });
};

exports.listByVendor = async (req, res) => {
const authKey = req.get('authKey');
if (!authKey) {
return res.status(403).json({ message: "Authorization key not sent." });
}

const targetVendorId = Number(req.params.id);
if (!Number.isInteger(targetVendorId) || targetVendorId <= 0) {
return res.status(400).json({ message: "Invalid vendor id." });
}

const isMaster = await IsMaster(authKey);
if (!isMaster) {
const authCompanyId = await GetCompanyId(authKey);
const vendorCompanyId = await GetCompanyIdByPovId(targetVendorId);
if (authCompanyId === -1 || vendorCompanyId === -1 || authCompanyId !== vendorCompanyId) {
return res.status(403).json({ message: "Invalid Authorization Key." });
}
}

const requestedLimit = parseInt(req.query.limit, 10);
const limit = Number.isInteger(requestedLimit) && requestedLimit > 0
? Math.min(requestedLimit, 500) : 100;
const requestedOffset = parseInt(req.query.offset, 10);
const offset = Number.isInteger(requestedOffset) && requestedOffset >= 0
? requestedOffset : 0;

try {
const { count, rows } = await PurchaseOrderHeader.findAndCountAll({
where: { pohPovId: targetVendorId, pohArch: false },
limit, offset,
order: [['pohDate', 'DESC']],
});
return res.status(200).json({
message: "Successfully retrieved purchase orders for VendorId " + targetVendorId,
count, limit, offset, purchaseOrderHeaders: rows,
});
} catch (error) {
log.error({ err: error }, 'PurchaseOrderHeader.findAndCountAll failed');
return res.status(500).json({ message: "Error!", error: String(error) });
}
};

exports.update = async (req, res) => {
const authKey = req.get('authKey');
if (!authKey) {
return res.status(403).json({ message: "Authorization key not sent." });
}

let header;
try {
header = await PurchaseOrderHeader.findByPk(req.params.id);
} catch (error) {
log.error({ err: error }, 'PurchaseOrderHeader.findByPk failed');
return res.status(500).json({ message: "Error!", error: String(error) });
}
if (!header || header.pohArch) {
return res.status(404).json({ message: "Not found." });
}

const isMaster = await IsMaster(authKey);
if (!isMaster) {
const authCompanyId = await GetCompanyId(authKey);
const vendorCompanyId = await GetCompanyIdByPovId(header.pohPovId);
if (authCompanyId === -1 || vendorCompanyId === -1 || authCompanyId !== vendorCompanyId) {
return res.status(403).json({ message: "Invalid Authorization Key." });
}
}

const body = req.body || {};
const updates = {};
for (const f of ALLOWED_FIELDS_UPDATE) {
if (body[f] !== undefined) updates[f] = body[f];
}
if (Object.keys(updates).length === 0) {
return res.status(400).json({ message: "No updatable fields supplied." });
}

try {
await header.update(updates);
return res.status(200).json({ message: "Updated.", purchaseOrderHeader: header });
} catch (error) {
log.error({ err: error }, 'PurchaseOrderHeader.update failed');
return res.status(500).json({ message: "Error!", error: String(error) });
}
};

exports.remove = async (req, res) => {
const authKey = req.get('authKey');
if (!authKey) {
return res.status(403).json({ message: "Authorization key not sent." });
}

let header;
try {
header = await PurchaseOrderHeader.findByPk(req.params.id);
} catch (error) {
log.error({ err: error }, 'PurchaseOrderHeader.findByPk failed');
return res.status(500).json({ message: "Error!", error: String(error) });
}
if (!header || header.pohArch) {
return res.status(404).json({ message: "Not found." });
}

const isMaster = await IsMaster(authKey);
if (!isMaster) {
const authCompanyId = await GetCompanyId(authKey);
const vendorCompanyId = await GetCompanyIdByPovId(header.pohPovId);
if (authCompanyId === -1 || vendorCompanyId === -1 || authCompanyId !== vendorCompanyId) {
return res.status(403).json({ message: "Invalid Authorization Key." });
}
}

try {
await header.update({ pohArch: true });
return res.status(200).json({ message: "Archived.", id: header.pohId });
} catch (error) {
log.error({ err: error }, 'PurchaseOrderHeader archive failed');
return res.status(500).json({ message: "Error!", error: String(error) });
}
};

exports._internals = { IsMaster, GetCompanyId, GetCompanyIdByPovId };
Loading
Loading