Package Plan Sync with Stripe

Description

This document describes how package plan data are synchronized from Stripe Price events into the system via webhooks. The sync covers mapping to local tables:

  • Stripe Price → package_plans and package_plan_to_providers

The logic ensures local records are created or updated from Stripe events and linked to the correct Package derived from the related Stripe Product.

Prerequisites:

  • Stripe is configured and webhook endpoint is set to the system.
  • A Stripe Payment Provider record exists and is registered in the app container as app('stripe.payment.provider').

Process Flow Diagram

---
config:
  theme: base
  layout: dagre
  flowchart:
    curve: linear
    htmlLabels: false
  themeVariables:
    edgeLabelBackground: "transparent"
---
flowchart TD
    %% External Services
    subgraph ExternalServices["External Services"]
        StripeAPI((Stripe API))
    end

    %% API Controller Layer
    subgraph ApiControllerLayer["API Controller Layer"]
        WebhookController[WebhookController]
    end

    %% Business Services
    subgraph BusinessServices["Business Logic Services"]
        PackagePlanService(PackagePlanService)
    end

    %% Repository Layer
    subgraph RepositoryLayer["Repository Layer"]
        PackagePlanRepository(PackagePlanRepository)
        PackagePlanToProviderRepository(PackagePlanToProviderRepository)
        PackageRepository(PackageRepository)
    end

    %% Database Layer
    subgraph DatabaseLayer["Database Layer"]
        PackagePlansDB[(package_plans)]
        PlanToProvidersDB[(package_plan_to_providers)]
        PackagesDB[(packages)]
        ProvidersDB[(payment_providers)]
    end

    %% Flow
    StripeAPI --- E1["Send webhook (price events)"]
    E1 --> WebhookController
    WebhookController --- E2["Verify & dispatch by event type"]
    E2 -->|price.created / price.updated| PackagePlanService

    PackagePlanService --> PackageRepository
    PackagePlanService --> PackagePlanRepository
    PackagePlanService --> PackagePlanToProviderRepository
    PackageRepository --> PackagesDB
    PackagePlanRepository --> PackagePlansDB
    PackagePlanToProviderRepository --> PlanToProvidersDB
    ProvidersDB -. reference .- PlanToProvidersDB

    %% Styling
    style ExternalServices fill:#fcd9d9,stroke:#cc3333,stroke-width:2px
    style ApiControllerLayer fill:#e6f3ff,stroke:#0066cc,stroke-width:2px
    style BusinessServices fill:#f5f0ff,stroke:#9966cc,stroke-width:2px
    style RepositoryLayer fill:#f0f8e6,stroke:#339933,stroke-width:2px
    style DatabaseLayer fill:#ffe6cc,stroke:#ff9900,stroke-width:2px
    style E1 fill:transparent,stroke:transparent,stroke-width:1px
    style E2 fill:transparent,stroke:transparent,stroke-width:1px

Use Cases

Case: Sync Price (Stripe → Package Plan)

Description

Handle price.created / price.updated from Stripe to create or update local package_plans and package_plan_to_providers. Ensures parent Package exists by retrieving related Product from Stripe.

Sequence Diagram

sequenceDiagram
    participant StripeAPI as Stripe API
    participant Webhook as WebhookController
    participant PlanSvc as PackagePlanService
    participant Repo as Repositories
    participant DB as Database

    Note over StripeAPI,DB: Price Sync

    StripeAPI->>Webhook: POST /api/v1/admin/stripe/webhook (price.*)
    Webhook->>Webhook: Verify signature & construct event
    Webhook->>PlanSvc: syncFromStripe(price)

    rect rgb(230, 255, 255)
    Note right of PlanSvc: Validate & map
    alt lookup_key missing
        PlanSvc-->>Webhook: Log & skip (no slug)
    else proceed
        PlanSvc->>StripeAPI: retrieve product(price.product)
        StripeAPI-->>PlanSvc: product
        PlanSvc->>Repo: getOrCreatePackageFromStripe(product)
        PlanSvc->>Repo: findBySlug(lookup_key)
        PlanSvc->>Repo: create/update package_plan
        PlanSvc->>Repo: upsert package_plan_to_providers(provider_price_id)
        Repo->>DB: persist changes
    end
    end

    Webhook-->>StripeAPI: 200 OK

Steps

Step 1: Receive Price Event

  • Description: Stripe sends price.created or price.updated.
  • Validation: Ensure id is present; ensure lookup_key (used as plan slug). If missing, log and skip.

Step 2: Ensure Package Exists

  • Retrieve related Product from Stripe, then create or update packages based on product metadata.

Step 3: Map Package Plan Fields

  • Mapping (from PackagePlanService::buildPackagePlanParams):
    • name: nickname, slug: lookup_key, package_id
    • amount: unit_amount, currency
    • type: type, billing_plan: recurring.interval

Step 4: Upsert Package Plan and Provider Mapping

  • Create/update package_plans by slug.
  • Upsert package_plan_to_providers with provider_price_id and status.

Step 5: Response

  • On success: 200 OK; on error: log, notify, and return error status.

Related Database Structure

erDiagram
    package_plans {
        bigint id PK "Primary key"
        string name "Plan name"
        string slug "Plan slug (unique)"
        bigint package_id FK "Linked to packages table"
        double amount "Amount"
        string currency "Currency"
        string type "Plan type (recurring, one_time)"
        string billing_plan "Billing cycle"
        tinyint status "Plan status"
        timestamp created_at "Created at"
        timestamp updated_at "Updated at"
    }

    package_plan_to_providers {
        bigint id PK "Primary key"
        bigint package_plan_id FK "Linked to package_plans table"
        bigint provider_id FK "Linked to payment_providers table"
        string provider_price_id "Price ID from provider (Stripe)"
        tinyint status "Status"
        timestamp created_at "Created at"
        timestamp updated_at "Updated at"
    }

    packages {
        bigint id PK "Primary key"
        string name "Package name"
        string slug "Package slug (unique)"
    }

    payment_providers {
        bigint id PK "Primary key"
        string name "Provider name"
        string slug "Provider slug (unique)"
        tinyint status "Status"
        timestamp created_at "Created at"
        timestamp updated_at "Updated at"
    }

    packages ||--o{ package_plans : has
    package_plans ||--o{ package_plan_to_providers : uses
    payment_providers ||--o{ package_plan_to_providers : linked

Related API Endpoints

Method Endpoint Controller Description
POST /api/v1/admin/stripe/webhook WebhookController@handleWebhook Receive Stripe price events
GET /api/v1/general/package-plan PackagePlanController@index Get service package plan list

Error Handling

  • Log & Notifications:

    • All sync failures are logged; critical cases may notify Slack.
  • Error Detail:

Status Code Error Message Description
400 "Invalid request" Missing required identifiers from Stripe payload
400 "Price created without slug" Stripe Price missing lookup_key used as plan slug
404 "Package not found" Referenced product/package cannot be resolved
500 "Failed to create price for plan ..." Price creation failed on Stripe side

Additional Notes

  • Price lookup_key must be set and should include the package context to avoid collisions.
  • All upsert logic is idempotent; repeated events will not create duplicate records.