Skip to content

LF-5276: Save and return which crops or animals each expense is linked to models endpoints tests#4169

Merged
kathyavini merged 21 commits into
integrationfrom
LF-5276/Save_and_return_which_crops_or_animals_each_expense_is_linked_to_models_endpoints_tests
May 19, 2026
Merged

LF-5276: Save and return which crops or animals each expense is linked to models endpoints tests#4169
kathyavini merged 21 commits into
integrationfrom
LF-5276/Save_and_return_which_crops_or_animals_each_expense_is_linked_to_models_endpoints_tests

Conversation

@SayakaOno
Copy link
Copy Markdown
Collaborator

@SayakaOno SayakaOno commented May 11, 2026

Description

  • Add farmExpenseAnimalModel and farmExpenseCropVarietyModel
  • Update farmExpense relations
  • Add checkFarmExpense middleware for validation
    • Create crop variety model method cropVarietiesBelongToFarm
    • Extract common utility functions from checkSale
  • Update farmExpenseController to handle allocation modifications
  • Add tests
  • (Irrelevant to the ticket) Update GET response to return [] instead of 404 when there are no expenses
    • Adjust saga

PATCH sample requests:

  • Delete existing animal expense
    { "farm_expense_animal": [] }
    
    or
    { "farm_expense_animal": null }
    
    or add farm_expense_crop_variety
    { "farm_expense_crop_variety": [...] }
    

Jira link: https://lite-farm.atlassian.net/browse/LF-5276

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

  • Passes test case
  • UI components visually reviewed on desktop view
  • UI components visually reviewed on mobile view
  • Other (please explain)

Checklist:

  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • The precommit and linting ran successfully
  • I have added or updated language tags for text that's part of the UI
  • I have ordered translation keys alphabetically (optional: run pnpm i18n to help with this)
  • I have added the GNU General Public License to all new files

SayakaOno and others added 6 commits May 11, 2026 13:24
…relations and cropVarietiesBelongToFarm

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… allocations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use .length check so PATCH can send farm_expense_animal: [] with
farm_expense_crop_variety items to transition between allocation types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the 404 workaround in the saga now that the API consistently
returns 200 with an empty array for farms with no expenses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@SayakaOno SayakaOno self-assigned this May 11, 2026
@SayakaOno SayakaOno added the enhancement New feature or request label May 11, 2026
@SayakaOno SayakaOno changed the title [WIP] LF-5276: Save and return which crops or animals each expense is linked to models endpoints tests LF-5276: Save and return which crops or animals each expense is linked to models endpoints tests May 12, 2026
@SayakaOno SayakaOno marked this pull request as ready for review May 12, 2026 22:03
@SayakaOno SayakaOno requested review from a team as code owners May 12, 2026 22:03
@SayakaOno SayakaOno requested review from kathyavini and removed request for a team May 12, 2026 22:03
@SayakaOno SayakaOno marked this pull request as draft May 13, 2026 16:18
@SayakaOno SayakaOno marked this pull request as ready for review May 13, 2026 17:47
Copy link
Copy Markdown
Collaborator

@kathyavini kathyavini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice! The middleware is very well organized and the controller logic looks great. Thank you for handling and thinking of so many required checks -- again I'm surprised by how much can go wrong when multiple farm entities are involved 😅

In testing in Postman I (accidentally 😬) found two points in the controllers that cause a full API crash; neither is from the new code and hopefully those are the only ones!

Copy link
Copy Markdown
Collaborator

@kathyavini kathyavini May 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not from this PR, but could this be moved inside the try/catch?

I crashed my backend when testing in Postman sending a non-existent expense_type_id; didn't expect that to bring the whole API down 😝

Additionally (for future safety) should baseController.isDeleted also return record?.deleted? Caller is not really set up to handle that case though (not deleted but not found)...

const isAddingAnimalExpense = !!farm_expense_animal?.length;
const isAddingCropVarietyExpense = !!farm_expense_crop_variety?.length;

if ((isNewExpense || !newValue) && !isAddingAnimalExpense && !isAddingCropVarietyExpense) {
Copy link
Copy Markdown
Collaborator

@kathyavini kathyavini May 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since values can be 0, I think it will have to be newValue === undefined rather than !newValue to avoid early return on checking the allocations for a value that's been updated to 0.


interface AnimalExpenseItem {
id?: number;
farm_expense_id: number;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

farm_expense_id is a uuid, but it looks like it's number in three of these types (and string in the 4th!)


interface CropVarietyExpenseItem {
farm_expense_id: number;
crop_variety_id: string | null;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the crop_variety_id ever be null?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No 😅 Thank you! 🙏

@@ -30,7 +32,9 @@ const farmExpenseController = {
Copy link
Copy Markdown
Collaborator

@kathyavini kathyavini May 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not this PR, but this caused a server crash in my testing! This needs a return before the res.status or sending an object will crash with a

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

I think the controller definitely needs fixing there anyway, but I wonder why Claude went with a more permissive pattern in the middleware that allows non-arrays, do you know? 🤔

// checkFarmExpense.ts
const expenses = Array.isArray(body) ? body : [body];

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request body is an array in POST, but an object in PATCH.
I initially wondered that too 😂

const isAddingCropVarietyExpense = !!farm_expense_crop_variety?.length;
const isAddingAnimalExpense = !!farm_expense_animal?.length;

if (farm_expense_crop_variety !== undefined || isAddingAnimalExpense) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand the conditionals at first glance, but this is very clever! ❤️


try {
({ animalIds, batchIds } = getUniqueAnimalAndBatchIdsFromAnimalSale(animal_sale));
({ animalIds, batchIds } = getUniqueAnimalAndBatchIds(animal_sale));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just for the error message, but looks like it needs the new 2nd parameter entityName here too

return res.status(400).send('value is required when creating a farm expense');
}

if (farm_expense_animal?.length) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think an array check will be needed here as well. insertGraph is happy to accept a single object as well as array, but since length is falsy the check won't run. So sending an object gets around the farm ownership check:

"farm_expense_animal": {
  "animal_id": 45, // another farm's animal ID
  "allocated_value": 150
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohhh thank you!

});

test('Returns 400 if value is missing', async () => {
const { value: _value, ...expenseWithoutValue } = mocks.fakeExpense({
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you get an unused variable linting error here? For me it's fine even without the rename to _value 🤔

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI did it, so I didn't check, but I'm not getting an error either. I guess I would if it were TypeScript though 🤔

Copy link
Copy Markdown
Collaborator Author

@SayakaOno SayakaOno left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for catching all those errors!! I think I’ve addressed them all.
Could you please re-review? Thank you 🙏

@@ -30,7 +32,9 @@ const farmExpenseController = {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request body is an array in POST, but an object in PATCH.
I initially wondered that too 😂


interface CropVarietyExpenseItem {
farm_expense_id: number;
crop_variety_id: string | null;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No 😅 Thank you! 🙏

return res.status(400).send('value is required when creating a farm expense');
}

if (farm_expense_animal?.length) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohhh thank you!

});

test('Returns 400 if value is missing', async () => {
const { value: _value, ...expenseWithoutValue } = mocks.fakeExpense({
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI did it, so I didn't check, but I'm not getting an error either. I guess I would if it were TypeScript though 🤔

Copy link
Copy Markdown
Collaborator

@kathyavini kathyavini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

@kathyavini kathyavini added this pull request to the merge queue May 19, 2026
Merged via the queue into integration with commit 633c0a8 May 19, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants