Villa Backend SDK — Design
This document explains how the SDK is structured, why it is organized this way, and how data flows from login through order construction, validation, and payment.
Goals
The SDK exists to give developers a **small, predictable surface area** for Villa Market backend operations:
1. Authenticate a shopper via **AWS Cognito**
2. Build an **order** that matches the official villaMasterSchema contract
3. **Validate** that order locally and against backend pricing APIs
4. **Pay** using the Villa payment endpoints
Each step is isolated in its own module so it can be tested, replaced, or extended independently.
High-level architecture
The SDK uses a **layered architecture** with a thin facade on top:
┌─────────────────────────────────────────────────────────────┐
│ VillaClient │
│ (facade: wires services, login(), health(), context mgr) │
└───────────────┬───────────────┬───────────────┬─────────────┘
│ │ │
┌───────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ auth │ │ orders │ │ payments │
│ CognitoAuth │ │ OrderService│ │PaymentService│
└───────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└───────────────┼───────────────┘
│
┌───────────▼───────────┐
│ validation │
│ schema + API checks │
└───────────┬───────────┘
│
┌───────────▼───────────┐
│ http │
│ requests + auth hdrs │
└───────────┬───────────┘
│
┌───────────▼───────────┐
│ config │
│ URLs, env, API paths │
└───────────────────────┘
Design principles
| Principle | What it means in this SDK |
|---|---|
| **Single responsibility** | auth/ only handles Cognito. payments/ only handles payment endpoints. |
| **Dependency injection** | Services receive HttpClient and VillaConfig — tests can substitute mocks without patching globals. |
| **Schema as contract** | Order shape comes from bundled villaMasterSchema YAML, not ad-hoc dicts. |
| **Fail fast before payment** | PrePaymentValidator runs local + remote checks before money moves. |
| **Thin facade** | VillaClient composes services; most logic lives in modules you can import directly. |
Module reference
config.py — configuration
Centralizes all runtime settings:
VILLA_BASE_URL) — default https://shop.villamarket.comVILLA_ENV) — dev or prod, switches payment pathsVILLA_COGNITO_*) — user pool, client ID, optional secretApiPaths) — every backend route is configurablePayment paths switch by environment:
| Environment | Card token | Card payment |
|---|---|---|
dev | /api/payment3devapi/cardtoken | /api/payment3devapi/cardpayment |
prod | /api/payment3/cardtoken | /api/payment3/cardpayment |
http.py — transport layer
Wraps requests and adds:
VillaApiError on HTTP/network failures/api/orders/{order_id})Nothing in http.py knows about orders or payments — it only sends HTTP.
auth/ — AWS Cognito login
User credentials
│
▼
CognitoAuthService.login()
│
▼
boto3 cognito-idp InitiateAuth (USER_PASSWORD_AUTH)
│
▼
AuthTokens (AccessToken, IdToken, RefreshToken)
│
▼
HttpClient.set_bearer_token(IdToken)
USER_PASSWORD_AUTH flowSECRET_HASH when a client secret is configuredVillaAuthErrorAuthorization: Bearer … on subsequent API callsorders/ — order construction and CRUD
Three pieces work together:
| File | Role |
|---|---|
models.py | Pydantic types mirroring order.yaml (camelCase aliases) |
builder.py | Fluent OrderBuilder for constructing valid orders in code |
service.py | HTTP calls: generate ID, create, get, list |
**OrderBuilder** example flow:
order = (
OrderBuilder()
.with_ids(order_id="...", owner_id="...", basket_id="...")
.with_branch("1000")
.add_product(cprcode=25281, quantity=1)
.with_delivery_shipping(shipping_postcode="10330", shipping_phone="...")
.with_payment_totals(grand_total=174.0)
.build()
)
order.to_api_payload() emits camelCase JSON matching the backend contract.
validation/ — local schema + remote API checks
This is the **pre-payment gate**. Two validators compose into PrePaymentValidator:
#### 1. Local: OrderSchemaValidator
schemas/order/order.yamljsonschema (Draft 4)#### 2. Remote: OrderApiValidator
Calls backend pricing/verification APIs:
| Method | API path | Purpose |
|---|---|---|
calculate_grand_total() | /api/webPayment/calculateGrandtotal | Server-side total |
calculate_cost() | /api/calculateCost/getCost | Line-item pricing |
verify_payment() | /api/payment/verify | Post-payment check |
validate_order_exists() | /api/orders/{order_id} | Order exists on server |
#### 3. Orchestrator: PrePaymentValidator
Runs a **pipeline** and returns PrePaymentValidationResult:
Step 1: local_schema → jsonschema against order.yaml
Step 2: drafting check → reject if isDrafting=true
Step 3: remote_order_exists → optional GET order
Step 4: calculate_grand_total → compare with expected amount
Step 5: calculate_cost → confirm cart pricing
Each step appends to result.steps on success or result.errors on failure. The pipeline **stops at the first failure**.
payments/ — card token and card payment
Handles Villa + KBank payment endpoints:
create_card_token() — POST form to cardtoken endpointinitiate_card_payment() — POST form to cardpayment endpointparse_callback() — extract query params from redirect URLverify() — post-payment verificationPayment requests use **form-urlencoded** bodies, not JSON, matching the live Villa payment API.
cli/ — command-line interface
Maps 1:1 to SDK modules:
| Command group | SDK module |
|---|---|
villa auth login | auth/ |
villa order … | orders/ |
villa validate … | validation/ |
villa payment … | payments/ |
Schema contract
Order shape is defined by villaMasterSchema/order/order.yaml.
Bundled copies live in:
villa_backend_sdk/schemas/
├── order/order.yaml
├── order/genOrderId.yaml
├── webPayment/calculateGrandtotal.yaml
└── calculateCost/getCost.yaml
**Required order fields:** orderId, ownerId, basketId
The SDK keeps schemas in the package so:
Typical checkout sequence
sequenceDiagram
participant Dev as Application
participant SDK as VillaClient
participant Cognito as AWS Cognito
participant API as shop.villamarket.com
Dev->>SDK: login(email, password)
SDK->>Cognito: InitiateAuth
Cognito-->>SDK: IdToken
SDK->>SDK: set Bearer token on HttpClient
Dev->>SDK: OrderBuilder.build()
Dev->>SDK: pre_payment.validate(order)
SDK->>SDK: jsonschema (local)
SDK->>API: POST calculateGrandtotal
API-->>SDK: grandTotal
SDK->>API: POST calculateCost
API-->>SDK: pricing breakdown
alt validation passed
Dev->>SDK: payments.create_card_token(...)
SDK->>API: POST cardtoken
Dev->>SDK: payments.initiate_card_payment(...)
SDK->>API: POST cardpayment
API-->>Dev: redirect URL with status
else validation failed
SDK-->>Dev: PrePaymentValidationResult(errors=...)
end
Error model
All SDK errors inherit from VillaError:
| Exception | When raised |
|---|---|
VillaConfigError | Missing/invalid env config |
VillaAuthError | Cognito login failure |
VillaValidationError | Schema or pre-payment validation failure |
VillaApiError | HTTP 4xx/5xx or network error |
VillaPaymentError | Payment endpoint returned failure |
VillaValidationError carries an errors: list[str] with human-readable messages from jsonschema or the validation pipeline.
Extension points
| Need | How to extend |
|---|---|
| Custom API base URL | VillaClient(base_url=...) or VILLA_BASE_URL |
| Custom API paths | VillaConfig(api_paths=ApiPaths(...)) |
| Mock Cognito in tests | Pass client=FakeCognitoClient() to CognitoAuthService |
| Skip remote validation | pre_payment.validate(..., verify_grand_total=False) |
| Direct module use | Import OrderService, PaymentService, etc. without VillaClient |