Claude Cursor Skill

polar-billing

This skill should be used when working on Polar billing system, Stripe integration, subscription lifecycle, checkout flows, or benefit provisioning.

LLM Mart · 0 points · 6 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download fcakyon-claude-codex-settings-plugins_polar-skills_skills_polar-billing-4632eb3.zip · 7 KB
Part of fcakyon/claude-codex-settings — 83 skills

Install

skills CLI npx skills add https://github.com/fcakyon/claude-codex-settings/tree/main/plugins/polar-skills/skills/polar-billing
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install fcakyon-claude-codex-settings@llmmart
Git git clone https://github.com/fcakyon/claude-codex-settings.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole fcakyon/claude-codex-settings collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Polar Billing System

Comprehensive guide to Polar's billing infrastructure, covering entities, flows, Stripe integration, and benefit provisioning.

Quick Reference

Checkout → Payment → Order → Transaction → Benefits
                         ↓
                   Subscription (if recurring)
                         ↓
                   Subscription Cycle → Order → ...

Table of Contents

  1. Core Entities
  2. Entity Relationships
  3. Main Services
  4. Dramatiq Background Tasks
  5. Stripe Integration
  6. Subscription Lifecycle
  7. Proration System
  8. Benefits & Credits
  9. Dunning & Payment Retry
  10. Transaction Ledger
  11. Key File Locations

1. Core Entities

Checkout

File: server/polar/models/checkout.py

Shopping cart/payment session before order confirmation.

Field Type Description
status CheckoutStatus open, expired, confirmed, succeeded, failed
payment_processor PaymentProcessor stripe, manual
client_secret str Unique identifier for frontend
amount, currency int, str Price in cents
tax_amount, discount_amount int Calculated amounts
allow_trial, trial_end bool, datetime Trial configuration
seats int For seat-based products

Relationships: organization, customer, product, product_price, discount, subscription (for upgrades)


CheckoutLink

File: server/polar/models/checkout_link.py

Persistent URL that creates Checkout Sessions on visit.

Field Type Description
client_secret str Unique identifier for the URL
seats int | None Preconfigured seat count for seat-based pricing
discount_id UUID | None Preset discount to apply
trial_interval, trial_interval_count Trial config Override product trial settings

Relationships: organization, products, discount


Order

File: server/polar/models/order.py

Represents a billing event (one-time purchase or subscription cycle).

Field Type Description
status OrderStatus pending, paid, refunded, partially_refunded
billing_reason OrderBillingReason purchase, subscription_create, subscription_cycle, subscription_update
subtotal_amount int Amount before discount/tax
discount_amount int Discount applied
tax_amount int Tax collected
applied_balance_amount int Account balance applied
platform_fee_amount int Polar's fee
refunded_amount int Already refunded
next_payment_attempt_at datetime Dunning retry time

Computed Properties:

  • net_amount = subtotal - discount
  • total_amount = net + tax
  • due_amount = max(0, total + applied_balance)
  • payout_amount = net - platform_fee - refunded

Subscription

File: server/polar/models/subscription.py

Recurring billing relationship.

Field Type Description
status SubscriptionStatus incomplete, trialing, active, past_due, canceled, unpaid
amount, currency int, str Subscription gross price
net_amount int Net amount (gross minus inclusive tax, equal to gross if tax-exclusive)
tax_behavior TaxBehavior | None Inclusive, exclusive, or null (set at creation)
recurring_interval Interval month, year
current_period_start/end datetime Billing period
trial_start/end datetime Trial period
cancel_at_period_end bool Scheduled cancellation
canceled_at, ended_at datetime Lifecycle timestamps
past_due_at datetime When payment failed
seats int For seat-based pricing

Relationships: organization, customer, product, payment_method, discount, meters, grants (benefits)


Transaction

File: server/polar/models/transaction.py

All money flows in the system.

Field Type Description
type TransactionType payment, processor_fee, refund, dispute, balance, payout
processor Processor stripe, manual
amount, currency int, str Transaction amount
tax_amount int Tax portion

Self-referential relationships: payment_transaction, balance_transactions, incurred_transactions


Payment

File: server/polar/models/payment.py

Individual payment transaction.

Field Type Description
status PaymentStatus pending, succeeded, failed
processor_id str Stripe charge ID
method str card, bank_transfer, etc.
trigger PaymentTrigger | None What initiated payment: purchase, subscription_cycle, retry_dunning, retry_customer, retry_payment_method_update, retry_admin
decline_reason str Why payment failed
risk_level, risk_score str, int Fraud assessment

Refund

File: server/polar/models/refund.py

Field Type Description
status RefundStatus pending, succeeded, failed, canceled
reason RefundReason duplicate, fraudulent, customer_request, etc.
amount, tax_amount int Refund amounts
revoke_benefits bool Whether to revoke customer benefits

Customer

File: server/polar/models/customer.py

Field Type Description
email, name str Contact info
billing_name str Name for invoices (falls back to name)
stripe_customer_id str Stripe link
billing_address Address Stored address
tax_id str For tax compliance

Product & ProductPrice

Files: server/polar/models/product.py, server/polar/models/product_price.py

ProductPrice Types Description
ProductPriceFixed Fixed amount (set price_amount=0 for free)
ProductPriceCustom Merchant sets at checkout
ProductPriceMeteredUnit Pay-per-unit
ProductPriceSeatUnit Per-seat with tiers

BillingEntry

File: server/polar/models/billing_entry.py

Audit log for billing calculations.

Field Type Description
type BillingEntryType cycle, proration, metered, seats_increase, seats_decrease
direction Direction debit, credit
amount int Entry amount

2. Entity Relationships

