# Stripe Integration Documentation

## Overview

This document outlines the Stripe integration for Magasinet KBH's subscription system, which supports both personal and business subscriptions with volume-based pricing.

## Architecture

### Technology Stack
- **Payment Provider**: Stripe
- **Auth System**: Better Auth v1.3.7 with Stripe Plugin v1.3.26
- **Framework**: Next.js 15.5 with TypeScript
- **Database**: PostgreSQL with Drizzle ORM
- **Email**: Nodemailer for invoice delivery

### Subscription Types

#### 1. Personal Subscriptions
- **Basis**: 29 DKK/month - Basic access to content
- **Plus**: 69 DKK/month - Full access + KBH Plus section + perks (mug, no ads)
- Both plans support annual billing options

#### 2. Business Subscriptions
Volume-based pricing for organizations:

| Employees | Monthly (DKK/employee) | Annual (DKK/employee) |
|-----------|------------------------|------------------------|
| 1         | 85                     | 75                     |
| 2         | 79                     | 69                     |
| 3-4       | 69                     | 59                     |
| 5+        | 59                     | 49                     |

**Features**:
- Invoice generation and email delivery
- CVR (Danish business number) collection
- Billing address required
- Tax ID collection enabled
- Multi-user seat management via organization system

---

## Configuration

### 1. Environment Variables

Add to `.env.local`:

```env
# Stripe Configuration
STRIPE_SECRET_KEY=sk_test_...                          # Test: sk_test_... | Live: sk_live_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...        # Test: pk_test_... | Live: pk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...                        # From Stripe Dashboard webhook setup
```

### 2. Database Schema

Stripe tables created via migration `0006_sleepy_thor.sql`:

#### `stripe_customer` table
```sql
CREATE TABLE "stripe_customer" (
  "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  "user_id" uuid NOT NULL REFERENCES users(id),
  "stripe_customer_id" text UNIQUE NOT NULL,
  "email" text NOT NULL,
  "name" text,
  "created_at" timestamp DEFAULT now(),
  "updated_at" timestamp DEFAULT now()
);
```

#### `stripe_subscription` table
```sql
CREATE TABLE "stripe_subscription" (
  "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  "user_id" uuid NOT NULL REFERENCES users(id),
  "stripe_customer_id" text NOT NULL,
  "stripe_subscription_id" text UNIQUE NOT NULL,
  "plan" text NOT NULL,
  "status" text DEFAULT 'active',
  "reference_id" text,                    -- Links to organization.id for business subs
  "metadata" jsonb,                       -- Stores employeeCount, cvr, etc.
  "current_period_start" timestamp,
  "current_period_end" timestamp,
  "trial_start" timestamp,
  "trial_end" timestamp,
  "cancel_at_period_end" boolean DEFAULT false,
  "canceled_at" timestamp,
  "created_at" timestamp DEFAULT now(),
  "updated_at" timestamp DEFAULT now()
);
```

#### Organization fields (extended)
```sql
ALTER TABLE "organizations" ADD COLUMN "contact_name" text;
ALTER TABLE "organizations" ADD COLUMN "address" text;
ALTER TABLE "organizations" ADD COLUMN "postal_code" text;
ALTER TABLE "organizations" ADD COLUMN "city" text;
ALTER TABLE "organizations" ADD COLUMN "cvr" text;
```

### 3. Better Auth Configuration

**File**: `/src/lib/auth/auth.ts`

