Skip to content

Implement nitrogen reduction for grassland renewal and destruction#415

Merged
SvenVw merged 7 commits into
developmentfrom
FDM412
Jan 15, 2026
Merged

Implement nitrogen reduction for grassland renewal and destruction#415
SvenVw merged 7 commits into
developmentfrom
FDM412

Conversation

@SvenVw
Copy link
Copy Markdown
Collaborator

@SvenVw SvenVw commented Jan 12, 2026

Summary by CodeRabbit

  • New Features

    • Implemented grassland renewal (50 kg N/ha) and destruction (65 kg N/ha) reductions with region- and year-specific date windows, NV-area and derogation rules; 2026 limited to sandy/loess regions and includes expanded catch‑crop/sowing-date handling.
  • Documentation

    • Updated nitrogen usage norms to describe renewal/destruction reductions, soil-type windows, derogation/NV considerations, and localized Dutch error messages for invalid dates.
  • Tests

    • Added extensive 2025/2026 test coverage across regions, dates, crops, and edge cases.

✏️ Tip: You can customize this high-level summary in your review settings.

Closes #412

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jan 12, 2026

🦋 Changeset detected

Latest commit: 9bf9265

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@svenvw/fdm-docs Patch
@svenvw/fdm-calculator Minor
@svenvw/fdm-app Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@SvenVw SvenVw self-assigned this Jan 12, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 12, 2026

Walkthrough

Implements grassland renewal (50 kg N/ha) and destruction (65 kg N/ha) korting for NL 2025 and 2026, adding region, derogation and NV-area-aware date windows, throwing localized Dutch errors for invalid dates, expanding unit tests, and updating documentation and changesets.

Changes

Cohort / File(s) Summary
Changesets
\.changeset/nitrogen-korting-docs.md, \.changeset/nitrogen-korting-grassland.md
Adds changeset entries for docs and the calculator version bump describing the grassland korting additions.
2025 Implementation
fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.ts
Extends calculateKorting signature to accept is_derogatie_bedrijf and is_nv_area; adds region (sand/loess vs clay/peat) and status-aware date windows; sorts cultivations, detects Gras‑na‑Gras (50 kg) and Gras‑naar‑Bouwland (65 kg), and throws Dutch error messages for invalid windows.
2025 Tests
fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.test.ts
Adds extensive tests covering sand vs clay centroids, renewal/destruction scenarios, NV/derogation interactions, invalid-date errors, centroid mocks, and normSource expectations.
2026 Implementation
fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.ts
Restricts 2026 korting to Sand/Loess regions; sorts cultivations, detects transitions, applies renewal (50 kg) and destruction (65 kg) rules with explicit windows; enforces catch-crop exclusions, vanggewas/sowing-date handling, and tightens derogation/maize behavior.
2026 Tests
fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.test.ts
Adds tests for 2026 region eligibility, renewal/destruction timing, potato-type filtering (exclude pootaardappelen), catch-crop exclusions, boundary cases, and centroid-driven scenarios.
Documentation
fdm-docs/docs/insights/fertilizer-application-norms/nl/2025/stikstofgebruiksnorm.md
Adds "Grassland Renewal and Destruction Reductions" section (50/65 kg), describes soil-type windows, derogation/NV rules, updated implementation notes, and explains error behaviour for out-of-window actions.

Sequence Diagram(s)