Organization
├── Product
│   ├── ProductPrice (multiple per product)
│   └── ProductBenefit → Benefit
├── Customer
│   ├── Subscription → Organization, Product, Discount
│   │   ├── SubscriptionProductPrice
│   │   ├── SubscriptionMeter
│   │   └── BenefitGrant
│   ├── Order → Product, Subscription
│   │   └── OrderItem
│   ├── PaymentMethod
│   └── Wallet
├── Checkout → Customer, Product
├── Discount
│   └── DiscountRedemption
└── Account (for payouts)
    └── Payout → Transaction

Transaction (ledger)
├── payment → Order, Customer
├── refund → Refund, Order
├── dispute → Dispute, Order
├── processor_fee → parent payment
└── payout → Account

3. Main Services

SubscriptionService

File: server/polar/subscription/service.py

Core subscription operations:

# Creation
create_or_update_from_checkout(checkout, payment_method) → (Subscription, created)

# Updates
update_product(subscription, product_id, proration_behavior, discount=None)
update_seats(subscription, seats, proration_behavior)
update_discount(subscription, discount)  # discount: UUID | Literal["unset"]
update_trial(subscription, trial_end)
update_metadata(subscription, metadata)

# Lifecycle
cycle(subscription)  # Period renewal
cancel(subscription)  # At period end
revoke(subscription)  # Immediately
uncancel(subscription)

# Benefits
enqueue_benefits_grants(task="grant"|"revoke", customer, product)

OrderService

File: server/polar/order/service.py

create_from_checkout(checkout)  # One-time purchase
create_subscription_order(subscription, billing_reason)  # Recurring
trigger_payment(order)  # Charge customer
create_order_balance(order)  # Ledger entries

CheckoutService

File: server/polar/checkout/service.py

create(product, customer_data, discount_code)
confirm(checkout)  # Lock checkout for payment
handle_stripe_success(checkout, charge)
handle_free_success(checkout)  # No payment needed

PaymentService

File: server/polar/payment/service.py

upsert_from_stripe_charge(charge, checkout, order)
handle_success(payment)  # Complete order
handle_failure(payment)  # Update order status

RefundService

File: server/polar/refund/service.py

create(order, amount, reason, revoke_benefits)
upsert_from_stripe(stripe_refund)
# Also enqueues chargeback prevention notice for dispute_prevention refunds

BenefitGrantService

File: server/polar/benefit/grant/service.py

enqueue_benefits_grants(task, customer, product, order=None, subscription=None)
grant_benefit(customer, benefit)
revoke_benefit(customer, benefit)

4. Dramatiq Background Tasks

Subscription Tasks

File: server/polar/subscription/tasks.py

Task Trigger Action
subscription.cycle Scheduler at period end Renew subscription, create order
subscription.update_product_benefits_grants Product benefits changed Update all grants
subscription.cancel_customer Customer deleted Cancel all billable subscriptions (trialing, active, past_due)

Order Tasks

File: server/polar/order/tasks.py

Task Trigger Action
order.create_subscription_order Subscription cycle Create billing order
order.trigger_payment Order ready Charge payment method
order.balance Payment success Create ledger entries
order.invoice Order created Generate PDF invoice
order.process_dunning Hourly cron Find orders for retry
order.process_dunning_order Individual retry Retry single payment

Stripe Webhook Tasks

File: server/polar/integrations/stripe/tasks.py

Task Stripe Event Action
charge.succeeded Payment complete Create order, provision benefits
charge.failed Payment failed Mark order failed
charge.updated Charge settled Create ledger transaction
refund.created/updated Refund processed Update refund record
charge.dispute.created Chargeback Create dispute, revoke benefits
payout.paid Payout complete Update payout status

Benefit Tasks

File: server/polar/benefit/tasks.py

Task Trigger Action
benefit.enqueue_benefits_grants Order/subscription Queue individual grants
benefit.grant Individual benefit Provision access (GitHub, Discord, etc.)
benefit.revoke Cancellation/refund Remove access
benefit.cycle Subscription renewal Reset credits with rollover

Checkout Tasks

File: server/polar/checkout/tasks.py

Task Trigger Action
checkout.handle_free_success Free product Complete without payment
checkout.expire_open_checkouts Every 15 min Mark expired checkouts

Payout Tasks

File: server/polar/payout/tasks.py

Task Trigger Action
payout.trigger_stripe_payouts Daily 00:15 UTC Initiate pending payouts
payout.created Payout created Event hook (fires for held payouts too)
payout.transfer After payout.created Stripe transfer (skipped for held)
payout.release_held_payouts Org approved Move held → pending, enqueue transfers
payout.cancel_account_payouts Org denied/blocked/offboarding Cancel held+pending payouts
payout.cancel_held_payouts Payout account swap Cancel only held payouts on old account

Refund Tasks

File: server/polar/refund/tasks.py

Task Trigger Action
refund.send_chargeback_prevention_notice Dispute prevention refund created Email org owners/admins about refund

5. Stripe Integration

Webhook Endpoints

File: server/polar/integrations/stripe/endpoints.py

  • /v1/integrations/stripe/webhook - Direct webhooks
  • /v1/integrations/stripe/webhook-connect - Connect account webhooks

Implemented Webhooks

Payment Flow:

  • payment_intent.succeeded - Payment complete
  • payment_intent.payment_failed - Payment failed
  • setup_intent.succeeded - Card saved
  • charge.pending/failed/succeeded/updated - Charge lifecycle

Refunds:

  • refund.created/updated/failed

Disputes:

  • charge.dispute.created/updated/closed

Connect:

  • account.updated - Account info changed
  • payout.updated/paid - Payout lifecycle

Webhook Processing Flow

Stripe POST → Verify signature → ExternalEvent.enqueue()
                                        ↓
                               Store in external_events table
                                        ↓
                               Enqueue Dramatiq task
                                        ↓
                               Worker processes async
                                        ↓
                               Mark handled_at on success

StripeService

File: server/polar/integrations/stripe/service.py