```typescript
import { stripe } from '@better-auth/stripe';
import Stripe from 'stripe';

export const auth = betterAuth({
  // ... other config
  plugins: [
    // Organization plugin MUST come before Stripe
    organization({
      schema: {
        organization: {
          additionalFields: {
            contactName: { type: "string", input: true, required: false },
            address: { type: "string", input: true, required: false },
            postalCode: { type: "string", input: true, required: false },
            city: { type: "string", input: true, required: false },
            cvr: { type: "string", input: true, required: false }
          }
        }
      }
    }),
    stripe({
      stripeClient: new Stripe(config.stripe.secretKey!, {
        apiVersion: '2025-02-24.acacia',
      }),
      stripeWebhookSecret: config.stripe.webhookSecret || '',
      plans: [
        // Personal plans
        { name: 'basis', priceId: 'price_basis_monthly', limits: { access: 'basic' } },
        { name: 'basis-annual', priceId: 'price_basis_annual', limits: { access: 'basic' } },
        { name: 'plus', priceId: 'price_plus_monthly', limits: { access: 'full', features: ['kbh-plus', 'no-ads', 'mug'] } },
        { name: 'plus-annual', priceId: 'price_plus_annual', limits: { access: 'full', features: ['kbh-plus', 'no-ads', 'mug'] } },
        
        // Business plans (dynamic pricing)
        { name: 'business-monthly', priceId: 'price_business_monthly', limits: { access: 'full', features: ['kbh-plus', 'no-ads', 'business'] } },
        { name: 'business-annual', priceId: 'price_business_annual', limits: { access: 'full', features: ['kbh-plus', 'no-ads', 'business'] } }
      ],
      getCheckoutSessionParams: async ({ user, plan, subscription }, request) => {
        // Dynamic pricing for business plans
        if (plan.name.includes('business')) {
          const employeeCount = subscription?.metadata?.employeeCount || 1;
          const isAnnual = plan.name.includes('annual');
          
          const calculatePrice = (count: number, annual: boolean) => {
            if (annual) {
              if (count === 1) return 75;
              if (count === 2) return 69;
              if (count >= 3 && count <= 4) return 59;
              return 49; // 5+
            } else {
              if (count === 1) return 85;
              if (count === 2) return 79;
              if (count >= 3 && count <= 4) return 69;
              return 59; // 5+
            }
          };
          
          const pricePerEmployee = calculatePrice(employeeCount, isAnnual);
          const totalAmount = pricePerEmployee * 100 * employeeCount; // Convert to øre
          
          return {
            params: {
              mode: 'subscription',
              currency: 'dkk',
              line_items: [{
                price_data: {
                  currency: 'dkk',
                  product_data: {
                    name: `Business Subscription - ${employeeCount} medarbejdere`,
                    description: isAnnual ? 'Årligt abonnement' : 'Månedligt abonnement'
                  },
                  unit_amount: totalAmount,
                  recurring: {
                    interval: isAnnual ? 'year' : 'month'
                  }
                },
                quantity: 1
              }],
              metadata: {
                employeeCount: employeeCount.toString(),
                cvr: subscription?.metadata?.cvr || '',
                organizationId: subscription?.referenceId || ''
              },
              tax_id_collection: { enabled: true },
              billing_address_collection: 'required',
              invoice_creation: { enabled: true },
              payment_method_collection: 'if_required'
            }
          };
        }
        
        // Personal plans use default behavior
        return {};
      }
    })
  ]
});
```

### 4. Auth Client Configuration

**File**: `/src/lib/auth/auth-client.ts`

```typescript
import { stripeClient } from '@better-auth/stripe/client';

export const client = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_APP_URL,
  plugins: [
    // ... other plugins
    stripeClient()  // Note: publishableKey no longer required in v1.3.27+
  ]
});

export const { subscription } = client;
```

---

## Stripe Dashboard Setup

### 1. Create Products

#### Personal Products
1. **Magasinet KBH - Basis**
   - Recurring: Monthly
   - Price: 29 DKK
   - Copy Price ID → Replace `price_basis_monthly` in auth.ts

2. **Magasinet KBH - Basis (Årligt)**
   - Recurring: Yearly
   - Price: 348 DKK (29 × 12)
   - Copy Price ID → Replace `price_basis_annual` in auth.ts

3. **Magasinet KBH - Plus**
   - Recurring: Monthly
   - Price: 69 DKK
   - Copy Price ID → Replace `price_plus_monthly` in auth.ts

4. **Magasinet KBH - Plus (Årligt)**
   - Recurring: Yearly
   - Price: 828 DKK (69 × 12)
   - Copy Price ID → Replace `price_plus_annual` in auth.ts

#### Business Products
5. **Magasinet KBH - Business (Månedligt)**
   - Type: Product only (no fixed price)
   - Description: "Dynamic pricing based on employee count"
   - Note: Pricing handled via `price_data` in code

6. **Magasinet KBH - Business (Årligt)**
   - Type: Product only (no fixed price)
   - Description: "Dynamic pricing based on employee count"
   - Note: Pricing handled via `price_data` in code

### 2. Configure Webhooks

**Navigation**: Stripe Dashboard → Developers → Webhooks → Add endpoint

#### Webhook URL
```
https://your-domain.com/api/auth/stripe/webhook
```

#### Events to Select
- ✅ `checkout.session.completed` - When payment succeeds
- ✅ `customer.subscription.created` - New subscription
- ✅ `customer.subscription.updated` - Plan changes, payment method updates
- ✅ `customer.subscription.deleted` - Cancellations
- ✅ `invoice.payment_succeeded` - Recurring payments (for invoice emails)
- ✅ `invoice.payment_failed` - Failed payments
- ✅ `customer.subscription.trial_will_end` - Trial ending notifications