(Skipped — changes are internal library logic and tests; no multi-component runtime sequence added that requires visualization.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • gerardhros
  • BoraIneviNMI

Poem

🐰 I hopped through code and meadow lines,
Dates and soils and tidy signs.
Fifty for renewal, sixty‑five for break,
I counted norms for every stake.
Hooray—korting lands, I wiggle my whiskers, cake!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main objective: implementing nitrogen reduction logic for grassland renewal and destruction across 2025 and 2026.
Linked Issues check ✅ Passed The PR successfully implements all core requirements from issue #412: grassland renewal (50 kg N/ha) and destruction (65 kg N/ha) logic for both 2025 and 2026 with proper soil-region mapping, potato-type validation, catch-crop exclusion, descriptive error handling, and comprehensive unit test coverage.
Out of Scope Changes check ✅ Passed All changes directly support the linked issue requirements: implementation files (stikstofgebruiksnorm.ts for both years), comprehensive test coverage, documentation updates, and changesets. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch FDM412

🧹 Recent nitpick comments
fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.ts (1)

367-371: Consider moving null-dated cultivations to the end of the sorted array.

When b_lu_start is null, returning 0 leaves items in their original relative position, which could yield non-deterministic results. Pushing null-dated entries to the end provides more predictable behavior.

♻️ Suggested improvement
     const sortedCultivations = [...cultivations].sort((a, b) => {
-        if (!a.b_lu_start || !b.b_lu_start) return 0
+        if (!a.b_lu_start && !b.b_lu_start) return 0
+        if (!a.b_lu_start) return 1  // Push nulls to end
+        if (!b.b_lu_start) return -1
         return a.b_lu_start.getTime() - b.b_lu_start.getTime()
     })
fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.ts (1)

455-459: Same sorting consideration as 2026 module.

Consider the same null-date handling improvement for consistency.

♻️ Suggested improvement
     const sortedCultivations = [...cultivations].sort((a, b) => {
-        if (!a.b_lu_start || !b.b_lu_start) return 0
+        if (!a.b_lu_start && !b.b_lu_start) return 0
+        if (!a.b_lu_start) return 1
+        if (!b.b_lu_start) return -1
         return a.b_lu_start.getTime() - b.b_lu_start.getTime()
     })

📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 70207e9 and 9bf9265.

📒 Files selected for processing (2)
  • fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.ts
  • fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.ts
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 156
File: fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts:295-303
Timestamp: 2025-07-21T12:06:07.070Z
Learning: Functions in the fdm-calculator with "NL2025" in their names are specifically designed for Netherlands 2025 agricultural norms calculation and hardcoded 2025 dates are appropriate in this context, as different years would have separate calculation modules.
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 285
File: fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.ts:6-9
Timestamp: 2025-10-22T08:09:17.727Z
Learning: In fdm-calculator for NL 2025 phosphate stimulus ("Stimuleren organische stofrijke meststoffen"), the correct RVO mestcode groupings are: 25% discount group includes ["111", "112"] (Compost and Zeer schone compost), and the 75% discount group for organic farms includes ["40"] (Varkens - Vaste mest). The 75% base group includes ["110", "10", "61", "25", "56"].
📚 Learning: 2025-07-21T12:06:07.070Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 156
File: fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts:295-303
Timestamp: 2025-07-21T12:06:07.070Z
Learning: Functions in the fdm-calculator with "NL2025" in their names are specifically designed for Netherlands 2025 agricultural norms calculation and hardcoded 2025 dates are appropriate in this context, as different years would have separate calculation modules.

Applied to files:

  • fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.ts
  • fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.ts
📚 Learning: 2025-08-11T11:55:26.053Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 233
File: fdm-app/app/integrations/nmi.ts:54-0
Timestamp: 2025-08-11T11:55:26.053Z
Learning: The NMI API Estimates endpoint (`https://api.nmi-agro.nl/estimates`) always returns the fields `b_gwl_ghg`, `b_gwl_glg`, and `cultivations` according to its specification. These fields should be kept as required (not optional) in the TypeScript return type and Zod validation schema in `fdm-app/app/integrations/nmi.ts`.

Applied to files:

  • fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.ts
📚 Learning: 2025-10-22T08:09:17.727Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 285
File: fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.ts:6-9
Timestamp: 2025-10-22T08:09:17.727Z
Learning: In fdm-calculator for NL 2025 phosphate stimulus ("Stimuleren organische stofrijke meststoffen"), the correct RVO mestcode groupings are: 25% discount group includes ["111", "112"] (Compost and Zeer schone compost), and the 75% discount group for organic farms includes ["40"] (Varkens - Vaste mest). The 75% base group includes ["110", "10", "61", "25", "56"].

Applied to files:

  • fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.ts
  • fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.ts
🔇 Additional comments (7)
fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.ts (3)

232-236: LGTM!

Clear handling of the 2026 derogation rule change with appropriate documentation.


393-411: LGTM!

Grassland renewal logic correctly implements the 2026 Sand/Loess-only rule with the June 1 - August 31 window and appropriate Dutch error messaging.


413-453: LGTM!

Grassland destruction logic correctly implements:

  • Seed potato exclusion via multiple detection methods
  • Catch crop exclusion with both explicit is_vanggewas check and sowing-date inference
  • 2026 Sand/Loess date window (Feb 1 - May 10)
fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.ts (4)

443-448: LGTM!

Function signature appropriately expanded to support the 2025 derogation and NV-area dependent date windows.


475-543: LGTM!

Comprehensive grassland renewal logic covering all 2025 region/derogation/NV combinations with appropriate date windows and descriptive error messages.


545-608: LGTM!

Grassland destruction logic correctly implements all 2025 region/NV combinations. The absence of catch-crop exclusion (compared to 2026) correctly reflects the year-specific rules per the objectives.


912-914: LGTM!

Call site correctly updated to pass the new is_derogatie_bedrijf and is_nv_area parameters.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 12, 2026

Codecov Report

❌ Patch coverage is 96.82540% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.04%. Comparing base (d74898d) to head (9bf9265).
⚠️ Report is 8 commits behind head on development.

Files with missing lines Patch % Lines
...or/src/norms/nl/2025/value/stikstofgebruiksnorm.ts 95.23% 4 Missing ⚠️
Additional details and impacted files
@@               Coverage Diff               @@
##           development     #415      +/-   ##
===============================================
+ Coverage        87.72%   88.04%   +0.31%     
===============================================
  Files               91       91              
  Lines             4466     4591     +125     
  Branches          1394     1468      +74     
===============================================
+ Hits              3918     4042     +124     
- Misses             548      549       +1     
Flag Coverage Δ
fdm-calculator 88.67% <96.82%> (+0.54%) ⬆️
fdm-core 86.81% <ø> (ø)
fdm-data 92.12% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot changed the title @coderabbitai Implement nitrogen reduction for grassland renewal and destruction Jan 12, 2026
@coderabbitai coderabbitai Bot added branch:development Issue only affecting development, not the main branch (yet) enhancement New feature or request fdm-calculator fdm-docs labels Jan 12, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.ts (1)

276-280: Sorting with missing dates returns 0, which may lead to inconsistent ordering.

When b_lu_start is null/undefined, the comparison returns 0, resulting in unstable sort order for those items. While the subsequent loop skips entries without b_lu_end, consider filtering out incomplete cultivations before sorting for clarity.

♻️ Optional: Filter before sorting
-    // Sort cultivations by start date
-    const sortedCultivations = [...cultivations].sort((a, b) => {
-        if (!a.b_lu_start || !b.b_lu_start) return 0
-        return a.b_lu_start.getTime() - b.b_lu_start.getTime()
-    })
+    // Sort cultivations by start date, filtering out incomplete records
+    const sortedCultivations = [...cultivations]
+        .filter((c) => c.b_lu_start && c.b_lu_end)
+        .sort((a, b) => a.b_lu_start!.getTime() - b.b_lu_start!.getTime())
fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.test.ts (1)

655-773: Good coverage, consider adding a test for consumption/factory potato destruction.

The tests cover key scenarios well:

  • Valid maize transition discount
  • Catch crop exclusion
  • Seed potato exclusion
  • Invalid date error handling

However, there's no test verifying the 65 kg discount applies when transitioning grass to consumption or factory potatoes (the valid potato case). The 2025 test file includes this scenario (lines 728-758).

Consider adding a test case similar to this:

it("should apply 65 discount on Sand (Consumption Potato, Feb 1 - May 10)", async () => {
    const mockInput: NL2026NormsInput = {
        farm: { has_grazing_intention: false },
        field: {
            b_id: "1",
            b_centroid: sandCentroid,
        } as Field,
        cultivations: [
            {
                b_lu_catalogue: "nl_265", // Grass
                b_lu_start: new Date(2026, 0, 1),
                b_lu_end: new Date(2026, 2, 15), // March 15
            },
            {
                b_lu_catalogue: "nl_2014", // Consumption Potato
                b_lu_variety: "Agria",
                b_lu_start: new Date(2026, 2, 16),
                b_lu_end: new Date(2026, 9, 1),
            },
        ] as NL2026NormsInputForCultivation[],
        soilAnalysis: { a_p_al: 20, a_p_cc: 0.9 },
    }

    const result = await calculateNL2026StikstofGebruiksNorm(mockInput)
    expect(result.normSource).toContain("Korting: 65kg N/ha: graslandvernietiging")
})
fdm-docs/docs/insights/fertilizer-application-norms/nl/2025/stikstofgebruiksnorm.md (1)

70-86: Markdown list indentation inconsistency flagged by linter.

Static analysis (markdownlint MD007) indicates nested lists use 4-space indentation instead of the expected 2-space indentation. If your project enforces this rule, consider adjusting the indentation.

🔧 Suggested fix for list indentation
 * **Sand and Loess Soils**: June 1st – August 31st.
 * **Clay and Peat Soils**:
-    * **Derogation Farm + NV-Area**: June 1st – August 31st.
-    * **Derogation Farm + Non-NV-Area**: June 1st – September 15th.
-    * **Non-Derogation Farm**: February 1st – September 15th.
+  * **Derogation Farm + NV-Area**: June 1st – August 31st.
+  * **Derogation Farm + Non-NV-Area**: June 1st – September 15th.
+  * **Non-Derogation Farm**: February 1st – September 15th.
 * **Allowed Periods**:
-    * **Sand and Loess Soils**: February 1st – May 10th.
-    * **Clay and Peat Soils**:
-        * **NV-Area**: February 1st – March 15th.
-        * **Non-NV-Area**: February 1st – May 31st.
+  * **Sand and Loess Soils**: February 1st – May 10th.
+  * **Clay and Peat Soils**:
+    * **NV-Area**: February 1st – March 15th.
+    * **Non-NV-Area**: February 1st – May 31st.
fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.ts (1)

365-368: Sort comparator could produce unstable results with undefined dates.

When b_lu_start is undefined for either cultivation, returning 0 treats them as equal, which can lead to non-deterministic ordering. Consider moving cultivations with undefined dates to the end or filtering them out before sorting.

♻️ Suggested improvement
     const sortedCultivations = [...cultivations].sort((a, b) => {
-        if (!a.b_lu_start || !b.b_lu_start) return 0
-        return a.b_lu_start.getTime() - b.b_lu_start.getTime()
+        if (!a.b_lu_start && !b.b_lu_start) return 0
+        if (!a.b_lu_start) return 1  // Push undefined to end
+        if (!b.b_lu_start) return -1
+        return a.b_lu_start.getTime() - b.b_lu_start.getTime()
     })
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d2c6b0 and 3675792.

📒 Files selected for processing (7)
  • .changeset/nitrogen-korting-docs.md
  • .changeset/nitrogen-korting-grassland.md
  • fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.test.ts
  • fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.ts
  • fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.test.ts
  • fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.ts
  • fdm-docs/docs/insights/fertilizer-application-norms/nl/2025/stikstofgebruiksnorm.md
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 156
File: fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts:295-303
Timestamp: 2025-07-21T12:06:07.070Z
Learning: Functions in the fdm-calculator with "NL2025" in their names are specifically designed for Netherlands 2025 agricultural norms calculation and hardcoded 2025 dates are appropriate in this context, as different years would have separate calculation modules.
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 88
File: fdm-calculator/src/doses/calculate-dose.ts:18-18
Timestamp: 2025-03-04T10:56:35.540Z
Learning: In the FDM calculator, fertilizer nutrient rates (p_n_rt, p_p_rt, p_k_rt) are measured in g/kg, and are converted to fractions by dividing by 10 during dose calculations.
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 285
File: fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.ts:6-9
Timestamp: 2025-10-22T08:09:17.727Z
Learning: In fdm-calculator for NL 2025 phosphate stimulus ("Stimuleren organische stofrijke meststoffen"), the correct RVO mestcode groupings are: 25% discount group includes ["111", "112"] (Compost and Zeer schone compost), and the 75% discount group for organic farms includes ["40"] (Varkens - Vaste mest). The 75% base group includes ["110", "10", "61", "25", "56"].
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 343
File: fdm-calculator/src/balance/organic-matter/types.d.ts:12-132
Timestamp: 2025-11-21T10:02:25.556Z
Learning: In the organic matter balance calculation system (fdm-calculator), degradation values are negative by definition. This means the balance calculation using supply.total.plus(degradation.total) is correct, as it effectively computes supply - |degradation|. This follows the same pattern as the nitrogen balance system.
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 236
File: fdm-calculator/src/balance/nitrogen/index.ts:173-0
Timestamp: 2025-08-14T14:31:55.384Z
Learning: In nitrogen balance calculations for agricultural systems, the balance should only include ammonia emissions (emission.ammonia.total) and should not include nitrate leaching from the emission calculation. The nitrate component (emission.nitrate) should be excluded from the balance formula.
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 134
File: fdm-calculator/src/balance/nitrogen/index.ts:162-168
Timestamp: 2025-05-26T10:32:00.674Z
Learning: In the nitrogen balance calculation system (fdm-calculator), removal and volatilization values are negative by definition. This means the balance calculation using supply.total.add(removal.total).add(volatilization.total) is correct, as it effectively computes supply - |removal| - |volatilization|.
📚 Learning: 2025-07-21T12:06:07.070Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 156
File: fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts:295-303
Timestamp: 2025-07-21T12:06:07.070Z
Learning: Functions in the fdm-calculator with "NL2025" in their names are specifically designed for Netherlands 2025 agricultural norms calculation and hardcoded 2025 dates are appropriate in this context, as different years would have separate calculation modules.

Applied to files:

  • fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.test.ts
  • fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.test.ts
  • fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.ts
  • fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.ts
  • .changeset/nitrogen-korting-grassland.md
  • fdm-docs/docs/insights/fertilizer-application-norms/nl/2025/stikstofgebruiksnorm.md
📚 Learning: 2025-10-22T08:09:17.727Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 285
File: fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.ts:6-9
Timestamp: 2025-10-22T08:09:17.727Z
Learning: In fdm-calculator for NL 2025 phosphate stimulus ("Stimuleren organische stofrijke meststoffen"), the correct RVO mestcode groupings are: 25% discount group includes ["111", "112"] (Compost and Zeer schone compost), and the 75% discount group for organic farms includes ["40"] (Varkens - Vaste mest). The 75% base group includes ["110", "10", "61", "25", "56"].

Applied to files:

  • fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.test.ts
  • fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.ts
  • .changeset/nitrogen-korting-grassland.md
  • fdm-docs/docs/insights/fertilizer-application-norms/nl/2025/stikstofgebruiksnorm.md
📚 Learning: 2025-08-11T11:55:26.053Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 233
File: fdm-app/app/integrations/nmi.ts:54-0
Timestamp: 2025-08-11T11:55:26.053Z
Learning: The NMI API Estimates endpoint (`https://api.nmi-agro.nl/estimates`) always returns the fields `b_gwl_ghg`, `b_gwl_glg`, and `cultivations` according to its specification. These fields should be kept as required (not optional) in the TypeScript return type and Zod validation schema in `fdm-app/app/integrations/nmi.ts`.

Applied to files:

  • fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.ts
📚 Learning: 2025-08-13T10:33:05.313Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 0
File: :0-0
Timestamp: 2025-08-13T10:33:05.313Z
Learning: In the fdm project, fdm-calculator integration for new features like b_lu_variety is handled in separate updates from the core data model changes. When fdm-core functions are updated to support new fields, fdm-calculator can consume these enhanced APIs without requiring changes in the same PR that introduces the core functionality.

Applied to files:

  • .changeset/nitrogen-korting-grassland.md
📚 Learning: 2025-02-14T09:56:37.606Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 75
File: fdm-app/app/routes/farm.$b_id_farm.field.$b_id.fertilizer.tsx:68-71
Timestamp: 2025-02-14T09:56:37.606Z
Learning: The `calculateDose` function in `svenvw/fdm-calculator` is a synchronous function that includes built-in validation for negative application amounts and nutrient rates.

Applied to files:

  • .changeset/nitrogen-korting-grassland.md
📚 Learning: 2024-11-25T14:50:16.074Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 6
File: fdm-app/app/routes/app.addfarm.$b_id_farm.fields.tsx:63-99
Timestamp: 2024-11-25T14:50:16.074Z
Learning: i18n will be added in future PRs; for now, hardcoded Dutch text is acceptable until a translation system is implemented.

Applied to files:

  • .changeset/nitrogen-korting-grassland.md
📚 Learning: 2025-09-23T12:27:07.391Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 274
File: fdm-app/app/routes/farm.$b_id_farm._index.tsx:151-204
Timestamp: 2025-09-23T12:27:07.391Z
Learning: In the FDM application, field overview functionality is implemented as a dedicated page accessible via `farm/{farmId}/{calendar}/field` rather than as a direct listing on the dashboard. The dashboard includes a "Perceelsoverzicht" quick action card that provides navigation to this comprehensive field management interface.

Applied to files:

  • fdm-docs/docs/insights/fertilizer-application-norms/nl/2025/stikstofgebruiksnorm.md
🧬 Code graph analysis (2)
fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.test.ts (2)
fdm-calculator/src/norms/nl/2025/value/types.d.ts (2)
  • NL2025NormsInput (14-26)
  • NL2025NormsInputForCultivation (6-9)
fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.ts (1)
  • calculateNL2025StikstofGebruiksNorm (721-838)
fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.ts (2)
fdm-core/src/db/schema.ts (1)
  • cultivations (373-385)
fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm-data.ts (1)
  • nitrogenStandardsData (1-2739)
🪛 markdownlint-cli2 (0.18.1)
fdm-docs/docs/insights/fertilizer-application-norms/nl/2025/stikstofgebruiksnorm.md

72-72: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


73-73: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


74-74: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


83-83: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


84-84: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


85-85: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)


