Coverage for polar/customer/schemas/customer.py: 98%
53 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 hashlib
2from datetime import datetime
3from typing import Annotated
5from annotated_types import MaxLen
6from fastapi import Path
7from pydantic import UUID4, Field, computed_field
9from polar.kit.address import Address, AddressInput
10from polar.kit.email import EmailStrDNS
11from polar.kit.metadata import (
12 MetadataInputMixin,
13 MetadataOutputMixin,
14)
15from polar.kit.schemas import (
16 CUSTOMER_ID_EXAMPLE,
17 ORGANIZATION_ID_EXAMPLE,
18 EmptyStrToNoneValidator,
19 IDSchema,
20 Schema,
21 TimestampedSchema,
22)
23from polar.kit.tax import TaxID
24from polar.member import Member, OwnerCreate
25from polar.organization.schemas import OrganizationID
27CustomerID = Annotated[UUID4, Path(description="The customer ID.")]
28ExternalCustomerID = Annotated[str, Path(description="The customer external ID.")]
30_external_id_description = (
31 "The ID of the customer in your system. "
32 "This must be unique within the organization. "
33 "Once set, it can't be updated."
34)
35_external_id_example = "usr_1337"
36_email_description = (
37 "The email address of the customer. This must be unique within the organization."
38)
39_email_example = "customer@example.com"
40_name_description = "The name of the customer."
41_name_example = "John Doe"
43CustomerNameInput = Annotated[
44 str,
45 MaxLen(256),
46 Field(description=_name_description, examples=[_name_example]),
47 EmptyStrToNoneValidator,
48]
51class CustomerCreate(MetadataInputMixin, Schema):
52 external_id: Annotated[str | None, EmptyStrToNoneValidator] = Field(
53 default=None,
54 description=_external_id_description,
55 examples=[_external_id_example],
56 )
57 email: EmailStrDNS = Field(
58 description=_email_description, examples=[_email_example]
59 )
60 name: CustomerNameInput | None = None
61 billing_address: AddressInput | None = None
62 tax_id: TaxID | None = None
63 organization_id: OrganizationID | None = Field(
64 default=None,
65 description=(
66 "The ID of the organization owning the customer. "
67 "**Required unless you use an organization token.**"
68 ),
69 )
70 owner: OwnerCreate | None = Field(
71 default=None,
72 description=(
73 "Optional owner member to create with the customer. "
74 "If not provided, an owner member will be automatically created "
75 "using the customer's email and name."
76 ),
77 )
80class CustomerUpdateBase(MetadataInputMixin, Schema):
81 email: EmailStrDNS | None = Field(
82 default=None, description=_email_description, examples=[_email_example]
83 )
84 name: CustomerNameInput | None = None
85 billing_address: AddressInput | None = None
86 tax_id: TaxID | None = None
89class CustomerUpdate(CustomerUpdateBase):
90 external_id: Annotated[str | None, EmptyStrToNoneValidator] = Field(
91 default=None,
92 description=_external_id_description,
93 examples=[_external_id_example],
94 )
97class CustomerUpdateExternalID(CustomerUpdateBase): ... 97 ↛ 100line 97 didn't jump to line 100 because
100class CustomerBase(MetadataOutputMixin, TimestampedSchema, IDSchema):
101 id: UUID4 = Field(
102 description="The ID of the customer.", examples=[CUSTOMER_ID_EXAMPLE]
103 )
104 external_id: str | None = Field(
105 description=_external_id_description, examples=[_external_id_example]
106 )
107 email: str = Field(description=_email_description, examples=[_email_example])
108 email_verified: bool = Field(
109 description=(
110 "Whether the customer email address is verified. "
111 "The address is automatically verified when the customer accesses "
112 "the customer portal using their email address."
113 ),
114 examples=[True],
115 )
116 name: str | None = Field(description=_name_description, examples=[_name_example])
117 billing_address: Address | None
118 tax_id: TaxID | None
119 organization_id: UUID4 = Field(
120 description="The ID of the organization owning the customer.",
121 examples=[ORGANIZATION_ID_EXAMPLE],
122 )
124 deleted_at: datetime | None = Field(
125 description="Timestamp for when the customer was soft deleted."
126 )
128 @computed_field(examples=["https://www.gravatar.com/avatar/xxx?d=404"])
129 def avatar_url(self) -> str:
130 email_hash = hashlib.sha256(self.email.lower().encode()).hexdigest()
131 return f"https://www.gravatar.com/avatar/{email_hash}?d=404"
134class Customer(CustomerBase):
135 """A customer in an organization."""
138class CustomerWithMembers(Customer):
139 """A customer in an organization with their members loaded."""
141 members: list[Member] = Field(
142 default_factory=list,
143 description="List of members belonging to this customer.",
144 )