#### After Creating Webhook
1. Copy the **Signing secret** (starts with `whsec_...`)
2. Add to `.env.local` as `STRIPE_WEBHOOK_SECRET`

### 3. Tax Settings

**Navigation**: Stripe Dashboard → Settings → Tax

1. Enable **Tax ID collection**
   - Allows CVR number collection at checkout
2. Configure **Danish VAT** (if applicable)
   - Note: Digital content subscriptions may be VAT-exempt
3. Enable **Automatic tax calculation** (optional)
   - Requires tax registration setup

### 4. Invoice Settings

**Navigation**: Stripe Dashboard → Settings → Invoices

1. **Email invoices automatically**: Enable
2. **Invoice template**: Use default or customize with Magasinet KBH branding
3. **Invoice footer**: Add company details
   ```
   Magasinet KBH
   CVR: [Your CVR Number]
   medlem@magasinetkbh.dk
   ```

---

## Implementation Guide

### Personal Subscription Flow

**Component**: `/src/components/subscription/subscription-button.tsx`

```typescript
export function SubscriptionButton({ planName, children, className }) {
  const handleSubscribe = async () => {
    const result = await client.subscription.upgrade({
      plan: planName,                                          // 'basis' or 'plus'
      successUrl: `${window.location.origin}/payments/success`,
      cancelUrl: `${window.location.origin}/payments/personal`
    });

    if (result.data?.url) {
      window.location.href = result.data.url;                  // Redirect to Stripe Checkout
    }
  };
  
  return <button onClick={handleSubscribe}>{children}</button>;
}
```

**Usage in page**:
```tsx
<SubscriptionButton planName="basis">
  Ja tak
</SubscriptionButton>
```

### Business Subscription Flow

**Component**: `/src/app/(main)/payments/business/_components/select-form.tsx`

```typescript
const onSubmit = async (data: CompanyFormData) => {
  // 1. Get or create organization
  let organization = await findOrganizationByCVR(data.cvr);
  if (!organization) {
    organization = await client.organization.create({
      name: data.name,
      contactName: data.contactName,
      address: data.address,
      postalCode: data.postalCode,
      city: data.city,
      cvr: data.cvr
    });
  }

  // 2. Create subscription with organization reference
  const planName = isAnnual ? 'business-annual' : 'business-monthly';
  
  await client.subscription.upgrade({
    plan: planName,
    referenceId: organization.id,                    // Links to organization
    metadata: {
      employeeCount: selectedOption,                 // Used in pricing calculation
      cvr: data.cvr,
      billingEmail: session.user.email
    },
    successUrl: `${window.location.origin}/payments/success`,
    cancelUrl: `${window.location.origin}/payments/business`
  });
};
```

### Success Page

**Component**: `/src/app/(main)/payments/success/_components/success-page-content.tsx`

```typescript
export function SuccessPageContent() {
  const [subscriptionDetails, setSubscriptionDetails] = useState(null);

  useEffect(() => {
    const fetchSubscriptionDetails = async () => {
      const session = await client.getSession();
      const subscriptions = await client.subscription.list();
      
      const latestSubscription = subscriptions.data[0];
      
      // Get organization details for business subscriptions
      if (latestSubscription.referenceId) {
        const orgs = await client.organization.list();
        const org = orgs.data?.find(o => o.id === latestSubscription.referenceId);
        // Display organization name, employee count, etc.
      }
    };
    
    fetchSubscriptionDetails();
  }, []);

  return (
    <div>
      {/* Display subscription details */}
    </div>
  );
}
```

---

## Invoice Email System

### Email Template

**File**: `/src/lib/email/email-templates.ts`

```typescript
export async function generateInvoiceEmail(props: InvoiceEmailProps): Promise<string> {
  const { organizationName, invoiceUrl, amount, employeeCount, billingPeriod, nextBillingDate } = props;
  
  return `
    <!DOCTYPE html>
    <html>
      <body>
        <h1>Faktura - Magasinet KBH</h1>
        <p>Hej ${organizationName},</p>
        <p>Tak for dit abonnement til Magasinet KBH. Her er din faktura for ${billingPeriod}.</p>
        
        <div>
          <p><strong>Antal medarbejdere:</strong> ${employeeCount}</p>
          <p><strong>Beløb:</strong> ${amount} DKK</p>
          <p><strong>Næste fakturering:</strong> ${nextBillingDate}</p>
        </div>
        
        <a href="${invoiceUrl}">Download Faktura</a>
      </body>
    </html>
  `;
}
```

