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
122 changes: 11 additions & 111 deletions server/emails/src/emails/notification_new_sale.tsx
Original file line number Diff line number Diff line change
@@ -1,136 +1,36 @@
import { Hr, Img, Preview, Section, Text } from '@react-email/components'
import Button from '../components/Button'
import { Preview } from '@react-email/components'
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_image_url,
order_date,
order_url,
product_price_amount,
organization_name,
}: schemas['MaintainerNewProductSaleNotificationPayload']) {
const displayName = customer_name || customer_email

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

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

return (
<Wrapper>
<Preview>
{displayName} placed an order for {product_name}
</Preview>
<Preview>New {product_name} sale</Preview>
<PolarHeader />

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

<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-4 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" />

<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>
<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>

<IntroWithHi hiMsg="Congratulations!">
{customer_name} purchased <strong>{product_name}</strong> for{' '}
{formatted_price_amount}.
</IntroWithHi>
<Footer email={null} />
</Wrapper>
)
}

NotificationNewSale.PreviewProps = {
customer_email: 'bob@ross.com',
customer_name: 'Bob Ross',
billing_address_country: 'US',
billing_address_city: 'San Francisco',
billing_address_line1: '123 Main St',
customer_name: 'John Doe',
formatted_price_amount: '$45.95',
formatted_billing_reason: 'One-time purchase',
formatted_address_country: 'United States',
product_name: 'Beginners guide to painting',
product_name: 'Ultimate Magento webshop template',
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
80 changes: 2 additions & 78 deletions server/emails/src/types/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -831,54 +831,16 @@ export interface components {
}
/** MaintainerNewProductSaleNotificationPayload */
MaintainerNewProductSaleNotificationPayload: {
/** Customer Email */
customer_email: string
/**
* Customer Name
* @default null
*/
customer_name: 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
/** Customer Name */
customer_name: string
/** Product Name */
product_name: string
/** Product Price Amount */
product_price_amount: number
/**
* Product Image Url
* @default null
*/
product_image_url: string | null
/** Order Id */
order_id: string
/** Order Date */
order_date: string
/** Organization Name */
organization_name: string
/** Organization Slug */
organization_slug: string
billing_reason: components['schemas']['OrderBillingReasonInternal']
/** Formatted Price Amount */
readonly formatted_price_amount: string
/** Formatted Billing Reason */
readonly formatted_billing_reason: string
/** Formatted Address Country */
readonly formatted_address_country: string | null
/** Order Url */
readonly order_url: string
}
/** NotificationCreateAccountEmail */
NotificationCreateAccountEmail: {
Expand Down Expand Up @@ -963,17 +925,6 @@ 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 @@ -1273,25 +1224,6 @@ 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 @@ -1337,12 +1269,6 @@ 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 @@ -1432,8 +1358,6 @@ 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
39 changes: 1 addition & 38 deletions server/polar/notifications/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@
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 @@ -92,49 +89,15 @@ class MaintainerNewPaidSubscriptionNotification(NotificationBase):


class MaintainerNewProductSaleNotificationPayload(NotificationPayloadBase):
customer_email: str
customer_name: str | None = None
billing_address_country: str | None = None
billing_address_city: str | None = None
billing_address_line1: str | None = None
customer_name: str
product_name: str
product_price_amount: int
product_image_url: str | None = None
order_id: str
order_date: str
organization_name: str
organization_slug: str
billing_reason: OrderBillingReasonInternal

@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:
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:
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
33 changes: 2 additions & 31 deletions server/polar/order/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
from polar.event.system import OrderPaidMetadata, SystemEvent, build_system_event
from polar.eventstream.service import publish as eventstream_publish
from polar.exceptions import PolarError
from polar.file.s3 import S3_SERVICES
from polar.held_balance.service import held_balance as held_balance_service
from polar.integrations.stripe.schemas import ProductType
from polar.integrations.stripe.service import stripe as stripe_service
Expand Down Expand Up @@ -1543,44 +1542,16 @@ async def send_admin_notification(
return

if organization.notification_settings["new_order"]:
product_image_url: str | None = None
try:
if product.product_medias and len(product.product_medias) > 0:
first_media = product.product_medias[0].file
product_image_url = S3_SERVICES[first_media.service].get_public_url(
first_media.path
)
except Exception:
pass

billing_address = order.billing_address
customer = order.customer

await notifications_service.send_to_org_members(
session,
org_id=organization.id,
notif=PartialNotification(
type=NotificationType.maintainer_new_product_sale,
payload=MaintainerNewProductSaleNotificationPayload(
customer_email=customer.email,
customer_name=customer.name or order.billing_name,
billing_address_country=billing_address.country
if billing_address
else None,
billing_address_city=billing_address.city
if billing_address
else None,
billing_address_line1=billing_address.line1
if billing_address
else None,
customer_name=order.customer.email,
product_name=product.name,
product_price_amount=order.net_amount,
product_image_url=product_image_url,
order_id=str(order.id),
order_date=order.created_at.isoformat(),
organization_name=organization.name,
organization_slug=organization.slug,
billing_reason=order.billing_reason,
organization_name=organization.slug,
),
),
)
Expand Down
Loading