86-86: Unordered list indentation
Expected: 4; Actual: 8

(MD007, ul-indent)

🔇 Additional comments (15)
.changeset/nitrogen-korting-grassland.md (1)

1-5: LGTM!

The changeset correctly declares a minor version bump for the new korting feature, with a clear description of the added functionality.

fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.ts (3)

141-145: LGTM!

The maize derogation handling correctly reflects that derogation is no longer possible in 2026, always returning "non-derogatie". Based on learnings, year-specific hardcoded logic is appropriate for these NL20XX functions.


306-324: LGTM!

The grassland renewal logic correctly:

  • Applies 50 kg N/ha discount for grass-to-grass transitions
  • Validates the June 1 – August 31 window for Sand/Loess regions
  • Throws a descriptive Dutch error message for invalid renewal dates

326-366: LGTM!

The grassland destruction logic correctly implements:

  • 65 kg N/ha discount for grass→maize or grass→consumption/factory potato transitions
  • Exclusion of seed potatoes (pootaardappelen, uitgroeiteelt)
  • Catch crop detection via is_vanggewas flag or autumn sowing inference
  • February 1 – May 10 window validation for Sand/Loess
  • Descriptive Dutch error message for invalid destruction dates
.changeset/nitrogen-korting-docs.md (1)

1-5: LGTM!