Key methods:

  • create_payment_intent(), create_setup_intent()
  • create_refund(), get_refund()
  • create_tax_calculation(), create_tax_transaction()
  • transfer(), create_payout()

6. Subscription Lifecycle

Creation Flow

1. Checkout created (status=open)
2. Customer completes payment
3. Stripe charge.succeeded webhook
4. payment.handle_success() called
5. checkout_service.handle_stripe_success()
6. subscription_service.create_or_update_from_checkout()
   - Creates Subscription (status=active or trialing)
   - Sets billing period
   - Applies discount
   - Resets meters
7. Enqueue benefit grants
8. Send confirmation email

Cycle Flow (Renewal)

1. APScheduler triggers at period end
2. subscription.cycle task runs
3. subscription_service.cycle()
   - Check cancel_at_period_end
   - If true: set status=canceled, revoke benefits
   - If false: advance period dates, check discount expiry
4. Create billing entry (type=cycle)
5. Enqueue order.create_subscription_order
6. Order created with billing_reason=subscription_cycle
7. Enqueue order.trigger_payment
8. Stripe charges payment method
9. charge.succeeded → ledger entries → benefits renewed

Cancellation Flow

At Period End:

subscription_service.cancel(subscription)
# Sets cancel_at_period_end=True, ends_at=current_period_end
# Benefits remain until period ends
# On next cycle: status=canceled, benefits revoked

Immediately:

subscription_service.revoke(subscription)
# Sets status=canceled, ended_at=now
# Benefits revoked immediately
# Seats canceled if seat-based

Trial Flow

1. Checkout with trial_end set
2. Subscription created with status=trialing
3. No payment during trial
4. At trial_end, cycle task runs
5. Status transitions to active
6. Order created with billing_reason=subscription_cycle_after_trial
7. First payment charged

7. Proration System

When Prorations Occur

  1. Product change - Upgrade/downgrade to different tier
  2. Seat change - Add/remove seats
  3. Interval change - Monthly to yearly

Proration Calculation

# Calculate time remaining in period
pct_remaining = (period_end - now) / (period_end - period_start)

# Old product credit (what they paid but won't use)
old_credit = old_price * old_pct_remaining

# New product debit (what they owe for remainder)
new_debit = new_price * new_pct_remaining

# Net proration
net = new_debit - old_credit

Proration Behaviors

Behavior Action
prorate Add to next invoice
invoice Create order immediately

BillingEntry for Prorations

# Credit entry (old product)
BillingEntry(
    type=BillingEntryType.proration,
    direction=BillingEntryDirection.credit,
    amount=prorated_old_amount
)

# Debit entry (new product)
BillingEntry(
    type=BillingEntryType.proration,
    direction=BillingEntryDirection.debit,
    amount=prorated_new_amount
)

Seat Proration

# Adding 2 seats at $10/seat with 50% time remaining
delta_amount = 2 * $10 * 0.5 = $10

BillingEntry(
    type=BillingEntryType.subscription_seats_increase,
    direction=BillingEntryDirection.debit,
    amount=1000  # cents
)

8. Benefits & Credits

Benefit Types

Type Description Grant Action
meter_credit Usage allowances Create meter_credited event
github_repository Repo access Add to GitHub team
discord Server role Assign Discord role
license_keys License distribution Generate key
downloadables File access Grant download permission
slack_shared_channel Slack Connect channel Create/invite to shared channel
feature_flag Feature toggle (API-only) None — merchant reads via API
custom Customer-visible note None — displayed in customer portal

Benefit Grant Flow

1. Order/Subscription created
2. enqueue_benefits_grants(task="grant")
3. For each benefit in product:
   - Skip if already granted
   - Enqueue benefit.grant task
4. benefit.grant task:
   - Get/create BenefitGrant record
   - Call strategy.grant() (type-specific)
   - Set granted_at
   - Store properties
   - Send webhook

Benefit Revocation Flow

1. Subscription canceled or order refunded
2. enqueue_benefits_grants(task="revoke")
3. For each granted benefit:
   - Enqueue benefit.revoke task
4. benefit.revoke task:
   - Call strategy.revoke() (type-specific)
   - Set revoked_at
   - Send webhook

Meter Credits

Grant:

# Create event with units
Event(type="meter_credited", units=100)
# Update CustomerMeter

Cycle (renewal):

# Calculate rollover
rollover = min(remaining_units, rollover_limit)
# Reset meter
Event(type="meter_reset")
# Credit new period + rollover
Event(type="meter_credited", units=base_units + rollover)

Revoke:

# Negative credit event
Event(type="meter_credited", units=-remaining_units)

Grace Period

Organizations can configure benefit_revocation_grace_period (days) to delay benefit revocation for past_due subscriptions.


9. Dunning & Payment Retry

Dunning Process

1. order.process_dunning runs hourly
2. Finds orders where next_payment_attempt_at <= now
3. For each order:
   - Enqueue order.process_dunning_order
4. process_dunning_order:
   - Get customer's payment method
   - Attempt payment via Stripe
   - On success: mark order paid
   - On failure: schedule next attempt

Retry Schedule

Configured in organization settings. Typical pattern:

  • Day 1: First failure
  • Day 3: Retry 1
  • Day 5: Retry 2
  • Day 7: Final retry, then mark unpaid

Subscription Status During Dunning

payment fails → status=past_due, past_due_at=now
               ↓
         benefits may continue (grace period)
               ↓
         retry succeeds → status=active
               ↓
         retry fails → status=unpaid, benefits revoked

10. Transaction Ledger

Transaction Types

Type Description
payment Customer payment received
processor_fee Stripe fees
refund Money returned to customer
refund_reversal Refund failed/reversed
dispute Chargeback loss
dispute_reversal Won dispute
balance Internal balance transfer
payout Money sent to creator

Creating Payment Transactions

1. charge.updated webhook (charge settled)
2. Get balance_transaction from Stripe
3. Extract settlement amount and fees
4. Create Transaction(type=payment)
5. Enqueue processor_fee.create_payment_fees
6. Create Transaction(type=processor_fee)

