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
10 changes: 10 additions & 0 deletions lago_python_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
from .invoiced_usages.clients import InvoicedUsageClient
from .invoices.clients import InvoiceClient
from .mrrs.clients import MrrClient
from .order_forms.clients import OrderFormClient
from .orders.clients import OrderClient
from .organizations.clients import OrganizationClient
from .overdue_balances.clients import OverdueBalanceClient
from .payment_receipts.clients import PaymentReceiptClient
Expand Down Expand Up @@ -182,6 +184,14 @@ def invoiced_usages(self) -> InvoicedUsageClient:
def mrrs(self) -> MrrClient:
return self._create_client(MrrClient, self.base_api_url, self.api_key)

@callable_cached_property
def order_forms(self) -> OrderFormClient:
return self._create_client(OrderFormClient, self.base_api_url, self.api_key)

@callable_cached_property
def orders(self) -> OrderClient:
return self._create_client(OrderClient, self.base_api_url, self.api_key)

@callable_cached_property
def organizations(self) -> OrganizationClient:
return self._create_client(OrganizationClient, self.base_api_url, self.api_key)
Expand Down
15 changes: 15 additions & 0 deletions lago_python_client/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,21 @@
from .minimum_commitment import (
MinimumCommitmentResponse as MinimumCommitmentResponse,
)
from .order import (
OrderExecute as OrderExecute,
)
from .order import (
OrderExecutionRecordResponse as OrderExecutionRecordResponse,
)
from .order import (
OrderResponse as OrderResponse,
)
from .order_form import (
OrderFormMarkAsSigned as OrderFormMarkAsSigned,
)
from .order_form import (
OrderFormResponse as OrderFormResponse,
)
from .organization import (
Organization as Organization,
)
Expand Down
39 changes: 39 additions & 0 deletions lago_python_client/models/order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from typing import List, Optional

from lago_python_client.base_model import BaseModel

from ..base_model import BaseResponseModel
from .quote import QuoteBillingItemsResponse


class OrderExecute(BaseModel):
execution_mode: Optional[str]


class OrderExecutionRecordResponse(BaseResponseModel):
executed_at: Optional[str]
execution_mode: Optional[str]
invoice_id: Optional[str]
subscription_ids: Optional[List[str]]
terminated_subscription_ids: Optional[List[str]]
applied_coupon_ids: Optional[List[str]]
wallet_ids: Optional[List[str]]
errors: Optional[List[str]]


class OrderResponse(BaseResponseModel):
lago_id: str
number: str
status: str
order_type: str
execution_mode: Optional[str]
currency: Optional[str]
executed_at: Optional[str]
execution_record: Optional[OrderExecutionRecordResponse]
lago_organization_id: str
lago_customer_id: str
lago_order_form_id: str
created_at: str
updated_at: str
# Omitted from the webhook payloads, being a heavy blob.
billing_snapshot: Optional[QuoteBillingItemsResponse]
28 changes: 28 additions & 0 deletions lago_python_client/models/order_form.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from typing import Optional

from lago_python_client.base_model import BaseModel

from ..base_model import BaseResponseModel


class OrderFormMarkAsSigned(BaseModel):
signed_document: Optional[str]
execution_mode: Optional[str]
execute_at: Optional[str]


class OrderFormResponse(BaseResponseModel):
lago_id: str
number: str
status: str
void_reason: Optional[str]
expires_at: Optional[str]
signed_at: Optional[str]
voided_at: Optional[str]
signed_document_url: Optional[str]
lago_organization_id: str
lago_customer_id: str
lago_quote_id: str
lago_quote_version_id: str
created_at: str
updated_at: str
Empty file.
74 changes: 74 additions & 0 deletions lago_python_client/order_forms/clients.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from typing import ClassVar, Optional, Type

import httpx

from ..base_client import BaseClient
from ..mixins import (
DEFAULT_TIMEOUT,
FindAllCommandMixin,
FindCommandMixin,
)
from ..models.order_form import OrderFormMarkAsSigned, OrderFormResponse
from ..services.json import to_json
from ..services.request import (
make_headers,
make_url,
send_post_request,
)
from ..services.response import Response, get_response_data, prepare_object_response


class OrderFormClient(
FindCommandMixin[OrderFormResponse],
FindAllCommandMixin[OrderFormResponse],
BaseClient,
):
API_RESOURCE: ClassVar[str] = "order_forms"
RESPONSE_MODEL: ClassVar[Type[OrderFormResponse]] = OrderFormResponse
ROOT_NAME: ClassVar[str] = "order_form"