The changeset correctly declares a patch version bump for the documentation update, with a clear description of the added content.

fdm-calculator/src/norms/nl/2026/value/stikstofgebruiksnorm.test.ts (2)

559-561: LGTM!

Good practice to extract centroid constants for reuse across tests, improving readability and maintainability.


564-653: LGTM!

Comprehensive test coverage for grassland renewal:

  • Valid discount application on Sand/Loess within the allowed window
  • Correct exclusion of Clay regions (2026 rule)
  • Error handling for invalid renewal dates

The tests align well with the implementation requirements.

fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.test.ts (3)

592-594: LGTM!

Consistent pattern with the 2026 test file for centroid constants.


597-694: LGTM!

Excellent test coverage for 2025 grassland renewal:

  • Sand/Loess valid window test
  • Clay with derogation extended window test (up to Sep 15)
  • Invalid date error handling

The tests correctly reflect the 2025 rules where Clay/Peat eligibility depends on derogation status.


696-821: LGTM!

Comprehensive test coverage for 2025 grassland destruction:

  • Valid maize transition on Sand
  • Valid consumption potato transition on Clay (tests broader 2025 window)
  • Seed potato exclusion
  • Invalid date error handling

The tests properly validate the 2025 rules including Clay region eligibility.

fdm-docs/docs/insights/fertilizer-application-norms/nl/2025/stikstofgebruiksnorm.md (1)

