Skip to content

Commit

Permalink
Merge pull request #3287 from LiteFarmOrg/LF-4260b-patch-and-delete-p…
Browse files Browse the repository at this point in the history
…roduct

Lf 4260b patch and delete product
  • Loading branch information
kathyavini authored and SayakaOno committed Jul 14, 2024
1 parent 685f111 commit ae158c3
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 68 deletions.
2 changes: 1 addition & 1 deletion packages/api/src/controllers/baseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export default {
return await model
.query(trx)
.context({ user_id: req?.auth?.user_id, ...context })
.upsertGraph(data, { insertMissing: true });
.upsertGraph(removeAdditionalPropertiesWithRelations(model, data), { insertMissing: true });
},

// fetch an object and all of its related objects
Expand Down
120 changes: 86 additions & 34 deletions packages/api/src/middleware/validation/checkProductValidity.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,39 +18,64 @@ import { handleObjectionError } from '../../util/errorCodes.js';
import ProductModel from '../../models/productModel.js';
import { checkAndTrimString } from '../../util/util.js';

const taskProductRelationMap = {
soil_amendment_task: 'soil_amendment_product',
// pest_control_task: 'pest_control_product',
// cleaning_task: 'cleaning_product'
};
const productCheckMap = {
soil_amendment_task: checkSoilAmendmentProduct,
// pest_control_task: checkPestControlProduct,
// cleaning_task: checkCleaningProduct
};

function checkSoilAmendmentProduct(res, sap) {
const elements = [
'n',
'p',
'k',
'calcium',
'magnesium',
'sulfur',
'copper',
'manganese',
'boron',
];
const molecularCompounds = ['ammonium', 'nitrate'];
// Check that element values are all positive
if (!elements.every((element) => !sap[element] || sap[element] >= 0)) {
return res.status(400).send('element values must all be positive');
}

// Check that element values do not exceed 100 if element_unit is percent
if (
sap.elemental_unit === 'percent' &&
elements.reduce((sum, element) => sum + (sap[element] || 0), 0) > 100
) {
return res.status(400).send('percent elemental values must not exceed 100');
}

// Check that compound values are all positive
if (!molecularCompounds.every((compound) => !sap[compound] || sap[compound] >= 0)) {
return res.status(400).send('compounds values must all be positive');
}

if (sap.moisture_content_percent && sap.moisture_content_percent < 0) {
return res.status(400).send('moisture content value must be positive');
}
}