def mark_as_signed(
self,
resource_id: str,
input_object: Optional[OrderFormMarkAsSigned] = None,
timeout: Optional[httpx.Timeout] = DEFAULT_TIMEOUT,
) -> OrderFormResponse:
"""Record the customer's signature and create the order carrying the deal out."""
payload = input_object.dict(exclude_none=True) if input_object else {}

api_response: Response = send_post_request(
url=make_url(
origin=self.base_url,
path_parts=(self.API_RESOURCE, resource_id, "mark_as_signed"),
),
content=to_json({self.ROOT_NAME: payload}) if payload else None,
headers=make_headers(api_key=self.api_key),
timeout=timeout,
rate_limit_retry_config=self.rate_limit_retry_config,
)

return prepare_object_response(
response_model=self.RESPONSE_MODEL,
data=get_response_data(response=api_response, key=self.ROOT_NAME),
)

def void(
self,
resource_id: str,
timeout: Optional[httpx.Timeout] = DEFAULT_TIMEOUT,
) -> OrderFormResponse:
"""Void a generated order form, cascading to the quote version it came from."""
api_response: Response = send_post_request(
url=make_url(
origin=self.base_url,
path_parts=(self.API_RESOURCE, resource_id, "void"),
),
headers=make_headers(api_key=self.api_key),
timeout=timeout,
rate_limit_retry_config=self.rate_limit_retry_config,
)

return prepare_object_response(
response_model=self.RESPONSE_MODEL,
data=get_response_data(response=api_response, key=self.ROOT_NAME),
)
Empty file.
53 changes: 53 additions & 0 deletions lago_python_client/orders/clients.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import ClassVar, Optional, Type

import httpx

from ..base_client import BaseClient
from ..mixins import (
DEFAULT_TIMEOUT,
FindAllCommandMixin,
FindCommandMixin,
)
from ..models.order import OrderExecute, OrderResponse
from ..services.json import to_json
from ..services.request import (
make_headers,
make_url,
send_post_request,
)
from ..services.response import Response, get_response_data, prepare_object_response


class OrderClient(
FindCommandMixin[OrderResponse],
FindAllCommandMixin[OrderResponse],
BaseClient,
):
API_RESOURCE: ClassVar[str] = "orders"
RESPONSE_MODEL: ClassVar[Type[OrderResponse]] = OrderResponse
ROOT_NAME: ClassVar[str] = "order"

def execute(
self,
resource_id: str,
input_object: Optional[OrderExecute] = None,
timeout: Optional[httpx.Timeout] = DEFAULT_TIMEOUT,
) -> OrderResponse:
"""Carry out an order on demand, without waiting for its schedule."""
payload = input_object.dict(exclude_none=True) if input_object else {}

api_response: Response = send_post_request(
url=make_url(
origin=self.base_url,
path_parts=(self.API_RESOURCE, resource_id, "execute"),
),
content=to_json({self.ROOT_NAME: payload}) if payload else None,
headers=make_headers(api_key=self.api_key),
timeout=timeout,
rate_limit_retry_config=self.rate_limit_retry_config,
)

