Skip to content
Open
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
64 changes: 63 additions & 1 deletion server/emails/src/emails/subscription_confirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,25 @@ import Button from '../components/Button'
import Footer from '../components/Footer'
import OrganizationHeader from '../components/OrganizationHeader'
import Wrapper from '../components/Wrapper'
import type { OrganizationProps, ProductProps } from '../types'
import type { OrganizationProps, ProductProps, PurchaseDetailsProps } from '../types'

interface SubscriptionConfirmationProps {
organization: OrganizationProps
product: ProductProps
purchase_details: PurchaseDetailsProps
url: string
}

export function SubscriptionConfirmation({
organization,
product,
purchase_details,
url,
}: SubscriptionConfirmationProps) {
const intervalDisplay = purchase_details.recurring_interval
? ` / ${purchase_details.recurring_interval}`
: ''

return (
<Wrapper>
<Preview>Thank you for your subscription to {product.name}!</Preview>
Expand All @@ -30,6 +36,47 @@ export function SubscriptionConfirmation({
is now active.
</BodyText>
</Section>

<Section className="my-8 rounded-lg bg-gray-50 p-6">
<Heading as="h2" className="text-lg font-semibold text-gray-900 mb-4">
Purchase Details
</Heading>

<div className="space-y-3">
<div className="flex justify-between items-center">
<Text className="text-gray-700 mb-0">
{product.name}{intervalDisplay}
</Text>
<Text className="font-medium text-gray-900 mb-0">
{purchase_details.formatted_amount}
</Text>
</div>

{purchase_details.discount && purchase_details.formatted_discount_amount && (
<div className="flex justify-between items-center">
<Text className="text-gray-700 mb-0">
Discount ({purchase_details.discount.name}
{purchase_details.discount.code && ` - ${purchase_details.discount.code}`})
</Text>
<Text className="font-medium text-green-600 mb-0">
-{purchase_details.formatted_discount_amount}
</Text>
</div>
)}

<hr className="border-gray-300" />

<div className="flex justify-between items-center">
<Text className="font-semibold text-gray-900 mb-0">
Total{intervalDisplay}
</Text>
<Text className="font-bold text-lg text-gray-900 mb-0">
{purchase_details.formatted_discounted_amount}
</Text>
</div>
</div>
</Section>

<Section className="my-8 text-center">
<Button href={url}>Access my purchase</Button>
</Section>
Expand Down Expand Up @@ -61,6 +108,21 @@ SubscriptionConfirmation.PreviewProps = {
name: 'Premium Subscription',
benefits: [],
},
purchase_details: {
amount: 2000,
currency: 'usd',
recurring_interval: 'month',
discount: {
name: 'Early Bird Discount',
code: 'EARLY20',
type: 'percentage',
basis_points: 2000,
},
discounted_amount: 1600,
formatted_amount: '$20.00',
formatted_discounted_amount: '$16.00',
formatted_discount_amount: '$4.00',
},
url: 'https://polar.sh/acme-inc/portal/subscriptions/12345',
}

Expand Down
20 changes: 20 additions & 0 deletions server/emails/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,23 @@ export interface ProductProps {
name: string
benefits: BenefitProps[]
}

export interface DiscountProps {
name: string
code: string | null
type: 'fixed' | 'percentage'
amount?: number
basis_points?: number
currency?: string
}

export interface PurchaseDetailsProps {
amount: number
currency: string
recurring_interval: string | null
discount: DiscountProps | null
discounted_amount: number
formatted_amount: string
formatted_discounted_amount: string
formatted_discount_amount: string | null
}
47 changes: 47 additions & 0 deletions server/polar/subscription/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from polar.integrations.stripe.schemas import ProductType
from polar.integrations.stripe.service import stripe as stripe_service
from polar.integrations.stripe.utils import get_expandable_id
from polar.invoice.generator import format_currency
from polar.kit.db.postgres import AsyncSession
from polar.kit.metadata import MetadataQuery, apply_metadata_clause
from polar.kit.pagination import PaginationParams
Expand Down Expand Up @@ -1599,11 +1600,15 @@ async def update_product_benefits_grants(
async def send_confirmation_email(
self, session: AsyncSession, subscription: Subscription
) -> None:
purchase_details = self._build_purchase_details(subscription)
return await self._send_customer_email(
session,
subscription,
subject_template="Your {product.name} subscription",
template_name="subscription_confirmation",
extra_context={
"purchase_details": purchase_details,
},
)

async def send_cycled_email(
Expand Down Expand Up @@ -1754,6 +1759,48 @@ async def _send_customer_email(
html_content=body,
)

def _build_purchase_details(self, subscription: Subscription) -> dict[str, JSONProperty]:
base_amount = sum(price.amount for price in subscription.subscription_product_prices)

discount_data = None
discount_amount = 0
if subscription.discount:
discount_amount = subscription.discount.get_discount_amount(base_amount)
discount_data = {
"name": subscription.discount.name,
"code": subscription.discount.code,
"type": subscription.discount.type,
}
if subscription.discount.type == "fixed":
discount_data["amount"] = subscription.discount.amount
discount_data["currency"] = subscription.discount.currency
elif subscription.discount.type == "percentage":
discount_data["basis_points"] = subscription.discount.basis_points

final_amount = base_amount - discount_amount

currency = subscription.currency
formatted_base_amount = format_currency(base_amount, currency)
formatted_final_amount = format_currency(final_amount, currency)
formatted_discount_amount = format_currency(discount_amount, currency) if discount_amount > 0 else None

result: dict[str, JSONProperty] = {
"amount": base_amount,
"currency": currency,
"recurring_interval": subscription.recurring_interval,
"discounted_amount": final_amount,
"formatted_amount": formatted_base_amount,
"formatted_discounted_amount": formatted_final_amount,
}

if discount_data is not None:
result["discount"] = discount_data

if formatted_discount_amount is not None:
result["formatted_discount_amount"] = formatted_discount_amount

return result

async def _get_outdated_grants(
self,
session: AsyncSession,
Expand Down