Skip to content
Merged
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
130 changes: 119 additions & 11 deletions server/emails/src/emails/notification_new_sale.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,144 @@
import { Preview } from '@react-email/components'
import { Hr, Img, Preview, Section, Text } from '@react-email/components'
import Button from '../components/Button'
import Footer from '../components/Footer'
import IntroWithHi from '../components/IntroWithHi'
import PolarHeader from '../components/PolarHeader'
import Wrapper from '../components/Wrapper'
import type { schemas } from '../types'

export function NotificationNewSale({
customer_email,
customer_name,
billing_address_city,
billing_address_line1,
formatted_price_amount,
formatted_billing_reason,
formatted_address_country,
product_name,
product_price_amount,
organization_name,
product_image_url,
order_date,
order_url,
}: schemas['MaintainerNewProductSaleNotificationPayload']) {
const displayName = customer_name || customer_email || 'A customer'

const formattedDate = order_date
? new Date(order_date).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
})
: null

const addressParts = [billing_address_line1, billing_address_city].filter(
Boolean,
)
const formattedAddress =
addressParts.length > 0 ? addressParts.join(', ') : null

return (
<Wrapper>
<Preview>New {product_name} sale</Preview>
<Preview>
{displayName} placed an order for {product_name}
</Preview>
<PolarHeader />
<IntroWithHi hiMsg="Congratulations!">
{customer_name} purchased <strong>{product_name}</strong> for{' '}
{formatted_price_amount}.
</IntroWithHi>

<Section className="pt-8">
<Text className="m-0 text-lg text-gray-900">
<strong>{displayName}</strong> placed an order{formattedDate ? ` on ${formattedDate}` : ''}!
</Text>
</Section>

{order_url && (
<Section className="mt-6 mb-8">
<Button href={order_url}>View order</Button>
</Section>
)}

<Hr className="my-6 border-gray-200" />

<Section>
<Text className="my-0 mb-2 text-base font-semibold text-gray-900">
Order Summary
</Text>
<table className="w-full">
<tbody>
<tr>
{product_image_url && (
<td className="w-[72px] pr-3 align-top">
<Img
src={product_image_url}
width={64}
height={64}
className="rounded-lg border border-gray-200"
/>
</td>
)}
<td className="align-middle">
<Text className="m-0 text-sm font-medium text-gray-900">
{product_name}
</Text>
<Text className="m-0 text-sm text-gray-500">
{formatted_price_amount}
</Text>
</td>
</tr>
</tbody>
</table>
</Section>

<Hr className="my-6 border-gray-200" />

{formatted_billing_reason && (
<Section>
<Text className="m-0 text-sm font-semibold text-gray-900">
Order Type
</Text>
<Text className="m-0 text-sm text-gray-600">
{formatted_billing_reason}
</Text>
</Section>
)}

<Section className="mt-4 mb-6">
<Text className="m-0 text-sm font-semibold text-gray-900">
Customer
</Text>
<Text className="m-0 text-sm text-gray-600">{displayName}</Text>
{customer_email && (
<Text className="m-0 text-sm text-gray-600">{customer_email}</Text>
)}
{formattedAddress && (
<Text className="m-0 text-sm text-gray-600">{formattedAddress}</Text>
)}
{formatted_address_country && (
<Text className="m-0 text-sm text-gray-600">
{formatted_address_country}
</Text>
)}
</Section>

<Footer email={null} />
</Wrapper>
)
}

NotificationNewSale.PreviewProps = {
customer_name: 'John Doe',
customer_email: 'bob@ross.com',
customer_name: 'Bob Ross',
billing_address_country: 'US',
billing_address_city: 'San Francisco',
billing_address_line1: '123 Main St',
formatted_price_amount: '$45.95',
product_name: 'Ultimate Magento webshop template',
formatted_billing_reason: 'One-time purchase',
formatted_address_country: 'United States',
product_name: 'Beginners guide to painting',
product_price_amount: 4595,
product_image_url: 'https://placehold.co/64x64',
order_id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
order_date: '2024-11-05T20:41:00Z',
order_url:
'https://polar.sh/dashboard/acme-inc/sales/a1b2c3d4-e5f6-7890-abcd-ef1234567890',
organization_name: 'Acme Inc.',
organization_slug: 'acme-inc',
billing_reason: 'purchase',
}