62-93: Documentation accurately reflects the new korting implementation.

The new section comprehensively documents the grassland renewal (50 kg N/ha) and destruction (65 kg N/ha) reductions with soil-type specific windows and derogation conditions. This aligns well with the TypeScript implementation in stikstofgebruiksnorm.ts.

fdm-calculator/src/norms/nl/2025/value/stikstofgebruiksnorm.ts (4)

371-524: Verify behavior with multiple grassland transitions in the same year.

The loop returns early upon finding the first valid renewal or destruction transition. If a field has multiple grassland transitions in 2025 (e.g., Grass→Grass→Maize), only the first detected transition's korting is applied. Confirm this is the intended business logic.


388-457: Grassland renewal logic correctly implements the date windows per soil type and derogation status.

The nested conditions properly handle all combinations:

  • Sand/Loess: June 1 – August 31
  • Clay/Peat + Derogation + NV: June 1 – August 31
  • Clay/Peat + Derogation + Non-NV: June 1 – September 15
  • Clay/Peat + Non-Derogation: February 1 – September 15

Error messages are descriptive and in Dutch, matching the target audience. Based on learnings, hardcoded 2025 dates are appropriate for this NL2025-specific module.


824-826: Call site correctly updated with new parameters.

The calculateKorting invocation now passes is_derogatie_bedrijf and is_nv_area as required by the updated signature. Both values are properly derived earlier in the function.


