Coverage for polar/customer/schemas/customer.py: 96%

55 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-12-05 17:15 +0000

1import hashlib 1a

2from datetime import datetime 1a

3from typing import Annotated 1a

4 

5from annotated_types import MaxLen 1a

6from fastapi import Path 1a

7from pydantic import UUID4, Field, computed_field 1a

8 

9from polar.kit.address import Address, AddressInput 1a

10from polar.kit.email import EmailStrDNS 1a

11from polar.kit.metadata import ( 1a

12 MetadataInputMixin, 

13 MetadataOutputMixin, 

14) 

15from polar.kit.schemas import ( 1a

16 CUSTOMER_ID_EXAMPLE, 

17 ORGANIZATION_ID_EXAMPLE, 

18 EmptyStrToNoneValidator, 

19 IDSchema, 

20 Schema, 

21 TimestampedSchema, 

22) 

23from polar.kit.tax import TaxID 1a

24from polar.member import Member, OwnerCreate 1a

25from polar.organization.schemas import OrganizationID 1a

26 

27CustomerID = Annotated[UUID4, Path(description="The customer ID.")] 1a

28ExternalCustomerID = Annotated[str, Path(description="The customer external ID.")] 1a

29 

30_external_id_description = ( 1a

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" 1a

36_email_description = ( 1a

37 "The email address of the customer. This must be unique within the organization." 

38) 

39_email_example = "customer@example.com" 1a

40_name_description = "The name of the customer." 1a

41_name_example = "John Doe" 1a

42 

43CustomerNameInput = Annotated[ 1a

44 str, 

45 MaxLen(256), 

46 Field(description=_name_description, examples=[_name_example]), 

47 EmptyStrToNoneValidator, 

48] 

49 

50 

51class CustomerCreate(MetadataInputMixin, Schema): 1a

52 external_id: Annotated[str | None, EmptyStrToNoneValidator] = Field( 1a

53 default=None, 

54 description=_external_id_description, 

55 examples=[_external_id_example], 

56 ) 

57 email: EmailStrDNS = Field( 1a

58 description=_email_description, examples=[_email_example] 

59 ) 

60 name: CustomerNameInput | None = None 1a

61 billing_address: AddressInput | None = None 1a

62 tax_id: TaxID | None = None 1a

63 organization_id: OrganizationID | None = Field( 1a

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( 1a

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 ) 

78 

79 

80class CustomerUpdateBase(MetadataInputMixin, Schema): 1a

81 email: EmailStrDNS | None = Field( 1a

82 default=None, description=_email_description, examples=[_email_example] 

83 ) 

84 name: CustomerNameInput | None = None 1a

85 billing_address: AddressInput | None = None 1a

86 tax_id: TaxID | None = None 1a

87 

88 

89class CustomerUpdate(CustomerUpdateBase): 1a

90 external_id: Annotated[str | None, EmptyStrToNoneValidator] = Field( 1a

91 default=None, 

92 description=_external_id_description, 

93 examples=[_external_id_example], 

94 ) 

95 

96 

97class CustomerUpdateExternalID(CustomerUpdateBase): ... 1a

98 

99 

100class CustomerBase(MetadataOutputMixin, TimestampedSchema, IDSchema): 1a

101 id: UUID4 = Field( 1a

102 description="The ID of the customer.", examples=[CUSTOMER_ID_EXAMPLE] 

103 ) 

104 external_id: str | None = Field( 1a

105 description=_external_id_description, examples=[_external_id_example] 

106 ) 

107 email: str = Field(description=_email_description, examples=[_email_example]) 1a

108 email_verified: bool = Field( 1a

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]) 1a

117 billing_address: Address | None 1a

118 tax_id: TaxID | None 1a

119 organization_id: UUID4 = Field( 1a

120 description="The ID of the organization owning the customer.", 

121 examples=[ORGANIZATION_ID_EXAMPLE], 

122 ) 

123 

124 deleted_at: datetime | None = Field( 1a

125 description="Timestamp for when the customer was soft deleted." 

126 ) 

127 

128 @computed_field(examples=["https://www.gravatar.com/avatar/xxx?d=404"]) 1a

129 def avatar_url(self) -> str: 1a

130 email_hash = hashlib.sha256(self.email.lower().encode()).hexdigest() 

131 return f"https://www.gravatar.com/avatar/{email_hash}?d=404" 

132 

133 

134class Customer(CustomerBase): 1a

135 """A customer in an organization.""" 

136 

137 

138class CustomerWithMembers(Customer): 1a

139 """A customer in an organization with their members loaded.""" 

140 

141 members: list[Member] = Field( 1a

142 default_factory=list, 

143 description="List of members belonging to this customer.", 

144 )