return prepare_object_response(
response_model=self.RESPONSE_MODEL,
data=get_response_data(response=api_response, key=self.ROOT_NAME),
)
36 changes: 36 additions & 0 deletions tests/fixtures/executed_order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"order": {
"lago_id": "cc33cc33-cc33-cc33-cc33-cc33cc33cc33",
"number": "OR-2026-0001",
"status": "executed",
"order_type": "subscription_creation",
"execution_mode": "execute_in_lago",
"currency": "EUR",
"executed_at": "2026-07-01T00:00:00Z",
"execution_record": {
"executed_at": "2026-07-01T00:00:00Z",
"execution_mode": "execute_in_lago",
"invoice_id": null,
"subscription_ids": ["dd44dd44-dd44-dd44-dd44-dd44dd44dd44"],
"terminated_subscription_ids": [],
"applied_coupon_ids": ["ee55ee55-ee55-ee55-ee55-ee55ee55ee55"],
"wallet_ids": [],
"errors": []
},
"lago_organization_id": "3c123c12-3c12-3c12-3c12-3c123c123c12",
"lago_customer_id": "2b012b01-2b01-2b01-2b01-2b012b012b01",
"lago_order_form_id": "aa11aa11-aa11-aa11-aa11-aa11aa11aa11",
"created_at": "2026-05-02T10:15:00Z",
"updated_at": "2026-07-01T00:00:00Z",
"billing_snapshot": {
"plans": [
{
"id": "7a567a56-7a56-7a56-7a56-7a567a567a56",
"localId": "b5c1e2a4-4e1e-4a7f-9f0e-9c1a0c7e1f2b",
"type": "plan",
"payload": { "code": "premium_plan" }
}
]
}
}
}
36 changes: 36 additions & 0 deletions tests/fixtures/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"order": {
"lago_id": "cc33cc33-cc33-cc33-cc33-cc33cc33cc33",
"number": "OR-2026-0001",
"status": "created",
"order_type": "subscription_creation",
"execution_mode": "execute_in_lago",
"currency": "EUR",
"executed_at": null,
"execution_record": {
"executed_at": null,
"execution_mode": "execute_in_lago",
"invoice_id": null,
"subscription_ids": [],
"terminated_subscription_ids": [],
"applied_coupon_ids": [],
"wallet_ids": [],
"errors": []
},
"lago_organization_id": "3c123c12-3c12-3c12-3c12-3c123c123c12",
"lago_customer_id": "2b012b01-2b01-2b01-2b01-2b012b012b01",
"lago_order_form_id": "aa11aa11-aa11-aa11-aa11-aa11aa11aa11",
"created_at": "2026-05-02T10:15:00Z",
"updated_at": "2026-05-02T10:15:00Z",
"billing_snapshot": {
"plans": [
{
"id": "7a567a56-7a56-7a56-7a56-7a567a567a56",
"localId": "b5c1e2a4-4e1e-4a7f-9f0e-9c1a0c7e1f2b",
"type": "plan",
"payload": { "code": "premium_plan" }
}
]
}
}
}
18 changes: 18 additions & 0 deletions tests/fixtures/order_form.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"order_form": {
"lago_id": "aa11aa11-aa11-aa11-aa11-aa11aa11aa11",
"number": "OF-2026-0001",
"status": "generated",
"void_reason": null,
"expires_at": "2026-06-30T23:59:59Z",
"signed_at": null,
"voided_at": null,
"signed_document_url": null,
"lago_organization_id": "3c123c12-3c12-3c12-3c12-3c123c123c12",
"lago_customer_id": "2b012b01-2b01-2b01-2b01-2b012b012b01",
"lago_quote_id": "1a901a90-1a90-1a90-1a90-1a901a901a90",
"lago_quote_version_id": "4d234d23-4d23-4d23-4d23-4d234d234d23",
"created_at": "2026-04-29T08:59:51Z",
"updated_at": "2026-04-29T08:59:51Z"
}
}
43 changes: 43 additions & 0 deletions tests/fixtures/order_form_index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"order_forms": [
{
"lago_id": "aa11aa11-aa11-aa11-aa11-aa11aa11aa11",
"number": "OF-2026-0001",
"status": "generated",
"void_reason": null,
"expires_at": "2026-06-30T23:59:59Z",
"signed_at": null,
"voided_at": null,
"signed_document_url": null,
"lago_organization_id": "3c123c12-3c12-3c12-3c12-3c123c123c12",
"lago_customer_id": "2b012b01-2b01-2b01-2b01-2b012b012b01",
"lago_quote_id": "1a901a90-1a90-1a90-1a90-1a901a901a90",
"lago_quote_version_id": "4d234d23-4d23-4d23-4d23-4d234d234d23",
"created_at": "2026-04-29T08:59:51Z",
"updated_at": "2026-04-29T08:59:51Z"
},
{
"lago_id": "bb22bb22-bb22-bb22-bb22-bb22bb22bb22",
"number": "OF-2026-0002",
"status": "voided",
"void_reason": "manual",
"expires_at": null,
"signed_at": null,
"voided_at": "2026-05-01T09:00:00Z",
"signed_document_url": null,
"lago_organization_id": "3c123c12-3c12-3c12-3c12-3c123c123c12",
"lago_customer_id": "2b012b01-2b01-2b01-2b01-2b012b012b01",
"lago_quote_id": "6f456f45-6f45-6f45-6f45-6f456f456f45",
"lago_quote_version_id": "9c789c78-9c78-9c78-9c78-9c789c789c78",
"created_at": "2026-04-30T08:59:51Z",
"updated_at": "2026-05-01T09:00:00Z"
}
],
"meta": {
"current_page": 1,
"next_page": null,
"prev_page": null,
"total_pages": 1,
"total_count": 2
}
}
Loading
Loading