Compare commits
4 commits
9592f38902
...
c17cc3fbbc
| Author | SHA1 | Date | |
|---|---|---|---|
| c17cc3fbbc | |||
| 65d31a3f2f | |||
| f30b31ec00 | |||
| 1f06ec9e44 |
5 changed files with 1452 additions and 39 deletions
294
README.md
294
README.md
|
|
@ -1,62 +1,286 @@
|
|||
# eBay REST Client
|
||||
|
||||
This workspace contains a Python-first eBay REST client foundation with:
|
||||
This workspace contains a REST-first Python client for the newer eBay APIs backed by:
|
||||
|
||||
- a shared OAuth 2.0 core
|
||||
- a shared HTTP transport layer
|
||||
- public API wrappers per eBay REST domain
|
||||
- an isolated Pydantic model generation script for each contract in this folder
|
||||
- handwritten public wrappers per API domain
|
||||
- isolated OpenAPI-generated Pydantic models per contract
|
||||
|
||||
Currently wired API domains include Notification, Inventory, Fulfillment, Account, Feed, and Media.
|
||||
Implemented API domains:
|
||||
|
||||
## Media Helpers
|
||||
- Notification
|
||||
- Inventory
|
||||
- Fulfillment
|
||||
- Account
|
||||
- Feed
|
||||
- Media
|
||||
|
||||
The Media wrapper includes workflow helpers on top of the raw endpoints:
|
||||
The public entry point is `EbayClient`, which wires all six wrappers behind one shared transport and one shared token store.
|
||||
|
||||
- `VideoWorkflowResult` and `DocumentWorkflowResult` to return structured data from the higher-level workflows
|
||||
- `extract_resource_id()` to pull a media resource ID from a `Location` header
|
||||
- `guess_media_content_type()` to infer a content type from a file name when possible
|
||||
- `wait_for_video()` to poll until a video reaches `LIVE` or a terminal failure state
|
||||
- `wait_for_document()` to poll until a document reaches `ACCEPTED` or a terminal failure state
|
||||
- `create_upload_and_wait_video()` to stage, upload, and poll a video in one call
|
||||
- `create_upload_and_wait_video_from_path()` to do the same directly from a local file path
|
||||
- `create_upload_and_wait_document()` to stage, upload, and poll a document in one call
|
||||
- `create_upload_and_wait_document_from_path()` to do the same directly from a local file path
|
||||
- `create_document_from_url_and_wait()` to create a document from a URL and poll until it is accepted
|
||||
- `create_image_from_path()` and `upload_document_from_path()` for path-based local file uploads
|
||||
## Project Layout
|
||||
|
||||
A concrete workflow example is available in `examples/media_workflows.py` for:
|
||||
- `ebay_client/client.py`: top-level `EbayClient` facade
|
||||
- `ebay_client/<api>/client.py`: handwritten wrapper layer for each API
|
||||
- `ebay_client/generated/<api>/models.py`: generated Pydantic models for that contract
|
||||
- `ebay_client/core/auth/`: OAuth configuration, token model, token store, and OAuth client
|
||||
- `ebay_client/core/http/`: shared transport and error handling
|
||||
- `examples/`: runnable examples for media workflows and FastAPI notification webhooks
|
||||
|
||||
- uploading an image from a file
|
||||
- creating an image from a URL
|
||||
- staging, uploading, and polling a document
|
||||
- staging, uploading, and polling a video
|
||||
## Quick Start
|
||||
|
||||
## Generate Low-Level Clients
|
||||
Install the package from the project root:
|
||||
|
||||
The project uses a dedicated code generation environment because the main runtime is currently on Python 3.14 while the model generator still targets earlier Python versions.
|
||||
```powershell
|
||||
& .\.venv\Scripts\python.exe -m pip install -e .
|
||||
```
|
||||
|
||||
Run the generator script from the project root:
|
||||
Install development dependencies when you want tests and code generation tooling:
|
||||
|
||||
```powershell
|
||||
& .\.venv\Scripts\python.exe -m pip install -e .[dev]
|
||||
```
|
||||
|
||||
Create a client with your eBay credentials:
|
||||
|
||||
```python
|
||||
from ebay_client import EbayClient
|
||||
from ebay_client.core.auth.models import EbayOAuthConfig
|
||||
|
||||
oauth_config = EbayOAuthConfig(
|
||||
client_id="your-client-id",
|
||||
client_secret="your-client-secret",
|
||||
default_scopes=[
|
||||
"https://api.ebay.com/oauth/api_scope/sell.inventory",
|
||||
],
|
||||
)
|
||||
|
||||
client = EbayClient(oauth_config)
|
||||
|
||||
inventory_page = client.inventory.get_inventory_items(limit=25)
|
||||
orders_page = client.fulfillment.get_orders(limit=25)
|
||||
```
|
||||
|
||||
`EbayClient` exposes these wrappers:
|
||||
|
||||
- `client.notification`
|
||||
- `client.inventory`
|
||||
- `client.fulfillment`
|
||||
- `client.account`
|
||||
- `client.feed`
|
||||
- `client.media`
|
||||
|
||||
## Authentication
|
||||
|
||||
The client uses a custom OAuth implementation in `ebay_client.core.auth.oauth.EbayOAuthClient`. There is no dependency on eBay's official OAuth client.
|
||||
|
||||
Important behavior:
|
||||
|
||||
- `EbayClient` creates an internal `EbayOAuthClient` and uses it automatically for API calls.
|
||||
- The default token store is `InMemoryTokenStore`, so tokens are not persisted across process restarts.
|
||||
- If you want persistent tokens, provide your own `TokenStore` implementation to `EbayClient`.
|
||||
- `redirect_uri` is only required when you use the authorization-code flow.
|
||||
- For plain wrapper usage, the transport can obtain client-credentials tokens automatically.
|
||||
|
||||
If you need interactive OAuth flows, build and use `EbayOAuthClient` directly with the same token store you pass to `EbayClient`:
|
||||
|
||||
```python
|
||||
from ebay_client import EbayClient
|
||||
from ebay_client.core.auth.models import EbayOAuthConfig
|
||||
from ebay_client.core.auth.oauth import EbayOAuthClient
|
||||
from ebay_client.core.auth.store import InMemoryTokenStore
|
||||
|
||||
store = InMemoryTokenStore()
|
||||
|
||||
oauth_config = EbayOAuthConfig(
|
||||
client_id="your-client-id",
|
||||
client_secret="your-client-secret",
|
||||
redirect_uri="https://your-app.example/callback",
|
||||
default_scopes=[
|
||||
"https://api.ebay.com/oauth/api_scope",
|
||||
"https://api.ebay.com/oauth/api_scope/commerce.notification.subscription",
|
||||
],
|
||||
)
|
||||
|
||||
oauth = EbayOAuthClient(oauth_config, token_store=store)
|
||||
authorization_url = oauth.build_authorization_url(state="csrf-token")
|
||||
|
||||
client = EbayClient(oauth_config, token_store=store)
|
||||
```
|
||||
|
||||
## Authentication Caveats
|
||||
|
||||
The wrappers handle scope selection for you, but a few behaviors are worth calling out explicitly.
|
||||
|
||||
- Read methods often accept either a readonly scope or the broader write scope. Internally, the wrappers express this through `scope_options`, so an already-cached broader token is reused instead of forcing a narrower re-auth.
|
||||
- Write methods request the stronger write scope directly.
|
||||
- If the current token is missing required scopes, the OAuth layer requests a new token with the needed scope set.
|
||||
- `default_scopes` matter most when you explicitly use `build_authorization_url()`, `exchange_code()`, or `refresh_access_token()` without passing scopes.
|
||||
- Notification inbound webhooks are not authenticated with bearer tokens. They are validated through the `X-EBAY-SIGNATURE` header and eBay's public-key lookup flow.
|
||||
|
||||
Known endpoint-specific caveats:
|
||||
|
||||
- `FulfillmentClient.issue_refund()` uses `https://api.ebay.com/oauth/api_scope/sell.finances`, not the normal fulfillment scope.
|
||||
- Payment-dispute methods use `https://api.ebay.com/oauth/api_scope/sell.payment.dispute` and target `https://apiz.ebay.com/sell/fulfillment/v1/...`. The shared transport already supports this alternate host.
|
||||
- Some Inventory write endpoints require a `Content-Language` header. Those wrapper methods require `content_language=...` explicitly.
|
||||
- eBay documents additional Digital Signatures requirements for some refund scenarios, especially for some EU and UK sellers. That signing flow is not implemented in this client yet, so refund support is limited to the normal OAuth-backed request path.
|
||||
|
||||
## Implemented Functionality
|
||||
|
||||
This section is intended as the quick capability map for the handwritten wrappers. For payload and response shapes, use the generated models in `ebay_client/generated/<api>/models.py`.
|
||||
|
||||
### Notification
|
||||
|
||||
Management and subscription features:
|
||||
|
||||
- config: `get_config()`, `update_config()`
|
||||
- topics: `get_topics()`, `get_topic()`
|
||||
- destinations: `get_destinations()`, `create_destination()`, `get_destination()`, `update_destination()`, `delete_destination()`
|
||||
- subscriptions: `get_subscriptions()`, `create_subscription()`, `get_subscription()`, `update_subscription()`, `delete_subscription()`
|
||||
- subscription state actions: `disable_subscription()`, `enable_subscription()`, `test_subscription()`
|
||||
- filters: `create_subscription_filter()`, `get_subscription_filter()`, `delete_subscription_filter()`
|
||||
- public-key lookup: `get_public_key()`
|
||||
|
||||
Webhook helpers in `ebay_client.notification.webhook` cover:
|
||||
|
||||
- challenge responses
|
||||
- signature-header parsing
|
||||
- signature verification against eBay public keys
|
||||
- normalized event envelopes
|
||||
- framework-neutral request handling
|
||||
|
||||
See `examples/fastapi_notification_webhook.py` for a concrete FastAPI integration.
|
||||
|
||||
### Inventory
|
||||
|
||||
Inventory wrapper coverage includes:
|
||||
|
||||
- inventory items: `get_inventory_item()`, `get_inventory_items()`, `create_or_replace_inventory_item()`, `delete_inventory_item()`
|
||||
- bulk item workflows: `bulk_get_inventory_item()`, `bulk_create_or_replace_inventory_item()`
|
||||
- price and quantity updates: `bulk_update_price_quantity()`
|
||||
- product compatibility: `get_product_compatibility()`, `create_or_replace_product_compatibility()`, `delete_product_compatibility()`
|
||||
- inventory item groups: `get_inventory_item_group()`, `create_or_replace_inventory_item_group()`, `delete_inventory_item_group()`
|
||||
- offers: `get_offer()`, `get_offers()`, `create_offer()`, `bulk_create_offer()`, `update_offer()`, `delete_offer()`
|
||||
- listing publication: `publish_offer()`, `bulk_publish_offer()`, `publish_offer_by_inventory_item_group()`
|
||||
- listing withdrawal: `withdraw_offer()`, `withdraw_offer_by_inventory_item_group()`
|
||||
- fees: `get_listing_fees()`
|
||||
- locations: `get_inventory_location()`, `get_inventory_locations()`, `create_inventory_location()`, `update_inventory_location()`, `delete_inventory_location()`, `enable_inventory_location()`, `disable_inventory_location()`
|
||||
- migration and mapping: `bulk_migrate_listing()`, `get_sku_location_mapping()`, `create_or_replace_sku_location_mapping()`, `delete_sku_location_mapping()`
|
||||
|
||||
### Fulfillment
|
||||
|
||||
Fulfillment wrapper coverage includes:
|
||||
|
||||
- orders: `get_order()`, `get_orders()`
|
||||
- refunds: `issue_refund()`
|
||||
- shipping fulfillments: `get_shipping_fulfillments()`, `get_shipping_fulfillment()`, `create_shipping_fulfillment()`
|
||||
- payment disputes: `get_payment_dispute()`, `get_payment_dispute_summaries()`, `get_payment_dispute_activities()`, `contest_payment_dispute()`, `accept_payment_dispute()`
|
||||
- dispute evidence: `upload_evidence_file()`, `add_evidence()`, `update_evidence()`, `fetch_evidence_content()`
|
||||
- helper types: `CreatedShippingFulfillment`, `EvidenceFileDownload`, `extract_fulfillment_id()`
|
||||
|
||||
### Account
|
||||
|
||||
Account wrapper coverage includes:
|
||||
|
||||
- fulfillment policies: list, get, create, update, delete, and `get_fulfillment_policy_by_name()`
|
||||
- payment policies: list, get, create, update, delete, and `get_payment_policy_by_name()`
|
||||
- return policies: list, get, create, update, delete, and `get_return_policy_by_name()`
|
||||
- seller capabilities: `get_privileges()`, `get_opted_in_programs()`
|
||||
|
||||
### Feed
|
||||
|
||||
Feed wrapper coverage includes:
|
||||
|
||||
- tasks: `get_tasks()`, `create_task()`, `get_task()`
|
||||
- task files: `upload_file()`, `get_input_file()`, `get_result_file()`, `get_latest_result_file()`
|
||||
- schedule templates: `get_schedule_templates()`, `get_schedule_template()`
|
||||
- schedules: `get_schedules()`, `create_schedule()`, `get_schedule()`, `update_schedule()`, `delete_schedule()`
|
||||
- helper types: `CreatedFeedResource`, `FeedFileDownload`, `extract_feed_resource_id()`
|
||||
|
||||
### Media
|
||||
|
||||
Media wrapper coverage includes raw API methods plus higher-level workflow helpers.
|
||||
|
||||
Core operations:
|
||||
|
||||
- images: `create_image_from_file()`, `create_image_from_path()`, `create_image_from_url()`, `get_image()`
|
||||
- videos: `create_video()`, `upload_video()`, `get_video()`, `wait_for_video()`
|
||||
- documents: `create_document()`, `create_document_from_url()`, `upload_document()`, `upload_document_from_path()`, `get_document()`, `wait_for_document()`
|
||||
|
||||
Workflow helpers:
|
||||
|
||||
- `create_upload_and_wait_video()`
|
||||
- `create_upload_and_wait_video_from_path()`
|
||||
- `create_upload_and_wait_document()`
|
||||
- `create_upload_and_wait_document_from_path()`
|
||||
- `create_document_from_url_and_wait()`
|
||||
- `extract_resource_id()`
|
||||
- `guess_media_content_type()`
|
||||
- result types: `VideoWorkflowResult`, `DocumentWorkflowResult`
|
||||
|
||||
See `examples/media_workflows.py` for end-to-end examples.
|
||||
|
||||
## Working With Generated Models
|
||||
|
||||
The wrapper layer is intentionally thin. For request bodies and typed responses, import the generated models for the relevant API package.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
from ebay_client import EbayClient
|
||||
from ebay_client.core.auth.models import EbayOAuthConfig
|
||||
from ebay_client.generated.media.models import CreateDocumentRequest
|
||||
|
||||
client = EbayClient(
|
||||
EbayOAuthConfig(
|
||||
client_id="your-client-id",
|
||||
client_secret="your-client-secret",
|
||||
default_scopes=["https://api.ebay.com/oauth/api_scope/sell.inventory"],
|
||||
)
|
||||
)
|
||||
|
||||
result = client.media.create_upload_and_wait_document_from_path(
|
||||
CreateDocumentRequest(
|
||||
documentType="USER_GUIDE_OR_MANUAL",
|
||||
languages=["en-US"],
|
||||
),
|
||||
"./manual.pdf",
|
||||
)
|
||||
```
|
||||
|
||||
## Webhook Usage
|
||||
|
||||
For Notification inbound events, the recommended flow is:
|
||||
|
||||
- handle the eBay challenge request with `WebhookRequestHandler.handle_challenge()`
|
||||
- resolve the notification public key through `WebhookPublicKeyResolver`
|
||||
- validate `X-EBAY-SIGNATURE` with `WebhookSignatureValidator`
|
||||
- dispatch the verified event envelope returned by `WebhookRequestHandler.handle_notification()`
|
||||
|
||||
The FastAPI example in `examples/fastapi_notification_webhook.py` shows the complete GET challenge and POST notification flow.
|
||||
|
||||
## Code Generation
|
||||
|
||||
The project uses a dedicated code generation environment because the main runtime is currently on Python 3.14 while `datamodel-code-generator` is run from the separate codegen environment.
|
||||
|
||||
Generate all API model packages from the project root:
|
||||
|
||||
```powershell
|
||||
& .\.venv\Scripts\python.exe .\scripts\generate_clients.py
|
||||
```
|
||||
|
||||
To generate only one API package:
|
||||
Generate only one API package:
|
||||
|
||||
```powershell
|
||||
& .\.venv\Scripts\python.exe .\scripts\generate_clients.py --api notification
|
||||
```
|
||||
|
||||
This regenerates Pydantic v2 models into `ebay_client/generated/<api>/models.py`.
|
||||
Generated output is written to `ebay_client/generated/<api>/models.py`.
|
||||
|
||||
## Webhook Helpers
|
||||
## Validation
|
||||
|
||||
The Notification package also includes framework-agnostic webhook utilities for:
|
||||
Run the test suite from the project root:
|
||||
|
||||
- responding to eBay challenge requests
|
||||
- parsing and validating the `X-EBAY-SIGNATURE` header
|
||||
- verifying signed notification payloads against the cached public key
|
||||
- turning a verified notification body into a normalized `WebhookEventEnvelope`
|
||||
|
||||
A concrete FastAPI integration example is available in `examples/fastapi_notification_webhook.py`.
|
||||
```powershell
|
||||
& .\.venv\Scripts\python.exe -m pytest
|
||||
```
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ class ApiTransport:
|
|||
filtered_params = None
|
||||
if params is not None:
|
||||
filtered_params = {key: value for key, value in params.items() if value is not None}
|
||||
if path.startswith(("http://", "https://")):
|
||||
url = path
|
||||
else:
|
||||
url = f"{self.base_url}{path}"
|
||||
try:
|
||||
with httpx.Client(timeout=self.timeout_seconds) as client:
|
||||
|
|
|
|||
|
|
@ -1,22 +1,77 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ebay_client.core.http.transport import ApiTransport
|
||||
from ebay_client.generated.fulfillment.models import (
|
||||
AcceptPaymentDisputeRequest,
|
||||
AddEvidencePaymentDisputeRequest,
|
||||
AddEvidencePaymentDisputeResponse,
|
||||
ContestPaymentDisputeRequest,
|
||||
DisputeSummaryResponse,
|
||||
FileEvidence,
|
||||
IssueRefundRequest,
|
||||
Order,
|
||||
OrderSearchPagedCollection,
|
||||
PaymentDispute,
|
||||
PaymentDisputeActivityHistory,
|
||||
Refund,
|
||||
ShippingFulfillment,
|
||||
ShippingFulfillmentDetails,
|
||||
ShippingFulfillmentPagedCollection,
|
||||
UpdateEvidencePaymentDisputeRequest,
|
||||
)
|
||||
|
||||
FULFILLMENT_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.fulfillment"
|
||||
FULFILLMENT_READ_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.fulfillment.readonly"
|
||||
FULFILLMENT_READ_SCOPE_OPTIONS = [[FULFILLMENT_READ_SCOPE], [FULFILLMENT_SCOPE]]
|
||||
FINANCES_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.finances"
|
||||
PAYMENT_DISPUTE_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.payment.dispute"
|
||||
PAYMENT_DISPUTE_BASE_URL = "https://apiz.ebay.com/sell/fulfillment/v1"
|
||||
|
||||
|
||||
class CreatedShippingFulfillment(BaseModel):
|
||||
location: str | None = None
|
||||
fulfillment_id: str | None = None
|
||||
|
||||
|
||||
class EvidenceFileDownload(BaseModel):
|
||||
content: bytes
|
||||
content_disposition: str | None = None
|
||||
file_name: str | None = None
|
||||
|
||||
|
||||
def extract_fulfillment_id(location: str | None) -> str | None:
|
||||
if not location:
|
||||
return None
|
||||
|
||||
path = urlparse(location).path.rstrip("/")
|
||||
if not path:
|
||||
return None
|
||||
|
||||
_, _, resource_id = path.rpartition("/")
|
||||
return resource_id or None
|
||||
|
||||
|
||||
def _extract_file_name(content_disposition: str | None) -> str | None:
|
||||
if not content_disposition or "filename=" not in content_disposition:
|
||||
return None
|
||||
|
||||
value = content_disposition.split("filename=", 1)[1].strip()
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
return value[1:-1]
|
||||
return value
|
||||
|
||||
|
||||
class FulfillmentClient:
|
||||
def __init__(self, transport: ApiTransport) -> None:
|
||||
self.transport = transport
|
||||
|
||||
def _dispute_path(self, path: str) -> str:
|
||||
return f"{PAYMENT_DISPUTE_BASE_URL}{path}"
|
||||
|
||||
def get_order(self, order_id: str) -> Order:
|
||||
return self.transport.request_model(
|
||||
Order,
|
||||
|
|
@ -34,6 +89,16 @@ class FulfillmentClient:
|
|||
params={"limit": limit, "offset": offset},
|
||||
)
|
||||
|
||||
def issue_refund(self, order_id: str, payload: IssueRefundRequest) -> Refund:
|
||||
return self.transport.request_model(
|
||||
Refund,
|
||||
"POST",
|
||||
f"/sell/fulfillment/v1/order/{order_id}/issue_refund",
|
||||
scopes=[FINANCES_SCOPE],
|
||||
headers={"Content-Type": "application/json"},
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def get_shipping_fulfillments(self, order_id: str) -> ShippingFulfillmentPagedCollection:
|
||||
return self.transport.request_model(
|
||||
ShippingFulfillmentPagedCollection,
|
||||
|
|
@ -49,3 +114,153 @@ class FulfillmentClient:
|
|||
f"/sell/fulfillment/v1/order/{order_id}/shipping_fulfillment/{fulfillment_id}",
|
||||
scope_options=FULFILLMENT_READ_SCOPE_OPTIONS,
|
||||
)
|
||||
|
||||
def create_shipping_fulfillment(
|
||||
self,
|
||||
order_id: str,
|
||||
payload: ShippingFulfillmentDetails,
|
||||
) -> CreatedShippingFulfillment:
|
||||
response = self.transport.request(
|
||||
"POST",
|
||||
f"/sell/fulfillment/v1/order/{order_id}/shipping_fulfillment",
|
||||
scopes=[FULFILLMENT_SCOPE],
|
||||
headers={"Content-Type": "application/json"},
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
location = response.headers.get("Location")
|
||||
return CreatedShippingFulfillment(location=location, fulfillment_id=extract_fulfillment_id(location))
|
||||
|
||||
def get_payment_dispute(self, payment_dispute_id: str) -> PaymentDispute:
|
||||
return self.transport.request_model(
|
||||
PaymentDispute,
|
||||
"GET",
|
||||
self._dispute_path(f"/payment_dispute/{payment_dispute_id}"),
|
||||
scopes=[PAYMENT_DISPUTE_SCOPE],
|
||||
)
|
||||
|
||||
def fetch_evidence_content(
|
||||
self,
|
||||
payment_dispute_id: str,
|
||||
*,
|
||||
evidence_id: str,
|
||||
file_id: str,
|
||||
) -> EvidenceFileDownload:
|
||||
response = self.transport.request(
|
||||
"GET",
|
||||
self._dispute_path(f"/payment_dispute/{payment_dispute_id}/fetch_evidence_content"),
|
||||
scopes=[PAYMENT_DISPUTE_SCOPE],
|
||||
params={"evidence_id": evidence_id, "file_id": file_id},
|
||||
)
|
||||
content_disposition = response.headers.get("content-disposition")
|
||||
return EvidenceFileDownload(
|
||||
content=response.content,
|
||||
content_disposition=content_disposition,
|
||||
file_name=_extract_file_name(content_disposition),
|
||||
)
|
||||
|
||||
def get_payment_dispute_activities(self, payment_dispute_id: str) -> PaymentDisputeActivityHistory:
|
||||
return self.transport.request_model(
|
||||
PaymentDisputeActivityHistory,
|
||||
"GET",
|
||||
self._dispute_path(f"/payment_dispute/{payment_dispute_id}/activity"),
|
||||
scopes=[PAYMENT_DISPUTE_SCOPE],
|
||||
)
|
||||
|
||||
def get_payment_dispute_summaries(
|
||||
self,
|
||||
*,
|
||||
order_id: str | None = None,
|
||||
buyer_username: str | None = None,
|
||||
open_date_from: str | None = None,
|
||||
open_date_to: str | None = None,
|
||||
payment_dispute_status: list[str] | None = None,
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
) -> DisputeSummaryResponse:
|
||||
params: dict[str, object] = {
|
||||
"order_id": order_id,
|
||||
"buyer_username": buyer_username,
|
||||
"open_date_from": open_date_from,
|
||||
"open_date_to": open_date_to,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
if payment_dispute_status is not None:
|
||||
params["payment_dispute_status"] = payment_dispute_status
|
||||
|
||||
return self.transport.request_model(
|
||||
DisputeSummaryResponse,
|
||||
"GET",
|
||||
self._dispute_path("/payment_dispute_summary"),
|
||||
scopes=[PAYMENT_DISPUTE_SCOPE],
|
||||
params=params,
|
||||
)
|
||||
|
||||
def contest_payment_dispute(
|
||||
self,
|
||||
payment_dispute_id: str,
|
||||
payload: ContestPaymentDisputeRequest,
|
||||
) -> None:
|
||||
self.transport.request_json(
|
||||
"POST",
|
||||
self._dispute_path(f"/payment_dispute/{payment_dispute_id}/contest"),
|
||||
scopes=[PAYMENT_DISPUTE_SCOPE],
|
||||
headers={"Content-Type": "application/json"},
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def accept_payment_dispute(
|
||||
self,
|
||||
payment_dispute_id: str,
|
||||
payload: AcceptPaymentDisputeRequest,
|
||||
) -> None:
|
||||
self.transport.request_json(
|
||||
"POST",
|
||||
self._dispute_path(f"/payment_dispute/{payment_dispute_id}/accept"),
|
||||
scopes=[PAYMENT_DISPUTE_SCOPE],
|
||||
headers={"Content-Type": "application/json"},
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def upload_evidence_file(
|
||||
self,
|
||||
payment_dispute_id: str,
|
||||
*,
|
||||
file_name: str,
|
||||
content: bytes,
|
||||
content_type: str = "image/jpeg",
|
||||
) -> FileEvidence:
|
||||
return self.transport.request_model(
|
||||
FileEvidence,
|
||||
"POST",
|
||||
self._dispute_path(f"/payment_dispute/{payment_dispute_id}/upload_evidence_file"),
|
||||
scopes=[PAYMENT_DISPUTE_SCOPE],
|
||||
files={"file": (file_name, content, content_type)},
|
||||
)
|
||||
|
||||
def add_evidence(
|
||||
self,
|
||||
payment_dispute_id: str,
|
||||
payload: AddEvidencePaymentDisputeRequest,
|
||||
) -> AddEvidencePaymentDisputeResponse:
|
||||
return self.transport.request_model(
|
||||
AddEvidencePaymentDisputeResponse,
|
||||
"POST",
|
||||
self._dispute_path(f"/payment_dispute/{payment_dispute_id}/add_evidence"),
|
||||
scopes=[PAYMENT_DISPUTE_SCOPE],
|
||||
headers={"Content-Type": "application/json"},
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def update_evidence(
|
||||
self,
|
||||
payment_dispute_id: str,
|
||||
payload: UpdateEvidencePaymentDisputeRequest,
|
||||
) -> None:
|
||||
self.transport.request_json(
|
||||
"POST",
|
||||
self._dispute_path(f"/payment_dispute/{payment_dispute_id}/update_evidence"),
|
||||
scopes=[PAYMENT_DISPUTE_SCOPE],
|
||||
headers={"Content-Type": "application/json"},
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,10 +2,40 @@ from __future__ import annotations
|
|||
|
||||
from ebay_client.core.http.transport import ApiTransport
|
||||
from ebay_client.generated.inventory.models import (
|
||||
BaseResponse,
|
||||
BulkEbayOfferDetailsWithKeys,
|
||||
BulkGetInventoryItem,
|
||||
BulkGetInventoryItemResponse,
|
||||
BulkInventoryItem,
|
||||
BulkInventoryItemResponse,
|
||||
BulkMigrateListing,
|
||||
BulkMigrateListingResponse,
|
||||
BulkOffer,
|
||||
BulkOfferResponse,
|
||||
BulkPriceQuantity,
|
||||
BulkPriceQuantityResponse,
|
||||
BulkPublishResponse,
|
||||
Compatibility,
|
||||
EbayOfferDetailsWithId,
|
||||
EbayOfferDetailsWithKeys,
|
||||
FeesSummaryResponse,
|
||||
InventoryItem,
|
||||
InventoryItemGroup,
|
||||
InventoryLocation,
|
||||
InventoryLocationFull,
|
||||
InventoryLocationResponse,
|
||||
InventoryItemWithSkuLocaleGroupid,
|
||||
InventoryItems,
|
||||
LocationMapping,
|
||||
LocationResponse,
|
||||
OfferKeysWithId,
|
||||
OfferResponse,
|
||||
OfferResponseWithListingId,
|
||||
Offers,
|
||||
PublishByInventoryItemGroupRequest,
|
||||
PublishResponse,
|
||||
WithdrawByInventoryItemGroupRequest,
|
||||
WithdrawResponse,
|
||||
)
|
||||
|
||||
INVENTORY_SCOPE = "https://api.ebay.com/oauth/api_scope/sell.inventory"
|
||||
|
|
@ -17,6 +47,12 @@ class InventoryClient:
|
|||
def __init__(self, transport: ApiTransport) -> None:
|
||||
self.transport = transport
|
||||
|
||||
def _write_headers(self, *, content_language: str | None = None) -> dict[str, str]:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if content_language is not None:
|
||||
headers["Content-Language"] = content_language
|
||||
return headers
|
||||
|
||||
def get_inventory_item(self, sku: str) -> InventoryItemWithSkuLocaleGroupid:
|
||||
return self.transport.request_model(
|
||||
InventoryItemWithSkuLocaleGroupid,
|
||||
|
|
@ -25,6 +61,44 @@ class InventoryClient:
|
|||
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
|
||||
)
|
||||
|
||||
def create_or_replace_inventory_item(
|
||||
self,
|
||||
sku: str,
|
||||
payload: InventoryItem,
|
||||
*,
|
||||
content_language: str,
|
||||
) -> BaseResponse:
|
||||
return self.transport.request_model(
|
||||
BaseResponse,
|
||||
"PUT",
|
||||
f"/sell/inventory/v1/inventory_item/{sku}",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
headers=self._write_headers(content_language=content_language),
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def bulk_create_or_replace_inventory_item(
|
||||
self,
|
||||
payload: BulkInventoryItem,
|
||||
*,
|
||||
content_language: str,
|
||||
) -> BulkInventoryItemResponse:
|
||||
return self.transport.request_model(
|
||||
BulkInventoryItemResponse,
|
||||
"POST",
|
||||
"/sell/inventory/v1/bulk_create_or_replace_inventory_item",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
headers=self._write_headers(content_language=content_language),
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def delete_inventory_item(self, sku: str) -> None:
|
||||
self.transport.request_json(
|
||||
"DELETE",
|
||||
f"/sell/inventory/v1/inventory_item/{sku}",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
)
|
||||
|
||||
def get_inventory_items(self, *, limit: int | None = None, offset: int | None = None) -> InventoryItems:
|
||||
return self.transport.request_model(
|
||||
InventoryItems,
|
||||
|
|
@ -34,6 +108,88 @@ class InventoryClient:
|
|||
params={"limit": limit, "offset": offset},
|
||||
)
|
||||
|
||||
def bulk_get_inventory_item(self, payload: BulkGetInventoryItem) -> BulkGetInventoryItemResponse:
|
||||
return self.transport.request_model(
|
||||
BulkGetInventoryItemResponse,
|
||||
"POST",
|
||||
"/sell/inventory/v1/bulk_get_inventory_item",
|
||||
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
|
||||
headers={"Content-Type": "application/json"},
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def bulk_update_price_quantity(self, payload: BulkPriceQuantity) -> BulkPriceQuantityResponse:
|
||||
return self.transport.request_model(
|
||||
BulkPriceQuantityResponse,
|
||||
"POST",
|
||||
"/sell/inventory/v1/bulk_update_price_quantity",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
headers={"Content-Type": "application/json"},
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def get_product_compatibility(self, sku: str) -> Compatibility:
|
||||
return self.transport.request_model(
|
||||
Compatibility,
|
||||
"GET",
|
||||
f"/sell/inventory/v1/inventory_item/{sku}/product_compatibility",
|
||||
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
|
||||
)
|
||||
|
||||
def create_or_replace_product_compatibility(
|
||||
self,
|
||||
sku: str,
|
||||
payload: Compatibility,
|
||||
*,
|
||||
content_language: str,
|
||||
) -> BaseResponse:
|
||||
return self.transport.request_model(
|
||||
BaseResponse,
|
||||
"PUT",
|
||||
f"/sell/inventory/v1/inventory_item/{sku}/product_compatibility",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
headers=self._write_headers(content_language=content_language),
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def delete_product_compatibility(self, sku: str) -> None:
|
||||
self.transport.request_json(
|
||||
"DELETE",
|
||||
f"/sell/inventory/v1/inventory_item/{sku}/product_compatibility",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
)
|
||||
|
||||
def get_inventory_item_group(self, inventory_item_group_key: str) -> InventoryItemGroup:
|
||||
return self.transport.request_model(
|
||||
InventoryItemGroup,
|
||||
"GET",
|
||||
f"/sell/inventory/v1/inventory_item_group/{inventory_item_group_key}",
|
||||
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
|
||||
)
|
||||
|
||||
def create_or_replace_inventory_item_group(
|
||||
self,
|
||||
inventory_item_group_key: str,
|
||||
payload: InventoryItemGroup,
|
||||
*,
|
||||
content_language: str,
|
||||
) -> BaseResponse:
|
||||
return self.transport.request_model(
|
||||
BaseResponse,
|
||||
"PUT",
|
||||
f"/sell/inventory/v1/inventory_item_group/{inventory_item_group_key}",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
headers=self._write_headers(content_language=content_language),
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def delete_inventory_item_group(self, inventory_item_group_key: str) -> None:
|
||||
self.transport.request_json(
|
||||
"DELETE",
|
||||
f"/sell/inventory/v1/inventory_item_group/{inventory_item_group_key}",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
)
|
||||
|
||||
def get_offer(self, offer_id: str) -> OfferResponseWithListingId:
|
||||
return self.transport.request_model(
|
||||
OfferResponseWithListingId,
|
||||
|
|
@ -42,6 +198,59 @@ class InventoryClient:
|
|||
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
|
||||
)
|
||||
|
||||
def create_offer(
|
||||
self,
|
||||
payload: EbayOfferDetailsWithKeys,
|
||||
*,
|
||||
content_language: str,
|
||||
) -> OfferResponse:
|
||||
return self.transport.request_model(
|
||||
OfferResponse,
|
||||
"POST",
|
||||
"/sell/inventory/v1/offer",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
headers=self._write_headers(content_language=content_language),
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def bulk_create_offer(
|
||||
self,
|
||||
payload: BulkEbayOfferDetailsWithKeys,
|
||||
*,
|
||||
content_language: str,
|
||||
) -> BulkOfferResponse:
|
||||
return self.transport.request_model(
|
||||
BulkOfferResponse,
|
||||
"POST",
|
||||
"/sell/inventory/v1/bulk_create_offer",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
headers=self._write_headers(content_language=content_language),
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def update_offer(
|
||||
self,
|
||||
offer_id: str,
|
||||
payload: EbayOfferDetailsWithId,
|
||||
*,
|
||||
content_language: str,
|
||||
) -> OfferResponse:
|
||||
return self.transport.request_model(
|
||||
OfferResponse,
|
||||
"PUT",
|
||||
f"/sell/inventory/v1/offer/{offer_id}",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
headers=self._write_headers(content_language=content_language),
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def delete_offer(self, offer_id: str) -> None:
|
||||
self.transport.request_json(
|
||||
"DELETE",
|
||||
f"/sell/inventory/v1/offer/{offer_id}",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
)
|
||||
|
||||
def get_offers(self, *, limit: int | None = None, offset: int | None = None, sku: str | None = None) -> Offers:
|
||||
return self.transport.request_model(
|
||||
Offers,
|
||||
|
|
@ -50,3 +259,172 @@ class InventoryClient:
|
|||
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
|
||||
params={"limit": limit, "offset": offset, "sku": sku},
|
||||
)
|
||||
|
||||
def get_listing_fees(self, payload: OfferKeysWithId | None = None) -> FeesSummaryResponse:
|
||||
return self.transport.request_model(
|
||||
FeesSummaryResponse,
|
||||
"POST",
|
||||
"/sell/inventory/v1/offer/get_listing_fees",
|
||||
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
|
||||
headers={"Content-Type": "application/json"},
|
||||
json_body=None if payload is None else payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def publish_offer(self, offer_id: str) -> PublishResponse:
|
||||
return self.transport.request_model(
|
||||
PublishResponse,
|
||||
"POST",
|
||||
f"/sell/inventory/v1/offer/{offer_id}/publish",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
)
|
||||
|
||||
def bulk_publish_offer(self, payload: BulkOffer) -> BulkPublishResponse:
|
||||
return self.transport.request_model(
|
||||
BulkPublishResponse,
|
||||
"POST",
|
||||
"/sell/inventory/v1/bulk_publish_offer",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
headers={"Content-Type": "application/json"},
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def publish_offer_by_inventory_item_group(
|
||||
self,
|
||||
payload: PublishByInventoryItemGroupRequest,
|
||||
) -> PublishResponse:
|
||||
return self.transport.request_model(
|
||||
PublishResponse,
|
||||
"POST",
|
||||
"/sell/inventory/v1/offer/publish_by_inventory_item_group",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
headers={"Content-Type": "application/json"},
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def withdraw_offer(self, offer_id: str) -> WithdrawResponse:
|
||||
return self.transport.request_model(
|
||||
WithdrawResponse,
|
||||
"POST",
|
||||
f"/sell/inventory/v1/offer/{offer_id}/withdraw",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
)
|
||||
|
||||
def withdraw_offer_by_inventory_item_group(
|
||||
self,
|
||||
payload: WithdrawByInventoryItemGroupRequest,
|
||||
) -> None:
|
||||
self.transport.request_json(
|
||||
"POST",
|
||||
"/sell/inventory/v1/offer/withdraw_by_inventory_item_group",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
headers={"Content-Type": "application/json"},
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def get_inventory_location(self, merchant_location_key: str) -> InventoryLocationResponse:
|
||||
return self.transport.request_model(
|
||||
InventoryLocationResponse,
|
||||
"GET",
|
||||
f"/sell/inventory/v1/location/{merchant_location_key}",
|
||||
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
|
||||
)
|
||||
|
||||
def create_inventory_location(
|
||||
self,
|
||||
merchant_location_key: str,
|
||||
payload: InventoryLocationFull,
|
||||
) -> None:
|
||||
self.transport.request_json(
|
||||
"POST",
|
||||
f"/sell/inventory/v1/location/{merchant_location_key}",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
headers={"Content-Type": "application/json"},
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def delete_inventory_location(self, merchant_location_key: str) -> None:
|
||||
self.transport.request_json(
|
||||
"DELETE",
|
||||
f"/sell/inventory/v1/location/{merchant_location_key}",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
)
|
||||
|
||||
def disable_inventory_location(self, merchant_location_key: str) -> None:
|
||||
self.transport.request_json(
|
||||
"POST",
|
||||
f"/sell/inventory/v1/location/{merchant_location_key}/disable",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
)
|
||||
|
||||
def enable_inventory_location(self, merchant_location_key: str) -> None:
|
||||
self.transport.request_json(
|
||||
"POST",
|
||||
f"/sell/inventory/v1/location/{merchant_location_key}/enable",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
)
|
||||
|
||||
def get_inventory_locations(
|
||||
self,
|
||||
*,
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
) -> LocationResponse:
|
||||
return self.transport.request_model(
|
||||
LocationResponse,
|
||||
"GET",
|
||||
"/sell/inventory/v1/location",
|
||||
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
|
||||
params={"limit": limit, "offset": offset},
|
||||
)
|
||||
|
||||
def update_inventory_location(
|
||||
self,
|
||||
merchant_location_key: str,
|
||||
payload: InventoryLocation,
|
||||
) -> None:
|
||||
self.transport.request_json(
|
||||
"POST",
|
||||
f"/sell/inventory/v1/location/{merchant_location_key}/update_location_details",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
headers={"Content-Type": "application/json"},
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def bulk_migrate_listing(self, payload: BulkMigrateListing) -> BulkMigrateListingResponse:
|
||||
return self.transport.request_model(
|
||||
BulkMigrateListingResponse,
|
||||
"POST",
|
||||
"/sell/inventory/v1/bulk_migrate_listing",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
headers={"Content-Type": "application/json"},
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def get_sku_location_mapping(self, listing_id: str, sku: str) -> LocationMapping:
|
||||
return self.transport.request_model(
|
||||
LocationMapping,
|
||||
"GET",
|
||||
f"/sell/inventory/v1/listing/{listing_id}/sku/{sku}/locations",
|
||||
scope_options=INVENTORY_READ_SCOPE_OPTIONS,
|
||||
)
|
||||
|
||||
def create_or_replace_sku_location_mapping(
|
||||
self,
|
||||
listing_id: str,
|
||||
sku: str,
|
||||
payload: LocationMapping,
|
||||
) -> None:
|
||||
self.transport.request_json(
|
||||
"PUT",
|
||||
f"/sell/inventory/v1/listing/{listing_id}/sku/{sku}/locations",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
headers={"Content-Type": "application/json"},
|
||||
json_body=payload.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
def delete_sku_location_mapping(self, listing_id: str, sku: str) -> None:
|
||||
self.transport.request_json(
|
||||
"DELETE",
|
||||
f"/sell/inventory/v1/listing/{listing_id}/sku/{sku}/locations",
|
||||
scopes=[INVENTORY_SCOPE],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from ebay_client.account.client import AccountClient
|
|||
from ebay_client.core.auth.models import OAuthToken
|
||||
from ebay_client.core.http.transport import ApiTransport
|
||||
from ebay_client.feed.client import CreatedFeedResource, FeedClient, FeedFileDownload
|
||||
from ebay_client.fulfillment.client import FulfillmentClient
|
||||
from ebay_client.fulfillment.client import CreatedShippingFulfillment, EvidenceFileDownload, FulfillmentClient
|
||||
from ebay_client.generated.account.models import (
|
||||
FulfillmentPolicy,
|
||||
FulfillmentPolicyRequest,
|
||||
|
|
@ -26,8 +26,65 @@ from ebay_client.generated.feed.models import (
|
|||
UpdateUserScheduleRequest,
|
||||
UserScheduleResponse,
|
||||
)
|
||||
from ebay_client.generated.fulfillment.models import Order
|
||||
from ebay_client.generated.inventory.models import InventoryItemWithSkuLocaleGroupid
|
||||
from ebay_client.generated.fulfillment.models import (
|
||||
AcceptPaymentDisputeRequest,
|
||||
AddEvidencePaymentDisputeRequest,
|
||||
AddEvidencePaymentDisputeResponse,
|
||||
FileEvidence,
|
||||
IssueRefundRequest,
|
||||
Order,
|
||||
OrderLineItems,
|
||||
PaymentDispute,
|
||||
PaymentDisputeActivityHistory,
|
||||
Refund,
|
||||
ShippingFulfillmentDetails,
|
||||
LineItemReference,
|
||||
ContestPaymentDisputeRequest,
|
||||
DisputeSummaryResponse,
|
||||
UpdateEvidencePaymentDisputeRequest,
|
||||
)
|
||||
from ebay_client.generated.inventory.models import (
|
||||
Address,
|
||||
BaseResponse,
|
||||
BulkEbayOfferDetailsWithKeys,
|
||||
BulkGetInventoryItem,
|
||||
BulkGetInventoryItemResponse,
|
||||
BulkInventoryItem,
|
||||
BulkInventoryItemResponse,
|
||||
BulkMigrateListing,
|
||||
BulkMigrateListingResponse,
|
||||
BulkOffer,
|
||||
BulkOfferResponse,
|
||||
BulkPriceQuantity,
|
||||
BulkPriceQuantityResponse,
|
||||
BulkPublishResponse,
|
||||
Compatibility,
|
||||
EbayOfferDetailsWithId,
|
||||
EbayOfferDetailsWithKeys,
|
||||
FeesSummaryResponse,
|
||||
GetInventoryItem,
|
||||
InventoryItem,
|
||||
InventoryItemGroup,
|
||||
InventoryItemWithSkuLocale,
|
||||
InventoryItemWithSkuLocaleGroupid,
|
||||
InventoryLocation,
|
||||
InventoryLocationFull,
|
||||
InventoryLocationResponse,
|
||||
LocationAvailabilityDetails,
|
||||
LocationDetails,
|
||||
LocationMapping,
|
||||
LocationResponse,
|
||||
MigrateListing,
|
||||
NameValueList,
|
||||
OfferKeyWithId,
|
||||
OfferKeysWithId,
|
||||
OfferResponse,
|
||||
OfferPriceQuantity,
|
||||
PublishByInventoryItemGroupRequest,
|
||||
PublishResponse,
|
||||
WithdrawByInventoryItemGroupRequest,
|
||||
WithdrawResponse,
|
||||
)
|
||||
from ebay_client.generated.media.models import (
|
||||
CreateDocumentFromUrlRequest,
|
||||
CreateDocumentRequest,
|
||||
|
|
@ -277,6 +334,368 @@ def test_inventory_wrapper_returns_inventory_item_model(httpx_mock: HTTPXMock) -
|
|||
assert result.sku == "SKU-1"
|
||||
|
||||
|
||||
def test_inventory_wrapper_supports_core_item_offer_and_group_workflows(httpx_mock: HTTPXMock) -> None:
|
||||
httpx_mock.add_response(
|
||||
method="PUT",
|
||||
url="https://api.ebay.com/sell/inventory/v1/inventory_item/SKU-1",
|
||||
json={"warnings": []},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="DELETE",
|
||||
url="https://api.ebay.com/sell/inventory/v1/inventory_item/SKU-1",
|
||||
status_code=204,
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="PUT",
|
||||
url="https://api.ebay.com/sell/inventory/v1/inventory_item_group/GROUP-1",
|
||||
json={"warnings": []},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="GET",
|
||||
url="https://api.ebay.com/sell/inventory/v1/inventory_item_group/GROUP-1",
|
||||
json={"inventoryItemGroupKey": "GROUP-1", "variantSKUs": ["SKU-1"]},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/inventory/v1/offer",
|
||||
json={"offerId": "OFFER-1"},
|
||||
status_code=201,
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="PUT",
|
||||
url="https://api.ebay.com/sell/inventory/v1/offer/OFFER-1",
|
||||
json={"warnings": []},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/inventory/v1/offer/OFFER-1/publish",
|
||||
json={"offerId": "OFFER-1", "listingId": "ITEM-1"},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/inventory/v1/offer/OFFER-1/withdraw",
|
||||
json={"offerId": "OFFER-1", "listingId": "ITEM-1"},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/inventory/v1/offer/publish_by_inventory_item_group",
|
||||
json={"listingId": "ITEM-2"},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/inventory/v1/offer/withdraw_by_inventory_item_group",
|
||||
status_code=204,
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/inventory/v1/offer/get_listing_fees",
|
||||
json={"feeSummaries": []},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="DELETE",
|
||||
url="https://api.ebay.com/sell/inventory/v1/offer/OFFER-1",
|
||||
status_code=204,
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="DELETE",
|
||||
url="https://api.ebay.com/sell/inventory/v1/inventory_item_group/GROUP-1",
|
||||
status_code=204,
|
||||
)
|
||||
|
||||
client = InventoryClient(build_transport())
|
||||
item_result = client.create_or_replace_inventory_item(
|
||||
"SKU-1",
|
||||
InventoryItem(condition="NEW"),
|
||||
content_language="en-US",
|
||||
)
|
||||
client.delete_inventory_item("SKU-1")
|
||||
group_result = client.create_or_replace_inventory_item_group(
|
||||
"GROUP-1",
|
||||
InventoryItemGroup(variantSKUs=["SKU-1"]),
|
||||
content_language="en-US",
|
||||
)
|
||||
group = client.get_inventory_item_group("GROUP-1")
|
||||
offer = client.create_offer(
|
||||
EbayOfferDetailsWithKeys(sku="SKU-1", marketplaceId="EBAY_US", format="FIXED_PRICE"),
|
||||
content_language="en-US",
|
||||
)
|
||||
updated_offer = client.update_offer(
|
||||
"OFFER-1",
|
||||
EbayOfferDetailsWithId(availableQuantity=2),
|
||||
content_language="en-US",
|
||||
)
|
||||
published_offer = client.publish_offer("OFFER-1")
|
||||
withdrawn_offer = client.withdraw_offer("OFFER-1")
|
||||
group_publish = client.publish_offer_by_inventory_item_group(
|
||||
PublishByInventoryItemGroupRequest(inventoryItemGroupKey="GROUP-1", marketplaceId="EBAY_US")
|
||||
)
|
||||
client.withdraw_offer_by_inventory_item_group(
|
||||
WithdrawByInventoryItemGroupRequest(inventoryItemGroupKey="GROUP-1", marketplaceId="EBAY_US")
|
||||
)
|
||||
fees = client.get_listing_fees(OfferKeysWithId(offers=[OfferKeyWithId(offerId="OFFER-1")]))
|
||||
client.delete_offer("OFFER-1")
|
||||
client.delete_inventory_item_group("GROUP-1")
|
||||
|
||||
assert isinstance(item_result, BaseResponse)
|
||||
assert isinstance(group_result, BaseResponse)
|
||||
assert isinstance(group, InventoryItemGroup)
|
||||
assert group.inventoryItemGroupKey == "GROUP-1"
|
||||
assert isinstance(offer, OfferResponse)
|
||||
assert offer.offerId == "OFFER-1"
|
||||
assert isinstance(updated_offer, OfferResponse)
|
||||
assert isinstance(published_offer, PublishResponse)
|
||||
assert published_offer.listingId == "ITEM-1"
|
||||
assert isinstance(withdrawn_offer, WithdrawResponse)
|
||||
assert withdrawn_offer.listingId == "ITEM-1"
|
||||
assert isinstance(group_publish, PublishResponse)
|
||||
assert group_publish.listingId == "ITEM-2"
|
||||
assert isinstance(fees, FeesSummaryResponse)
|
||||
|
||||
item_request = httpx_mock.get_requests()[0]
|
||||
assert item_request.headers["Content-Language"] == "en-US"
|
||||
item_body = json.loads(item_request.content.decode("utf-8"))
|
||||
assert item_body["condition"] == "NEW"
|
||||
|
||||
offer_request = httpx_mock.get_requests()[4]
|
||||
assert offer_request.headers["Content-Language"] == "en-US"
|
||||
offer_body = json.loads(offer_request.content.decode("utf-8"))
|
||||
assert offer_body["sku"] == "SKU-1"
|
||||
assert offer_body["marketplaceId"] == "EBAY_US"
|
||||
|
||||
update_request = httpx_mock.get_requests()[5]
|
||||
update_body = json.loads(update_request.content.decode("utf-8"))
|
||||
assert update_body["availableQuantity"] == 2
|
||||
|
||||
group_publish_request = httpx_mock.get_requests()[8]
|
||||
assert group_publish_request.headers["Content-Type"] == "application/json"
|
||||
group_publish_body = json.loads(group_publish_request.content.decode("utf-8"))
|
||||
assert group_publish_body["inventoryItemGroupKey"] == "GROUP-1"
|
||||
|
||||
fees_request = httpx_mock.get_requests()[10]
|
||||
fees_body = json.loads(fees_request.content.decode("utf-8"))
|
||||
assert fees_body["offers"] == [{"offerId": "OFFER-1"}]
|
||||
|
||||
|
||||
def test_inventory_wrapper_supports_location_management(httpx_mock: HTTPXMock) -> None:
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/inventory/v1/location/LOC-1",
|
||||
status_code=204,
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="GET",
|
||||
url="https://api.ebay.com/sell/inventory/v1/location/LOC-1",
|
||||
json={"merchantLocationKey": "LOC-1", "name": "Main warehouse"},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/inventory/v1/location/LOC-1/update_location_details",
|
||||
status_code=204,
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/inventory/v1/location/LOC-1/disable",
|
||||
json={},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/inventory/v1/location/LOC-1/enable",
|
||||
json={},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="GET",
|
||||
url="https://api.ebay.com/sell/inventory/v1/location?limit=25&offset=0",
|
||||
json={"locations": [{"merchantLocationKey": "LOC-1"}], "total": 1},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="DELETE",
|
||||
url="https://api.ebay.com/sell/inventory/v1/location/LOC-1",
|
||||
status_code=204,
|
||||
)
|
||||
|
||||
client = InventoryClient(build_transport())
|
||||
client.create_inventory_location(
|
||||
"LOC-1",
|
||||
InventoryLocationFull(location=LocationDetails(address=Address(country="US", postalCode="10001"))),
|
||||
)
|
||||
location = client.get_inventory_location("LOC-1")
|
||||
client.update_inventory_location("LOC-1", InventoryLocation(name="Main warehouse"))
|
||||
client.disable_inventory_location("LOC-1")
|
||||
client.enable_inventory_location("LOC-1")
|
||||
locations = client.get_inventory_locations(limit=25, offset=0)
|
||||
client.delete_inventory_location("LOC-1")
|
||||
|
||||
assert isinstance(location, InventoryLocationResponse)
|
||||
assert location.merchantLocationKey == "LOC-1"
|
||||
assert isinstance(locations, LocationResponse)
|
||||
assert locations.total == 1
|
||||
|
||||
create_request = httpx_mock.get_requests()[0]
|
||||
create_body = json.loads(create_request.content.decode("utf-8"))
|
||||
assert create_body["location"]["address"]["country"] == "US"
|
||||
|
||||
update_request = httpx_mock.get_requests()[2]
|
||||
update_body = json.loads(update_request.content.decode("utf-8"))
|
||||
assert update_body["name"] == "Main warehouse"
|
||||
|
||||
|
||||
def test_inventory_wrapper_supports_bulk_and_auxiliary_endpoints(httpx_mock: HTTPXMock) -> None:
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/inventory/v1/bulk_create_or_replace_inventory_item",
|
||||
json={"responses": [{"sku": "SKU-2", "statusCode": 200}]},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/inventory/v1/bulk_get_inventory_item",
|
||||
json={"responses": [{"sku": "SKU-2"}]},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/inventory/v1/bulk_update_price_quantity",
|
||||
json={"responses": [{"offerId": "OFFER-2", "sku": "SKU-2", "statusCode": 200}]},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="GET",
|
||||
url="https://api.ebay.com/sell/inventory/v1/inventory_item/SKU-2/product_compatibility",
|
||||
json={"sku": "SKU-2", "compatibleProducts": [{"compatibilityProperties": [{"name": "make", "value": "Toyota"}]}]},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="PUT",
|
||||
url="https://api.ebay.com/sell/inventory/v1/inventory_item/SKU-2/product_compatibility",
|
||||
json={"warnings": []},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="DELETE",
|
||||
url="https://api.ebay.com/sell/inventory/v1/inventory_item/SKU-2/product_compatibility",
|
||||
status_code=204,
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/inventory/v1/bulk_migrate_listing",
|
||||
json={"responses": [{"listingId": "ITEM-1", "statusCode": 200}]},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="GET",
|
||||
url="https://api.ebay.com/sell/inventory/v1/listing/ITEM-1/sku/SKU-2/locations",
|
||||
json={"locations": [{"merchantLocationKey": "FC-1"}]},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="PUT",
|
||||
url="https://api.ebay.com/sell/inventory/v1/listing/ITEM-1/sku/SKU-2/locations",
|
||||
status_code=204,
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="DELETE",
|
||||
url="https://api.ebay.com/sell/inventory/v1/listing/ITEM-1/sku/SKU-2/locations",
|
||||
status_code=204,
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/inventory/v1/bulk_create_offer",
|
||||
json={"responses": [{"offerId": "OFFER-2", "sku": "SKU-2", "marketplaceId": "EBAY_US"}]},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/inventory/v1/bulk_publish_offer",
|
||||
json={"responses": [{"offerId": "OFFER-2", "listingId": "ITEM-2", "statusCode": 200}]},
|
||||
)
|
||||
|
||||
client = InventoryClient(build_transport())
|
||||
bulk_item_response = client.bulk_create_or_replace_inventory_item(
|
||||
BulkInventoryItem(
|
||||
requests=[InventoryItemWithSkuLocale(sku="SKU-2", locale="en_US", condition="NEW")]
|
||||
),
|
||||
content_language="en-US",
|
||||
)
|
||||
bulk_get_response = client.bulk_get_inventory_item(
|
||||
BulkGetInventoryItem(requests=[GetInventoryItem(sku="SKU-2")])
|
||||
)
|
||||
bulk_price_response = client.bulk_update_price_quantity(
|
||||
BulkPriceQuantity(
|
||||
requests=[
|
||||
{
|
||||
"sku": "SKU-2",
|
||||
"shipToLocationAvailability": {"quantity": 5},
|
||||
"offers": [{"offerId": "OFFER-2", "availableQuantity": 2}],
|
||||
}
|
||||
]
|
||||
)
|
||||
)
|
||||
compatibility = client.get_product_compatibility("SKU-2")
|
||||
compatibility_result = client.create_or_replace_product_compatibility(
|
||||
"SKU-2",
|
||||
Compatibility(compatibleProducts=[{"compatibilityProperties": [{"name": "make", "value": "Toyota"}]}]),
|
||||
content_language="en-US",
|
||||
)
|
||||
client.delete_product_compatibility("SKU-2")
|
||||
migrate_response = client.bulk_migrate_listing(BulkMigrateListing(requests=[MigrateListing(listingId="ITEM-1")]))
|
||||
location_mapping = client.get_sku_location_mapping("ITEM-1", "SKU-2")
|
||||
client.create_or_replace_sku_location_mapping(
|
||||
"ITEM-1",
|
||||
"SKU-2",
|
||||
LocationMapping(locations=[LocationAvailabilityDetails(merchantLocationKey="FC-1")]),
|
||||
)
|
||||
client.delete_sku_location_mapping("ITEM-1", "SKU-2")
|
||||
bulk_offer_response = client.bulk_create_offer(
|
||||
BulkEbayOfferDetailsWithKeys(
|
||||
requests=[EbayOfferDetailsWithKeys(sku="SKU-2", marketplaceId="EBAY_US", format="FIXED_PRICE")]
|
||||
),
|
||||
content_language="en-US",
|
||||
)
|
||||
bulk_publish_response = client.bulk_publish_offer(
|
||||
BulkOffer(requests=[OfferKeyWithId(offerId="OFFER-2")])
|
||||
)
|
||||
|
||||
assert isinstance(bulk_item_response, BulkInventoryItemResponse)
|
||||
assert bulk_item_response.responses and bulk_item_response.responses[0].sku == "SKU-2"
|
||||
assert isinstance(bulk_get_response, BulkGetInventoryItemResponse)
|
||||
assert bulk_get_response.responses and bulk_get_response.responses[0].sku == "SKU-2"
|
||||
assert isinstance(bulk_price_response, BulkPriceQuantityResponse)
|
||||
assert bulk_price_response.responses and bulk_price_response.responses[0].offerId == "OFFER-2"
|
||||
assert isinstance(compatibility, Compatibility)
|
||||
assert compatibility.sku == "SKU-2"
|
||||
assert isinstance(compatibility_result, BaseResponse)
|
||||
assert isinstance(migrate_response, BulkMigrateListingResponse)
|
||||
assert migrate_response.responses and migrate_response.responses[0].listingId == "ITEM-1"
|
||||
assert isinstance(location_mapping, LocationMapping)
|
||||
assert location_mapping.locations and location_mapping.locations[0].merchantLocationKey == "FC-1"
|
||||
assert isinstance(bulk_offer_response, BulkOfferResponse)
|
||||
assert bulk_offer_response.responses and bulk_offer_response.responses[0].offerId == "OFFER-2"
|
||||
assert isinstance(bulk_publish_response, BulkPublishResponse)
|
||||
assert bulk_publish_response.responses and bulk_publish_response.responses[0].listingId == "ITEM-2"
|
||||
|
||||
bulk_item_request = httpx_mock.get_requests()[0]
|
||||
assert bulk_item_request.headers["Content-Language"] == "en-US"
|
||||
bulk_item_body = json.loads(bulk_item_request.content.decode("utf-8"))
|
||||
assert bulk_item_body["requests"][0]["sku"] == "SKU-2"
|
||||
|
||||
bulk_get_request = httpx_mock.get_requests()[1]
|
||||
bulk_get_body = json.loads(bulk_get_request.content.decode("utf-8"))
|
||||
assert bulk_get_body["requests"][0]["sku"] == "SKU-2"
|
||||
|
||||
bulk_price_request = httpx_mock.get_requests()[2]
|
||||
bulk_price_body = json.loads(bulk_price_request.content.decode("utf-8"))
|
||||
assert bulk_price_body["requests"][0]["sku"] == "SKU-2"
|
||||
assert bulk_price_body["requests"][0]["offers"][0]["offerId"] == "OFFER-2"
|
||||
|
||||
compatibility_request = httpx_mock.get_requests()[4]
|
||||
assert compatibility_request.headers["Content-Language"] == "en-US"
|
||||
|
||||
location_mapping_request = httpx_mock.get_requests()[8]
|
||||
location_mapping_body = json.loads(location_mapping_request.content.decode("utf-8"))
|
||||
assert location_mapping_body["locations"][0]["merchantLocationKey"] == "FC-1"
|
||||
|
||||
bulk_offer_request = httpx_mock.get_requests()[10]
|
||||
assert bulk_offer_request.headers["Content-Language"] == "en-US"
|
||||
bulk_offer_body = json.loads(bulk_offer_request.content.decode("utf-8"))
|
||||
assert bulk_offer_body["requests"][0]["sku"] == "SKU-2"
|
||||
|
||||
bulk_publish_request = httpx_mock.get_requests()[11]
|
||||
bulk_publish_body = json.loads(bulk_publish_request.content.decode("utf-8"))
|
||||
assert bulk_publish_body["requests"] == [{"offerId": "OFFER-2"}]
|
||||
|
||||
|
||||
def test_fulfillment_wrapper_returns_order_model(httpx_mock: HTTPXMock) -> None:
|
||||
httpx_mock.add_response(
|
||||
method="GET",
|
||||
|
|
@ -291,6 +710,180 @@ def test_fulfillment_wrapper_returns_order_model(httpx_mock: HTTPXMock) -> None:
|
|||
assert result.orderId == "ORDER-1"
|
||||
|
||||
|
||||
def test_fulfillment_wrapper_supports_refund_and_shipping_creation(httpx_mock: HTTPXMock) -> None:
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/fulfillment/v1/order/ORDER-1/issue_refund",
|
||||
json={"refundId": "REFUND-1", "refundStatus": "PENDING"},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://api.ebay.com/sell/fulfillment/v1/order/ORDER-1/shipping_fulfillment",
|
||||
status_code=201,
|
||||
headers={
|
||||
"Location": "https://api.ebay.com/sell/fulfillment/v1/order/ORDER-1/shipping_fulfillment/FULFILL-1"
|
||||
},
|
||||
json={},
|
||||
)
|
||||
|
||||
client = FulfillmentClient(build_transport())
|
||||
refund = client.issue_refund(
|
||||
"ORDER-1",
|
||||
IssueRefundRequest(reasonForRefund="BUYER_CANCELLED", comment="requested", orderLevelRefundAmount={"currency": "USD", "value": "10.00"}),
|
||||
)
|
||||
fulfillment = client.create_shipping_fulfillment(
|
||||
"ORDER-1",
|
||||
ShippingFulfillmentDetails(
|
||||
lineItems=[LineItemReference(lineItemId="LINE-1", quantity=1)],
|
||||
shippingCarrierCode="USPS",
|
||||
trackingNumber="TRACK123",
|
||||
),
|
||||
)
|
||||
|
||||
assert isinstance(refund, Refund)
|
||||
assert refund.refundId == "REFUND-1"
|
||||
assert isinstance(fulfillment, CreatedShippingFulfillment)
|
||||
assert fulfillment.fulfillment_id == "FULFILL-1"
|
||||
|
||||
refund_request = httpx_mock.get_requests()[0]
|
||||
refund_body = json.loads(refund_request.content.decode("utf-8"))
|
||||
assert refund_body["reasonForRefund"] == "BUYER_CANCELLED"
|
||||
assert refund_request.headers["Authorization"] == "Bearer test-token"
|
||||
|
||||
shipping_request = httpx_mock.get_requests()[1]
|
||||
shipping_body = json.loads(shipping_request.content.decode("utf-8"))
|
||||
assert shipping_body["shippingCarrierCode"] == "USPS"
|
||||
assert shipping_body["trackingNumber"] == "TRACK123"
|
||||
|
||||
|
||||
def test_fulfillment_wrapper_supports_payment_dispute_workflow(httpx_mock: HTTPXMock) -> None:
|
||||
httpx_mock.add_response(
|
||||
method="GET",
|
||||
url="https://apiz.ebay.com/sell/fulfillment/v1/payment_dispute/DISPUTE-1",
|
||||
json={"paymentDisputeId": "DISPUTE-1", "revision": 3},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="GET",
|
||||
url="https://apiz.ebay.com/sell/fulfillment/v1/payment_dispute/DISPUTE-1/activity",
|
||||
json={"activities": []},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="GET",
|
||||
url=(
|
||||
"https://apiz.ebay.com/sell/fulfillment/v1/payment_dispute_summary?buyer_username=buyer1"
|
||||
"&payment_dispute_status=OPEN&payment_dispute_status=ACTION_NEEDED&limit=25&offset=0"
|
||||
),
|
||||
json={"paymentDisputeSummaries": [], "total": 0},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://apiz.ebay.com/sell/fulfillment/v1/payment_dispute/DISPUTE-1/contest",
|
||||
status_code=204,
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://apiz.ebay.com/sell/fulfillment/v1/payment_dispute/DISPUTE-1/accept",
|
||||
status_code=204,
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://apiz.ebay.com/sell/fulfillment/v1/payment_dispute/DISPUTE-1/upload_evidence_file",
|
||||
json={"fileId": "FILE-1", "fileName": "label.jpg"},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://apiz.ebay.com/sell/fulfillment/v1/payment_dispute/DISPUTE-1/add_evidence",
|
||||
json={"evidenceId": "EVID-1"},
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="POST",
|
||||
url="https://apiz.ebay.com/sell/fulfillment/v1/payment_dispute/DISPUTE-1/update_evidence",
|
||||
status_code=204,
|
||||
)
|
||||
httpx_mock.add_response(
|
||||
method="GET",
|
||||
url="https://apiz.ebay.com/sell/fulfillment/v1/payment_dispute/DISPUTE-1/fetch_evidence_content?evidence_id=EVID-1&file_id=FILE-1",
|
||||
status_code=200,
|
||||
headers={"content-disposition": 'attachment; filename="label.jpg"'},
|
||||
content=b"binary-evidence",
|
||||
)
|
||||
|
||||
client = FulfillmentClient(build_transport())
|
||||
dispute = client.get_payment_dispute("DISPUTE-1")
|
||||
activities = client.get_payment_dispute_activities("DISPUTE-1")
|
||||
summaries = client.get_payment_dispute_summaries(
|
||||
buyer_username="buyer1",
|
||||
payment_dispute_status=["OPEN", "ACTION_NEEDED"],
|
||||
limit=25,
|
||||
offset=0,
|
||||
)
|
||||
client.contest_payment_dispute("DISPUTE-1", ContestPaymentDisputeRequest(revision=3, note="tracking attached"))
|
||||
client.accept_payment_dispute("DISPUTE-1", AcceptPaymentDisputeRequest(revision=3))
|
||||
file_evidence = client.upload_evidence_file(
|
||||
"DISPUTE-1",
|
||||
file_name="label.jpg",
|
||||
content=b"jpg-bytes",
|
||||
content_type="image/jpeg",
|
||||
)
|
||||
evidence_response = client.add_evidence(
|
||||
"DISPUTE-1",
|
||||
AddEvidencePaymentDisputeRequest(
|
||||
evidenceType="PROOF_OF_DELIVERY",
|
||||
files=[FileEvidence(fileId="FILE-1")],
|
||||
lineItems=[OrderLineItems(lineItemId="LINE-1", itemId="ITEM-1")],
|
||||
),
|
||||
)
|
||||
client.update_evidence(
|
||||
"DISPUTE-1",
|
||||
UpdateEvidencePaymentDisputeRequest(
|
||||
evidenceId="EVID-1",
|
||||
evidenceType="PROOF_OF_DELIVERY",
|
||||
files=[FileEvidence(fileId="FILE-1")],
|
||||
lineItems=[OrderLineItems(lineItemId="LINE-1", itemId="ITEM-1")],
|
||||
),
|
||||
)
|
||||
evidence_file = client.fetch_evidence_content("DISPUTE-1", evidence_id="EVID-1", file_id="FILE-1")
|
||||
|
||||
assert isinstance(dispute, PaymentDispute)
|
||||
assert isinstance(activities, PaymentDisputeActivityHistory)
|
||||
assert isinstance(summaries, DisputeSummaryResponse)
|
||||
assert summaries.total == 0
|
||||
assert isinstance(file_evidence, FileEvidence)
|
||||
assert file_evidence.fileId == "FILE-1"
|
||||
assert isinstance(evidence_response, AddEvidencePaymentDisputeResponse)
|
||||
assert evidence_response.evidenceId == "EVID-1"
|
||||
assert isinstance(evidence_file, EvidenceFileDownload)
|
||||
assert evidence_file.file_name == "label.jpg"
|
||||
assert evidence_file.content == b"binary-evidence"
|
||||
|
||||
contest_request = httpx_mock.get_requests()[3]
|
||||
assert contest_request.url.host == "apiz.ebay.com"
|
||||
contest_body = json.loads(contest_request.content.decode("utf-8"))
|
||||
assert contest_body["revision"] == 3
|
||||
|
||||
upload_request = httpx_mock.get_requests()[5]
|
||||
assert upload_request.url.host == "apiz.ebay.com"
|
||||
assert upload_request.headers["Content-Type"].startswith("multipart/form-data;")
|
||||
|
||||
|
||||
def test_fulfillment_wrapper_accepts_readonly_or_full_scope_options_for_shipping_reads(httpx_mock: HTTPXMock) -> None:
|
||||
oauth_client = RecordingOAuthClient()
|
||||
transport = ApiTransport(base_url="https://api.ebay.com", oauth_client=oauth_client)
|
||||
httpx_mock.add_response(
|
||||
method="GET",
|
||||
url="https://api.ebay.com/sell/fulfillment/v1/order/ORDER-1/shipping_fulfillment/FULL-1",
|
||||
json={"fulfillmentId": "FULL-1"},
|
||||
)
|
||||
|
||||
FulfillmentClient(transport).get_shipping_fulfillment("ORDER-1", "FULL-1")
|
||||
|
||||
assert oauth_client.last_scopes is None
|
||||
assert oauth_client.last_scope_options == [
|
||||
["https://api.ebay.com/oauth/api_scope/sell.fulfillment.readonly"],
|
||||
["https://api.ebay.com/oauth/api_scope/sell.fulfillment"],
|
||||
]
|
||||
|
||||
|
||||
def test_account_wrapper_returns_programs_model(httpx_mock: HTTPXMock) -> None:
|
||||
httpx_mock.add_response(
|
||||
method="GET",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue