Design Spec: Customer to User Subscription (#956)

Date: 2025-04-13
Status: Approved
Type: Feature (Backend + Frontend)


1. Overview

Goal: Enable recurring subscriptions from Customers to Users (sellers) through Stripe Connect using Destination Charges.

Architecture Decisions:

  • Build Backend and Frontend together in one feature branch
  • “Subscription Content” as new service type (Type::SUBSCRIPTION = 3) in existing service creation flow
  • Stripe Connect with Destination Charges (Platform owns subscription)
  • Frontend follows メニューフォーム pattern (existing CalendarService/EventSeminarService structure)

2. Backend Architecture

2.1 New Trait: StripeConnectSubscription.php

Location: yoyacoo_be/common/src/Base/Traits/StripeConnectSubscription.php

Extends existing StripeConnectPayment patterns for subscription handling.

Key Method:

public function createConnectSubscription($customerId, $priceId, $destinationAccountId, $feePercent) {
    return Subscription::create([
        'customer' => $customerId,
        'items' => [['price' => $priceId]],
        'transfer_data' => [
            'destination' => $destinationAccountId,
        ],
        'application_fee_percent' => $feePercent,
        'payment_behavior' => 'default_incomplete',
        'expand' => ['latest_invoice.payment_intent'],
    ], $this->stripeConnectOptions());
}

2.2 Database Schema

New Table: order_subscriptions

Column Type Description
id BigInt Primary Key
order_id BigInt Foreign key to original order
user_id BigInt User (seller) receiving funds
customer_id BigInt Customer (buyer) paying
stripe_subscription_id String Stripe Subscription ID (sub_…)
stripe_price_id String Stripe Price ID (price_…)
status String active, trialing, past_due, canceled
current_period_end DateTime Next payment due date
created_at Timestamp
updated_at Timestamp

Migration Location: yoyacoo_be/common/database/migrations/

2.3 Webhook Handlers

File: yoyacoo_be/admin/app/Domains/Subscription/Controllers/ConnectWebhookController.php

Extend existing ConnectWebhookController to handle:

Event Handler Action
invoice.paid handleInvoicePaid() Update current_period_end, log recurring payment
customer.subscription.deleted handleSubscriptionDeleted() Mark subscription as canceled
invoice.payment_failed handleInvoicePaymentFailed() Notify customer/user

2.4 Service Integration

  • Type::SUBSCRIPTION = 3 already exists in Service model
  • Order flow extended to support subscription type
  • OrderService updated to call createConnectSubscription for subscription orders

3. Frontend Architecture (Supplier App)

3.1 Component Structure

Location: yoyacoo_fe/supplier/src/components/organisms/services/content/services/SubscriptionService/

SubscriptionService/
└── SubscriptionForm.tsx

Follows existing メニューフォーム pattern (CalendarService/EventSeminarService structure).

3.2 Form Structure

5 collapsible sections matching the wireframe:

  1. Basic Settings

    • Service Name (required, max 100 chars)
    • Description (optional, max 500 chars)
    • Availability Status (Continuous/Scheduled/Stopped)
  2. Subscription Settings

    • Billing Start Date (date picker)
    • Payment Cycle (Monthly from App Date / Monthly from 1st of Month)
    • Payment Method (Credit Card Only - read-only)
  3. Menu & Options

    • Menu Name (required, max 50 chars)
    • Price (JPY, required)
    • Capacity Limit (optional)
    • Contract Period (1mo/3mo/6mo/1yr/Ongoing)
    • Free Trial Period (None/7days/14days/30days)
    • Menu Description (optional)
    • Selection Method (Direct/Application Form)
  4. Post-Purchase

    • Download URL (optional)
    • Video URL (optional)
    • Terms of Use (Display/External Link)
  5. Contact Information

    • Name (required)
    • Email (required)
    • Phone (optional)

3.3 State Management

Jotai Atoms extended for subscription:

  • serviceFormAtom - includes subscription-specific fields
  • Follows existing atom/operations/selectors pattern

3.4 API Integration

Service API methods:

serviceApi.postSubscription()   // POST /api/subscriptions
serviceApi.getSubscription()    // GET /api/subscriptions/{id}
serviceApi.deleteSubscription() // DELETE /api/subscriptions/{id}

All calls use wrap() utility for consistent error handling.

3.5 Validation

Yup schemas following existing pattern:

  • subscriptionBasicSettingsSchema
  • subscriptionMenuSettingsSchema
  • subscriptionPostPurchaseSchema
  • subscriptionContactSchema

4. Data Flow

1. User creates Subscription Content service
   ↓
2. Customer views and subscribes to service
   ↓
3. Order created with subscription type4. Backend calls Stripe API:
   Stripe::subscriptions()->create([
       'customer' => $customerId,
       'items' => [['price' => $priceId]],
       'transfer_data' => ['destination' => $userStripeConnectId],
       'application_fee_percent' => $feePercent,
   ])
   ↓
5. Subscription saved to order_subscriptions table
   ↓
6. Webhooks update status on invoice.paid, subscription.deleted

5. API Endpoints

Backend (Laravel)

Method Endpoint Description
POST /api/subscriptions Create subscription
GET /api/subscriptions/{id} Get subscription status
DELETE /api/subscriptions/{id} Cancel subscription

Frontend (Service API)

// In supplier/src/apis/service/serviceApi.ts
const serviceApi = {
  postSubscription: wrap(ServiceApi, ServiceApi.prototype.postSubscription),
  getSubscription: wrap(ServiceApi, ServiceApi.prototype.getSubscription),
  deleteSubscription: wrap(ServiceApi, ServiceApi.prototype.deleteSubscription),
};

6. Error Handling

Frontend

  • Form validation with Yup (on blur + on submit)
  • API errors handled via ApiValidationError and ApiResponseError
  • Toast notifications for success/error states

Backend

  • Stripe webhook signature verification
  • Idempotent webhook handlers
  • Logging for all payment events

7. Testing

Backend

  • Unit tests for calcFeeAmount logic
  • Integration tests with Stripe CLI: stripe trigger invoice.paid

Frontend

  • Playwright E2E tests for subscription flow
  • Form validation tests

8. File Changes Summary

Backend (yoyacoo_be/common)

  • New: src/Base/Traits/StripeConnectSubscription.php
  • New: Migration for order_subscriptions table
  • Modified: OrderService to handle subscription type
  • Modified: ConnectWebhookController for subscription events

Frontend (yoyacoo_fe/supplier)

  • New: src/components/organisms/services/content/services/SubscriptionService/SubscriptionForm.tsx
  • New: Validation schemas for subscription
  • Modified: serviceApi.ts with subscription endpoints
  • Modified: serviceFormAtom for subscription state

9. Out of Scope (Phase 1)

  • User dashboard to view active subscribers
  • Customer subscription management UI (cancel/pause)
  • Proration handling for upgrades/downgrades
  • Direct Charges option (future enhancement)