Skip to main content

riffdb_client_rust/generated/
legal_spend.rs

1//! `@generated` fixture-scoped WP-130 bindings for the canonical POC contract.
2//!
3//! Reproduce this file with `./scripts/generate-rust-sdk`. The wrapper first
4//! runs the typed compiler fixture check that owns the fixture-to-bundle proof.
5//! The fixture-scoped renderer consumes and validates the identities and shapes
6//! in `bundle-metadata.txt`, `command-plans.txt`, and `lineage-ledger.txt`; it
7//! rejects unsupported shape drift instead of inventing client semantics, but
8//! does not decode `bundle.bin`.
9//!
10//! Do not add database semantics here. Stable field and outcome identities are
11//! copied from the checked contract bundle and used only for wire ergonomics.
12
13use riffdb_proto::v1;
14use riffdb_types::{MAX_IDEMPOTENCY_KEY_BYTES, RequestId, Timestamp};
15
16use super::{GeneratedCommand, GeneratedCommandError};
17use crate::IdempotentCommand;
18
19/// Stable contract lineage compiled by this generated module.
20pub const CONTRACT_LINEAGE: &str = "LegalSpend";
21/// Exact contract version compiled by this generated module.
22pub const CONTRACT_VERSION: u64 = 1;
23/// Frozen plan hash for `CreateBudget` at contract version 1.
24pub const CREATE_BUDGET_PLAN_HASH: [u8; 32] = [
25    0x49, 0xf2, 0x57, 0x73, 0xdf, 0xaa, 0x1c, 0x3e, 0x16, 0x2f, 0x75, 0x33, 0xa8, 0x17, 0x62, 0x04,
26    0x7e, 0x25, 0x75, 0x0f, 0xf7, 0xb0, 0xab, 0x38, 0xc5, 0x4a, 0x86, 0x88, 0x21, 0x16, 0x96, 0xf1,
27];
28/// Frozen plan hash for `AllocateBudget` at contract version 1.
29pub const ALLOCATE_BUDGET_PLAN_HASH: [u8; 32] = [
30    0x1f, 0xaa, 0xca, 0xc3, 0x30, 0xda, 0x29, 0xe4, 0xf7, 0x5d, 0x2a, 0xfd, 0xa9, 0x3c, 0xd7, 0x3d,
31    0x3f, 0x8e, 0xfc, 0x53, 0xf2, 0xd8, 0x9c, 0x4a, 0x0e, 0x40, 0x9e, 0xea, 0xa2, 0x33, 0x54, 0xf0,
32];
33const MAX_DECIMAL_COEFFICIENT: i128 = 9_999_999_999_999_999_999_999_999_999;
34
35/// Exact `decimal<28,2>` value represented in minor units.
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub struct Amount(i128);
38
39impl Amount {
40    /// Constructs an amount from its exact scale-two coefficient.
41    #[must_use]
42    pub const fn from_minor_units(coefficient: i128) -> Option<Self> {
43        if coefficient >= -MAX_DECIMAL_COEFFICIENT && coefficient <= MAX_DECIMAL_COEFFICIENT {
44            Some(Self(coefficient))
45        } else {
46            None
47        }
48    }
49
50    /// Returns the exact scale-two coefficient.
51    #[must_use]
52    pub const fn minor_units(self) -> i128 {
53        self.0
54    }
55}
56
57/// Complete public Budget record.
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59pub struct Budget {
60    /// Business organization UUID bytes.
61    pub organization_id: [u8; 16],
62    /// Fiscal year.
63    pub fiscal_year: i64,
64    /// Approved amount.
65    pub approved_amount: Amount,
66    /// Allocated amount.
67    pub allocated_amount: Amount,
68    /// Last command logical time.
69    pub updated_at: Timestamp,
70}
71
72/// Typed `CreateBudget` input.
73#[derive(Clone, Eq, PartialEq)]
74pub struct CreateBudget {
75    /// Caller-retained idempotency key.
76    pub idempotency_key: String,
77    /// Business organization UUID bytes.
78    pub organization_id: [u8; 16],
79    /// Fiscal year.
80    pub fiscal_year: i64,
81    /// Approved amount.
82    pub approved_amount: Amount,
83}
84
85/// Declared `CreateBudget` outcomes.
86#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87pub enum CreateBudgetOutcome {
88    /// A new budget was created.
89    BudgetCreated {
90        /// The newly created complete budget.
91        budget: Budget,
92    },
93    /// The budget identity already existed.
94    BudgetAlreadyExists {
95        /// The existing business organization UUID bytes.
96        organization_id: [u8; 16],
97        /// The existing fiscal year.
98        fiscal_year: i64,
99    },
100    /// The requested approval was not positive.
101    InvalidApprovedAmount {
102        /// The minimum accepted approved amount.
103        minimum: Amount,
104    },
105}
106
107impl GeneratedCommand for CreateBudget {
108    type Outcome = CreateBudgetOutcome;
109
110    fn idempotent_command(&self) -> Result<IdempotentCommand, GeneratedCommandError> {
111        let idempotency_key = checked_idempotency_key(&self.idempotency_key)?;
112        IdempotentCommand::new(
113            "CreateBudget",
114            Some(CONTRACT_VERSION),
115            record(vec![
116                field(1, i64_value(self.fiscal_year)),
117                field(2, decimal_value(self.approved_amount)),
118                field(3, string_value(idempotency_key.to_owned())),
119                field(4, uuid_value(self.organization_id)),
120            ]),
121        )
122        .map_err(Into::into)
123    }
124
125    fn outcome_request(
126        &self,
127        request_id: RequestId,
128    ) -> Result<v1::GetOutcomeRequest, GeneratedCommandError> {
129        Ok(v1::GetOutcomeRequest {
130            request_id: request_id.into_bytes().to_vec(),
131            contract_lineage: CONTRACT_LINEAGE.to_owned(),
132            command_name: "CreateBudget".to_owned(),
133            idempotency_key: checked_idempotency_key(&self.idempotency_key)?.to_owned(),
134            outcome_uri: None,
135        })
136    }
137
138    fn decode_outcome(
139        &self,
140        response: &v1::ExecuteCommandResponse,
141    ) -> Result<Self::Outcome, GeneratedCommandError> {
142        let fields = outcome_fields(response, &CREATE_BUDGET_PLAN_HASH)?;
143        match response.outcome_type.as_str() {
144            "BudgetCreated" => {
145                require_field_ids(fields, &[1])?;
146                Ok(CreateBudgetOutcome::BudgetCreated {
147                    budget: decode_budget(record_field(fields, 1)?)?,
148                })
149            }
150            "BudgetAlreadyExists" => {
151                require_field_ids(fields, &[1, 2])?;
152                Ok(CreateBudgetOutcome::BudgetAlreadyExists {
153                    fiscal_year: i64_field(fields, 1)?,
154                    organization_id: uuid_field(fields, 2)?,
155                })
156            }
157            "InvalidApprovedAmount" => {
158                require_field_ids(fields, &[1])?;
159                Ok(CreateBudgetOutcome::InvalidApprovedAmount {
160                    minimum: minimum_field(fields, 1)?,
161                })
162            }
163            _ => Err(GeneratedCommandError::InvalidOutcomeShape),
164        }
165    }
166}
167
168/// Typed `AllocateBudget` input.
169#[derive(Clone, Eq, PartialEq)]
170pub struct AllocateBudget {
171    /// Caller-retained idempotency key.
172    pub idempotency_key: String,
173    /// Business organization UUID bytes.
174    pub organization_id: [u8; 16],
175    /// Fiscal year.
176    pub fiscal_year: i64,
177    /// Business matter UUID bytes.
178    pub matter_id: [u8; 16],
179    /// Amount to allocate.
180    pub amount: Amount,
181}
182
183/// Declared `AllocateBudget` outcomes.
184#[derive(Clone, Copy, Debug, Eq, PartialEq)]
185pub enum AllocateBudgetOutcome {
186    /// The amount was allocated.
187    Allocated {
188        /// The complete budget after allocation.
189        budget: Budget,
190        /// The exact amount remaining after allocation.
191        remaining: Amount,
192    },
193    /// The requested amount was not positive.
194    InvalidAmount {
195        /// The minimum accepted allocation amount.
196        minimum: Amount,
197    },
198    /// The budget identity did not exist.
199    BudgetNotFound {
200        /// The missing business organization UUID bytes.
201        organization_id: [u8; 16],
202        /// The missing fiscal year.
203        fiscal_year: i64,
204    },
205    /// The budget had insufficient remaining capacity.
206    InsufficientBudget {
207        /// The current approved amount.
208        approved: Amount,
209        /// The current allocated amount.
210        allocated: Amount,
211        /// The requested allocation amount.
212        requested: Amount,
213    },
214}
215
216impl GeneratedCommand for AllocateBudget {
217    type Outcome = AllocateBudgetOutcome;
218
219    fn idempotent_command(&self) -> Result<IdempotentCommand, GeneratedCommandError> {
220        let idempotency_key = checked_idempotency_key(&self.idempotency_key)?;
221        IdempotentCommand::new(
222            "AllocateBudget",
223            Some(CONTRACT_VERSION),
224            record(vec![
225                field(1, decimal_value(self.amount)),
226                field(2, uuid_value(self.matter_id)),
227                field(3, i64_value(self.fiscal_year)),
228                field(4, string_value(idempotency_key.to_owned())),
229                field(5, uuid_value(self.organization_id)),
230            ]),
231        )
232        .map_err(Into::into)
233    }
234
235    fn outcome_request(
236        &self,
237        request_id: RequestId,
238    ) -> Result<v1::GetOutcomeRequest, GeneratedCommandError> {
239        Ok(v1::GetOutcomeRequest {
240            request_id: request_id.into_bytes().to_vec(),
241            contract_lineage: CONTRACT_LINEAGE.to_owned(),
242            command_name: "AllocateBudget".to_owned(),
243            idempotency_key: checked_idempotency_key(&self.idempotency_key)?.to_owned(),
244            outcome_uri: None,
245        })
246    }
247
248    fn decode_outcome(
249        &self,
250        response: &v1::ExecuteCommandResponse,
251    ) -> Result<Self::Outcome, GeneratedCommandError> {
252        let fields = outcome_fields(response, &ALLOCATE_BUDGET_PLAN_HASH)?;
253        match response.outcome_type.as_str() {
254            "Allocated" => {
255                require_field_ids(fields, &[1, 2])?;
256                Ok(AllocateBudgetOutcome::Allocated {
257                    budget: decode_budget(record_field(fields, 1)?)?,
258                    remaining: decimal_field(fields, 2)?,
259                })
260            }
261            "InvalidAmount" => {
262                require_field_ids(fields, &[1])?;
263                Ok(AllocateBudgetOutcome::InvalidAmount {
264                    minimum: minimum_field(fields, 1)?,
265                })
266            }
267            "BudgetNotFound" => {
268                require_field_ids(fields, &[1, 2])?;
269                Ok(AllocateBudgetOutcome::BudgetNotFound {
270                    fiscal_year: i64_field(fields, 1)?,
271                    organization_id: uuid_field(fields, 2)?,
272                })
273            }
274            "InsufficientBudget" => {
275                require_field_ids(fields, &[1, 2, 3])?;
276                Ok(AllocateBudgetOutcome::InsufficientBudget {
277                    approved: decimal_field(fields, 1)?,
278                    allocated: decimal_field(fields, 2)?,
279                    requested: decimal_field(fields, 3)?,
280                })
281            }
282            _ => Err(GeneratedCommandError::InvalidOutcomeShape),
283        }
284    }
285}
286
287fn decode_budget(value: &v1::ValueRecord) -> Result<Budget, GeneratedCommandError> {
288    require_field_ids(value, &[1, 2, 3, 4, 5])?;
289    Ok(Budget {
290        updated_at: timestamp_field(value, 1)?,
291        fiscal_year: i64_field(value, 2)?,
292        approved_amount: decimal_field(value, 3)?,
293        organization_id: uuid_field(value, 4)?,
294        allocated_amount: decimal_field(value, 5)?,
295    })
296}
297
298fn checked_idempotency_key(value: &str) -> Result<&str, GeneratedCommandError> {
299    if value.is_empty() || value.len() > MAX_IDEMPOTENCY_KEY_BYTES {
300        return Err(GeneratedCommandError::InvalidInputShape);
301    }
302    Ok(value)
303}
304
305fn outcome_fields<'response>(
306    response: &'response v1::ExecuteCommandResponse,
307    plan_hash: &[u8; 32],
308) -> Result<&'response v1::ValueRecord, GeneratedCommandError> {
309    if response.contract_version != CONTRACT_VERSION
310        || response.plan_hash.as_slice() != plan_hash
311        || !matches!(
312            v1::execute_command_response::CompletionStatus::try_from(response.status),
313            Ok(v1::execute_command_response::CompletionStatus::Committed)
314                | Ok(v1::execute_command_response::CompletionStatus::Replayed)
315        )
316    {
317        return Err(GeneratedCommandError::InvalidOutcomeShape);
318    }
319    let Some(v1::Value {
320        kind: Some(v1::value::Kind::RecordValue(fields)),
321    }) = response.outcome.as_ref()
322    else {
323        return Err(GeneratedCommandError::InvalidOutcomeShape);
324    };
325    Ok(fields)
326}
327
328fn require_field_ids(
329    record: &v1::ValueRecord,
330    expected: &[u32],
331) -> Result<(), GeneratedCommandError> {
332    if record.fields.len() != expected.len()
333        || !record
334            .fields
335            .iter()
336            .zip(expected)
337            .all(|(field, expected)| field.field_id == Some(*expected))
338    {
339        return Err(GeneratedCommandError::InvalidOutcomeShape);
340    }
341    Ok(())
342}
343
344fn value_field(
345    record: &v1::ValueRecord,
346    field_id: u32,
347) -> Result<&v1::Value, GeneratedCommandError> {
348    record
349        .fields
350        .iter()
351        .find(|field| field.field_id == Some(field_id))
352        .and_then(|field| field.value.as_ref())
353        .ok_or(GeneratedCommandError::InvalidOutcomeShape)
354}
355
356fn record_field(
357    record: &v1::ValueRecord,
358    field_id: u32,
359) -> Result<&v1::ValueRecord, GeneratedCommandError> {
360    match value_field(record, field_id)?.kind.as_ref() {
361        Some(v1::value::Kind::RecordValue(value)) => Ok(value),
362        _ => Err(GeneratedCommandError::InvalidOutcomeShape),
363    }
364}
365
366fn i64_field(record: &v1::ValueRecord, field_id: u32) -> Result<i64, GeneratedCommandError> {
367    match value_field(record, field_id)?.kind.as_ref() {
368        Some(v1::value::Kind::I64Value(value)) => Ok(*value),
369        _ => Err(GeneratedCommandError::InvalidOutcomeShape),
370    }
371}
372
373fn uuid_field(record: &v1::ValueRecord, field_id: u32) -> Result<[u8; 16], GeneratedCommandError> {
374    match value_field(record, field_id)?.kind.as_ref() {
375        Some(v1::value::Kind::UuidValue(value)) => value
376            .as_slice()
377            .try_into()
378            .map_err(|_| GeneratedCommandError::InvalidOutcomeShape),
379        _ => Err(GeneratedCommandError::InvalidOutcomeShape),
380    }
381}
382
383fn timestamp_field(
384    record: &v1::ValueRecord,
385    field_id: u32,
386) -> Result<Timestamp, GeneratedCommandError> {
387    match value_field(record, field_id)?.kind.as_ref() {
388        Some(v1::value::Kind::TimestampValue(value)) => Timestamp::new(value.seconds, value.nanos)
389            .map_err(|_| GeneratedCommandError::InvalidOutcomeShape),
390        _ => Err(GeneratedCommandError::InvalidOutcomeShape),
391    }
392}
393
394fn decimal_field(record: &v1::ValueRecord, field_id: u32) -> Result<Amount, GeneratedCommandError> {
395    match value_field(record, field_id)?.kind.as_ref() {
396        Some(v1::value::Kind::DecimalValue(value)) if value.scale == 2 => {
397            let coefficient = decode_i128(&value.coefficient_twos_complement)?;
398            Amount::from_minor_units(coefficient).ok_or(GeneratedCommandError::InvalidOutcomeShape)
399        }
400        _ => Err(GeneratedCommandError::InvalidOutcomeShape),
401    }
402}
403
404fn minimum_field(record: &v1::ValueRecord, field_id: u32) -> Result<Amount, GeneratedCommandError> {
405    let amount = decimal_field(record, field_id)?;
406    if !(-99..=99).contains(&amount.minor_units()) {
407        return Err(GeneratedCommandError::InvalidOutcomeShape);
408    }
409    Ok(amount)
410}
411
412fn record(fields: Vec<v1::ValueField>) -> v1::Value {
413    v1::Value {
414        kind: Some(v1::value::Kind::RecordValue(v1::ValueRecord { fields })),
415    }
416}
417
418fn field(field_id: u32, value: v1::Value) -> v1::ValueField {
419    v1::ValueField {
420        field_id: Some(field_id),
421        name: String::new(),
422        value: Some(value),
423    }
424}
425
426fn i64_value(value: i64) -> v1::Value {
427    v1::Value {
428        kind: Some(v1::value::Kind::I64Value(value)),
429    }
430}
431
432fn string_value(value: String) -> v1::Value {
433    v1::Value {
434        kind: Some(v1::value::Kind::StringValue(value)),
435    }
436}
437
438fn uuid_value(value: [u8; 16]) -> v1::Value {
439    v1::Value {
440        kind: Some(v1::value::Kind::UuidValue(value.to_vec())),
441    }
442}
443
444fn decimal_value(value: Amount) -> v1::Value {
445    v1::Value {
446        kind: Some(v1::value::Kind::DecimalValue(v1::Decimal {
447            coefficient_twos_complement: encode_i128(value.minor_units()),
448            scale: 2,
449            precision: Some(28),
450        })),
451    }
452}
453
454fn encode_i128(value: i128) -> Vec<u8> {
455    let bytes = value.to_be_bytes();
456    let mut first = 0;
457    while first < bytes.len() - 1 {
458        let redundant_zero = bytes[first] == 0 && bytes[first + 1] & 0x80 == 0;
459        let redundant_one = bytes[first] == 0xff && bytes[first + 1] & 0x80 != 0;
460        if !redundant_zero && !redundant_one {
461            break;
462        }
463        first += 1;
464    }
465    bytes[first..].to_vec()
466}
467
468fn decode_i128(bytes: &[u8]) -> Result<i128, GeneratedCommandError> {
469    if bytes.is_empty() || bytes.len() > 16 {
470        return Err(GeneratedCommandError::InvalidOutcomeShape);
471    }
472    if bytes.len() > 1
473        && ((bytes[0] == 0 && bytes[1] & 0x80 == 0) || (bytes[0] == 0xff && bytes[1] & 0x80 != 0))
474    {
475        return Err(GeneratedCommandError::InvalidOutcomeShape);
476    }
477    let mut full = if bytes[0] & 0x80 == 0 {
478        [0_u8; 16]
479    } else {
480        [0xff_u8; 16]
481    };
482    full[16 - bytes.len()..].copy_from_slice(bytes);
483    Ok(i128::from_be_bytes(full))
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489
490    #[test]
491    fn generated_create_input_uses_frozen_field_ids_and_exact_decimal() {
492        let input = CreateBudget {
493            idempotency_key: "create-1".to_owned(),
494            organization_id: [0x11; 16],
495            fiscal_year: 2026,
496            approved_amount: Amount::from_minor_units(10_000).expect("amount"),
497        };
498        let command = input.idempotent_command().expect("command");
499        assert_eq!(command.command_name(), "CreateBudget");
500        let Some(v1::value::Kind::RecordValue(record)) = command.input().kind.as_ref() else {
501            panic!("record input")
502        };
503        assert_eq!(
504            record
505                .fields
506                .iter()
507                .map(|field| field.field_id.expect("ID"))
508                .collect::<Vec<_>>(),
509            [1, 2, 3, 4]
510        );
511        let Some(v1::value::Kind::DecimalValue(decimal)) = record.fields[1]
512            .value
513            .as_ref()
514            .and_then(|value| value.kind.as_ref())
515        else {
516            panic!("decimal")
517        };
518        assert_eq!(decimal.scale, 2);
519        assert_eq!(
520            decode_i128(&decimal.coefficient_twos_complement),
521            Ok(10_000)
522        );
523
524        let request_id =
525            RequestId::from_unix_milliseconds_and_random(1, [7; 10]).expect("request ID");
526        let recovery = input.outcome_request(request_id).expect("recovery request");
527        assert_eq!(recovery.request_id, request_id.into_bytes());
528        assert_eq!(recovery.contract_lineage, CONTRACT_LINEAGE);
529        assert_eq!(recovery.command_name, "CreateBudget");
530        assert_eq!(recovery.idempotency_key, "create-1");
531    }
532
533    #[test]
534    fn generated_allocate_input_uses_every_frozen_field_and_exact_value() {
535        let input = AllocateBudget {
536            idempotency_key: "allocate-1".to_owned(),
537            organization_id: [0x11; 16],
538            fiscal_year: 2026,
539            matter_id: [0x22; 16],
540            amount: Amount::from_minor_units(8_000).expect("amount"),
541        };
542        let command = input.idempotent_command().expect("command");
543        assert_eq!(command.command_name(), "AllocateBudget");
544        let Some(v1::value::Kind::RecordValue(record)) = command.input().kind.as_ref() else {
545            panic!("record input")
546        };
547        assert_eq!(
548            record
549                .fields
550                .iter()
551                .map(|field| field.field_id.expect("ID"))
552                .collect::<Vec<_>>(),
553            [1, 2, 3, 4, 5]
554        );
555        assert_eq!(decimal_field(record, 1), Ok(input.amount));
556        assert_eq!(uuid_field(record, 2), Ok(input.matter_id));
557        assert_eq!(i64_field(record, 3), Ok(input.fiscal_year));
558        assert!(matches!(
559            value_field(record, 4).and_then(|value| match value.kind.as_ref() {
560                Some(v1::value::Kind::StringValue(value)) => Ok(value.as_str()),
561                _ => Err(GeneratedCommandError::InvalidOutcomeShape),
562            }),
563            Ok("allocate-1")
564        ));
565        assert_eq!(uuid_field(record, 5), Ok(input.organization_id));
566
567        let request_id =
568            RequestId::from_unix_milliseconds_and_random(2, [8; 10]).expect("request ID");
569        let recovery = input.outcome_request(request_id).expect("recovery request");
570        assert_eq!(recovery.request_id, request_id.into_bytes());
571        assert_eq!(recovery.contract_lineage, CONTRACT_LINEAGE);
572        assert_eq!(recovery.command_name, "AllocateBudget");
573        assert_eq!(recovery.idempotency_key, "allocate-1");
574    }
575
576    #[test]
577    fn generated_happy_path_outcomes_decode_complete_budget_values() {
578        let budget = Budget {
579            organization_id: [0x11; 16],
580            fiscal_year: 2026,
581            approved_amount: Amount::from_minor_units(10_000).expect("approved"),
582            allocated_amount: Amount::from_minor_units(8_000).expect("allocated"),
583            updated_at: Timestamp::new(1_700_000_000, 123_456_789).expect("timestamp"),
584        };
585        let create = CreateBudget {
586            idempotency_key: "create-1".to_owned(),
587            organization_id: budget.organization_id,
588            fiscal_year: budget.fiscal_year,
589            approved_amount: budget.approved_amount,
590        };
591        let create_response = successful_response(
592            v1::execute_command_response::CompletionStatus::Committed,
593            &CREATE_BUDGET_PLAN_HASH,
594            "BudgetCreated",
595            record(vec![field(1, budget_value(budget))]),
596        );
597        assert_eq!(
598            create.decode_outcome(&create_response),
599            Ok(CreateBudgetOutcome::BudgetCreated { budget })
600        );
601
602        let allocate = AllocateBudget {
603            idempotency_key: "allocate-1".to_owned(),
604            organization_id: budget.organization_id,
605            fiscal_year: budget.fiscal_year,
606            matter_id: [0x22; 16],
607            amount: Amount::from_minor_units(8_000).expect("amount"),
608        };
609        let remaining = Amount::from_minor_units(2_000).expect("remaining");
610        let allocate_response = successful_response(
611            v1::execute_command_response::CompletionStatus::Replayed,
612            &ALLOCATE_BUDGET_PLAN_HASH,
613            "Allocated",
614            record(vec![
615                field(1, budget_value(budget)),
616                field(2, decimal_value(remaining)),
617            ]),
618        );
619        assert_eq!(
620            allocate.decode_outcome(&allocate_response),
621            Ok(AllocateBudgetOutcome::Allocated { budget, remaining })
622        );
623    }
624
625    #[test]
626    fn generated_idempotency_recovery_rejects_unbound_invalid_keys() {
627        let input = AllocateBudget {
628            idempotency_key: String::new(),
629            organization_id: [0x11; 16],
630            fiscal_year: 2026,
631            matter_id: [0x22; 16],
632            amount: Amount::from_minor_units(100).expect("amount"),
633        };
634        let request_id =
635            RequestId::from_unix_milliseconds_and_random(3, [9; 10]).expect("request ID");
636        assert_eq!(
637            input.idempotent_command(),
638            Err(GeneratedCommandError::InvalidInputShape)
639        );
640        assert_eq!(
641            input.outcome_request(request_id),
642            Err(GeneratedCommandError::InvalidInputShape)
643        );
644    }
645
646    #[test]
647    fn generated_values_never_wrap_decimal_coefficients() {
648        assert!(Amount::from_minor_units(MAX_DECIMAL_COEFFICIENT).is_some());
649        assert!(Amount::from_minor_units(-MAX_DECIMAL_COEFFICIENT).is_some());
650        assert!(Amount::from_minor_units(MAX_DECIMAL_COEFFICIENT + 1).is_none());
651        assert!(Amount::from_minor_units(-MAX_DECIMAL_COEFFICIENT - 1).is_none());
652        for value in [i128::MIN, -129, -128, -1, 0, 1, 127, 128, i128::MAX] {
653            assert_eq!(decode_i128(&encode_i128(value)), Ok(value));
654        }
655    }
656
657    #[test]
658    fn generated_plan_hashes_match_the_checked_compiler_fixture() {
659        let metadata = include_str!("../../../../fixtures/compiler/bundle-metadata.txt");
660        assert_eq!(fixture_value(metadata, "lineage"), CONTRACT_LINEAGE);
661        assert_eq!(
662            fixture_value(metadata, "contract_version"),
663            CONTRACT_VERSION.to_string()
664        );
665
666        let plans = include_str!("../../../../fixtures/compiler/command-plans.txt");
667        assert_plan_fixture(
668            plans,
669            "CreateBudget",
670            CONTRACT_VERSION,
671            &CREATE_BUDGET_PLAN_HASH,
672        );
673        assert_plan_fixture(
674            plans,
675            "AllocateBudget",
676            CONTRACT_VERSION,
677            &ALLOCATE_BUDGET_PLAN_HASH,
678        );
679    }
680
681    #[test]
682    fn generated_decoder_rejects_a_structurally_valid_wrong_plan() {
683        let command = CreateBudget {
684            idempotency_key: "create-1".to_owned(),
685            organization_id: [0x11; 16],
686            fiscal_year: 2026,
687            approved_amount: Amount::from_minor_units(10_000).expect("amount"),
688        };
689        let response = v1::ExecuteCommandResponse {
690            status: v1::execute_command_response::CompletionStatus::Committed as i32,
691            commit_sequence: 1,
692            contract_version: CONTRACT_VERSION,
693            plan_hash: vec![0; 32],
694            outcome_type: "BudgetCreated".to_owned(),
695            outcome: Some(record(Vec::new())),
696            provenance_uri: "riffdb://provenance/018f22e2-79b7-7cc3-a85f-250f0f80c78e".to_owned(),
697            durability_mode: "sync".to_owned(),
698            outcome_uri: None,
699            history_incarnation: 1,
700        };
701        assert_eq!(
702            command.decode_outcome(&response),
703            Err(GeneratedCommandError::InvalidOutcomeShape)
704        );
705    }
706
707    fn budget_value(budget: Budget) -> v1::Value {
708        record(vec![
709            field(
710                1,
711                v1::Value {
712                    kind: Some(v1::value::Kind::TimestampValue(v1::Timestamp {
713                        seconds: budget.updated_at.seconds(),
714                        nanos: budget.updated_at.nanos(),
715                    })),
716                },
717            ),
718            field(2, i64_value(budget.fiscal_year)),
719            field(3, decimal_value(budget.approved_amount)),
720            field(4, uuid_value(budget.organization_id)),
721            field(5, decimal_value(budget.allocated_amount)),
722        ])
723    }
724
725    fn successful_response(
726        status: v1::execute_command_response::CompletionStatus,
727        plan_hash: &[u8; 32],
728        outcome_type: &str,
729        outcome: v1::Value,
730    ) -> v1::ExecuteCommandResponse {
731        v1::ExecuteCommandResponse {
732            status: status as i32,
733            commit_sequence: 1,
734            contract_version: CONTRACT_VERSION,
735            plan_hash: plan_hash.to_vec(),
736            outcome_type: outcome_type.to_owned(),
737            outcome: Some(outcome),
738            provenance_uri: "riffdb://provenance/018f22e2-79b7-7cc3-a85f-250f0f80c78e".to_owned(),
739            durability_mode: "sync".to_owned(),
740            outcome_uri: None,
741            history_incarnation: 1,
742        }
743    }
744
745    fn fixture_value<'fixture>(fixture: &'fixture str, key: &str) -> &'fixture str {
746        fixture
747            .lines()
748            .find_map(|line| line.strip_prefix(key)?.strip_prefix('='))
749            .unwrap_or_else(|| panic!("missing compiler fixture key {key}"))
750    }
751
752    fn assert_plan_fixture(
753        fixture: &str,
754        command_name: &str,
755        contract_version: u64,
756        plan_hash: &[u8; 32],
757    ) {
758        let block = fixture
759            .split("\n[command ")
760            .find(|block| {
761                block
762                    .lines()
763                    .any(|line| line == format!("name={command_name}"))
764            })
765            .unwrap_or_else(|| panic!("missing compiler plan for {command_name}"));
766        assert_eq!(
767            fixture_value(block, "contract_version"),
768            contract_version.to_string()
769        );
770        assert_eq!(fixture_value(block, "plan_hash"), lowercase_hex(plan_hash));
771    }
772
773    fn lowercase_hex(bytes: &[u8]) -> String {
774        const DIGITS: &[u8; 16] = b"0123456789abcdef";
775        let mut output = String::with_capacity(bytes.len() * 2);
776        for byte in bytes {
777            output.push(char::from(DIGITS[usize::from(byte >> 4)]));
778            output.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
779        }
780        output
781    }
782}