### Webhook Handler (TODO)

**File**: `/src/app/api/webhooks/stripe-invoice/route.ts` (Not yet implemented)

```typescript
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { nodemailerService } from '@/lib/email/nodemailer';
import { generateInvoiceEmail } from '@/lib/email/email-templates';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(request: NextRequest) {
  const signature = request.headers.get('stripe-signature')!;
  const body = await request.text();

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
  }

  if (event.type === 'invoice.payment_succeeded') {
    const invoice = event.data.object as Stripe.Invoice;
    
    // Check if this is a business subscription
    const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string);
    const isBusiness = subscription.metadata?.organizationId;
    
    if (isBusiness) {
      // Get organization details from database
      const orgId = subscription.metadata.organizationId;
      const employeeCount = parseInt(subscription.metadata.employeeCount || '1');
      
      // Send invoice email
      const html = await generateInvoiceEmail({
        organizationName: 'Organization Name', // Fetch from DB
        invoiceUrl: invoice.invoice_pdf || '',
        amount: (invoice.amount_paid / 100).toFixed(2),
        employeeCount,
        billingPeriod: subscription.metadata.billingPeriod || 'måneden',
        nextBillingDate: new Date(subscription.current_period_end * 1000).toLocaleDateString('da-DK')
      });

      await nodemailerService.emails.send({
        from: 'medlem@magasinetkbh.dk',
        to: invoice.customer_email!,
        subject: 'Faktura - Magasinet KBH',
        html
      });
    }
  }

  return NextResponse.json({ received: true });
}
```

---

## Testing

### Local Development

#### 1. Start Stripe CLI Webhook Forwarding
```bash
stripe listen --forward-to localhost:3000/api/auth/stripe/webhook
```

Copy the webhook signing secret from CLI output and add to `.env.local`:
```env
STRIPE_WEBHOOK_SECRET=whsec_...
```

#### 2. Test Cards

**Successful Payment**:
```
Card: 4242 4242 4242 4242
Exp: Any future date
CVC: Any 3 digits
```

**Payment Declined**:
```
Card: 4000 0000 0000 0002
```

**3D Secure Authentication**:
```
Card: 4000 0025 0000 3155
```

#### 3. Test Scenarios

1. **Personal Subscription - Basis**
   - Click "Ja tak" on Basis plan
   - Complete payment with test card
   - Verify redirect to success page
   - Check database for subscription record

2. **Personal Subscription - Plus**
   - Same as above for Plus plan

3. **Business Subscription - 1 Employee**
   - Fill company form with test data
   - Select 1 employee
   - Complete payment
   - Verify organization created
   - Verify subscription linked to organization
   - Check total: 85 DKK monthly or 75 DKK annual

4. **Business Subscription - 5 Employees**
   - Fill company form
   - Select 5 employees
   - Complete payment
   - Check total: 295 DKK monthly (59 × 5) or 245 DKK annual (49 × 5)

5. **Failed Payment**
   - Use declined test card
   - Verify error handling
   - User stays on subscription page

6. **Webhook Events**
   - Monitor Stripe CLI output
   - Verify `checkout.session.completed` received
   - Verify subscription status updated in database

---

## Subscription Management

### List User Subscriptions

```typescript
const subscriptions = await client.subscription.list({
  referenceId: organizationId  // Optional: filter by organization
});

const activeSubscription = subscriptions.data?.find(
  sub => sub.status === 'active' || sub.status === 'trialing'
);
```

### Cancel Subscription

```typescript
await client.subscription.cancel({
  subscriptionId: 'sub_xxx',
  referenceId: organizationId,  // Required for business subscriptions
  returnUrl: '/profile'
});
```

### Restore Canceled Subscription

```typescript
await client.subscription.restore({
  subscriptionId: 'sub_xxx',
  referenceId: organizationId
});
```

### Access Billing Portal

```typescript
const { data } = await client.subscription.billingPortal({
  referenceId: organizationId,
  returnUrl: '/profile',
  locale: 'da'
});

window.location.href = data.url;  // Redirect to Stripe Customer Portal
```

---

## Security Considerations

### 1. Webhook Signature Verification
- Better Auth automatically verifies webhook signatures
- Always use `STRIPE_WEBHOOK_SECRET` for verification
- Never process webhooks without signature validation

### 2. Environment Variables
- Never commit Stripe keys to version control
- Use different keys for test/production
- Rotate keys periodically

### 3. Metadata Sanitization
- Validate all metadata before storing
- Don't store sensitive information in metadata
- Use `referenceId` for linking, not metadata

