# AI Credits & Subscription Model

## Overview

Shamra Academia's AI Playground uses a **credit-based billing system** integrated with Stripe. Users purchase monthly/yearly subscription tiers that provide AI credits for operations like writing, translation, OCR, and research planning.

---

## Subscription Tiers

| Tier | Monthly Price | Yearly Price | Monthly Credits | Daily Limit |
|------|--------------|-------------|-----------------|-------------|
| **Trial** | Free (one-time) | — | 100 | — |
| **Basic** | $4.99 | $49.99 | 0 | 0 |
| **Starter** | $9.00 | $90.00 | 500 | 50 |
| **Researcher** | $19.00 | $190.00 | 1,500 | 150 |
| **Professional** | $39.00 | $390.00 | 4,000 | 500 |
| **Institution** | $99.00 | $990.00 | 15,000 | 2,000 |

> **Yearly plans** provide ~17% savings (equivalent to 2 months free).

### Tier Feature Matrix

| Feature | Basic | Starter | Researcher | Professional | Institution |
|---------|-------|---------|------------|--------------|-------------|
| AI Writing | — | ✅ | ✅ | ✅ | ✅ |
| Rephrase / Proofread | — | ✅ | ✅ | ✅ | ✅ |
| Translation | — | — | ✅ | ✅ | ✅ |
| PDF Export | ✅ | ✅ | ✅ | ✅ | ✅ |
| DOCX Export | — | — | ✅ | ✅ | ✅ |
| LaTeX Export | — | — | — | ✅ | ✅ |
| Max Projects | — | 5 | 15 | Unlimited | Unlimited |
| Storage | — | 200 MB | 500 MB | 2 GB | 10 GB |
| Models | — | GPT-4o-mini | GPT-4o-mini, GPT-4o | All | All |
| API Access | — | — | — | ✅ | ✅ |
| Team Management | — | — | — | — | ✅ |
| SSO | — | — | — | — | ✅ |

---

## Credit System

Credits are consumed per AI operation. Defined in `UsageMonitorService::OPERATION_CREDITS`:

| Operation | Credits | Description |
|-----------|---------|-------------|
| `suggest_keywords` | 1 | Generate keywords from title |
| `check_relevance` | 1 | Score search result relevance |
| `rephrase` | 2 | Rephrase selected text |
| `proofread` | 2 | Grammar and style check |
| `translate` | 2 | Translate text |
| `chat` | 2 | AI chat message |
| `completion` | 2 | Text completion |
| `summarize` | 3 | Summarize document |
| `ask_ai` | 3 | Custom question |
| `arxiv_search` | 3 | Search arXiv with AI extraction |
| `generate_short` | 2 | Generate short paragraph |
| `generate_long` | 4 | Generate long section |
| `criticize` | 4 | Get AI feedback |
| `ocr_extract` | 5 | OCR text extraction (Mistral) |

### Credit Reset

Credits reset monthly via three redundant mechanisms:

| Mechanism | Trigger | Implementation |
|-----------|---------|----------------|
| **Lazy reset** | First API call of new month | `PlaygroundSubscription::checkAndResetCredits()` |
| **Login reset** | User login | `PlaygroundCreditsLoginListener` |
| **Cron job** | Daily batch | `app:reset-playground-credits` |

---

## Trial Subscription

- **Duration:** 14 days
- **Credits:** 100 (one-time, non-renewable)
- **Models:** Access to all models
- **Activation:** `GET /playground/activate-demo`
- **Limit:** One trial per user (enforced by database)

---

## Upgrade Process

### Flow

1. User visits `/subscription` and selects a higher tier
2. Stripe Checkout redirects to payment
3. On successful payment, Stripe sends `invoice.payment_succeeded` webhook
4. `PlaygroundSubscriptionService::handlePaymentSucceeded()`:
   - Updates tier and `monthlyCredits`
   - Sets new `subscriptionEnd` (period end date)
   - **If upgrading mid-cycle:** Credits are NOT reset; user retains remaining credits plus the new tier's allotment starts next billing cycle
   - **If billing reason is `subscription_update`:** Credits ARE reset immediately

