From f9a6e482515161ce862af03fd71205423b6eba83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 8 Apr 2026 13:32:33 +0200 Subject: [PATCH 01/38] Add option to select fertilizer catalogue application amount unit --- .../blocks/fertilizer/formschema.tsx | 18 + .../app/components/blocks/fertilizer/utils.ts | 2 + .../src/doses/calculate-dose.test.ts | 1 + .../src/doses/get-dose-field.test.ts | 1 + .../2026/filling/stikstofgebruiksnorm.test.ts | 6 + fdm-core/src/catalogues.ts | 14 +- fdm-core/src/cultivation.test.ts | 1 + .../db/migrations/0026_p_app_amount_unit.sql | 3 + .../src/db/migrations/meta/0026_snapshot.json | 3951 +++++++++++++++++ fdm-core/src/db/migrations/meta/_journal.json | 7 + fdm-core/src/db/schema.ts | 9 + fdm-core/src/fertilizer.d.ts | 3 + fdm-core/src/fertilizer.test.ts | 14 +- fdm-core/src/fertilizer.ts | 16 + fdm-core/src/unit-conversion.test.ts | 164 + fdm-core/src/unit-conversion.ts | 108 + fdm-data/src/fertilizers/d.ts | 1 + 17 files changed, 4316 insertions(+), 3 deletions(-) create mode 100644 fdm-core/src/db/migrations/0026_p_app_amount_unit.sql create mode 100644 fdm-core/src/db/migrations/meta/0026_snapshot.json create mode 100644 fdm-core/src/unit-conversion.test.ts create mode 100644 fdm-core/src/unit-conversion.ts diff --git a/fdm-app/app/components/blocks/fertilizer/formschema.tsx b/fdm-app/app/components/blocks/fertilizer/formschema.tsx index 3b99803fa..358e54383 100644 --- a/fdm-app/app/components/blocks/fertilizer/formschema.tsx +++ b/fdm-app/app/components/blocks/fertilizer/formschema.tsx @@ -464,7 +464,25 @@ export const FormSchema = z .refine((value) => value.some((item) => item), { error: "Selecteer minimaal 1 methode", }), + p_app_amount_unit: z + .enum(["kg/ha", "l/ha", "m3/ha", "ton/ha"]) + .default("kg/ha"), }) + .refine( + (data) => { + if ( + data.p_app_amount_unit === "m3/ha" || + data.p_app_amount_unit === "l/ha" + ) { + return data.p_density !== undefined + } + return true + }, + { + path: ["p_density"], + error: "Dichtheid is verplicht bij gebruik van l/ha of m3/ha", + }, + ) .refine( (data) => { if (data.p_n_rt && data.p_n_wc === undefined) { diff --git a/fdm-app/app/components/blocks/fertilizer/utils.ts b/fdm-app/app/components/blocks/fertilizer/utils.ts index 9255c57e4..779094429 100644 --- a/fdm-app/app/components/blocks/fertilizer/utils.ts +++ b/fdm-app/app/components/blocks/fertilizer/utils.ts @@ -61,6 +61,7 @@ export function buildFertilizerDefaults( p_hg_rt: toUndefined(fertilizer.p_hg_rt), p_cl_rt: toUndefined(fertilizer.p_cl_rt), p_app_method_options: fertilizer.p_app_method_options || [], + p_app_amount_unit: fertilizer.p_app_amount_unit || "kg/ha", } } @@ -125,5 +126,6 @@ export function buildCataloguePayload( p_cl_rt: formValues.p_cl_rt, p_ef_nh3: undefined, p_app_method_options: formValues.p_app_method_options, + p_app_amount_unit: formValues.p_app_amount_unit, } } diff --git a/fdm-calculator/src/doses/calculate-dose.test.ts b/fdm-calculator/src/doses/calculate-dose.test.ts index ee6f04c84..439413d96 100644 --- a/fdm-calculator/src/doses/calculate-dose.test.ts +++ b/fdm-calculator/src/doses/calculate-dose.test.ts @@ -53,6 +53,7 @@ const baseFertilizer: Fertilizer = { p_name_en: null, p_description: null, p_app_method_options: null, + p_app_amount_unit: "kg/ha", p_app_amount: null, p_date_acquiring: null, p_picking_date: null, diff --git a/fdm-calculator/src/doses/get-dose-field.test.ts b/fdm-calculator/src/doses/get-dose-field.test.ts index 52817aeae..0de521407 100644 --- a/fdm-calculator/src/doses/get-dose-field.test.ts +++ b/fdm-calculator/src/doses/get-dose-field.test.ts @@ -104,6 +104,7 @@ describe("getDoseForField", () => { p_cl_rt: 0, p_type: "manure", p_app_method_options: undefined, + p_app_amount_unit: "kg/ha", p_no3_rt: undefined, p_nh4_rt: undefined, p_cr_rt: undefined, diff --git a/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts b/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts index 7d845fc4d..234eb8602 100644 --- a/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts +++ b/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts @@ -617,6 +617,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( p_description: null, p_app_method_options: null, p_app_amount: null, + p_app_amount_unit: "kg/ha", p_date_acquiring: null, p_picking_date: null, p_n_if: null, @@ -722,6 +723,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( p_description: null, p_app_method_options: null, p_app_amount: null, + p_app_amount_unit: "kg/ha", p_date_acquiring: null, p_picking_date: null, p_n_if: null, @@ -779,6 +781,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( p_description: null, p_app_method_options: null, p_app_amount: null, + p_app_amount_unit: "kg/ha", p_date_acquiring: null, p_picking_date: null, p_n_if: null, @@ -881,6 +884,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( p_description: null, p_app_method_options: null, p_app_amount: null, + p_app_amount_unit: "kg/ha", p_date_acquiring: null, p_picking_date: null, p_n_if: null, @@ -1011,6 +1015,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( p_description: null, p_app_method_options: null, p_app_amount: null, + p_app_amount_unit: "kg/ha", p_date_acquiring: null, p_picking_date: null, p_n_if: null, @@ -1110,6 +1115,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( p_description: null, p_app_method_options: null, p_app_amount: null, + p_app_amount_unit: "kg/ha", p_date_acquiring: null, p_picking_date: null, p_n_if: null, diff --git a/fdm-core/src/catalogues.ts b/fdm-core/src/catalogues.ts index 476377b8c..3c424aa0e 100644 --- a/fdm-core/src/catalogues.ts +++ b/fdm-core/src/catalogues.ts @@ -11,6 +11,7 @@ import * as schema from "./db/schema" import { handleError } from "./error" import type { FdmType } from "./fdm" import type { FdmServerType } from "./fdm-server.d" +import { suggestUnitFromRvoCode } from "./unit-conversion" /** * Gets all enabled fertilizer catalogues for a farm. @@ -516,9 +517,20 @@ async function syncFertilizerCatalogue(fdm: FdmType) { existing[0].hash === undefined || existing[0].hash !== hash ) { + const values = { + ...item, + p_app_amount_unit: + item.p_app_amount_unit ?? + (item.p_type_rvo + ? suggestUnitFromRvoCode(item.p_type_rvo) + : undefined), + hash: hash, + updated: new Date(), + } + await tx .update(schema.fertilizersCatalogue) - .set({ ...item, hash: hash, updated: new Date() }) + .set(values) .where( eq( schema.fertilizersCatalogue.p_id_catalogue, diff --git a/fdm-core/src/cultivation.test.ts b/fdm-core/src/cultivation.test.ts index f7994c2f4..521a33020 100644 --- a/fdm-core/src/cultivation.test.ts +++ b/fdm-core/src/cultivation.test.ts @@ -1582,6 +1582,7 @@ describe("Cultivation Data Model", () => { b_id_farm, { p_app_method_options: null, + p_app_amount_unit: undefined, p_name_nl, p_name_en, p_description, diff --git a/fdm-core/src/db/migrations/0026_p_app_amount_unit.sql b/fdm-core/src/db/migrations/0026_p_app_amount_unit.sql new file mode 100644 index 000000000..f74ccf8cb --- /dev/null +++ b/fdm-core/src/db/migrations/0026_p_app_amount_unit.sql @@ -0,0 +1,3 @@ +CREATE TYPE "fdm"."p_app_amount_unit" AS ENUM('kg/ha', 'l/ha', 'm3/ha', 'ton/ha');--> statement-breakpoint +ALTER TABLE "fdm"."fertilizers_catalogue" ADD COLUMN "p_app_amount_unit" "fdm"."p_app_amount_unit" DEFAULT 'kg/ha' NOT NULL;--> statement-breakpoint +UPDATE "fdm"."fertilizers_catalogue" SET "hash" = '0000' WHERE "p_source" IN ('baat', 'srm'); \ No newline at end of file diff --git a/fdm-core/src/db/migrations/meta/0026_snapshot.json b/fdm-core/src/db/migrations/meta/0026_snapshot.json new file mode 100644 index 000000000..d06cc4a37 --- /dev/null +++ b/fdm-core/src/db/migrations/meta/0026_snapshot.json @@ -0,0 +1,3951 @@ +{ + "id": "fb39566c-e775-4a6e-8953-3be53ff56e54", + "prevId": "af3bd68d-428d-4e65-b1ca-36a32d210fbf", + "version": "7", + "dialect": "postgresql", + "tables": { + "fdm.cultivation_catalogue_selecting": { + "name": "cultivation_catalogue_selecting", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_source": { + "name": "b_lu_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cultivation_catalogue_selecting_b_id_farm_farms_b_id_farm_fk": { + "name": "cultivation_catalogue_selecting_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "cultivation_catalogue_selecting", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivation_ending": { + "name": "cultivation_ending", + "schema": "fdm", + "columns": { + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_end": { + "name": "b_lu_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "m_cropresidue": { + "name": "m_cropresidue", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cultivation_ending_b_lu_cultivations_b_lu_fk": { + "name": "cultivation_ending_b_lu_cultivations_b_lu_fk", + "tableFrom": "cultivation_ending", + "tableTo": "cultivations", + "schemaTo": "fdm", + "columnsFrom": [ + "b_lu" + ], + "columnsTo": [ + "b_lu" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivation_harvesting": { + "name": "cultivation_harvesting", + "schema": "fdm", + "columns": { + "b_id_harvesting": { + "name": "b_id_harvesting", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_id_harvestable": { + "name": "b_id_harvestable", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_harvest_date": { + "name": "b_lu_harvest_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cultivation_harvesting_b_id_harvestable_harvestables_b_id_harvestable_fk": { + "name": "cultivation_harvesting_b_id_harvestable_harvestables_b_id_harvestable_fk", + "tableFrom": "cultivation_harvesting", + "tableTo": "harvestables", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_harvestable" + ], + "columnsTo": [ + "b_id_harvestable" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cultivation_harvesting_b_lu_cultivations_b_lu_fk": { + "name": "cultivation_harvesting_b_lu_cultivations_b_lu_fk", + "tableFrom": "cultivation_harvesting", + "tableTo": "cultivations", + "schemaTo": "fdm", + "columnsFrom": [ + "b_lu" + ], + "columnsTo": [ + "b_lu" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivation_starting": { + "name": "cultivation_starting", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_start": { + "name": "b_lu_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "b_sowing_amount": { + "name": "b_sowing_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_sowing_method": { + "name": "b_sowing_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cultivation_starting_b_id_fields_b_id_fk": { + "name": "cultivation_starting_b_id_fields_b_id_fk", + "tableFrom": "cultivation_starting", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cultivation_starting_b_lu_cultivations_b_lu_fk": { + "name": "cultivation_starting_b_lu_cultivations_b_lu_fk", + "tableFrom": "cultivation_starting", + "tableTo": "cultivations", + "schemaTo": "fdm", + "columnsFrom": [ + "b_lu" + ], + "columnsTo": [ + "b_lu" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivations": { + "name": "cultivations", + "schema": "fdm", + "columns": { + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_lu_catalogue": { + "name": "b_lu_catalogue", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_variety": { + "name": "b_lu_variety", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_lu_idx": { + "name": "b_lu_idx", + "columns": [ + { + "expression": "b_lu", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cultivations_b_lu_catalogue_cultivations_catalogue_b_lu_catalogue_fk": { + "name": "cultivations_b_lu_catalogue_cultivations_catalogue_b_lu_catalogue_fk", + "tableFrom": "cultivations", + "tableTo": "cultivations_catalogue", + "schemaTo": "fdm", + "columnsFrom": [ + "b_lu_catalogue" + ], + "columnsTo": [ + "b_lu_catalogue" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivations_catalogue": { + "name": "cultivations_catalogue", + "schema": "fdm", + "columns": { + "b_lu_catalogue": { + "name": "b_lu_catalogue", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_lu_source": { + "name": "b_lu_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_name": { + "name": "b_lu_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_name_en": { + "name": "b_lu_name_en", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_lu_harvestable": { + "name": "b_lu_harvestable", + "type": "b_lu_harvestable", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": true + }, + "b_lu_harvestcat": { + "name": "b_lu_harvestcat", + "type": "b_lu_harvestcat", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "b_lu_hcat3": { + "name": "b_lu_hcat3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_lu_hcat3_name": { + "name": "b_lu_hcat3_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_lu_croprotation": { + "name": "b_lu_croprotation", + "type": "b_lu_croprotation", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "b_lu_yield": { + "name": "b_lu_yield", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_dm": { + "name": "b_lu_dm", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_hi": { + "name": "b_lu_hi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_n_harvestable": { + "name": "b_lu_n_harvestable", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_n_residue": { + "name": "b_lu_n_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_n_fixation": { + "name": "b_n_fixation", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_eom": { + "name": "b_lu_eom", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_eom_residue": { + "name": "b_lu_eom_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_rest_oravib": { + "name": "b_lu_rest_oravib", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "b_lu_variety_options": { + "name": "b_lu_variety_options", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "b_lu_start_default": { + "name": "b_lu_start_default", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_date_harvest_default": { + "name": "b_date_harvest_default", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_lu_catalogue_idx": { + "name": "b_lu_catalogue_idx", + "columns": [ + { + "expression": "b_lu_catalogue", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "b_lu_start_default_format": { + "name": "b_lu_start_default_format", + "value": "b_lu_start_default IS NULL OR b_lu_start_default ~ '^(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$'" + }, + "b_date_harvest_default_format": { + "name": "b_date_harvest_default_format", + "value": "b_date_harvest_default IS NULL OR b_date_harvest_default ~ '^(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$'" + } + }, + "isRLSEnabled": false + }, + "fdm.derogation_applying": { + "name": "derogation_applying", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_id_derogation": { + "name": "b_id_derogation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "derogation_one_per_farm_per": { + "name": "derogation_one_per_farm_per", + "columns": [ + { + "expression": "b_id_derogation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "derogation_applying_b_id_farm_farms_b_id_farm_fk": { + "name": "derogation_applying_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "derogation_applying", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "derogation_applying_b_id_derogation_derogations_b_id_derogation_fk": { + "name": "derogation_applying_b_id_derogation_derogations_b_id_derogation_fk", + "tableFrom": "derogation_applying", + "tableTo": "derogations", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_derogation" + ], + "columnsTo": [ + "b_id_derogation" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.derogations": { + "name": "derogations", + "schema": "fdm", + "columns": { + "b_id_derogation": { + "name": "b_id_derogation", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_derogation_year": { + "name": "b_derogation_year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.farms": { + "name": "farms", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_name_farm": { + "name": "b_name_farm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_businessid_farm": { + "name": "b_businessid_farm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_address_farm": { + "name": "b_address_farm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_postalcode_farm": { + "name": "b_postalcode_farm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_id_farm_idx": { + "name": "b_id_farm_idx", + "columns": [ + { + "expression": "b_id_farm", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizer_acquiring": { + "name": "fertilizer_acquiring", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_id": { + "name": "p_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_acquiring_amount": { + "name": "p_acquiring_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_acquiring_date": { + "name": "p_acquiring_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "fertilizer_acquiring_b_id_farm_farms_b_id_farm_fk": { + "name": "fertilizer_acquiring_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "fertilizer_acquiring", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fertilizer_acquiring_p_id_fertilizers_p_id_fk": { + "name": "fertilizer_acquiring_p_id_fertilizers_p_id_fk", + "tableFrom": "fertilizer_acquiring", + "tableTo": "fertilizers", + "schemaTo": "fdm", + "columnsFrom": [ + "p_id" + ], + "columnsTo": [ + "p_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizer_applying": { + "name": "fertilizer_applying", + "schema": "fdm", + "columns": { + "p_app_id": { + "name": "p_app_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_id": { + "name": "p_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_app_amount": { + "name": "p_app_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_app_method": { + "name": "p_app_method", + "type": "p_app_method", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "p_app_date": { + "name": "p_app_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "p_app_idx": { + "name": "p_app_idx", + "columns": [ + { + "expression": "p_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fertilizer_applying_b_id_fields_b_id_fk": { + "name": "fertilizer_applying_b_id_fields_b_id_fk", + "tableFrom": "fertilizer_applying", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fertilizer_applying_p_id_fertilizers_p_id_fk": { + "name": "fertilizer_applying_p_id_fertilizers_p_id_fk", + "tableFrom": "fertilizer_applying", + "tableTo": "fertilizers", + "schemaTo": "fdm", + "columnsFrom": [ + "p_id" + ], + "columnsTo": [ + "p_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizer_catalogue_enabling": { + "name": "fertilizer_catalogue_enabling", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_source": { + "name": "p_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "fertilizer_catalogue_enabling_b_id_farm_farms_b_id_farm_fk": { + "name": "fertilizer_catalogue_enabling_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "fertilizer_catalogue_enabling", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizer_picking": { + "name": "fertilizer_picking", + "schema": "fdm", + "columns": { + "p_id": { + "name": "p_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_id_catalogue": { + "name": "p_id_catalogue", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_picking_date": { + "name": "p_picking_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "fertilizer_picking_p_id_fertilizers_p_id_fk": { + "name": "fertilizer_picking_p_id_fertilizers_p_id_fk", + "tableFrom": "fertilizer_picking", + "tableTo": "fertilizers", + "schemaTo": "fdm", + "columnsFrom": [ + "p_id" + ], + "columnsTo": [ + "p_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fertilizer_picking_p_id_catalogue_fertilizers_catalogue_p_id_catalogue_fk": { + "name": "fertilizer_picking_p_id_catalogue_fertilizers_catalogue_p_id_catalogue_fk", + "tableFrom": "fertilizer_picking", + "tableTo": "fertilizers_catalogue", + "schemaTo": "fdm", + "columnsFrom": [ + "p_id_catalogue" + ], + "columnsTo": [ + "p_id_catalogue" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizers": { + "name": "fertilizers", + "schema": "fdm", + "columns": { + "p_id": { + "name": "p_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "p_id_idx": { + "name": "p_id_idx", + "columns": [ + { + "expression": "p_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizers_catalogue": { + "name": "fertilizers_catalogue", + "schema": "fdm", + "columns": { + "p_id_catalogue": { + "name": "p_id_catalogue", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "p_source": { + "name": "p_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_name_nl": { + "name": "p_name_nl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_name_en": { + "name": "p_name_en", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "p_description": { + "name": "p_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "p_app_method_options": { + "name": "p_app_method_options", + "type": "p_app_method[]", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "p_app_amount_unit": { + "name": "p_app_amount_unit", + "type": "p_app_amount_unit", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": true, + "default": "'kg/ha'" + }, + "p_dm": { + "name": "p_dm", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_density": { + "name": "p_density", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_om": { + "name": "p_om", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_a": { + "name": "p_a", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_hc": { + "name": "p_hc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_eom": { + "name": "p_eom", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_eoc": { + "name": "p_eoc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_c_rt": { + "name": "p_c_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_c_of": { + "name": "p_c_of", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_c_if": { + "name": "p_c_if", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_c_fr": { + "name": "p_c_fr", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cn_of": { + "name": "p_cn_of", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_n_rt": { + "name": "p_n_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_n_if": { + "name": "p_n_if", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_n_of": { + "name": "p_n_of", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_n_wc": { + "name": "p_n_wc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_no3_rt": { + "name": "p_no3_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_nh4_rt": { + "name": "p_nh4_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_p_rt": { + "name": "p_p_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_k_rt": { + "name": "p_k_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_mg_rt": { + "name": "p_mg_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_ca_rt": { + "name": "p_ca_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_ne": { + "name": "p_ne", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_s_rt": { + "name": "p_s_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_s_wc": { + "name": "p_s_wc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cu_rt": { + "name": "p_cu_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_zn_rt": { + "name": "p_zn_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_na_rt": { + "name": "p_na_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_si_rt": { + "name": "p_si_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_b_rt": { + "name": "p_b_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_mn_rt": { + "name": "p_mn_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_ni_rt": { + "name": "p_ni_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_fe_rt": { + "name": "p_fe_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_mo_rt": { + "name": "p_mo_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_co_rt": { + "name": "p_co_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_as_rt": { + "name": "p_as_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cd_rt": { + "name": "p_cd_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cr_rt": { + "name": "p_cr_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cr_vi": { + "name": "p_cr_vi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_pb_rt": { + "name": "p_pb_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_hg_rt": { + "name": "p_hg_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cl_rt": { + "name": "p_cl_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_ef_nh3": { + "name": "p_ef_nh3", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_type_manure": { + "name": "p_type_manure", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "p_type_mineral": { + "name": "p_type_mineral", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "p_type_compost": { + "name": "p_type_compost", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "p_type_rvo": { + "name": "p_type_rvo", + "type": "p_type_rvo", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "p_id_catalogue_idx": { + "name": "p_id_catalogue_idx", + "columns": [ + { + "expression": "p_id_catalogue", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.field_acquiring": { + "name": "field_acquiring", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_start": { + "name": "b_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "b_acquiring_method": { + "name": "b_acquiring_method", + "type": "b_acquiring_method", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "field_acquiring_b_id_fields_b_id_fk": { + "name": "field_acquiring_b_id_fields_b_id_fk", + "tableFrom": "field_acquiring", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "field_acquiring_b_id_farm_farms_b_id_farm_fk": { + "name": "field_acquiring_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "field_acquiring", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.field_discarding": { + "name": "field_discarding", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_end": { + "name": "b_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "field_discarding_b_id_fields_b_id_fk": { + "name": "field_discarding_b_id_fields_b_id_fk", + "tableFrom": "field_discarding", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fields": { + "name": "fields", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_name": { + "name": "b_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_geometry": { + "name": "b_geometry", + "type": "geometry(Polygon,4326)", + "primaryKey": false, + "notNull": false + }, + "b_id_source": { + "name": "b_id_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_bufferstrip": { + "name": "b_bufferstrip", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_id_idx": { + "name": "b_id_idx", + "columns": [ + { + "expression": "b_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "b_geom_idx": { + "name": "b_geom_idx", + "columns": [ + { + "expression": "b_geometry", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gist", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.harvestable_analyses": { + "name": "harvestable_analyses", + "schema": "fdm", + "columns": { + "b_id_harvestable_analysis": { + "name": "b_id_harvestable_analysis", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_lu_yield": { + "name": "b_lu_yield", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_yield_fresh": { + "name": "b_lu_yield_fresh", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_yield_bruto": { + "name": "b_lu_yield_bruto", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_tarra": { + "name": "b_lu_tarra", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_dm": { + "name": "b_lu_dm", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_moist": { + "name": "b_lu_moist", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_uww": { + "name": "b_lu_uww", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_cp": { + "name": "b_lu_cp", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_n_harvestable": { + "name": "b_lu_n_harvestable", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_n_residue": { + "name": "b_lu_n_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_p_harvestable": { + "name": "b_lu_p_harvestable", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_p_residue": { + "name": "b_lu_p_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_k_harvestable": { + "name": "b_lu_k_harvestable", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_k_residue": { + "name": "b_lu_k_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_id_harvestable_analyses_idx": { + "name": "b_id_harvestable_analyses_idx", + "columns": [ + { + "expression": "b_id_harvestable_analysis", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.harvestable_sampling": { + "name": "harvestable_sampling", + "schema": "fdm", + "columns": { + "b_id_harvestable": { + "name": "b_id_harvestable", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_id_harvestable_analysis": { + "name": "b_id_harvestable_analysis", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_sampling_date": { + "name": "b_sampling_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "harvestable_sampling_b_id_harvestable_harvestables_b_id_harvestable_fk": { + "name": "harvestable_sampling_b_id_harvestable_harvestables_b_id_harvestable_fk", + "tableFrom": "harvestable_sampling", + "tableTo": "harvestables", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_harvestable" + ], + "columnsTo": [ + "b_id_harvestable" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "harvestable_sampling_b_id_harvestable_analysis_harvestable_analyses_b_id_harvestable_analysis_fk": { + "name": "harvestable_sampling_b_id_harvestable_analysis_harvestable_analyses_b_id_harvestable_analysis_fk", + "tableFrom": "harvestable_sampling", + "tableTo": "harvestable_analyses", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_harvestable_analysis" + ], + "columnsTo": [ + "b_id_harvestable_analysis" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.harvestables": { + "name": "harvestables", + "schema": "fdm", + "columns": { + "b_id_harvestable": { + "name": "b_id_harvestable", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_id_harvestable_idx": { + "name": "b_id_harvestable_idx", + "columns": [ + { + "expression": "b_id_harvestable", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.intending_grazing": { + "name": "intending_grazing", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_grazing_intention": { + "name": "b_grazing_intention", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "b_grazing_intention_year": { + "name": "b_grazing_intention_year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "intending_grazing_b_id_farm_farms_b_id_farm_fk": { + "name": "intending_grazing_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "intending_grazing", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "intending_grazing_b_id_farm_b_grazing_intention_year_pk": { + "name": "intending_grazing_b_id_farm_b_grazing_intention_year_pk", + "columns": [ + "b_id_farm", + "b_grazing_intention_year" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.organic_certifications": { + "name": "organic_certifications", + "schema": "fdm", + "columns": { + "b_id_organic": { + "name": "b_id_organic", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_organic_traces": { + "name": "b_organic_traces", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_organic_skal": { + "name": "b_organic_skal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_organic_issued": { + "name": "b_organic_issued", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "b_organic_expires": { + "name": "b_organic_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.organic_certifications_holding": { + "name": "organic_certifications_holding", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_id_organic": { + "name": "b_id_organic", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organic_one_farm_per_cert": { + "name": "organic_one_farm_per_cert", + "columns": [ + { + "expression": "b_id_organic", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organic_certifications_holding_b_id_farm_farms_b_id_farm_fk": { + "name": "organic_certifications_holding_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "organic_certifications_holding", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "organic_certifications_holding_b_id_organic_organic_certifications_b_id_organic_fk": { + "name": "organic_certifications_holding_b_id_organic_organic_certifications_b_id_organic_fk", + "tableFrom": "organic_certifications_holding", + "tableTo": "organic_certifications", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_organic" + ], + "columnsTo": [ + "b_id_organic" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.soil_analysis": { + "name": "soil_analysis", + "schema": "fdm", + "columns": { + "a_id": { + "name": "a_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "a_date": { + "name": "a_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "a_source": { + "name": "a_source", + "type": "a_source", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false, + "default": "'other'" + }, + "a_al_ox": { + "name": "a_al_ox", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_c_of": { + "name": "a_c_of", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_ca_co": { + "name": "a_ca_co", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_ca_co_po": { + "name": "a_ca_co_po", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_caco3_if": { + "name": "a_caco3_if", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_cec_co": { + "name": "a_cec_co", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_clay_mi": { + "name": "a_clay_mi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_cn_fr": { + "name": "a_cn_fr", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_com_fr": { + "name": "a_com_fr", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_cu_cc": { + "name": "a_cu_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_density_sa": { + "name": "a_density_sa", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_fe_ox": { + "name": "a_fe_ox", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_k_cc": { + "name": "a_k_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_k_co": { + "name": "a_k_co", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_k_co_po": { + "name": "a_k_co_po", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_mg_cc": { + "name": "a_mg_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_mg_co": { + "name": "a_mg_co", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_mg_co_po": { + "name": "a_mg_co_po", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_n_pmn": { + "name": "a_n_pmn", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_n_rt": { + "name": "a_n_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_nh4_cc": { + "name": "a_nh4_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_nmin_cc": { + "name": "a_nmin_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_no3_cc": { + "name": "a_no3_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_al": { + "name": "a_p_al", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_cc": { + "name": "a_p_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_ox": { + "name": "a_p_ox", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_rt": { + "name": "a_p_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_sg": { + "name": "a_p_sg", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_wa": { + "name": "a_p_wa", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_ph_cc": { + "name": "a_ph_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_s_rt": { + "name": "a_s_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_sand_mi": { + "name": "a_sand_mi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_silt_mi": { + "name": "a_silt_mi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_som_loi": { + "name": "a_som_loi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_zn_cc": { + "name": "a_zn_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_gwl_class": { + "name": "b_gwl_class", + "type": "b_gwl_class", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "b_soiltype_agr": { + "name": "b_soiltype_agr", + "type": "b_soiltype_agr", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.soil_sampling": { + "name": "soil_sampling", + "schema": "fdm", + "columns": { + "b_id_sampling": { + "name": "b_id_sampling", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "a_id": { + "name": "a_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "a_depth_upper": { + "name": "a_depth_upper", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "a_depth_lower": { + "name": "a_depth_lower", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_sampling_date": { + "name": "b_sampling_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "b_sampling_geometry": { + "name": "b_sampling_geometry", + "type": "geometry(MultiPoint,4326)", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "soil_sampling_b_id_fields_b_id_fk": { + "name": "soil_sampling_b_id_fields_b_id_fk", + "tableFrom": "soil_sampling", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "soil_sampling_a_id_soil_analysis_a_id_fk": { + "name": "soil_sampling_a_id_soil_analysis_a_id_fk", + "tableFrom": "soil_sampling", + "tableTo": "soil_analysis", + "schemaTo": "fdm", + "columnsFrom": [ + "a_id" + ], + "columnsTo": [ + "a_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.account": { + "name": "account", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.invitation": { + "name": "invitation", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.member": { + "name": "member", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.organization": { + "name": "organization", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.rate_limit": { + "name": "rate_limit", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "last_request": { + "name": "last_request", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "rate_limit_key_unique": { + "name": "rate_limit_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.session": { + "name": "session", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.user": { + "name": "user", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "firstname": { + "name": "firstname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surname": { + "name": "surname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lang": { + "name": "lang", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'nl-NL'" + }, + "farm_active": { + "name": "farm_active", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.verification": { + "name": "verification", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authz.audit": { + "name": "audit", + "schema": "fdm-authz", + "columns": { + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "audit_timestamp": { + "name": "audit_timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "audit_origin": { + "name": "audit_origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_resource": { + "name": "target_resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_resource_id": { + "name": "target_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granting_resource": { + "name": "granting_resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granting_resource_id": { + "name": "granting_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed": { + "name": "allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authz.invitation": { + "name": "invitation", + "schema": "fdm-authz", + "columns": { + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_email": { + "name": "target_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_principal_id": { + "name": "target_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "invitation_unique_email_idx": { + "name": "invitation_unique_email_idx", + "columns": [ + { + "expression": "resource", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"fdm-authz\".\"invitation\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_unique_principal_idx": { + "name": "invitation_unique_principal_idx", + "columns": [ + { + "expression": "resource", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"fdm-authz\".\"invitation\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_target_email_idx": { + "name": "invitation_pending_target_email_idx", + "columns": [ + { + "expression": "target_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"fdm-authz\".\"invitation\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "invitation_target_check": { + "name": "invitation_target_check", + "value": "\"fdm-authz\".\"invitation\".\"target_email\" IS NOT NULL OR \"fdm-authz\".\"invitation\".\"target_principal_id\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "fdm-authz.role": { + "name": "role", + "schema": "fdm-authz", + "columns": { + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted": { + "name": "deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "role_idx": { + "name": "role_idx", + "columns": [ + { + "expression": "resource", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-calculator.calculation_cache": { + "name": "calculation_cache", + "schema": "fdm-calculator", + "columns": { + "calculation_hash": { + "name": "calculation_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "calculation_function": { + "name": "calculation_function", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "calculator_version": { + "name": "calculator_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-calculator.calculation_errors": { + "name": "calculation_errors", + "schema": "fdm-calculator", + "columns": { + "calculation_error_id": { + "name": "calculation_error_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "calculation_function": { + "name": "calculation_function", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "calculator_version": { + "name": "calculator_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stack_trace": { + "name": "stack_trace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "fdm.b_acquiring_method": { + "name": "b_acquiring_method", + "schema": "fdm", + "values": [ + "nl_01", + "nl_02", + "nl_03", + "nl_04", + "nl_07", + "nl_09", + "nl_10", + "nl_11", + "nl_12", + "nl_13", + "nl_61", + "nl_63", + "unknown" + ] + }, + "fdm.p_app_method": { + "name": "p_app_method", + "schema": "fdm", + "values": [ + "slotted coulter", + "incorporation", + "incorporation 2 tracks", + "injection", + "shallow injection", + "spraying", + "broadcasting", + "spoke wheel", + "pocket placement", + "narrowband" + ] + }, + "fdm.b_gwl_class": { + "name": "b_gwl_class", + "schema": "fdm", + "values": [ + "I", + "Ia", + "Ic", + "II", + "IIa", + "IIb", + "IIc", + "III", + "IIIa", + "IIIb", + "IV", + "IVu", + "IVc", + "V", + "Va", + "Vao", + "Vad", + "Vb", + "Vbo", + "Vbd", + "sV", + "sVb", + "VI", + "VIo", + "VId", + "VII", + "VIIo", + "VIId", + "VIII", + "VIIIo", + "VIIId" + ] + }, + "fdm.b_lu_harvestcat": { + "name": "b_lu_harvestcat", + "schema": "fdm", + "values": [ + "HC010", + "HC020", + "HC031", + "HC040", + "HC041", + "HC042", + "HC050" + ] + }, + "fdm.b_lu_harvestable": { + "name": "b_lu_harvestable", + "schema": "fdm", + "values": [ + "none", + "once", + "multiple" + ] + }, + "fdm.b_lu_croprotation": { + "name": "b_lu_croprotation", + "schema": "fdm", + "values": [ + "other", + "clover", + "nature", + "potato", + "grass", + "rapeseed", + "starch", + "maize", + "cereal", + "sugarbeet", + "alfalfa", + "catchcrop" + ] + }, + "fdm.a_source": { + "name": "a_source", + "schema": "fdm", + "values": [ + "nl-rva-l122", + "nl-rva-l136", + "nl-rva-l264", + "nl-rva-l320", + "nl-rva-l335", + "nl-rva-l610", + "nl-rva-l648", + "nl-rva-l697", + "nl-other-nmi", + "other" + ] + }, + "fdm.b_soiltype_agr": { + "name": "b_soiltype_agr", + "schema": "fdm", + "values": [ + "moerige_klei", + "rivierklei", + "dekzand", + "zeeklei", + "dalgrond", + "veen", + "loess", + "duinzand", + "maasklei" + ] + }, + "fdm.p_app_amount_unit": { + "name": "p_app_amount_unit", + "schema": "fdm", + "values": [ + "kg/ha", + "l/ha", + "m3/ha", + "ton/ha" + ] + }, + "fdm.p_type_rvo": { + "name": "p_type_rvo", + "schema": "fdm", + "values": [ + "10", + "11", + "12", + "13", + "14", + "17", + "18", + "19", + "23", + "30", + "31", + "32", + "33", + "35", + "39", + "40", + "41", + "42", + "43", + "46", + "50", + "56", + "60", + "61", + "75", + "76", + "80", + "81", + "90", + "91", + "92", + "25", + "26", + "27", + "95", + "96", + "97", + "98", + "99", + "100", + "101", + "102", + "103", + "104", + "105", + "106", + "107", + "108", + "109", + "110", + "111", + "112", + "113", + "114", + "115", + "116", + "117", + "120" + ] + } + }, + "schemas": { + "fdm": "fdm", + "fdm-authn": "fdm-authn", + "fdm-authz": "fdm-authz", + "fdm-calculator": "fdm-calculator" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/fdm-core/src/db/migrations/meta/_journal.json b/fdm-core/src/db/migrations/meta/_journal.json index 33564b153..0bbbadf67 100644 --- a/fdm-core/src/db/migrations/meta/_journal.json +++ b/fdm-core/src/db/migrations/meta/_journal.json @@ -183,6 +183,13 @@ "when": 1773751940467, "tag": "0025_rename-b-lu-eom-residue", "breakpoints": true + }, + { + "idx": 26, + "version": "7", + "when": 1775635544356, + "tag": "0026_p_app_amount_unit", + "breakpoints": true } ] } diff --git a/fdm-core/src/db/schema.ts b/fdm-core/src/db/schema.ts index 2ee8514ce..2f71a5601 100644 --- a/fdm-core/src/db/schema.ts +++ b/fdm-core/src/db/schema.ts @@ -11,6 +11,7 @@ import { timestamp, uniqueIndex, } from "drizzle-orm/pg-core" +import { APP_AMOUNT_UNITS } from "../unit-conversion" import { geometry, numericCasted } from "./schema-custom-types" // Define postgres schema @@ -183,6 +184,7 @@ export const applicationMethodEnum = fdmSchema.enum( "p_app_method", applicationMethodOptions.map((x) => x.value) as [string, ...string[]], ) + export const fertilizerApplication = fdmSchema.table( "fertilizer_applying", { @@ -284,6 +286,10 @@ export const typeRvoEnum = fdmSchema.enum( "p_type_rvo", typeRvoOptions.map((x) => x.value) as [string, ...string[]], ) +export const typeApplicationAmountUnitsEnum = fdmSchema.enum( + "p_app_amount_unit", + APP_AMOUNT_UNITS.map((x) => x.value) as [string, ...string[]], +) // Define fertilizers_catalogue table export const fertilizersCatalogue = fdmSchema.table( @@ -295,6 +301,9 @@ export const fertilizersCatalogue = fdmSchema.table( p_name_en: text(), p_description: text(), p_app_method_options: applicationMethodEnum().array(), + p_app_amount_unit: typeApplicationAmountUnitsEnum() + .notNull() + .default("kg/ha"), p_dm: numericCasted(), p_density: numericCasted(), p_om: numericCasted(), diff --git a/fdm-core/src/fertilizer.d.ts b/fdm-core/src/fertilizer.d.ts index 8394ad66e..75a859cd4 100644 --- a/fdm-core/src/fertilizer.d.ts +++ b/fdm-core/src/fertilizer.d.ts @@ -1,5 +1,6 @@ import type { ApplicationMethods } from "@nmi-agro/fdm-data" import type * as schema from "./db/schema" +import type { AppAmountUnit } from "./unit-conversion" export interface FertilizerCatalogue { p_id_catalogue: string @@ -8,6 +9,7 @@ export interface FertilizerCatalogue { p_name_en: string | null p_description: string | null p_app_method_options: ApplicationMethods[] | null + p_app_amount_unit: AppAmountUnit | undefined p_dm: number | null p_density: number | null p_om: number | null @@ -82,6 +84,7 @@ export type FertilizerParameters = | "p_name_en" | "p_description" | "p_app_method_options" + | "p_app_amount_unit" | "p_dm" | "p_density" | "p_om" diff --git a/fdm-core/src/fertilizer.test.ts b/fdm-core/src/fertilizer.test.ts index dae78eacd..5189dbc8b 100644 --- a/fdm-core/src/fertilizer.test.ts +++ b/fdm-core/src/fertilizer.test.ts @@ -108,6 +108,7 @@ describe("Fertilizer Data Model", () => { p_name_en, p_description, p_app_method_options: ["injection", "incorporation"], + p_app_amount_unit: undefined, p_dm: 37, p_density: 20, p_om: 20, @@ -188,6 +189,7 @@ describe("Fertilizer Data Model", () => { p_name_en, p_description, p_app_method_options: [], + p_app_amount_unit: undefined, p_dm: 37, p_density: 20, p_om: 20, @@ -266,6 +268,7 @@ describe("Fertilizer Data Model", () => { p_name_en, p_description, p_app_method_options: [], + p_app_amount_unit: undefined, p_dm: 37, p_density: 20, p_om: 20, @@ -375,6 +378,7 @@ describe("Fertilizer Data Model", () => { randomAppMethod(), ]), ], + p_app_amount_unit: undefined, }) return fert as Parameters[3] } @@ -550,6 +554,7 @@ describe("Fertilizer Data Model", () => { p_name_en, p_description, p_app_method_options: [], + p_app_amount_unit: undefined, p_dm: 37, p_density: 20, p_om: 20, @@ -650,6 +655,7 @@ describe("Fertilizer Data Model", () => { p_name_en: "Test Fertilizer (EN)", p_description: "This is a test fertilizer", p_app_method_options: [], + p_app_amount_unit: undefined, p_dm: 37, p_density: 20, p_om: 20, @@ -867,6 +873,7 @@ describe("Fertilizer Data Model", () => { p_name_en: "Test Fertilizer (EN) 2", p_description: "This is a test fertilizer 2", p_app_method_options: [], + p_app_amount_unit: undefined, p_dm: 37, p_density: 20, p_om: 20, @@ -939,6 +946,7 @@ describe("Fertilizer Data Model", () => { p_name_en: "RVO-mapped fertilizer (EN)", p_description: "This is a test fertilizer for RVO mapping", p_app_method_options: [], + p_app_amount_unit: undefined, p_dm: 100, p_density: 1, p_om: 0, @@ -1071,6 +1079,7 @@ describe("Fertilizer Data Model", () => { p_name_en, p_description, p_app_method_options: [], + p_app_amount_unit: undefined, p_dm: 37, p_density: 20, p_om: 20, @@ -1368,7 +1377,7 @@ describe("Fertilizer Data Model", () => { describe("getFertilizerParametersDescription", () => { it("should return the correct fertilizer parameter descriptions for NL-nl locale", () => { const descriptions = getFertilizerParametersDescription("NL-nl") - expect(descriptions).toHaveLength(24) + expect(descriptions).toHaveLength(25) for (const description of descriptions) { expect(description).toHaveProperty("parameter") expect(description).toHaveProperty("unit") @@ -1393,7 +1402,7 @@ describe("getFertilizerParametersDescription", () => { it("should return the correct fertilizer parameter descriptions for default locale", () => { const descriptions = getFertilizerParametersDescription() - expect(descriptions).toHaveLength(24) + expect(descriptions).toHaveLength(25) for (const description of descriptions) { expect(description).toHaveProperty("parameter") expect(description).toHaveProperty("unit") @@ -1434,6 +1443,7 @@ describe("getFertilizerApplicationsForFarm", () => { p_name_en: "Test Fertilizer EN", p_description: "desc", p_app_method_options: [] as [], + p_app_amount_unit: undefined, p_dm: 37, p_density: 20, p_om: 20, diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 8b9c77285..60a8b1491 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -19,6 +19,7 @@ import type { } from "./fertilizer.d" import { createId } from "./id" import type { Timeframe } from "./timeframe" +import { APP_AMOUNT_UNITS, type AppAmountUnit } from "./unit-conversion" /** * Retrieves all fertilizers from the enabled catalogues for a farm. @@ -131,6 +132,7 @@ export async function getFertilizersFromCatalogues( p_app_method_options: result.p_app_method_options as | ApplicationMethods[] | null, + p_app_amount_unit: result.p_app_amount_unit as AppAmountUnit, p_type: deriveFertilizerType(result), })) } catch (err) { @@ -160,6 +162,7 @@ export async function addFertilizerToCatalogue( p_name_en: schema.fertilizersCatalogueTypeInsert["p_name_en"] p_description: schema.fertilizersCatalogueTypeInsert["p_description"] p_app_method_options: schema.fertilizersCatalogueTypeInsert["p_app_method_options"] + p_app_amount_unit: schema.fertilizersCatalogueTypeInsert["p_app_amount_unit"] p_dm: schema.fertilizersCatalogueTypeInsert["p_dm"] p_density: schema.fertilizersCatalogueTypeInsert["p_density"] p_om: schema.fertilizersCatalogueTypeInsert["p_om"] @@ -344,6 +347,8 @@ export async function getFertilizer( p_description: schema.fertilizersCatalogue.p_description, p_app_method_options: schema.fertilizersCatalogue.p_app_method_options, + p_app_amount_unit: + schema.fertilizersCatalogue.p_app_amount_unit, p_acquiring_amount: schema.fertilizerAcquiring.p_acquiring_amount, p_acquiring_date: schema.fertilizerAcquiring.p_acquiring_date, @@ -609,6 +614,8 @@ export async function getFertilizers( p_description: schema.fertilizersCatalogue.p_description, p_app_method_options: schema.fertilizersCatalogue.p_app_method_options, + p_app_amount_unit: + schema.fertilizersCatalogue.p_app_amount_unit, p_acquiring_amount: schema.fertilizerAcquiring.p_acquiring_amount, p_acquiring_date: schema.fertilizerAcquiring.p_acquiring_date, @@ -1222,6 +1229,15 @@ export function getFertilizerParametersDescription( description: "Mestcode volgens RVO", options: schema.typeRvoOptions, }, + { + parameter: "p_app_amount_unit", + unit: "", + name: "Toedieningshoeveelheidsunit", + type: "enum", + category: "general", + description: "Toedieningsmethodes mogelijk voor deze meststof", + options: APP_AMOUNT_UNITS, + }, { parameter: "p_app_method_options", unit: "", diff --git a/fdm-core/src/unit-conversion.test.ts b/fdm-core/src/unit-conversion.test.ts new file mode 100644 index 000000000..05c1e48d9 --- /dev/null +++ b/fdm-core/src/unit-conversion.test.ts @@ -0,0 +1,164 @@ +import Decimal from "decimal.js" +import { describe, expect, it } from "vitest" +import { + type AppAmountUnit, + fromKgPerHa, + suggestUnitFromRvoCode, + toKgPerHa, +} from "./unit-conversion" + +interface ConversionUnitTestCase { + input: number + unit: AppAmountUnit + density?: number | undefined + + output?: number | null + throws?: string +} + +describe("toKgPerHa", () => { + const tests: ConversionUnitTestCase[] = [ + { input: 20, unit: "kg/ha", output: 20 }, + { input: 20, unit: "ton/ha", output: 20000 }, + { input: 20, unit: "l/ha", density: 0.8, output: 16 }, + { input: 20, unit: "m3/ha", density: 0.8, output: 16000 }, + { input: 0, unit: "kg/ha", output: 0 }, + { input: 0, unit: "ton/ha", output: 0 }, + { input: 0, unit: "l/ha", density: 0.8, output: 0 }, + { input: 0, unit: "m3/ha", density: 0.8, output: 0 }, + ] + + const throwingTests: ConversionUnitTestCase[] = [ + { + input: 20, + unit: "l/ha", + density: undefined, + throws: "Density (p_density) is required for l/ha → kg/ha conversion", + }, + { + input: 20, + unit: "m3/ha", + density: undefined, + throws: "Density (p_density) is required for m3/ha → kg/ha conversion", + }, + { + input: 20, + unit: "ft3/ha" as AppAmountUnit, + density: 2, + throws: "ft3/ha → kg/ha conversion is not supported", + }, + ] + + for (const { input, unit, density, output } of tests) { + it( + density !== undefined + ? `should convert ${unit} to kg/ha with density ${density} kg/l` + : `should convert ${unit} to kg/ha without density specified`, + () => { + expect(toKgPerHa(input, unit, density).toNumber()).toBe(output) + }, + ) + } + + for (const { input, unit, density, throws } of throwingTests) { + it( + density !== undefined + ? `should throw exception on conversion from ${unit} to kg/ha` + : `should throw exception on conversion from ${unit} to kg/ha without density specified`, + () => { + expect(() => toKgPerHa(input, unit, density)).toThrow(throws) + }, + ) + } + + it("should accept input of type Decimal", () => { + expect( + toKgPerHa(new Decimal(20), "m3/ha", new Decimal(2)).toNumber(), + ).toBe(40000) + }) +}) + +describe("fromKgPerHa", () => { + const tests: ConversionUnitTestCase[] = [ + { input: 20, unit: "kg/ha", output: 20 }, + { input: 20000, unit: "ton/ha", output: 20 }, + { input: 16, unit: "l/ha", density: 0.8, output: 20 }, + { input: 16000, unit: "m3/ha", density: 0.8, output: 20 }, + { input: 0, unit: "kg/ha", output: 0 }, + { input: 0, unit: "ton/ha", output: 0 }, + { input: 0, unit: "l/ha", density: 0.8, output: 0 }, + { input: 0, unit: "m3/ha", density: 0.8, output: 0 }, + { + input: 20, + unit: "l/ha", + density: undefined, + output: null, + }, + { + input: 20, + unit: "m3/ha", + density: undefined, + output: null, + }, + { + input: 20, + unit: "ft3/ha" as AppAmountUnit, + density: 2, + output: null, + }, + ] + + for (const { input, unit, density, output } of tests) { + it( + density !== undefined + ? `should convert kg/ha to ${unit} with density ${density} kg/l` + : `should convert kg/ha to ${unit} without density specified`, + () => { + const value = fromKgPerHa(input, unit, density) + expect(value !== null ? value.toNumber() : null).toBe(output) + }, + ) + } + + it("should accept input of type Decimal", () => { + expect( + toKgPerHa(new Decimal(20), "m3/ha", new Decimal(2)).toNumber(), + ).toBe(40000) + }) +}) + +interface SuggestionUnitTestCase { + rvoCode: string + type: string + unit: AppAmountUnit +} +describe("suggestUnitFromRvoCode", () => { + const tests: SuggestionUnitTestCase[] = [ + { rvoCode: "10", type: "slurry", unit: "m3/ha" }, + { rvoCode: "11", type: "slurry", unit: "m3/ha" }, + { rvoCode: "12", type: "slurry", unit: "m3/ha" }, + { rvoCode: "13", type: "slurry", unit: "m3/ha" }, + { rvoCode: "14", type: "slurry", unit: "m3/ha" }, + { rvoCode: "30", type: "slurry", unit: "m3/ha" }, + { rvoCode: "31", type: "slurry", unit: "m3/ha" }, + { rvoCode: "32", type: "slurry", unit: "m3/ha" }, + { rvoCode: "33", type: "slurry", unit: "m3/ha" }, + { rvoCode: "34", type: "slurry", unit: "m3/ha" }, + { rvoCode: "115", type: "liquid", unit: "l/ha" }, + { rvoCode: "116", type: "liquid", unit: "l/ha" }, + { rvoCode: "120", type: "liquid", unit: "l/ha" }, + { rvoCode: "107", type: "compost", unit: "ton/ha" }, + { rvoCode: "108", type: "compost", unit: "ton/ha" }, + { rvoCode: "109", type: "compost", unit: "ton/ha" }, + { rvoCode: "111", type: "compost", unit: "ton/ha" }, + { rvoCode: "112", type: "compost", unit: "ton/ha" }, + { rvoCode: "113", type: "other", unit: "kg/ha" }, + { rvoCode: "114", type: "other", unit: "kg/ha" }, + ] + + for (const { rvoCode, type, unit } of tests) { + it(`should suggest ${unit} for ${type} ${rvoCode}`, () => { + expect(suggestUnitFromRvoCode(rvoCode)).toBe(unit) + }) + } +}) diff --git a/fdm-core/src/unit-conversion.ts b/fdm-core/src/unit-conversion.ts new file mode 100644 index 000000000..e0fd104b6 --- /dev/null +++ b/fdm-core/src/unit-conversion.ts @@ -0,0 +1,108 @@ +import Decimal from "decimal.js" + +export type AppAmountUnit = "kg/ha" | "l/ha" | "m3/ha" | "ton/ha" + +export const APP_AMOUNT_UNITS: { value: AppAmountUnit; label: string }[] = [ + { value: "kg/ha", label: "kg/ha" }, + { value: "l/ha", label: "l/ha" }, + { value: "m3/ha", label: "m3/ha" }, + { value: "ton/ha", label: "ton/ha" }, +] + +/** + * Convert a user-entered amount (in display unit) to kg/ha for storage. + * Uses Decimal.js to avoid floating-point rounding errors. + * Throws if conversion requires density but density is null/undefined/0. + */ +export function toKgPerHa( + value: number | Decimal | string, + unit: AppAmountUnit, + density?: number | Decimal | null, // kg/l +): Decimal { + const d = new Decimal(value) + switch (unit) { + case "kg/ha": + return new Decimal(d) + case "ton/ha": + return new Decimal(1000).times(d) + case "l/ha": + if (!density) + throw new Error( + "Density (p_density) is required for l/ha → kg/ha conversion", + ) + return new Decimal(density).times(d) + case "m3/ha": + if (!density) + throw new Error( + "Density (p_density) is required for m3/ha → kg/ha conversion", + ) + return new Decimal(1000).times(d).times(density) + default: + throw new Error(`${unit} → kg/ha conversion is not supported`) + } +} + +/** + * Convert a stored kg/ha value back to the preferred display unit. + * Uses Decimal.js to avoid floating-point rounding errors. + * Returns null if conversion requires density but density is missing. + */ +export function fromKgPerHa( + valueKgPerHa: number | Decimal | string, + unit: AppAmountUnit, + density?: number | Decimal | null, // kg/l +): Decimal | null { + const d = new Decimal(valueKgPerHa) + switch (unit) { + case "kg/ha": + return d + case "ton/ha": + return d.dividedBy(1000) + case "l/ha": + if (!density) return null + return d.dividedBy(new Decimal(density)) + case "m3/ha": + if (!density) return null + return d.dividedBy(new Decimal(density).times(1000)) + default: + return null + } +} + +/** + * Suggest a default display unit based on an RVO fertilizer type code. + * The suggestion is a sensible starting point; the user can always override it. + * + * Mapping rationale (RVO mestcode ranges): + * - Slurry / drijfmest codes → m3/ha + * - Liquid concentrates / digestate → l/ha + * - Compost / solid organic matter → ton/ha + * - Mineral / other → kg/ha (default) + * + * The exact code-to-unit mapping should be reviewed with domain experts during + * implementation and can be updated independently of the rest of the logic. + */ +export function suggestUnitFromRvoCode(p_type_rvo: string): AppAmountUnit { + // Slurry codes (drijfmest, digestaat) — volume in m3 + const slurryCodes = new Set([ + "10", + "11", + "12", + "13", + "14", + "30", + "31", + "32", + "33", + "34", + ]) + // Liquid concentrate codes (vloeibare meststoffen) — volume in l + const liquidCodes = new Set(["115", "116", "120"]) + // Compost / solid organic matter codes — mass in ton + const compostCodes = new Set(["107", "108", "109", "111", "112"]) + + if (slurryCodes.has(p_type_rvo)) return "m3/ha" + if (liquidCodes.has(p_type_rvo)) return "l/ha" + if (compostCodes.has(p_type_rvo)) return "ton/ha" + return "kg/ha" +} diff --git a/fdm-data/src/fertilizers/d.ts b/fdm-data/src/fertilizers/d.ts index 7b1df1d6b..0e8b835e2 100644 --- a/fdm-data/src/fertilizers/d.ts +++ b/fdm-data/src/fertilizers/d.ts @@ -19,6 +19,7 @@ export interface CatalogueFertilizerItem { p_name_en?: string | null | undefined p_description?: string | null | undefined p_app_method_options?: ApplicationMethods[] | null | undefined + p_app_amount_unit?: "kg/ha" | "ton/ha" | "l/ha" | "m3/ha" | undefined p_ef_nh3?: number | null p_dm?: number | null p_density?: number | null From a30916f6a66e5a1d8de15f3501f55e44b6e31eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 8 Apr 2026 16:47:10 +0200 Subject: [PATCH 02/38] Adapt fdm-app to use the new fertilizer application amount display value and units --- fdm-agents/src/agents/gerrit/agent.ts | 10 +- .../fertilizer-applications/columns.tsx | 14 ++- .../blocks/fertilizer-applications/form.tsx | 70 +++++++++-- .../fertilizer-applications/formschema.tsx | 2 +- .../blocks/fertilizer-applications/list.tsx | 6 +- .../fertilizer-applications/types.d.tsx | 2 + fdm-app/app/components/blocks/gerrit/types.ts | 2 +- ...calendar.field.$b_id.fertilizer._index.tsx | 6 +- ...farm.$calendar.field.fertilizer._index.tsx | 3 +- .../farm.$b_id_farm.$calendar.gerrit.tsx | 6 +- ....$calendar.rotation_.fertilizer._index.tsx | 3 +- fdm-core/src/fertilizer.d.ts | 2 + fdm-core/src/fertilizer.ts | 116 +++++++++++++++--- fdm-core/src/index.ts | 1 + 14 files changed, 191 insertions(+), 52 deletions(-) diff --git a/fdm-agents/src/agents/gerrit/agent.ts b/fdm-agents/src/agents/gerrit/agent.ts index 75728cefa..e237999f3 100644 --- a/fdm-agents/src/agents/gerrit/agent.ts +++ b/fdm-agents/src/agents/gerrit/agent.ts @@ -39,9 +39,9 @@ IMPORTANT CONSTRAINTS: 5. BUFFER STRIPS: Fields designated as buffer strips ("b_bufferstrip": true) MUST NOT receive any fertilizer applications. Ensure your plan contains zero applications for these fields. 6. APPLICATION METHOD: For each application, you must propose a valid "p_app_method". Choose ONLY from the "p_app_method_options" returned by the search tool for that specific fertilizer. 7. REALISTIC DATES: Ensure all "p_app_date" values are realistic for the crop type, cultivation season, and Dutch climate. Use the provided "b_lu_start" (sowing/start date) as a critical reference point for each crop. -8. REALISTIC APPLICATION AMOUNTS: Ensure the proposed "p_app_amount" per application matches the technical capabilities of common farming equipment. If the total advice requires more, you MUST split it into multiple applications on different dates. - - slurry (drijfmest): 15,000 - 30,000 kg/ha per application (15-30 m³/ha). - - Solid manure / compost (vaste mest): 10,000 - 30,000 kg/ha per application (10-30 t/ha). +8. REALISTIC APPLICATION AMOUNTS: Ensure the proposed "p_app_amount_display" per application matches the technical capabilities of common farming equipment. If the total advice requires more, you MUST split it into multiple applications on different dates. + - slurry (drijfmest): 15 - 30 m³/ha per application. + - Solid manure / compost (vaste mest): 10 - 30 t/ha per application. - Mineral fertilizers: 50 - 450 kg/ha per application. 9. PRIORITIZATION: If legal norms (especially Nitrogen or Phosphate) limit the total nutrient space on the farm, prioritize fulfilling the nutrient advice for high-value crops (e.g., potatoes, onions, sugar beets, vegetables) over lower-value crops or grasslands. Strategy should focus on maximizing the economic return of the limited nutrient space. 10. ORGANIC FARMING: If "Organic Farming" is YES, you MUST NOT use any mineral fertilizers ("p_type": "mineral") in the plan. @@ -83,7 +83,7 @@ Your final response MUST be a JSON object with exactly this structure (all field { "b_id": "string", "applications": [ - { "p_id_catalogue": "string", "p_app_amount": number, "p_app_date": "YYYY-MM-DD", "p_app_method": "string" } + { "p_id_catalogue": "string", "p_app_amount_display": number, "p_app_date": "YYYY-MM-DD", "p_app_method": "string" } ], "fieldMetrics": { "advice": { @@ -136,7 +136,7 @@ CALCULATOR REFERENCE (units and semantics for the simulation tool): - "norms.manure / nitrogen / phosphate": the legal maximum (farm total in kg, field level in kg/ha). Field level results include a "normSource" string explaining the origin of the limit. - "omBalance" (organische stofbalans): net organic matter balance, kg EOM/ha. Positive = good. Aim for ≥ 0. - "nBalance": nitrogen balance structured exactly as fdm-calculator outputs. "nBalance.balance" and "nBalance.target" are in kg N/ha. "nBalance.emission.ammonia.total" and "nBalance.emission.nitrate.total" are also in kg N/ha. The farm-level averages are automatically area-weighted by the simulation tool. nBalance.balance must be ≤ nBalance.target if keepNitrogenBalanceBelowTarget is YES. -- "p_app_amount": application amount — **always in kg/ha, regardless of fertilizer type**. +- "p_app_amount_display": application amount — **in one of the units below**. - Liquid manure / digestate / slurry: convert m³/ha → kg/ha using 1 m³ = 1000 kg. Round to nearest 1000. Example: 18 m³/ha = 18000 kg/ha. - Solid manure / compost: convert t/ha → kg/ha using 1 t = 1000 kg. Round to nearest 1000. Example: 20 t/ha = 20000 kg/ha. - Mineral fertilizers: already in kg/ha, round to nearest 5 or 10. Example: 200 kg/ha KAS. diff --git a/fdm-app/app/components/blocks/fertilizer-applications/columns.tsx b/fdm-app/app/components/blocks/fertilizer-applications/columns.tsx index ffe12121e..325d8c637 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/columns.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/columns.tsx @@ -61,14 +61,16 @@ function formatDateRange(dates: Date[]) { * @param numbers array of numbers. Nulls and undefined items are not allowed. * @returns the formatted string. */ -function formatNumberRange(numbers: number[], unit = "") { +function formatNumberRange(numbers: number[], unit = "", precision = 2) { if (numbers.length === 0) return "" const firstNumber = numbers[0] const lastNumber = numbers[numbers.length - 1] + const pow = 10 ** precision + const round = (x: number) => Math.round(pow * x) / pow return firstNumber === lastNumber || Math.abs(lastNumber - firstNumber) < Math.abs(firstNumber) / 100 - ? `${firstNumber} ${unit}` - : `${firstNumber} - ${lastNumber} ${unit}` + ? `${round(firstNumber)} ${unit}` + : `${round(firstNumber)} - ${round(lastNumber)} ${unit}` } /** @@ -163,10 +165,10 @@ export const columns: ColumnDef[] = [ cell: ({ row }) => formatNumberRange( row.original.applications - .map((application) => application.p_app_amount) - .filter((amount) => amount !== null) + .map((application) => application.p_app_amount_display) + .filter((amount) => amount !== null && amount !== undefined) .sort((a, b) => a - b), - "kg / ha", + row.original.applications[0]?.p_app_amount_unit ?? "kg/ha", ), }, { diff --git a/fdm-app/app/components/blocks/fertilizer-applications/form.tsx b/fdm-app/app/components/blocks/fertilizer-applications/form.tsx index 8f2fb98f1..f03b29dbc 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/form.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/form.tsx @@ -1,9 +1,10 @@ import { zodResolver } from "@hookform/resolvers/zod" +import type { AppAmountUnit } from "@nmi-agro/fdm-core" import { formatDate } from "date-fns" import { nl } from "date-fns/locale" import { Plus } from "lucide-react" import type { MouseEvent } from "react" -import { useEffect, useId } from "react" +import { useEffect, useId, useState } from "react" import { Controller } from "react-hook-form" import type { Navigation } from "react-router" import { Form, useNavigate, useSearchParams } from "react-router" @@ -41,6 +42,7 @@ export type FertilizerOption = { value: string label: string applicationMethodOptions?: { value: string; label: string }[] + p_app_amount_unit: AppAmountUnit } /** @@ -92,7 +94,7 @@ export function FertilizerApplicationForm({ : fertilizerApplication?.p_app_id, p_id: fertilizerApplication?.p_id, p_app_method: fertilizerApplication?.p_app_method, - p_app_amount: undefined, // Handled through an effect due to blank behavior + p_app_amount_display: undefined, // Handled through an effect due to blank behavior p_app_date: fertilizerApplication?.p_app_date ? fertilizerApplication.p_app_date : exampleFertilizerApplication @@ -107,6 +109,17 @@ export function FertilizerApplicationForm({ const selectedFertilizer = options.find((option) => option.value === p_id) const isSubmitting = navigation.state !== "idle" + // Conversion factor used to transform the application amount when the user changes the fertilizer + // It is also used to show a more helpful application amount example + // 1 kg/L fertilizer density is assumed + const conversionFactors = { + "kg/ha": 1000, + "ton/ha": 1, + "l/ha": 1000, + "m3/ha": 1, + } as const + + // If the user switched the fertilizer, clear the application method useEffect(() => { if ( p_id && @@ -138,6 +151,7 @@ export function FertilizerApplicationForm({ const fieldFertilizerFormStore = useFieldFertilizerFormStore() + // If the user had a saved fertilizer form and was creating a new fertilizer, fill the form back in useEffect(() => { if (b_id_farm && b_id_or_b_lu_catalogue) { const savedFormValues = fieldFertilizerFormStore.load( @@ -165,11 +179,43 @@ export function FertilizerApplicationForm({ ]) useEffect(() => { - const p_app_amount = fertilizerApplication?.p_app_amount - if (p_app_amount !== null && typeof p_app_amount !== "undefined") { - form.setValue("p_app_amount", p_app_amount) + const p_app_amount_display = fertilizerApplication?.p_app_amount_display + if ( + p_app_amount_display !== null && + typeof p_app_amount_display !== "undefined" + ) { + form.setValue( + "p_app_amount_display", + Math.round(100 * p_app_amount_display) / 100, + ) + } + }, [p_id, fertilizerApplication?.p_app_amount_display, form.setValue]) + + // Transform the application amount based on the changing application units + const currentApplicationUnit = + options.find((opt) => opt.value === p_id)?.p_app_amount_unit ?? "kg/ha" + console.log(currentApplicationUnit) + const currentConversionFactor = conversionFactors[currentApplicationUnit] + const [lastConversionFactor, setLastConversionFactor] = useState( + currentConversionFactor, + ) + useEffect(() => { + console.log(form.getValues()) + const p_app_amount_display = form.getValues().p_app_amount_display + if (currentConversionFactor !== lastConversionFactor) { + if (p_app_amount_display !== undefined) { + form.setValue( + "p_app_amount_display", + Math.round( + (100 * + (p_app_amount_display * currentConversionFactor)) / + lastConversionFactor, + ) / 100, + ) + setLastConversionFactor(currentConversionFactor) + } } - }, [fertilizerApplication?.p_app_amount, form.setValue]) + }, [form, lastConversionFactor, currentConversionFactor]) // Change fertilizer selection if the user has added a new fertilizer const new_p_id = searchParams.get("p_id") @@ -293,21 +339,23 @@ export function FertilizerApplicationForm({ )} /> ( - Hoeveelheid + + Hoeveelheid ({currentApplicationUnit}) + (typeof val === "string" && val !== "" ? Number(val) : val), z .number({ diff --git a/fdm-app/app/components/blocks/fertilizer-applications/list.tsx b/fdm-app/app/components/blocks/fertilizer-applications/list.tsx index 8f4780d43..160dcc85e 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/list.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/list.tsx @@ -106,8 +106,10 @@ export function FertilizerApplicationsList({

- {application.p_app_amount} kg / - ha + { + application.p_app_amount_display + }{" "} + {application.p_app_amount_unit}

{application.p_app_method diff --git a/fdm-app/app/components/blocks/fertilizer-applications/types.d.tsx b/fdm-app/app/components/blocks/fertilizer-applications/types.d.tsx index 5a8d3b41a..cca98d834 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/types.d.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/types.d.tsx @@ -1,3 +1,4 @@ +import type { AppAmountUnit } from "@nmi-agro/fdm-core" import type { ApplicationMethods } from "@nmi-agro/fdm-data" export interface FertilizerApplication { @@ -16,6 +17,7 @@ export interface FertilizerOption { value: ApplicationMethods label: string }[] + p_app_amount_unit: AppAmountUnit } export interface FertilizerApplicationsFormProps { diff --git a/fdm-app/app/components/blocks/gerrit/types.ts b/fdm-app/app/components/blocks/gerrit/types.ts index 1b4611490..6ccd9dad9 100644 --- a/fdm-app/app/components/blocks/gerrit/types.ts +++ b/fdm-app/app/components/blocks/gerrit/types.ts @@ -10,7 +10,7 @@ import type { export interface ParsedPlanApplication { p_id_catalogue: string - p_app_amount: number + p_app_amount_display: number p_app_date: string p_app_method?: string | null p_app_method_name?: string | null diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx index ba1be5f3a..cc84f290b 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx @@ -127,6 +127,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { value: fertilizer.p_id, label: fertilizer.p_name_nl, applicationMethodOptions: applicationMethodOptions, + p_app_amount_unit: fertilizer.p_app_amount_unit, } }) @@ -334,14 +335,15 @@ export async function action({ request, params }: ActionFunctionArgs) { request, FormSchema, ) - const { p_id, p_app_amount, p_app_date, p_app_method } = formValues + const { p_id, p_app_amount_display, p_app_date, p_app_method } = + formValues await addFertilizerApplication( fdm, session.principal_id, b_id, p_id, - p_app_amount, + p_app_amount_display, p_app_method, p_app_date, ) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer._index.tsx index 189ce8e74..c87e5de6a 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer._index.tsx @@ -185,6 +185,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { value: fertilizer.p_id, label: fertilizer.p_name_nl, applicationMethodOptions: applicationMethodOptions, + p_app_amount_unit: fertilizer.p_app_amount_unit, } }) @@ -681,7 +682,7 @@ export async function action({ request, params }: ActionFunctionArgs) { session.principal_id, b_id, validatedData.p_id, - validatedData.p_app_amount, + validatedData.p_app_amount_display, validatedData.p_app_method, validatedData.p_app_date, ), diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx index d55117b88..397fe38fd 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx @@ -167,7 +167,7 @@ async function computePlanMetrics( b_bufferstrip: boolean applications: Array<{ p_id_catalogue: string - p_app_amount: number + p_app_amount_display: number p_app_date: string p_app_method?: string | null }> @@ -268,7 +268,7 @@ async function computePlanMetrics( p_id: fert?.p_id ?? app.p_id_catalogue, p_id_catalogue: app.p_id_catalogue, p_name_nl: fert?.p_name_nl ?? null, - p_app_amount: app.p_app_amount, + p_app_amount_display: app.p_app_amount_display, p_app_date: new Date(app.p_app_date), p_app_id: `plan-${field.b_id}-${i}`, p_app_method: app.p_app_method ?? null, @@ -858,7 +858,7 @@ export async function action({ request, params }: ActionFunctionArgs) { session.principal_id, field.b_id, fertilizer.p_id, - app.p_app_amount, + app.p_app_amount_display, app.p_app_method, new Date(app.p_app_date), ) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation_.fertilizer._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation_.fertilizer._index.tsx index c629b0e7f..6eb48d770 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation_.fertilizer._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation_.fertilizer._index.tsx @@ -243,6 +243,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { value: fertilizer.p_id, label: fertilizer.p_name_nl, applicationMethodOptions: applicationMethodOptions, + p_app_amount_unit: fertilizer.p_app_amount_unit, } }, ) @@ -715,7 +716,7 @@ export async function action({ request, params }: ActionFunctionArgs) { session.principal_id, fieldId, validatedData.p_id, - validatedData.p_app_amount, + validatedData.p_app_amount_display, validatedData.p_app_method, validatedData.p_app_date, ), diff --git a/fdm-core/src/fertilizer.d.ts b/fdm-core/src/fertilizer.d.ts index 75a859cd4..32af4c713 100644 --- a/fdm-core/src/fertilizer.d.ts +++ b/fdm-core/src/fertilizer.d.ts @@ -72,6 +72,8 @@ export interface FertilizerApplication { p_id_catalogue: string p_name_nl: string | null p_app_amount: number | null + p_app_amount_unit?: string | null + p_app_amount_display?: number | null p_app_method: ApplicationMethods | null p_app_date: Date p_app_id: string diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 60a8b1491..3303287fd 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -19,7 +19,12 @@ import type { } from "./fertilizer.d" import { createId } from "./id" import type { Timeframe } from "./timeframe" -import { APP_AMOUNT_UNITS, type AppAmountUnit } from "./unit-conversion" +import { + APP_AMOUNT_UNITS, + type AppAmountUnit, + fromKgPerHa, + toKgPerHa, +} from "./unit-conversion" /** * Retrieves all fertilizers from the enabled catalogues for a farm. @@ -755,7 +760,7 @@ export async function addFertilizerApplication( principal_id: PrincipalId, b_id: schema.fertilizerApplicationTypeInsert["b_id"], p_id: schema.fertilizerApplicationTypeInsert["p_id"], - p_app_amount: schema.fertilizerApplicationTypeInsert["p_app_amount"], + p_app_amount_display: schema.fertilizerApplicationTypeInsert["p_app_amount"], p_app_method: schema.fertilizerApplicationTypeInsert["p_app_method"], p_app_date: schema.fertilizerApplicationTypeInsert["p_app_date"], ): Promise { @@ -778,18 +783,19 @@ export async function addFertilizerApplication( throw new Error(`Field with b_id ${b_id} does not exist`) } - // Validate that the fertilizer exists - const fertilizerExists = await fdm - .select() - .from(schema.fertilizers) - .where(eq(schema.fertilizers.p_id, p_id)) - .limit(1) - if (fertilizerExists.length === 0) { - throw new Error(`Fertilizer with p_id ${p_id} does not exist`) - } + // Validate that the fertilizer exists and get it + const fertilizer = await getFertilizer(fdm, p_id) const p_app_id = createId() + const p_app_amount = + p_app_amount_display !== null && p_app_amount_display !== undefined + ? toKgPerHa( + p_app_amount_display, + fertilizer.p_app_amount_unit ?? "kg/ha", + ) + : null + await fdm.insert(schema.fertilizerApplication).values({ p_app_id, b_id, @@ -804,7 +810,7 @@ export async function addFertilizerApplication( throw handleError(err, "Exception for addFertilizerApplication", { b_id, p_id, - p_app_amount, + p_app_amount_display, p_app_method, p_app_date, }) @@ -829,7 +835,7 @@ export async function updateFertilizerApplication( principal_id: PrincipalId, p_app_id: schema.fertilizerApplicationTypeInsert["p_app_id"], p_id: schema.fertilizerApplicationTypeInsert["p_id"], - p_app_amount: schema.fertilizerApplicationTypeInsert["p_app_amount"], + p_app_amount_display: schema.fertilizerApplicationTypeInsert["p_app_amount"], p_app_method: schema.fertilizerApplicationTypeInsert["p_app_method"], p_app_date: schema.fertilizerApplicationTypeInsert["p_app_date"], ): Promise { @@ -842,6 +848,14 @@ export async function updateFertilizerApplication( principal_id, "updateFertilizerApplication", ) + const fertilizer = await getFertilizer(fdm, p_id) + const p_app_amount = + p_app_amount_display !== null && p_app_amount_display !== undefined + ? toKgPerHa( + p_app_amount_display, + fertilizer.p_app_amount_unit ?? "kg/ha", + ) + : p_app_amount_display await fdm .update(schema.fertilizerApplication) .set({ p_id, p_app_amount, p_app_method, p_app_date }) @@ -850,7 +864,7 @@ export async function updateFertilizerApplication( throw handleError(err, "Exception for updateFertilizerApplication", { p_app_id, p_id, - p_app_amount, + p_app_amount_display, p_app_method, p_app_date, }) @@ -894,6 +908,37 @@ export async function removeFertilizerApplication( } } +/** + * Extends the given fertilizer application with computed data and removes unknown properties + * @param app fertilizer application + * @returns the same fertilizer application with p_app_amount_display filled in and properties + * that do not belong to FertilizerApplication removed + */ +function extendFertilizerApplication( + app: T, + p_app_amount_unit: AppAmountUnit, + p_density: number | null, +): FertilizerApplication { + return { + p_id: app.p_id, + p_id_catalogue: app.p_id_catalogue, + p_name_nl: app.p_name_nl, + p_app_date: app.p_app_date, + p_app_method: app.p_app_method, + p_app_amount: app.p_app_amount, + p_app_amount_display: + app.p_app_amount !== null && app.p_app_amount !== undefined + ? fromKgPerHa( + app.p_app_amount, + p_app_amount_unit, + p_density, + )?.toNumber() + : app.p_app_amount, + p_app_amount_unit: p_app_amount_unit, + p_app_id: app.p_app_id, + } +} + /** * Retrieves a fertilizer application record by its unique identifier. * @@ -928,6 +973,9 @@ export async function getFertilizerApplication( p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, p_name_nl: schema.fertilizersCatalogue.p_name_nl, p_app_amount: schema.fertilizerApplication.p_app_amount, + p_app_amount_unit: + schema.fertilizersCatalogue.p_app_amount_unit, + p_density: schema.fertilizersCatalogue.p_density, p_app_method: schema.fertilizerApplication.p_app_method, p_app_date: schema.fertilizerApplication.p_app_date, p_app_id: schema.fertilizerApplication.p_app_id, @@ -949,7 +997,13 @@ export async function getFertilizerApplication( ) .where(eq(schema.fertilizerApplication.p_app_id, p_app_id)) - return result[0] || null + return result.length > 0 + ? extendFertilizerApplication( + result[0], + result[0].p_app_amount_unit, + result[0].p_density, + ) + : null } catch (err) { throw handleError(err, "Exception for getFertilizerApplication", { p_app_id, @@ -987,12 +1041,15 @@ export async function getFertilizerApplications( "getFertilizerApplications", ) - return await fdm + const results = await fdm .select({ p_id: schema.fertilizerApplication.p_id, p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, p_name_nl: schema.fertilizersCatalogue.p_name_nl, p_app_amount: schema.fertilizerApplication.p_app_amount, + p_app_amount_unit: + schema.fertilizersCatalogue.p_app_amount_unit, + p_density: schema.fertilizersCatalogue.p_density, p_app_method: schema.fertilizerApplication.p_app_method, p_app_date: schema.fertilizerApplication.p_app_date, p_app_id: schema.fertilizerApplication.p_app_id, @@ -1032,6 +1089,20 @@ export async function getFertilizerApplications( : eq(schema.fertilizerApplication.b_id, b_id), ) .orderBy(desc(schema.fertilizerApplication.p_app_date)) + + return results.map( + ( + result: FertilizerApplication & { + p_app_amount_unit: AppAmountUnit + p_density: number | null + }, + ) => + extendFertilizerApplication( + result, + result.p_app_amount_unit, + result.p_density, + ), + ) } catch (err) { throw handleError(err, "Exception for getFertilizerApplications", { b_id, @@ -1075,6 +1146,9 @@ export async function getFertilizerApplicationsForFarm( p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, p_name_nl: schema.fertilizersCatalogue.p_name_nl, p_app_amount: schema.fertilizerApplication.p_app_amount, + p_app_amount_unit: + schema.fertilizersCatalogue.p_app_amount_unit, + p_density: schema.fertilizersCatalogue.p_density, p_app_method: schema.fertilizerApplication.p_app_method, p_app_date: schema.fertilizerApplication.p_app_date, p_app_id: schema.fertilizerApplication.p_app_id, @@ -1128,12 +1202,16 @@ export async function getFertilizerApplicationsForFarm( for (const row of rows) { if (!row.b_id) continue // b_id is used for grouping only and is not part of FertilizerApplication - const { b_id, ...fertilizerApplication } = row - const existing = result.get(b_id) + const fertilizerApplication = extendFertilizerApplication( + row, + row.p_app_amount_unit, + row.p_density, + ) + const existing = result.get(row.b_id) if (existing) { existing.push(fertilizerApplication as FertilizerApplication) } else { - result.set(b_id, [ + result.set(row.b_id, [ fertilizerApplication as FertilizerApplication, ]) } diff --git a/fdm-core/src/index.ts b/fdm-core/src/index.ts index a0c4650c4..71c077dd7 100644 --- a/fdm-core/src/index.ts +++ b/fdm-core/src/index.ts @@ -193,4 +193,5 @@ export type { SoilParameters, } from "./soil.d" export type { Timeframe } from "./timeframe.d" +export type { AppAmountUnit } from "./unit-conversion" export { fdmSchema } From 3ce3f81256b84d1311b1ffda2eeabd9785f48964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 8 Apr 2026 16:54:26 +0200 Subject: [PATCH 03/38] Add changeset --- .changeset/late-cycles-tan.md | 5 +++++ .changeset/ninety-sloths-notice.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/late-cycles-tan.md create mode 100644 .changeset/ninety-sloths-notice.md diff --git a/.changeset/late-cycles-tan.md b/.changeset/late-cycles-tan.md new file mode 100644 index 000000000..d0f86c2a2 --- /dev/null +++ b/.changeset/late-cycles-tan.md @@ -0,0 +1,5 @@ +--- +"@nmi-agro/fdm-app": minor +--- + +Fertilizer application amounts are now shown in units relevant to the fertilizer type used, possibly saving the user from converting application amounts to kg/ha all the time. diff --git a/.changeset/ninety-sloths-notice.md b/.changeset/ninety-sloths-notice.md new file mode 100644 index 000000000..00adb323d --- /dev/null +++ b/.changeset/ninety-sloths-notice.md @@ -0,0 +1,5 @@ +--- +"@nmi-agro/fdm-core": minor +--- + +addFertilizerApplication and updateFertilizerApplication functions now expect an application value in the unit defined for the fertilizer, instead of kg/ha for every fertilizer. The unit is included in the return values of `getFertilizer`, `getFertilizers` etc. as `p_app_amount_unit`. `getFertilizerApplication`, `getFertilizerApplications` etc. now include `p_app_amount_display` and `p_app_amount_unit` which are to be shown to the user instead of p_app_amount and `kg/ha`. From c3e20c90bf73eae3306040fd9e6e1e62461b312f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Fri, 10 Apr 2026 10:03:45 +0200 Subject: [PATCH 04/38] Address nitpicks --- .../blocks/fertilizer-applications/form.tsx | 6 +- .../blocks/fertilizer-applications/list.tsx | 15 +- fdm-core/src/fertilizer.d.ts | 4 +- fdm-core/src/fertilizer.test.ts | 131 ++++++++++++++++++ fdm-core/src/fertilizer.ts | 19 +-- fdm-core/src/unit-conversion.test.ts | 12 +- 6 files changed, 165 insertions(+), 22 deletions(-) diff --git a/fdm-app/app/components/blocks/fertilizer-applications/form.tsx b/fdm-app/app/components/blocks/fertilizer-applications/form.tsx index f03b29dbc..a26283d32 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/form.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/form.tsx @@ -189,19 +189,17 @@ export function FertilizerApplicationForm({ Math.round(100 * p_app_amount_display) / 100, ) } - }, [p_id, fertilizerApplication?.p_app_amount_display, form.setValue]) + }, [fertilizerApplication?.p_app_amount_display, form.setValue]) // Transform the application amount based on the changing application units const currentApplicationUnit = options.find((opt) => opt.value === p_id)?.p_app_amount_unit ?? "kg/ha" - console.log(currentApplicationUnit) const currentConversionFactor = conversionFactors[currentApplicationUnit] const [lastConversionFactor, setLastConversionFactor] = useState( currentConversionFactor, ) useEffect(() => { - console.log(form.getValues()) - const p_app_amount_display = form.getValues().p_app_amount_display + const p_app_amount_display = form.getValues("p_app_amount_display") if (currentConversionFactor !== lastConversionFactor) { if (p_app_amount_display !== undefined) { form.setValue( diff --git a/fdm-app/app/components/blocks/fertilizer-applications/list.tsx b/fdm-app/app/components/blocks/fertilizer-applications/list.tsx index 160dcc85e..e44f3f694 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/list.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/list.tsx @@ -106,10 +106,17 @@ export function FertilizerApplicationsList({

- { - application.p_app_amount_display - }{" "} - {application.p_app_amount_unit} + {application.p_app_amount_display !== + null && + application.p_app_amount_display !== + undefined + ? `${application.p_app_amount_display} ${application.p_app_amount_unit}` + : application.p_app_amount !== + null && + application.p_app_amount !== + undefined + ? `${application.p_app_amount} kg/ha` + : "Onbekend hoeveelheid"}

{application.p_app_method diff --git a/fdm-core/src/fertilizer.d.ts b/fdm-core/src/fertilizer.d.ts index 32af4c713..97c33afe1 100644 --- a/fdm-core/src/fertilizer.d.ts +++ b/fdm-core/src/fertilizer.d.ts @@ -9,7 +9,7 @@ export interface FertilizerCatalogue { p_name_en: string | null p_description: string | null p_app_method_options: ApplicationMethods[] | null - p_app_amount_unit: AppAmountUnit | undefined + p_app_amount_unit: AppAmountUnit | null p_dm: number | null p_density: number | null p_om: number | null @@ -72,7 +72,7 @@ export interface FertilizerApplication { p_id_catalogue: string p_name_nl: string | null p_app_amount: number | null - p_app_amount_unit?: string | null + p_app_amount_unit?: AppAmountUnit | null p_app_amount_display?: number | null p_app_method: ApplicationMethods | null p_app_date: Date diff --git a/fdm-core/src/fertilizer.test.ts b/fdm-core/src/fertilizer.test.ts index 5189dbc8b..72fcbc1ef 100644 --- a/fdm-core/src/fertilizer.test.ts +++ b/fdm-core/src/fertilizer.test.ts @@ -1028,6 +1028,7 @@ describe("Fertilizer Data Model", () => { describe("Fertilizer Application", () => { let b_id: string let p_id: string + let p_id_liquid: string beforeAll(async () => { const farmName = "Test Farm" @@ -1138,6 +1139,76 @@ describe("Fertilizer Data Model", () => { p_acquiring_amount, p_acquiring_date, ) + + // Fertilizer whose application amount is given in volume per ha + const p_id_catalogue_liquid = await addFertilizerToCatalogue( + fdm, + principal_id, + b_id_farm, + { + p_name_nl, + p_name_en, + p_description, + p_app_method_options: [], + p_app_amount_unit: "l/ha", + p_dm: 37, + p_density: 1.2, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_no3_rt: 400, + p_nh4_rt: 410, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + p_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_rt: 390, + p_ef_nh3: 0.8, + p_type: "mineral", + p_type_rvo: "115", + }, + ) + + const p_acquiring_amount_liquid = 1000 + const p_acquiring_date_liquid = new Date() + p_id_liquid = await addFertilizer( + fdm, + principal_id, + p_id_catalogue_liquid, + b_id_farm, + p_acquiring_amount_liquid, + p_acquiring_date_liquid, + ) }) afterAll(async () => { @@ -1170,6 +1241,32 @@ describe("Fertilizer Data Model", () => { expect(fertilizerApplication?.p_app_date).toEqual(p_app_date) }) + it("should add a new fertilizer application with amount specified in volume per ha", async () => { + const p_app_date = new Date("2024-03-15") + + const new_p_app_id = await addFertilizerApplication( + fdm, + principal_id, + b_id, + p_id_liquid, + 100, + "broadcasting", + p_app_date, + ) + expect(new_p_app_id).toBeDefined() + + const fertilizerApplication = await getFertilizerApplication( + fdm, + principal_id, + new_p_app_id, + ) + expect(fertilizerApplication).toBeDefined() + expect(fertilizerApplication?.p_id).toBe(p_id_liquid) + expect(fertilizerApplication?.p_app_amount).toBe(120) + expect(fertilizerApplication?.p_app_method).toBe("broadcasting") + expect(fertilizerApplication?.p_app_date).toEqual(p_app_date) + }) + it("should update a fertilizer application", async () => { const p_app_date1 = new Date("2024-03-15") const p_app_date2 = new Date("2024-04-20") @@ -1204,6 +1301,40 @@ describe("Fertilizer Data Model", () => { expect(updatedApplication?.p_app_date).toEqual(p_app_date2) }) + it("should update a fertilizer application with amount specified in volume per ha", async () => { + const p_app_date1 = new Date("2024-03-15") + const p_app_date2 = new Date("2024-04-20") + + const p_app_id = await addFertilizerApplication( + fdm, + principal_id, + b_id, + p_id_liquid, + 100, + "broadcasting", + p_app_date1, + ) + + await updateFertilizerApplication( + fdm, + principal_id, + p_app_id, + p_id_liquid, + 200, + "injection", + p_app_date2, + ) + + const updatedApplication = await getFertilizerApplication( + fdm, + principal_id, + p_app_id, + ) + expect(updatedApplication?.p_app_amount).toBe(240) + expect(updatedApplication?.p_app_method).toBe("injection") + expect(updatedApplication?.p_app_date).toEqual(p_app_date2) + }) + it("should remove a fertilizer application", async () => { const new_p_app_id = await addFertilizerApplication( fdm, diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 3303287fd..b35eea7ef 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -793,6 +793,7 @@ export async function addFertilizerApplication( ? toKgPerHa( p_app_amount_display, fertilizer.p_app_amount_unit ?? "kg/ha", + fertilizer.p_density, ) : null @@ -854,6 +855,7 @@ export async function updateFertilizerApplication( ? toKgPerHa( p_app_amount_display, fertilizer.p_app_amount_unit ?? "kg/ha", + fertilizer.p_density, ) : p_app_amount_display await fdm @@ -919,6 +921,14 @@ function extendFertilizerApplication( p_app_amount_unit: AppAmountUnit, p_density: number | null, ): FertilizerApplication { + const maybe_p_app_amount_display = + app.p_app_amount !== null && app.p_app_amount !== undefined + ? fromKgPerHa( + app.p_app_amount, + p_app_amount_unit, + p_density, + )?.toNumber() + : app.p_app_amount return { p_id: app.p_id, p_id_catalogue: app.p_id_catalogue, @@ -926,14 +936,7 @@ function extendFertilizerApplication( p_app_date: app.p_app_date, p_app_method: app.p_app_method, p_app_amount: app.p_app_amount, - p_app_amount_display: - app.p_app_amount !== null && app.p_app_amount !== undefined - ? fromKgPerHa( - app.p_app_amount, - p_app_amount_unit, - p_density, - )?.toNumber() - : app.p_app_amount, + p_app_amount_display: maybe_p_app_amount_display ?? null, p_app_amount_unit: p_app_amount_unit, p_app_id: app.p_app_id, } diff --git a/fdm-core/src/unit-conversion.test.ts b/fdm-core/src/unit-conversion.test.ts index 05c1e48d9..ee5424bb3 100644 --- a/fdm-core/src/unit-conversion.test.ts +++ b/fdm-core/src/unit-conversion.test.ts @@ -73,8 +73,8 @@ describe("toKgPerHa", () => { it("should accept input of type Decimal", () => { expect( - toKgPerHa(new Decimal(20), "m3/ha", new Decimal(2)).toNumber(), - ).toBe(40000) + toKgPerHa(new Decimal(10), "m3/ha", new Decimal(2)).toNumber(), + ).toBe(20000) }) }) @@ -122,8 +122,12 @@ describe("fromKgPerHa", () => { it("should accept input of type Decimal", () => { expect( - toKgPerHa(new Decimal(20), "m3/ha", new Decimal(2)).toNumber(), - ).toBe(40000) + fromKgPerHa( + new Decimal(20000), + "m3/ha", + new Decimal(2), + )?.toNumber(), + ).toBe(10) }) }) From ae030416d64c122e886b0d267892ddd790c544ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Fri, 10 Apr 2026 10:24:01 +0200 Subject: [PATCH 05/38] Add fertilizer application units to the bemestingsplan PDF --- .../pdf/bemestingsplan/BemestingsplanPDF.tsx | 26 ++++++++++++++++--- .../blocks/pdf/bemestingsplan/types.d.ts | 4 +++ ...id_farm.$calendar.bemestingsplan[.]pdf.tsx | 4 ++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/fdm-app/app/components/blocks/pdf/bemestingsplan/BemestingsplanPDF.tsx b/fdm-app/app/components/blocks/pdf/bemestingsplan/BemestingsplanPDF.tsx index cca183f59..6f67b9de9 100644 --- a/fdm-app/app/components/blocks/pdf/bemestingsplan/BemestingsplanPDF.tsx +++ b/fdm-app/app/components/blocks/pdf/bemestingsplan/BemestingsplanPDF.tsx @@ -905,15 +905,29 @@ export const BemestingsplanPDF = ({ data }: { data: BemestingsplanData }) => ( if (!acc[app.product]) { acc[app.product] = { amount: 0, + amount_display: 0, + amount_unit: ( + { + "kg/ha": "kg", + "ton/ha": "ton", + "l/ha": "L", + "m3/ha": "m³", + } as const + )[app.quantity_unit], n: 0, nw: 0, p: 0, k: 0, } } - // app.quantity is per ha, so multiply by area + // app.quantity and app.quantity_display are per ha, so multiply by area acc[app.product].amount += app.quantity * f.area + acc[ + app.product + ].amount_display += + app.quantity_display * + f.area acc[app.product].n += app.p_dose_n * f.area acc[app.product].nw += @@ -929,6 +943,12 @@ export const BemestingsplanPDF = ({ data }: { data: BemestingsplanData }) => ( string, { amount: number + amount_display: number + amount_unit: + | "kg" + | "ton" + | "L" + | "m³" n: number nw: number p: number @@ -985,11 +1005,11 @@ export const BemestingsplanPDF = ({ data }: { data: BemestingsplanData }) => ( > {Math.round( - stats.amount, + stats.amount_display, ).toLocaleString( "nl-NL", )}{" "} - kg + {stats.amount_unit} Date: Mon, 13 Apr 2026 11:58:30 +0200 Subject: [PATCH 06/38] Use hardcoded table for RVO code unit suggestions --- fdm-core/src/unit-conversion.test.ts | 69 +++++++------ fdm-core/src/unit-conversion.ts | 139 ++++++++++++++++++++++----- 2 files changed, 151 insertions(+), 57 deletions(-) diff --git a/fdm-core/src/unit-conversion.test.ts b/fdm-core/src/unit-conversion.test.ts index ee5424bb3..0b9219121 100644 --- a/fdm-core/src/unit-conversion.test.ts +++ b/fdm-core/src/unit-conversion.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest" import { type AppAmountUnit, fromKgPerHa, + type RvoUnitSuggestionTableItem, suggestUnitFromRvoCode, toKgPerHa, } from "./unit-conversion" @@ -131,38 +132,44 @@ describe("fromKgPerHa", () => { }) }) -interface SuggestionUnitTestCase { - rvoCode: string - type: string - unit: AppAmountUnit -} describe("suggestUnitFromRvoCode", () => { - const tests: SuggestionUnitTestCase[] = [ - { rvoCode: "10", type: "slurry", unit: "m3/ha" }, - { rvoCode: "11", type: "slurry", unit: "m3/ha" }, - { rvoCode: "12", type: "slurry", unit: "m3/ha" }, - { rvoCode: "13", type: "slurry", unit: "m3/ha" }, - { rvoCode: "14", type: "slurry", unit: "m3/ha" }, - { rvoCode: "30", type: "slurry", unit: "m3/ha" }, - { rvoCode: "31", type: "slurry", unit: "m3/ha" }, - { rvoCode: "32", type: "slurry", unit: "m3/ha" }, - { rvoCode: "33", type: "slurry", unit: "m3/ha" }, - { rvoCode: "34", type: "slurry", unit: "m3/ha" }, - { rvoCode: "115", type: "liquid", unit: "l/ha" }, - { rvoCode: "116", type: "liquid", unit: "l/ha" }, - { rvoCode: "120", type: "liquid", unit: "l/ha" }, - { rvoCode: "107", type: "compost", unit: "ton/ha" }, - { rvoCode: "108", type: "compost", unit: "ton/ha" }, - { rvoCode: "109", type: "compost", unit: "ton/ha" }, - { rvoCode: "111", type: "compost", unit: "ton/ha" }, - { rvoCode: "112", type: "compost", unit: "ton/ha" }, - { rvoCode: "113", type: "other", unit: "kg/ha" }, - { rvoCode: "114", type: "other", unit: "kg/ha" }, - ] + describe("internal table", () => { + it("should return ton/ha for solid cattle manure", () => { + expect(suggestUnitFromRvoCode("10")).toBe("ton/ha") + }) + it("should return m3/ha for swine slurry", () => { + expect(suggestUnitFromRvoCode("42")).toBe("m3/ha") + }) + it("should return l/ha for liquid goat manure", () => { + expect(suggestUnitFromRvoCode("60")).toBe("l/ha") + }) + it("should return kg/ha for mineral fertilizers", () => { + expect(suggestUnitFromRvoCode("115")).toBe("kg/ha") + }) + }) - for (const { rvoCode, type, unit } of tests) { - it(`should suggest ${unit} for ${type} ${rvoCode}`, () => { - expect(suggestUnitFromRvoCode(rvoCode)).toBe(unit) + describe("custom table", () => { + const customTable: RvoUnitSuggestionTableItem[] = [ + { p_type_rvo: "42", type: "other", unit: "kg/ha" }, + { p_type_rvo: "113", type: "solid sewage", unit: "ton/ha" }, + { p_type_rvo: "114", type: "liquid sewage", unit: "l/ha" }, + { p_type_rvo: "115", type: "swine slurry", unit: "m3/ha" }, + ] + + it("should return kg/ha for other fertilizers in custom table", () => { + expect(suggestUnitFromRvoCode("42", customTable)).toBe("kg/ha") }) - } + it("should return ton/ha for solid sewage", () => { + expect(suggestUnitFromRvoCode("113", customTable)).toBe("ton/ha") + }) + it("should return l/ha for liquid sewage in custom table", () => { + expect(suggestUnitFromRvoCode("114", customTable)).toBe("l/ha") + }) + it("should return m3/ha for swine slurry", () => { + expect(suggestUnitFromRvoCode("115", customTable)).toBe("m3/ha") + }) + it("should return kg/ha for code not in table", () => { + expect(suggestUnitFromRvoCode("10", customTable)).toBe("kg/ha") + }) + }) }) diff --git a/fdm-core/src/unit-conversion.ts b/fdm-core/src/unit-conversion.ts index e0fd104b6..e0ee62ded 100644 --- a/fdm-core/src/unit-conversion.ts +++ b/fdm-core/src/unit-conversion.ts @@ -73,36 +73,123 @@ export function fromKgPerHa( * Suggest a default display unit based on an RVO fertilizer type code. * The suggestion is a sensible starting point; the user can always override it. * - * Mapping rationale (RVO mestcode ranges): + * A table of suggestions is provided internally. Callers can pass the table argument to use a different table. + * + * The internal table is based on the Tabel 11 mest codes provided by RVO and is based on the rationale: * - Slurry / drijfmest codes → m3/ha * - Liquid concentrates / digestate → l/ha * - Compost / solid organic matter → ton/ha * - Mineral / other → kg/ha (default) * - * The exact code-to-unit mapping should be reviewed with domain experts during - * implementation and can be updated independently of the rest of the logic. + * @param p_type_rvo: mest code to look for + * @param table: optional: table to use for conversion. The type can be used to add remarks to each item when hardcoding tables. */ -export function suggestUnitFromRvoCode(p_type_rvo: string): AppAmountUnit { - // Slurry codes (drijfmest, digestaat) — volume in m3 - const slurryCodes = new Set([ - "10", - "11", - "12", - "13", - "14", - "30", - "31", - "32", - "33", - "34", - ]) - // Liquid concentrate codes (vloeibare meststoffen) — volume in l - const liquidCodes = new Set(["115", "116", "120"]) - // Compost / solid organic matter codes — mass in ton - const compostCodes = new Set(["107", "108", "109", "111", "112"]) - - if (slurryCodes.has(p_type_rvo)) return "m3/ha" - if (liquidCodes.has(p_type_rvo)) return "l/ha" - if (compostCodes.has(p_type_rvo)) return "ton/ha" - return "kg/ha" +export function suggestUnitFromRvoCode( + p_type_rvo: string, + table = RVO_RECOMMENDED_UNITS, +): AppAmountUnit { + const rowOrDefault = table.find((row) => row.p_type_rvo === p_type_rvo) ?? { + p_type_rvo, + unit: "kg/ha", + } + + return rowOrDefault.unit } + +export type RvoUnitSuggestionTableItem = { + p_type_rvo: string + unit: AppAmountUnit + type?: string +} + +export const RVO_RECOMMENDED_UNITS: RvoUnitSuggestionTableItem[] = [ + // Cattle + { p_type_rvo: "10", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "11", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "12", type: "slurry", unit: "m3/ha" }, + { p_type_rvo: "13", type: "solid (dikke fractie)", unit: "ton/ha" }, + { p_type_rvo: "14", type: "liquid", unit: "l/ha" }, + { p_type_rvo: "17", type: "slurry", unit: "m3/ha" }, + { p_type_rvo: "18", type: "solid (young calf)", unit: "ton/ha" }, + { p_type_rvo: "19", type: "solid (older meat calf)", unit: "ton/ha" }, + + // Turkey + { p_type_rvo: "23", type: "solid", unit: "ton/ha" }, + + // Equines + { p_type_rvo: "25", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "26", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "27", type: "solid", unit: "ton/ha" }, + + // Poultry + { p_type_rvo: "30", type: "liquid", unit: "l/ha" }, + { p_type_rvo: "31", type: "solid (deep pit)", unit: "ton/ha" }, + { p_type_rvo: "32", type: "solid (mestband)", unit: "ton/ha" }, + { p_type_rvo: "33", type: "solid (mestband + nadroog)", unit: "ton/ha" }, + { p_type_rvo: "35", type: "solid (strooiselstal)", unit: "ton/ha" }, + + // Game fowl + { p_type_rvo: "39", type: "solid", unit: "ton/ha" }, + + // Hogs + { p_type_rvo: "40", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "41", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "42", type: "slurry", unit: "m3/ha" }, + { p_type_rvo: "43", type: "solid (dikke fractie)", unit: "ton/ha" }, + { p_type_rvo: "46", type: "liquid", unit: "l/ha" }, + { p_type_rvo: "50", type: "liquid", unit: "l/ha" }, + + // Sheep + { p_type_rvo: "56", type: "solid", unit: "ton/ha" }, + + // Goat + { p_type_rvo: "60", type: "liquid", unit: "l/ha" }, + { p_type_rvo: "61", type: "solid", unit: "ton/ha" }, + + // Nerts / Mink + { p_type_rvo: "75", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "76", type: "liquid", unit: "l/ha" }, + + // Ducks + { p_type_rvo: "80", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "81", type: "liquid", unit: "l/ha" }, + + // Rabbit + { p_type_rvo: "90", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "91", type: "liquid (very diluted)", unit: "l/ha" }, + { p_type_rvo: "92", type: "liquid", unit: "l/ha" }, + + // Deer + { p_type_rvo: "95", type: "solid", unit: "ton/ha" }, + + // Water buffalo + { p_type_rvo: "96", type: "solid", unit: "ton/ha" }, + + // Other birds + { p_type_rvo: "97", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "98", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "99", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "100", type: "solid", unit: "ton/ha" }, + + // Rodents + { p_type_rvo: "101", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "102", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "103", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "104", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "105", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "106", type: "solid", unit: "ton/ha" }, + + // Compost + { p_type_rvo: "107", type: "solid (phase 1)", unit: "ton/ha" }, + { p_type_rvo: "108", type: "solid (phase 2)", unit: "ton/ha" }, + { p_type_rvo: "109", type: "solid (phase 3)", unit: "ton/ha" }, + { p_type_rvo: "110", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "111", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "117", type: "solid", unit: "ton/ha" }, + + // Other + { p_type_rvo: "113", type: "liquid sewage", unit: "l/ha" }, + { p_type_rvo: "114", type: "solid sewage", unit: "ton/ha" }, + { p_type_rvo: "115", type: "other", unit: "kg/ha" }, + { p_type_rvo: "116", type: "other", unit: "kg/ha" }, +] From b6724d3b03d427243ae6c9c75a91341d585defc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Mon, 13 Apr 2026 13:50:01 +0200 Subject: [PATCH 07/38] Add extendCatalogueFertilizer and improve coverage --- fdm-core/src/catalogues.test.ts | 67 ++++++++++++++++++++++++++++++++- fdm-core/src/catalogues.ts | 58 ++++++++++++++++++---------- fdm-core/src/fertilizer.test.ts | 27 +++++++++++++ 3 files changed, 131 insertions(+), 21 deletions(-) diff --git a/fdm-core/src/catalogues.test.ts b/fdm-core/src/catalogues.test.ts index 3bcd38011..874212017 100644 --- a/fdm-core/src/catalogues.test.ts +++ b/fdm-core/src/catalogues.test.ts @@ -2,8 +2,8 @@ import { getCultivationCatalogue, getFertilizersCatalogue, } from "@nmi-agro/fdm-data" -import { eq, isNotNull } from "drizzle-orm" -import { beforeEach, describe, expect, inject, it } from "vitest" +import { eq, isNotNull, like } from "drizzle-orm" +import { afterEach, beforeEach, describe, expect, inject, it } from "vitest" import { disableCultivationCatalogue, disableFertilizerCatalogue, @@ -14,11 +14,13 @@ import { isCultivationCatalogueEnabled, isFertilizerCatalogueEnabled, syncCatalogues, + syncFertilizerCatalogueArray, } from "./catalogues" import * as schema from "./db/schema" import { addFarm } from "./farm" import type { FdmType } from "./fdm" import { createFdmServer } from "./fdm-server" +import { createId } from "./id" describe("Catalogues", () => { let fdm: FdmType @@ -588,6 +590,20 @@ describe("Catalogues syncing", () => { fdm = createFdmServer(host, port, user, password, database) }) + afterEach(async () => { + await fdm + .delete(schema.fertilizersCatalogue) + .where(like(schema.fertilizersCatalogue.p_id_catalogue, "test_%")) + + await fdm + .delete(schema.cultivationsCatalogue) + .where(like(schema.cultivationsCatalogue.b_lu_catalogue, "test_%")) + }) + + function createTestCatalogueId() { + return `test_${createId()}` + } + it("should sync catalogues", async () => { await syncCatalogues(fdm) @@ -709,6 +725,53 @@ describe("Catalogues syncing", () => { expect(syncedItem[0].p_name_nl).not.toBe("Updated Name") }) + it("should use default for p_app_amount_unit if p_type_rvo is not specified", async () => { + const p_id_catalogue = createTestCatalogueId() + + await syncFertilizerCatalogueArray(fdm, [ + { + p_id_catalogue: p_id_catalogue, + p_source: "baat", + p_name_nl: "Custom Fertilizer", + }, + ]) + + const syncedItem = await fdm + .select() + .from(schema.fertilizersCatalogue) + .where( + eq(schema.fertilizersCatalogue.p_id_catalogue, p_id_catalogue), + ) + .limit(1) + + expect(syncedItem[0].p_app_amount_unit).toBe("kg/ha") + }) + + it("should use any specified p_type_rvo", async () => { + const p_id_catalogue = createTestCatalogueId() + + await syncFertilizerCatalogueArray(fdm, [ + { + p_id_catalogue: p_id_catalogue, + p_source: "baat", + p_name_nl: "Suspicious Liquid Fertilizer", + p_type_rvo: "10", // Assumed to be solid so its unit would be estimated to be ton/ha + p_app_amount_unit: "l/ha", + p_density: 1.1, + }, + ]) + + const syncedItem = await fdm + .select() + .from(schema.fertilizersCatalogue) + .where( + eq(schema.fertilizersCatalogue.p_id_catalogue, p_id_catalogue), + ) + .limit(1) + + expect(syncedItem[0].p_app_amount_unit).toBe("l/ha") + }) + it("should update cultivation catalogue", async () => { await syncCatalogues(fdm) diff --git a/fdm-core/src/catalogues.ts b/fdm-core/src/catalogues.ts index 3c424aa0e..d60424e2d 100644 --- a/fdm-core/src/catalogues.ts +++ b/fdm-core/src/catalogues.ts @@ -1,3 +1,7 @@ +import type { + CatalogueFertilizer, + CatalogueFertilizerItem, +} from "@nmi-agro/fdm-data" import { getCultivationCatalogue, getFertilizersCatalogue, @@ -490,10 +494,17 @@ async function syncFertilizerCatalogue(fdm: FdmType) { const baatCatalogue = await getFertilizersCatalogue("baat") const fertilizersCatalogue = [...srmCatalogue, ...baatCatalogue] + return syncFertilizerCatalogueArray(fdm, fertilizersCatalogue) +} + +export async function syncFertilizerCatalogueArray( + fdm: FdmType, + fertilizersCatalogue: CatalogueFertilizer, +) { await fdm.transaction(async (tx: FdmServerType) => { try { - for (const item of fertilizersCatalogue) { - const hash = await hashFertilizer(item) + for (const catalogueItem of fertilizersCatalogue) { + const item = await extendCatalogueFertilizer(catalogueItem) const existing = await tx .select({ hash: schema.fertilizersCatalogue.hash }) .from(schema.fertilizersCatalogue) @@ -506,31 +517,17 @@ async function syncFertilizerCatalogue(fdm: FdmType) { .limit(1) if (existing.length === 0) { //add the item if does not exist - await tx.insert(schema.fertilizersCatalogue).values({ - ...item, - hash: hash, - }) + await tx.insert(schema.fertilizersCatalogue).values(item) } else { // update the hash if it is undefined, null or different if ( existing[0].hash === null || existing[0].hash === undefined || - existing[0].hash !== hash + existing[0].hash !== item.hash ) { - const values = { - ...item, - p_app_amount_unit: - item.p_app_amount_unit ?? - (item.p_type_rvo - ? suggestUnitFromRvoCode(item.p_type_rvo) - : undefined), - hash: hash, - updated: new Date(), - } - await tx .update(schema.fertilizersCatalogue) - .set(values) + .set({ ...item, updated: new Date() }) .where( eq( schema.fertilizersCatalogue.p_id_catalogue, @@ -546,6 +543,29 @@ async function syncFertilizerCatalogue(fdm: FdmType) { }) } +/** + * Extends a catalogue fertilizer with computed properties and its up-to-date hash + * + * @param catalogueFertilizer fertilizer out of the catalogue + * @returns a fertilizer object, ready for fertilizers_catalogue table insertion/update + */ +async function extendCatalogueFertilizer( + catalogueFertilizer: CatalogueFertilizerItem, +) { + const fertWithComputedProps = { + ...catalogueFertilizer, + p_app_amount_unit: + catalogueFertilizer.p_app_amount_unit ?? + (catalogueFertilizer.p_type_rvo + ? suggestUnitFromRvoCode(catalogueFertilizer.p_type_rvo) + : undefined), + } + return { + ...fertWithComputedProps, + hash: await hashFertilizer(fertWithComputedProps), + } +} + async function syncCultivationCatalogue(fdm: FdmType) { const brpCatalogue = await getCultivationCatalogue("brp") diff --git a/fdm-core/src/fertilizer.test.ts b/fdm-core/src/fertilizer.test.ts index 72fcbc1ef..028effb38 100644 --- a/fdm-core/src/fertilizer.test.ts +++ b/fdm-core/src/fertilizer.test.ts @@ -1267,6 +1267,33 @@ describe("Fertilizer Data Model", () => { expect(fertilizerApplication?.p_app_date).toEqual(p_app_date) }) + it("should add a new fertilizer application with no amount specified", async () => { + const p_app_date = new Date("2024-03-15") + + const new_p_app_id = await addFertilizerApplication( + fdm, + principal_id, + b_id, + p_id_liquid, + null, + "broadcasting", + p_app_date, + ) + expect(new_p_app_id).toBeDefined() + + const fertilizerApplication = await getFertilizerApplication( + fdm, + principal_id, + new_p_app_id, + ) + expect(fertilizerApplication).toBeDefined() + expect(fertilizerApplication?.p_id).toBe(p_id_liquid) + expect(fertilizerApplication?.p_app_amount).toBe(null) + expect(fertilizerApplication?.p_app_amount_display).toBe(null) + expect(fertilizerApplication?.p_app_method).toBe("broadcasting") + expect(fertilizerApplication?.p_app_date).toEqual(p_app_date) + }) + it("should update a fertilizer application", async () => { const p_app_date1 = new Date("2024-03-15") const p_app_date2 = new Date("2024-04-20") From bdb0b209463b5936c3513fd4cc32632d9b5f2c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 14 Apr 2026 11:42:07 +0200 Subject: [PATCH 08/38] Address nitpicks --- .../blocks/fertilizer-applications/form.tsx | 66 ++++++------------- .../blocks/fertilizer-applications/utils.ts | 26 ++++++++ .../app/components/blocks/fertilizer/form.tsx | 5 +- .../blocks/fertilizer/formschema.tsx | 2 +- .../pdf/bemestingsplan/BemestingsplanPDF.tsx | 43 +++++++----- .../blocks/pdf/bemestingsplan/types.d.ts | 6 +- ...id_farm.$calendar.bemestingsplan[.]pdf.tsx | 2 +- ...calendar.field.$b_id.fertilizer._index.tsx | 6 +- ...farm.$calendar.field.fertilizer._index.tsx | 25 +++---- fdm-core/src/catalogues.test.ts | 45 ++++++++----- fdm-core/src/fertilizer.ts | 13 ++-- fdm-core/src/fertilizer.types.d.ts | 6 +- fdm-core/src/unit-conversion.ts | 8 +-- 13 files changed, 141 insertions(+), 112 deletions(-) create mode 100644 fdm-app/app/components/blocks/fertilizer-applications/utils.ts diff --git a/fdm-app/app/components/blocks/fertilizer-applications/form.tsx b/fdm-app/app/components/blocks/fertilizer-applications/form.tsx index a26283d32..989dda76c 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/form.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/form.tsx @@ -1,5 +1,4 @@ import { zodResolver } from "@hookform/resolvers/zod" -import type { AppAmountUnit } from "@nmi-agro/fdm-core" import { formatDate } from "date-fns" import { nl } from "date-fns/locale" import { Plus } from "lucide-react" @@ -10,6 +9,7 @@ import type { Navigation } from "react-router" import { Form, useNavigate, useSearchParams } from "react-router" import { RemixFormProvider, useRemixForm } from "remix-hook-form" import { useFieldFertilizerFormStore } from "@/app/store/field-fertilizer-form" +import { getApplicationAmountUnitLabel } from "~/components/blocks/fertilizer-applications/utils" import { Combobox } from "~/components/custom/combobox" import { DatePicker } from "~/components/custom/date-picker-v2" import { Button } from "~/components/ui/button" @@ -37,13 +37,7 @@ import { FormSchemaModify, type FormSchemaPartial, } from "./formschema" - -export type FertilizerOption = { - value: string - label: string - applicationMethodOptions?: { value: string; label: string }[] - p_app_amount_unit: AppAmountUnit -} +import type { FertilizerOption } from "./types.d" /** * Renders a fertilizer application creation or modification form. @@ -109,23 +103,17 @@ export function FertilizerApplicationForm({ const selectedFertilizer = options.find((option) => option.value === p_id) const isSubmitting = navigation.state !== "idle" - // Conversion factor used to transform the application amount when the user changes the fertilizer - // It is also used to show a more helpful application amount example - // 1 kg/L fertilizer density is assumed - const conversionFactors = { - "kg/ha": 1000, - "ton/ha": 1, - "l/ha": 1000, - "m3/ha": 1, - } as const - - // If the user switched the fertilizer, clear the application method + // If the user switched the fertilizer, clear the application method and amount useEffect(() => { if ( p_id && (!fertilizerApplication || fertilizerApplication.p_id !== p_id) ) { form.setValue("p_app_method", "") + form.setValue( + "p_app_amount_display", + undefined as unknown as number, + ) } }, [p_id, fertilizerApplication, form.setValue]) @@ -191,30 +179,6 @@ export function FertilizerApplicationForm({ } }, [fertilizerApplication?.p_app_amount_display, form.setValue]) - // Transform the application amount based on the changing application units - const currentApplicationUnit = - options.find((opt) => opt.value === p_id)?.p_app_amount_unit ?? "kg/ha" - const currentConversionFactor = conversionFactors[currentApplicationUnit] - const [lastConversionFactor, setLastConversionFactor] = useState( - currentConversionFactor, - ) - useEffect(() => { - const p_app_amount_display = form.getValues("p_app_amount_display") - if (currentConversionFactor !== lastConversionFactor) { - if (p_app_amount_display !== undefined) { - form.setValue( - "p_app_amount_display", - Math.round( - (100 * - (p_app_amount_display * currentConversionFactor)) / - lastConversionFactor, - ) / 100, - ) - setLastConversionFactor(currentConversionFactor) - } - } - }, [form, lastConversionFactor, currentConversionFactor]) - // Change fertilizer selection if the user has added a new fertilizer const new_p_id = searchParams.get("p_id") useEffect(() => { @@ -253,6 +217,12 @@ export function FertilizerApplicationForm({ ) } + const currentApplicationUnit = + options.find((opt) => opt.value === p_id)?.p_app_amount_unit ?? "kg/ha" + const currentApplicationUnitLabel = getApplicationAmountUnitLabel( + currentApplicationUnit, + ) + return (

({ className="gap-1" > - Hoeveelheid ({currentApplicationUnit}) + Hoeveelheid ( + {currentApplicationUnitLabel}) - {`${option.label} (${option.value})`} + {param.parameter === + "p_app_amount_unit" + ? option.label + : `${option.label} (${option.value})`} ))} diff --git a/fdm-app/app/components/blocks/fertilizer/formschema.tsx b/fdm-app/app/components/blocks/fertilizer/formschema.tsx index 358e54383..29dc4c9d6 100644 --- a/fdm-app/app/components/blocks/fertilizer/formschema.tsx +++ b/fdm-app/app/components/blocks/fertilizer/formschema.tsx @@ -480,7 +480,7 @@ export const FormSchema = z }, { path: ["p_density"], - error: "Dichtheid is verplicht bij gebruik van l/ha of m3/ha", + error: "Dichtheid is verplicht bij gebruik van L/ha of m³/ha", }, ) .refine( diff --git a/fdm-app/app/components/blocks/pdf/bemestingsplan/BemestingsplanPDF.tsx b/fdm-app/app/components/blocks/pdf/bemestingsplan/BemestingsplanPDF.tsx index 6f67b9de9..f15f7c1db 100644 --- a/fdm-app/app/components/blocks/pdf/bemestingsplan/BemestingsplanPDF.tsx +++ b/fdm-app/app/components/blocks/pdf/bemestingsplan/BemestingsplanPDF.tsx @@ -1,6 +1,10 @@ import { Document, Image, Link, Page, Text, View } from "@react-pdf/renderer" import { format } from "date-fns" import { nl } from "date-fns/locale" +import { + getApplicationAmountTotalUnitLabel, + getApplicationAmountUnitLabel, +} from "~/components/blocks/fertilizer-applications/utils" import { PdfCard } from "../PdfCard" import { PdfTable, @@ -881,7 +885,7 @@ export const BemestingsplanPDF = ({ data }: { data: BemestingsplanData }) => ( Product - Totaal (kg) + Totaal N-totaal (kg) @@ -902,18 +906,22 @@ export const BemestingsplanPDF = ({ data }: { data: BemestingsplanData }) => ( .reduce( (acc, f) => { f.applications.forEach((app) => { + if ( + app.quantity === null || + app.quantity_display === + null + ) { + return + } + if (!acc[app.product]) { acc[app.product] = { amount: 0, amount_display: 0, - amount_unit: ( - { - "kg/ha": "kg", - "ton/ha": "ton", - "l/ha": "L", - "m3/ha": "m³", - } as const - )[app.quantity_unit], + amount_unit: + getApplicationAmountTotalUnitLabel( + app.quantity_unit, + ) ?? "kg", n: 0, nw: 0, p: 0, @@ -944,11 +952,7 @@ export const BemestingsplanPDF = ({ data }: { data: BemestingsplanData }) => ( { amount: number amount_display: number - amount_unit: - | "kg" - | "ton" - | "L" - | "m³" + amount_unit: string n: number nw: number p: number @@ -1841,7 +1845,7 @@ export const BemestingsplanPDF = ({ data }: { data: BemestingsplanData }) => ( Datum / product - Hoeveelheid (kg/ha) + Hoeveelheid N tot. / w. (kg/ha) @@ -1875,7 +1879,14 @@ export const BemestingsplanPDF = ({ data }: { data: BemestingsplanData }) => ( - {Math.round(app.quantity)} kg/ha + {app.quantity_display + ? Math.round( + app.quantity_display, + ) + : "?"}{" "} + {getApplicationAmountUnitLabel( + app.quantity_unit, + )} diff --git a/fdm-app/app/components/blocks/pdf/bemestingsplan/types.d.ts b/fdm-app/app/components/blocks/pdf/bemestingsplan/types.d.ts index 71de3297c..797eef1c1 100644 --- a/fdm-app/app/components/blocks/pdf/bemestingsplan/types.d.ts +++ b/fdm-app/app/components/blocks/pdf/bemestingsplan/types.d.ts @@ -1,4 +1,4 @@ -import { AppAmountUnit } from "@nmi-agro/fdm-core" +import type { AppAmountUnit } from "@nmi-agro/fdm-core" export interface BemestingsplanData { config: { @@ -116,8 +116,8 @@ export interface BemestingsplanData { applications: Array<{ date: string product: string - quantity: number - quantity_display: number + quantity: number | null + quantity_display: number | null quantity_unit: AppAmountUnit p_dose_n: number p_dose_nw: number diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.bemestingsplan[.]pdf.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.bemestingsplan[.]pdf.tsx index 49d1353b5..ee3171a16 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.bemestingsplan[.]pdf.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.bemestingsplan[.]pdf.tsx @@ -283,7 +283,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { product: fert?.p_name_nl || app.p_id, quantity: app.p_app_amount ?? 0, quantity_display: app.p_app_amount_display ?? 0, - quantity_unit: app.p_app_amount_unit ?? "kg/ha", + quantity_unit: app.p_app_amount_unit, p_dose_n: appDose.p_dose_n || 0, p_dose_nw: appDose.p_dose_nw || 0, p_dose_p: appDose.p_dose_p || 0, diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx index cc84f290b..d3b1a7cb1 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx @@ -116,9 +116,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Map fertilizers to options for the combobox const fertilizerOptions = fertilizers.map((fertilizer) => { const applicationMethodOptions = fertilizer.p_app_method_options - .map((opt: any) => { - const meta = applicationMethods.options.find( - (x: any) => x.value === opt, + ?.map((opt) => { + const meta = applicationMethods.options?.find( + (x) => x.value === opt, ) return meta ? { value: opt, label: meta.label } : undefined }) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer._index.tsx index c87e5de6a..d0ed22d11 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer._index.tsx @@ -171,19 +171,16 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Map fertilizers to options for the combobox const fertilizerOptions = fertilizers.map((fertilizer) => { const applicationMethodOptions = fertilizer.p_app_method_options - .map((opt) => { - const meta = applicationMethods.options.find( + ?.map((opt) => { + const meta = applicationMethods.options?.find( (x) => x.value === opt, ) return meta ? { value: opt, label: meta.label } : undefined }) - .filter( - (option): option is { value: string; label: string } => - option !== undefined, - ) + .filter((option) => option !== undefined) return { value: fertilizer.p_id, - label: fertilizer.p_name_nl, + label: fertilizer.p_name_nl as string, applicationMethodOptions: applicationMethodOptions, p_app_amount_unit: fertilizer.p_app_amount_unit, } @@ -224,7 +221,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { p_id: "string", p_app_date: "date", p_app_method: "string", - p_app_amount: "number", + p_app_amount_display: "number", } as const const keys = Object.keys(keyTypes) as (keyof typeof keyTypes)[] @@ -276,10 +273,15 @@ export async function loader({ request, params }: LoaderFunctionArgs) { if (!fertilizerApplication.p_id) { delete fertilizerApplication.p_app_method // Also, no specific placeholder should be shown - delete exampleFertilizerApplication.p_app_amount + delete exampleFertilizerApplication.p_app_amount_display } - loaderExampleFertilizerApplication = exampleFertilizerApplication + loaderExampleFertilizerApplication = Object.fromEntries( + Object.entries(exampleFertilizerApplication).map(([k, v]) => [ + k, + v === null ? undefined : v, + ]), + ) } const loaderFertilizerApplication = fertilizerApplication @@ -646,7 +648,8 @@ export async function action({ request, params }: ActionFunctionArgs) { session.principal_id, p_app_id, validatedData.p_id ?? original.p_id, - validatedData.p_app_amount ?? original.p_app_amount, + validatedData.p_app_amount_display ?? + original.p_app_amount_display, validatedData.p_app_method ?? original.p_app_method, validatedData.p_app_date ?? original.p_app_date, ) diff --git a/fdm-core/src/catalogues.test.ts b/fdm-core/src/catalogues.test.ts index 20015b8ca..fb4515d95 100644 --- a/fdm-core/src/catalogues.test.ts +++ b/fdm-core/src/catalogues.test.ts @@ -2,8 +2,8 @@ import { getCultivationCatalogue, getFertilizersCatalogue, } from "@nmi-agro/fdm-data" -import { eq, isNotNull, like } from "drizzle-orm" -import { afterEach, beforeEach, describe, expect, inject, it } from "vitest" +import { eq, isNotNull } from "drizzle-orm" +import { beforeEach, describe, expect, inject, it } from "vitest" import { disableCultivationCatalogue, disableFertilizerCatalogue, @@ -590,20 +590,6 @@ describe("Catalogues syncing", () => { fdm = createFdmServer(host, port, user, password, database) }) - afterEach(async () => { - await fdm - .delete(schema.fertilizersCatalogue) - .where(like(schema.fertilizersCatalogue.p_id_catalogue, "test_%")) - - await fdm - .delete(schema.cultivationsCatalogue) - .where(like(schema.cultivationsCatalogue.b_lu_catalogue, "test_%")) - }) - - function createTestCatalogueId() { - return `test_${createId()}` - } - it("should sync catalogues", async () => { await syncCatalogues(fdm) @@ -726,7 +712,7 @@ describe("Catalogues syncing", () => { }) it("should use default for p_app_amount_unit if p_type_rvo is not specified", async () => { - const p_id_catalogue = createTestCatalogueId() + const p_id_catalogue = createId() await syncFertilizerCatalogueArray(fdm, [ { @@ -747,8 +733,31 @@ describe("Catalogues syncing", () => { expect(syncedItem[0].p_app_amount_unit).toBe("kg/ha") }) + it("should use derived p_app_amount_unit if p_type_rvo is specified", async () => { + const p_id_catalogue = createId() + + await syncFertilizerCatalogueArray(fdm, [ + { + p_id_catalogue: p_id_catalogue, + p_source: "baat", + p_name_nl: "Custom Fertilizer", + p_type_rvo: "10", + }, + ]) + + const syncedItem = await fdm + .select() + .from(schema.fertilizersCatalogue) + .where( + eq(schema.fertilizersCatalogue.p_id_catalogue, p_id_catalogue), + ) + .limit(1) + + expect(syncedItem[0].p_app_amount_unit).toBe("ton/ha") + }) + it("should use any specified p_type_rvo", async () => { - const p_id_catalogue = createTestCatalogueId() + const p_id_catalogue = createId() await syncFertilizerCatalogueArray(fdm, [ { diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index e13ca00ff..38fc4e577 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -914,13 +914,17 @@ export async function removeFertilizerApplication( } } +type IncompleteFertilizerApplication = Omit< + FertilizerApplication, + "p_app_amount_display" | "p_app_amount_unit" +> /** * Extends the given fertilizer application with computed data and removes unknown properties * @param app fertilizer application * @returns the same fertilizer application with p_app_amount_display filled in and properties * that do not belong to FertilizerApplication removed */ -function extendFertilizerApplication( +function extendFertilizerApplication( app: T, p_app_amount_unit: AppAmountUnit, p_density: number | null, @@ -1006,7 +1010,7 @@ export async function getFertilizerApplication( return result.length > 0 ? extendFertilizerApplication( - result[0] as FertilizerApplication, + result[0] as IncompleteFertilizerApplication, result[0].p_app_amount_unit as AppAmountUnit, result[0].p_density as number, ) @@ -1322,10 +1326,11 @@ export function getFertilizerParametersDescription( { parameter: "p_app_amount_unit", unit: "", - name: "Toedieningshoeveelheidsunit", + name: "Hoeveelheidsunit", type: "enum", category: "general", - description: "Toedieningsmethodes mogelijk voor deze meststof", + description: + "Unit van voorkeur waarin de applicatiebedragen worden weergegeven.", options: APP_AMOUNT_UNITS, }, { diff --git a/fdm-core/src/fertilizer.types.d.ts b/fdm-core/src/fertilizer.types.d.ts index 97c33afe1..14b354416 100644 --- a/fdm-core/src/fertilizer.types.d.ts +++ b/fdm-core/src/fertilizer.types.d.ts @@ -9,7 +9,7 @@ export interface FertilizerCatalogue { p_name_en: string | null p_description: string | null p_app_method_options: ApplicationMethods[] | null - p_app_amount_unit: AppAmountUnit | null + p_app_amount_unit: AppAmountUnit p_dm: number | null p_density: number | null p_om: number | null @@ -72,8 +72,8 @@ export interface FertilizerApplication { p_id_catalogue: string p_name_nl: string | null p_app_amount: number | null - p_app_amount_unit?: AppAmountUnit | null - p_app_amount_display?: number | null + p_app_amount_unit: AppAmountUnit + p_app_amount_display: number | null p_app_method: ApplicationMethods | null p_app_date: Date p_app_id: string diff --git a/fdm-core/src/unit-conversion.ts b/fdm-core/src/unit-conversion.ts index e0ee62ded..ebac4443a 100644 --- a/fdm-core/src/unit-conversion.ts +++ b/fdm-core/src/unit-conversion.ts @@ -4,8 +4,8 @@ export type AppAmountUnit = "kg/ha" | "l/ha" | "m3/ha" | "ton/ha" export const APP_AMOUNT_UNITS: { value: AppAmountUnit; label: string }[] = [ { value: "kg/ha", label: "kg/ha" }, - { value: "l/ha", label: "l/ha" }, - { value: "m3/ha", label: "m3/ha" }, + { value: "l/ha", label: "L/ha" }, + { value: "m3/ha", label: "m³/ha" }, { value: "ton/ha", label: "ton/ha" }, ] @@ -26,13 +26,13 @@ export function toKgPerHa( case "ton/ha": return new Decimal(1000).times(d) case "l/ha": - if (!density) + if (density === null || density === undefined) throw new Error( "Density (p_density) is required for l/ha → kg/ha conversion", ) return new Decimal(density).times(d) case "m3/ha": - if (!density) + if (density === null || density === undefined) throw new Error( "Density (p_density) is required for m3/ha → kg/ha conversion", ) From 57f980280f20baff3bf0146998398ffe05bff228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 14 Apr 2026 12:48:17 +0200 Subject: [PATCH 09/38] Remove unused import --- fdm-app/app/components/blocks/fertilizer-applications/form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdm-app/app/components/blocks/fertilizer-applications/form.tsx b/fdm-app/app/components/blocks/fertilizer-applications/form.tsx index 989dda76c..98badadf2 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/form.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/form.tsx @@ -3,7 +3,7 @@ import { formatDate } from "date-fns" import { nl } from "date-fns/locale" import { Plus } from "lucide-react" import type { MouseEvent } from "react" -import { useEffect, useId, useState } from "react" +import { useEffect, useId } from "react" import { Controller } from "react-hook-form" import type { Navigation } from "react-router" import { Form, useNavigate, useSearchParams } from "react-router" From 81c97fca9d93e298b26e3e0b0391ead64488fb50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 14 Apr 2026 14:08:33 +0200 Subject: [PATCH 10/38] Move unit conversions in Gerrit --- fdm-agents/src/agents/gerrit/agent.ts | 10 +++---- .../components/blocks/gerrit/plan-table.tsx | 9 ++++-- fdm-app/app/components/blocks/gerrit/types.ts | 5 +++- .../farm.$b_id_farm.$calendar.gerrit.tsx | 28 +++++++++++++++++-- fdm-core/src/index.ts | 1 + 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/fdm-agents/src/agents/gerrit/agent.ts b/fdm-agents/src/agents/gerrit/agent.ts index e237999f3..75728cefa 100644 --- a/fdm-agents/src/agents/gerrit/agent.ts +++ b/fdm-agents/src/agents/gerrit/agent.ts @@ -39,9 +39,9 @@ IMPORTANT CONSTRAINTS: 5. BUFFER STRIPS: Fields designated as buffer strips ("b_bufferstrip": true) MUST NOT receive any fertilizer applications. Ensure your plan contains zero applications for these fields. 6. APPLICATION METHOD: For each application, you must propose a valid "p_app_method". Choose ONLY from the "p_app_method_options" returned by the search tool for that specific fertilizer. 7. REALISTIC DATES: Ensure all "p_app_date" values are realistic for the crop type, cultivation season, and Dutch climate. Use the provided "b_lu_start" (sowing/start date) as a critical reference point for each crop. -8. REALISTIC APPLICATION AMOUNTS: Ensure the proposed "p_app_amount_display" per application matches the technical capabilities of common farming equipment. If the total advice requires more, you MUST split it into multiple applications on different dates. - - slurry (drijfmest): 15 - 30 m³/ha per application. - - Solid manure / compost (vaste mest): 10 - 30 t/ha per application. +8. REALISTIC APPLICATION AMOUNTS: Ensure the proposed "p_app_amount" per application matches the technical capabilities of common farming equipment. If the total advice requires more, you MUST split it into multiple applications on different dates. + - slurry (drijfmest): 15,000 - 30,000 kg/ha per application (15-30 m³/ha). + - Solid manure / compost (vaste mest): 10,000 - 30,000 kg/ha per application (10-30 t/ha). - Mineral fertilizers: 50 - 450 kg/ha per application. 9. PRIORITIZATION: If legal norms (especially Nitrogen or Phosphate) limit the total nutrient space on the farm, prioritize fulfilling the nutrient advice for high-value crops (e.g., potatoes, onions, sugar beets, vegetables) over lower-value crops or grasslands. Strategy should focus on maximizing the economic return of the limited nutrient space. 10. ORGANIC FARMING: If "Organic Farming" is YES, you MUST NOT use any mineral fertilizers ("p_type": "mineral") in the plan. @@ -83,7 +83,7 @@ Your final response MUST be a JSON object with exactly this structure (all field { "b_id": "string", "applications": [ - { "p_id_catalogue": "string", "p_app_amount_display": number, "p_app_date": "YYYY-MM-DD", "p_app_method": "string" } + { "p_id_catalogue": "string", "p_app_amount": number, "p_app_date": "YYYY-MM-DD", "p_app_method": "string" } ], "fieldMetrics": { "advice": { @@ -136,7 +136,7 @@ CALCULATOR REFERENCE (units and semantics for the simulation tool): - "norms.manure / nitrogen / phosphate": the legal maximum (farm total in kg, field level in kg/ha). Field level results include a "normSource" string explaining the origin of the limit. - "omBalance" (organische stofbalans): net organic matter balance, kg EOM/ha. Positive = good. Aim for ≥ 0. - "nBalance": nitrogen balance structured exactly as fdm-calculator outputs. "nBalance.balance" and "nBalance.target" are in kg N/ha. "nBalance.emission.ammonia.total" and "nBalance.emission.nitrate.total" are also in kg N/ha. The farm-level averages are automatically area-weighted by the simulation tool. nBalance.balance must be ≤ nBalance.target if keepNitrogenBalanceBelowTarget is YES. -- "p_app_amount_display": application amount — **in one of the units below**. +- "p_app_amount": application amount — **always in kg/ha, regardless of fertilizer type**. - Liquid manure / digestate / slurry: convert m³/ha → kg/ha using 1 m³ = 1000 kg. Round to nearest 1000. Example: 18 m³/ha = 18000 kg/ha. - Solid manure / compost: convert t/ha → kg/ha using 1 t = 1000 kg. Round to nearest 1000. Example: 20 t/ha = 20000 kg/ha. - Mineral fertilizers: already in kg/ha, round to nearest 5 or 10. Example: 200 kg/ha KAS. diff --git a/fdm-app/app/components/blocks/gerrit/plan-table.tsx b/fdm-app/app/components/blocks/gerrit/plan-table.tsx index 617450012..4ba793560 100644 --- a/fdm-app/app/components/blocks/gerrit/plan-table.tsx +++ b/fdm-app/app/components/blocks/gerrit/plan-table.tsx @@ -36,6 +36,7 @@ import { } from "~/components/ui/tooltip" import { FertilizerIcon } from "./fertilizer-icon" import type { ParsedPlan, PlanRow } from "./types" +import { getApplicationAmountUnitLabel } from "../fertilizer-applications/utils" const columnHelper = createColumnHelper() @@ -102,7 +103,7 @@ const columns = [
{apps.map((app, _i) => ( @@ -115,7 +116,11 @@ const columns = [ {app.p_name_nl} - {app.p_app_amount} {"kg/ha"} + {app.p_app_amount_display ?? null}{" "} + {getApplicationAmountUnitLabel( + app.p_app_amount_unit ?? + "kg/ha", + )} · diff --git a/fdm-app/app/components/blocks/gerrit/types.ts b/fdm-app/app/components/blocks/gerrit/types.ts index 6ccd9dad9..cfbbe41ed 100644 --- a/fdm-app/app/components/blocks/gerrit/types.ts +++ b/fdm-app/app/components/blocks/gerrit/types.ts @@ -7,10 +7,11 @@ import type { NormFilling, NutrientAdvice, } from "@nmi-agro/fdm-calculator" +import type { AppAmountUnit } from "@nmi-agro/fdm-core" export interface ParsedPlanApplication { p_id_catalogue: string - p_app_amount_display: number + p_app_amount: number p_app_date: string p_app_method?: string | null p_app_method_name?: string | null @@ -58,6 +59,8 @@ export interface PlanRow { p_name_nl: string | null p_type: string p_app_method_name?: string | null + p_app_amount_display: number + p_app_amount_unit: AppAmountUnit } > fieldMetrics: FieldMetrics | null diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx index 397fe38fd..0293b108f 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx @@ -26,6 +26,7 @@ import { addFertilizerApplication, type Fertilizer, type FertilizerApplication, + fromKgPerHa, getCultivations, getCurrentSoilData, getFarms, @@ -167,7 +168,7 @@ async function computePlanMetrics( b_bufferstrip: boolean applications: Array<{ p_id_catalogue: string - p_app_amount_display: number + p_app_amount: number p_app_date: string p_app_method?: string | null }> @@ -268,7 +269,7 @@ async function computePlanMetrics( p_id: fert?.p_id ?? app.p_id_catalogue, p_id_catalogue: app.p_id_catalogue, p_name_nl: fert?.p_name_nl ?? null, - p_app_amount_display: app.p_app_amount_display, + p_app_amount: app.p_app_amount, p_app_date: new Date(app.p_app_date), p_app_id: `plan-${field.b_id}-${i}`, p_app_method: app.p_app_method ?? null, @@ -711,8 +712,31 @@ export async function action({ request, params }: ActionFunctionArgs) { applicationMethods?.options?.find( (x: any) => x.value === app.p_app_method, ) + const p_app_amount_display = fert + ? fromKgPerHa( + app.p_app_amount, + fert.p_app_amount_unit, + fert.p_density, + ) + : null + const unitConvertedAmount = + fert && p_app_amount_display !== null + ? { + p_app_amount_display: + p_app_amount_display + .toDecimalPlaces(2) + .toNumber(), + p_app_amount_unit: + fert.p_app_amount_unit, + } + : { + p_app_amount_display: + app.p_app_amount, + p_app_amount_unit: "kg/ha", + } return { ...app, + ...unitConvertedAmount, p_name_nl: fert?.p_name_nl || app.p_id_catalogue, p_type: fert?.p_type || "other", diff --git a/fdm-core/src/index.ts b/fdm-core/src/index.ts index 79ffd4906..9062ba440 100644 --- a/fdm-core/src/index.ts +++ b/fdm-core/src/index.ts @@ -194,4 +194,5 @@ export type { } from "./soil.types" export type { Timeframe } from "./timeframe.d" export type { AppAmountUnit } from "./unit-conversion" +export { fromKgPerHa, toKgPerHa } from "./unit-conversion" export { fdmSchema } From 2cd07c9dd6fa18699af958f67cf7f54161a30823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 14 Apr 2026 14:42:39 +0200 Subject: [PATCH 11/38] Fix type errors in fdm-calculator --- .../nitrogen/emission/ammonia/fertilizers.ts | 4 ++-- .../src/balance/nitrogen/types.d.ts | 5 ++++- .../src/balance/organic-matter/types.ts | 4 ++-- .../src/doses/calculate-dose.test.ts | 19 ++++++++++--------- fdm-calculator/src/doses/calculate-dose.ts | 5 +++-- .../2025/filling/fosfaatgebruiksnorm.test.ts | 5 +++-- .../2026/filling/fosfaatgebruiksnorm.test.ts | 5 +++-- .../2026/filling/stikstofgebruiksnorm.test.ts | 19 ++++++++----------- fdm-calculator/src/shared/types.d.ts | 13 +++++++++++++ fdm-calculator/tsconfig.json | 3 ++- 10 files changed, 50 insertions(+), 32 deletions(-) create mode 100644 fdm-calculator/src/shared/types.d.ts diff --git a/fdm-calculator/src/balance/nitrogen/emission/ammonia/fertilizers.ts b/fdm-calculator/src/balance/nitrogen/emission/ammonia/fertilizers.ts index 5af070193..b816c21e1 100644 --- a/fdm-calculator/src/balance/nitrogen/emission/ammonia/fertilizers.ts +++ b/fdm-calculator/src/balance/nitrogen/emission/ammonia/fertilizers.ts @@ -1,5 +1,5 @@ -import type { FertilizerApplication } from "@nmi-agro/fdm-core" import Decimal from "decimal.js" +import type { CalculatorFertilizerApplication } from "~/shared/types" import type { CultivationDetail, FertilizerDetail, @@ -184,7 +184,7 @@ function determineMineralAmmoniaEmissionFactor( * @throws Error if an unsupported application method is provided for the given land type. */ function determineManureAmmoniaEmissionFactor( - fertilizerApplication: FertilizerApplication, + fertilizerApplication: CalculatorFertilizerApplication, cultivations: FieldInput["cultivations"], cultivationDetails: Map, ) { diff --git a/fdm-calculator/src/balance/nitrogen/types.d.ts b/fdm-calculator/src/balance/nitrogen/types.d.ts index 3f0aa2581..b8204c5af 100644 --- a/fdm-calculator/src/balance/nitrogen/types.d.ts +++ b/fdm-calculator/src/balance/nitrogen/types.d.ts @@ -496,7 +496,10 @@ export type FieldInput = { | "b_soiltype_agr" | "b_gwl_class" >[] - fertilizerApplications: FertilizerApplication[] + fertilizerApplications: Omit< + FertilizerApplication, + "p_app_amount_display" | "p_app_amount_unit" + >[] depositionSupply?: NitrogenSupplyDeposition } diff --git a/fdm-calculator/src/balance/organic-matter/types.ts b/fdm-calculator/src/balance/organic-matter/types.ts index 2ebf7c008..3ef777da4 100644 --- a/fdm-calculator/src/balance/organic-matter/types.ts +++ b/fdm-calculator/src/balance/organic-matter/types.ts @@ -1,11 +1,11 @@ import type { Cultivation, CultivationCatalogue, - FertilizerApplication, Field, SoilAnalysis, } from "@nmi-agro/fdm-core" import type { Decimal } from "decimal.js" +import type { CalculatorFertilizerApplication } from "~/shared/types" /** * Represents the organic matter supply from various fertilizer applications, categorized by type. @@ -219,7 +219,7 @@ export type FieldInput = { | "b_soiltype_agr" >[] /** The list of fertilizer applications on the field. */ - fertilizerApplications: FertilizerApplication[] + fertilizerApplications: CalculatorFertilizerApplication[] } /** diff --git a/fdm-calculator/src/doses/calculate-dose.test.ts b/fdm-calculator/src/doses/calculate-dose.test.ts index 439413d96..334e29a23 100644 --- a/fdm-calculator/src/doses/calculate-dose.test.ts +++ b/fdm-calculator/src/doses/calculate-dose.test.ts @@ -1,5 +1,6 @@ -import type { Fertilizer, FertilizerApplication } from "@nmi-agro/fdm-core" +import type { Fertilizer } from "@nmi-agro/fdm-core" import { describe, expect, it } from "vitest" +import type { CalculatorFertilizerApplication } from "~/shared/types" import { calculateDose } from "./calculate-dose" const initialDose = { @@ -20,7 +21,7 @@ const initialDose = { p_dose_b: 0, } -const baseApplication: FertilizerApplication = { +const baseApplication: CalculatorFertilizerApplication = { p_app_id: "app1", p_id_catalogue: "fert1", p_app_amount: 100, @@ -91,7 +92,7 @@ const baseFertilizer: Fertilizer = { describe("calculateDose", () => { it("should calculate all nutrient doses correctly", () => { - const applications: FertilizerApplication[] = [ + const applications: CalculatorFertilizerApplication[] = [ { ...baseApplication, }, @@ -151,7 +152,7 @@ describe("calculateDose", () => { }) it("should handle zero application amounts correctly", () => { - const applications: FertilizerApplication[] = [ + const applications: CalculatorFertilizerApplication[] = [ { ...baseApplication, p_app_amount: 0, @@ -167,7 +168,7 @@ describe("calculateDose", () => { }) it("should handle zero nutrient rates correctly", () => { - const applications: FertilizerApplication[] = [ + const applications: CalculatorFertilizerApplication[] = [ { ...baseApplication, }, @@ -196,7 +197,7 @@ describe("calculateDose", () => { }) it("should throw an error for negative application amounts", () => { - const applications: FertilizerApplication[] = [ + const applications: CalculatorFertilizerApplication[] = [ { ...baseApplication, p_app_amount: -100, @@ -209,7 +210,7 @@ describe("calculateDose", () => { }) it("should throw an error for negative nutrient rates", () => { - const applications: FertilizerApplication[] = [ + const applications: CalculatorFertilizerApplication[] = [ { ...baseApplication, }, @@ -226,7 +227,7 @@ describe("calculateDose", () => { }) it("should throw an error for missing fertilizers", () => { - const applications: FertilizerApplication[] = [ + const applications: CalculatorFertilizerApplication[] = [ { ...baseApplication, p_id_catalogue: "fert_missing", @@ -249,7 +250,7 @@ describe("calculateDose", () => { }) it("should throw an error for empty fertilizers array", () => { - const applications: FertilizerApplication[] = [ + const applications: CalculatorFertilizerApplication[] = [ { ...baseApplication, }, diff --git a/fdm-calculator/src/doses/calculate-dose.ts b/fdm-calculator/src/doses/calculate-dose.ts index 5b22b3417..cd3f086d4 100644 --- a/fdm-calculator/src/doses/calculate-dose.ts +++ b/fdm-calculator/src/doses/calculate-dose.ts @@ -1,4 +1,5 @@ -import type { Fertilizer, FertilizerApplication } from "@nmi-agro/fdm-core" +import type { Fertilizer } from "@nmi-agro/fdm-core" +import type { CalculatorFertilizerApplication } from "~/shared/types" import type { Dose, NumericDoseKeys } from "./d" /** @@ -38,7 +39,7 @@ export function calculateDose({ applications, fertilizers, }: { - applications: FertilizerApplication[] + applications: CalculatorFertilizerApplication[] fertilizers: Fertilizer[] }): { dose: Dose; applications: Dose[] } { if (applications.some((app) => (app.p_app_amount ?? 0) < 0)) { diff --git a/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts b/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts index 9e7a2dafc..4af44b895 100644 --- a/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts +++ b/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts @@ -1,5 +1,6 @@ -import type { Fertilizer, FertilizerApplication } from "@nmi-agro/fdm-core" +import type { Fertilizer } from "@nmi-agro/fdm-core" import { describe, expect, it } from "vitest" +import type { CalculatorFertilizerApplication } from "~/shared/types" import { calculateNL2025FertilizerApplicationFillingForFosfaatGebruiksNorm } from "./fosfaatgebruiksnorm" import type { NL2025NormsFillingInput } from "./types" @@ -93,7 +94,7 @@ describe("calculateNL2025FertilizerApplicationFillingForFosfaatGebruiksNorm", () fertilizerId: string, amount: number, appId: string, - ): FertilizerApplication => ({ + ): CalculatorFertilizerApplication => ({ p_app_id: appId, p_id: appId, p_id_catalogue: fertilizerId, diff --git a/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts b/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts index 48e5fa04d..27a5c75b0 100644 --- a/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts +++ b/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts @@ -1,5 +1,6 @@ -import type { Fertilizer, FertilizerApplication } from "@nmi-agro/fdm-core" +import type { Fertilizer } from "@nmi-agro/fdm-core" import { describe, expect, it } from "vitest" +import type { CalculatorFertilizerApplication } from "~/shared/types" import { calculateNL2026FertilizerApplicationFillingForFosfaatGebruiksNorm } from "./fosfaatgebruiksnorm" import type { NL2026NormsFillingInput } from "./types" @@ -93,7 +94,7 @@ describe("calculateNL2026FertilizerApplicationFillingForFosfaatGebruiksNorm", () fertilizerId: string, amount: number, appId: string, - ): FertilizerApplication => ({ + ): CalculatorFertilizerApplication => ({ p_app_id: appId, p_id: appId, p_id_catalogue: fertilizerId, diff --git a/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts b/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts index 234eb8602..7477ae728 100644 --- a/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts +++ b/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts @@ -1,9 +1,6 @@ -import type { - Cultivation, - Fertilizer, - FertilizerApplication, -} from "@nmi-agro/fdm-core" +import type { Cultivation, Fertilizer } from "@nmi-agro/fdm-core" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" +import type { CalculatorFertilizerApplication } from "~/shared/types" import { getRegion } from "../../2025/value/stikstofgebruiksnorm" import type { RegionKey } from "../value/types" import { @@ -595,7 +592,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( }) it("should calculate norm filling correctly for a single application with known nitrogen content", async () => { - const applications: FertilizerApplication[] = [ + const applications: CalculatorFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-05-01"), @@ -692,7 +689,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( }) it("should calculate norm filling correctly for multiple applications", async () => { - const applications: FertilizerApplication[] = [ + const applications: CalculatorFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-05-01"), @@ -862,7 +859,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( }) it("should use table11Mestcodes for nitrogen content if p_n_rt is 0", async () => { - const applications: FertilizerApplication[] = [ + const applications: CalculatorFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-05-01"), @@ -960,7 +957,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( it("should throw an error if fertilizer cannot be found", async () => { vi.mocked(getRegion).mockResolvedValue("klei") - const applications: FertilizerApplication[] = [ + const applications: CalculatorFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-05-01"), @@ -993,7 +990,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( it("should treat onFarmProduced as false when has_grazing_intention is false for drijfmest", async () => { vi.mocked(getRegion).mockResolvedValue("zand_nwc") - const applications: FertilizerApplication[] = [ + const applications: CalculatorFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-05-01"), @@ -1093,7 +1090,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( it("should correctly apply bouwland logic for working coefficient", async () => { vi.mocked(getRegion).mockResolvedValue("klei") // Soil type for bouwland rule - const applications: FertilizerApplication[] = [ + const applications: CalculatorFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-10-15"), // Sep 1 to Jan 31 period diff --git a/fdm-calculator/src/shared/types.d.ts b/fdm-calculator/src/shared/types.d.ts new file mode 100644 index 000000000..0573098f9 --- /dev/null +++ b/fdm-calculator/src/shared/types.d.ts @@ -0,0 +1,13 @@ +import type { FertilizerApplication } from "@nmi-agro/fdm-core" + +export type CalculatorFertilizerApplication = Pick< + FertilizerApplication, + | "p_id" + | "p_id_catalogue" + | "p_name_nl" + | "p_app_id" + | "p_app_date" + | "p_app_method" + | "p_app_amount" +> & + Partial diff --git a/fdm-calculator/tsconfig.json b/fdm-calculator/tsconfig.json index fd9966745..07394146e 100644 --- a/fdm-calculator/tsconfig.json +++ b/fdm-calculator/tsconfig.json @@ -4,7 +4,8 @@ "noEmit": true, "paths": { "@nmi-agro/fdm-core": ["../fdm-core/src/index.ts"], - "@nmi-agro/fdm-data": ["../fdm-data/src/index.ts"] + "@nmi-agro/fdm-data": ["../fdm-data/src/index.ts"], + "~/*": ["./src/*"] } }, "include": ["src/**/*", "vitest.config.ts"] From 708bfe929672d78f55a04671fd359b981711ff00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 14 Apr 2026 14:50:17 +0200 Subject: [PATCH 12/38] Do not use path alias in fdm-calculator --- .../src/balance/nitrogen/emission/ammonia/fertilizers.ts | 2 +- fdm-calculator/src/balance/organic-matter/types.ts | 2 +- fdm-calculator/src/doses/calculate-dose.test.ts | 2 +- fdm-calculator/src/doses/calculate-dose.ts | 2 +- .../src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts | 2 +- .../src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts | 2 +- .../src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts | 2 +- fdm-calculator/tsconfig.json | 3 +-- 8 files changed, 8 insertions(+), 9 deletions(-) diff --git a/fdm-calculator/src/balance/nitrogen/emission/ammonia/fertilizers.ts b/fdm-calculator/src/balance/nitrogen/emission/ammonia/fertilizers.ts index b816c21e1..bec7b4df2 100644 --- a/fdm-calculator/src/balance/nitrogen/emission/ammonia/fertilizers.ts +++ b/fdm-calculator/src/balance/nitrogen/emission/ammonia/fertilizers.ts @@ -1,5 +1,5 @@ import Decimal from "decimal.js" -import type { CalculatorFertilizerApplication } from "~/shared/types" +import type { CalculatorFertilizerApplication } from "../../../../shared/types" import type { CultivationDetail, FertilizerDetail, diff --git a/fdm-calculator/src/balance/organic-matter/types.ts b/fdm-calculator/src/balance/organic-matter/types.ts index 3ef777da4..e3edd914d 100644 --- a/fdm-calculator/src/balance/organic-matter/types.ts +++ b/fdm-calculator/src/balance/organic-matter/types.ts @@ -5,7 +5,7 @@ import type { SoilAnalysis, } from "@nmi-agro/fdm-core" import type { Decimal } from "decimal.js" -import type { CalculatorFertilizerApplication } from "~/shared/types" +import type { CalculatorFertilizerApplication } from "../../shared/types" /** * Represents the organic matter supply from various fertilizer applications, categorized by type. diff --git a/fdm-calculator/src/doses/calculate-dose.test.ts b/fdm-calculator/src/doses/calculate-dose.test.ts index 334e29a23..2ba1ffb39 100644 --- a/fdm-calculator/src/doses/calculate-dose.test.ts +++ b/fdm-calculator/src/doses/calculate-dose.test.ts @@ -1,6 +1,6 @@ import type { Fertilizer } from "@nmi-agro/fdm-core" import { describe, expect, it } from "vitest" -import type { CalculatorFertilizerApplication } from "~/shared/types" +import type { CalculatorFertilizerApplication } from "../shared/types" import { calculateDose } from "./calculate-dose" const initialDose = { diff --git a/fdm-calculator/src/doses/calculate-dose.ts b/fdm-calculator/src/doses/calculate-dose.ts index cd3f086d4..18e8fd768 100644 --- a/fdm-calculator/src/doses/calculate-dose.ts +++ b/fdm-calculator/src/doses/calculate-dose.ts @@ -1,5 +1,5 @@ import type { Fertilizer } from "@nmi-agro/fdm-core" -import type { CalculatorFertilizerApplication } from "~/shared/types" +import type { CalculatorFertilizerApplication } from "../shared/types" import type { Dose, NumericDoseKeys } from "./d" /** diff --git a/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts b/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts index 4af44b895..de5d13cd2 100644 --- a/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts +++ b/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts @@ -1,6 +1,6 @@ import type { Fertilizer } from "@nmi-agro/fdm-core" import { describe, expect, it } from "vitest" -import type { CalculatorFertilizerApplication } from "~/shared/types" +import type { CalculatorFertilizerApplication } from "../../../../shared/types" import { calculateNL2025FertilizerApplicationFillingForFosfaatGebruiksNorm } from "./fosfaatgebruiksnorm" import type { NL2025NormsFillingInput } from "./types" diff --git a/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts b/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts index 27a5c75b0..c4dad37af 100644 --- a/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts +++ b/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts @@ -1,6 +1,6 @@ import type { Fertilizer } from "@nmi-agro/fdm-core" import { describe, expect, it } from "vitest" -import type { CalculatorFertilizerApplication } from "~/shared/types" +import type { CalculatorFertilizerApplication } from "../../../../shared/types" import { calculateNL2026FertilizerApplicationFillingForFosfaatGebruiksNorm } from "./fosfaatgebruiksnorm" import type { NL2026NormsFillingInput } from "./types" diff --git a/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts b/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts index 7477ae728..30f918505 100644 --- a/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts +++ b/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts @@ -1,6 +1,6 @@ import type { Cultivation, Fertilizer } from "@nmi-agro/fdm-core" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" -import type { CalculatorFertilizerApplication } from "~/shared/types" +import type { CalculatorFertilizerApplication } from "../../../../shared/types" import { getRegion } from "../../2025/value/stikstofgebruiksnorm" import type { RegionKey } from "../value/types" import { diff --git a/fdm-calculator/tsconfig.json b/fdm-calculator/tsconfig.json index 07394146e..fd9966745 100644 --- a/fdm-calculator/tsconfig.json +++ b/fdm-calculator/tsconfig.json @@ -4,8 +4,7 @@ "noEmit": true, "paths": { "@nmi-agro/fdm-core": ["../fdm-core/src/index.ts"], - "@nmi-agro/fdm-data": ["../fdm-data/src/index.ts"], - "~/*": ["./src/*"] + "@nmi-agro/fdm-data": ["../fdm-data/src/index.ts"] } }, "include": ["src/**/*", "vitest.config.ts"] From 4097f6bc62023df46f016be74952a6b3ba29d483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 14 Apr 2026 15:26:12 +0200 Subject: [PATCH 13/38] Improve things --- .../components/blocks/gerrit/plan-table.tsx | 5 +++- .../farm.$b_id_farm.$calendar.gerrit.tsx | 26 ++++++++----------- .../src/balance/nitrogen/types.d.ts | 7 ++--- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/fdm-app/app/components/blocks/gerrit/plan-table.tsx b/fdm-app/app/components/blocks/gerrit/plan-table.tsx index 4ba793560..0e9a72e0a 100644 --- a/fdm-app/app/components/blocks/gerrit/plan-table.tsx +++ b/fdm-app/app/components/blocks/gerrit/plan-table.tsx @@ -116,7 +116,10 @@ const columns = [ {app.p_name_nl} - {app.p_app_amount_display ?? null}{" "} + {Math.round( + (app.p_app_amount_display ?? + 0) * 100, + ) / 100}{" "} {getApplicationAmountUnitLabel( app.p_app_amount_unit ?? "kg/ha", diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx index 0293b108f..8360c344b 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx @@ -719,21 +719,17 @@ export async function action({ request, params }: ActionFunctionArgs) { fert.p_density, ) : null - const unitConvertedAmount = - fert && p_app_amount_display !== null - ? { - p_app_amount_display: - p_app_amount_display - .toDecimalPlaces(2) - .toNumber(), - p_app_amount_unit: - fert.p_app_amount_unit, - } - : { - p_app_amount_display: - app.p_app_amount, - p_app_amount_unit: "kg/ha", - } + const unitConvertedAmount = fert + ? { + p_app_amount_display: p_app_amount_display + ? p_app_amount_display.toNumber() + : null, + p_app_amount_unit: fert.p_app_amount_unit, + } + : { + p_app_amount_display: app.p_app_amount, + p_app_amount_unit: "kg/ha", + } return { ...app, ...unitConvertedAmount, diff --git a/fdm-calculator/src/balance/nitrogen/types.d.ts b/fdm-calculator/src/balance/nitrogen/types.d.ts index b8204c5af..d4dfbe794 100644 --- a/fdm-calculator/src/balance/nitrogen/types.d.ts +++ b/fdm-calculator/src/balance/nitrogen/types.d.ts @@ -2,12 +2,12 @@ import type { Cultivation, CultivationCatalogue, Fertilizer, - FertilizerApplication, Field, Harvest, SoilAnalysis, } from "@nmi-agro/fdm-core" import type { Decimal } from "decimal.js" +import type { CalculatorFertilizerApplication } from "../../shared/types" /** * Represents the nitrogen supply derived from various fertilizer applications. @@ -496,10 +496,7 @@ export type FieldInput = { | "b_soiltype_agr" | "b_gwl_class" >[] - fertilizerApplications: Omit< - FertilizerApplication, - "p_app_amount_display" | "p_app_amount_unit" - >[] + fertilizerApplications: CalculatorFertilizerApplication[] depositionSupply?: NitrogenSupplyDeposition } From 53d774a0d08db535991b6afef49a444747f35e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 14 Apr 2026 16:10:20 +0200 Subject: [PATCH 14/38] Add fertilizer RVO code 120 --- fdm-core/src/unit-conversion.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fdm-core/src/unit-conversion.ts b/fdm-core/src/unit-conversion.ts index ebac4443a..25df76236 100644 --- a/fdm-core/src/unit-conversion.ts +++ b/fdm-core/src/unit-conversion.ts @@ -192,4 +192,7 @@ export const RVO_RECOMMENDED_UNITS: RvoUnitSuggestionTableItem[] = [ { p_type_rvo: "114", type: "solid sewage", unit: "ton/ha" }, { p_type_rvo: "115", type: "other", unit: "kg/ha" }, { p_type_rvo: "116", type: "other", unit: "kg/ha" }, + + // Maybe non-standard + { p_type_rvo: "120", type: "other - mineral concentrate", unit: "kg/ha" }, ] From e367153347747fdca7582333205bd0b92f274bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 14 Apr 2026 16:47:43 +0200 Subject: [PATCH 15/38] Reject non-positive density values in fromKgPerHa --- fdm-core/src/unit-conversion.test.ts | 12 ++++++++++++ fdm-core/src/unit-conversion.ts | 8 ++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/fdm-core/src/unit-conversion.test.ts b/fdm-core/src/unit-conversion.test.ts index 0b9219121..87e8234e6 100644 --- a/fdm-core/src/unit-conversion.test.ts +++ b/fdm-core/src/unit-conversion.test.ts @@ -101,6 +101,18 @@ describe("fromKgPerHa", () => { density: undefined, output: null, }, + { + input: 20, + unit: "m3/ha", + density: 0, + output: null, + }, + { + input: 20, + unit: "m3/ha", + density: -1, + output: null, + }, { input: 20, unit: "ft3/ha" as AppAmountUnit, diff --git a/fdm-core/src/unit-conversion.ts b/fdm-core/src/unit-conversion.ts index 25df76236..a90374aac 100644 --- a/fdm-core/src/unit-conversion.ts +++ b/fdm-core/src/unit-conversion.ts @@ -53,16 +53,20 @@ export function fromKgPerHa( density?: number | Decimal | null, // kg/l ): Decimal | null { const d = new Decimal(valueKgPerHa) + const densityNotProvided = + density === null || + typeof density === "undefined" || + new Decimal(0).greaterThanOrEqualTo(density) switch (unit) { case "kg/ha": return d case "ton/ha": return d.dividedBy(1000) case "l/ha": - if (!density) return null + if (densityNotProvided) return null return d.dividedBy(new Decimal(density)) case "m3/ha": - if (!density) return null + if (densityNotProvided) return null return d.dividedBy(new Decimal(density).times(1000)) default: return null From 945e92c4b9e5930647099ffe52f3713af09bd2d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 14 Apr 2026 17:32:23 +0200 Subject: [PATCH 16/38] Add a way to specify fertilizer amounts directly in kg/ha --- .../farm.$b_id_farm.$calendar.gerrit.tsx | 5 +- fdm-core/src/fertilizer.ts | 15 ++--- fdm-core/src/unit-conversion.test.ts | 59 +++++++++++++++++++ fdm-core/src/unit-conversion.ts | 53 +++++++++++++++++ 4 files changed, 124 insertions(+), 8 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx index 8360c344b..736849807 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx @@ -878,7 +878,10 @@ export async function action({ request, params }: ActionFunctionArgs) { session.principal_id, field.b_id, fertilizer.p_id, - app.p_app_amount_display, + { + p_app_amount_display: app.p_app_amount, + p_app_amount_unit: "kg/ha", + }, app.p_app_method, new Date(app.p_app_date), ) diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 38fc4e577..7100bae71 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -21,9 +21,10 @@ import { createId } from "./id" import type { Timeframe } from "./timeframe" import { APP_AMOUNT_UNITS, + type AppAmount, type AppAmountUnit, fromKgPerHa, - toKgPerHa, + normalizeToKgPerHa, } from "./unit-conversion" /** @@ -764,7 +765,7 @@ export async function addFertilizerApplication( principal_id: PrincipalId, b_id: schema.fertilizerApplicationTypeInsert["b_id"], p_id: schema.fertilizerApplicationTypeInsert["p_id"], - p_app_amount_display: schema.fertilizerApplicationTypeInsert["p_app_amount"], + p_app_amount_display: AppAmount, p_app_method: schema.fertilizerApplicationTypeInsert["p_app_method"], p_app_date: schema.fertilizerApplicationTypeInsert["p_app_date"], ): Promise { @@ -794,9 +795,9 @@ export async function addFertilizerApplication( const p_app_amount = p_app_amount_display !== null && p_app_amount_display !== undefined - ? toKgPerHa( + ? normalizeToKgPerHa( p_app_amount_display, - fertilizer.p_app_amount_unit ?? "kg/ha", + fertilizer.p_app_amount_unit, fertilizer.p_density, ).toNumber() : null @@ -840,7 +841,7 @@ export async function updateFertilizerApplication( principal_id: PrincipalId, p_app_id: schema.fertilizerApplicationTypeInsert["p_app_id"], p_id: schema.fertilizerApplicationTypeInsert["p_id"], - p_app_amount_display: schema.fertilizerApplicationTypeInsert["p_app_amount"], + p_app_amount_display: AppAmount, p_app_method: schema.fertilizerApplicationTypeInsert["p_app_method"], p_app_date: schema.fertilizerApplicationTypeInsert["p_app_date"], ): Promise { @@ -856,9 +857,9 @@ export async function updateFertilizerApplication( const fertilizer = await getFertilizer(fdm, p_id) const p_app_amount = p_app_amount_display !== null && p_app_amount_display !== undefined - ? toKgPerHa( + ? normalizeToKgPerHa( p_app_amount_display, - fertilizer.p_app_amount_unit ?? "kg/ha", + fertilizer.p_app_amount_unit, fertilizer.p_density, ).toNumber() : p_app_amount_display diff --git a/fdm-core/src/unit-conversion.test.ts b/fdm-core/src/unit-conversion.test.ts index 87e8234e6..2b43caba9 100644 --- a/fdm-core/src/unit-conversion.test.ts +++ b/fdm-core/src/unit-conversion.test.ts @@ -1,8 +1,10 @@ import Decimal from "decimal.js" import { describe, expect, it } from "vitest" import { + type AppAmount, type AppAmountUnit, fromKgPerHa, + normalizeToKgPerHa, type RvoUnitSuggestionTableItem, suggestUnitFromRvoCode, toKgPerHa, @@ -17,6 +19,63 @@ interface ConversionUnitTestCase { throws?: string } +describe("normalizeToKgPerHa", () => { + it("should handle null", () => { + expect(() => + normalizeToKgPerHa(null as unknown as AppAmount, "kg/ha", 0), + ).toThrow("Amount was not properly specified") + }) + it("should handle undefined", () => { + expect(() => + normalizeToKgPerHa(undefined as unknown as AppAmount, "kg/ha", 0), + ).toThrow("Amount was not properly specified") + }) + it("should convert numbers", () => { + expect(normalizeToKgPerHa(2, "l/ha", 5).toNumber()).toBe(10) + }) + it("should convert Decimal", () => { + expect(normalizeToKgPerHa(new Decimal(2), "l/ha", 5).toNumber()).toBe( + 10, + ) + }) + it("should respect specified unit", () => { + expect( + normalizeToKgPerHa( + { p_app_amount_display: 2, p_app_amount_unit: "ton/ha" }, + "l/ha", + 5, + ).toNumber(), + ).toBe(2000) + }) + it("should handle unit not specified in object", () => { + expect( + normalizeToKgPerHa( + { p_app_amount_display: 2 } as AppAmount, + "l/ha", + 5, + ).toNumber(), + ).toBe(10) + }) + it("should handle unit not specified in object", () => { + expect(() => + normalizeToKgPerHa( + { p_app_amount_unit: "kg/ha" } as AppAmount, + "l/ha", + 5, + ), + ).toThrow("Amount was not properly specified") + }) + it("should handle arbitrary object as amount", () => { + expect(() => + normalizeToKgPerHa( + { name: "John" } as unknown as AppAmount, + "l/ha", + 5, + ), + ).toThrow("Amount was not properly specified") + }) +}) + describe("toKgPerHa", () => { const tests: ConversionUnitTestCase[] = [ { input: 20, unit: "kg/ha", output: 20 }, diff --git a/fdm-core/src/unit-conversion.ts b/fdm-core/src/unit-conversion.ts index a90374aac..f41818265 100644 --- a/fdm-core/src/unit-conversion.ts +++ b/fdm-core/src/unit-conversion.ts @@ -1,6 +1,13 @@ import Decimal from "decimal.js" export type AppAmountUnit = "kg/ha" | "l/ha" | "m3/ha" | "ton/ha" +export type AppAmountDimensionless = number | Decimal | string +export type AppAmount = + | AppAmountDimensionless + | { + p_app_amount_display: AppAmountDimensionless + p_app_amount_unit: AppAmountUnit + } export const APP_AMOUNT_UNITS: { value: AppAmountUnit; label: string }[] = [ { value: "kg/ha", label: "kg/ha" }, @@ -9,6 +16,52 @@ export const APP_AMOUNT_UNITS: { value: AppAmountUnit; label: string }[] = [ { value: "ton/ha", label: "ton/ha" }, ] +/** + * Convert an amount and its unit to kg/ha. This offers flexibility in how the amount is specified. + * + * Take care of which defaultUnit and which density are used. Usually defaultUnit is the + * configured display unit for the amount, and p_density is the fertilizer's density in catalogue. + * @param p_app_amount_display value to convert, maybe with the unit specified + * @param default_p_app_amount_unit unit to use if input has no unit specified + * @param p_density density value to use when converting volume to mass + * @returns the amount converted to kg/ha + * + * @throws {Error} If the amount is null or undefined, or if the density was not specified when it was needed for conversion. + */ +export function normalizeToKgPerHa( + p_app_amount_display: AppAmount, + default_p_app_amount_unit: AppAmountUnit, + p_density?: number | Decimal | null, +) { + let converted_p_app_amount_display: AppAmount = p_app_amount_display + let p_app_amount_unit = default_p_app_amount_unit + if ( + p_app_amount_display instanceof Object && + "p_app_amount_display" in p_app_amount_display + ) { + converted_p_app_amount_display = + p_app_amount_display.p_app_amount_display + if (p_app_amount_display.p_app_amount_unit) { + p_app_amount_unit = p_app_amount_display.p_app_amount_unit + } + } + + if ( + (converted_p_app_amount_display instanceof Object && + !(converted_p_app_amount_display instanceof Decimal)) || + converted_p_app_amount_display === null || + typeof converted_p_app_amount_display === "undefined" + ) { + throw new Error("Amount was not properly specified") + } + + return toKgPerHa( + converted_p_app_amount_display, + p_app_amount_unit, + p_density, + ) +} + /** * Convert a user-entered amount (in display unit) to kg/ha for storage. * Uses Decimal.js to avoid floating-point rounding errors. From 2f53d99af00e79aad7f3c4776551b6ca7c90bfa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 14 Apr 2026 17:40:35 +0200 Subject: [PATCH 17/38] Fix type errors --- fdm-core/src/fertilizer.ts | 4 ++-- fdm-core/src/unit-conversion.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 7100bae71..4867a3e84 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -765,7 +765,7 @@ export async function addFertilizerApplication( principal_id: PrincipalId, b_id: schema.fertilizerApplicationTypeInsert["b_id"], p_id: schema.fertilizerApplicationTypeInsert["p_id"], - p_app_amount_display: AppAmount, + p_app_amount_display: AppAmount | null, p_app_method: schema.fertilizerApplicationTypeInsert["p_app_method"], p_app_date: schema.fertilizerApplicationTypeInsert["p_app_date"], ): Promise { @@ -841,7 +841,7 @@ export async function updateFertilizerApplication( principal_id: PrincipalId, p_app_id: schema.fertilizerApplicationTypeInsert["p_app_id"], p_id: schema.fertilizerApplicationTypeInsert["p_id"], - p_app_amount_display: AppAmount, + p_app_amount_display: AppAmount | null, p_app_method: schema.fertilizerApplicationTypeInsert["p_app_method"], p_app_date: schema.fertilizerApplicationTypeInsert["p_app_date"], ): Promise { diff --git a/fdm-core/src/unit-conversion.ts b/fdm-core/src/unit-conversion.ts index f41818265..389f5c7df 100644 --- a/fdm-core/src/unit-conversion.ts +++ b/fdm-core/src/unit-conversion.ts @@ -19,8 +19,8 @@ export const APP_AMOUNT_UNITS: { value: AppAmountUnit; label: string }[] = [ /** * Convert an amount and its unit to kg/ha. This offers flexibility in how the amount is specified. * - * Take care of which defaultUnit and which density are used. Usually defaultUnit is the - * configured display unit for the amount, and p_density is the fertilizer's density in catalogue. + * Take care of which defaultUnit and which density are used. Usually default_p_app_amount_unit is the + * configured display unit for the fertilizer, and p_density is the fertilizer's density in catalogue. * @param p_app_amount_display value to convert, maybe with the unit specified * @param default_p_app_amount_unit unit to use if input has no unit specified * @param p_density density value to use when converting volume to mass @@ -68,7 +68,7 @@ export function normalizeToKgPerHa( * Throws if conversion requires density but density is null/undefined/0. */ export function toKgPerHa( - value: number | Decimal | string, + value: AppAmountDimensionless, unit: AppAmountUnit, density?: number | Decimal | null, // kg/l ): Decimal { From 0b4107b8fd384a6f82d2c702d31aaeda9d26adbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 14 Apr 2026 18:06:14 +0200 Subject: [PATCH 18/38] Combine CalculatorFertilizerApplication and IncompleteFertilizerApplication into single type --- .../nitrogen/emission/ammonia/fertilizers.ts | 4 ++-- .../src/balance/nitrogen/types.d.ts | 4 ++-- .../src/balance/organic-matter/types.ts | 4 ++-- .../src/doses/calculate-dose.test.ts | 19 +++++++++---------- fdm-calculator/src/doses/calculate-dose.ts | 5 ++--- .../2025/filling/fosfaatgebruiksnorm.test.ts | 5 ++--- .../nl/2025/filling/fosfaatgebruiksnorm.ts | 10 +++++----- .../src/norms/nl/2025/filling/input.test.ts | 5 +++-- .../src/norms/nl/2025/filling/types.d.ts | 4 ++-- .../2026/filling/fosfaatgebruiksnorm.test.ts | 5 ++--- .../nl/2026/filling/fosfaatgebruiksnorm.ts | 8 ++++---- .../src/norms/nl/2026/filling/input.test.ts | 5 +++-- .../2026/filling/stikstofgebruiksnorm.test.ts | 19 +++++++++++-------- .../src/norms/nl/2026/filling/types.d.ts | 4 ++-- fdm-calculator/src/shared/types.d.ts | 13 ------------- fdm-core/src/fertilizer.ts | 9 +++------ fdm-core/src/fertilizer.types.d.ts | 9 ++++++--- fdm-core/src/index.ts | 1 + 18 files changed, 61 insertions(+), 72 deletions(-) delete mode 100644 fdm-calculator/src/shared/types.d.ts diff --git a/fdm-calculator/src/balance/nitrogen/emission/ammonia/fertilizers.ts b/fdm-calculator/src/balance/nitrogen/emission/ammonia/fertilizers.ts index bec7b4df2..27c8714ba 100644 --- a/fdm-calculator/src/balance/nitrogen/emission/ammonia/fertilizers.ts +++ b/fdm-calculator/src/balance/nitrogen/emission/ammonia/fertilizers.ts @@ -1,5 +1,5 @@ +import type { BaseFertilizerApplication } from "@nmi-agro/fdm-core" import Decimal from "decimal.js" -import type { CalculatorFertilizerApplication } from "../../../../shared/types" import type { CultivationDetail, FertilizerDetail, @@ -184,7 +184,7 @@ function determineMineralAmmoniaEmissionFactor( * @throws Error if an unsupported application method is provided for the given land type. */ function determineManureAmmoniaEmissionFactor( - fertilizerApplication: CalculatorFertilizerApplication, + fertilizerApplication: BaseFertilizerApplication, cultivations: FieldInput["cultivations"], cultivationDetails: Map, ) { diff --git a/fdm-calculator/src/balance/nitrogen/types.d.ts b/fdm-calculator/src/balance/nitrogen/types.d.ts index d4dfbe794..7a3b2f02c 100644 --- a/fdm-calculator/src/balance/nitrogen/types.d.ts +++ b/fdm-calculator/src/balance/nitrogen/types.d.ts @@ -1,4 +1,5 @@ import type { + BaseFertilizerApplication, Cultivation, CultivationCatalogue, Fertilizer, @@ -7,7 +8,6 @@ import type { SoilAnalysis, } from "@nmi-agro/fdm-core" import type { Decimal } from "decimal.js" -import type { CalculatorFertilizerApplication } from "../../shared/types" /** * Represents the nitrogen supply derived from various fertilizer applications. @@ -496,7 +496,7 @@ export type FieldInput = { | "b_soiltype_agr" | "b_gwl_class" >[] - fertilizerApplications: CalculatorFertilizerApplication[] + fertilizerApplications: BaseFertilizerApplication[] depositionSupply?: NitrogenSupplyDeposition } diff --git a/fdm-calculator/src/balance/organic-matter/types.ts b/fdm-calculator/src/balance/organic-matter/types.ts index e3edd914d..067b487e5 100644 --- a/fdm-calculator/src/balance/organic-matter/types.ts +++ b/fdm-calculator/src/balance/organic-matter/types.ts @@ -1,11 +1,11 @@ import type { + BaseFertilizerApplication, Cultivation, CultivationCatalogue, Field, SoilAnalysis, } from "@nmi-agro/fdm-core" import type { Decimal } from "decimal.js" -import type { CalculatorFertilizerApplication } from "../../shared/types" /** * Represents the organic matter supply from various fertilizer applications, categorized by type. @@ -219,7 +219,7 @@ export type FieldInput = { | "b_soiltype_agr" >[] /** The list of fertilizer applications on the field. */ - fertilizerApplications: CalculatorFertilizerApplication[] + fertilizerApplications: BaseFertilizerApplication[] } /** diff --git a/fdm-calculator/src/doses/calculate-dose.test.ts b/fdm-calculator/src/doses/calculate-dose.test.ts index 2ba1ffb39..46c2be7d9 100644 --- a/fdm-calculator/src/doses/calculate-dose.test.ts +++ b/fdm-calculator/src/doses/calculate-dose.test.ts @@ -1,6 +1,5 @@ -import type { Fertilizer } from "@nmi-agro/fdm-core" +import type { BaseFertilizerApplication, Fertilizer } from "@nmi-agro/fdm-core" import { describe, expect, it } from "vitest" -import type { CalculatorFertilizerApplication } from "../shared/types" import { calculateDose } from "./calculate-dose" const initialDose = { @@ -21,7 +20,7 @@ const initialDose = { p_dose_b: 0, } -const baseApplication: CalculatorFertilizerApplication = { +const baseApplication: BaseFertilizerApplication = { p_app_id: "app1", p_id_catalogue: "fert1", p_app_amount: 100, @@ -92,7 +91,7 @@ const baseFertilizer: Fertilizer = { describe("calculateDose", () => { it("should calculate all nutrient doses correctly", () => { - const applications: CalculatorFertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { ...baseApplication, }, @@ -152,7 +151,7 @@ describe("calculateDose", () => { }) it("should handle zero application amounts correctly", () => { - const applications: CalculatorFertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { ...baseApplication, p_app_amount: 0, @@ -168,7 +167,7 @@ describe("calculateDose", () => { }) it("should handle zero nutrient rates correctly", () => { - const applications: CalculatorFertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { ...baseApplication, }, @@ -197,7 +196,7 @@ describe("calculateDose", () => { }) it("should throw an error for negative application amounts", () => { - const applications: CalculatorFertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { ...baseApplication, p_app_amount: -100, @@ -210,7 +209,7 @@ describe("calculateDose", () => { }) it("should throw an error for negative nutrient rates", () => { - const applications: CalculatorFertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { ...baseApplication, }, @@ -227,7 +226,7 @@ describe("calculateDose", () => { }) it("should throw an error for missing fertilizers", () => { - const applications: CalculatorFertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { ...baseApplication, p_id_catalogue: "fert_missing", @@ -250,7 +249,7 @@ describe("calculateDose", () => { }) it("should throw an error for empty fertilizers array", () => { - const applications: CalculatorFertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { ...baseApplication, }, diff --git a/fdm-calculator/src/doses/calculate-dose.ts b/fdm-calculator/src/doses/calculate-dose.ts index 18e8fd768..795cb6ce2 100644 --- a/fdm-calculator/src/doses/calculate-dose.ts +++ b/fdm-calculator/src/doses/calculate-dose.ts @@ -1,5 +1,4 @@ -import type { Fertilizer } from "@nmi-agro/fdm-core" -import type { CalculatorFertilizerApplication } from "../shared/types" +import type { BaseFertilizerApplication, Fertilizer } from "@nmi-agro/fdm-core" import type { Dose, NumericDoseKeys } from "./d" /** @@ -39,7 +38,7 @@ export function calculateDose({ applications, fertilizers, }: { - applications: CalculatorFertilizerApplication[] + applications: BaseFertilizerApplication[] fertilizers: Fertilizer[] }): { dose: Dose; applications: Dose[] } { if (applications.some((app) => (app.p_app_amount ?? 0) < 0)) { diff --git a/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts b/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts index de5d13cd2..9977f4fb6 100644 --- a/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts +++ b/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts @@ -1,6 +1,5 @@ -import type { Fertilizer } from "@nmi-agro/fdm-core" +import type { BaseFertilizerApplication, Fertilizer } from "@nmi-agro/fdm-core" import { describe, expect, it } from "vitest" -import type { CalculatorFertilizerApplication } from "../../../../shared/types" import { calculateNL2025FertilizerApplicationFillingForFosfaatGebruiksNorm } from "./fosfaatgebruiksnorm" import type { NL2025NormsFillingInput } from "./types" @@ -94,7 +93,7 @@ describe("calculateNL2025FertilizerApplicationFillingForFosfaatGebruiksNorm", () fertilizerId: string, amount: number, appId: string, - ): CalculatorFertilizerApplication => ({ + ): BaseFertilizerApplication => ({ p_app_id: appId, p_id: appId, p_id_catalogue: fertilizerId, diff --git a/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.ts b/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.ts index 32f880245..6cad45b66 100644 --- a/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.ts +++ b/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.ts @@ -1,6 +1,6 @@ import { + type BaseFertilizerApplication, type Fertilizer, - type FertilizerApplication, withCalculationCache, } from "@nmi-agro/fdm-core" import Decimal from "decimal.js" @@ -70,12 +70,12 @@ export function calculateNL2025FertilizerApplicationFillingForFosfaatGebruiksNor // Separate applications into standard and organic-rich const standardApplications: { - application: FertilizerApplication + application: BaseFertilizerApplication p_p_rt: Decimal p_app_amount: Decimal }[] = [] const organicRichApplications: { - application: FertilizerApplication + application: BaseFertilizerApplication p_p_rt: Decimal p_app_amount: Decimal p_type_rvo: string @@ -237,13 +237,13 @@ export function calculateNL2025FertilizerApplicationFillingForFosfaatGebruiksNor * Determines if at least 20 kg P2O5 / ha is applied with organic-rich fertilizers. * This is Condition 1 for the "Stimuleren organische stofrijke meststoffen" regulation. * - * @param {FertilizerApplication[]} applications - An array of fertilizer applications. + * @param {BaseFertilizerApplication[]} applications - An array of fertilizer applications. * @param {Map} fertilizersMap - A map of fertilizers for efficient lookup. * @param {boolean} has_organic_certification - Indicates if the farm has organic certification. * @returns {boolean} True if the 20 kg/ha threshold is met, false otherwise. */ function determineCondition1StimuleringOrganischeStofrijkeMeststoffen( - applications: FertilizerApplication[], + applications: BaseFertilizerApplication[], fertilizersMap: Map, has_organic_certification: boolean, ): boolean { diff --git a/fdm-calculator/src/norms/nl/2025/filling/input.test.ts b/fdm-calculator/src/norms/nl/2025/filling/input.test.ts index b495d24cb..908680e22 100644 --- a/fdm-calculator/src/norms/nl/2025/filling/input.test.ts +++ b/fdm-calculator/src/norms/nl/2025/filling/input.test.ts @@ -1,4 +1,5 @@ import type { + BaseFertilizerApplication, Cultivation, FdmType, Fertilizer, @@ -83,12 +84,12 @@ describe("collectNL2025InputForFertilizerApplicationFilling", () => { b_lu_catalogue: "nl_2014", }, ] - const expectedApplications: FertilizerApplication[] = [ + const expectedApplications: BaseFertilizerApplication[] = [ { p_app_id: "app1", p_id_catalogue: "fert1", p_app_amount: 1000, - } as FertilizerApplication, + } as BaseFertilizerApplication, ] const expectedFertilizers: Fertilizer[] = [ { p_id: "fert1", p_n_rt: 5, p_type_rvo: "115" }, diff --git a/fdm-calculator/src/norms/nl/2025/filling/types.d.ts b/fdm-calculator/src/norms/nl/2025/filling/types.d.ts index a73d3ce5f..acea1058f 100644 --- a/fdm-calculator/src/norms/nl/2025/filling/types.d.ts +++ b/fdm-calculator/src/norms/nl/2025/filling/types.d.ts @@ -1,8 +1,8 @@ import type * as schema from "@nmi-agro/fdm-core" import type { + BaseFertilizerApplication, Cultivation, Fertilizer, - FertilizerApplication, Field, } from "@nmi-agro/fdm-core" import type { RegionKey } from "../value/types" @@ -37,7 +37,7 @@ export type WorkingCoefficientDetails = { export type NL2025NormsFillingInput = { cultivations: Cultivation[] - applications: FertilizerApplication[] + applications: BaseFertilizerApplication[] fertilizers: Fertilizer[] has_organic_certification: boolean has_grazing_intention: boolean diff --git a/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts b/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts index c4dad37af..81a07fba4 100644 --- a/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts +++ b/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts @@ -1,6 +1,5 @@ -import type { Fertilizer } from "@nmi-agro/fdm-core" +import type { BaseFertilizerApplication, Fertilizer } from "@nmi-agro/fdm-core" import { describe, expect, it } from "vitest" -import type { CalculatorFertilizerApplication } from "../../../../shared/types" import { calculateNL2026FertilizerApplicationFillingForFosfaatGebruiksNorm } from "./fosfaatgebruiksnorm" import type { NL2026NormsFillingInput } from "./types" @@ -94,7 +93,7 @@ describe("calculateNL2026FertilizerApplicationFillingForFosfaatGebruiksNorm", () fertilizerId: string, amount: number, appId: string, - ): CalculatorFertilizerApplication => ({ + ): BaseFertilizerApplication => ({ p_app_id: appId, p_id: appId, p_id_catalogue: fertilizerId, diff --git a/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.ts b/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.ts index 25c7e5d4a..0d124abe9 100644 --- a/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.ts +++ b/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.ts @@ -1,6 +1,6 @@ import { + type BaseFertilizerApplication, type Fertilizer, - type FertilizerApplication, withCalculationCache, } from "@nmi-agro/fdm-core" import Decimal from "decimal.js" @@ -70,12 +70,12 @@ export function calculateNL2026FertilizerApplicationFillingForFosfaatGebruiksNor // Separate applications into standard and organic-rich const standardApplications: { - application: FertilizerApplication + application: BaseFertilizerApplication p_p_rt: Decimal p_app_amount: Decimal }[] = [] const organicRichApplications: { - application: FertilizerApplication + application: BaseFertilizerApplication p_p_rt: Decimal p_app_amount: Decimal p_type_rvo: string @@ -243,7 +243,7 @@ export function calculateNL2026FertilizerApplicationFillingForFosfaatGebruiksNor * @returns {boolean} True if the 20 kg/ha threshold is met, false otherwise. */ function determineCondition1StimuleringOrganischeStofrijkeMeststoffen( - applications: FertilizerApplication[], + applications: BaseFertilizerApplication[], fertilizersMap: Map, has_organic_certification: boolean, ): boolean { diff --git a/fdm-calculator/src/norms/nl/2026/filling/input.test.ts b/fdm-calculator/src/norms/nl/2026/filling/input.test.ts index f1781e7fc..60cf98bb3 100644 --- a/fdm-calculator/src/norms/nl/2026/filling/input.test.ts +++ b/fdm-calculator/src/norms/nl/2026/filling/input.test.ts @@ -1,4 +1,5 @@ import type { + BaseFertilizerApplication, Cultivation, FdmType, Fertilizer, @@ -83,12 +84,12 @@ describe("collectNL2026InputForFertilizerApplicationFilling", () => { b_lu_catalogue: "nl_2014", }, ] - const expectedApplications: FertilizerApplication[] = [ + const expectedApplications: BaseFertilizerApplication[] = [ { p_app_id: "app1", p_id_catalogue: "fert1", p_app_amount: 1000, - } as FertilizerApplication, + } as BaseFertilizerApplication, ] const expectedFertilizers: Fertilizer[] = [ { diff --git a/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts b/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts index 30f918505..d95ad4c46 100644 --- a/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts +++ b/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts @@ -1,6 +1,9 @@ -import type { Cultivation, Fertilizer } from "@nmi-agro/fdm-core" +import type { + BaseFertilizerApplication, + Cultivation, + Fertilizer, +} from "@nmi-agro/fdm-core" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" -import type { CalculatorFertilizerApplication } from "../../../../shared/types" import { getRegion } from "../../2025/value/stikstofgebruiksnorm" import type { RegionKey } from "../value/types" import { @@ -592,7 +595,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( }) it("should calculate norm filling correctly for a single application with known nitrogen content", async () => { - const applications: CalculatorFertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-05-01"), @@ -689,7 +692,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( }) it("should calculate norm filling correctly for multiple applications", async () => { - const applications: CalculatorFertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-05-01"), @@ -859,7 +862,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( }) it("should use table11Mestcodes for nitrogen content if p_n_rt is 0", async () => { - const applications: CalculatorFertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-05-01"), @@ -957,7 +960,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( it("should throw an error if fertilizer cannot be found", async () => { vi.mocked(getRegion).mockResolvedValue("klei") - const applications: CalculatorFertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-05-01"), @@ -990,7 +993,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( it("should treat onFarmProduced as false when has_grazing_intention is false for drijfmest", async () => { vi.mocked(getRegion).mockResolvedValue("zand_nwc") - const applications: CalculatorFertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-05-01"), @@ -1090,7 +1093,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( it("should correctly apply bouwland logic for working coefficient", async () => { vi.mocked(getRegion).mockResolvedValue("klei") // Soil type for bouwland rule - const applications: CalculatorFertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-10-15"), // Sep 1 to Jan 31 period diff --git a/fdm-calculator/src/norms/nl/2026/filling/types.d.ts b/fdm-calculator/src/norms/nl/2026/filling/types.d.ts index fcaf7f354..bbeed3581 100644 --- a/fdm-calculator/src/norms/nl/2026/filling/types.d.ts +++ b/fdm-calculator/src/norms/nl/2026/filling/types.d.ts @@ -1,8 +1,8 @@ import type * as schema from "@nmi-agro/fdm-core" import type { + BaseFertilizerApplication, Cultivation, Fertilizer, - FertilizerApplication, Field, } from "@nmi-agro/fdm-core" import type { RegionKey } from "../value/types" @@ -37,7 +37,7 @@ export type WorkingCoefficientDetails = { export type NL2026NormsFillingInput = { cultivations: Cultivation[] - applications: FertilizerApplication[] + applications: BaseFertilizerApplication[] fertilizers: Fertilizer[] has_organic_certification: boolean has_grazing_intention: boolean diff --git a/fdm-calculator/src/shared/types.d.ts b/fdm-calculator/src/shared/types.d.ts deleted file mode 100644 index 0573098f9..000000000 --- a/fdm-calculator/src/shared/types.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { FertilizerApplication } from "@nmi-agro/fdm-core" - -export type CalculatorFertilizerApplication = Pick< - FertilizerApplication, - | "p_id" - | "p_id_catalogue" - | "p_name_nl" - | "p_app_id" - | "p_app_date" - | "p_app_method" - | "p_app_amount" -> & - Partial diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 4867a3e84..619f36eba 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -12,6 +12,7 @@ import * as authZSchema from "./db/schema-authz" import { handleError } from "./error" import type { FdmType } from "./fdm.types" import type { + BaseFertilizerApplication, Fertilizer, FertilizerApplication, FertilizerCatalogue, @@ -915,17 +916,13 @@ export async function removeFertilizerApplication( } } -type IncompleteFertilizerApplication = Omit< - FertilizerApplication, - "p_app_amount_display" | "p_app_amount_unit" -> /** * Extends the given fertilizer application with computed data and removes unknown properties * @param app fertilizer application * @returns the same fertilizer application with p_app_amount_display filled in and properties * that do not belong to FertilizerApplication removed */ -function extendFertilizerApplication( +function extendFertilizerApplication( app: T, p_app_amount_unit: AppAmountUnit, p_density: number | null, @@ -1011,7 +1008,7 @@ export async function getFertilizerApplication( return result.length > 0 ? extendFertilizerApplication( - result[0] as IncompleteFertilizerApplication, + result[0] as BaseFertilizerApplication, result[0].p_app_amount_unit as AppAmountUnit, result[0].p_density as number, ) diff --git a/fdm-core/src/fertilizer.types.d.ts b/fdm-core/src/fertilizer.types.d.ts index 14b354416..54615b1b9 100644 --- a/fdm-core/src/fertilizer.types.d.ts +++ b/fdm-core/src/fertilizer.types.d.ts @@ -67,18 +67,21 @@ export interface Fertilizer extends FertilizerCatalogue { type FertilizerType = "manure" | "mineral" | "compost" -export interface FertilizerApplication { +export interface BaseFertilizerApplication { p_id: string p_id_catalogue: string p_name_nl: string | null p_app_amount: number | null - p_app_amount_unit: AppAmountUnit - p_app_amount_display: number | null p_app_method: ApplicationMethods | null p_app_date: Date p_app_id: string } +export interface FertilizerApplication extends BaseFertilizerApplication { + p_app_amount_unit: AppAmountUnit + p_app_amount_display: number | null +} + export type FertilizerParameters = | "p_id_catalogue" | "p_source" diff --git a/fdm-core/src/index.ts b/fdm-core/src/index.ts index 9062ba440..fe0bdab63 100644 --- a/fdm-core/src/index.ts +++ b/fdm-core/src/index.ts @@ -114,6 +114,7 @@ export { updateFertilizerFromCatalogue, } from "./fertilizer" export type { + BaseFertilizerApplication, Fertilizer, FertilizerApplication, FertilizerCatalogue, From 68f884fa2670eaad40b1912b0e2e52c954f1abce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 15 Apr 2026 10:23:05 +0200 Subject: [PATCH 19/38] Make stuff consistent --- fdm-core/src/fertilizer.ts | 25 +++++++++++++++---------- fdm-core/src/unit-conversion.test.ts | 4 ++-- fdm-core/src/unit-conversion.ts | 16 ++++++++++++---- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 619f36eba..9ad57d58d 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -976,7 +976,7 @@ export async function getFertilizerApplication( "getFertilizerApplication", ) - const result = await fdm + const result = (await fdm .select({ p_id: schema.fertilizerApplication.p_id, p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, @@ -1004,13 +1004,18 @@ export async function getFertilizerApplication( schema.fertilizerPicking.p_id_catalogue, ), ) - .where(eq(schema.fertilizerApplication.p_app_id, p_app_id)) + .where( + eq(schema.fertilizerApplication.p_app_id, p_app_id), + )) as (BaseFertilizerApplication & { + p_app_amount_unit: AppAmountUnit + p_density: schema.fertilizersCatalogueTypeSelect["p_density"] + })[] return result.length > 0 ? extendFertilizerApplication( - result[0] as BaseFertilizerApplication, - result[0].p_app_amount_unit as AppAmountUnit, - result[0].p_density as number, + result[0], + result[0].p_app_amount_unit, + result[0].p_density, ) : null } catch (err) { @@ -1099,7 +1104,7 @@ export async function getFertilizerApplications( ) .orderBy( desc(schema.fertilizerApplication.p_app_date), - )) as (FertilizerApplication & { + )) as (BaseFertilizerApplication & { p_app_amount_unit: AppAmountUnit p_density: number | null })[] @@ -1206,10 +1211,10 @@ export async function getFertilizerApplicationsForFarm( ) .orderBy( desc(schema.fertilizerApplication.p_app_date), - )) as (FertilizerApplication & { - b_id: string - p_app_amount_unit: string - p_density: number | null + )) as (BaseFertilizerApplication & { + b_id: schema.fertilizerApplicationTypeSelect["b_id"] + p_app_amount_unit: AppAmountUnit + p_density: schema.fertilizersCatalogueTypeSelect["p_density"] })[] const result = new Map() diff --git a/fdm-core/src/unit-conversion.test.ts b/fdm-core/src/unit-conversion.test.ts index 2b43caba9..47584cd36 100644 --- a/fdm-core/src/unit-conversion.test.ts +++ b/fdm-core/src/unit-conversion.test.ts @@ -93,13 +93,13 @@ describe("toKgPerHa", () => { input: 20, unit: "l/ha", density: undefined, - throws: "Density (p_density) is required for l/ha → kg/ha conversion", + throws: "Positive density (p_density) is required for l/ha → kg/ha conversion", }, { input: 20, unit: "m3/ha", density: undefined, - throws: "Density (p_density) is required for m3/ha → kg/ha conversion", + throws: "Positive density (p_density) is required for m3/ha → kg/ha conversion", }, { input: 20, diff --git a/fdm-core/src/unit-conversion.ts b/fdm-core/src/unit-conversion.ts index 389f5c7df..07973e356 100644 --- a/fdm-core/src/unit-conversion.ts +++ b/fdm-core/src/unit-conversion.ts @@ -79,15 +79,23 @@ export function toKgPerHa( case "ton/ha": return new Decimal(1000).times(d) case "l/ha": - if (density === null || density === undefined) + if ( + density === null || + density === undefined || + new Decimal(0).greaterThanOrEqualTo(density) + ) throw new Error( - "Density (p_density) is required for l/ha → kg/ha conversion", + "Positive density (p_density) is required for l/ha → kg/ha conversion", ) return new Decimal(density).times(d) case "m3/ha": - if (density === null || density === undefined) + if ( + density === null || + density === undefined || + new Decimal(0).greaterThanOrEqualTo(density) + ) throw new Error( - "Density (p_density) is required for m3/ha → kg/ha conversion", + "Positive density (p_density) is required for m3/ha → kg/ha conversion", ) return new Decimal(1000).times(d).times(density) default: From 6b0251dc8edf4d0187fa2bd18393412517c14e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Thu, 16 Apr 2026 10:35:01 +0200 Subject: [PATCH 20/38] Address nitpicks --- .../farm.$b_id_farm.$calendar.gerrit.tsx | 24 ++++++++++--------- fdm-core/src/unit-conversion.ts | 1 + 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx index 736849807..3bec78dab 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx @@ -719,17 +719,19 @@ export async function action({ request, params }: ActionFunctionArgs) { fert.p_density, ) : null - const unitConvertedAmount = fert - ? { - p_app_amount_display: p_app_amount_display - ? p_app_amount_display.toNumber() - : null, - p_app_amount_unit: fert.p_app_amount_unit, - } - : { - p_app_amount_display: app.p_app_amount, - p_app_amount_unit: "kg/ha", - } + const unitConvertedAmount = + fert && p_app_amount_display !== null + ? { + p_app_amount_display: + p_app_amount_display.toNumber(), + p_app_amount_unit: + fert.p_app_amount_unit, + } + : { + p_app_amount_display: + app.p_app_amount, + p_app_amount_unit: "kg/ha", + } return { ...app, ...unitConvertedAmount, diff --git a/fdm-core/src/unit-conversion.ts b/fdm-core/src/unit-conversion.ts index 07973e356..98668c0b9 100644 --- a/fdm-core/src/unit-conversion.ts +++ b/fdm-core/src/unit-conversion.ts @@ -250,6 +250,7 @@ export const RVO_RECOMMENDED_UNITS: RvoUnitSuggestionTableItem[] = [ { p_type_rvo: "109", type: "solid (phase 3)", unit: "ton/ha" }, { p_type_rvo: "110", type: "solid", unit: "ton/ha" }, { p_type_rvo: "111", type: "solid", unit: "ton/ha" }, + { p_type_rvo: "112", type: "solid", unit: "ton/ha" }, { p_type_rvo: "117", type: "solid", unit: "ton/ha" }, // Other From 62d302a015dbe9d9fab0e54304efe79d881369e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Fri, 17 Apr 2026 11:41:20 +0200 Subject: [PATCH 21/38] Remove the way to specify fertilizer application amount unit --- .../farm.$b_id_farm.$calendar.gerrit.tsx | 17 ++++-- fdm-core/src/fertilizer.ts | 11 ++-- fdm-core/src/unit-conversion.test.ts | 59 ------------------- fdm-core/src/unit-conversion.ts | 55 +---------------- 4 files changed, 19 insertions(+), 123 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx index 3bec78dab..d7ffa60cf 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx @@ -875,15 +875,24 @@ export async function action({ request, params }: ActionFunctionArgs) { ) } + const amount = fromKgPerHa( + app.p_app_amount, + fertilizer.p_app_amount_unit, + fertilizer.p_density, + ) + + if (amount === null) { + throw new Error( + `Meststof "${fertilizer.p_name_nl}" moet een waarde hebben voor zijn dichtheid.`, + ) + } + await addFertilizerApplication( tx, session.principal_id, field.b_id, fertilizer.p_id, - { - p_app_amount_display: app.p_app_amount, - p_app_amount_unit: "kg/ha", - }, + amount.toNumber(), app.p_app_method, new Date(app.p_app_date), ) diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 9ad57d58d..505c9ed6b 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -22,10 +22,9 @@ import { createId } from "./id" import type { Timeframe } from "./timeframe" import { APP_AMOUNT_UNITS, - type AppAmount, type AppAmountUnit, fromKgPerHa, - normalizeToKgPerHa, + toKgPerHa, } from "./unit-conversion" /** @@ -766,7 +765,7 @@ export async function addFertilizerApplication( principal_id: PrincipalId, b_id: schema.fertilizerApplicationTypeInsert["b_id"], p_id: schema.fertilizerApplicationTypeInsert["p_id"], - p_app_amount_display: AppAmount | null, + p_app_amount_display: schema.fertilizerApplicationTypeInsert["p_app_amount"], p_app_method: schema.fertilizerApplicationTypeInsert["p_app_method"], p_app_date: schema.fertilizerApplicationTypeInsert["p_app_date"], ): Promise { @@ -796,7 +795,7 @@ export async function addFertilizerApplication( const p_app_amount = p_app_amount_display !== null && p_app_amount_display !== undefined - ? normalizeToKgPerHa( + ? toKgPerHa( p_app_amount_display, fertilizer.p_app_amount_unit, fertilizer.p_density, @@ -842,7 +841,7 @@ export async function updateFertilizerApplication( principal_id: PrincipalId, p_app_id: schema.fertilizerApplicationTypeInsert["p_app_id"], p_id: schema.fertilizerApplicationTypeInsert["p_id"], - p_app_amount_display: AppAmount | null, + p_app_amount_display: schema.fertilizerApplicationTypeInsert["p_app_amount"], p_app_method: schema.fertilizerApplicationTypeInsert["p_app_method"], p_app_date: schema.fertilizerApplicationTypeInsert["p_app_date"], ): Promise { @@ -858,7 +857,7 @@ export async function updateFertilizerApplication( const fertilizer = await getFertilizer(fdm, p_id) const p_app_amount = p_app_amount_display !== null && p_app_amount_display !== undefined - ? normalizeToKgPerHa( + ? toKgPerHa( p_app_amount_display, fertilizer.p_app_amount_unit, fertilizer.p_density, diff --git a/fdm-core/src/unit-conversion.test.ts b/fdm-core/src/unit-conversion.test.ts index 47584cd36..6d2ffb69e 100644 --- a/fdm-core/src/unit-conversion.test.ts +++ b/fdm-core/src/unit-conversion.test.ts @@ -1,10 +1,8 @@ import Decimal from "decimal.js" import { describe, expect, it } from "vitest" import { - type AppAmount, type AppAmountUnit, fromKgPerHa, - normalizeToKgPerHa, type RvoUnitSuggestionTableItem, suggestUnitFromRvoCode, toKgPerHa, @@ -19,63 +17,6 @@ interface ConversionUnitTestCase { throws?: string } -describe("normalizeToKgPerHa", () => { - it("should handle null", () => { - expect(() => - normalizeToKgPerHa(null as unknown as AppAmount, "kg/ha", 0), - ).toThrow("Amount was not properly specified") - }) - it("should handle undefined", () => { - expect(() => - normalizeToKgPerHa(undefined as unknown as AppAmount, "kg/ha", 0), - ).toThrow("Amount was not properly specified") - }) - it("should convert numbers", () => { - expect(normalizeToKgPerHa(2, "l/ha", 5).toNumber()).toBe(10) - }) - it("should convert Decimal", () => { - expect(normalizeToKgPerHa(new Decimal(2), "l/ha", 5).toNumber()).toBe( - 10, - ) - }) - it("should respect specified unit", () => { - expect( - normalizeToKgPerHa( - { p_app_amount_display: 2, p_app_amount_unit: "ton/ha" }, - "l/ha", - 5, - ).toNumber(), - ).toBe(2000) - }) - it("should handle unit not specified in object", () => { - expect( - normalizeToKgPerHa( - { p_app_amount_display: 2 } as AppAmount, - "l/ha", - 5, - ).toNumber(), - ).toBe(10) - }) - it("should handle unit not specified in object", () => { - expect(() => - normalizeToKgPerHa( - { p_app_amount_unit: "kg/ha" } as AppAmount, - "l/ha", - 5, - ), - ).toThrow("Amount was not properly specified") - }) - it("should handle arbitrary object as amount", () => { - expect(() => - normalizeToKgPerHa( - { name: "John" } as unknown as AppAmount, - "l/ha", - 5, - ), - ).toThrow("Amount was not properly specified") - }) -}) - describe("toKgPerHa", () => { const tests: ConversionUnitTestCase[] = [ { input: 20, unit: "kg/ha", output: 20 }, diff --git a/fdm-core/src/unit-conversion.ts b/fdm-core/src/unit-conversion.ts index 98668c0b9..5aa96a4fe 100644 --- a/fdm-core/src/unit-conversion.ts +++ b/fdm-core/src/unit-conversion.ts @@ -1,13 +1,6 @@ import Decimal from "decimal.js" export type AppAmountUnit = "kg/ha" | "l/ha" | "m3/ha" | "ton/ha" -export type AppAmountDimensionless = number | Decimal | string -export type AppAmount = - | AppAmountDimensionless - | { - p_app_amount_display: AppAmountDimensionless - p_app_amount_unit: AppAmountUnit - } export const APP_AMOUNT_UNITS: { value: AppAmountUnit; label: string }[] = [ { value: "kg/ha", label: "kg/ha" }, @@ -16,59 +9,13 @@ export const APP_AMOUNT_UNITS: { value: AppAmountUnit; label: string }[] = [ { value: "ton/ha", label: "ton/ha" }, ] -/** - * Convert an amount and its unit to kg/ha. This offers flexibility in how the amount is specified. - * - * Take care of which defaultUnit and which density are used. Usually default_p_app_amount_unit is the - * configured display unit for the fertilizer, and p_density is the fertilizer's density in catalogue. - * @param p_app_amount_display value to convert, maybe with the unit specified - * @param default_p_app_amount_unit unit to use if input has no unit specified - * @param p_density density value to use when converting volume to mass - * @returns the amount converted to kg/ha - * - * @throws {Error} If the amount is null or undefined, or if the density was not specified when it was needed for conversion. - */ -export function normalizeToKgPerHa( - p_app_amount_display: AppAmount, - default_p_app_amount_unit: AppAmountUnit, - p_density?: number | Decimal | null, -) { - let converted_p_app_amount_display: AppAmount = p_app_amount_display - let p_app_amount_unit = default_p_app_amount_unit - if ( - p_app_amount_display instanceof Object && - "p_app_amount_display" in p_app_amount_display - ) { - converted_p_app_amount_display = - p_app_amount_display.p_app_amount_display - if (p_app_amount_display.p_app_amount_unit) { - p_app_amount_unit = p_app_amount_display.p_app_amount_unit - } - } - - if ( - (converted_p_app_amount_display instanceof Object && - !(converted_p_app_amount_display instanceof Decimal)) || - converted_p_app_amount_display === null || - typeof converted_p_app_amount_display === "undefined" - ) { - throw new Error("Amount was not properly specified") - } - - return toKgPerHa( - converted_p_app_amount_display, - p_app_amount_unit, - p_density, - ) -} - /** * Convert a user-entered amount (in display unit) to kg/ha for storage. * Uses Decimal.js to avoid floating-point rounding errors. * Throws if conversion requires density but density is null/undefined/0. */ export function toKgPerHa( - value: AppAmountDimensionless, + value: number | Decimal | string, unit: AppAmountUnit, density?: number | Decimal | null, // kg/l ): Decimal { From 2d1e8630bd7e31ac5692ae8d1ef4f6644242ea1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Fri, 17 Apr 2026 12:00:10 +0200 Subject: [PATCH 22/38] Rename unit-conversion module and add more tests --- fdm-core/src/catalogues.ts | 2 +- fdm-core/src/db/schema.ts | 2 +- ...fertilizer-application-unit-conversion.test.ts} | 14 +++++++++++++- ...s => fertilizer-application-unit-conversion.ts} | 0 fdm-core/src/fertilizer.ts | 6 +++--- fdm-core/src/fertilizer.types.d.ts | 2 +- fdm-core/src/index.ts | 7 +++++-- 7 files changed, 24 insertions(+), 9 deletions(-) rename fdm-core/src/{unit-conversion.test.ts => fertilizer-application-unit-conversion.test.ts} (93%) rename fdm-core/src/{unit-conversion.ts => fertilizer-application-unit-conversion.ts} (100%) diff --git a/fdm-core/src/catalogues.ts b/fdm-core/src/catalogues.ts index 97e4b1f2d..86c130be3 100644 --- a/fdm-core/src/catalogues.ts +++ b/fdm-core/src/catalogues.ts @@ -14,7 +14,7 @@ import type { PrincipalId } from "./authorization.types" import * as schema from "./db/schema" import { handleError } from "./error" import type { FdmType } from "./fdm.types" -import { suggestUnitFromRvoCode } from "./unit-conversion" +import { suggestUnitFromRvoCode } from "./fertilizer-application-unit-conversion" /** * Gets all enabled fertilizer catalogues for a farm. diff --git a/fdm-core/src/db/schema.ts b/fdm-core/src/db/schema.ts index 9b38e718e..3890fb88f 100644 --- a/fdm-core/src/db/schema.ts +++ b/fdm-core/src/db/schema.ts @@ -11,7 +11,7 @@ import { timestamp, uniqueIndex, } from "drizzle-orm/pg-core" -import { APP_AMOUNT_UNITS } from "../unit-conversion" +import { APP_AMOUNT_UNITS } from "../fertilizer-application-unit-conversion" import { geometry, numericCasted } from "./schema-custom-types" // Define postgres schema diff --git a/fdm-core/src/unit-conversion.test.ts b/fdm-core/src/fertilizer-application-unit-conversion.test.ts similarity index 93% rename from fdm-core/src/unit-conversion.test.ts rename to fdm-core/src/fertilizer-application-unit-conversion.test.ts index 6d2ffb69e..21afda628 100644 --- a/fdm-core/src/unit-conversion.test.ts +++ b/fdm-core/src/fertilizer-application-unit-conversion.test.ts @@ -6,7 +6,7 @@ import { type RvoUnitSuggestionTableItem, suggestUnitFromRvoCode, toKgPerHa, -} from "./unit-conversion" +} from "./fertilizer-application-unit-conversion" interface ConversionUnitTestCase { input: number @@ -48,6 +48,18 @@ describe("toKgPerHa", () => { density: 2, throws: "ft3/ha → kg/ha conversion is not supported", }, + { + input: 20, + unit: "m3/ha", + density: 0, + throws: "Positive density (p_density) is required for m3/ha → kg/ha conversion", + }, + { + input: 20, + unit: "m3/ha", + density: -1, + throws: "Positive density (p_density) is required for m3/ha → kg/ha conversion", + }, ] for (const { input, unit, density, output } of tests) { diff --git a/fdm-core/src/unit-conversion.ts b/fdm-core/src/fertilizer-application-unit-conversion.ts similarity index 100% rename from fdm-core/src/unit-conversion.ts rename to fdm-core/src/fertilizer-application-unit-conversion.ts diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 505c9ed6b..aa3e3e758 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -18,14 +18,14 @@ import type { FertilizerCatalogue, FertilizerParameterDescription, } from "./fertilizer.types" -import { createId } from "./id" -import type { Timeframe } from "./timeframe" import { APP_AMOUNT_UNITS, type AppAmountUnit, fromKgPerHa, toKgPerHa, -} from "./unit-conversion" +} from "./fertilizer-application-unit-conversion" +import { createId } from "./id" +import type { Timeframe } from "./timeframe" /** * Retrieves all fertilizers from the enabled catalogues for a farm. diff --git a/fdm-core/src/fertilizer.types.d.ts b/fdm-core/src/fertilizer.types.d.ts index 54615b1b9..bbd6ed29d 100644 --- a/fdm-core/src/fertilizer.types.d.ts +++ b/fdm-core/src/fertilizer.types.d.ts @@ -1,6 +1,6 @@ import type { ApplicationMethods } from "@nmi-agro/fdm-data" import type * as schema from "./db/schema" -import type { AppAmountUnit } from "./unit-conversion" +import type { AppAmountUnit } from "./fertilizer-application-unit-conversion" export interface FertilizerCatalogue { p_id_catalogue: string diff --git a/fdm-core/src/index.ts b/fdm-core/src/index.ts index fe0bdab63..49ccf50c1 100644 --- a/fdm-core/src/index.ts +++ b/fdm-core/src/index.ts @@ -122,6 +122,11 @@ export type { FertilizerParameterDescriptionItem, FertilizerParameters, } from "./fertilizer.types" +export type { AppAmountUnit } from "./fertilizer-application-unit-conversion" +export { + fromKgPerHa, + toKgPerHa, +} from "./fertilizer-application-unit-conversion" export { addField, getField, @@ -194,6 +199,4 @@ export type { SoilParameters, } from "./soil.types" export type { Timeframe } from "./timeframe.d" -export type { AppAmountUnit } from "./unit-conversion" -export { fromKgPerHa, toKgPerHa } from "./unit-conversion" export { fdmSchema } From 91ffed362edb798fa8cf2615c710256a014d6c7d Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:36:34 +0200 Subject: [PATCH 23/38] fix: use lowercase for liters --- fdm-core/src/fertilizer-application-unit-conversion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdm-core/src/fertilizer-application-unit-conversion.ts b/fdm-core/src/fertilizer-application-unit-conversion.ts index 5aa96a4fe..d87e5745e 100644 --- a/fdm-core/src/fertilizer-application-unit-conversion.ts +++ b/fdm-core/src/fertilizer-application-unit-conversion.ts @@ -4,7 +4,7 @@ export type AppAmountUnit = "kg/ha" | "l/ha" | "m3/ha" | "ton/ha" export const APP_AMOUNT_UNITS: { value: AppAmountUnit; label: string }[] = [ { value: "kg/ha", label: "kg/ha" }, - { value: "l/ha", label: "L/ha" }, + { value: "l/ha", label: "l/ha" }, { value: "m3/ha", label: "m³/ha" }, { value: "ton/ha", label: "ton/ha" }, ] From 1ded75c1a03d6105b1d649d360612900d8c859d8 Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:43:03 +0200 Subject: [PATCH 24/38] refactor: at the unit conversion return number instead of decimal --- .../farm.$b_id_farm.$calendar.gerrit.tsx | 2 +- ...ilizer-application-unit-conversion.test.ts | 14 ++++--------- .../fertilizer-application-unit-conversion.ts | 20 +++++++++---------- fdm-core/src/fertilizer.ts | 10 +++------- 4 files changed, 18 insertions(+), 28 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx index d7ffa60cf..16358d5f2 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx @@ -723,7 +723,7 @@ export async function action({ request, params }: ActionFunctionArgs) { fert && p_app_amount_display !== null ? { p_app_amount_display: - p_app_amount_display.toNumber(), + p_app_amount_display, p_app_amount_unit: fert.p_app_amount_unit, } diff --git a/fdm-core/src/fertilizer-application-unit-conversion.test.ts b/fdm-core/src/fertilizer-application-unit-conversion.test.ts index 21afda628..f4a0cfcbc 100644 --- a/fdm-core/src/fertilizer-application-unit-conversion.test.ts +++ b/fdm-core/src/fertilizer-application-unit-conversion.test.ts @@ -68,7 +68,7 @@ describe("toKgPerHa", () => { ? `should convert ${unit} to kg/ha with density ${density} kg/l` : `should convert ${unit} to kg/ha without density specified`, () => { - expect(toKgPerHa(input, unit, density).toNumber()).toBe(output) + expect(toKgPerHa(input, unit, density)).toBe(output) }, ) } @@ -85,9 +85,7 @@ describe("toKgPerHa", () => { } it("should accept input of type Decimal", () => { - expect( - toKgPerHa(new Decimal(10), "m3/ha", new Decimal(2)).toNumber(), - ).toBe(20000) + expect(toKgPerHa(new Decimal(10), "m3/ha", new Decimal(2))).toBe(20000) }) }) @@ -140,18 +138,14 @@ describe("fromKgPerHa", () => { : `should convert kg/ha to ${unit} without density specified`, () => { const value = fromKgPerHa(input, unit, density) - expect(value !== null ? value.toNumber() : null).toBe(output) + expect(value).toBe(output) }, ) } it("should accept input of type Decimal", () => { expect( - fromKgPerHa( - new Decimal(20000), - "m3/ha", - new Decimal(2), - )?.toNumber(), + fromKgPerHa(new Decimal(20000), "m3/ha", new Decimal(2)), ).toBe(10) }) }) diff --git a/fdm-core/src/fertilizer-application-unit-conversion.ts b/fdm-core/src/fertilizer-application-unit-conversion.ts index d87e5745e..860aa13b1 100644 --- a/fdm-core/src/fertilizer-application-unit-conversion.ts +++ b/fdm-core/src/fertilizer-application-unit-conversion.ts @@ -18,13 +18,13 @@ export function toKgPerHa( value: number | Decimal | string, unit: AppAmountUnit, density?: number | Decimal | null, // kg/l -): Decimal { +): number { const d = new Decimal(value) switch (unit) { case "kg/ha": - return new Decimal(d) + return new Decimal(d).toNumber() case "ton/ha": - return new Decimal(1000).times(d) + return new Decimal(1000).times(d).toNumber() case "l/ha": if ( density === null || @@ -34,7 +34,7 @@ export function toKgPerHa( throw new Error( "Positive density (p_density) is required for l/ha → kg/ha conversion", ) - return new Decimal(density).times(d) + return new Decimal(density).times(d).toNumber() case "m3/ha": if ( density === null || @@ -44,7 +44,7 @@ export function toKgPerHa( throw new Error( "Positive density (p_density) is required for m3/ha → kg/ha conversion", ) - return new Decimal(1000).times(d).times(density) + return new Decimal(1000).times(d).times(density).toNumber() default: throw new Error(`${unit} → kg/ha conversion is not supported`) } @@ -59,7 +59,7 @@ export function fromKgPerHa( valueKgPerHa: number | Decimal | string, unit: AppAmountUnit, density?: number | Decimal | null, // kg/l -): Decimal | null { +): number | null { const d = new Decimal(valueKgPerHa) const densityNotProvided = density === null || @@ -67,15 +67,15 @@ export function fromKgPerHa( new Decimal(0).greaterThanOrEqualTo(density) switch (unit) { case "kg/ha": - return d + return d.toNumber() case "ton/ha": - return d.dividedBy(1000) + return d.dividedBy(1000).toNumber() case "l/ha": if (densityNotProvided) return null - return d.dividedBy(new Decimal(density)) + return d.dividedBy(new Decimal(density)).toNumber() case "m3/ha": if (densityNotProvided) return null - return d.dividedBy(new Decimal(density).times(1000)) + return d.dividedBy(new Decimal(density).times(1000)).toNumber() default: return null } diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index aa3e3e758..95992f0e9 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -799,7 +799,7 @@ export async function addFertilizerApplication( p_app_amount_display, fertilizer.p_app_amount_unit, fertilizer.p_density, - ).toNumber() + ) : null await fdm.insert(schema.fertilizerApplication).values({ @@ -861,7 +861,7 @@ export async function updateFertilizerApplication( p_app_amount_display, fertilizer.p_app_amount_unit, fertilizer.p_density, - ).toNumber() + ) : p_app_amount_display await fdm .update(schema.fertilizerApplication) @@ -928,11 +928,7 @@ function extendFertilizerApplication( ): FertilizerApplication { const maybe_p_app_amount_display = app.p_app_amount !== null && app.p_app_amount !== undefined - ? fromKgPerHa( - app.p_app_amount, - p_app_amount_unit, - p_density, - )?.toNumber() + ? fromKgPerHa(app.p_app_amount, p_app_amount_unit, p_density) : app.p_app_amount return { p_id: app.p_id, From 8ae13875c5fadcf9b0525c9a78dc954768e1e996 Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:45:25 +0200 Subject: [PATCH 25/38] docs: improve typedocs for fertilizer application unit conversion functions --- .../fertilizer-application-unit-conversion.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/fdm-core/src/fertilizer-application-unit-conversion.ts b/fdm-core/src/fertilizer-application-unit-conversion.ts index 860aa13b1..4ddf675b5 100644 --- a/fdm-core/src/fertilizer-application-unit-conversion.ts +++ b/fdm-core/src/fertilizer-application-unit-conversion.ts @@ -11,8 +11,15 @@ export const APP_AMOUNT_UNITS: { value: AppAmountUnit; label: string }[] = [ /** * Convert a user-entered amount (in display unit) to kg/ha for storage. + * * Uses Decimal.js to avoid floating-point rounding errors. * Throws if conversion requires density but density is null/undefined/0. + * + * @param value The value to convert. + * @param unit The display unit of the value. + * @param density The density of the fertilizer in kg/l. Required for volume-based units. + * @returns The converted value in kg/ha. + * @alpha */ export function toKgPerHa( value: number | Decimal | string, @@ -52,8 +59,15 @@ export function toKgPerHa( /** * Convert a stored kg/ha value back to the preferred display unit. + * * Uses Decimal.js to avoid floating-point rounding errors. * Returns null if conversion requires density but density is missing. + * + * @param valueKgPerHa The value in kg/ha to convert. + * @param unit The target display unit. + * @param density The density of the fertilizer in kg/l. Required for volume-based units. + * @returns The converted value in the target unit, or null if density is missing. + * @alpha */ export function fromKgPerHa( valueKgPerHa: number | Decimal | string, @@ -83,6 +97,7 @@ export function fromKgPerHa( /** * Suggest a default display unit based on an RVO fertilizer type code. + * * The suggestion is a sensible starting point; the user can always override it. * * A table of suggestions is provided internally. Callers can pass the table argument to use a different table. @@ -93,8 +108,10 @@ export function fromKgPerHa( * - Compost / solid organic matter → ton/ha * - Mineral / other → kg/ha (default) * - * @param p_type_rvo: mest code to look for - * @param table: optional: table to use for conversion. The type can be used to add remarks to each item when hardcoding tables. + * @param p_type_rvo The mest code to look for. + * @param table Optional table to use for conversion. The type can be used to add remarks to each item when hardcoding tables. + * @returns The suggested display unit. + * @alpha */ export function suggestUnitFromRvoCode( p_type_rvo: string, From 45ba4c688b452f787a30ede902a44f2bcf5d5559 Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:53:58 +0200 Subject: [PATCH 26/38] refactor: remove functio to guess unit of fertilizer --- fdm-core/src/catalogues.ts | 7 +-- ...ilizer-application-unit-conversion.test.ts | 49 ++----------------- .../fertilizer-application-unit-conversion.ts | 30 ------------ 3 files changed, 4 insertions(+), 82 deletions(-) diff --git a/fdm-core/src/catalogues.ts b/fdm-core/src/catalogues.ts index 86c130be3..72d98b7e2 100644 --- a/fdm-core/src/catalogues.ts +++ b/fdm-core/src/catalogues.ts @@ -14,7 +14,6 @@ import type { PrincipalId } from "./authorization.types" import * as schema from "./db/schema" import { handleError } from "./error" import type { FdmType } from "./fdm.types" -import { suggestUnitFromRvoCode } from "./fertilizer-application-unit-conversion" /** * Gets all enabled fertilizer catalogues for a farm. @@ -553,11 +552,7 @@ async function extendCatalogueFertilizer( ) { const fertWithComputedProps = { ...catalogueFertilizer, - p_app_amount_unit: - catalogueFertilizer.p_app_amount_unit ?? - (catalogueFertilizer.p_type_rvo - ? suggestUnitFromRvoCode(catalogueFertilizer.p_type_rvo) - : undefined), + p_app_amount_unit: catalogueFertilizer.p_app_amount_unit, } return { ...fertWithComputedProps, diff --git a/fdm-core/src/fertilizer-application-unit-conversion.test.ts b/fdm-core/src/fertilizer-application-unit-conversion.test.ts index f4a0cfcbc..ba39bedc6 100644 --- a/fdm-core/src/fertilizer-application-unit-conversion.test.ts +++ b/fdm-core/src/fertilizer-application-unit-conversion.test.ts @@ -4,7 +4,6 @@ import { type AppAmountUnit, fromKgPerHa, type RvoUnitSuggestionTableItem, - suggestUnitFromRvoCode, toKgPerHa, } from "./fertilizer-application-unit-conversion" @@ -144,50 +143,8 @@ describe("fromKgPerHa", () => { } it("should accept input of type Decimal", () => { - expect( - fromKgPerHa(new Decimal(20000), "m3/ha", new Decimal(2)), - ).toBe(10) - }) -}) - -describe("suggestUnitFromRvoCode", () => { - describe("internal table", () => { - it("should return ton/ha for solid cattle manure", () => { - expect(suggestUnitFromRvoCode("10")).toBe("ton/ha") - }) - it("should return m3/ha for swine slurry", () => { - expect(suggestUnitFromRvoCode("42")).toBe("m3/ha") - }) - it("should return l/ha for liquid goat manure", () => { - expect(suggestUnitFromRvoCode("60")).toBe("l/ha") - }) - it("should return kg/ha for mineral fertilizers", () => { - expect(suggestUnitFromRvoCode("115")).toBe("kg/ha") - }) - }) - - describe("custom table", () => { - const customTable: RvoUnitSuggestionTableItem[] = [ - { p_type_rvo: "42", type: "other", unit: "kg/ha" }, - { p_type_rvo: "113", type: "solid sewage", unit: "ton/ha" }, - { p_type_rvo: "114", type: "liquid sewage", unit: "l/ha" }, - { p_type_rvo: "115", type: "swine slurry", unit: "m3/ha" }, - ] - - it("should return kg/ha for other fertilizers in custom table", () => { - expect(suggestUnitFromRvoCode("42", customTable)).toBe("kg/ha") - }) - it("should return ton/ha for solid sewage", () => { - expect(suggestUnitFromRvoCode("113", customTable)).toBe("ton/ha") - }) - it("should return l/ha for liquid sewage in custom table", () => { - expect(suggestUnitFromRvoCode("114", customTable)).toBe("l/ha") - }) - it("should return m3/ha for swine slurry", () => { - expect(suggestUnitFromRvoCode("115", customTable)).toBe("m3/ha") - }) - it("should return kg/ha for code not in table", () => { - expect(suggestUnitFromRvoCode("10", customTable)).toBe("kg/ha") - }) + expect(fromKgPerHa(new Decimal(20000), "m3/ha", new Decimal(2))).toBe( + 10, + ) }) }) diff --git a/fdm-core/src/fertilizer-application-unit-conversion.ts b/fdm-core/src/fertilizer-application-unit-conversion.ts index 4ddf675b5..57135278b 100644 --- a/fdm-core/src/fertilizer-application-unit-conversion.ts +++ b/fdm-core/src/fertilizer-application-unit-conversion.ts @@ -95,36 +95,6 @@ export function fromKgPerHa( } } -/** - * Suggest a default display unit based on an RVO fertilizer type code. - * - * The suggestion is a sensible starting point; the user can always override it. - * - * A table of suggestions is provided internally. Callers can pass the table argument to use a different table. - * - * The internal table is based on the Tabel 11 mest codes provided by RVO and is based on the rationale: - * - Slurry / drijfmest codes → m3/ha - * - Liquid concentrates / digestate → l/ha - * - Compost / solid organic matter → ton/ha - * - Mineral / other → kg/ha (default) - * - * @param p_type_rvo The mest code to look for. - * @param table Optional table to use for conversion. The type can be used to add remarks to each item when hardcoding tables. - * @returns The suggested display unit. - * @alpha - */ -export function suggestUnitFromRvoCode( - p_type_rvo: string, - table = RVO_RECOMMENDED_UNITS, -): AppAmountUnit { - const rowOrDefault = table.find((row) => row.p_type_rvo === p_type_rvo) ?? { - p_type_rvo, - unit: "kg/ha", - } - - return rowOrDefault.unit -} - export type RvoUnitSuggestionTableItem = { p_type_rvo: string unit: AppAmountUnit From a922790e92121bf5c1371885e17533fab3c27bf8 Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:13:51 +0200 Subject: [PATCH 27/38] feat: Add default values for p_app_amount_unit to baat catalogue --- .changeset/fiery-candies-kneel.md | 5 ++ fdm-data/src/fertilizers/catalogues/baat.json | 70 +++++++++++++++++++ fdm-data/src/fertilizers/catalogues/baat.ts | 5 ++ fdm-data/src/fertilizers/catalogues/srm.ts | 6 ++ fdm-data/src/fertilizers/d.ts | 4 +- 5 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 .changeset/fiery-candies-kneel.md diff --git a/.changeset/fiery-candies-kneel.md b/.changeset/fiery-candies-kneel.md new file mode 100644 index 000000000..9ddc12263 --- /dev/null +++ b/.changeset/fiery-candies-kneel.md @@ -0,0 +1,5 @@ +--- +"@nmi-agro/fdm-data": minor +--- + +Add default values for p_app_amount_unit to baat catalogue diff --git a/fdm-data/src/fertilizers/catalogues/baat.json b/fdm-data/src/fertilizers/catalogues/baat.json index 21e493a9f..643c745d0 100644 --- a/fdm-data/src/fertilizers/catalogues/baat.json +++ b/fdm-data/src/fertilizers/catalogues/baat.json @@ -36,6 +36,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -77,6 +78,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -118,6 +120,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "spoke wheel||spraying", + "p_app_amount_unit": "l/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -159,6 +162,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "spoke wheel||spraying", + "p_app_amount_unit": "l/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -200,6 +204,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -241,6 +246,7 @@ "p_zn_rt": null, "p_type_rvo": 116, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": false @@ -282,6 +288,7 @@ "p_zn_rt": null, "p_type_rvo": 116, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": false @@ -323,6 +330,7 @@ "p_zn_rt": null, "p_type_rvo": 116, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": false @@ -364,6 +372,7 @@ "p_zn_rt": null, "p_type_rvo": 116, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": false @@ -405,6 +414,7 @@ "p_zn_rt": null, "p_type_rvo": 116, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": false @@ -446,6 +456,7 @@ "p_zn_rt": null, "p_type_rvo": 116, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": false @@ -487,6 +498,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -528,6 +540,7 @@ "p_zn_rt": 42, "p_type_rvo": 110, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": true @@ -569,6 +582,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -610,6 +624,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -651,6 +666,7 @@ "p_zn_rt": 73, "p_type_rvo": 80, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -692,6 +708,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -733,6 +750,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -774,6 +792,7 @@ "p_zn_rt": 55, "p_type_rvo": 61, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -815,6 +834,7 @@ "p_zn_rt": 111, "p_type_rvo": 111, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": true @@ -856,6 +876,7 @@ "p_zn_rt": 38, "p_type_rvo": 111, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": true @@ -897,6 +918,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -938,6 +960,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -979,6 +1002,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1020,6 +1044,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1061,6 +1086,7 @@ "p_zn_rt": 167, "p_type_rvo": 23, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -1102,6 +1128,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1143,6 +1170,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1184,6 +1212,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1225,6 +1254,7 @@ "p_zn_rt": 190, "p_type_rvo": 35, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -1266,6 +1296,7 @@ "p_zn_rt": 81, "p_type_rvo": 90, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -1307,6 +1338,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1348,6 +1380,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1389,6 +1422,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1430,6 +1464,7 @@ "p_zn_rt": null, "p_type_rvo": 120, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -1471,6 +1506,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1512,6 +1548,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1553,6 +1590,7 @@ "p_zn_rt": 89, "p_type_rvo": 75, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -1594,6 +1632,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1635,6 +1674,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1676,6 +1716,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1717,6 +1758,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1758,6 +1800,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1799,6 +1842,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "spoke wheel||spraying", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1840,6 +1884,7 @@ "p_zn_rt": 57, "p_type_rvo": 25, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -1881,6 +1926,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1922,6 +1968,7 @@ "p_zn_rt": null, "p_type_rvo": 33, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -1963,6 +2010,7 @@ "p_zn_rt": 4, "p_type_rvo": 19, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2004,6 +2052,7 @@ "p_zn_rt": 38, "p_type_rvo": 10, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2045,6 +2094,7 @@ "p_zn_rt": 17, "p_type_rvo": 14, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2086,6 +2136,7 @@ "p_zn_rt": 5, "p_type_rvo": 12, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2127,6 +2178,7 @@ "p_zn_rt": null, "p_type_rvo": 13, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2168,6 +2220,7 @@ "p_zn_rt": 55, "p_type_rvo": 56, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2209,6 +2262,7 @@ "p_zn_rt": null, "p_type_rvo": 116, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2250,6 +2304,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -2291,6 +2346,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -2332,6 +2388,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "spoke wheel||spraying", + "p_app_amount_unit": "l/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -2373,6 +2430,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -2414,6 +2472,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "spoke wheel||spraying", + "p_app_amount_unit": "l/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -2455,6 +2514,7 @@ "p_zn_rt": 248, "p_type_rvo": 40, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2496,6 +2556,7 @@ "p_zn_rt": null, "p_type_rvo": 43, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2537,6 +2598,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": false @@ -2578,6 +2640,7 @@ "p_zn_rt": 23, "p_type_rvo": 39, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2619,6 +2682,7 @@ "p_zn_rt": 89, "p_type_rvo": 50, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2660,6 +2724,7 @@ "p_zn_rt": 81, "p_type_rvo": 42, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2701,6 +2766,7 @@ "p_zn_rt": 4, "p_type_rvo": 18, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2742,6 +2808,7 @@ "p_zn_rt": 64, "p_type_rvo": 46, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2783,6 +2850,7 @@ "p_zn_rt": 10, "p_type_rvo": 42, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2824,6 +2892,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -2865,6 +2934,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false diff --git a/fdm-data/src/fertilizers/catalogues/baat.ts b/fdm-data/src/fertilizers/catalogues/baat.ts index d3e255726..ff6df0d2a 100644 --- a/fdm-data/src/fertilizers/catalogues/baat.ts +++ b/fdm-data/src/fertilizers/catalogues/baat.ts @@ -1,5 +1,6 @@ import type { ApplicationMethods, + ApplicationUnits, CatalogueFertilizer, CatalogueFertilizerItem, } from "../d" @@ -30,6 +31,10 @@ export async function getCatalogueBaat(): Promise { : (fertilizer.p_app_method_options.split( "||", ) as ApplicationMethods[]), + p_app_amount_unit: + fertilizer.p_app_amount_unit === undefined + ? "kg/ha" + : (fertilizer.p_app_amount_unit as ApplicationUnits), p_ef_nh3: fertilizer.p_ef_nh3 === undefined ? null : fertilizer.p_ef_nh3, p_dm: fertilizer.p_dm === undefined ? null : fertilizer.p_dm, diff --git a/fdm-data/src/fertilizers/catalogues/srm.ts b/fdm-data/src/fertilizers/catalogues/srm.ts index 48fd7f0e7..65fb6b3c7 100644 --- a/fdm-data/src/fertilizers/catalogues/srm.ts +++ b/fdm-data/src/fertilizers/catalogues/srm.ts @@ -1,5 +1,6 @@ import type { ApplicationMethods, + ApplicationUnits, CatalogueFertilizer, CatalogueFertilizerItem, } from "../d" @@ -30,6 +31,11 @@ export async function getCatalogueSrm(): Promise { : (fertilizer.p_app_method_options.split( "||", ) as ApplicationMethods[]), + p_app_amount_unit: + (fertilizer as any).p_app_amount_unit === undefined + ? "kg/ha" + : ((fertilizer as any) + .p_app_amount_unit as ApplicationUnits), p_dm: fertilizer.p_dm === undefined ? null : fertilizer.p_dm, p_density: fertilizer.p_density === undefined diff --git a/fdm-data/src/fertilizers/d.ts b/fdm-data/src/fertilizers/d.ts index 0e8b835e2..fdceb9a34 100644 --- a/fdm-data/src/fertilizers/d.ts +++ b/fdm-data/src/fertilizers/d.ts @@ -12,6 +12,8 @@ export type ApplicationMethods = | "pocket placement" | "narrowband" +export type ApplicationUnits = "kg/ha" | "ton/ha" | "l/ha" | "m3/ha" + export interface CatalogueFertilizerItem { p_source: CatalogueFertilizerName | string p_id_catalogue: string @@ -19,7 +21,7 @@ export interface CatalogueFertilizerItem { p_name_en?: string | null | undefined p_description?: string | null | undefined p_app_method_options?: ApplicationMethods[] | null | undefined - p_app_amount_unit?: "kg/ha" | "ton/ha" | "l/ha" | "m3/ha" | undefined + p_app_amount_unit?: ApplicationUnits | null | undefined p_ef_nh3?: number | null p_dm?: number | null p_density?: number | null From 30dfb1b29b6fca43659db9eeaca5a8a9392ccc59 Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:14:46 +0200 Subject: [PATCH 28/38] refactor: remove some left over code --- ...ilizer-application-unit-conversion.test.ts | 2 - .../fertilizer-application-unit-conversion.ts | 104 +----------------- 2 files changed, 1 insertion(+), 105 deletions(-) diff --git a/fdm-core/src/fertilizer-application-unit-conversion.test.ts b/fdm-core/src/fertilizer-application-unit-conversion.test.ts index ba39bedc6..a6633ecfb 100644 --- a/fdm-core/src/fertilizer-application-unit-conversion.test.ts +++ b/fdm-core/src/fertilizer-application-unit-conversion.test.ts @@ -3,7 +3,6 @@ import { describe, expect, it } from "vitest" import { type AppAmountUnit, fromKgPerHa, - type RvoUnitSuggestionTableItem, toKgPerHa, } from "./fertilizer-application-unit-conversion" @@ -11,7 +10,6 @@ interface ConversionUnitTestCase { input: number unit: AppAmountUnit density?: number | undefined - output?: number | null throws?: string } diff --git a/fdm-core/src/fertilizer-application-unit-conversion.ts b/fdm-core/src/fertilizer-application-unit-conversion.ts index 57135278b..bbe1d292d 100644 --- a/fdm-core/src/fertilizer-application-unit-conversion.ts +++ b/fdm-core/src/fertilizer-application-unit-conversion.ts @@ -93,106 +93,4 @@ export function fromKgPerHa( default: return null } -} - -export type RvoUnitSuggestionTableItem = { - p_type_rvo: string - unit: AppAmountUnit - type?: string -} - -export const RVO_RECOMMENDED_UNITS: RvoUnitSuggestionTableItem[] = [ - // Cattle - { p_type_rvo: "10", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "11", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "12", type: "slurry", unit: "m3/ha" }, - { p_type_rvo: "13", type: "solid (dikke fractie)", unit: "ton/ha" }, - { p_type_rvo: "14", type: "liquid", unit: "l/ha" }, - { p_type_rvo: "17", type: "slurry", unit: "m3/ha" }, - { p_type_rvo: "18", type: "solid (young calf)", unit: "ton/ha" }, - { p_type_rvo: "19", type: "solid (older meat calf)", unit: "ton/ha" }, - - // Turkey - { p_type_rvo: "23", type: "solid", unit: "ton/ha" }, - - // Equines - { p_type_rvo: "25", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "26", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "27", type: "solid", unit: "ton/ha" }, - - // Poultry - { p_type_rvo: "30", type: "liquid", unit: "l/ha" }, - { p_type_rvo: "31", type: "solid (deep pit)", unit: "ton/ha" }, - { p_type_rvo: "32", type: "solid (mestband)", unit: "ton/ha" }, - { p_type_rvo: "33", type: "solid (mestband + nadroog)", unit: "ton/ha" }, - { p_type_rvo: "35", type: "solid (strooiselstal)", unit: "ton/ha" }, - - // Game fowl - { p_type_rvo: "39", type: "solid", unit: "ton/ha" }, - - // Hogs - { p_type_rvo: "40", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "41", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "42", type: "slurry", unit: "m3/ha" }, - { p_type_rvo: "43", type: "solid (dikke fractie)", unit: "ton/ha" }, - { p_type_rvo: "46", type: "liquid", unit: "l/ha" }, - { p_type_rvo: "50", type: "liquid", unit: "l/ha" }, - - // Sheep - { p_type_rvo: "56", type: "solid", unit: "ton/ha" }, - - // Goat - { p_type_rvo: "60", type: "liquid", unit: "l/ha" }, - { p_type_rvo: "61", type: "solid", unit: "ton/ha" }, - - // Nerts / Mink - { p_type_rvo: "75", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "76", type: "liquid", unit: "l/ha" }, - - // Ducks - { p_type_rvo: "80", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "81", type: "liquid", unit: "l/ha" }, - - // Rabbit - { p_type_rvo: "90", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "91", type: "liquid (very diluted)", unit: "l/ha" }, - { p_type_rvo: "92", type: "liquid", unit: "l/ha" }, - - // Deer - { p_type_rvo: "95", type: "solid", unit: "ton/ha" }, - - // Water buffalo - { p_type_rvo: "96", type: "solid", unit: "ton/ha" }, - - // Other birds - { p_type_rvo: "97", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "98", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "99", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "100", type: "solid", unit: "ton/ha" }, - - // Rodents - { p_type_rvo: "101", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "102", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "103", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "104", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "105", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "106", type: "solid", unit: "ton/ha" }, - - // Compost - { p_type_rvo: "107", type: "solid (phase 1)", unit: "ton/ha" }, - { p_type_rvo: "108", type: "solid (phase 2)", unit: "ton/ha" }, - { p_type_rvo: "109", type: "solid (phase 3)", unit: "ton/ha" }, - { p_type_rvo: "110", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "111", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "112", type: "solid", unit: "ton/ha" }, - { p_type_rvo: "117", type: "solid", unit: "ton/ha" }, - - // Other - { p_type_rvo: "113", type: "liquid sewage", unit: "l/ha" }, - { p_type_rvo: "114", type: "solid sewage", unit: "ton/ha" }, - { p_type_rvo: "115", type: "other", unit: "kg/ha" }, - { p_type_rvo: "116", type: "other", unit: "kg/ha" }, - - // Maybe non-standard - { p_type_rvo: "120", type: "other - mineral concentrate", unit: "kg/ha" }, -] +} \ No newline at end of file From 3cbec2cdf36b4352b29914817c0bc5084faf66d8 Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:46:51 +0200 Subject: [PATCH 29/38] refactor: use lowecase for liter --- fdm-app/app/components/blocks/fertilizer-applications/utils.ts | 2 +- fdm-app/app/components/blocks/fertilizer/formschema.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fdm-app/app/components/blocks/fertilizer-applications/utils.ts b/fdm-app/app/components/blocks/fertilizer-applications/utils.ts index 4feaa1272..fa97d9c33 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/utils.ts +++ b/fdm-app/app/components/blocks/fertilizer-applications/utils.ts @@ -3,7 +3,7 @@ import type { AppAmountUnit } from "@nmi-agro/fdm-core" export const applicationUnitOptions = { "kg/ha": { label: "kg/ha", totalLabel: "kg" }, "ton/ha": { label: "ton/ha", totalLabel: "ton" }, - "l/ha": { label: "L/ha", totalLabel: "L" }, + "l/ha": { label: "l/ha", totalLabel: "L" }, "m3/ha": { label: "m³/ha", totalLabel: "m³" }, } as const diff --git a/fdm-app/app/components/blocks/fertilizer/formschema.tsx b/fdm-app/app/components/blocks/fertilizer/formschema.tsx index 29dc4c9d6..65d9968e9 100644 --- a/fdm-app/app/components/blocks/fertilizer/formschema.tsx +++ b/fdm-app/app/components/blocks/fertilizer/formschema.tsx @@ -480,7 +480,7 @@ export const FormSchema = z }, { path: ["p_density"], - error: "Dichtheid is verplicht bij gebruik van L/ha of m³/ha", + error: "Dichtheid is verplicht bij gebruik van l/ha of m³/ha", }, ) .refine( From e9c797ddacb658ff5ffe2477a1fc959038686096 Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 17 Apr 2026 14:47:45 +0200 Subject: [PATCH 30/38] refactor: some type improvements --- fdm-core/src/catalogues.ts | 4 +- fdm-core/src/fertilizer.test.ts | 12 ++--- fdm-core/src/fertilizer.ts | 83 +++++++++++++++--------------- fdm-core/src/fertilizer.types.d.ts | 12 +++-- 4 files changed, 57 insertions(+), 54 deletions(-) diff --git a/fdm-core/src/catalogues.ts b/fdm-core/src/catalogues.ts index 72d98b7e2..b5b7b4d83 100644 --- a/fdm-core/src/catalogues.ts +++ b/fdm-core/src/catalogues.ts @@ -14,6 +14,7 @@ import type { PrincipalId } from "./authorization.types" import * as schema from "./db/schema" import { handleError } from "./error" import type { FdmType } from "./fdm.types" +import type { AppAmountUnit } from "./fertilizer-application-unit-conversion" /** * Gets all enabled fertilizer catalogues for a farm. @@ -552,7 +553,8 @@ async function extendCatalogueFertilizer( ) { const fertWithComputedProps = { ...catalogueFertilizer, - p_app_amount_unit: catalogueFertilizer.p_app_amount_unit, + p_app_amount_unit: (catalogueFertilizer.p_app_amount_unit ?? + "kg/ha") as AppAmountUnit, } return { ...fertWithComputedProps, diff --git a/fdm-core/src/fertilizer.test.ts b/fdm-core/src/fertilizer.test.ts index d99752d55..5dfe46833 100644 --- a/fdm-core/src/fertilizer.test.ts +++ b/fdm-core/src/fertilizer.test.ts @@ -1215,15 +1215,15 @@ describe("Fertilizer Data Model", () => { // Clean up the database after each test (optional) }) - it("should add a new fertilizer application", async () => { + it("should add a new fertilizer application with no amount specified", async () => { const p_app_date = new Date("2024-03-15") const new_p_app_id = await addFertilizerApplication( fdm, principal_id, b_id, - p_id, - 100, + p_id_liquid, + 0, "broadcasting", p_app_date, ) @@ -1241,7 +1241,7 @@ describe("Fertilizer Data Model", () => { expect(fertilizerApplication?.p_app_date).toEqual(p_app_date) }) - it("should add a new fertilizer application with amount specified in volume per ha", async () => { + it("should add a new fertilizer application with no amount specified", async () => { const p_app_date = new Date("2024-03-15") const new_p_app_id = await addFertilizerApplication( @@ -1249,7 +1249,7 @@ describe("Fertilizer Data Model", () => { principal_id, b_id, p_id_liquid, - 100, + 0, "broadcasting", p_app_date, ) @@ -1275,7 +1275,7 @@ describe("Fertilizer Data Model", () => { principal_id, b_id, p_id_liquid, - null, + 0, "broadcasting", p_app_date, ) diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 95992f0e9..226068a6f 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -753,7 +753,7 @@ export async function removeFertilizer( * @param principal_id - The ID of the principal performing the operation. * @param b_id - The ID of the field where the fertilizer application is recorded. * @param p_id - The ID of the fertilizer to be applied. - * @param p_app_amount - The amount of fertilizer applied. + * @param p_app_amount_display - The amount of fertilizer applied in the display unit. * @param p_app_method - The method used for applying the fertilizer. * @param p_app_date - The date of the fertilizer application. * @returns A Promise that resolves with the unique ID of the newly created fertilizer application record. @@ -765,7 +765,7 @@ export async function addFertilizerApplication( principal_id: PrincipalId, b_id: schema.fertilizerApplicationTypeInsert["b_id"], p_id: schema.fertilizerApplicationTypeInsert["p_id"], - p_app_amount_display: schema.fertilizerApplicationTypeInsert["p_app_amount"], + p_app_amount_display: number, p_app_method: schema.fertilizerApplicationTypeInsert["p_app_method"], p_app_date: schema.fertilizerApplicationTypeInsert["p_app_date"], ): Promise { @@ -793,14 +793,11 @@ export async function addFertilizerApplication( const p_app_id = createId() - const p_app_amount = - p_app_amount_display !== null && p_app_amount_display !== undefined - ? toKgPerHa( - p_app_amount_display, - fertilizer.p_app_amount_unit, - fertilizer.p_density, - ) - : null + const p_app_amount = toKgPerHa( + p_app_amount_display, + fertilizer.p_app_amount_unit, + fertilizer.p_density, + ) await fdm.insert(schema.fertilizerApplication).values({ p_app_id, @@ -830,7 +827,7 @@ export async function addFertilizerApplication( * @param principal_id - The ID of the principal performing the update. * @param p_app_id - The unique identifier of the fertilizer application record. * @param p_id - The unique identifier of the associated fertilizer. - * @param p_app_amount - The amount of fertilizer applied. + * @param p_app_amount_display - The amount of fertilizer applied in the display unit. * @param p_app_method - The method used for applying the fertilizer. * @param p_app_date - The date when the fertilizer was applied. * @@ -841,7 +838,7 @@ export async function updateFertilizerApplication( principal_id: PrincipalId, p_app_id: schema.fertilizerApplicationTypeInsert["p_app_id"], p_id: schema.fertilizerApplicationTypeInsert["p_id"], - p_app_amount_display: schema.fertilizerApplicationTypeInsert["p_app_amount"], + p_app_amount_display: number | undefined | null, p_app_method: schema.fertilizerApplicationTypeInsert["p_app_method"], p_app_date: schema.fertilizerApplicationTypeInsert["p_app_date"], ): Promise { @@ -862,7 +859,7 @@ export async function updateFertilizerApplication( fertilizer.p_app_amount_unit, fertilizer.p_density, ) - : p_app_amount_display + : undefined await fdm .update(schema.fertilizerApplication) .set({ p_id, p_app_amount, p_app_method, p_app_date }) @@ -915,34 +912,6 @@ export async function removeFertilizerApplication( } } -/** - * Extends the given fertilizer application with computed data and removes unknown properties - * @param app fertilizer application - * @returns the same fertilizer application with p_app_amount_display filled in and properties - * that do not belong to FertilizerApplication removed - */ -function extendFertilizerApplication( - app: T, - p_app_amount_unit: AppAmountUnit, - p_density: number | null, -): FertilizerApplication { - const maybe_p_app_amount_display = - app.p_app_amount !== null && app.p_app_amount !== undefined - ? fromKgPerHa(app.p_app_amount, p_app_amount_unit, p_density) - : app.p_app_amount - return { - p_id: app.p_id, - p_id_catalogue: app.p_id_catalogue, - p_name_nl: app.p_name_nl, - p_app_date: app.p_app_date, - p_app_method: app.p_app_method, - p_app_amount: app.p_app_amount, - p_app_amount_display: maybe_p_app_amount_display ?? null, - p_app_amount_unit: p_app_amount_unit, - p_app_id: app.p_app_id, - } -} - /** * Retrieves a fertilizer application record by its unique identifier. * @@ -1003,7 +972,7 @@ export async function getFertilizerApplication( eq(schema.fertilizerApplication.p_app_id, p_app_id), )) as (BaseFertilizerApplication & { p_app_amount_unit: AppAmountUnit - p_density: schema.fertilizersCatalogueTypeSelect["p_density"] + p_density: number | null })[] return result.length > 0 @@ -1654,3 +1623,33 @@ function deriveFertilizerType( } return null } + +/** + * Extends the given fertilizer application with computed data and removes unknown properties + * @param app fertilizer application + * @returns the same fertilizer application with p_app_amount_display filled in and properties + * that do not belong to FertilizerApplication removed + */ +function extendFertilizerApplication< + T extends BaseFertilizerApplication & { + p_app_amount_unit: AppAmountUnit + p_density: number | null + }, +>(app: T, p_app_amount_unit: AppAmountUnit, p_density: number | null): FertilizerApplication { + const p_app_amount_display = + app.p_app_amount !== null && app.p_app_amount !== undefined + ? fromKgPerHa(app.p_app_amount, p_app_amount_unit, p_density) + : app.p_app_amount + + return { + p_id: app.p_id, + p_id_catalogue: app.p_id_catalogue, + p_name_nl: app.p_name_nl, + p_app_amount: app.p_app_amount, + p_app_amount_unit: p_app_amount_unit, + p_app_amount_display: p_app_amount_display, + p_app_method: app.p_app_method, + p_app_date: app.p_app_date, + p_app_id: app.p_app_id, + } +} diff --git a/fdm-core/src/fertilizer.types.d.ts b/fdm-core/src/fertilizer.types.d.ts index bbd6ed29d..f4b4c6297 100644 --- a/fdm-core/src/fertilizer.types.d.ts +++ b/fdm-core/src/fertilizer.types.d.ts @@ -67,20 +67,22 @@ export interface Fertilizer extends FertilizerCatalogue { type FertilizerType = "manure" | "mineral" | "compost" -export interface BaseFertilizerApplication { +export interface FertilizerApplication { p_id: string p_id_catalogue: string p_name_nl: string | null p_app_amount: number | null + p_app_amount_unit: AppAmountUnit + p_app_amount_display: number | null p_app_method: ApplicationMethods | null p_app_date: Date p_app_id: string } -export interface FertilizerApplication extends BaseFertilizerApplication { - p_app_amount_unit: AppAmountUnit - p_app_amount_display: number | null -} +export type BaseFertilizerApplication = Omit< + FertilizerApplication, + "p_app_amount_display" | "p_app_amount_unit" +> export type FertilizerParameters = | "p_id_catalogue" From f8502b17d656b07f04ac35600c1c71be2017d459 Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:07:15 +0200 Subject: [PATCH 31/38] refactor: improve text --- fdm-core/src/fertilizer.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index 226068a6f..d1b614640 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -1293,11 +1293,11 @@ export function getFertilizerParametersDescription( { parameter: "p_app_amount_unit", unit: "", - name: "Hoeveelheidsunit", + name: "Voorkeurseenheid", type: "enum", category: "general", description: - "Unit van voorkeur waarin de applicatiebedragen worden weergegeven.", + "Eenheid voor het weergeven van de hoeveelheid van deze meststof", options: APP_AMOUNT_UNITS, }, { @@ -1635,7 +1635,11 @@ function extendFertilizerApplication< p_app_amount_unit: AppAmountUnit p_density: number | null }, ->(app: T, p_app_amount_unit: AppAmountUnit, p_density: number | null): FertilizerApplication { +>( + app: T, + p_app_amount_unit: AppAmountUnit, + p_density: number | null, +): FertilizerApplication { const p_app_amount_display = app.p_app_amount !== null && app.p_app_amount !== undefined ? fromKgPerHa(app.p_app_amount, p_app_amount_unit, p_density) @@ -1650,6 +1654,6 @@ function extendFertilizerApplication< p_app_amount_display: p_app_amount_display, p_app_method: app.p_app_method, p_app_date: app.p_app_date, - p_app_id: app.p_app_id, + p_app_id: app.p_app_id, } } From deb98d2fec8792eb0349a43f2ac933a6e9893457 Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:34:50 +0200 Subject: [PATCH 32/38] refactor: make sure gerrit uses display units --- fdm-agents/src/agents/gerrit/agent.ts | 34 +++++++++++++++---- .../src/tools/fertilizer-planner/index.ts | 14 ++++++++ .../farm.$b_id_farm.$calendar.gerrit.tsx | 2 +- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/fdm-agents/src/agents/gerrit/agent.ts b/fdm-agents/src/agents/gerrit/agent.ts index 75728cefa..c4566ced9 100644 --- a/fdm-agents/src/agents/gerrit/agent.ts +++ b/fdm-agents/src/agents/gerrit/agent.ts @@ -40,9 +40,10 @@ IMPORTANT CONSTRAINTS: 6. APPLICATION METHOD: For each application, you must propose a valid "p_app_method". Choose ONLY from the "p_app_method_options" returned by the search tool for that specific fertilizer. 7. REALISTIC DATES: Ensure all "p_app_date" values are realistic for the crop type, cultivation season, and Dutch climate. Use the provided "b_lu_start" (sowing/start date) as a critical reference point for each crop. 8. REALISTIC APPLICATION AMOUNTS: Ensure the proposed "p_app_amount" per application matches the technical capabilities of common farming equipment. If the total advice requires more, you MUST split it into multiple applications on different dates. - - slurry (drijfmest): 15,000 - 30,000 kg/ha per application (15-30 m³/ha). - - Solid manure / compost (vaste mest): 10,000 - 30,000 kg/ha per application (10-30 t/ha). + - slurry (drijfmest): 15-30 m³/ha per application. + - Solid manure / compost (vaste mest): 10-30 t/ha per application. - Mineral fertilizers: 50 - 450 kg/ha per application. + - Liquid mineral fertilizers (oplossing): 10 - 1000 l/ha per application. 9. PRIORITIZATION: If legal norms (especially Nitrogen or Phosphate) limit the total nutrient space on the farm, prioritize fulfilling the nutrient advice for high-value crops (e.g., potatoes, onions, sugar beets, vegetables) over lower-value crops or grasslands. Strategy should focus on maximizing the economic return of the limited nutrient space. 10. ORGANIC FARMING: If "Organic Farming" is YES, you MUST NOT use any mineral fertilizers ("p_type": "mineral") in the plan. 11. MANURE FILLING STRATEGY: @@ -83,7 +84,14 @@ Your final response MUST be a JSON object with exactly this structure (all field { "b_id": "string", "applications": [ - { "p_id_catalogue": "string", "p_app_amount": number, "p_app_date": "YYYY-MM-DD", "p_app_method": "string" } + { + "p_id_catalogue": "string", + "p_app_amount": number, + "p_app_amount_display": number, + "p_app_amount_unit": "kg/ha" | "l/ha" | "t/ha" | "m3/ha", + "p_app_date": "YYYY-MM-DD", + "p_app_method": "string" + } ], "fieldMetrics": { "advice": { @@ -137,15 +145,27 @@ CALCULATOR REFERENCE (units and semantics for the simulation tool): - "omBalance" (organische stofbalans): net organic matter balance, kg EOM/ha. Positive = good. Aim for ≥ 0. - "nBalance": nitrogen balance structured exactly as fdm-calculator outputs. "nBalance.balance" and "nBalance.target" are in kg N/ha. "nBalance.emission.ammonia.total" and "nBalance.emission.nitrate.total" are also in kg N/ha. The farm-level averages are automatically area-weighted by the simulation tool. nBalance.balance must be ≤ nBalance.target if keepNitrogenBalanceBelowTarget is YES. - "p_app_amount": application amount — **always in kg/ha, regardless of fertilizer type**. - - Liquid manure / digestate / slurry: convert m³/ha → kg/ha using 1 m³ = 1000 kg. Round to nearest 1000. Example: 18 m³/ha = 18000 kg/ha. - - Solid manure / compost: convert t/ha → kg/ha using 1 t = 1000 kg. Round to nearest 1000. Example: 20 t/ha = 20000 kg/ha. - - Mineral fertilizers: already in kg/ha, round to nearest 5 or 10. Example: 200 kg/ha KAS. + - Propose a round number for the native display unit (e.g., 25 m³/ha, 20 t/ha, 300 l/ha, 200 kg/ha) and then convert to kg/ha for the tools. + - Liquid manure / digestate / slurry: convert m³/ha → kg/ha using: 1 m³/ha = 1000 * density (kg/l). + Example: 25 m³/ha with density 1.005 kg/l = 25 * 1000 * 1.005 = 25125 kg/ha. + - Solid manure / compost: convert t/ha → kg/ha using: 1 t/ha = 1000 kg/ha. + Example: 20 t/ha = 20000 kg/ha. + - Liquid mineral fertilizers (e.g. Ammoniumnitraatureanoplossing): convert l/ha → kg/ha using: 1 l/ha = density (kg/l). + Example: 300 l/ha with density 1.2 kg/l = 300 * 1.2 = 360 kg/ha. + - Solid mineral fertilizers: already in kg/ha, round to nearest 5 or 10. + Example: 200 kg/ha KAS. +- "p_app_amount_display": application amount formatted with its native unit — **use this for the user-facing plan and summary**. + - Use the "p_app_amount_unit" and "p_density" (if needed for volume-to-mass conversion) from the search tool results to determine the correct unit. + - Liquid manure / slurry: e.g., "18 m³/ha". + - Solid manure / compost: e.g., "20 t/ha". + - Mineral fertilizers: e.g., "200 kg/ha". + - Liquid mineral fertilizers: e.g., "300 l/ha". - "p_ef_nh3": ammonia emission factor (fraction of N applied lost as NH3). Lower = less emission. TOOL RETURN SHAPES: - "getFarmFields" returns { fields: [...] } — access the array via result.fields. Each field includes main cultivation details (b_lu_catalogue, b_lu_name, b_lu_start). - "getFarmNutrientAdvice" returns { advicePerField: [...] } — access via result.advicePerField - "getFarmLegalNorms" returns { normsPerField: [...] } — access via result.normsPerField -- "searchFertilizers" returns { fertilizers: [...] } — access via result.fertilizers +- "searchFertilizers" returns { fertilizers: [...] } — access via result.fertilizers. Each fertilizer includes "p_app_amount_unit" and "p_density". - "simulateFarmPlan" returns { fieldResults: [...], farmTotals: {...}, isValid: bool, complianceIssues: [...], agronomicWarnings: [...] }. Each entry in "fieldResults" has: { b_id, b_area, isValid, fieldMetrics: { normsFilling: { manure, nitrogen, phosphate }, norms: { manure, nitrogen, phosphate }, proposedDose: { p_dose_n, p_dose_nw, p_dose_p, p_dose_k, p_dose_s, p_dose_mg, p_dose_ca, p_dose_na, p_dose_cu, p_dose_zn, p_dose_b, p_dose_mn, p_dose_mo, p_dose_co }, omBalance, nBalance, advice } }. Use "proposedDose.p_dose_nw" (werkzame stikstof, kg/ha) to compare against "advice.d_n_req" — this is the agronomically correct workable-N value. "proposedDose.p_dose_n" is total N and is provided for reference only. diff --git a/fdm-agents/src/tools/fertilizer-planner/index.ts b/fdm-agents/src/tools/fertilizer-planner/index.ts index 3b1cd2cc2..9c4d26cc1 100644 --- a/fdm-agents/src/tools/fertilizer-planner/index.ts +++ b/fdm-agents/src/tools/fertilizer-planner/index.ts @@ -321,6 +321,8 @@ export function createFertilizerPlannerTools(fdm: FdmType) { p_eom: f.p_eom, p_ef_nh3: f.p_ef_nh3, p_source: f.p_source, + p_app_amount_unit: f.p_app_amount_unit, + p_density: f.p_density, })), } }, @@ -371,6 +373,18 @@ export function createFertilizerPlannerTools(fdm: FdmType) { p_app_amount: z .number() .describe("Application amount in kg/ha"), + p_app_amount_unit: z + .string() + .optional() + .describe( + "The unit of the application amount (e.g., m3/ha, kg/ha, l/ha, t/ha)", + ), + p_app_amount_display: z + .string() + .optional() + .describe( + "Application amount with unit (e.g., 18 m3/ha, 200 kg/ha)", + ), p_app_date: z .string() .describe( diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx index 16358d5f2..3ee61c9ef 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx @@ -892,7 +892,7 @@ export async function action({ request, params }: ActionFunctionArgs) { session.principal_id, field.b_id, fertilizer.p_id, - amount.toNumber(), + amount, app.p_app_method, new Date(app.p_app_date), ) From f70fff3ee58ce1c0d9074dc7bb34a188b536f5df Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:58:34 +0200 Subject: [PATCH 33/38] fix: enable to update the unit of fertilzier --- fdm-core/src/fertilizer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index d1b614640..78fea848c 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -466,6 +466,7 @@ export async function updateFertilizerFromCatalogue( p_name_en: schema.fertilizersCatalogueTypeInsert["p_name_en"] p_description: schema.fertilizersCatalogueTypeInsert["p_description"] p_app_method_options: schema.fertilizersCatalogueTypeInsert["p_app_method_options"] + p_app_amount_unit: schema.fertilizersCatalogueTypeInsert["p_app_amount_unit"] p_dm: schema.fertilizersCatalogueTypeInsert["p_dm"] p_density: schema.fertilizersCatalogueTypeInsert["p_density"] p_om: schema.fertilizersCatalogueTypeInsert["p_om"] From 5fae90ed252170f9618484e39623097d52759590 Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 17 Apr 2026 16:00:19 +0200 Subject: [PATCH 34/38] fix: zod for p_amount_app_display --- fdm-agents/src/tools/fertilizer-planner/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdm-agents/src/tools/fertilizer-planner/index.ts b/fdm-agents/src/tools/fertilizer-planner/index.ts index 9c4d26cc1..cd120e4d4 100644 --- a/fdm-agents/src/tools/fertilizer-planner/index.ts +++ b/fdm-agents/src/tools/fertilizer-planner/index.ts @@ -380,10 +380,10 @@ export function createFertilizerPlannerTools(fdm: FdmType) { "The unit of the application amount (e.g., m3/ha, kg/ha, l/ha, t/ha)", ), p_app_amount_display: z - .string() + .number() .optional() .describe( - "Application amount with unit (e.g., 18 m3/ha, 200 kg/ha)", + "The numeric application amount (unit is carried separately in p_app_amount_unit)", ), p_app_date: z .string() From fe7a0ec96b6e83186474b5fd2dbed3a91f1ca36a Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 17 Apr 2026 16:04:22 +0200 Subject: [PATCH 35/38] tests: remove not used tests --- fdm-core/src/catalogues.test.ts | 70 --------------------------------- 1 file changed, 70 deletions(-) diff --git a/fdm-core/src/catalogues.test.ts b/fdm-core/src/catalogues.test.ts index fb4515d95..220029f6f 100644 --- a/fdm-core/src/catalogues.test.ts +++ b/fdm-core/src/catalogues.test.ts @@ -711,76 +711,6 @@ describe("Catalogues syncing", () => { expect(syncedItem[0].p_name_nl).not.toBe("Updated Name") }) - it("should use default for p_app_amount_unit if p_type_rvo is not specified", async () => { - const p_id_catalogue = createId() - - await syncFertilizerCatalogueArray(fdm, [ - { - p_id_catalogue: p_id_catalogue, - p_source: "baat", - p_name_nl: "Custom Fertilizer", - }, - ]) - - const syncedItem = await fdm - .select() - .from(schema.fertilizersCatalogue) - .where( - eq(schema.fertilizersCatalogue.p_id_catalogue, p_id_catalogue), - ) - .limit(1) - - expect(syncedItem[0].p_app_amount_unit).toBe("kg/ha") - }) - - it("should use derived p_app_amount_unit if p_type_rvo is specified", async () => { - const p_id_catalogue = createId() - - await syncFertilizerCatalogueArray(fdm, [ - { - p_id_catalogue: p_id_catalogue, - p_source: "baat", - p_name_nl: "Custom Fertilizer", - p_type_rvo: "10", - }, - ]) - - const syncedItem = await fdm - .select() - .from(schema.fertilizersCatalogue) - .where( - eq(schema.fertilizersCatalogue.p_id_catalogue, p_id_catalogue), - ) - .limit(1) - - expect(syncedItem[0].p_app_amount_unit).toBe("ton/ha") - }) - - it("should use any specified p_type_rvo", async () => { - const p_id_catalogue = createId() - - await syncFertilizerCatalogueArray(fdm, [ - { - p_id_catalogue: p_id_catalogue, - p_source: "baat", - p_name_nl: "Suspicious Liquid Fertilizer", - p_type_rvo: "10", // Assumed to be solid so its unit would be estimated to be ton/ha - p_app_amount_unit: "l/ha", - p_density: 1.1, - }, - ]) - - const syncedItem = await fdm - .select() - .from(schema.fertilizersCatalogue) - .where( - eq(schema.fertilizersCatalogue.p_id_catalogue, p_id_catalogue), - ) - .limit(1) - - expect(syncedItem[0].p_app_amount_unit).toBe("l/ha") - }) - it("should update cultivation catalogue", async () => { await syncCatalogues(fdm) From 2ba6be31381c73a7fc16adb27d13c36b5609935e Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 17 Apr 2026 16:07:59 +0200 Subject: [PATCH 36/38] tests: fix --- fdm-core/src/fertilizer.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fdm-core/src/fertilizer.test.ts b/fdm-core/src/fertilizer.test.ts index 5dfe46833..c9707fd95 100644 --- a/fdm-core/src/fertilizer.test.ts +++ b/fdm-core/src/fertilizer.test.ts @@ -1241,7 +1241,7 @@ describe("Fertilizer Data Model", () => { expect(fertilizerApplication?.p_app_date).toEqual(p_app_date) }) - it("should add a new fertilizer application with no amount specified", async () => { + it("should add a new fertilizer application with amount specified", async () => { const p_app_date = new Date("2024-03-15") const new_p_app_id = await addFertilizerApplication( @@ -1249,7 +1249,7 @@ describe("Fertilizer Data Model", () => { principal_id, b_id, p_id_liquid, - 0, + 120, "broadcasting", p_app_date, ) @@ -1288,7 +1288,7 @@ describe("Fertilizer Data Model", () => { ) expect(fertilizerApplication).toBeDefined() expect(fertilizerApplication?.p_id).toBe(p_id_liquid) - expect(fertilizerApplication?.p_app_amount).toBe(null) + expect(fertilizerApplication?.p_app_amount).toBe(0) expect(fertilizerApplication?.p_app_amount_display).toBe(null) expect(fertilizerApplication?.p_app_method).toBe("broadcasting") expect(fertilizerApplication?.p_app_date).toEqual(p_app_date) From 60eac40ddafa54b52b35ca51be31098846d680fd Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 17 Apr 2026 16:10:07 +0200 Subject: [PATCH 37/38] fix: type issue --- fdm-core/src/catalogues.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fdm-core/src/catalogues.test.ts b/fdm-core/src/catalogues.test.ts index 220029f6f..5eb5d47d0 100644 --- a/fdm-core/src/catalogues.test.ts +++ b/fdm-core/src/catalogues.test.ts @@ -14,13 +14,11 @@ import { isCultivationCatalogueEnabled, isFertilizerCatalogueEnabled, syncCatalogues, - syncFertilizerCatalogueArray, } from "./catalogues" import * as schema from "./db/schema" import { addFarm } from "./farm" import type { FdmType } from "./fdm.types" -import { createFdmServer } from "./fdm-server" -import { createId } from "./id" +import { createFdmServer } from "./fdm-server"" describe("Catalogues", () => { let fdm: FdmType From 16e9f40086e419186f313d916018989db9286c03 Mon Sep 17 00:00:00 2001 From: SvenVw <37927107+SvenVw@users.noreply.github.com> Date: Fri, 17 Apr 2026 16:28:51 +0200 Subject: [PATCH 38/38] tests: fixes --- fdm-core/src/catalogues.test.ts | 2 +- fdm-core/src/fertilizer.test.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/fdm-core/src/catalogues.test.ts b/fdm-core/src/catalogues.test.ts index 5eb5d47d0..02a021b6f 100644 --- a/fdm-core/src/catalogues.test.ts +++ b/fdm-core/src/catalogues.test.ts @@ -18,7 +18,7 @@ import { import * as schema from "./db/schema" import { addFarm } from "./farm" import type { FdmType } from "./fdm.types" -import { createFdmServer } from "./fdm-server"" +import { createFdmServer } from "./fdm-server" describe("Catalogues", () => { let fdm: FdmType diff --git a/fdm-core/src/fertilizer.test.ts b/fdm-core/src/fertilizer.test.ts index c9707fd95..d580fbea6 100644 --- a/fdm-core/src/fertilizer.test.ts +++ b/fdm-core/src/fertilizer.test.ts @@ -1222,7 +1222,7 @@ describe("Fertilizer Data Model", () => { fdm, principal_id, b_id, - p_id_liquid, + p_id, 0, "broadcasting", p_app_date, @@ -1236,7 +1236,7 @@ describe("Fertilizer Data Model", () => { ) expect(fertilizerApplication).toBeDefined() expect(fertilizerApplication?.p_id).toBe(p_id) - expect(fertilizerApplication?.p_app_amount).toBe(100) + expect(fertilizerApplication?.p_app_amount).toBe(0) expect(fertilizerApplication?.p_app_method).toBe("broadcasting") expect(fertilizerApplication?.p_app_date).toEqual(p_app_date) }) @@ -1262,7 +1262,7 @@ describe("Fertilizer Data Model", () => { ) expect(fertilizerApplication).toBeDefined() expect(fertilizerApplication?.p_id).toBe(p_id_liquid) - expect(fertilizerApplication?.p_app_amount).toBe(120) + expect(fertilizerApplication?.p_app_amount).toBe(144) expect(fertilizerApplication?.p_app_method).toBe("broadcasting") expect(fertilizerApplication?.p_app_date).toEqual(p_app_date) }) @@ -1289,7 +1289,7 @@ describe("Fertilizer Data Model", () => { expect(fertilizerApplication).toBeDefined() expect(fertilizerApplication?.p_id).toBe(p_id_liquid) expect(fertilizerApplication?.p_app_amount).toBe(0) - expect(fertilizerApplication?.p_app_amount_display).toBe(null) + expect(fertilizerApplication?.p_app_amount_display).toBe(0) expect(fertilizerApplication?.p_app_method).toBe("broadcasting") expect(fertilizerApplication?.p_app_date).toEqual(p_app_date) })