Package Sync with Stripe

Description

This document describes how package and package plan data are synchronized from Stripe into the system via webhooks. The sync covers two artifact types:

  • Stripe Product → packages and package_to_providers
  • Stripe Price → package_plans and package_plan_to_providers

The logic ensures local records are created or updated from Stripe events, maintaining provider linkage for checkout and subscription flows.

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"]
        PackageService(PackageService)
        PackagePlanService(PackagePlanService)
    end

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

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

    %% Flow
    StripeAPI --- E1["Send webhook (product/price events)"]
    E1 --> WebhookController

    WebhookController --- E2["Verify & dispatch by event type"]

    E2 -->|product.created / product.updated / product.deleted| PackageService
    E2 -->|price.created / price.updated| PackagePlanService

    PackageService --> PackageRepository
    PackageService --> PackageToProviderRepository
    PackageRepository --> PackagesDB
    PackageToProviderRepository --> PackageToProvidersDB

    PackagePlanService --> PackagePlanRepository
    PackagePlanService --> PackagePlanToProviderRepository
    PackagePlanService --> PackageRepository
    PackagePlanRepository --> PackagePlansDB
    PackagePlanToProviderRepository --> PlanToProvidersDB
    PackageRepository --> PackagesDB
    ProvidersDB -. reference .- PackageToProvidersDB
    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 1: Sync Product (Stripe → Package)

Description

Handle product.created / product.updated from Stripe to create or update local packages and package_to_providers.

Sequence Diagram

sequenceDiagram
    participant StripeAPI as Stripe API
    participant Webhook as WebhookController
    participant PkgSvc as PackageService
    participant Repo as Repositories
    participant DB as Database

    Note over StripeAPI,DB: Product Sync

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

    rect rgb(230, 255, 255)
    Note right of PkgSvc: Map fields
    PkgSvc->>Repo: findBySlug(product.metadata.slug)
    alt Package exists
        PkgSvc->>Repo: update package fields (name, description, status, limits, visibility)
    else Not found
        PkgSvc->>Repo: create package
    end
    PkgSvc->>Repo: upsert package_to_providers (provider_product_id)
    Repo->>DB: persist changes
    end

    Webhook-->>StripeAPI: 200 OK

Steps

Step 1: Receive Product Event

  • Description: Stripe sends product.created or product.updated.
  • Validation: Ensure payload has id. If missing, reject as invalid.

Step 2: Map Package Fields

  • Mapping (from PackageService::mapPackageParams):
    • name, description, status: active
    • slug from metadata.slug
    • Limits: max_member, max_product_group, max_product, max_category, max_search_query, max_viewpoint
    • data_visible, api_available, schedule_id, schedule_priority

Step 3: Upsert Package

  • Find by slug; create or update accordingly.

Step 4: Upsert Provider Mapping

  • Insert or update package_to_providers with:
    • provider_id, provider_product_id, status, created_at, updated_at (from Stripe timestamps)

Step 5: Response

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

Case 3: Product Deleted

Description

When a product.deleted event is received, the system can mark the corresponding package as inactive or deprecated depending on active subscriptions (implementation detail may vary).

Related Database Structure

erDiagram
    packages {
        bigint id PK "Primary key"
        string name "Package name"
        string slug "Package slug (unique)"
        text description "Package description (nullable)"
        text image "Image path (nullable)"
        integer schedule_id "Schedule ID"
        integer schedule_priority "Schedule priority"
        integer max_member "Max members (nullable)"
        integer max_product_group "Max product groups (nullable)"
        integer max_product "Max products (nullable)"
        integer max_category "Max categories (nullable)"
        integer max_search_query "Max search queries (nullable)"
        integer max_viewpoint "Max viewpoints (nullable)"
        string data_visible "Data visibility"
        tinyint api_available "API available"
        tinyint status "Package status"
        timestamp created_at "Created at"
        timestamp updated_at "Updated at"
    }

    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_to_providers {
        bigint id PK "Primary key"
        bigint package_id FK "Linked to packages table"
        bigint provider_id FK "Linked to payment_providers table"
        string provider_product_id "Product ID from provider (Stripe)"
        tinyint status "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"
    }

    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
    packages ||--o{ package_to_providers : uses
    package_plans ||--o{ package_plan_to_providers : uses
    payment_providers ||--o{ package_to_providers : linked
    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 product/price events
GET /api/v1/admin/groups/packages PackageController@index Get available service packages list
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 "Product created without slug" Stripe Product metadata missing slug
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

  • Package slugs should be stable and unique; used to correlate Stripe Product metadata with local records.
  • Price lookup_key must be set and should include the package context to avoid collisions.
  • On monthly plan creation, the Product's default_price may be set to the monthly price for convenience.
  • All upsert logic is idempotent; repeated events will not create duplicate records.