### Billing Behavior

| Scenario | Behavior |
|----------|----------|
| **Mid-cycle upgrade** | Stripe prorates the charge. User keeps current credits until next reset. |
| **Renewal upgrade** | Credits reset to new tier's amount on renewal date. |
| **Trial → Paid** | Trial credits expire; new tier credits start immediately. |

### Code

```php
// PlaygroundSubscriptionService::handlePaymentSucceeded()
$subscription->setTier($tier);
$subscription->setMonthlyCredits(self::TIER_CONFIG[$tier]['monthly_credits']);

// Only reset credits on renewal or plan change
if ($isRenewal) {
    $subscription->resetCredits();
}
```

---

## Downgrade Process

### Flow

1. User selects a lower tier on `/subscription`
2. Stripe schedules the downgrade for the end of the current billing period
3. Until then, user retains current tier benefits and credits
4. At period end, Stripe sends `invoice.payment_succeeded` with the new tier
5. `handlePaymentSucceeded()` updates tier and sets new (lower) `monthlyCredits`

### Key Points

- **No immediate credit reduction** — users keep their current credits until the end of the billing cycle
- **New credits on renewal** — at renewal, credits reset to the lower tier's amount
- **Grace period** — users can use existing credits even if they exceed the downgrade tier's limit until reset

### Example

| Day | Event |
|-----|-------|
| Mar 1 | User on Researcher (1,500 credits), has 600 remaining |
| Mar 10 | User downgrades to Starter (500 credits) — scheduled for Mar 31 |
| Mar 10–30 | User still has Researcher benefits, 600 credits remain |
| Mar 31 | Renewal: credits reset to 500 (Starter) |

---

## Cancellation Process

### Flow

1. User cancels via Stripe customer portal or `/settings/subscription`
2. Stripe sends `customer.subscription.deleted` webhook
3. `PlaygroundSubscriptionService::handleSubscriptionCancelled()`:
   - Sets `isActive = false`
   - User loses access to AI features immediately

### Behavior

| Scenario | Behavior |
|----------|----------|
| **Cancel mid-cycle** | Access ends immediately. No refund (configurable in Stripe). |
| **Cancel at period end** | Stripe can be configured for this via portal settings. |
| **Re-subscribe** | User can re-subscribe anytime; new subscription starts fresh. |

### Code

```php
// PlaygroundSubscriptionService::handleSubscriptionCancelled()
public function handleSubscriptionCancelled(string $stripeSubscriptionId): bool
{
    $subscription = $this->repo->findOneBy(['stripeSubscriptionId' => $stripeSubscriptionId]);
    
    if (!$subscription) {
        return false;
    }

    $subscription->setIsActive(false);
    $subscription->setUpdatedAt(new \DateTime());
    
    $this->em->flush();

    $this->logger->info('Playground subscription cancelled', [
        'user_id' => $subscription->getUser()->getId(),
        'subscription_id' => $stripeSubscriptionId,
    ]);

    return true;
}
```

---

## Stripe Webhook Events

Webhook endpoint: `POST /webhook` (handles both legacy and Playground subscriptions)

| Event | Action |
|-------|--------|
| `invoice.payment_succeeded` | Activate/renew subscription, reset credits if renewal |
| `invoice.payment_failed` | Deactivate subscription |
| `customer.subscription.deleted` | Cancel subscription |

### Billing Reason Handling

The `billing_reason` field in `invoice.payment_succeeded` determines behavior:

| Billing Reason | Meaning | Credits Action |
|---------------|---------|----------------|
| `subscription_create` | New subscription | Initialize with tier credits |
| `subscription_cycle` | Monthly/yearly renewal | Reset credits |
| `subscription_update` | Plan change (upgrade/downgrade) | Reset credits |

