Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions code/samples/go/pkg/ap2/types/mandate.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ type IntentMandate struct {
SKUs []string `json:"skus,omitempty"`
RequiresRefundability *bool `json:"requires_refundability,omitempty"`
IntentExpiry string `json:"intent_expiry"`
// Budget is the maximum total amount the agent is authorized to spend when
// fulfilling this intent. If set, the agent must not place orders whose
// total exceeds this value.
Budget *PaymentCurrencyAmount `json:"budget,omitempty"`
}

func NewIntentMandate() *IntentMandate {
Expand Down
129 changes: 129 additions & 0 deletions code/samples/go/pkg/ap2/types/mandate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package types

import (
"encoding/json"
"testing"
)

func TestIntentMandateBudgetOptional(t *testing.T) {
mandate := &IntentMandate{
NaturalLanguageDescription: "Red basketball shoes",
IntentExpiry: "2026-12-31T00:00:00Z",
}
if mandate.Budget != nil {
t.Errorf("expected Budget to be nil, got %+v", mandate.Budget)
}
}

func TestIntentMandateBudgetCanBeSet(t *testing.T) {
budget := &PaymentCurrencyAmount{Currency: "USD", Value: 150.00}
mandate := &IntentMandate{
NaturalLanguageDescription: "Red basketball shoes",
IntentExpiry: "2026-12-31T00:00:00Z",
Budget: budget,
}
if mandate.Budget == nil {
t.Fatal("expected Budget to be set")
}
if mandate.Budget.Currency != "USD" {
t.Errorf("expected currency USD, got %s", mandate.Budget.Currency)
}
if mandate.Budget.Value != 150.00 {
t.Errorf("expected value 150.00, got %f", mandate.Budget.Value)
}
}

func TestIntentMandateBudgetSerializesToJSON(t *testing.T) {
budget := &PaymentCurrencyAmount{Currency: "EUR", Value: 200.00}
mandate := &IntentMandate{
NaturalLanguageDescription: "Concert tickets",
IntentExpiry: "2026-06-01T00:00:00Z",
Budget: budget,
}
data, err := json.Marshal(mandate)
if err != nil {
t.Fatalf("json.Marshal failed: %v", err)
}

var decoded map[string]interface{}
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("json.Unmarshal failed: %v", err)
}

budgetField, ok := decoded["budget"]
if !ok {
t.Fatal("expected 'budget' key in JSON output")
}
budgetMap, ok := budgetField.(map[string]interface{})
if !ok {
t.Fatalf("expected budget to be an object, got %T", budgetField)
}
if budgetMap["currency"] != "EUR" {
t.Errorf("expected currency EUR, got %v", budgetMap["currency"])
}
if budgetMap["value"] != 200.00 {
t.Errorf("expected value 200.00, got %v", budgetMap["value"])
}
}

func TestIntentMandateBudgetAbsentOmittedFromJSON(t *testing.T) {
mandate := &IntentMandate{
NaturalLanguageDescription: "Groceries",
IntentExpiry: "2026-01-01T00:00:00Z",
}
data, err := json.Marshal(mandate)
if err != nil {
t.Fatalf("json.Marshal failed: %v", err)
}

var decoded map[string]interface{}
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("json.Unmarshal failed: %v", err)
}

if _, ok := decoded["budget"]; ok {
t.Error("expected 'budget' key to be omitted from JSON when nil")
}
}

func TestIntentMandateBudgetRoundTrip(t *testing.T) {
budget := &PaymentCurrencyAmount{Currency: "GBP", Value: 75.50}
original := &IntentMandate{
NaturalLanguageDescription: "Books",
IntentExpiry: "2026-03-01T00:00:00Z",
Budget: budget,
}
serialized, err := json.Marshal(original)
if err != nil {
t.Fatalf("json.Marshal failed: %v", err)
}

var restored IntentMandate
if err := json.Unmarshal(serialized, &restored); err != nil {
t.Fatalf("json.Unmarshal failed: %v", err)
}

if restored.Budget == nil {
t.Fatal("expected restored Budget to be set")
}
if restored.Budget.Currency != "GBP" {
t.Errorf("expected currency GBP, got %s", restored.Budget.Currency)
}
if restored.Budget.Value != 75.50 {
t.Errorf("expected value 75.50, got %f", restored.Budget.Value)
}
}
9 changes: 9 additions & 0 deletions code/sdk/python/ap2/models/mandate.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from datetime import UTC, datetime

from ap2.models.payment_request import (
PaymentCurrencyAmount,
PaymentItem,
PaymentRequest,
PaymentResponse,
Expand Down Expand Up @@ -74,6 +75,14 @@ class IntentMandate(BaseModel):
...,
description='When the intent mandate expires, in ISO 8601 format.',
)
budget: PaymentCurrencyAmount | None = Field(
None,
description=(
'The maximum total amount the agent is authorized to spend when'
' fulfilling this intent. If set, the agent must not place orders'
' whose total exceeds this value.'
),
)


class CartContents(BaseModel):
Expand Down
91 changes: 91 additions & 0 deletions code/sdk/python/ap2/tests/mandate_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for AP2 mandate models."""

import json

from ap2.models.mandate import IntentMandate
from ap2.models.payment_request import PaymentCurrencyAmount


def test_intent_mandate_budget_is_optional():
"""IntentMandate can be created without a budget."""
mandate = IntentMandate(
natural_language_description='Red basketball shoes',
intent_expiry='2026-12-31T00:00:00Z',
)
assert mandate.budget is None


def test_intent_mandate_budget_can_be_set():
"""IntentMandate accepts a PaymentCurrencyAmount budget."""
budget = PaymentCurrencyAmount(currency='USD', value=150.00)
mandate = IntentMandate(
natural_language_description='Red basketball shoes',
intent_expiry='2026-12-31T00:00:00Z',
budget=budget,
)
assert mandate.budget is not None
assert mandate.budget.currency == 'USD'
assert mandate.budget.value == 150.00


def test_intent_mandate_budget_serializes_to_json():
"""Budget field serializes correctly in JSON output."""
budget = PaymentCurrencyAmount(currency='EUR', value=200.00)
mandate = IntentMandate(
natural_language_description='Concert tickets',
intent_expiry='2026-06-01T00:00:00Z',
budget=budget,
)
data = json.loads(mandate.model_dump_json())
assert data['budget'] == {'currency': 'EUR', 'value': 200.00}


def test_intent_mandate_budget_absent_omitted_from_json():
"""Budget field is absent from JSON when not set."""
mandate = IntentMandate(
natural_language_description='Groceries',
intent_expiry='2026-01-01T00:00:00Z',
)
data = json.loads(mandate.model_dump_json(exclude_none=True))
assert 'budget' not in data


def test_intent_mandate_budget_round_trip():
"""IntentMandate with budget survives a JSON round-trip."""
budget = PaymentCurrencyAmount(currency='GBP', value=75.50)
original = IntentMandate(
natural_language_description='Books',
intent_expiry='2026-03-01T00:00:00Z',
budget=budget,
)
serialized = original.model_dump_json()
restored = IntentMandate.model_validate_json(serialized)
assert restored.budget is not None
assert restored.budget.currency == 'GBP'
assert restored.budget.value == 75.50


def test_intent_mandate_budget_zero_value_allowed():
"""Budget of zero is a valid (if unusual) value."""
budget = PaymentCurrencyAmount(currency='USD', value=0.0)
mandate = IntentMandate(
natural_language_description='Free items only',
intent_expiry='2026-12-31T00:00:00Z',
budget=budget,
)
assert mandate.budget is not None
assert mandate.budget.value == 0.0
Loading