# Paystack Subscription Integration — Implementation Plan

> **Project:** Business & Birthdays Platform  
> **Stack:** Laravel 12 API Backend  
> **Status:** Planning Phase  
> **Date:** 2026-07-12

---

## Table of Contents

1. [Current State Analysis](#1-current-state-analysis)
2. [What Needs to Change](#2-what-needs-to-change)
3. [Database Schema Changes](#3-database-schema-changes)
4. [PaystackService Enhancements](#4-paystackservice-enhancements)
5. [SubscriptionController Enhancements](#5-subscriptioncontroller-enhancements)
6. [Webhook Handling](#6-webhook-handling)
7. [Route Updates](#7-route-updates)
8. [Subscription Flow (User Journey)](#8-subscription-flow-user-journey)
9. [Frontend Implementation Guide Structure](#9-frontend-implementation-guide-structure)
10. [Implementation Order](#10-implementation-order)

---

## 1. Current State Analysis

### Existing Subscription System

The platform already has a **basic manual subscription system** with:

| Component | Details |
|-----------|---------|
| **Table:** `subscription_plans` | `id`, `name`, `slug`, `price`, `daily_hours_limit`, `duration_days`, `is_active`, `features` |
| **Table:** `user_subscriptions` | `id`, `user_id`, `subscription_plan_id`, `start_date`, `end_date`, `payment_status`(pending/paid/failed/refunded), `payment_reference`, `amount_paid`, `is_active`, `auto_renew` |
| **Model:** `SubscriptionPlan` | Scopes: `active()`. Relationships: `userSubscriptions()` |
| **Model:** `UserSubscription` | Scopes: `active()`. Methods: `isExpired()`, `daysRemaining()`. Relations to `User`, `Plan` |
| **Model:** `User` | Methods: `subscriptions()`, `activeSubscription()`, `hasActiveSubscription()`, `getDailyRemainingHours()` |
| **Controller:** `SubscriptionController` | Methods: `plans()`, `subscribe()`, `mySubscription()`, `history()`, `cancelAutoRenew()`, `remainingHours()`, admin CRUD |
| **Routes:** `routes/api/v1/subscriptions.php` | Public: `GET /plans`. Auth: `POST /subscribe`, `GET /my-subscription`, `GET /history`, `POST /cancel-auto-renew`, `GET /remaining-hours`. Admin: CRUD for plans |

### Existing Paystack Integration

The platform has a robust [`PaystackService`](app/Services/PaystackService.php) that handles:

- **Payment initialization** (`initializeTransaction`) — creates one-time payment
- **Transaction verification** (`verifyTransaction`)
- **Customer creation** (`createCustomer`) — creates customer on Paystack, stores as `PaystackCustomer`
- **Transfers** — create recipients, initiate transfers, verify transfers
- **DVA** — create/assign dedicated virtual accounts, process DVA payments
- **Webhooks** — `charge.success`, `transfer.success`, `transfer.failed`, `transfer.reversed`
- **Bank services** — resolve accounts, list banks, merchant balance

**Missing:** ❌ No subscription-related Paystack methods exist. The current subscription system operates independently of Paystack's recurring payment infrastructure.

### Existing Models Used

- [`PaystackCustomer`](app/Models/PaystackCustomer.php) — Stores Paystack customer data linked to user
- [`Transaction`](app/Models/Transaction.php) — Generic transaction model with polymorphic `payable()` relationship
- [`Customer`](app/Models/Customer.php) — Polymorphic customer model

---

## 2. What Needs to Change

```mermaid
graph TD
    subgraph "Current System"
        A[User selects plan] --> B[subscribe endpoint]
        B --> C[Creates UserSubscription record]
        C --> D[payment_status='pending']
        D --> E[Manual payment reference]
        E --> F[Admin confirms payment]
    end

    subgraph "Target System with Paystack"
        G[User selects plan] --> H[initialize endpoint]
        H --> I[PaystackService.initializeTransaction]
        I --> J[User redirected to Paystack checkout]
        J --> K[User completes payment]
        K --> L[Paystack redirects back with reference]
        L --> M[verify + create Subscription endpoint]
        M --> N[PaystackService.createSubscription on Paystack]
        N --> O[Paystack manages recurring billing]
        O --> P[Webhooks sync status to local DB]
    end
```

**Key Changes:**

1. **Database migrations** — Add Paystack-specific fields to `subscription_plans` and `user_subscriptions`
2. **PaystackService subscription methods** — CRUD for Paystack subscriptions, plans
3. **Updated SubscriptionController** — New endpoints for Paystack subscription flow
4. **Webhook handlers** — Process subscription events from Paystack
5. **Frontend guide** — Document all endpoints for frontend consumption

---

## 3. Database Schema Changes

### Migration 1: Add `paystack_plan_code` to `subscription_plans`

```php
Schema::table('subscription_plans', function (Blueprint $table) {
    $table->string('paystack_plan_code', 50)->nullable()->unique()->after('slug');
    $table->unsignedBigInteger('paystack_plan_id')->nullable()->after('paystack_plan_code');
    $table->string('currency', 3)->default('NGN')->after('price');
    $table->string('interval', 20)->default('monthly')->after('duration_days')
          ->comment('monthly, quarterly, annually');
});
```

### Migration 2: Add Paystack fields to `user_subscriptions`

```php
Schema::table('user_subscriptions', function (Blueprint $table) {
    // Paystack subscription identifiers
    $table->string('paystack_subscription_code', 50)->nullable()->unique()->after('payment_reference');
    $table->string('paystack_email_token', 100)->nullable()->after('paystack_subscription_code');
    $table->string('paystack_authorization_code', 50)->nullable()->after('paystack_email_token');
    $table->string('paystack_customer_code', 50)->nullable()->after('paystack_authorization_code');
    $table->string('paystack_plan_code', 50)->nullable()->after('paystack_customer_code');

    // Paystack subscription management fields
    $table->dateTime('next_payment_date')->nullable()->after('paystack_plan_code');
    $table->string('paystack_status', 20)->nullable()->after('next_payment_date')
          ->comment('active, paused, cancelled, expired, complete');
    $table->json('paystack_data')->nullable()->after('paystack_status')
          ->comment('Full Paystack subscription response data');

    // indexes
    $table->index('paystack_subscription_code');
    $table->index('paystack_status');
});
```

---

## 4. PaystackService Enhancements

All new methods go into [`app/Services/PaystackService.php`](app/Services/PaystackService.php).

### 4.1 Plan Management

#### `createPlan(array $data): array`
Creates a plan on Paystack.

```php
public function createPlan(array $data): array
{
    try {
        $response = $this->client->post('/plan', [
            'json' => [
                'name' => $data['name'],
                'amount' => $data['amount'] * 100, // convert to kobo
                'interval' => $data['interval'] ?? 'monthly',
                'currency' => $data['currency'] ?? 'NGN',
                'description' => $data['description'] ?? null,
            ]
        ]);
        $responseData = json_decode($response->getBody(), true);
        // ... handle response
    } catch (\Exception $e) { ... }
}
```

#### `listPlans(array $params = []): array`
Lists plans from Paystack.

#### `fetchPlan(string $planCode): array`
Fetches a single plan from Paystack.

### 4.2 Subscription Management

#### `createSubscription(array $data): array`
Creates a subscription on Paystack.

**Request to Paystack:**
```json
{
    "customer": "CUS_xnxdt6s1zg1f4nx",
    "plan": "PLN_gx2wn530m0i3w3m",
    "authorization": "AUTH_6tmt288t0o",
    "start_date": "2017-05-16T00:30:13+01:00"
}
```

**Response from Paystack:**
```json
{
    "status": true,
    "message": "Subscription successfully created",
    "data": {
        "subscription_code": "SUB_vsyqdmlzble3uii",
        "email_token": "d7gofp6yppn3qz7",
        "status": "active",
        "customer": 1173,
        "plan": 28,
        "amount": 50000,
        "authorization": { "...": "..." },
        "next_payment_date": "2016-04-28T07:00:00.000Z",
        "...": "..."
    }
}
```

**Steps:**
1. Ensure Paystack customer exists for the user (call `createCustomer` if needed)
2. Get customer's most recent authorization code (or use the one provided)
3. Call Paystack POST `/subscription`
4. Store the returned `subscription_code`, `email_token`, and full response in `user_subscriptions`
5. Update the local subscription record

#### `listSubscriptions(array $params = []): array`
Gets subscriptions from Paystack.

**GET** `/subscription?perPage=50&page=1&customer=CUS_xxx&plan=PLN_xxx`

#### `fetchSubscription(string $idOrCode): array`
Fetches a single subscription from Paystack.

**GET** `/subscription/{id_or_code}`

#### `enableSubscription(string $code, string $token): array`
Enables a subscription on Paystack.

**POST** `/subscription/enable`
```json
{ "code": "SUB_vsyqdmlzble3uii", "token": "d7gofp6yppn3qz7" }
```

#### `disableSubscription(string $code, string $token): array`
Disables a subscription on Paystack.

**POST** `/subscription/disable`
```json
{ "code": "SUB_vsyqdmlzble3uii", "token": "d7gofp6yppn3qz7" }
```

#### `generateUpdateSubscriptionLink(string $code): array`
Generates a link for updating the card on a subscription.

**GET** `/subscription/{code}/manage/link/`

#### `sendUpdateSubscriptionEmail(string $code): array`
Emails a customer a link for updating the card.

**POST** `/subscription/{code}/manage/email/`

---

## 5. SubscriptionController Enhancements

New methods to add to [`app/Http/Controllers/SubscriptionController.php`](app/Http/Controllers/SubscriptionController.php):

### 5.1 `initializeSubscription(Request $request)`

**Purpose:** Initialize a Paystack transaction for subscription payment, then create the subscription after payment confirmation.

**Flow:**
1. Validate request: `plan_id`, optionally `authorization_code`
2. Check user doesn't already have an active subscription
3. Get the plan from DB (ensure it has `paystack_plan_code`)
4. Ensure Paystack customer exists for user
5. Get user's authorizations (or use provided one)
6. Initialize a Paystack transaction with metadata indicating it's for subscription
7. Return `authorization_url` for frontend to redirect user

**Response:**
```json
{
    "success": true,
    "message": "Subscription initialization successful",
    "data": {
        "authorization_url": "https://checkout.paystack.com/...",
        "reference": "trx-ps-abc123",
        "access_code": "..."
    }
}
```

### 5.2 `verifyAndCreateSubscription(Request $request)`

**Purpose:** After user completes payment on Paystack, verify and create the subscription.

**Steps:**
1. Validate: `reference`, `plan_id`
2. Verify the transaction via `PaystackService.verifyTransaction()`
3. On success, call `PaystackService.createSubscription()` with customer code and plan code
4. Update local `user_subscriptions` record with Paystack response data
5. Return subscription details

### 5.3 `paystackPlans()`

**Purpose:** List plans from Paystack (admin sync).

### 5.4 `paystackSubscriptions(Request $request)`

**Purpose:** List subscriptions from Paystack (admin).

### 5.5 `paystackFetchSubscription(string $code)`

**Purpose:** Fetch subscription details from Paystack by code.

### 5.6 `paystackEnableSubscription(Request $request)`

**Purpose:** Enable a disabled subscription.

### 5.7 `paystackDisableSubscription(Request $request)`

**Purpose:** Disable an active subscription.

### 5.8 `paystackManageLink(string $code)`

**Purpose:** Generate a link for updating the card on a subscription.

### 5.9 `paystackManageEmail(string $code)`

**Purpose:** Send the card update email to the customer.

---

## 6. Webhook Handling

Add these handlers to [`PaystackService`](app/Services/PaystackService.php) and register them in the [`webhook()`](app/Services/PaystackService.php:751) method.

### 6.1 `handleSubscriptionCreate(array $payload)`

**Triggered when:** A subscription is successfully created on Paystack.

**Actions:**
1. Extract `subscription_code` from payload
2. Find local `UserSubscription` by `paystack_subscription_code`
3. Update `paystack_status`, `next_payment_date`, `paystack_data`
4. If status is `active`, mark `payment_status = 'paid'` and `is_active = true`

### 6.2 `handleSubscriptionDisable(array $payload)`

**Triggered when:** A subscription is disabled on Paystack.

**Actions:**
1. Extract `subscription_code`
2. Find local record
3. Update `paystack_status = 'cancelled'`, `is_active = false`

### 6.3 `handleInvoiceUpdate(array $payload)`

**Triggered when:** An invoice status changes (e.g., payment successful).

**Actions:**
1. Extract `subscription_code` and `invoice` status
2. Find local `UserSubscription`
3. If `invoice.status === 'success'`:
   - Update `next_payment_date`
   - Extend `end_date` by plan duration
   - Update `payment_status = 'paid'`

### 6.4 `handleInvoiceCreate(array $payload)`

**Triggered when:** A new invoice is created for a subscription.

**Actions:**
1. Log the event
2. May need to notify the user about upcoming payment

### 6.5 Updated Webhook Router

In the [`webhook()`](app/Services/PaystackService.php:770) method, update the match expression:

```php
$result = match($event) {
    'charge.success' => $this->handleChargeSuccess($data['reference'] ?? null, $data),
    'transfer.success' => $this->handleTransferSuccess($data['reference'] ?? null, $data),
    'transfer.failed' => $this->handleTransferFailed($data['reference'] ?? null, $data),
    'transfer.reversed' => $this->handleTransferReversed($data['reference'] ?? null, $data),
    // NEW: Subscription events
    'subscription.create' => $this->handleSubscriptionCreate($data),
    'subscription.disable' => $this->handleSubscriptionDisable($data),
    'invoice.updated' => $this->handleInvoiceUpdate($data),
    'invoice.create' => $this->handleInvoiceCreate($data),
    default => $this->handleUnknownEvent($event, $data)
};
```

---

## 7. Route Updates

Update [`routes/api/v1/subscriptions.php`](routes/api/v1/subscriptions.php) with:

```php
<?php

use App\Http\Controllers\SubscriptionController;
use Illuminate\Support\Facades\Route;

// Public routes
Route::prefix('v1/subscriptions')->group(function () {
    Route::get('/plans', [SubscriptionController::class, 'plans']);
});

// Authenticated user routes
Route::middleware('auth:sanctum')->prefix('v1/subscriptions')->group(function () {
    // Existing
    Route::post('/subscribe', [SubscriptionController::class, 'subscribe']);
    Route::get('/my-subscription', [SubscriptionController::class, 'mySubscription']);
    Route::get('/history', [SubscriptionController::class, 'history']);
    Route::post('/cancel-auto-renew', [SubscriptionController::class, 'cancelAutoRenew']);
    Route::get('/remaining-hours', [SubscriptionController::class, 'remainingHours']);

    // NEW: Paystack subscription flow
    Route::post('/initialize', [SubscriptionController::class, 'initializeSubscription']);
    Route::post('/verify-and-create', [SubscriptionController::class, 'verifyAndCreateSubscription']);

    // NEW: Paystack subscription management
    Route::post('/paystack/enable', [SubscriptionController::class, 'paystackEnableSubscription']);
    Route::post('/paystack/disable', [SubscriptionController::class, 'paystackDisableSubscription']);
    Route::get('/paystack/{code}/manage/link', [SubscriptionController::class, 'paystackManageLink']);
    Route::post('/paystack/{code}/manage/email', [SubscriptionController::class, 'paystackManageEmail']);
});

// Admin subscription management routes
Route::middleware(['auth:sanctum'])->prefix('v1/admin/subscriptions')->group(function () {
    // Existing
    Route::get('/', [SubscriptionController::class, 'adminIndex']);
    Route::post('/plans', [SubscriptionController::class, 'adminUpsertPlan']);
    Route::put('/plans/{id}', [SubscriptionController::class, 'adminUpsertPlan']);
    Route::delete('/plans/{id}', [SubscriptionController::class, 'adminDeletePlan']);

    // NEW: Paystack admin management
    Route::get('/paystack/plans', [SubscriptionController::class, 'paystackPlans']);
    Route::get('/paystack/subscriptions', [SubscriptionController::class, 'paystackSubscriptions']);
    Route::get('/paystack/subscriptions/{code}', [SubscriptionController::class, 'paystackFetchSubscription']);
});
```

---

## 8. Subscription Flow (User Journey)

```mermaid
sequenceDiagram
    participant User
    participant Frontend
    participant Backend
    participant Paystack

    User->>Frontend: Select subscription plan
    Frontend->>Backend: POST /subscriptions/initialize {plan_id}
    Backend->>Paystack: POST /transaction/initialize {amount, email, ...}
    Paystack-->>Backend: {authorization_url, reference}
    Backend-->>Frontend: {authorization_url, reference}
    Frontend->>Paystack: Redirect user to authorization_url
    User->>Paystack: Complete payment
    Paystack-->>Frontend: Redirect back with ?reference=xxx
    Frontend->>Backend: POST /subscriptions/verify-and-create {reference, plan_id}
    Backend->>Paystack: GET /transaction/verify/{reference}
    Paystack-->>Backend: {status: true, data: {...authorization...}}
    Backend->>Paystack: POST /subscription {customer, plan, authorization}
    Paystack-->>Backend: {status: true, data: {subscription_code, email_token, ...}}
    Backend->>Backend: Update user_subscriptions with Paystack data
    Backend-->>Frontend: {subscription details}
    Frontend-->>User: Show success, subscription active

    Note over Backend,Paystack: Recurring billing handled by Paystack
    Paystack->>Backend: Webhook: invoice.updated (payment successful)
    Backend->>Backend: Extend subscription end_date
    Paystack->>Backend: Webhook: subscription.disable
    Backend->>Backend: Mark subscription as inactive
```

### Alternative Flow: Direct Paystack Subscription (Card on File)

Some users may already have an authorization (saved card). In this case:

```mermaid
sequenceDiagram
    participant User
    participant Frontend
    participant Backend
    participant Paystack

    User->>Frontend: Select plan (has saved card)
    Frontend->>Backend: POST /subscriptions/paystack/create {plan_id, authorization_code}
    Backend->>Paystack: POST /subscription {customer, plan, authorization}
    Paystack-->>Backend: Subscription created + first charge
    Backend-->>Frontend: {subscription active}
```

---

## 9. Frontend Implementation Guide Structure

A comprehensive [`FRONTEND_SUBSCRIPTION_GUIDE.md`](plans/FRONTEND_PAYSTACK_SUBSCRIPTION_GUIDE.md) will be created with:

### Sections

1. **Overview** — Purpose and flow summary
2. **Base Configuration** — API setup with Sanctum auth
3. **Endpoint Reference** — Each endpoint with:
   - HTTP method and URL
   - Request headers
   - Request body parameters (table format)
   - Example request (JSON)
   - Example response (JSON)
   - Description of what it does
   - Error responses

4. **Endpoints to Document:**

| # | Method | Endpoint | Auth | Purpose |
|---|--------|----------|------|---------|
| 1 | GET | `/v1/subscriptions/plans` | No | List active subscription plans |
| 2 | GET | `/v1/subscriptions/my-subscription` | Yes | Get current user's subscription |
| 3 | POST | `/v1/subscriptions/initialize` | Yes | Initialize Paystack payment for subscription |
| 4 | POST | `/v1/subscriptions/verify-and-create` | Yes | Verify payment and create Paystack subscription |
| 5 | POST | `/v1/subscriptions/paystack/enable` | Yes | Enable a Paystack subscription |
| 6 | POST | `/v1/subscriptions/paystack/disable` | Yes | Disable a Paystack subscription |
| 7 | GET | `/v1/subscriptions/paystack/{code}/manage/link` | Yes | Get card update link |
| 8 | POST | `/v1/subscriptions/paystack/{code}/manage/email` | Yes | Send card update email |
| 9 | GET | `/v1/subscriptions/history` | Yes | Get subscription history |
| 10 | POST | `/v1/subscriptions/cancel-auto-renew` | Yes | Cancel auto-renewal |
| 11 | GET | `/v1/subscriptions/remaining-hours` | Yes | Get daily remaining hours |

5. **TypeScript Interfaces** — Type definitions for all subscription-related data
6. **Workflow Examples** — Complete code examples for:
   - Initializing a subscription payment
   - Handling Paystack redirect
   - Verifying and creating subscription
   - Managing subscription (enable/disable)
   - Card update flow
7. **Webhook Events** — Document which webhooks the backend handles
8. **Error Handling** — Common errors and how to handle them

---

## 10. Implementation Order

The implementation should follow this order:

### Phase 1: Database + Models (Code mode)

| Step | File | Action |
|------|------|--------|
| 1.1 | [`database/migrations/XXXX_XX_XX_XXXXXX_add_paystack_fields_to_subscription_plans_table.php`](database/migrations) | Create migration: add `paystack_plan_code`, `paystack_plan_id`, `currency`, `interval` to `subscription_plans` |
| 1.2 | [`database/migrations/XXXX_XX_XX_XXXXXX_add_paystack_fields_to_user_subscriptions_table.php`](database/migrations) | Create migration: add Paystack subscription fields to `user_subscriptions` |
| 1.3 | [`app/Models/SubscriptionPlan.php`](app/Models/SubscriptionPlan.php) | Update `$fillable` and `$casts` with new fields |
| 1.4 | [`app/Models/UserSubscription.php`](app/Models/UserSubscription.php) | Update `$fillable` and `$casts` with new Paystack fields |

### Phase 2: PaystackService Subscription Methods (Code mode)

| Step | File | Action |
|------|------|--------|
| 2.1 | [`app/Services/PaystackService.php`](app/Services/PaystackService.php) | Add `createPlan()` method |
| 2.2 | Same file | Add `listPlans()`, `fetchPlan()` methods |
| 2.3 | Same file | Add `createSubscription()` method |
| 2.4 | Same file | Add `listSubscriptions()`, `fetchSubscription()` methods |
| 2.5 | Same file | Add `enableSubscription()`, `disableSubscription()` methods |
| 2.6 | Same file | Add `generateUpdateSubscriptionLink()`, `sendUpdateSubscriptionEmail()` methods |
| 2.7 | Same file | Add webhook handlers: `handleSubscriptionCreate()`, `handleSubscriptionDisable()`, `handleInvoiceUpdate()`, `handleInvoiceCreate()` |
| 2.8 | Same file | Update `webhook()` method to include new subscription event handlers |

### Phase 3: SubscriptionController Enhancements (Code mode)

| Step | File | Action |
|------|------|--------|
| 3.1 | [`app/Http/Controllers/SubscriptionController.php`](app/Http/Controllers/SubscriptionController.php) | Add `initializeSubscription()` method |
| 3.2 | Same file | Add `verifyAndCreateSubscription()` method |
| 3.3 | Same file | Add Paystack management methods (`paystackEnable`, `paystackDisable`, `paystackManageLink`, `paystackManageEmail`) |
| 3.4 | Same file | Add admin methods (`paystackPlans`, `paystackSubscriptions`, `paystackFetchSubscription`) |

### Phase 4: Routes (Code mode)

| Step | File | Action |
|------|------|--------|
| 4.1 | [`routes/api/v1/subscriptions.php`](routes/api/v1/subscriptions.php) | Add all new subscription routes |

### Phase 5: Frontend Guide (Code/Architect mode)

| Step | File | Action |
|------|------|--------|
| 5.1 | [`plans/FRONTEND_PAYSTACK_SUBSCRIPTION_GUIDE.md`](plans/FRONTEND_PAYSTACK_SUBSCRIPTION_GUIDE.md) or root dir | Create comprehensive frontend implementation guide |

---

## Appendix: API Response Format

All new endpoints follow the existing pattern:

```json
{
    "success": true|false,
    "message": "Human-readable message",
    "data": { ... }
}
```

Error responses use:

```json
{
    "success": false,
    "message": "Error description",
    "code": 400|401|404|422|500
}
```

---

## Appendix: Environment Variables

Ensure [`.env`](.env.example) has:

```
PAYSTACK_PUBLIC_KEY=pk_test_xxxxxxxxxxxxx
PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxxx
PAYSTACK_BASE_URL=https://api.paystack.co
```

These should already be in [`config/services.php`](config/services.php) as `services.paystack.sk`, `services.paystack.pk`, `services.paystack.base_url`.
