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
74 changes: 39 additions & 35 deletions .cspell/custom-words.txt
Original file line number Diff line number Diff line change
@@ -1,78 +1,82 @@
# cspell-specific custom words related to UCP
absl
absltest
adyen
Adyen
agentic
Alam
Amex
Ant
Anytown
atok
backorder
Backordered
Braintree
Carrefour
Centricity
checkout
Chewy
Commerce
Credentialless
Depot
EWALLET
Etsy
Flipkart
Gap
GitHub
Google
Gpay
Kroger
Lowe's
Macy's
Mastercard
Paymentech
Paypal
Preorders
Queensway
Sephora
Shopify
Shopee
Stripe
Target
UCP
Ulta
Visa
Wayfair
Worldpay
Zalando
adyen
agentic
atok
backorder
checkout
credentialless
credentialization
credentialless
Credentialless
cust
datamodel
Depot
dpan
Etsy
ewallet
EWALLET
Flipkart
fontawesome
fpan
fulfillable
Gap
GitHub
Google
gpay
Gpay
ingestions
inlinehilite
Kroger
linenums
llmstxt
Lowe's
Macy's
mastercard
Mastercard
mkdocs
mtok
openapi
openrpc
Paymentech
paypal
Paypal
permissionless
preorders
Preorders
proto
protobuf
pymdownx
Queensway
renderable
repudiable
schemas
sdjwt
Sephora
Shopee
shopify
Shopify
Stripe
superfences
Target
UCP
Ulta
Villagetown
Visa
vulnz
Wayfair
Worldpay
yaml
yml
yml
Zalando
157 changes: 157 additions & 0 deletions fulfillment_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,163 @@ def test_free_shipping_for_specific_item(self) -> None:
self.assertEqual(opt_total, 0)
self.assertIn("Free", free_shipping_option.title)

def test_split_shipments(self) -> None:
"""Test that split shipments are generated when supported by platform.

Given a checkout with multiple items and platform support for multi-group,
When fulfillment is triggered,
Then the response should contain multiple fulfillment groups.
"""
# Create checkout with 2 items
response_json = self.create_checkout_session(
quantity=1, item_id="bouquet_roses", select_fulfillment=False
)
checkout_obj = checkout.Checkout(**response_json)

# Add second item
item_2 = {
"id": "pot_ceramic",
"title": "Ceramic Pot",
}
line_item_2 = {
"quantity": 1,
"item": item_2,
}

# Enable supports_multi_group in PlatformConfig
# We pass it via headers which our extract_webhook_url doesn't use,
# but our service implementation checks platform.supports_multi_group.
# In integration_test_utils, platform is not currently sent in update.
# We need to manually construct the update with platform.

# addr_1 is US in CSV
addr_data = integration_test_utils.test_data.addresses[0]
address = {
"id": "dest_us",
"address_country": addr_data["country"],
"postal_code": addr_data["postal_code"],
}

fulfillment_payload = {
"methods": [
{
"type": "shipping",
"destinations": [address],
"selected_destination_id": "dest_us",
}
]
}

# Custom update call to include platform config
update_payload = {
"id": checkout_obj.id,
"currency": checkout_obj.currency,
"line_items": [
{
"id": checkout_obj.line_items[0].id,
"item": {"id": "bouquet_roses", "title": "Bouquet of Red Roses"},
"quantity": 1,
},
line_item_2,
],
"payment": {
"selected_instrument_id": checkout_obj.payment.selected_instrument_id,
"instruments": checkout_obj.payment.instruments,
"handlers": [
h.model_dump(mode="json", exclude_none=True)
for h in checkout_obj.payment.handlers
],
},
"fulfillment": fulfillment_payload,
"platform": {
"webhook_url": "http://localhost:8286/events",
"supports_multi_group": True,
},
}

response = self.client.put(
f"/checkout-sessions/{checkout_obj.id}",
json=update_payload,
headers=self.get_headers(),
)
self.assert_response_status(response, 200)
split_checkout = checkout.Checkout(**response.json())

# Verify multiple groups
method = split_checkout.fulfillment.root.methods[0]
self.assertGreater(
len(method.groups), 1, "Should have more than one fulfillment group"
)
# Verify each group has one item (based on our server implementation logic)
for group in method.groups:
self.assertEqual(len(group.line_item_ids), 1)

def test_multi_destination(self) -> None:
"""Test that multiple destinations can be provided for a single method.

Given a checkout session,
When an update provides multiple destinations for a method,
Then the response should reflect all destinations.
"""
response_json = self.create_checkout_session(select_fulfillment=False)
checkout_obj = checkout.Checkout(**response_json)

dest1 = {
"id": "dest_1",
"address_country": "US",
"postal_code": "10001",
"street_address": "123 Main St",
}
dest2 = {
"id": "dest_2",
"address_country": "US",
"postal_code": "90210",
"street_address": "456 Oak Ave",
}

fulfillment_payload = {
"methods": [
{
"type": "shipping",
"destinations": [dest1, dest2],
"selected_destination_id": "dest_1",
}
]
}

response_json = self.update_checkout_session(
checkout_obj, fulfillment=fulfillment_payload
)
updated_checkout = checkout.Checkout(**response_json)

method = updated_checkout.fulfillment.root.methods[0]
self.assertEqual(len(method.destinations), 2)
dest_ids = [d.root.id for d in method.destinations]
self.assertIn("dest_1", dest_ids)
self.assertIn("dest_2", dest_ids)

def test_available_methods(self) -> None:
"""Test that available_methods (inventory hints) are returned.

Given a checkout session,
When inventory is available,
Then the response should contain available_methods hints.
"""
response_json = self.create_checkout_session(select_fulfillment=False)
checkout_obj = checkout.Checkout(**response_json)

# Trigger a recalculation
response_json = self.update_checkout_session(
checkout_obj, fulfillment={"methods": [{"type": "shipping"}]}
)
updated_checkout = checkout.Checkout(**response_json)

self.assertIsNotNone(updated_checkout.fulfillment.root.available_methods)
available = updated_checkout.fulfillment.root.available_methods
self.assertNotEmpty(available)
self.assertEqual(available[0].type, "shipping")
self.assertEqual(available[0].fulfillable_on, "now")


if __name__ == "__main__":
absltest.main()
10 changes: 5 additions & 5 deletions protocol_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,17 +110,17 @@ def test_version_negotiation(self):
profile = UcpDiscoveryProfile(**discovery_resp.json())
shopping_service = profile.ucp.services.root["dev.ucp.shopping"]
self.assertIsNotNone(
shopping_service, "Shopping service not found in discovery"
shopping_service, "Shopping service not found in discovery"
)
self.assertIsNotNone(
shopping_service.rest, "REST config not found for shopping service"
shopping_service.rest, "REST config not found for shopping service"
)
self.assertIsNotNone(
shopping_service.rest.endpoint,
"Endpoint not found for shopping service",
shopping_service.rest.endpoint,
"Endpoint not found for shopping service",
)
checkout_sessions_url = (
f"{str(shopping_service.rest.endpoint).rstrip('/')}/checkout-sessions"
f"{str(shopping_service.rest.endpoint).rstrip('/')}/checkout-sessions"
)

create_payload = self.create_checkout_payload()
Expand Down
Loading