---

## Key Files

| File | Purpose |
|------|---------|
| `src/Entity/PlaygroundSubscription.php` | Subscription entity |
| `src/Repository/PlaygroundSubscriptionRepository.php` | Data access |
| `src/Service/Playground/PlaygroundSubscriptionService.php` | Webhook handling |
| `src/Service/Playground/UsageMonitorService.php` | Credit tracking & limits |
| `src/EventListener/PlaygroundCreditsLoginListener.php` | Login-based credit reset |
| `src/Controller/PlaygroundSubscriptionController.php` | Subscription UI routes |
| `src/Controller/WebhookController.php` | Stripe webhook endpoint |
| `templates/playground/subscription.html.twig` | Subscription page UI |

---

## Database Schema

### `playground_subscription` Table

| Column | Type | Description |
|--------|------|-------------|
| `id` | INT | Primary key |
| `user_id` | INT (FK) | User reference |
| `tier` | VARCHAR(30) | trial, basic, starter, researcher, professional, institution |
| `monthly_credits` | INT | Credits per month for the tier |
| `used_credits` | INT | Credits consumed this period |
| `bonus_credits` | INT | Extra credits (promotions) |
| `credits_reset_date` | DATE | Next credit reset date |
| `subscription_start` | DATE | Start of subscription |
| `subscription_end` | DATE | End of current billing period |
| `is_active` | BOOL | Whether subscription is active |
| `stripe_subscription_id` | VARCHAR(255) | Stripe subscription ID |
| `created_at` | DATETIME | Row creation time |
| `updated_at` | DATETIME | Last update time |

### `playground_tier_config` Table (Manual Config)

| Column | Type | Description |
|--------|------|-------------|
| `tier` | VARCHAR(30) | Tier name (PK) |
| `monthly_price` | DECIMAL | Monthly price in USD |
| `yearly_price` | DECIMAL | Yearly price in USD |
| `monthly_credits` | INT | Credits per month |
| `daily_limit` | INT | Max requests per day |

> **Note:** `playground_tier_config` was created via raw SQL (no migration or Doctrine entity). Must use `utf8mb4_unicode_ci` collation.

---

## Environment Variables

```env
# Stripe Product IDs
STRIPE_BASIC_MONTH_PRODUCT=prod_xxx
STRIPE_BASIC_YEAR_PRODUCT=prod_xxx
STRIPE_STARTER_MONTH_PRODUCT=prod_xxx
STRIPE_STARTER_YEAR_PRODUCT=prod_xxx
STRIPE_RESEARCHER_MONTH_PRODUCT=prod_xxx
STRIPE_RESEARCHER_YEAR_PRODUCT=prod_xxx
STRIPE_PROFESSIONAL_MONTH_PRODUCT=prod_xxx
STRIPE_PROFESSIONAL_YEAR_PRODUCT=prod_xxx
STRIPE_INSTITUTION_MONTH_PRODUCT=prod_xxx
STRIPE_INSTITUTION_YEAR_PRODUCT=prod_xxx

# Stripe Keys
STRIPE_SECRET_KEY=sk_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
```

---

## Admin Dashboard

Available at `/jim19ud83/playground/dashboard`:

- **MRR tracking** — Monthly Recurring Revenue from active subscriptions
- **Tier breakdown** — Active subscribers per tier
- **Credit usage** — Track consumption patterns
- **Trial conversion** — Monitor trial → paid conversion rate
- **Profitability** — Historical cost-per-user at `/jim19ud83/playground/profitability`

---

## Future Improvements

1. **Proration UI** — Show prorated amount before upgrade
2. **Downgrade preview** — Warn users what features they'll lose
3. **Credits rollover** — Option to roll over unused credits (premium feature)
4. **Team billing** — Institution tier with sub-accounts
5. **Usage alerts** — Email when 80% credits consumed
6. **Auto-upgrade suggestion** — Prompt when user consistently hits limits