Payout Flow

1. Creator has balance from transactions
2. payout.trigger_stripe_payouts (daily)
3. Calculate available balance
4. Create Payout record
   - ACTIVE/OFFBOARDED org: status=pending, enqueue payout.created + payout.transfer
   - REVIEW/SNOOZED org: status=held, enqueue payout.created only
5. stripe_service.transfer() to Connect account (skipped for held)
6. stripe_service.create_payout() to bank
7. payout.paid webhook → update status

Held payout lifecycle:
- When org approved: payout.release_held_payouts → status=pending, enqueue transfer
- When org denied/blocked/offboarding: payout.cancel_account_payouts → cancel + refund
- When payout account swapped: payout.cancel_held_payouts (old account only)

11. Key File Locations

Models

server/polar/models/
├── checkout.py
├── order.py
├── order_item.py
├── subscription.py
├── subscription_product_price.py
├── transaction.py
├── payment.py
├── refund.py
├── dispute.py
├── payout.py
├── customer.py
├── product.py
├── product_price.py
├── discount.py
├── benefit.py
├── benefit_grant.py
└── billing_entry.py

Services

server/polar/
├── subscription/service.py
├── order/service.py
├── checkout/service.py
├── payment/service.py
├── refund/service.py
├── dispute/service.py
├── payout/service.py
├── benefit/
│   ├── service.py
│   ├── grant/service.py
│   └── strategies/
│       ├── meter_credit/service.py
│       ├── github_repository/service.py
│       ├── discord/service.py
│       └── ...
└── transaction/service/
    ├── payment.py
    ├── refund.py
    └── dispute.py

Background Tasks

server/polar/
├── subscription/tasks.py
├── order/tasks.py
├── checkout/tasks.py
├── benefit/tasks.py
├── payout/tasks.py
├── refund/tasks.py
└── integrations/stripe/tasks.py

Stripe Integration

server/polar/integrations/stripe/
├── endpoints.py    # Webhook handlers
├── service.py      # Stripe API wrapper
├── tasks.py        # Webhook processing tasks
└── payment.py      # Payment resolution helpers

Common Debugging Scenarios

Payment Failed

  1. Check Payment record for decline_reason
  2. Check Order.status and next_payment_attempt_at
  3. Look at external_events for Stripe webhook

Benefits Not Granted

  1. Check BenefitGrant record for errors
  2. Look at benefit.grant task in Dramatiq logs
  3. Verify product has benefits attached

Proration Issues

  1. Check BillingEntry records for subscription
  2. Verify billing_reason on Order
  3. Check subscription's current_period dates

Subscription Not Cycling

  1. Check scheduler_locked_at on subscription
  2. Verify APScheduler is running
  3. Check subscription.cycle task logs
