diff --git a/server/emails/src/emails/subscription_confirmation.tsx b/server/emails/src/emails/subscription_confirmation.tsx index 01dcd761b7..e481c3b68c 100644 --- a/server/emails/src/emails/subscription_confirmation.tsx +++ b/server/emails/src/emails/subscription_confirmation.tsx @@ -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 ( Thank you for your subscription to {product.name}! @@ -30,6 +36,47 @@ export function SubscriptionConfirmation({ is now active. + +
+ + Purchase Details + + +
+
+ + {product.name}{intervalDisplay} + + + {purchase_details.formatted_amount} + +
+ + {purchase_details.discount && purchase_details.formatted_discount_amount && ( +
+ + Discount ({purchase_details.discount.name} + {purchase_details.discount.code && ` - ${purchase_details.discount.code}`}) + + + -{purchase_details.formatted_discount_amount} + +
+ )} + +
+ +
+ + Total{intervalDisplay} + + + {purchase_details.formatted_discounted_amount} + +
+
+
+
@@ -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', } diff --git a/server/emails/src/types.ts b/server/emails/src/types.ts index f03a19c2b5..99ddf710d2 100644 --- a/server/emails/src/types.ts +++ b/server/emails/src/types.ts @@ -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 +} diff --git a/server/polar/subscription/service.py b/server/polar/subscription/service.py index a8aa34a8ac..77bc528dc8 100644 --- a/server/polar/subscription/service.py +++ b/server/polar/subscription/service.py @@ -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 @@ -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( @@ -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,