export function checkProductValidity() {
return async (req, res, next) => {
const { soil_amendment_product: sap } = req.body;
const elements = [
'n',
'p',
'k',
'calcium',
'magnesium',
'sulfur',
'copper',
'manganese',
'boron',
];

if (sap) {
// Check that element values are all positive
if (!elements.every((element) => !sap[element] || sap[element] >= 0)) {
return res.status(400).send('element values must all be positive');
}
const { farm_id } = req.headers;
const { product_id } = req.params;
const isCreatingNew = req.method === 'POST';

// Check that a unit has been provided along with element values
if (elements.some((element) => sap[element]) && !sap.elemental_unit) {
return res.status(400).send('elemental_unit is required');
}
let { type, name } = req.body;
const { [taskProductRelationMap[type]]: productDetails } = req.body;

// Check that element values do not exceed 100 if element_unit is percent
if (
sap.elemental_unit === 'percent' &&
elements.reduce((sum, element) => sum + (sap[element] || 0), 0) > 100
) {
return res.status(400).send('percent elemental values must not exceed 100');
}
if (productDetails) {
productCheckMap[type](res, productDetails);
}

// Null empty strings
Expand All @@ -62,18 +87,45 @@ export function checkProductValidity() {

const trx = await transaction.start(ProductModel.knex());

// Check name uniqueness
try {
const { farm_id } = req.headers;
const { product_id } = req.params;
let { type, name } = req.body;

if (product_id) {
const currentRecord = await ProductModel.query(trx).findById(product_id);
// Prevent changing type for now, prevents orphan task type products
if (type && type != currentRecord.type) {
return res.status(400).send('cannot change product type');
}

type = type ?? currentRecord.type;
name = name ?? currentRecord.name;
}

// Prevents error on name uniqeness check
if (isCreatingNew && !name) {
return res.status(400).send('new product must have name');
}

// Prevents error on name uniqeness check
if (isCreatingNew && !type) {
return res.status(400).send('new product must have type');
}

const nonModifiableAssets = Object.values(taskProductRelationMap).filter((productType) => {
return productType !== taskProductRelationMap[type];
});

if (
isCreatingNew &&
taskProductRelationMap[type] &&
!req.body[taskProductRelationMap[type]]
) {
return res.status(400).send('must have product details');
}

if (nonModifiableAssets.some((asset) => Object.hasOwn(req.body, asset))) {
return res.status(400).send('must not have other product type details');
}

// Check name uniqueness
const existingRecord = await ProductModel.query(trx)
.where({ farm_id })
.andWhere({ type })
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/models/productModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class ProductModel extends baseModel {
static get jsonSchema() {
return {
type: 'object',
required: ['name', 'farm_id'],
required: ['name', 'farm_id', 'type'],
properties: {
product_id: { type: 'integer' },
name: { type: 'string' },
Expand Down
23 changes: 22 additions & 1 deletion packages/api/tests/mock.factories.js
Original file line number Diff line number Diff line change
Expand Up @@ -1131,11 +1131,30 @@ function fakeProduct(defaultData = {}) {
name: faker.lorem.words(2),
supplier: faker.lorem.words(3),
on_permitted_substances_list: faker.helpers.arrayElement(['YES', 'NO', 'NOT_SURE']),
type: faker.helpers.arrayElement(['soil_amendment_task', 'pest_control_task', 'cleaning_task']),
// For soil_amendment_task use soil_amendment_productFactory
type: faker.helpers.arrayElement(['pest_control_task', 'cleaning_task']),
...defaultData,
};
}

async function soil_amendment_productFactory({ promisedProduct = productFactory() } = {}) {
const [{ product_id, type }] = await promisedProduct;
const productDetails = fakeProductDetails(type);
return knex('soil_amendment_product')
.insert({ product_id, ...productDetails })
.returning('*');
}

function fakeProductDetails(type, defaultData = {}) {
if (type === 'soil_amendment_task') {
return {
...defaultData,
};
}

return {};
}

async function soil_amendment_methodFactory() {
return knex('soil_amendment_method').insert({ key: faker.lorem.word() }).returning('*');
}
Expand Down Expand Up @@ -2261,6 +2280,8 @@ export default {
fakeHarvestUse,
productFactory,
fakeProduct,
fakeProductDetails,
soil_amendment_productFactory,
soil_amendment_methodFactory,
soil_amendment_purposeFactory,
soil_amendment_fertiliser_typeFactory,
Expand Down
51 changes: 40 additions & 11 deletions packages/api/tests/product.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,26 @@ describe('Product Tests', () => {
}

async function createProductInDatabase(mainFarm, properties) {
const [product] = await mocks.productFactory(
{
promisedFarm: [mainFarm],
},
properties,
);
return product;
if (properties?.type === 'soil_amendment_task') {
const [product] = await mocks.productFactory(
{
promisedFarm: [mainFarm],
},
properties,
);
const [soilAmendmentProduct] = await mocks.soil_amendment_productFactory({
promisedProduct: [product],
});
return soilAmendmentProduct;
} else {
const [product] = await mocks.productFactory(
{
promisedFarm: [mainFarm],
},
properties,
);
return product;
}
}

afterAll(async (done) => {
Expand Down Expand Up @@ -244,12 +257,20 @@ describe('Product Tests', () => {
type: 'soil_amendment_task',
});

const soilAmendmentProductDetails = {
soil_amendment_product: mocks.fakeProductDetails('soil_amendment_task'),
};

await createProductInDatabase(userFarm, fertiliserProductA);

postProductRequest(fertiliserProductA, userFarm, (err, res) => {
expect(res.status).toBe(409);
done();
});
postProductRequest(
{ ...fertiliserProductA, ...soilAmendmentProductDetails },
userFarm,
(err, res) => {
expect(res.status).toBe(409);
done();
},
);
});

test('should successfully populate soil_amendment_product table', async (done) => {
Expand Down Expand Up @@ -364,11 +385,19 @@ describe('Product Tests', () => {

await createProductInDatabase(userFarm, fertiliserProductA);

const soilAmendmentProductDetailsA = {
soil_amendment_product: mocks.fakeProductDetails('soil_amendment_task'),
};

const fertiliserProductB = mocks.fakeProduct({
name: 'Fertiliser Product B',
type: 'soil_amendment_task',
});

const soilAmendmentProductDetailsB = {
soil_amendment_product: mocks.fakeProductDetails('soil_amendment_task'),
};

const origProduct = await createProductInDatabase(userFarm, fertiliserProductB);

const res = await patchRequest(
Expand Down
Loading

0 comments on commit ae158c3

Please sign in to comment.