818 lines
24 KiB
TypeScript
818 lines
24 KiB
TypeScript
/**
|
|
* Admin Workflow Cross-Feature Integration Tests
|
|
*
|
|
* Tests the admin-facing data contracts between:
|
|
* - Merchant (subscription tier management)
|
|
* - Marketplace (tier consumption)
|
|
* - Platform Admin (UI management)
|
|
*
|
|
* This validates:
|
|
* 1. Tier configuration flows correctly from Merchant to Marketplace
|
|
* 2. Usage limits are enforced based on tier
|
|
* 3. Admin operations produce expected data shapes
|
|
*/
|
|
|
|
import './setup';
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
/**
|
|
* Merchant Service Contract Types
|
|
*/
|
|
interface MerchantProduct {
|
|
id: string;
|
|
type: 'subscription' | 'one-time' | 'gift-card';
|
|
slug: string;
|
|
name: string;
|
|
description: string;
|
|
price: number;
|
|
currency: string;
|
|
isActive: boolean;
|
|
metadata: Record<string, unknown>;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
interface SubscriptionTierConfig {
|
|
id: string;
|
|
productId: string;
|
|
slug: string;
|
|
name: string;
|
|
description: string;
|
|
price: number;
|
|
currency: string;
|
|
billingPeriod: 'monthly' | 'yearly';
|
|
trialDays: number;
|
|
features: string[];
|
|
actionPools: {
|
|
messagesPerWeek: number;
|
|
viewsPerMonth: number;
|
|
discoveriesPerMonth: number;
|
|
};
|
|
rolloverPolicy: 'none' | 'partial' | 'full';
|
|
badge: string | null;
|
|
recencyCacheSeconds: number;
|
|
sortOrder: number;
|
|
isActive: boolean;
|
|
}
|
|
|
|
interface MerchantTierListResponse {
|
|
tiers: SubscriptionTierConfig[];
|
|
total: number;
|
|
}
|
|
|
|
interface MerchantTierStatsResponse {
|
|
tierSlug: string;
|
|
activeSubscribers: number;
|
|
monthlyRevenue: number;
|
|
churnRate: number;
|
|
averageLifetimeMonths: number;
|
|
}
|
|
|
|
/**
|
|
* Marketplace Service Contract Types
|
|
*/
|
|
interface MarketplaceTierResponse {
|
|
id: string;
|
|
slug: string;
|
|
name: string;
|
|
price: number;
|
|
currency: string;
|
|
billingPeriod: string;
|
|
features: string[];
|
|
limits: {
|
|
messagesPerWeek: number;
|
|
viewsPerMonth: number;
|
|
discoveriesPerMonth: number;
|
|
};
|
|
}
|
|
|
|
interface PlatformSubscription {
|
|
id: string;
|
|
userId: string;
|
|
tierId: string;
|
|
tierSlug: string;
|
|
status: 'active' | 'cancelled' | 'paused' | 'expired';
|
|
startedAt: string;
|
|
expiresAt: string;
|
|
usage: {
|
|
messagesUsed: number;
|
|
messagesLimit: number;
|
|
viewsUsed: number;
|
|
viewsLimit: number;
|
|
discoveriesUsed: number;
|
|
discoveriesLimit: number;
|
|
};
|
|
}
|
|
|
|
interface UsageTrackingResponse {
|
|
userId: string;
|
|
period: 'week' | 'month';
|
|
usage: {
|
|
messages: { used: number; limit: number; remaining: number };
|
|
views: { used: number; limit: number; remaining: number };
|
|
discoveries: { used: number; limit: number; remaining: number };
|
|
};
|
|
resetAt: string;
|
|
}
|
|
|
|
/**
|
|
* Platform Admin Contract Types
|
|
*/
|
|
interface AdminProductCreateRequest {
|
|
type: 'subscription';
|
|
slug: string;
|
|
name: string;
|
|
description: string;
|
|
price: number;
|
|
currency: string;
|
|
metadata: {
|
|
billingPeriod: 'monthly' | 'yearly';
|
|
trialDays: number;
|
|
features: string[];
|
|
actionPools: {
|
|
messagesPerWeek: number;
|
|
viewsPerMonth: number;
|
|
discoveriesPerMonth: number;
|
|
};
|
|
rolloverPolicy: 'none' | 'partial' | 'full';
|
|
badge?: string;
|
|
recencyCacheSeconds: number;
|
|
sortOrder: number;
|
|
};
|
|
}
|
|
|
|
interface AdminProductUpdateRequest {
|
|
name?: string;
|
|
description?: string;
|
|
price?: number;
|
|
isActive?: boolean;
|
|
metadata?: Partial<AdminProductCreateRequest['metadata']>;
|
|
}
|
|
|
|
describe('Admin Workflow - Merchant to Marketplace Integration', () => {
|
|
describe('Merchant Tier Configuration Contracts', () => {
|
|
it('validates subscription tier config has all required fields', () => {
|
|
const tier: SubscriptionTierConfig = {
|
|
id: 'tier_premium_monthly',
|
|
productId: 'prod_123',
|
|
slug: 'premium',
|
|
name: 'Premium',
|
|
description: 'Full access to all features',
|
|
price: 29.99,
|
|
currency: 'EUR',
|
|
billingPeriod: 'monthly',
|
|
trialDays: 7,
|
|
features: [
|
|
'Unlimited messages',
|
|
'Priority support',
|
|
'Verified badge',
|
|
'Advanced filters',
|
|
],
|
|
actionPools: {
|
|
messagesPerWeek: 500,
|
|
viewsPerMonth: 1000,
|
|
discoveriesPerMonth: 100,
|
|
},
|
|
rolloverPolicy: 'partial',
|
|
badge: 'premium',
|
|
recencyCacheSeconds: 300,
|
|
sortOrder: 2,
|
|
isActive: true,
|
|
};
|
|
|
|
expect(tier.slug).toMatch(/^[a-z0-9-]+$/);
|
|
expect(tier.price).toBeGreaterThanOrEqual(0);
|
|
expect(['monthly', 'yearly']).toContain(tier.billingPeriod);
|
|
expect(tier.trialDays).toBeGreaterThanOrEqual(0);
|
|
expect(tier.actionPools.messagesPerWeek).toBeGreaterThan(0);
|
|
expect(['none', 'partial', 'full']).toContain(tier.rolloverPolicy);
|
|
});
|
|
|
|
it('validates tier list response structure', () => {
|
|
const response: MerchantTierListResponse = {
|
|
tiers: [
|
|
{
|
|
id: 'tier_free',
|
|
productId: 'prod_free',
|
|
slug: 'free',
|
|
name: 'Free',
|
|
description: 'Basic access',
|
|
price: 0,
|
|
currency: 'EUR',
|
|
billingPeriod: 'monthly',
|
|
trialDays: 0,
|
|
features: ['Basic messaging'],
|
|
actionPools: {
|
|
messagesPerWeek: 5,
|
|
viewsPerMonth: 20,
|
|
discoveriesPerMonth: 10,
|
|
},
|
|
rolloverPolicy: 'none',
|
|
badge: null,
|
|
recencyCacheSeconds: 600,
|
|
sortOrder: 0,
|
|
isActive: true,
|
|
},
|
|
{
|
|
id: 'tier_premium',
|
|
productId: 'prod_premium',
|
|
slug: 'premium',
|
|
name: 'Premium',
|
|
description: 'Full access',
|
|
price: 29.99,
|
|
currency: 'EUR',
|
|
billingPeriod: 'monthly',
|
|
trialDays: 7,
|
|
features: ['Unlimited messages', 'Priority support'],
|
|
actionPools: {
|
|
messagesPerWeek: 500,
|
|
viewsPerMonth: 1000,
|
|
discoveriesPerMonth: 100,
|
|
},
|
|
rolloverPolicy: 'partial',
|
|
badge: 'premium',
|
|
recencyCacheSeconds: 300,
|
|
sortOrder: 1,
|
|
isActive: true,
|
|
},
|
|
],
|
|
total: 2,
|
|
};
|
|
|
|
expect(response.tiers.length).toBe(response.total);
|
|
expect(response.tiers[0].sortOrder).toBeLessThan(
|
|
response.tiers[1].sortOrder
|
|
);
|
|
});
|
|
|
|
it('validates tier stats response structure', () => {
|
|
const stats: MerchantTierStatsResponse = {
|
|
tierSlug: 'premium',
|
|
activeSubscribers: 1250,
|
|
monthlyRevenue: 37462.5,
|
|
churnRate: 0.05,
|
|
averageLifetimeMonths: 8.5,
|
|
};
|
|
|
|
expect(stats.activeSubscribers).toBeGreaterThanOrEqual(0);
|
|
expect(stats.monthlyRevenue).toBeGreaterThanOrEqual(0);
|
|
expect(stats.churnRate).toBeGreaterThanOrEqual(0);
|
|
expect(stats.churnRate).toBeLessThanOrEqual(1);
|
|
});
|
|
});
|
|
|
|
describe('Marketplace Tier Consumption Contracts', () => {
|
|
it('validates marketplace tier response matches merchant structure', () => {
|
|
// Merchant produces this
|
|
const merchantTier: SubscriptionTierConfig = {
|
|
id: 'tier_premium',
|
|
productId: 'prod_123',
|
|
slug: 'premium',
|
|
name: 'Premium',
|
|
description: 'Full access',
|
|
price: 29.99,
|
|
currency: 'EUR',
|
|
billingPeriod: 'monthly',
|
|
trialDays: 7,
|
|
features: ['Unlimited messages', 'Priority support'],
|
|
actionPools: {
|
|
messagesPerWeek: 500,
|
|
viewsPerMonth: 1000,
|
|
discoveriesPerMonth: 100,
|
|
},
|
|
rolloverPolicy: 'partial',
|
|
badge: 'premium',
|
|
recencyCacheSeconds: 300,
|
|
sortOrder: 1,
|
|
isActive: true,
|
|
};
|
|
|
|
// Marketplace transforms to this
|
|
const marketplaceTier: MarketplaceTierResponse = {
|
|
id: merchantTier.id,
|
|
slug: merchantTier.slug,
|
|
name: merchantTier.name,
|
|
price: merchantTier.price,
|
|
currency: merchantTier.currency,
|
|
billingPeriod: merchantTier.billingPeriod,
|
|
features: merchantTier.features,
|
|
limits: {
|
|
messagesPerWeek: merchantTier.actionPools.messagesPerWeek,
|
|
viewsPerMonth: merchantTier.actionPools.viewsPerMonth,
|
|
discoveriesPerMonth: merchantTier.actionPools.discoveriesPerMonth,
|
|
},
|
|
};
|
|
|
|
// Verify transformation preserves key data
|
|
expect(marketplaceTier.slug).toBe(merchantTier.slug);
|
|
expect(marketplaceTier.price).toBe(merchantTier.price);
|
|
expect(marketplaceTier.limits.messagesPerWeek).toBe(
|
|
merchantTier.actionPools.messagesPerWeek
|
|
);
|
|
});
|
|
|
|
it('validates platform subscription structure', () => {
|
|
const subscription: PlatformSubscription = {
|
|
id: 'sub_123',
|
|
userId: 'usr_456',
|
|
tierId: 'tier_premium',
|
|
tierSlug: 'premium',
|
|
status: 'active',
|
|
startedAt: new Date().toISOString(),
|
|
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
usage: {
|
|
messagesUsed: 45,
|
|
messagesLimit: 500,
|
|
viewsUsed: 120,
|
|
viewsLimit: 1000,
|
|
discoveriesUsed: 8,
|
|
discoveriesLimit: 100,
|
|
},
|
|
};
|
|
|
|
expect(subscription.usage.messagesUsed).toBeLessThanOrEqual(
|
|
subscription.usage.messagesLimit
|
|
);
|
|
expect(subscription.usage.viewsUsed).toBeLessThanOrEqual(
|
|
subscription.usage.viewsLimit
|
|
);
|
|
expect(['active', 'cancelled', 'paused', 'expired']).toContain(
|
|
subscription.status
|
|
);
|
|
});
|
|
|
|
it('validates usage tracking response structure', () => {
|
|
const usage: UsageTrackingResponse = {
|
|
userId: 'usr_456',
|
|
period: 'week',
|
|
usage: {
|
|
messages: { used: 45, limit: 500, remaining: 455 },
|
|
views: { used: 120, limit: 1000, remaining: 880 },
|
|
discoveries: { used: 8, limit: 100, remaining: 92 },
|
|
},
|
|
resetAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
};
|
|
|
|
expect(usage.usage.messages.used + usage.usage.messages.remaining).toBe(
|
|
usage.usage.messages.limit
|
|
);
|
|
expect(['week', 'month']).toContain(usage.period);
|
|
});
|
|
});
|
|
|
|
describe('Platform Admin Operations Contracts', () => {
|
|
it('validates product create request structure', () => {
|
|
const request: AdminProductCreateRequest = {
|
|
type: 'subscription',
|
|
slug: 'elite',
|
|
name: 'Elite',
|
|
description: 'Top-tier access for power users',
|
|
price: 99.99,
|
|
currency: 'EUR',
|
|
metadata: {
|
|
billingPeriod: 'monthly',
|
|
trialDays: 14,
|
|
features: [
|
|
'Unlimited everything',
|
|
'Dedicated support',
|
|
'Custom badge',
|
|
'Analytics dashboard',
|
|
],
|
|
actionPools: {
|
|
messagesPerWeek: 2000,
|
|
viewsPerMonth: 5000,
|
|
discoveriesPerMonth: 500,
|
|
},
|
|
rolloverPolicy: 'full',
|
|
badge: 'elite',
|
|
recencyCacheSeconds: 60,
|
|
sortOrder: 3,
|
|
},
|
|
};
|
|
|
|
expect(request.type).toBe('subscription');
|
|
expect(request.slug).toMatch(/^[a-z0-9-]+$/);
|
|
expect(request.metadata.actionPools.messagesPerWeek).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('validates product update request allows partial updates', () => {
|
|
const request: AdminProductUpdateRequest = {
|
|
price: 89.99,
|
|
metadata: {
|
|
features: [
|
|
'Unlimited everything',
|
|
'Dedicated support',
|
|
'Custom badge',
|
|
'Analytics dashboard',
|
|
'NEW: Priority matching',
|
|
],
|
|
},
|
|
};
|
|
|
|
// Only specified fields should be present
|
|
expect(request.price).toBeDefined();
|
|
expect(request.metadata?.features).toBeDefined();
|
|
expect(request.name).toBeUndefined();
|
|
expect(request.description).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Cross-Service Data Flow Contracts', () => {
|
|
it('validates tier creation flow: Admin → Merchant → Marketplace', () => {
|
|
// Step 1: Admin creates product via Platform Admin UI
|
|
const adminRequest: AdminProductCreateRequest = {
|
|
type: 'subscription',
|
|
slug: 'vip',
|
|
name: 'VIP',
|
|
description: 'VIP access',
|
|
price: 49.99,
|
|
currency: 'EUR',
|
|
metadata: {
|
|
billingPeriod: 'monthly',
|
|
trialDays: 0,
|
|
features: ['VIP features'],
|
|
actionPools: {
|
|
messagesPerWeek: 200,
|
|
viewsPerMonth: 500,
|
|
discoveriesPerMonth: 50,
|
|
},
|
|
rolloverPolicy: 'none',
|
|
recencyCacheSeconds: 300,
|
|
sortOrder: 2,
|
|
},
|
|
};
|
|
|
|
// Step 2: Merchant stores as product + tier config
|
|
const merchantProduct: MerchantProduct = {
|
|
id: 'prod_vip',
|
|
type: 'subscription',
|
|
slug: adminRequest.slug,
|
|
name: adminRequest.name,
|
|
description: adminRequest.description,
|
|
price: adminRequest.price,
|
|
currency: adminRequest.currency,
|
|
isActive: true,
|
|
metadata: adminRequest.metadata,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
const merchantTier: SubscriptionTierConfig = {
|
|
id: 'tier_vip',
|
|
productId: merchantProduct.id,
|
|
slug: merchantProduct.slug,
|
|
name: merchantProduct.name,
|
|
description: merchantProduct.description,
|
|
price: merchantProduct.price,
|
|
currency: merchantProduct.currency,
|
|
billingPeriod: adminRequest.metadata.billingPeriod,
|
|
trialDays: adminRequest.metadata.trialDays,
|
|
features: adminRequest.metadata.features,
|
|
actionPools: adminRequest.metadata.actionPools,
|
|
rolloverPolicy: adminRequest.metadata.rolloverPolicy,
|
|
badge: adminRequest.metadata.badge || null,
|
|
recencyCacheSeconds: adminRequest.metadata.recencyCacheSeconds,
|
|
sortOrder: adminRequest.metadata.sortOrder,
|
|
isActive: merchantProduct.isActive,
|
|
};
|
|
|
|
// Step 3: Marketplace fetches via MerchantClientService
|
|
const marketplaceTier: MarketplaceTierResponse = {
|
|
id: merchantTier.id,
|
|
slug: merchantTier.slug,
|
|
name: merchantTier.name,
|
|
price: merchantTier.price,
|
|
currency: merchantTier.currency,
|
|
billingPeriod: merchantTier.billingPeriod,
|
|
features: merchantTier.features,
|
|
limits: {
|
|
messagesPerWeek: merchantTier.actionPools.messagesPerWeek,
|
|
viewsPerMonth: merchantTier.actionPools.viewsPerMonth,
|
|
discoveriesPerMonth: merchantTier.actionPools.discoveriesPerMonth,
|
|
},
|
|
};
|
|
|
|
// Verify data flows correctly through all services
|
|
expect(marketplaceTier.slug).toBe(adminRequest.slug);
|
|
expect(marketplaceTier.price).toBe(adminRequest.price);
|
|
expect(marketplaceTier.limits.messagesPerWeek).toBe(
|
|
adminRequest.metadata.actionPools.messagesPerWeek
|
|
);
|
|
});
|
|
|
|
it('validates subscription flow: User subscribes → Usage limits applied', () => {
|
|
// Marketplace tier from Merchant
|
|
const tier: MarketplaceTierResponse = {
|
|
id: 'tier_premium',
|
|
slug: 'premium',
|
|
name: 'Premium',
|
|
price: 29.99,
|
|
currency: 'EUR',
|
|
billingPeriod: 'monthly',
|
|
features: ['Unlimited messages'],
|
|
limits: {
|
|
messagesPerWeek: 500,
|
|
viewsPerMonth: 1000,
|
|
discoveriesPerMonth: 100,
|
|
},
|
|
};
|
|
|
|
// User subscribes - Marketplace creates subscription
|
|
const subscription: PlatformSubscription = {
|
|
id: 'sub_new',
|
|
userId: 'usr_subscriber',
|
|
tierId: tier.id,
|
|
tierSlug: tier.slug,
|
|
status: 'active',
|
|
startedAt: new Date().toISOString(),
|
|
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
usage: {
|
|
messagesUsed: 0,
|
|
messagesLimit: tier.limits.messagesPerWeek,
|
|
viewsUsed: 0,
|
|
viewsLimit: tier.limits.viewsPerMonth,
|
|
discoveriesUsed: 0,
|
|
discoveriesLimit: tier.limits.discoveriesPerMonth,
|
|
},
|
|
};
|
|
|
|
// Limits match tier config
|
|
expect(subscription.usage.messagesLimit).toBe(tier.limits.messagesPerWeek);
|
|
expect(subscription.usage.viewsLimit).toBe(tier.limits.viewsPerMonth);
|
|
expect(subscription.tierSlug).toBe(tier.slug);
|
|
});
|
|
|
|
it('validates usage enforcement blocks actions at limit', () => {
|
|
const subscription: PlatformSubscription = {
|
|
id: 'sub_123',
|
|
userId: 'usr_456',
|
|
tierId: 'tier_free',
|
|
tierSlug: 'free',
|
|
status: 'active',
|
|
startedAt: new Date().toISOString(),
|
|
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
usage: {
|
|
messagesUsed: 5,
|
|
messagesLimit: 5,
|
|
viewsUsed: 18,
|
|
viewsLimit: 20,
|
|
discoveriesUsed: 10,
|
|
discoveriesLimit: 10,
|
|
},
|
|
};
|
|
|
|
// Check if user can perform actions
|
|
const canSendMessage =
|
|
subscription.usage.messagesUsed < subscription.usage.messagesLimit;
|
|
const canViewProfile =
|
|
subscription.usage.viewsUsed < subscription.usage.viewsLimit;
|
|
const canDiscover =
|
|
subscription.usage.discoveriesUsed < subscription.usage.discoveriesLimit;
|
|
|
|
expect(canSendMessage).toBe(false); // At limit
|
|
expect(canViewProfile).toBe(true); // Still has capacity
|
|
expect(canDiscover).toBe(false); // At limit
|
|
});
|
|
|
|
it('validates tier upgrade updates subscription limits', () => {
|
|
// Before upgrade - Free tier
|
|
const beforeUpgrade: PlatformSubscription = {
|
|
id: 'sub_123',
|
|
userId: 'usr_456',
|
|
tierId: 'tier_free',
|
|
tierSlug: 'free',
|
|
status: 'active',
|
|
startedAt: new Date().toISOString(),
|
|
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
usage: {
|
|
messagesUsed: 3,
|
|
messagesLimit: 5,
|
|
viewsUsed: 15,
|
|
viewsLimit: 20,
|
|
discoveriesUsed: 8,
|
|
discoveriesLimit: 10,
|
|
},
|
|
};
|
|
|
|
// New tier config
|
|
const premiumTier: MarketplaceTierResponse = {
|
|
id: 'tier_premium',
|
|
slug: 'premium',
|
|
name: 'Premium',
|
|
price: 29.99,
|
|
currency: 'EUR',
|
|
billingPeriod: 'monthly',
|
|
features: ['Unlimited messages'],
|
|
limits: {
|
|
messagesPerWeek: 500,
|
|
viewsPerMonth: 1000,
|
|
discoveriesPerMonth: 100,
|
|
},
|
|
};
|
|
|
|
// After upgrade - Premium tier, usage preserved but limits increased
|
|
const afterUpgrade: PlatformSubscription = {
|
|
...beforeUpgrade,
|
|
tierId: premiumTier.id,
|
|
tierSlug: premiumTier.slug,
|
|
usage: {
|
|
messagesUsed: beforeUpgrade.usage.messagesUsed, // Preserved
|
|
messagesLimit: premiumTier.limits.messagesPerWeek, // Increased
|
|
viewsUsed: beforeUpgrade.usage.viewsUsed,
|
|
viewsLimit: premiumTier.limits.viewsPerMonth,
|
|
discoveriesUsed: beforeUpgrade.usage.discoveriesUsed,
|
|
discoveriesLimit: premiumTier.limits.discoveriesPerMonth,
|
|
},
|
|
};
|
|
|
|
// Usage preserved, limits increased
|
|
expect(afterUpgrade.usage.messagesUsed).toBe(
|
|
beforeUpgrade.usage.messagesUsed
|
|
);
|
|
expect(afterUpgrade.usage.messagesLimit).toBeGreaterThan(
|
|
beforeUpgrade.usage.messagesLimit
|
|
);
|
|
expect(afterUpgrade.tierSlug).toBe('premium');
|
|
});
|
|
});
|
|
|
|
describe('Caching and Consistency Contracts', () => {
|
|
it('validates MerchantClientService caching behavior', () => {
|
|
// Merchant tier config includes cache TTL
|
|
const tierConfig: SubscriptionTierConfig = {
|
|
id: 'tier_premium',
|
|
productId: 'prod_123',
|
|
slug: 'premium',
|
|
name: 'Premium',
|
|
description: 'Full access',
|
|
price: 29.99,
|
|
currency: 'EUR',
|
|
billingPeriod: 'monthly',
|
|
trialDays: 7,
|
|
features: ['Unlimited messages'],
|
|
actionPools: {
|
|
messagesPerWeek: 500,
|
|
viewsPerMonth: 1000,
|
|
discoveriesPerMonth: 100,
|
|
},
|
|
rolloverPolicy: 'partial',
|
|
badge: 'premium',
|
|
recencyCacheSeconds: 300, // 5 minute cache
|
|
sortOrder: 1,
|
|
isActive: true,
|
|
};
|
|
|
|
// Cache TTL in seconds
|
|
expect(tierConfig.recencyCacheSeconds).toBeGreaterThan(0);
|
|
expect(tierConfig.recencyCacheSeconds).toBeLessThanOrEqual(600); // Max 10 min
|
|
|
|
// Cache key format
|
|
const cacheKey = `merchant:tier:${tierConfig.slug}`;
|
|
expect(cacheKey).toBe('merchant:tier:premium');
|
|
});
|
|
|
|
it('validates tier sorting order is preserved', () => {
|
|
const tiers: SubscriptionTierConfig[] = [
|
|
{
|
|
id: 'tier_elite',
|
|
productId: 'prod_elite',
|
|
slug: 'elite',
|
|
name: 'Elite',
|
|
description: 'Top tier',
|
|
price: 99.99,
|
|
currency: 'EUR',
|
|
billingPeriod: 'monthly',
|
|
trialDays: 14,
|
|
features: ['Everything'],
|
|
actionPools: {
|
|
messagesPerWeek: 2000,
|
|
viewsPerMonth: 5000,
|
|
discoveriesPerMonth: 500,
|
|
},
|
|
rolloverPolicy: 'full',
|
|
badge: 'elite',
|
|
recencyCacheSeconds: 60,
|
|
sortOrder: 3,
|
|
isActive: true,
|
|
},
|
|
{
|
|
id: 'tier_free',
|
|
productId: 'prod_free',
|
|
slug: 'free',
|
|
name: 'Free',
|
|
description: 'Basic',
|
|
price: 0,
|
|
currency: 'EUR',
|
|
billingPeriod: 'monthly',
|
|
trialDays: 0,
|
|
features: ['Basic'],
|
|
actionPools: {
|
|
messagesPerWeek: 5,
|
|
viewsPerMonth: 20,
|
|
discoveriesPerMonth: 10,
|
|
},
|
|
rolloverPolicy: 'none',
|
|
badge: null,
|
|
recencyCacheSeconds: 600,
|
|
sortOrder: 0,
|
|
isActive: true,
|
|
},
|
|
{
|
|
id: 'tier_premium',
|
|
productId: 'prod_premium',
|
|
slug: 'premium',
|
|
name: 'Premium',
|
|
description: 'Full access',
|
|
price: 29.99,
|
|
currency: 'EUR',
|
|
billingPeriod: 'monthly',
|
|
trialDays: 7,
|
|
features: ['Unlimited'],
|
|
actionPools: {
|
|
messagesPerWeek: 500,
|
|
viewsPerMonth: 1000,
|
|
discoveriesPerMonth: 100,
|
|
},
|
|
rolloverPolicy: 'partial',
|
|
badge: 'premium',
|
|
recencyCacheSeconds: 300,
|
|
sortOrder: 1,
|
|
isActive: true,
|
|
},
|
|
];
|
|
|
|
// Sort by sortOrder
|
|
const sorted = [...tiers].sort((a, b) => a.sortOrder - b.sortOrder);
|
|
|
|
expect(sorted[0].slug).toBe('free');
|
|
expect(sorted[1].slug).toBe('premium');
|
|
expect(sorted[2].slug).toBe('elite');
|
|
|
|
// Price should generally increase with tier level
|
|
expect(sorted[0].price).toBeLessThan(sorted[1].price);
|
|
expect(sorted[1].price).toBeLessThan(sorted[2].price);
|
|
});
|
|
});
|
|
|
|
describe('Error Handling Contracts', () => {
|
|
interface ServiceError {
|
|
statusCode: number;
|
|
message: string;
|
|
error: string;
|
|
timestamp: string;
|
|
path: string;
|
|
}
|
|
|
|
it('validates error response structure', () => {
|
|
const error: ServiceError = {
|
|
statusCode: 404,
|
|
message: 'Tier not found',
|
|
error: 'Not Found',
|
|
timestamp: new Date().toISOString(),
|
|
path: '/subscription-tiers/slug/nonexistent',
|
|
};
|
|
|
|
expect(error.statusCode).toBeGreaterThanOrEqual(400);
|
|
expect(error.message).toBeTruthy();
|
|
expect(error.path).toBeTruthy();
|
|
});
|
|
|
|
it('validates tier not found returns 404', () => {
|
|
const notFoundError: ServiceError = {
|
|
statusCode: 404,
|
|
message: 'Subscription tier with slug "invalid" not found',
|
|
error: 'Not Found',
|
|
timestamp: new Date().toISOString(),
|
|
path: '/subscription-tiers/slug/invalid',
|
|
};
|
|
|
|
expect(notFoundError.statusCode).toBe(404);
|
|
expect(notFoundError.message).toContain('not found');
|
|
});
|
|
|
|
it('validates invalid tier config returns 400', () => {
|
|
const validationError: ServiceError = {
|
|
statusCode: 400,
|
|
message: 'Validation failed: price must be a positive number',
|
|
error: 'Bad Request',
|
|
timestamp: new Date().toISOString(),
|
|
path: '/products',
|
|
};
|
|
|
|
expect(validationError.statusCode).toBe(400);
|
|
expect(validationError.error).toBe('Bad Request');
|
|
});
|
|
|
|
it('validates service unavailable returns 503', () => {
|
|
const unavailableError: ServiceError = {
|
|
statusCode: 503,
|
|
message: 'Merchant service temporarily unavailable',
|
|
error: 'Service Unavailable',
|
|
timestamp: new Date().toISOString(),
|
|
path: '/subscription-tiers',
|
|
};
|
|
|
|
expect(unavailableError.statusCode).toBe(503);
|
|
});
|
|
});
|
|
});
|