Skip to content

Commit

Permalink
server/integrations/plain: add endpoint to return Plain customer cards
Browse files Browse the repository at this point in the history
  • Loading branch information
frankie567 committed Jan 6, 2025
1 parent 80e1946 commit 60a8b17
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 0 deletions.
3 changes: 3 additions & 0 deletions server/polar/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
router as github_repository_benefit_router,
)
from polar.integrations.google.endpoints import router as google_router
from polar.integrations.plain.endpoints import router as plain_router
from polar.integrations.stripe.endpoints import router as stripe_router
from polar.issue.endpoints import router as issue_router
from polar.license_key.endpoints import router as license_key_router
Expand Down Expand Up @@ -138,3 +139,5 @@
router.include_router(email_update_router)
# /customer-sessions
router.include_router(customer_session_router)
# /integrations/plain
router.include_router(plain_router)
3 changes: 3 additions & 0 deletions server/polar/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ class Settings(BaseSettings):
# Logfire
LOGFIRE_TOKEN: str | None = None

# Plain
PLAIN_REQUEST_SIGNING_SECRET: str | None = None

# AWS (File Downloads)
AWS_ACCESS_KEY_ID: str = "polar-development"
AWS_SECRET_ACCESS_KEY: str = "polar123456789"
Expand Down
Empty file.
34 changes: 34 additions & 0 deletions server/polar/integrations/plain/endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import hashlib
import hmac

from fastapi import Depends, Header, HTTPException, Request

from polar.config import settings
from polar.postgres import AsyncSession, get_db_session
from polar.routing import APIRouter

from .schemas import CustomerCardsRequest, CustomerCardsResponse
from .service import plain as plain_service

router = APIRouter(
prefix="/integrations/plain", tags=["integrations_plain"], include_in_schema=False
)


@router.post("/cards")
async def get_cards(
request: Request,
customer_cards_request: CustomerCardsRequest,
plain_request_signature: str = Header(...),
session: AsyncSession = Depends(get_db_session),
) -> CustomerCardsResponse:
secret = settings.PLAIN_REQUEST_SIGNING_SECRET
if secret is None:
raise HTTPException(status_code=404)

raw_body = await request.body()
signature = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, plain_request_signature):
raise HTTPException(status_code=403)

return await plain_service.get_cards(session, customer_cards_request)
37 changes: 37 additions & 0 deletions server/polar/integrations/plain/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from enum import StrEnum
from typing import Any

from pydantic import BaseModel


class CustomerCardKey(StrEnum):
organization = "organization"
customer = "customer"
latest_order = "latest_order"


class CustomerCardCustomer(BaseModel):
id: str
email: str
externalId: str | None


class CustomerCardThread(BaseModel):
id: str
externalId: str | None


class CustomerCardsRequest(BaseModel):
cardKeys: list[CustomerCardKey]
customer: CustomerCardCustomer
thread: CustomerCardThread | None


class CustomerCard(BaseModel):
key: CustomerCardKey
timeToLiveSeconds: int
components: list[dict[str, Any]] | None


class CustomerCardsResponse(BaseModel):
cards: list[CustomerCard]
271 changes: 271 additions & 0 deletions server/polar/integrations/plain/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
import asyncio

import pycountry
import pycountry.db
from sqlalchemy import func, or_, select

from polar.models import Customer, Organization, User, UserOrganization
from polar.postgres import AsyncSession

from .schemas import (
CustomerCard,
CustomerCardKey,
CustomerCardsRequest,
CustomerCardsResponse,
)


class PlainService:
async def get_cards(
self, session: AsyncSession, request: CustomerCardsRequest
) -> CustomerCardsResponse:
tasks: list[asyncio.Task[CustomerCard | None]] = []
async with asyncio.TaskGroup() as tg:
if CustomerCardKey.organization in request.cardKeys:
tasks.append(
tg.create_task(self._get_organization_card(session, request))
)
if CustomerCardKey.customer in request.cardKeys:
tasks.append(tg.create_task(self.get_customer_card(session, request)))

cards = [card for task in tasks if (card := task.result()) is not None]
return CustomerCardsResponse(cards=cards)

async def _get_organization_card(
self, session: AsyncSession, request: CustomerCardsRequest
) -> CustomerCard | None:
email = request.customer.email

