diff --git a/alembic/versions/k9f0a1b2c3d4_add_tier_id_fk_to_subscriptions.py b/alembic/versions/k9f0a1b2c3d4_add_tier_id_fk_to_subscriptions.py new file mode 100644 index 00000000..db364e03 --- /dev/null +++ b/alembic/versions/k9f0a1b2c3d4_add_tier_id_fk_to_subscriptions.py @@ -0,0 +1,71 @@ +"""Add tier_id FK to vendor_subscriptions + +Revision ID: k9f0a1b2c3d4 +Revises: 2953ed10d22c +Create Date: 2025-12-26 + +Adds tier_id column to vendor_subscriptions table with FK to subscription_tiers. +Backfills tier_id based on existing tier (code) values. +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "k9f0a1b2c3d4" +down_revision = "2953ed10d22c" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Use batch mode for SQLite compatibility + with op.batch_alter_table("vendor_subscriptions", schema=None) as batch_op: + # Add tier_id column (nullable for backfill) + batch_op.add_column( + sa.Column("tier_id", sa.Integer(), nullable=True) + ) + # Create index for tier_id + batch_op.create_index( + "ix_vendor_subscriptions_tier_id", + ["tier_id"], + unique=False, + ) + # Add FK constraint + batch_op.create_foreign_key( + "fk_vendor_subscriptions_tier_id", + "subscription_tiers", + ["tier_id"], + ["id"], + ondelete="SET NULL", + ) + + # Backfill tier_id from tier code + # This updates existing subscriptions to link to their tier + op.execute( + """ + UPDATE vendor_subscriptions + SET tier_id = ( + SELECT id FROM subscription_tiers + WHERE subscription_tiers.code = vendor_subscriptions.tier + ) + WHERE EXISTS ( + SELECT 1 FROM subscription_tiers + WHERE subscription_tiers.code = vendor_subscriptions.tier + ) + """ + ) + + +def downgrade() -> None: + with op.batch_alter_table("vendor_subscriptions", schema=None) as batch_op: + # Drop FK constraint + batch_op.drop_constraint( + "fk_vendor_subscriptions_tier_id", + type_="foreignkey", + ) + # Drop index + batch_op.drop_index("ix_vendor_subscriptions_tier_id") + # Drop column + batch_op.drop_column("tier_id") diff --git a/docs/implementation/subscription-workflow-plan.md b/docs/implementation/subscription-workflow-plan.md new file mode 100644 index 00000000..d14f507d --- /dev/null +++ b/docs/implementation/subscription-workflow-plan.md @@ -0,0 +1,401 @@ +# Subscription Workflow Plan + +## Overview + +End-to-end subscription management workflow for vendors on the platform. + +--- + +## 1. Vendor Subscribes to a Tier + +### 1.1 New Vendor Registration Flow + +``` +Vendor Registration → Select Tier → Trial Period → Payment Setup → Active Subscription +``` + +**Steps:** +1. Vendor creates account (existing flow) +2. During onboarding, vendor selects a tier: + - Show tier comparison cards (Essential, Professional, Business, Enterprise) + - Highlight features and limits for each tier + - Default to 14-day trial on selected tier +3. Create `VendorSubscription` record with: + - `tier` = selected tier code + - `status` = "trial" + - `trial_ends_at` = now + 14 days + - `period_start` / `period_end` set for trial period +4. Before trial ends, prompt vendor to add payment method +5. On payment method added → Create Stripe subscription → Status becomes "active" + +### 1.2 Database Changes Required + +**Add FK relationship to `subscription_tiers`:** +```python +# VendorSubscription - Add proper FK +tier_id = Column(Integer, ForeignKey("subscription_tiers.id"), nullable=True) +tier_code = Column(String(20), nullable=False) # Keep for backwards compat + +# Relationship +tier_obj = relationship("SubscriptionTier", backref="subscriptions") +``` + +**Migration:** +1. Add `tier_id` column (nullable initially) +2. Populate `tier_id` from existing `tier` code values +3. Add FK constraint + +### 1.3 API Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/vendor/subscription/tiers` | GET | List available tiers for selection | +| `/api/v1/vendor/subscription/select-tier` | POST | Select tier during onboarding | +| `/api/v1/vendor/subscription/setup-payment` | POST | Create Stripe checkout for payment | + +--- + +## 2. Admin Views Subscription on Vendor Page + +### 2.1 Vendor Detail Page Enhancement + +**Location:** `/admin/vendors/{vendor_id}` + +**New Subscription Card:** +``` +┌─────────────────────────────────────────────────────────────┐ +│ Subscription [Edit] │ +├─────────────────────────────────────────────────────────────┤ +│ Tier: Professional Status: Active │ +│ Price: €99/month Since: Jan 15, 2025 │ +│ Next Billing: Feb 15, 2025 │ +├─────────────────────────────────────────────────────────────┤ +│ Usage This Period │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Orders │ │ Products │ │ Team Members │ │ +│ │ 234 / 500 │ │ 156 / ∞ │ │ 2 / 3 │ │ +│ │ ████████░░ │ │ ████████████ │ │ ██████░░░░ │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ Add-ons: Custom Domain (mydomain.com), 5 Email Addresses │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 2.2 Files to Modify + +- `app/templates/admin/vendor-detail.html` - Add subscription card +- `static/admin/js/vendor-detail.js` - Load subscription data +- `app/api/v1/admin/vendors.py` - Include subscription in vendor response + +### 2.3 Admin Quick Actions + +From the vendor page, admin can: +- **Change Tier** - Upgrade/downgrade vendor +- **Override Limits** - Set custom limits (enterprise deals) +- **Extend Trial** - Give more trial days +- **Cancel Subscription** - With reason +- **Manage Add-ons** - Add/remove add-ons + +--- + +## 3. Tier Upgrade/Downgrade + +### 3.1 Admin-Initiated Change + +**Location:** Admin vendor page → Subscription card → [Edit] button + +**Modal: Change Subscription Tier** +``` +┌─────────────────────────────────────────────────────────┐ +│ Change Subscription Tier [X] │ +├─────────────────────────────────────────────────────────┤ +│ Current: Professional (€99/month) │ +│ │ +│ New Tier: │ +│ ○ Essential (€49/month) - Downgrade │ +│ ● Business (€199/month) - Upgrade │ +│ ○ Enterprise (Custom) - Contact required │ +│ │ +│ When to apply: │ +│ ○ Immediately (prorate current period) │ +│ ● At next billing cycle (Feb 15, 2025) │ +│ │ +│ [ ] Notify vendor by email │ +│ │ +│ [Cancel] [Apply Change] │ +└─────────────────────────────────────────────────────────┘ +``` + +### 3.2 Vendor-Initiated Change + +**Location:** Vendor dashboard → Billing page → [Change Plan] + +**Flow:** +1. Vendor clicks "Change Plan" on billing page +2. Shows tier comparison with current tier highlighted +3. Vendor selects new tier +4. For upgrades: + - Show prorated amount for immediate change + - Or option to change at next billing + - Redirect to Stripe checkout if needed +5. For downgrades: + - Always schedule for next billing cycle + - Show what features they'll lose + - Confirmation required + +### 3.3 API Endpoints + +| Endpoint | Method | Actor | Description | +|----------|--------|-------|-------------| +| `/api/v1/admin/subscriptions/{vendor_id}/change-tier` | POST | Admin | Change vendor's tier | +| `/api/v1/vendor/billing/change-tier` | POST | Vendor | Request tier change | +| `/api/v1/vendor/billing/preview-change` | POST | Vendor | Preview proration | + +### 3.4 Stripe Integration + +**Upgrade (Immediate):** +```python +stripe.Subscription.modify( + subscription_id, + items=[{"price": new_price_id}], + proration_behavior="create_prorations" +) +``` + +**Downgrade (Scheduled):** +```python +stripe.Subscription.modify( + subscription_id, + items=[{"price": new_price_id}], + proration_behavior="none", + billing_cycle_anchor="unchanged" +) +# Store scheduled change in our DB +``` + +--- + +## 4. Add-ons Upselling + +### 4.1 Where Add-ons Are Displayed + +#### A. Vendor Billing Page +``` +/vendor/{code}/billing + +┌─────────────────────────────────────────────────────────────┐ +│ Available Add-ons │ +├─────────────────────────────────────────────────────────────┤ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ 🌐 Custom Domain │ │ 📧 Email Package │ │ +│ │ €15/year │ │ From €5/month │ │ +│ │ Use your own domain │ │ 5, 10, or 25 emails │ │ +│ │ [Add to Plan] │ │ [Add to Plan] │ │ +│ └─────────────────────┘ └─────────────────────┘ │ +│ │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ 🔒 Premium SSL │ │ 💾 Extra Storage │ │ +│ │ €49/year │ │ €5/month per 10GB │ │ +│ │ EV certificate │ │ More product images │ │ +│ │ [Add to Plan] │ │ [Add to Plan] │ │ +│ └─────────────────────┘ └─────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### B. Contextual Upsells + +**When vendor hits a limit:** +``` +┌─────────────────────────────────────────────────────────┐ +│ ⚠️ You've reached your order limit for this month │ +│ │ +│ Upgrade to Professional to get 500 orders/month │ +│ [Upgrade Now] [Dismiss] │ +└─────────────────────────────────────────────────────────┘ +``` + +**In settings when configuring domain:** +``` +┌─────────────────────────────────────────────────────────┐ +│ 🌐 Custom Domain │ +│ │ +│ Your shop is available at: myshop.platform.com │ +│ │ +│ Want to use your own domain like www.myshop.com? │ +│ Add the Custom Domain add-on for just €15/year │ +│ │ +│ [Add Custom Domain] │ +└─────────────────────────────────────────────────────────┘ +``` + +#### C. Upgrade Prompts in Tier Comparison + +When showing tier comparison, highlight what add-ons come included: +- Professional: Includes 1 custom domain +- Business: Includes custom domain + 5 email addresses +- Enterprise: All add-ons included + +### 4.2 Add-on Purchase Flow + +``` +Vendor clicks [Add to Plan] + ↓ +Modal: Configure Add-on + - Domain: Enter domain name, check availability + - Email: Select package (5/10/25) + ↓ +Create Stripe checkout session for add-on price + ↓ +On success: Create VendorAddOn record + ↓ +Provision add-on (domain registration, email setup) +``` + +### 4.3 Add-on Management + +**Vendor can view/manage in Billing page:** +``` +┌─────────────────────────────────────────────────────────────┐ +│ Your Add-ons │ +├─────────────────────────────────────────────────────────────┤ +│ Custom Domain myshop.com €15/year [Manage] │ +│ Email Package 5 addresses €5/month [Manage] │ +│ │ +│ Next billing: Feb 15, 2025 │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 4.4 Database: `vendor_addons` Table + +```python +class VendorAddOn(Base): + id = Column(Integer, primary_key=True) + vendor_id = Column(Integer, ForeignKey("vendors.id")) + addon_product_id = Column(Integer, ForeignKey("addon_products.id")) + + # Config (e.g., domain name, email count) + config = Column(JSON, nullable=True) + + # Stripe + stripe_subscription_item_id = Column(String(100)) + + # Status + status = Column(String(20)) # active, cancelled, pending_setup + provisioned_at = Column(DateTime) + + # Billing + quantity = Column(Integer, default=1) + + created_at = Column(DateTime) + cancelled_at = Column(DateTime, nullable=True) +``` + +--- + +## 5. Implementation Phases + +### Phase 1: Database & Core (Day 1-2) +- [ ] Add `tier_id` FK to VendorSubscription +- [ ] Create migration with data backfill +- [ ] Update subscription service to use tier relationship +- [ ] Update admin subscription endpoints + +### Phase 2: Admin Vendor Page (Day 2-3) +- [ ] Add subscription card to vendor detail page +- [ ] Show usage meters (orders, products, team) +- [ ] Add "Edit Subscription" modal +- [ ] Implement tier change API (admin) + +### Phase 3: Vendor Billing Page (Day 3-4) +- [ ] Create `/vendor/{code}/billing` page +- [ ] Show current plan and usage +- [ ] Add tier comparison/change UI +- [ ] Implement tier change API (vendor) +- [ ] Add Stripe checkout integration for upgrades + +### Phase 4: Add-ons (Day 4-5) +- [ ] Seed add-on products in database +- [ ] Add "Available Add-ons" section to billing page +- [ ] Implement add-on purchase flow +- [ ] Create VendorAddOn management +- [ ] Add contextual upsell prompts + +### Phase 5: Polish & Testing (Day 5-6) +- [ ] Email notifications for tier changes +- [ ] Webhook handling for Stripe events +- [ ] Usage limit enforcement updates +- [ ] End-to-end testing +- [ ] Documentation + +--- + +## 6. Files to Create/Modify + +### New Files +| File | Purpose | +|------|---------| +| `app/templates/vendor/billing.html` | Vendor billing page | +| `static/vendor/js/billing.js` | Billing page JS | +| `app/api/v1/vendor/billing.py` | Vendor billing endpoints | +| `app/services/addon_service.py` | Add-on management | + +### Modified Files +| File | Changes | +|------|---------| +| `models/database/subscription.py` | Add tier_id FK | +| `app/templates/admin/vendor-detail.html` | Add subscription card | +| `static/admin/js/vendor-detail.js` | Load subscription data | +| `app/api/v1/admin/vendors.py` | Include subscription in response | +| `app/api/v1/admin/subscriptions.py` | Add tier change endpoint | +| `app/services/subscription_service.py` | Tier change logic | +| `app/templates/vendor/partials/sidebar.html` | Add Billing link | + +--- + +## 7. API Summary + +### Admin APIs +``` +GET /admin/vendors/{id} # Includes subscription +POST /admin/subscriptions/{vendor_id}/change-tier +POST /admin/subscriptions/{vendor_id}/override-limits +POST /admin/subscriptions/{vendor_id}/extend-trial +POST /admin/subscriptions/{vendor_id}/cancel +``` + +### Vendor APIs +``` +GET /vendor/billing/subscription # Current subscription +GET /vendor/billing/tiers # Available tiers +POST /vendor/billing/preview-change # Preview tier change +POST /vendor/billing/change-tier # Request tier change +POST /vendor/billing/checkout # Stripe checkout session + +GET /vendor/billing/addons # Available add-ons +GET /vendor/billing/my-addons # Vendor's add-ons +POST /vendor/billing/addons/purchase # Purchase add-on +DELETE /vendor/billing/addons/{id} # Cancel add-on +``` + +--- + +## 8. Questions to Resolve + +1. **Trial without payment method?** + - Allow full trial without card, or require card upfront? + +2. **Downgrade handling:** + - What happens if vendor has more products than new tier allows? + - Block downgrade, or just prevent new products? + +3. **Enterprise tier:** + - Self-service or contact sales only? + - Custom pricing in UI or hidden? + +4. **Add-on provisioning:** + - Domain: Use reseller API or manual process? + - Email: Integrate with email provider or manual? + +5. **Grace period:** + - How long after payment failure before suspension? + - What gets disabled first? diff --git a/models/database/subscription.py b/models/database/subscription.py index 5688f2c7..fdd941f1 100644 --- a/models/database/subscription.py +++ b/models/database/subscription.py @@ -431,7 +431,10 @@ class VendorSubscription(Base, TimestampMixin): Integer, ForeignKey("vendors.id"), unique=True, nullable=False, index=True ) - # Tier + # Tier - tier_id is the FK, tier (code) kept for backwards compatibility + tier_id = Column( + Integer, ForeignKey("subscription_tiers.id"), nullable=True, index=True + ) tier = Column( String(20), default=TierCode.ESSENTIAL.value, nullable=False, index=True ) @@ -479,6 +482,7 @@ class VendorSubscription(Base, TimestampMixin): # Relationships vendor = relationship("Vendor", back_populates="subscription") + tier_obj = relationship("SubscriptionTier", backref="subscriptions") __table_args__ = ( Index("idx_subscription_vendor_status", "vendor_id", "status"), @@ -494,7 +498,20 @@ class VendorSubscription(Base, TimestampMixin): @property def tier_limits(self) -> dict: - """Get the limit definitions for current tier.""" + """Get the limit definitions for current tier. + + Uses database tier (tier_obj) if available, otherwise falls back + to hardcoded TIER_LIMITS for backwards compatibility. + """ + # Use database tier if relationship is loaded + if self.tier_obj is not None: + return { + "orders_per_month": self.tier_obj.orders_per_month, + "products_limit": self.tier_obj.products_limit, + "team_members": self.tier_obj.team_members, + "features": self.tier_obj.features or [], + } + # Fall back to hardcoded limits return TIER_LIMITS.get(TierCode(self.tier), TIER_LIMITS[TierCode.ESSENTIAL]) @property