mirror of
https://github.com/vernu/textbee.git
synced 2026-07-30 17:07:46 -04:00
/billing/current-subscription answers in two shapes. A subscriber gets the
Subscription document, which always carries a status because the schema
requires one. A user with no subscription gets a synthesised
{ plan, isActive, usage } with no status, no amount and no dates.
The page read that second shape as a subscription with missing fields, so
every free user was told their subscription status was "Unknown" and shown
two "N/A" billing dates. Absence of a status is not an unknown status.
lib/billing.ts interprets the payload instead of guessing at it. A paid plan
arriving without a status is still genuinely unknown and is still reported
that way, so the earlier fix for fabricated "Active" is preserved.
The redesign splits one flat card into plan identity and usage. The old
inner panels were bg-card with shadow-sm sitting on a bg-card parent, so the
nesting was invisible; they are bordered muted panels now. Free accounts
drop the status pill, the always-N/A dates and the portal link they have
nothing to manage with.
CTAs are Buttons rather than hand-rolled Links with background utilities,
and the upgrade target comes from position on the plan ladder, so Scale
subscribers are no longer sold a tier above Scale and bespoke plans are not
pushed onto the self-serve ladder at all. Added a "Compare all plans" link
to textbee.dev/pricing, matching the label the onboarding plan picker
already uses for the same destination.
Also corrected the Pro fixture from 1900 to 999 cents, a price we do not
charge, and gave the portal link noopener noreferrer.
Verified: build clean, 0 lint errors (21 warnings, unchanged), 153 unit
tests (from 121), 78 e2e (from 75). The three free-account guards and the
pricing link guard were each confirmed to fail against the pre-change
component before being trusted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
212 lines
5.8 KiB
TypeScript
212 lines
5.8 KiB
TypeScript
// Shared, framework-agnostic API fixtures used by BOTH the MSW node server
|
|
// (unit/component tests) and the Playwright route-interception layer (e2e).
|
|
// These mirror the response envelopes the real backend returns, as consumed
|
|
// across the dashboard components. Tests must never hit a live backend, so
|
|
// these are the single source of truth for mocked API data.
|
|
|
|
export const API_BASE_URL =
|
|
process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001/api/v1'
|
|
|
|
export const TEST_ACCESS_TOKEN = 'test-access-token'
|
|
|
|
export const mockUser = {
|
|
_id: 'user_test_1',
|
|
id: 'user_test_1',
|
|
name: 'Test User',
|
|
email: 'test.user@example.com',
|
|
phone: '+15551234567',
|
|
role: 'user',
|
|
avatar: null,
|
|
emailVerifiedAt: new Date('2026-01-01T00:00:00.000Z').toISOString(),
|
|
onboardingCompletedAt: new Date('2026-01-01T00:00:00.000Z').toISOString(),
|
|
}
|
|
|
|
export const mockStats = {
|
|
totalSentSMSCount: 12840,
|
|
totalReceivedSMSCount: 3120,
|
|
totalDeviceCount: 2,
|
|
totalApiKeyCount: 3,
|
|
}
|
|
|
|
export const mockSubscription = {
|
|
plan: {
|
|
name: 'Pro',
|
|
dailyLimit: 5000,
|
|
monthlyLimit: 100000,
|
|
bulkSendLimit: 1000,
|
|
deviceLimit: 5,
|
|
},
|
|
usage: {
|
|
dailyLimit: 5000,
|
|
monthlyLimit: 100000,
|
|
bulkSendLimit: 1000,
|
|
deviceLimit: 5,
|
|
processedSmsToday: 320,
|
|
processedSmsLastMonth: 18450,
|
|
dailyRemaining: 4680,
|
|
monthlyRemaining: 81550,
|
|
dailyUsagePercentage: 6.4,
|
|
monthlyUsagePercentage: 18.45,
|
|
},
|
|
status: 'active',
|
|
// Cents, and it must match the Pro price on the pricing page. This fixture
|
|
// previously said 1900, a price we do not charge.
|
|
amount: 999,
|
|
currency: 'usd',
|
|
recurringInterval: 'month',
|
|
subscriptionStartDate: new Date('2026-06-01T00:00:00.000Z').toISOString(),
|
|
currentPeriodEnd: new Date('2026-08-01T00:00:00.000Z').toISOString(),
|
|
customDailyLimit: null,
|
|
customMonthlyLimit: null,
|
|
customBulkSendLimit: null,
|
|
customDeviceLimit: null,
|
|
}
|
|
|
|
// What /billing/current-subscription actually returns for a user with no
|
|
// subscription record: billing.service.ts synthesises this from the free plan.
|
|
// There is deliberately no status, no amount and no dates, because there is no
|
|
// subscription for them to belong to.
|
|
export const mockFreeSubscription = {
|
|
plan: {
|
|
name: 'free',
|
|
dailyLimit: 50,
|
|
monthlyLimit: 300,
|
|
bulkSendLimit: 50,
|
|
deviceLimit: 1,
|
|
},
|
|
isActive: true,
|
|
usage: {
|
|
dailyLimit: 50,
|
|
monthlyLimit: 300,
|
|
bulkSendLimit: 50,
|
|
deviceLimit: 1,
|
|
processedSmsToday: 4,
|
|
processedSmsLastMonth: 40,
|
|
dailyRemaining: 46,
|
|
monthlyRemaining: 260,
|
|
dailyUsagePercentage: 8,
|
|
monthlyUsagePercentage: 13,
|
|
},
|
|
}
|
|
|
|
export const mockDevices = [
|
|
{
|
|
_id: 'device_1',
|
|
brand: 'Google',
|
|
model: 'Pixel 8',
|
|
enabled: true,
|
|
batteryLevel: 82,
|
|
appVersionCode: 17,
|
|
createdAt: new Date('2026-05-10T00:00:00.000Z').toISOString(),
|
|
},
|
|
{
|
|
_id: 'device_2',
|
|
brand: 'Samsung',
|
|
model: 'Galaxy S23',
|
|
enabled: false,
|
|
batteryLevel: 40,
|
|
appVersionCode: 16,
|
|
createdAt: new Date('2026-05-20T00:00:00.000Z').toISOString(),
|
|
},
|
|
]
|
|
|
|
export const mockApiKeys = [
|
|
{
|
|
_id: 'key_1',
|
|
apiKey: 'tb_live_abcd1234',
|
|
name: 'Production',
|
|
status: 'active',
|
|
lastUsedAt: new Date('2026-07-09T00:00:00.000Z').toISOString(),
|
|
createdAt: new Date('2026-05-01T00:00:00.000Z').toISOString(),
|
|
},
|
|
]
|
|
|
|
export const mockWebhooks = [
|
|
{
|
|
_id: 'wh_1',
|
|
deliveryUrl: 'https://example.com/webhooks/textbee',
|
|
events: ['message.received'],
|
|
isActive: true,
|
|
createdAt: new Date('2026-06-15T00:00:00.000Z').toISOString(),
|
|
},
|
|
]
|
|
|
|
export const mockWebhookNotifications = { data: [], total: 0 }
|
|
|
|
// The real endpoint populates `device` (select: _id brand model buildId
|
|
// enabled), so the fixtures carry it too: replying reads message.device._id,
|
|
// and without it the mocked path would not exercise what production does.
|
|
// Anchored to local midnight rather than "N hours ago": a message 2 hours old
|
|
// falls on the previous day when the suite runs just after midnight, which
|
|
// made the Today/Yesterday grouping assertions depend on the wall clock.
|
|
const startOfToday = () => {
|
|
const d = new Date()
|
|
d.setHours(0, 0, 0, 0)
|
|
return d.getTime()
|
|
}
|
|
// Now is always today and never in the future.
|
|
const todayIso = () => new Date().toISOString()
|
|
// An hour before local midnight is always yesterday.
|
|
const yesterdayIso = () => new Date(startOfToday() - 60 * 60 * 1000).toISOString()
|
|
|
|
export const mockMessages = {
|
|
data: [
|
|
{
|
|
_id: 'msg_1',
|
|
recipient: '+15557654321',
|
|
message: 'Hello from textbee',
|
|
status: 'sent',
|
|
type: 'sent',
|
|
device: { _id: 'device_1', brand: 'Google', model: 'Pixel 8' },
|
|
requestedAt: todayIso(),
|
|
createdAt: todayIso(),
|
|
},
|
|
{
|
|
_id: 'msg_2',
|
|
sender: '+15551234567',
|
|
message: 'Reply from a customer',
|
|
status: 'received',
|
|
type: 'received',
|
|
device: { _id: 'device_1', brand: 'Google', model: 'Pixel 8' },
|
|
receivedAt: yesterdayIso(),
|
|
createdAt: yesterdayIso(),
|
|
},
|
|
],
|
|
meta: { total: 2, page: 1, limit: 20, totalPages: 1 },
|
|
}
|
|
|
|
// Shaped like the real /billing/plans response, which returns Plan documents:
|
|
// monthlyPrice in cents plus the limits, and no currency or amount. The old
|
|
// fixture used amount/currency, fields that endpoint has never sent, so a
|
|
// component reading them looked correct in tests and rendered every paid plan
|
|
// as free in production.
|
|
export const mockBillingPlans = [
|
|
{
|
|
name: 'Free',
|
|
monthlyPrice: 0,
|
|
dailyLimit: 50,
|
|
monthlyLimit: 500,
|
|
bulkSendLimit: 50,
|
|
deviceLimit: 1,
|
|
isActive: true,
|
|
},
|
|
{
|
|
name: 'Pro',
|
|
monthlyPrice: 1900,
|
|
dailyLimit: 5000,
|
|
monthlyLimit: 100000,
|
|
bulkSendLimit: 500,
|
|
deviceLimit: 5,
|
|
isActive: true,
|
|
},
|
|
{
|
|
name: 'Scale',
|
|
monthlyPrice: 4900,
|
|
dailyLimit: -1,
|
|
monthlyLimit: -1,
|
|
bulkSendLimit: 2000,
|
|
deviceLimit: -1,
|
|
isActive: true,
|
|
},
|
|
]
|