statement = (
select(Organization)
.join(
UserOrganization,
Organization.id == UserOrganization.organization_id,
isouter=True,
)
.join(User, User.id == UserOrganization.user_id)
.join(Customer, Customer.organization_id == Organization.id, isouter=True)
.where(
or_(
func.lower(Customer.email) == email.lower(),
func.lower(User.email) == email.lower(),
)
)
)
result = await session.execute(statement)
organization = result.unique().scalar_one_or_none()

if organization is None:
return None

return CustomerCard(
key=CustomerCardKey.organization,
timeToLiveSeconds=86400,
components=[
{
"componentContainer": {
"containerContent": [
{
"componentRow": {
"rowMainContent": [
{"componentText": {"text": organization.name}},
{
"componentText": {
"text": organization.slug,
"textColor": "MUTED",
}
},
],
"rowAsideContent": [
{
"componentLinkButton": {
"linkButtonLabel": "Omni ↗",
"linkButtonUrl": "https://example.com",
}
}
],
}
},
{"componentDivider": {"dividerSpacingSize": "M"}},
{
"componentRow": {
"rowMainContent": [
{
"componentText": {
"text": "ID",
"textSize": "S",
"textColor": "MUTED",
}
},
{"componentText": {"text": organization.id}},
],
"rowAsideContent": [
{
"componentCopyButton": {
"copyButtonValue": organization.id,
"copyButtonTooltipLabel": "Copy Organization ID",
}
}
],
}
},
{"componentSpacer": {"spacerSize": "M"}},
{
"componentText": {
"text": "Created At",
"textSize": "S",
"textColor": "MUTED",
}
},
{
"componentText": {
"text": organization.created_at.date().isoformat()
}
},
]
}
}
],
)

async def get_customer_card(
self, session: AsyncSession, request: CustomerCardsRequest
) -> CustomerCard | None:
email = request.customer.email

statement = select(Customer).where(func.lower(Customer.email) == email.lower())
result = await session.execute(statement)
customer = result.unique().scalar_one_or_none()

if customer is None:
return None

country: pycountry.db.Country | None = None
if customer.billing_address and customer.billing_address.country:
country = pycountry.countries.get(alpha_2=customer.billing_address.country)

return CustomerCard(
key=CustomerCardKey.customer,
timeToLiveSeconds=86400,
components=[
{
"componentContainer": {
"containerContent": [
{
"componentRow": {
"rowMainContent": [
{"componentText": {"text": customer.name}},
],
"rowAsideContent": [
{
"componentLinkButton": {
"linkButtonLabel": "Omni ↗",
"linkButtonUrl": "https://example.com",
}
}
],
}
},
{"componentDivider": {"dividerSpacingSize": "M"}},
{
"componentRow": {
"rowMainContent": [
{
"componentText": {
"text": "ID",
"textSize": "S",
"textColor": "MUTED",
}
},
{"componentText": {"text": customer.id}},
],
"rowAsideContent": [
{
"componentCopyButton": {
"copyButtonValue": customer.id,
"copyButtonTooltipLabel": "Copy Customer ID",
}
}
],
}
},
{"componentSpacer": {"spacerSize": "M"}},
{
"componentText": {
"text": "Created At",
"textSize": "S",
"textColor": "MUTED",
}
},
{
"componentText": {
"text": customer.created_at.date().isoformat()
}
},
*(
[
{"componentSpacer": {"spacerSize": "M"}},
{
"componentRow": {
"rowMainContent": [
{
"componentText": {
"text": "Country",
"textSize": "S",
"textColor": "MUTED",
}
},
{
"componentText": {
"text": country.name,
}
},
],
"rowAsideContent": [
{
"componentText": {
"text": country.flag
}
}
],
}
},
]
if country
else []
),
{"componentSpacer": {"spacerSize": "M"}},
{
"componentRow": {
"rowMainContent": [
{
"componentText": {
"text": "Stripe Customer ID",
"textSize": "S",
"textColor": "MUTED",
}
},
{
"componentText": {
"text": customer.stripe_customer_id,
}
},
],
"rowAsideContent": [
{
"componentLinkButton": {
"linkButtonLabel": "Stripe ↗",
"linkButtonUrl": f"https://dashboard.stripe.com/customers/{customer.stripe_customer_id}",
}
}
],
}
},
]
}
}
],
)


plain = PlainService()

0 comments on commit 60a8b17

Please sign in to comment.