export default NotificationNewSale
107 changes: 104 additions & 3 deletions server/emails/src/types/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -831,16 +831,79 @@ export interface components {
}
/** MaintainerNewProductSaleNotificationPayload */
MaintainerNewProductSaleNotificationPayload: {
/** Customer Name */
customer_name: string
/** Product Name */
product_name: string
/** Product Price Amount */
product_price_amount: number
/** Organization Name */
/**
* Customer Name
* @default A customer
*/
customer_name: string
/**
* Organization Name
* @default
*/
organization_name: string
/**
* Customer Email
* @default null
*/
customer_email: string | null
/**
* Billing Address Country
* @default null
*/
billing_address_country: string | null
/**
* Billing Address City
* @default null
*/
billing_address_city: string | null
/**
* Billing Address Line1
* @default null
*/
billing_address_line1: string | null
/**
* Product Image Url
* @default null
*/
product_image_url: string | null
/**
* Order Id
* @default null
*/
order_id: string | null
/**
* Order Date
* @default null
*/
order_date: string | null
/**
* Organization Slug
* @default null
*/
organization_slug: string | null
/**
* Billing Reason
* @default null
*/
billing_reason: components['schemas']['OrderBillingReasonInternal'] | null
/** Formatted Price Amount */
readonly formatted_price_amount: string
/**
* Formatted Billing Reason
* @default null
*/
readonly formatted_billing_reason: string | null
/** Formatted Address Country */
readonly formatted_address_country: string | null
/**
* Order Url
* @default null
*/
readonly order_url: string | null
}
/** NotificationCreateAccountEmail */
NotificationCreateAccountEmail: {
Expand Down Expand Up @@ -925,6 +988,17 @@ export interface components {
| 'subscription_create'
| 'subscription_cycle'
| 'subscription_update'
/**
* OrderBillingReasonInternal
* @description Internal billing reasons with additional granularity.
* @enum {string}
*/
OrderBillingReasonInternal:
| 'purchase'
| 'subscription_create'
| 'subscription_cycle'
| 'subscription_cycle_after_trial'
| 'subscription_update'
/** OrderConfirmationEmail */
OrderConfirmationEmail: {
/**
Expand Down Expand Up @@ -1224,6 +1298,25 @@ export interface components {
/** Url */
url: string
}
/** OrganizationAccountUnlinkEmail */
OrganizationAccountUnlinkEmail: {
/**
* Template
* @default organization_account_unlink
* @constant
*/
template: 'organization_account_unlink'
props: components['schemas']['OrganizationAccountUnlinkProps']
}
/** OrganizationAccountUnlinkProps */
OrganizationAccountUnlinkProps: {
/** Email */
email: string
/** Organization Kept Name */
organization_kept_name: string
/** Organizations Unlinked */
organizations_unlinked: string[]
}
/** OrganizationCustomerEmailSettings */
OrganizationCustomerEmailSettings: {
/** Order Confirmation */
Expand Down Expand Up @@ -1269,6 +1362,12 @@ export interface components {
* @default false
*/
wallets_enabled: boolean
/**
* Member Model Enabled
* @description If this organization has the Member model enabled
* @default false
*/
member_model_enabled: boolean
}
/** OrganizationInviteEmail */
OrganizationInviteEmail: {
Expand Down Expand Up @@ -1358,6 +1457,8 @@ export interface components {
proration_behavior: components['schemas']['SubscriptionProrationBehavior']
/** Benefit Revocation Grace Period */
benefit_revocation_grace_period: number
/** Prevent Trial Abuse */
prevent_trial_abuse: boolean
}
/** OrganizationUnderReviewEmail */
OrganizationUnderReviewEmail: {
Expand Down
46 changes: 44 additions & 2 deletions server/polar/notifications/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
from enum import StrEnum
from typing import Annotated, Literal

import pycountry
from babel.numbers import format_currency
from pydantic import UUID4, BaseModel, Discriminator, computed_field

from polar.config import settings
from polar.email.react import render_email_template
from polar.kit.schemas import Schema
from polar.models.order import OrderBillingReasonInternal


class NotificationType(StrEnum):
Expand Down Expand Up @@ -89,15 +92,54 @@ class MaintainerNewPaidSubscriptionNotification(NotificationBase):


class MaintainerNewProductSaleNotificationPayload(NotificationPayloadBase):
customer_name: str
product_name: str
product_price_amount: int
organization_name: str
customer_name: str = ""
organization_name: str = ""

customer_email: str | None = None
billing_address_country: str | None = None
billing_address_city: str | None = None
billing_address_line1: str | None = None
product_image_url: str | None = None
order_id: str | None = None
order_date: str | None = None
organization_slug: str | None = None
billing_reason: OrderBillingReasonInternal | None = None

@computed_field
def formatted_price_amount(self) -> str:
return format_currency(self.product_price_amount / 100, "USD", locale="en_US")

@computed_field
def formatted_billing_reason(self) -> str | None:
if self.billing_reason is None:
return None
match self.billing_reason:
case OrderBillingReasonInternal.purchase:
return "One-time purchase"
case OrderBillingReasonInternal.subscription_create:
return "New subscription"
case OrderBillingReasonInternal.subscription_cycle:
return "Subscription renewal"
case OrderBillingReasonInternal.subscription_cycle_after_trial:
return "Subscription started after trial"
case OrderBillingReasonInternal.subscription_update:
return "Subscription update"

@computed_field
def formatted_address_country(self) -> str | None:
if not self.billing_address_country:
return None
country = pycountry.countries.get(alpha_2=self.billing_address_country)
return country.name if country else self.billing_address_country

@computed_field
def order_url(self) -> str | None:
if not self.organization_slug or not self.order_id:
return None
return f"{settings.FRONTEND_BASE_URL}/dashboard/{self.organization_slug}/sales/{self.order_id}"

def subject(self) -> str:
return f"You've made a new sale ({self.formatted_price_amount})!"

Expand Down
Loading