Skip to content
Merged
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
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,29 @@ Example `test_fixtures.json`:
"email": "jane.doe@example.com"
},
"free_shipping_min_subtotal": 100.0,
"free_shipping_item_sku": "item_1"
"free_shipping_item_sku": "item_1",
"dynamic_fulfillment": {
"domestic": {
"destination": {
"street_address": "123 Main St",
"address_locality": "Springfield",
"address_region": "IL",
"postal_code": "62704",
"address_country": "US"
},
"expected_option_id": "exp-ship-us"
},
"international": {
"destination": {
"street_address": "25 King St W",
"address_locality": "Toronto",
"address_region": "ON",
"postal_code": "M5V 2H1",
"address_country": "CA"
},
"expected_option_id": "exp-ship-intl"
}
}
},
"shipping_locations": {
"domestic_destination": {
Expand All @@ -128,6 +150,7 @@ Example `test_fixtures.json`:
- `known_customer_without_address`: **Optional**. A buyer your server recognizes but has no stored addresses for. The corresponding test skips if not configured.
- `free_shipping_min_subtotal`: **Optional**. Order subtotal (major units) at which your server offers a zero-cost fulfillment option. The threshold test skips if not configured.
- `free_shipping_item_sku`: **Optional**. An item SKU eligible for free shipping regardless of order value. The item test skips if not configured.
- `dynamic_fulfillment`: **Optional**. Destination/option pairs for the dynamic fulfillment test: each case declares a destination address (response-schema field names) and the option id your server returns for that destination. The test verifies that updating the fulfillment address to the domestic destination makes that destination's option available, and likewise for the international destination; it skips if not configured.
- `shipping_locations`: Addresses used for fulfillment tests.

### 4. Run the Tests
Expand Down
87 changes: 47 additions & 40 deletions fulfillment_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,80 +195,87 @@ def test_dynamic_fulfillment(self) -> None:
"""Test that fulfillment options are dynamically generated based on address.

Given a checkout session,
When the fulfillment address is updated to a US address, then a US-specific
option is available.
When the fulfillment address is updated to a CA address, then a CA-specific
option is available.
When the fulfillment address is updated to the domestic destination, then
the option id declared for it in the fixtures is available.
When the fulfillment address is updated to the international destination,
then the option id declared for it in the fixtures is available.
"""
dynamic = self.fixture_ctx.get_dynamic_fulfillment()
if dynamic is None:
self.skipTest(
"No dynamic fulfillment cases configured in fixtures "
"(test_fixtures.dynamic_fulfillment)."
)

response_json = self.create_checkout_session(select_fulfillment=False)
checkout_obj = checkout.Checkout(**response_json)

# 1. Update with US Address
# addr_1 is US in CSV
addr_data = integration_test_utils.test_data.addresses[0]
us_address = {
"id": "dest_us",
"address_country": addr_data["country"],
"postal_code": addr_data["postal_code"],
# 1. Update with the domestic destination
domestic = dynamic["domestic"]
domestic_dest = {
"id": "dest_domestic",
**domestic["destination"],
}

fulfillment_us = {
fulfillment_domestic = {
"methods": [
{
"type": "shipping",
"id": "method_1",
"line_item_ids": [checkout_obj.line_items[0].id],
"destinations": [us_address],
"selected_destination_id": "dest_us",
"destinations": [domestic_dest],
"selected_destination_id": "dest_domestic",
}
]
}

response_json = self.update_checkout_session(
checkout_obj, fulfillment=fulfillment_us
checkout_obj, fulfillment=fulfillment_domestic
)
us_checkout = checkout.Checkout(**response_json)
domestic_checkout = checkout.Checkout(**response_json)

# Check for US options
options = us_checkout.model_extra["fulfillment"]["methods"][0]["groups"][0][
"options"
]
# Check that the declared domestic option is available
options = domestic_checkout.model_extra["fulfillment"]["methods"][0][
"groups"
][0]["options"]
self.assertTrue(
options and any(o["id"] == "exp-ship-us" for o in options),
f"Expected US express option, got {options}",
options
and any(o["id"] == domestic["expected_option_id"] for o in options),
"Expected domestic option "
f"{domestic['expected_option_id']}, got {options}",
)

# 2. Update with CA Address
ca_address = {
"id": "dest_ca",
"address_country": "CA",
"postal_code": "M5V 2H1",
# 2. Update with the international destination
international = dynamic["international"]
international_dest = {
"id": "dest_international",
**international["destination"],
}

fulfillment_ca = {
fulfillment_international = {
"methods": [
{
"type": "shipping",
"id": "method_1",
"line_item_ids": [checkout_obj.line_items[0].id],
"destinations": [ca_address],
"selected_destination_id": "dest_ca",
"destinations": [international_dest],
"selected_destination_id": "dest_international",
}
]
}

response_json = self.update_checkout_session(
us_checkout, fulfillment=fulfillment_ca
domestic_checkout, fulfillment=fulfillment_international
)
ca_checkout = checkout.Checkout(**response_json)
international_checkout = checkout.Checkout(**response_json)

# Check for International options
options = ca_checkout.model_extra["fulfillment"]["methods"][0]["groups"][0][
"options"
]
# Check that the declared international option is available
options = international_checkout.model_extra["fulfillment"]["methods"][0][
"groups"
][0]["options"]
self.assertTrue(
options and any(o["id"] == "exp-ship-intl" for o in options),
f"Expected Intl express option, got {options}",
options
and any(o["id"] == international["expected_option_id"] for o in options),
"Expected international option "
f"{international['expected_option_id']}, got {options}",
)

def test_unknown_customer_no_address(self) -> None:
Expand Down
43 changes: 43 additions & 0 deletions integration_test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,20 @@ class CustomerFixture(TypedDict, total=False):
addresses: list[AddressFixture]


class DynamicFulfillmentCaseFixture(TypedDict):
"""A destination and fulfillment option expected for dynamic options."""

destination: AddressFixture
expected_option_id: str


class DynamicFulfillmentFixture(TypedDict):
"""Dynamic fulfillment cases declared by the server under test."""

domestic: DynamicFulfillmentCaseFixture
international: DynamicFulfillmentCaseFixture


class DynamicFixtureContext:
"""Context for loading dynamic test fixtures from configuration."""

Expand Down Expand Up @@ -648,6 +662,35 @@ def get_free_shipping_item_sku(self) -> str | None:
return None
return str(val)

def get_dynamic_fulfillment(self) -> DynamicFulfillmentFixture | None:
"""Get destination/option pairs for dynamic fulfillment tests.

Returns None when the server under test declares no dynamic fulfillment
cases, in which case the dynamic-options test skips. Each case declares a
destination address (response-schema field names) and the fulfillment
option id the server returns for that destination.
"""
val = self._get_test_fixture("dynamic_fulfillment")
if not isinstance(val, dict):
return None
if not isinstance(val.get("domestic"), dict) or not isinstance(
val.get("international"), dict
):
return None
domestic = val["domestic"]
international = val["international"]
if not isinstance(domestic.get("destination"), dict) or not isinstance(
domestic.get("expected_option_id"), str
):
return None
if not isinstance(international.get("destination"), dict) or not isinstance(
international.get("expected_option_id"), str
):
return None
return DynamicFulfillmentFixture(
domestic=domestic, international=international
)


ConfiguredFixtureContext = DynamicFixtureContext

Expand Down
24 changes: 23 additions & 1 deletion test_data/flower_shop/test_fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,29 @@
"email": "jane.doe@example.com"
},
"free_shipping_min_subtotal": 100.0,
"free_shipping_item_sku": "bouquet_roses"
"free_shipping_item_sku": "bouquet_roses",
"dynamic_fulfillment": {
"domestic": {
"destination": {
"street_address": "123 Main St",
"address_locality": "Springfield",
"address_region": "IL",
"postal_code": "62704",
"address_country": "US"
},
"expected_option_id": "exp-ship-us"
},
"international": {
"destination": {
"street_address": "25 King St W",
"address_locality": "Toronto",
"address_region": "ON",
"postal_code": "M5V 2H1",
"address_country": "CA"
},
"expected_option_id": "exp-ship-intl"
}
}
},
"shipping_locations": {
"domestic_destination": {
Expand Down
Loading