Coverage for polar/refund/schemas.py: 75%

65 statements  

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

1import inspect 1a

2from typing import Annotated, Any 1a

3from uuid import UUID 1a

4 

5import stripe as stripe_lib 1a

6from babel.numbers import format_currency 1a

7from fastapi import Path 1a

8from pydantic import UUID4, Field 1a

9 

10from polar.enums import PaymentProcessor 1a

11from polar.kit.metadata import ( 1a

12 MetadataInputMixin, 

13 MetadataOutputMixin, 

14) 

15from polar.kit.schemas import IDSchema, Schema, TimestampedSchema 1a

16from polar.models.refund import ( 1a

17 RefundFailureReason, 

18 RefundReason, 

19 RefundStatus, 

20) 

21 

22RefundID = Annotated[UUID4, Path(description="The refund ID.")] 1a

23 

24 

25class Refund(MetadataOutputMixin, IDSchema, TimestampedSchema): 1a

26 status: RefundStatus 1a

27 reason: RefundReason 1a

28 amount: int 1a

29 tax_amount: int 1a

30 currency: str 1a

31 organization_id: UUID4 1a

32 order_id: UUID4 1a

33 subscription_id: UUID4 | None 1a

34 customer_id: UUID4 1a

35 revoke_benefits: bool 1a

36 

37 def get_amount_display(self) -> str: 1a

38 return f"{ 

39 format_currency( 

40 self.amount / 100, 

41 self.currency.upper(), 

42 locale='en_US', 

43 ) 

44 }" 

45 

46 

47class RefundCreate(MetadataInputMixin, Schema): 1a

48 order_id: UUID4 1a

49 reason: RefundReason 1a

50 amount: int = Field(description="Amount to refund in cents. Minimum is 1.", gt=0) 1a

51 comment: str | None = Field( 1a

52 None, description="An internal comment about the refund." 

53 ) 

54 revoke_benefits: bool = Field( 1a

55 False, 

56 description=inspect.cleandoc( 

57 """ 

58 Should this refund trigger the associated customer benefits to be revoked? 

59 

60 **Note:** 

61 Only allowed in case the `order` is a one-time purchase. 

62 Subscriptions automatically revoke customer benefits once the 

63 subscription itself is revoked, i.e fully canceled. 

64 """ 

65 ), 

66 ) 

67 

68 

69class InternalRefundCreate(MetadataInputMixin, Schema): 1a

70 status: RefundStatus 1a

71 reason: RefundReason 1a

72 amount: int 1a

73 tax_amount: int 1a

74 currency: str 1a

75 comment: str | None = None 1a

76 failure_reason: RefundFailureReason | None 1a

77 destination_details: dict[str, Any] = {} 1a

78 order_id: UUID | None 1a

79 subscription_id: UUID | None 1a

80 customer_id: UUID | None 1a

81 organization_id: UUID | None 1a

82 pledge_id: UUID | None 1a

83 processor: PaymentProcessor 1a

84 processor_id: str 1a

85 processor_receipt_number: str | None 1a

86 processor_reason: str 1a

87 processor_balance_transaction_id: str | None 1a

88 revoke_benefits: bool = False 1a

89 

90 @classmethod 1a

91 def from_stripe( 1a

92 cls, 

93 stripe_refund: stripe_lib.Refund, 

94 *, 

95 refunded_amount: int, 

96 refunded_tax_amount: int, 

97 order_id: UUID | None = None, 

98 subscription_id: UUID | None = None, 

99 customer_id: UUID | None = None, 

100 organization_id: UUID | None = None, 

101 pledge_id: UUID | None = None, 

102 ) -> "InternalRefundCreate": 

103 failure_reason = getattr(stripe_refund, "failure_reason", None) 

104 failure_reason = RefundFailureReason.from_stripe(failure_reason) 

105 stripe_reason = stripe_refund.reason if stripe_refund.reason else "other" 

106 reason = RefundReason.from_stripe(stripe_refund.reason) 

107 

108 destination_details: dict[str, Any] = getattr( 

109 stripe_refund, "destination_details", {} 

110 ) 

111 

112 status = RefundStatus.pending 

113 if stripe_refund.status: 

114 status = RefundStatus(stripe_refund.status) 

115 

116 balance_transaction_id = None 

117 if stripe_refund.balance_transaction: 

118 balance_transaction_id = str(stripe_refund.balance_transaction) 

119 

120 # Skip validation from trusted source (Stripe) 

121 return cls.model_construct( 

122 status=status, 

123 reason=reason, 

124 amount=refunded_amount, 

125 tax_amount=refunded_tax_amount, 

126 currency=stripe_refund.currency, 

127 failure_reason=failure_reason, 

128 destination_details=destination_details, 

129 order_id=order_id, 

130 subscription_id=subscription_id, 

131 customer_id=customer_id, 

132 organization_id=organization_id, 

133 pledge_id=pledge_id, 

134 revoke_benefits=False, 

135 processor=PaymentProcessor.stripe, 

136 processor_id=stripe_refund.id, 

137 processor_receipt_number=stripe_refund.receipt_number, 

138 processor_reason=stripe_reason, 

139 processor_balance_transaction_id=balance_transaction_id, 

140 )