### 4. CVR Validation
- Consider adding CVR format validation
- Optional: Integrate with CVR verification API
- Store CVR securely in organization table

---

## Troubleshooting

### Common Issues

#### 1. Webhook Not Receiving Events
**Symptoms**: Subscription created in Stripe but not in database

**Solutions**:
- Check webhook URL is correct
- Verify webhook secret in `.env.local`
- Check Stripe Dashboard → Webhooks → Recent events for errors
- Use Stripe CLI for local testing

#### 2. Dynamic Pricing Not Working
**Symptoms**: Wrong amount charged for business subscriptions

**Solutions**:
- Verify `employeeCount` in metadata
- Check `getCheckoutSessionParams` logic
- Ensure `price_data` is being used, not fixed `priceId`
- Log the calculated `totalAmount` before creating session

#### 3. Organization Not Created
**Symptoms**: Subscription created but no organization record

**Solutions**:
- Check user authentication before creating organization
- Verify organization creation response has no errors
- Check database permissions
- Look for CVR uniqueness constraint violations

#### 4. Invoice Emails Not Sending
**Symptoms**: Business subscriptions created but no invoice emails

**Solutions**:
- Verify `invoice.payment_succeeded` webhook is configured
- Check custom webhook handler is implemented
- Verify Nodemailer configuration
- Check email logs for errors

---

## Monitoring & Analytics

### Key Metrics to Track

1. **Subscription Conversions**
   - Personal vs Business breakdown
   - Monthly vs Annual preference
   - Drop-off at payment page

2. **Revenue Metrics**
   - Monthly Recurring Revenue (MRR)
   - Average Revenue Per User (ARPU)
   - Employee seat distribution

3. **Failed Payments**
   - Monitor `invoice.payment_failed` events
   - Set up retry logic
   - Email notifications to users

4. **Churn Analysis**
   - Cancellation reasons (add feedback form)
   - Subscription lifetime
   - Reactivation rate

### Stripe Dashboard Reports
- **Navigation**: Reports → Overview
- Track MRR, customers, failed payments
- Export data for deeper analysis

---

## Migration & Rollback

### Deploying to Production

1. **Pre-deployment Checklist**
   - [ ] Test environment variables set correctly
   - [ ] Webhook endpoint accessible from Stripe
   - [ ] All test scenarios passing
   - [ ] Database migrations applied
   - [ ] Email templates tested

2. **Switch to Live Mode**
   - Update `.env` with live Stripe keys (sk_live_..., pk_live_...)
   - Recreate products in live mode
   - Update price IDs in auth.ts
   - Configure live webhook with live secret

3. **Gradual Rollout**
   - Enable for admin users first
   - Monitor for errors
   - Gradually expand to all users

### Rollback Plan

If issues occur:
1. Switch back to test mode keys (stops new subscriptions)
2. Keep existing subscriptions active
3. Fix issues in staging
4. Redeploy with fixes

---

## Future Enhancements

### Planned Features

1. **Annual Billing Toggle** (Priority: High)
   - Add UI toggle on personal subscription page
   - Show savings for annual plans (e.g., "Save 2 months")

2. **Organization Management** (Priority: High)
   - Dashboard for organization admins
   - Add/remove employee emails
   - View seat usage
   - Upgrade seat count

3. **Subscription Upgrades/Downgrades** (Priority: Medium)
   - Allow changing from Basis to Plus
   - Prorate charges
   - Smooth transition UX

4. **Trial Periods** (Priority: Medium)
   - 14-day trial for Plus plan
   - Trial abuse prevention (built into Better Auth)
   - Trial reminder emails

5. **Discount Codes** (Priority: Low)
   - Promotional codes for new members
   - Referral discounts
   - Annual subscription discounts

6. **Multiple Payment Methods** (Priority: Low)
   - MobilePay integration
   - Bank transfer for annual plans
   - Invoice payment terms (net 30)

---

## Support

### Resources
- [Stripe Documentation](https://stripe.com/docs)
- [Better Auth Stripe Plugin](https://www.better-auth.com/docs/plugins/stripe)
- [Stripe API Reference](https://stripe.com/docs/api)

### Contact
For implementation questions or issues:
- Email: medlem@magasinetkbh.dk
- Stripe Support: [https://support.stripe.com](https://support.stripe.com)

---

## Version History

| Version | Date | Changes |
|---------|------|---------|
| 1.0.0   | 2025-10-06 | Initial Stripe integration with Better Auth |

---

**Last Updated**: October 6, 2025  
**Status**: ✅ Core implementation complete (75%), awaiting Stripe Dashboard setup

