feat: Add WonderQ-MiniAPP Public API documentation

- Introduced a comprehensive API contract for the WonderQ-MiniAPP, detailing endpoints for site configuration, product listings, and lead submissions.
- Defined data types for various entities including HeroSlide, Destination, Theme, CtaBanner, PublicProduct, and more.
- Specified request and response formats, including error handling guidelines.

chore: Update requirements to include python-multipart

- Added python-multipart dependency to requirements.txt for handling file uploads.

test: Implement API contract tests

- Created test suite for API contracts, validating serializers and endpoints for public products and leads.
- Included tests for destination and product serializers, ensuring correct data handling and validation.

test: Add configuration tests for OSS settings

- Implemented tests to verify that OSS settings are correctly loaded from environment variables.
This commit is contained in:
duanshuwen
2026-07-01 16:55:00 +08:00
parent 75f5a5a48b
commit a47b4b5dd0
14 changed files with 2210 additions and 104 deletions

View File

@@ -20,20 +20,60 @@ def model_dict(instance, include: dict[str, object] | None = None) -> dict:
return data
def product_dict(product) -> dict:
def _sorted_aliases(destination) -> list:
return sorted(destination.aliases or [], key=lambda item: ((item.alias or "").lower(), item.id or ""))
def _sorted_images(product) -> list:
return sorted(product.images or [], key=lambda item: (item.sortOrder, item.id or "", item.url))
def _product_images(product) -> list[dict]:
return [model_dict(image) for image in _sorted_images(product)]
def _detail_sections(product) -> list:
return encode_value(product.detailSections or [])
def admin_product_dict(product) -> dict:
return model_dict(
product,
{
"destination": model_dict(product.destination) if product.destination else None,
"images": [model_dict(image) for image in sorted(product.images, key=lambda item: item.sortOrder)],
"tags": list(product.tags or []),
"detailSections": _detail_sections(product),
"destination": destination_dict(product.destination) if product.destination else None,
"images": _product_images(product),
},
)
def destination_dict(destination, include_count: bool = False) -> dict:
data = model_dict(destination, {"aliases": [model_dict(alias) for alias in destination.aliases]})
def public_product_dict(product) -> dict:
return {
"id": product.id,
"sourceId": product.sourceId,
"title": product.title,
"subtitle": product.subtitle,
"destination": {"id": product.destination.id, "name": product.destination.name} if product.destination else None,
"priceAmount": product.priceAmount,
"priceUnit": product.priceUnit,
"tags": list(product.tags or []),
"coverImage": product.coverImage,
"summary": product.summary,
"images": _product_images(product),
"detailSections": _detail_sections(product),
"status": product.status,
}
def product_dict(product) -> dict:
return admin_product_dict(product)
def destination_dict(destination, include_count: bool = False, product_count: int | None = None) -> dict:
data = model_dict(destination, {"aliases": [model_dict(alias) for alias in _sorted_aliases(destination)]})
if include_count:
data["_count"] = {"products": len(destination.products)}
data["_count"] = {"products": product_count if product_count is not None else len(destination.products or [])}
return data