@@ -7,6 +7,12 @@ const MAX_COST_DIGITS = 18
77const MAX_HEXDIGEST_LENGTH = 255
88const DECIMAL_REGEX = / ^ \d + ( \. ( \d { 1 , 2 } ) ) ? $ /
99const INTEGER_REGEX = / ^ \d + $ /
10+ const SIGNED_INTEGER_REGEX = / ^ - ? \d + $ /
11+
12+ // Joi error type constants
13+ const ERROR_STRING_PATTERN_BASE = 'string.pattern.base'
14+ const ERROR_STRING_MAX = 'string.max'
15+ const ERROR_STRING_WHOLE_NUMBER_MAX = 'string.whole_number_max'
1016
1117/**
1218 * Validates a carbon decimal field value (up to 2 decimal places).
@@ -16,17 +22,17 @@ const INTEGER_REGEX = /^\d+$/
1622 */
1723const validateCarbonDecimalString = ( value , helpers ) => {
1824 if ( ! DECIMAL_REGEX . test ( value ) ) {
19- return helpers . error ( 'string.pattern.base' )
25+ return helpers . error ( ERROR_STRING_PATTERN_BASE )
2026 }
2127 const [ intPart , decPart ] = value . split ( '.' )
2228 if ( decPart === undefined ) {
2329 // Whole number: max 18 digits
2430 if ( intPart . length > MAX_WHOLE_NUMBER_DIGITS ) {
25- return helpers . error ( 'string.whole_number_max' )
31+ return helpers . error ( ERROR_STRING_WHOLE_NUMBER_MAX )
2632 }
2733 } else if ( intPart . length > MAX_EMISSION_DIGITS ) {
2834 // Decimal: max 16 digits before decimal point
29- return helpers . error ( 'string.max' )
35+ return helpers . error ( ERROR_STRING_MAX )
3036 } else {
3137 // no error
3238 }
@@ -39,10 +45,27 @@ const validateCarbonDecimalString = (value, helpers) => {
3945 */
4046const validateCarbonIntegerString = ( value , helpers ) => {
4147 if ( ! INTEGER_REGEX . test ( value ) ) {
42- return helpers . error ( 'string.pattern.base' )
48+ return helpers . error ( ERROR_STRING_PATTERN_BASE )
4349 }
4450 if ( value . length > MAX_COST_DIGITS ) {
45- return helpers . error ( 'string.max' )
51+ return helpers . error ( ERROR_STRING_MAX )
52+ }
53+ return value
54+ }
55+
56+ /**
57+ * Validates a carbon signed integer field value (allows negative numbers).
58+ * Used for £ fields that can be negative: net economic benefit.
59+ * Max digits excludes the minus sign.
60+ */
61+ const validateCarbonSignedIntegerString = ( value , helpers ) => {
62+ if ( ! SIGNED_INTEGER_REGEX . test ( value ) ) {
63+ return helpers . error ( ERROR_STRING_PATTERN_BASE )
64+ }
65+ // Count digits excluding minus sign
66+ const digits = value . replace ( / ^ - / , '' )
67+ if ( digits . length > MAX_COST_DIGITS ) {
68+ return helpers . error ( ERROR_STRING_MAX )
4669 }
4770 return value
4871}
@@ -90,6 +113,24 @@ const createOptionalCarbonIntegerSchema = (label) =>
90113 'string.max' : PROJECT_VALIDATION_MESSAGES . CARBON_COST_INVALID
91114 } )
92115
116+ const createOptionalCarbonSignedIntegerSchema = ( label ) =>
117+ Joi . string ( )
118+ . trim ( )
119+ . allow ( null , '' )
120+ . optional ( )
121+ . custom ( ( value , helpers ) => {
122+ if ( value === null || value === undefined || value === '' ) {
123+ return value
124+ }
125+ return validateCarbonSignedIntegerString ( value , helpers )
126+ } )
127+ . label ( label )
128+ . messages ( {
129+ 'string.base' : PROJECT_VALIDATION_MESSAGES . CARBON_COST_INVALID ,
130+ 'string.pattern.base' : PROJECT_VALIDATION_MESSAGES . CARBON_COST_INVALID ,
131+ 'string.max' : PROJECT_VALIDATION_MESSAGES . CARBON_COST_INVALID
132+ } )
133+
93134const createRequiredCarbonOperationalCostForecastSchema = ( label ) =>
94135 Joi . string ( )
95136 . trim ( )
@@ -117,7 +158,7 @@ export const carbonCostAvoidedOptionalSchema =
117158
118159// £ integer fields
119160export const carbonSavingsNetEconomicBenefitOptionalSchema =
120- createOptionalCarbonIntegerSchema ( 'carbonSavingsNetEconomicBenefit' )
161+ createOptionalCarbonSignedIntegerSchema ( 'carbonSavingsNetEconomicBenefit' )
121162export const carbonOperationalCostForecastRequiredSchema =
122163 createRequiredCarbonOperationalCostForecastSchema (
123164 'carbonOperationalCostForecast'
0 commit comments