Coverage for polar/customer_portal/service/order.py: 81%
82 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 12:42 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 12:42 +0000
1import uuid
2from collections.abc import Sequence
3from enum import StrEnum
4from typing import Any
6from sqlalchemy import UnaryExpression, asc, desc
7from sqlalchemy.orm import contains_eager
9from polar.auth.models import AuthSubject
10from polar.enums import PaymentProcessor
11from polar.exceptions import PolarError
12from polar.invoice.service import invoice as invoice_service
13from polar.kit.db.postgres import AsyncSession
14from polar.kit.pagination import PaginationParams
15from polar.kit.sorting import Sorting
16from polar.models import Customer, Order, Product
17from polar.models.product import ProductBillingType
18from polar.order.service import InvoiceDoesNotExist
19from polar.order.service import order as order_service
21from ..repository.order import CustomerOrderRepository
22from ..schemas.order import (
23 CustomerOrderInvoice,
24 CustomerOrderPaymentConfirmation,
25 CustomerOrderUpdate,
26)
29class CustomerOrderError(PolarError): ... 29 ↛ 32line 29 didn't jump to line 32 because
32class InvoiceNotAvailable(CustomerOrderError):
33 def __init__(self, order: Order) -> None:
34 self.order = order
35 message = "The invoice is not available for this order."
36 super().__init__(message, 404)
39class OrderNotEligibleForRetry(CustomerOrderError):
40 def __init__(self, order: Order) -> None:
41 self.order = order
42 message = "Order is not eligible for payment retry."
43 super().__init__(message, 422)
46class PaymentAlreadyInProgress(CustomerOrderError):
47 def __init__(self, order: Order) -> None:
48 self.order = order
49 message = "Payment for order is already in progress."
50 super().__init__(message, 409)
53class CustomerOrderSortProperty(StrEnum):
54 created_at = "created_at"
55 amount = "amount"
56 net_amount = "net_amount"
57 product = "product"
58 subscription = "subscription"
61class CustomerOrderService:
62 async def list(
63 self,
64 session: AsyncSession,
65 auth_subject: AuthSubject[Customer],
66 *,
67 product_id: Sequence[uuid.UUID] | None = None,
68 product_billing_type: Sequence[ProductBillingType] | None = None,
69 subscription_id: Sequence[uuid.UUID] | None = None,
70 query: str | None = None,
71 pagination: PaginationParams,
72 sorting: list[Sorting[CustomerOrderSortProperty]] = [
73 (CustomerOrderSortProperty.created_at, True)
74 ],
75 ) -> tuple[Sequence[Order], int]:
76 repository = CustomerOrderRepository.from_session(session)
77 statement = (
78 repository.get_readable_statement(auth_subject)
79 .join(Order.product, isouter=True)
80 .options(
81 *repository.get_eager_options(
82 product_load=contains_eager(Order.product)
83 )
84 )
85 )
87 if product_id is not None:
88 statement = statement.where(Order.product_id.in_(product_id))
90 if product_billing_type is not None:
91 statement = statement.where(Product.billing_type.in_(product_billing_type))
93 if subscription_id is not None:
94 statement = statement.where(Order.subscription_id.in_(subscription_id))
96 if query is not None:
97 statement = statement.where(Product.name.ilike(f"%{query}%"))
99 order_by_clauses: list[UnaryExpression[Any]] = []
100 for criterion, is_desc in sorting:
101 clause_function = desc if is_desc else asc
102 if criterion == CustomerOrderSortProperty.created_at:
103 order_by_clauses.append(clause_function(Order.created_at))
104 elif criterion in {
105 CustomerOrderSortProperty.amount,
106 CustomerOrderSortProperty.net_amount,
107 }:
108 order_by_clauses.append(clause_function(Order.net_amount))
109 elif criterion == CustomerOrderSortProperty.product:
110 order_by_clauses.append(clause_function(Product.name))
111 elif criterion == CustomerOrderSortProperty.subscription: 111 ↛ 100line 111 didn't jump to line 100 because the condition on line 111 was always true
112 order_by_clauses.append(clause_function(Order.subscription_id))
113 statement = statement.order_by(*order_by_clauses)
115 return await repository.paginate(
116 statement, limit=pagination.limit, page=pagination.page
117 )
119 async def get_by_id(
120 self,
121 session: AsyncSession,
122 auth_subject: AuthSubject[Customer],
123 id: uuid.UUID,
124 ) -> Order | None:
125 repository = CustomerOrderRepository.from_session(session)
126 statement = (
127 repository.get_readable_statement(auth_subject)
128 .where(Order.id == id)
129 .options(*repository.get_eager_options())
130 )
131 return await repository.get_one_or_none(statement)
133 async def update(
134 self, session: AsyncSession, order: Order, order_update: CustomerOrderUpdate
135 ) -> Order:
136 return await order_service.update(session, order, order_update)
138 async def trigger_invoice_generation(
139 self, session: AsyncSession, order: Order
140 ) -> None:
141 return await order_service.trigger_invoice_generation(session, order)
143 async def get_order_invoice(self, order: Order) -> CustomerOrderInvoice:
144 if order.invoice_path is None:
145 raise InvoiceDoesNotExist(order)
147 url, _ = await invoice_service.get_order_invoice_url(order)
148 return CustomerOrderInvoice(url=url)
150 async def confirm_retry_payment(
151 self,
152 session: AsyncSession,
153 order: Order,
154 confirmation_token_id: str | None,
155 payment_processor: PaymentProcessor,
156 payment_method_id: uuid.UUID | None = None,
157 ) -> CustomerOrderPaymentConfirmation:
158 return await order_service.process_retry_payment(
159 session, order, confirmation_token_id, payment_processor, payment_method_id
160 )
163customer_order = CustomerOrderService()