Send Payment Link
Overview Description
The Send Payment Link feature generates a Stripe Checkout session for a custom contract and sends the payment link via email to the customer. This allows customers to activate their custom contract by completing the payment process through Stripe.
Pre-conditions
- User must be authenticated as admin (SuperAdmin or AdminStaff role)
- Custom contract must exist with status 'draft' or 'offered'
- Contract must be linked to a valid subscription
- Subscription must have
pricing_type = 'custom'or be cancelled - Package plan must be configured with Stripe product ID
- Subscription must have valid Stripe customer ID (or will be created)
Dependencies
- Stripe Checkout Service (for creating payment session)
- SendGrid Email Service (for sending payment link email)
- User Service (for Stripe customer synchronization)
- Package To Provider mapping (for Stripe product configuration)
Swagger Link
API: Send Payment Link
Case Documentation
Case 1: Successful Payment Link Generation and Email Sending
Description
Administrator successfully generates a Stripe payment link for a custom contract and sends it via email to the customer. The contract status is updated from 'draft' to 'offered'.
Sequence Diagram
sequenceDiagram
participant Admin
participant Controller as CustomContractController
participant Request as SendPaymentLinkRequest
participant Service as CustomContractService
participant ContractRepo as CustomContractRepository
participant StripeService as CheckoutStripeService
participant EmailService as SendGridEmailService
participant Stripe((Stripe API))
participant Database
Note over Admin,Database: POST /api/v1/admin/custom-contracts/{id}/send-payment-link
rect rgb(200, 255, 200)
Note right of Admin: Happy Case Flow
Admin->>Controller: POST request with success_url and cancel_url
rect rgb(200, 230, 255)
Note right of Controller: Input Validation
Controller->>Request: validate(data)
Request->>Request: Check required URLs
Request->>Request: Validate URL format
Request-->>Controller: Validation passed
end
rect rgb(200, 255, 255)
Note right of Controller: Business Logic Processing
Controller->>Service: sendPaymentLink(contractId, data)
Service->>ContractRepo: findById(contractId)
ContractRepo->>Database: SELECT * FROM custom_contracts WHERE id = ?
Database-->>ContractRepo: Return contract data
ContractRepo-->>Service: Return contract with relations
Service->>Service: Validate contract status (draft/offered)
Service->>Service: Validate subscription pricing_type
Service->>Service: Get package plan and Stripe product ID
Service->>Service: Ensure Stripe customer ID exists
rect rgb(255, 230, 200)
Note right of Service: Stripe Checkout Session Creation
Service->>StripeService: createCheckoutSession(params)
StripeService->>Stripe: POST /v1/checkout/sessions
Stripe-->>StripeService: Return session object with URL
StripeService-->>Service: Return session data
end
Service->>ContractRepo: update provider_checkout_session_id
ContractRepo->>Database: UPDATE custom_contracts SET provider_checkout_session_id
Database-->>ContractRepo: Update confirmed
Service->>Service: Update status from draft to offered
Service->>ContractRepo: update status
ContractRepo->>Database: UPDATE custom_contracts SET status = 'offered'
Database-->>ContractRepo: Update confirmed
rect rgb(255, 230, 200)
Note right of Service: Email Notification
Service->>EmailService: sendNotification(email, subject, template_data)
EmailService->>EmailService: Compose email with payment link
EmailService-->>Service: Email sent confirmation
end
Service-->>Controller: Return payment_link URL
end
Controller-->>Admin: 200 OK (payment_link)
end
rect rgb(255, 200, 200)
Note right of Admin: Error Scenarios
rect rgb(255, 230, 230)
alt Validation Error
Request-->>Controller: Validation failed
Controller-->>Admin: 422 Unprocessable Entity
else Contract Not Found
ContractRepo-->>Service: Contract not found
Service-->>Controller: Error: Contract not found
Controller-->>Admin: 400 Bad Request
else Invalid Contract Status
Service-->>Service: Status not draft/offered
Service-->>Controller: Error: Invalid status
Controller-->>Admin: 400 Bad Request
else Package Plan Not Configured
Service-->>Service: Stripe product ID missing
Service-->>Controller: Error: Price not configured
Controller-->>Admin: 400 Bad Request
else Stripe API Error
Stripe-->>StripeService: 4xx/5xx Error
StripeService-->>Service: Stripe error
Service-->>Controller: Error: Payment link failed
Controller-->>Admin: 400 Bad Request
else Email Missing
Service-->>Service: No email address
Service-->>Controller: Error: Email missing
Controller-->>Admin: 400 Bad Request
end
end
end
Steps
Step 1: Request Validation
- Description: Validate payment link request parameters
- Request:
POST /api/v1/admin/custom-contracts/{id}/send-payment-link - Authorization: User must have SuperAdmin or AdminStaff role
- Validation Rules:
success_url: Required, valid URL formatcancel_url: Required, valid URL formatemail: Optional, valid email format (defaults to subscription email)
Step 2: Contract Validation
- Description: Verify contract exists and is in valid state
- Action:
- Load contract with subscription, group, user, and package plan relations
- Verify contract status is 'draft' or 'offered'
- Verify subscription has
pricing_type = 'custom'or is cancelled - Get package_plan_id from contract or subscription
- Business Rules:
- Only contracts in 'draft' or 'offered' status can have payment links sent
- Subscription must support custom pricing
- Potential Errors:
- Contract not found (404)
- Invalid contract status (400)
- Subscription type not allowed (400)
Step 3: Stripe Product Configuration
- Description: Retrieve Stripe product ID for the package plan
- Action:
- Get payment provider ID (Stripe)
- Query package_to_providers table for product mapping
- Verify provider_product_id exists
- Business Rules:
- Package plan must be configured in Stripe
- Product ID is required for creating checkout session
- Potential Errors:
- Package plan not configured (400)
- Price not found in Stripe (400)
Step 4: Stripe Customer Synchronization
- Description: Ensure subscription has valid Stripe customer ID
- Action:
- Check if subscription has
payment_provider_customer_id - If missing, get user from contract or subscription
- Sync Stripe customer using UserService
- Update subscription with customer ID
- Check if subscription has
- Database Operations:
- UPDATE subscriptions SET payment_provider_customer_id
Step 5: Create Stripe Checkout Session
- Description: Generate Stripe payment session with custom price
- Action:
- Build session parameters:
mode = 'subscription'customer = stripe_customer_idline_itemswithprice_data(custom amount and interval)subscription_data.metadata(custom_contract_id, subscription_slug)metadata(custom_contract_id, subscription_slug)success_urlandcancel_urlfrom request
- Call Stripe API to create checkout session
- Extract session URL from response
- Build session parameters:
- External API Call:
- POST to Stripe
/v1/checkout/sessions
- POST to Stripe
- Potential Errors:
- Stripe API error (network, validation, etc.)
Step 6: Save Session ID and Update Status
- Description: Store Stripe session ID and update contract status
- Action:
- Update contract with
provider_checkout_session_id - If status is 'draft', update to 'offered'
- Log session creation with contract and subscription IDs
- Update contract with
- Database Operations:
- UPDATE custom_contracts SET provider_checkout_session_id, status
Step 7: Send Email Notification
- Description: Send payment link via email to customer
- Action:
- Get recipient email (from request or subscription)
- Build email subject with contract code
- Send email using SendGrid with template data:
payment_link: Session URLcustom_contract_code: Contract codeamount: Contract amountbilling_interval: Billing interval
- Log email sending
- External API Call:
- SendGrid email API
- Potential Errors:
- Email address missing (400)
- Email sending failed (logged but not blocking)
Step 8: Return Response
- Description: Return payment link URL to admin
- Response Data:
payment_link: Stripe checkout session URL
- Success Message: "支払いリンクが送信されました" (Payment link sent)
Database Related Tables & Fields
erDiagram
custom_contracts {
bigint id PK
bigint subscription_id FK
bigint package_plan_id FK
string code "Contract identifier"
string billing_interval "month or year"
string currency "Currency code"
integer amount "Amount in minor unit"
string status "draft, offered, active, expired, cancelled"
string provider_checkout_session_id "Stripe session ID (updated in this API)"
string provider_price_id "Stripe price ID (nullable)"
string provider_subscription_item_id "Stripe subscription item ID (nullable)"
timestamp created_at
timestamp updated_at
}
subscriptions {
bigint id PK
string slug "Subscription identifier"
bigint package_plan_id FK
bigint group_id FK
bigint user_id FK
string pricing_type "standard or custom"
bigint custom_contract_id FK
string payment_provider_customer_id "Stripe customer ID"
string email "Subscription owner email"
timestamp created_at
timestamp updated_at
}
package_plans {
bigint id PK
bigint package_id FK
string name "Plan name"
timestamp created_at
timestamp updated_at
}
package_to_providers {
bigint id PK
bigint package_id FK
bigint provider_id FK
string provider_product_id "Stripe product ID (required)"
timestamp created_at
timestamp updated_at
}
custom_contracts ||--|| subscriptions : has
custom_contracts ||--|| package_plans : based_on
package_plans ||--o{ package_to_providers : has_mapping
Error Handling
- Log: Stripe operations are logged via
logStripe()method - Error Detail:
| Status Code | Error Message | Description |
|---|---|---|
| 422 | Validation error messages | When URLs are invalid or missing |
| 400 | "カスタムプランが見つかりませんでした" | When contract doesn't exist |
| 400 | "サブスクリプションが見つかりませんでした" | When subscription not linked to contract |
| 400 | "サブスクリプションのタイプ切り替えは許可されていません" | When subscription pricing_type invalid |
| 400 | "無効なステータスです" | When contract status not draft/offered |
| 400 | "パッケージプランが必要です" | When package_plan_id missing |
| 400 | "パッケージプランが見つかりませんでした" | When package plan doesn't exist |
| 400 | "価格が設定されていません" | When Stripe product not configured |
| 400 | "支払いリンクの作成に失敗しました" | When Stripe API fails |
| 400 | "メールアドレスが見つかりません" | When email address missing |
| 400 | Generic error with exception message | For unexpected errors |
Additional Notes
Stripe Checkout Session Configuration
- Mode:
subscription(notpayment) - Customer: Must be existing Stripe customer
- Line Items: Uses
price_datafor custom amounts (notpricefrom catalog) - Metadata: Critical for webhook processing
custom_contract_id: Links payment to contractsubscription_slug: Identifies target subscription
Payment Link Lifecycle
- Admin creates contract (status: draft)
- Admin sends payment link (status: offered, session_id saved)
- Customer clicks link and completes payment
- Stripe webhook
invoice.paidactivates contract (status: active) - Contract remains active until cancelled or expired
Email Template
- Subject includes contract code for easy identification
- Template includes:
- Payment link button
- Contract details (amount, interval)
- Instructions for completing payment
- Support contact information
Idempotency
- Can send payment link multiple times for same contract
- Each call creates new Stripe session (old ones expire automatically)
- Status remains 'offered' if already sent before
- Latest session ID overwrites previous one
Security Considerations
- Payment links are time-limited by Stripe (24 hours default)
- Links are single-use (expire after successful payment)
- Customer email must match subscription email for security
- Admin cannot see or modify payment data (handled by Stripe)
Performance Considerations
- External API calls to Stripe (average 500-1000ms)
- Email sending is synchronous but fast
- Database updates are minimal (single record)
- Consider adding queue for email sending in high-volume scenarios