Files (claude-codex-settings)
  • SKILL.md 23.4 KB
    ---
    name: polar-billing
    description: "This skill should be used when working on Polar billing system, Stripe integration, subscription lifecycle, checkout flows, or benefit provisioning."
    license: Apache-2.0
    ---
    
    # Polar Billing System
    
    Comprehensive guide to Polar's billing infrastructure, covering entities, flows, Stripe integration, and benefit provisioning.
    
    ## Quick Reference
    
    ```
    Checkout → Payment → Order → Transaction → Benefits
                             ↓
                       Subscription (if recurring)
                             ↓
                       Subscription Cycle → Order → ...
    ```
    
    ## Table of Contents
    
    1. [Core Entities](#1-core-entities)
    2. [Entity Relationships](#2-entity-relationships)
    3. [Main Services](#3-main-services)
    4. [Dramatiq Background Tasks](#4-dramatiq-background-tasks)
    5. [Stripe Integration](#5-stripe-integration)
    6. [Subscription Lifecycle](#6-subscription-lifecycle)
    7. [Proration System](#7-proration-system)
    8. [Benefits & Credits](#8-benefits--credits)
    9. [Dunning & Payment Retry](#9-dunning--payment-retry)
    10. [Transaction Ledger](#10-transaction-ledger)
    11. [Key File Locations](#11-key-file-locations)
    
    ---
    
    ## 1. Core Entities
    
    ### Checkout
    **File:** `server/polar/models/checkout.py`
    
    Shopping cart/payment session before order confirmation.
    
    | Field | Type | Description |
    |-------|------|-------------|
    | `status` | CheckoutStatus | open, expired, confirmed, succeeded, failed |
    | `payment_processor` | PaymentProcessor | stripe, manual |
    | `client_secret` | str | Unique identifier for frontend |
    | `amount`, `currency` | int, str | Price in cents |
    | `tax_amount`, `discount_amount` | int | Calculated amounts |
    | `allow_trial`, `trial_end` | bool, datetime | Trial configuration |
    | `seats` | int | For seat-based products |
    
    **Relationships:** organization, customer, product, product_price, discount, subscription (for upgrades)
    
    ---
    
    ### CheckoutLink
    **File:** `server/polar/models/checkout_link.py`
    
    Persistent URL that creates Checkout Sessions on visit.
    
    | Field | Type | Description |
    |-------|------|-------------|
    | `client_secret` | str | Unique identifier for the URL |
    | `seats` | int \| None | Preconfigured seat count for seat-based pricing |
    | `discount_id` | UUID \| None | Preset discount to apply |
    | `trial_interval`, `trial_interval_count` | Trial config | Override product trial settings |
    
    **Relationships:** organization, products, discount
    
    ---
    
    ### Order
    **File:** `server/polar/models/order.py`
    
    Represents a billing event (one-time purchase or subscription cycle).
    
    | Field | Type | Description |
    |-------|------|-------------|
    | `status` | OrderStatus | pending, paid, refunded, partially_refunded |
    | `billing_reason` | OrderBillingReason | purchase, subscription_create, subscription_cycle, subscription_update |
    | `subtotal_amount` | int | Amount before discount/tax |
    | `discount_amount` | int | Discount applied |
    | `tax_amount` | int | Tax collected |
    | `applied_balance_amount` | int | Account balance applied |
    | `platform_fee_amount` | int | Polar's fee |
    | `refunded_amount` | int | Already refunded |
    | `next_payment_attempt_at` | datetime | Dunning retry time |
    
    **Computed Properties:**
    - `net_amount` = subtotal - discount
    - `total_amount` = net + tax
    - `due_amount` = max(0, total + applied_balance)
    - `payout_amount` = net - platform_fee - refunded
    
    ---
    
    ### Subscription
    **File:** `server/polar/models/subscription.py`
    
    Recurring billing relationship.
    
    | Field | Type | Description |
    |-------|------|-------------|
    | `status` | SubscriptionStatus | incomplete, trialing, active, past_due, canceled, unpaid |
    | `amount`, `currency` | int, str | Subscription gross price |
    | `net_amount` | int | Net amount (gross minus inclusive tax, equal to gross if tax-exclusive) |
    | `tax_behavior` | TaxBehavior \| None | Inclusive, exclusive, or null (set at creation) |
    | `recurring_interval` | Interval | month, year |
    | `current_period_start/end` | datetime | Billing period |
    | `trial_start/end` | datetime | Trial period |
    | `cancel_at_period_end` | bool | Scheduled cancellation |
    | `canceled_at`, `ended_at` | datetime | Lifecycle timestamps |
    | `past_due_at` | datetime | When payment failed |
    | `seats` | int | For seat-based pricing |
    
    **Relationships:** organization, customer, product, payment_method, discount, meters, grants (benefits)
    
    ---
    
    ### Transaction
    **File:** `server/polar/models/transaction.py`
    
    All money flows in the system.
    
    | Field | Type | Description |
    |-------|------|-------------|
    | `type` | TransactionType | payment, processor_fee, refund, dispute, balance, payout |
    | `processor` | Processor | stripe, manual |
    | `amount`, `currency` | int, str | Transaction amount |
    | `tax_amount` | int | Tax portion |
    
    **Self-referential relationships:** payment_transaction, balance_transactions, incurred_transactions
    
    ---
    
    ### Payment
    **File:** `server/polar/models/payment.py`
    
    Individual payment transaction.
    
    | Field | Type | Description |
    |-------|------|-------------|
    | `status` | PaymentStatus | pending, succeeded, failed |
    | `processor_id` | str | Stripe charge ID |
    | `method` | str | card, bank_transfer, etc. |
    | `trigger` | PaymentTrigger \| None | What initiated payment: purchase, subscription_cycle, retry_dunning, retry_customer, retry_payment_method_update, retry_admin |
    | `decline_reason` | str | Why payment failed |
    | `risk_level`, `risk_score` | str, int | Fraud assessment |
    
    ---
    
    ### Refund
    **File:** `server/polar/models/refund.py`
    
    | Field | Type | Description |
    |-------|------|-------------|
    | `status` | RefundStatus | pending, succeeded, failed, canceled |
    | `reason` | RefundReason | duplicate, fraudulent, customer_request, etc. |
    | `amount`, `tax_amount` | int | Refund amounts |
    | `revoke_benefits` | bool | Whether to revoke customer benefits |
    
    ---
    
    ### Customer
    **File:** `server/polar/models/customer.py`
    
    | Field | Type | Description |
    |-------|------|-------------|
    | `email`, `name` | str | Contact info |
    | `billing_name` | str | Name for invoices (falls back to `name`) |
    | `stripe_customer_id` | str | Stripe link |
    | `billing_address` | Address | Stored address |
    | `tax_id` | str | For tax compliance |
    
    ---
    
    ### Product & ProductPrice
    **Files:** `server/polar/models/product.py`, `server/polar/models/product_price.py`
    
    | ProductPrice Types | Description |
    |-------------------|-------------|
    | `ProductPriceFixed` | Fixed amount (set `price_amount=0` for free) |
    | `ProductPriceCustom` | Merchant sets at checkout |
    | `ProductPriceMeteredUnit` | Pay-per-unit |
    | `ProductPriceSeatUnit` | Per-seat with tiers |
    
    ---
    
    ### BillingEntry
    **File:** `server/polar/models/billing_entry.py`
    
    Audit log for billing calculations.
    
    | Field | Type | Description |
    |-------|------|-------------|
    | `type` | BillingEntryType | cycle, proration, metered, seats_increase, seats_decrease |
    | `direction` | Direction | debit, credit |
    | `amount` | int | Entry amount |
    
    ---
    
    ## 2. Entity Relationships
    
    ```
    Organization
    ├── Product
    │   ├── ProductPrice (multiple per product)
    │   └── ProductBenefit → Benefit
    ├── Customer
    │   ├── Subscription → Organization, Product, Discount
    │   │   ├── SubscriptionProductPrice
    │   │   ├── SubscriptionMeter
    │   │   └── BenefitGrant
    │   ├── Order → Product, Subscription
    │   │   └── OrderItem
    │   ├── PaymentMethod
    │   └── Wallet
    ├── Checkout → Customer, Product
    ├── Discount
    │   └── DiscountRedemption
    └── Account (for payouts)
        └── Payout → Transaction
    
    Transaction (ledger)
    ├── payment → Order, Customer
    ├── refund → Refund, Order
    ├── dispute → Dispute, Order
    ├── processor_fee → parent payment
    └── payout → Account
    ```
    
    ---
    
    ## 3. Main Services
    
    ### SubscriptionService
    **File:** `server/polar/subscription/service.py`
    
    Core subscription operations:
    
    ```python
    # Creation
    create_or_update_from_checkout(checkout, payment_method) → (Subscription, created)
    
    # Updates
    update_product(subscription, product_id, proration_behavior, discount=None)
    update_seats(subscription, seats, proration_behavior)
    update_discount(subscription, discount)  # discount: UUID | Literal["unset"]
    update_trial(subscription, trial_end)
    update_metadata(subscription, metadata)
    
    # Lifecycle
    cycle(subscription)  # Period renewal
    cancel(subscription)  # At period end
    revoke(subscription)  # Immediately
    uncancel(subscription)
    
    # Benefits
    enqueue_benefits_grants(task="grant"|"revoke", customer, product)
    ```
    
    ### OrderService
    **File:** `server/polar/order/service.py`
    
    ```python
    create_from_checkout(checkout)  # One-time purchase
    create_subscription_order(subscription, billing_reason)  # Recurring
    trigger_payment(order)  # Charge customer
    create_order_balance(order)  # Ledger entries
    ```
    
    ### CheckoutService
    **File:** `server/polar/checkout/service.py`
    
    ```python
    create(product, customer_data, discount_code)
    confirm(checkout)  # Lock checkout for payment
    handle_stripe_success(checkout, charge)
    handle_free_success(checkout)  # No payment needed
    ```
    
    ### PaymentService
    **File:** `server/polar/payment/service.py`
    
    ```python
    upsert_from_stripe_charge(charge, checkout, order)
    handle_success(payment)  # Complete order
    handle_failure(payment)  # Update order status
    ```
    
    ### RefundService
    **File:** `server/polar/refund/service.py`
    
    ```python
    create(order, amount, reason, revoke_benefits)
    upsert_from_stripe(stripe_refund)
    # Also enqueues chargeback prevention notice for dispute_prevention refunds
    ```
    
    ### BenefitGrantService
    **File:** `server/polar/benefit/grant/service.py`
    
    ```python
    enqueue_benefits_grants(task, customer, product, order=None, subscription=None)
    grant_benefit(customer, benefit)
    revoke_benefit(customer, benefit)
    ```
    
    ---
    
    ## 4. Dramatiq Background Tasks
    
    ### Subscription Tasks
    **File:** `server/polar/subscription/tasks.py`
    
    | Task | Trigger | Action |
    |------|---------|--------|
    | `subscription.cycle` | Scheduler at period end | Renew subscription, create order |
    | `subscription.update_product_benefits_grants` | Product benefits changed | Update all grants |
    | `subscription.cancel_customer` | Customer deleted | Cancel all billable subscriptions (trialing, active, past_due) |
    
    ### Order Tasks
    **File:** `server/polar/order/tasks.py`
    
    | Task | Trigger | Action |
    |------|---------|--------|
    | `order.create_subscription_order` | Subscription cycle | Create billing order |
    | `order.trigger_payment` | Order ready | Charge payment method |
    | `order.balance` | Payment success | Create ledger entries |
    | `order.invoice` | Order created | Generate PDF invoice |
    | `order.process_dunning` | **Hourly cron** | Find orders for retry |
    | `order.process_dunning_order` | Individual retry | Retry single payment |
    
    ### Stripe Webhook Tasks
    **File:** `server/polar/integrations/stripe/tasks.py`
    
    | Task | Stripe Event | Action |
    |------|--------------|--------|
    | `charge.succeeded` | Payment complete | Create order, provision benefits |
    | `charge.failed` | Payment failed | Mark order failed |
    | `charge.updated` | Charge settled | Create ledger transaction |
    | `refund.created/updated` | Refund processed | Update refund record |
    | `charge.dispute.created` | Chargeback | Create dispute, revoke benefits |
    | `payout.paid` | Payout complete | Update payout status |
    
    ### Benefit Tasks
    **File:** `server/polar/benefit/tasks.py`
    
    | Task | Trigger | Action |
    |------|---------|--------|
    | `benefit.enqueue_benefits_grants` | Order/subscription | Queue individual grants |
    | `benefit.grant` | Individual benefit | Provision access (GitHub, Discord, etc.) |
    | `benefit.revoke` | Cancellation/refund | Remove access |
    | `benefit.cycle` | Subscription renewal | Reset credits with rollover |
    
    ### Checkout Tasks
    **File:** `server/polar/checkout/tasks.py`
    
    | Task | Trigger | Action |
    |------|---------|--------|
    | `checkout.handle_free_success` | Free product | Complete without payment |
    | `checkout.expire_open_checkouts` | **Every 15 min** | Mark expired checkouts |
    
    ### Payout Tasks
    **File:** `server/polar/payout/tasks.py`
    
    | Task | Trigger | Action |
    |------|---------|--------|
    | `payout.trigger_stripe_payouts` | **Daily 00:15 UTC** | Initiate pending payouts |
    | `payout.created` | Payout created | Event hook (fires for held payouts too) |
    | `payout.transfer` | After payout.created | Stripe transfer (skipped for held) |
    | `payout.release_held_payouts` | Org approved | Move held → pending, enqueue transfers |
    | `payout.cancel_account_payouts` | Org denied/blocked/offboarding | Cancel held+pending payouts |
    | `payout.cancel_held_payouts` | Payout account swap | Cancel only held payouts on old account |
    
    ### Refund Tasks
    **File:** `server/polar/refund/tasks.py`
    
    | Task | Trigger | Action |
    |------|---------|--------|
    | `refund.send_chargeback_prevention_notice` | Dispute prevention refund created | Email org owners/admins about refund |
    
    ---
    
    ## 5. Stripe Integration
    
    ### Webhook Endpoints
    **File:** `server/polar/integrations/stripe/endpoints.py`
    
    - `/v1/integrations/stripe/webhook` - Direct webhooks
    - `/v1/integrations/stripe/webhook-connect` - Connect account webhooks
    
    ### Implemented Webhooks
    
    **Payment Flow:**
    - `payment_intent.succeeded` - Payment complete
    - `payment_intent.payment_failed` - Payment failed
    - `setup_intent.succeeded` - Card saved
    - `charge.pending/failed/succeeded/updated` - Charge lifecycle
    
    **Refunds:**
    - `refund.created/updated/failed`
    
    **Disputes:**
    - `charge.dispute.created/updated/closed`
    
    **Connect:**
    - `account.updated` - Account info changed
    - `payout.updated/paid` - Payout lifecycle
    
    ### Webhook Processing Flow
    
    ```
    Stripe POST → Verify signature → ExternalEvent.enqueue()
                                            ↓
                                   Store in external_events table
                                            ↓
                                   Enqueue Dramatiq task
                                            ↓
                                   Worker processes async
                                            ↓
                                   Mark handled_at on success
    ```
    
    ### StripeService
    **File:** `server/polar/integrations/stripe/service.py`
    
    Key methods:
    - `create_payment_intent()`, `create_setup_intent()`
    - `create_refund()`, `get_refund()`
    - `create_tax_calculation()`, `create_tax_transaction()`
    - `transfer()`, `create_payout()`
    
    ---
    
    ## 6. Subscription Lifecycle
    
    ### Creation Flow
    
    ```
    1. Checkout created (status=open)
    2. Customer completes payment
    3. Stripe charge.succeeded webhook
    4. payment.handle_success() called
    5. checkout_service.handle_stripe_success()
    6. subscription_service.create_or_update_from_checkout()
       - Creates Subscription (status=active or trialing)
       - Sets billing period
       - Applies discount
       - Resets meters
    7. Enqueue benefit grants
    8. Send confirmation email
    ```
    
    ### Cycle Flow (Renewal)
    
    ```
    1. APScheduler triggers at period end
    2. subscription.cycle task runs
    3. subscription_service.cycle()
       - Check cancel_at_period_end
       - If true: set status=canceled, revoke benefits
       - If false: advance period dates, check discount expiry
    4. Create billing entry (type=cycle)
    5. Enqueue order.create_subscription_order
    6. Order created with billing_reason=subscription_cycle
    7. Enqueue order.trigger_payment
    8. Stripe charges payment method
    9. charge.succeeded → ledger entries → benefits renewed
    ```
    
    ### Cancellation Flow
    
    **At Period End:**
    ```python
    subscription_service.cancel(subscription)
    # Sets cancel_at_period_end=True, ends_at=current_period_end
    # Benefits remain until period ends
    # On next cycle: status=canceled, benefits revoked
    ```
    
    **Immediately:**
    ```python
    subscription_service.revoke(subscription)
    # Sets status=canceled, ended_at=now
    # Benefits revoked immediately
    # Seats canceled if seat-based
    ```
    
    ### Trial Flow
    
    ```
    1. Checkout with trial_end set
    2. Subscription created with status=trialing
    3. No payment during trial
    4. At trial_end, cycle task runs
    5. Status transitions to active
    6. Order created with billing_reason=subscription_cycle_after_trial
    7. First payment charged
    ```
    
    ---
    
    ## 7. Proration System
    
    ### When Prorations Occur
    
    1. **Product change** - Upgrade/downgrade to different tier
    2. **Seat change** - Add/remove seats
    3. **Interval change** - Monthly to yearly
    
    ### Proration Calculation
    
    ```python
    # Calculate time remaining in period
    pct_remaining = (period_end - now) / (period_end - period_start)
    
    # Old product credit (what they paid but won't use)
    old_credit = old_price * old_pct_remaining
    
    # New product debit (what they owe for remainder)
    new_debit = new_price * new_pct_remaining
    
    # Net proration
    net = new_debit - old_credit
    ```
    
    ### Proration Behaviors
    
    | Behavior | Action |
    |----------|--------|
    | `prorate` | Add to next invoice |
    | `invoice` | Create order immediately |
    
    ### BillingEntry for Prorations
    
    ```python
    # Credit entry (old product)
    BillingEntry(
        type=BillingEntryType.proration,
        direction=BillingEntryDirection.credit,
        amount=prorated_old_amount
    )
    
    # Debit entry (new product)
    BillingEntry(
        type=BillingEntryType.proration,
        direction=BillingEntryDirection.debit,
        amount=prorated_new_amount
    )
    ```
    
    ### Seat Proration
    
    ```python
    # Adding 2 seats at $10/seat with 50% time remaining
    delta_amount = 2 * $10 * 0.5 = $10
    
    BillingEntry(
        type=BillingEntryType.subscription_seats_increase,
        direction=BillingEntryDirection.debit,
        amount=1000  # cents
    )
    ```
    
    ---
    
    ## 8. Benefits & Credits
    
    ### Benefit Types
    
    | Type | Description | Grant Action |
    |------|-------------|--------------|
    | `meter_credit` | Usage allowances | Create meter_credited event |
    | `github_repository` | Repo access | Add to GitHub team |
    | `discord` | Server role | Assign Discord role |
    | `license_keys` | License distribution | Generate key |
    | `downloadables` | File access | Grant download permission |
    | `slack_shared_channel` | Slack Connect channel | Create/invite to shared channel |
    | `feature_flag` | Feature toggle (API-only) | None — merchant reads via API |
    | `custom` | Customer-visible note | None — displayed in customer portal |
    
    ### Benefit Grant Flow
    
    ```
    1. Order/Subscription created
    2. enqueue_benefits_grants(task="grant")
    3. For each benefit in product:
       - Skip if already granted
       - Enqueue benefit.grant task
    4. benefit.grant task:
       - Get/create BenefitGrant record
       - Call strategy.grant() (type-specific)
       - Set granted_at
       - Store properties
       - Send webhook
    ```
    
    ### Benefit Revocation Flow
    
    ```
    1. Subscription canceled or order refunded
    2. enqueue_benefits_grants(task="revoke")
    3. For each granted benefit:
       - Enqueue benefit.revoke task
    4. benefit.revoke task:
       - Call strategy.revoke() (type-specific)
       - Set revoked_at
       - Send webhook
    ```
    
    ### Meter Credits
    
    **Grant:**
    ```python
    # Create event with units
    Event(type="meter_credited", units=100)
    # Update CustomerMeter
    ```
    
    **Cycle (renewal):**
    ```python
    # Calculate rollover
    rollover = min(remaining_units, rollover_limit)
    # Reset meter
    Event(type="meter_reset")
    # Credit new period + rollover
    Event(type="meter_credited", units=base_units + rollover)
    ```
    
    **Revoke:**
    ```python
    # Negative credit event
    Event(type="meter_credited", units=-remaining_units)
    ```
    
    ### Grace Period
    
    Organizations can configure `benefit_revocation_grace_period` (days) to delay benefit revocation for `past_due` subscriptions.
    
    ---
    
    ## 9. Dunning & Payment Retry
    
    ### Dunning Process
    
    ```
    1. order.process_dunning runs hourly
    2. Finds orders where next_payment_attempt_at <= now
    3. For each order:
       - Enqueue order.process_dunning_order
    4. process_dunning_order:
       - Get customer's payment method
       - Attempt payment via Stripe
       - On success: mark order paid
       - On failure: schedule next attempt
    ```
    
    ### Retry Schedule
    
    Configured in organization settings. Typical pattern:
    - Day 1: First failure
    - Day 3: Retry 1
    - Day 5: Retry 2
    - Day 7: Final retry, then mark unpaid
    
    ### Subscription Status During Dunning
    
    ```
    payment fails → status=past_due, past_due_at=now
                   ↓
             benefits may continue (grace period)
                   ↓
             retry succeeds → status=active
                   ↓
             retry fails → status=unpaid, benefits revoked
    ```
    
    ---
    
    ## 10. Transaction Ledger
    
    ### Transaction Types
    
    | Type | Description |
    |------|-------------|
    | `payment` | Customer payment received |
    | `processor_fee` | Stripe fees |
    | `refund` | Money returned to customer |
    | `refund_reversal` | Refund failed/reversed |
    | `dispute` | Chargeback loss |
    | `dispute_reversal` | Won dispute |
    | `balance` | Internal balance transfer |
    | `payout` | Money sent to creator |
    
    ### Creating Payment Transactions
    
    ```
    1. charge.updated webhook (charge settled)
    2. Get balance_transaction from Stripe
    3. Extract settlement amount and fees
    4. Create Transaction(type=payment)
    5. Enqueue processor_fee.create_payment_fees
    6. Create Transaction(type=processor_fee)
    ```
    
    ### Payout Flow
    
    ```
    1. Creator has balance from transactions
    2. payout.trigger_stripe_payouts (daily)
    3. Calculate available balance
    4. Create Payout record
       - ACTIVE/OFFBOARDED org: status=pending, enqueue payout.created + payout.transfer
       - REVIEW/SNOOZED org: status=held, enqueue payout.created only
    5. stripe_service.transfer() to Connect account (skipped for held)
    6. stripe_service.create_payout() to bank
    7. payout.paid webhook → update status
    
    Held payout lifecycle:
    - When org approved: payout.release_held_payouts → status=pending, enqueue transfer
    - When org denied/blocked/offboarding: payout.cancel_account_payouts → cancel + refund
    - When payout account swapped: payout.cancel_held_payouts (old account only)
    ```
    
    ---
    
    ## 11. Key File Locations
    
    ### Models
    ```
    server/polar/models/
    ├── checkout.py
    ├── order.py
    ├── order_item.py
    ├── subscription.py
    ├── subscription_product_price.py
    ├── transaction.py
    ├── payment.py
    ├── refund.py
    ├── dispute.py
    ├── payout.py
    ├── customer.py
    ├── product.py
    ├── product_price.py
    ├── discount.py
    ├── benefit.py
    ├── benefit_grant.py
    └── billing_entry.py
    ```
    
    ### Services
    ```
    server/polar/
    ├── subscription/service.py
    ├── order/service.py
    ├── checkout/service.py
    ├── payment/service.py
    ├── refund/service.py
    ├── dispute/service.py
    ├── payout/service.py
    ├── benefit/
    │   ├── service.py
    │   ├── grant/service.py
    │   └── strategies/
    │       ├── meter_credit/service.py
    │       ├── github_repository/service.py
    │       ├── discord/service.py
    │       └── ...
    └── transaction/service/
        ├── payment.py
        ├── refund.py
        └── dispute.py
    ```
    
    ### Background Tasks
    ```
    server/polar/
    ├── subscription/tasks.py
    ├── order/tasks.py
    ├── checkout/tasks.py
    ├── benefit/tasks.py
    ├── payout/tasks.py
    ├── refund/tasks.py
    └── integrations/stripe/tasks.py
    ```
    
    ### Stripe Integration
    ```
    server/polar/integrations/stripe/
    ├── endpoints.py    # Webhook handlers
    ├── service.py      # Stripe API wrapper
    ├── tasks.py        # Webhook processing tasks
    └── payment.py      # Payment resolution helpers
    ```
    
    ---
    
    ## Common Debugging Scenarios
    
    ### Payment Failed
    1. Check `Payment` record for `decline_reason`
    2. Check `Order.status` and `next_payment_attempt_at`
    3. Look at external_events for Stripe webhook
    
    ### Benefits Not Granted
    1. Check `BenefitGrant` record for errors
    2. Look at benefit.grant task in Dramatiq logs
    3. Verify product has benefits attached
    
    ### Proration Issues
    1. Check `BillingEntry` records for subscription
    2. Verify billing_reason on Order
    3. Check subscription's current_period dates
    
    ### Subscription Not Cycling
    1. Check `scheduler_locked_at` on subscription
    2. Verify APScheduler is running
    3. Check subscription.cycle task logs
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related