461-467: Seed potato detection logic is complete and correct.

The verification confirms all seed potato entries in the BRP catalogue contain either "pootaardappelen" or "uitgroeiteelt" in their cultivation_rvo_table2 field. The detection logic correctly captures all three seed potato types in the data:

  • "Akkerbouwgewas, pootaardappelen"
  • "Pootaardappelen, uitgroeiteelt (loofvernietiging na 15 augustus)"
  • And correctly excludes non-seed potatoes like "Akkerbouwgewas, zetmeelaardappelen"

The catalogue code checks (nl_2015, nl_2016) provide redundant coverage alongside the name-based checks, ensuring no seed potatoes are incorrectly flagged for the 65 kg N/ha grassland destruction discount.

@SvenVw SvenVw requested a review from gerardhros January 12, 2026 15:36
} else {
throw new Error(
"Graslandvernieuwing op zand- en lössgrond is alleen toegestaan tussen 1 juni en 31 augustus.",
)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this implies that we do not allow users to adjust from the rules. that seems fine to me, but for nutrients we usually give a warning (so allowing exceedance) isn't? Here you trow out an error. is that by purpose?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

In this case we are not able to calculate the Korting as there is no value outside the valid ranges. This is different to a nutrient advice higher than the norm, in that case we are still able to calculate the value.

}

// 2. Grassland Destruction (Gras-naar-Bouwland) -> 65 kg N/ha korting
// Applies if New Crop is Maize OR Consumption/Factory/Starch Potatoes (NOT Seed Potatoes)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

check question: is this korting also applicable for fertilizer recommendation? usually the current endpoint ignores the past if i remember it well

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think so, but this should be handled by AgNuRe

const isPrevGrass =
prevStandard?.type === "grasland" ||
prevStandard?.type === "grasland_tijdelijk"
const isCurrGrass =
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

graszaad is considered not grassland?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, that is arable

Copy link
Copy Markdown
Collaborator

@gerardhros gerardhros left a comment

Choose a reason for hiding this comment

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

LGTM. all these exceptions makes it challenging ;-)

@SvenVw SvenVw merged commit 4827a6b into development Jan 15, 2026
10 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

branch:development Issue only affecting development, not the main branch (yet) enhancement New feature or request fdm-calculator fdm-docs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Nitrogen Discount (Korting) for Grassland Renewal/Destruction (2025 & 2026)

2 participants