@qwickapps/server 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +507 -0
- package/README.md +9 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/bans/bans-plugin.d.ts.map +1 -1
- package/dist/plugins/bans/bans-plugin.js +12 -3
- package/dist/plugins/bans/bans-plugin.js.map +1 -1
- package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts +11 -0
- package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts.map +1 -0
- package/dist/plugins/devices/__tests__/devices-plugin.test.js +410 -0
- package/dist/plugins/devices/__tests__/devices-plugin.test.js.map +1 -0
- package/dist/plugins/devices/__tests__/token-utils.test.d.ts +7 -0
- package/dist/plugins/devices/__tests__/token-utils.test.d.ts.map +1 -0
- package/dist/plugins/devices/__tests__/token-utils.test.js +197 -0
- package/dist/plugins/devices/__tests__/token-utils.test.js.map +1 -0
- package/dist/plugins/devices/adapters/compute-adapter.d.ts +36 -0
- package/dist/plugins/devices/adapters/compute-adapter.d.ts.map +1 -0
- package/dist/plugins/devices/adapters/compute-adapter.js +100 -0
- package/dist/plugins/devices/adapters/compute-adapter.js.map +1 -0
- package/dist/plugins/devices/adapters/index.d.ts +12 -0
- package/dist/plugins/devices/adapters/index.d.ts.map +1 -0
- package/dist/plugins/devices/adapters/index.js +10 -0
- package/dist/plugins/devices/adapters/index.js.map +1 -0
- package/dist/plugins/devices/adapters/mobile-adapter.d.ts +41 -0
- package/dist/plugins/devices/adapters/mobile-adapter.d.ts.map +1 -0
- package/dist/plugins/devices/adapters/mobile-adapter.js +131 -0
- package/dist/plugins/devices/adapters/mobile-adapter.js.map +1 -0
- package/dist/plugins/devices/devices-plugin.d.ts +70 -0
- package/dist/plugins/devices/devices-plugin.d.ts.map +1 -0
- package/dist/plugins/devices/devices-plugin.js +453 -0
- package/dist/plugins/devices/devices-plugin.js.map +1 -0
- package/dist/plugins/devices/index.d.ts +18 -0
- package/dist/plugins/devices/index.d.ts.map +1 -0
- package/dist/plugins/devices/index.js +18 -0
- package/dist/plugins/devices/index.js.map +1 -0
- package/dist/plugins/devices/stores/index.d.ts +9 -0
- package/dist/plugins/devices/stores/index.d.ts.map +1 -0
- package/dist/plugins/devices/stores/index.js +9 -0
- package/dist/plugins/devices/stores/index.js.map +1 -0
- package/dist/plugins/devices/stores/postgres-store.d.ts +26 -0
- package/dist/plugins/devices/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/devices/stores/postgres-store.js +199 -0
- package/dist/plugins/devices/stores/postgres-store.js.map +1 -0
- package/dist/plugins/devices/token-utils.d.ts +100 -0
- package/dist/plugins/devices/token-utils.d.ts.map +1 -0
- package/dist/plugins/devices/token-utils.js +162 -0
- package/dist/plugins/devices/token-utils.js.map +1 -0
- package/dist/plugins/devices/types.d.ts +307 -0
- package/dist/plugins/devices/types.d.ts.map +1 -0
- package/dist/plugins/devices/types.js +10 -0
- package/dist/plugins/devices/types.js.map +1 -0
- package/dist/plugins/index.d.ts +14 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +13 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts +5 -0
- package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts.map +1 -0
- package/dist/plugins/notifications/__tests__/notifications-manager.test.js +470 -0
- package/dist/plugins/notifications/__tests__/notifications-manager.test.js.map +1 -0
- package/dist/plugins/notifications/index.d.ts +71 -0
- package/dist/plugins/notifications/index.d.ts.map +1 -0
- package/dist/plugins/notifications/index.js +72 -0
- package/dist/plugins/notifications/index.js.map +1 -0
- package/dist/plugins/notifications/notifications-manager.d.ts +182 -0
- package/dist/plugins/notifications/notifications-manager.d.ts.map +1 -0
- package/dist/plugins/notifications/notifications-manager.js +610 -0
- package/dist/plugins/notifications/notifications-manager.js.map +1 -0
- package/dist/plugins/notifications/notifications-plugin.d.ts +83 -0
- package/dist/plugins/notifications/notifications-plugin.d.ts.map +1 -0
- package/dist/plugins/notifications/notifications-plugin.js +337 -0
- package/dist/plugins/notifications/notifications-plugin.js.map +1 -0
- package/dist/plugins/notifications/types.d.ts +164 -0
- package/dist/plugins/notifications/types.d.ts.map +1 -0
- package/dist/plugins/notifications/types.js +9 -0
- package/dist/plugins/notifications/types.js.map +1 -0
- package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts +12 -0
- package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts.map +1 -0
- package/dist/plugins/parental/__tests__/parental-plugin.test.js +349 -0
- package/dist/plugins/parental/__tests__/parental-plugin.test.js.map +1 -0
- package/dist/plugins/parental/adapters/index.d.ts +8 -0
- package/dist/plugins/parental/adapters/index.d.ts.map +1 -0
- package/dist/plugins/parental/adapters/index.js +7 -0
- package/dist/plugins/parental/adapters/index.js.map +1 -0
- package/dist/plugins/parental/adapters/kids-adapter.d.ts +24 -0
- package/dist/plugins/parental/adapters/kids-adapter.d.ts.map +1 -0
- package/dist/plugins/parental/adapters/kids-adapter.js +174 -0
- package/dist/plugins/parental/adapters/kids-adapter.js.map +1 -0
- package/dist/plugins/parental/index.d.ts +14 -0
- package/dist/plugins/parental/index.d.ts.map +1 -0
- package/dist/plugins/parental/index.js +15 -0
- package/dist/plugins/parental/index.js.map +1 -0
- package/dist/plugins/parental/parental-plugin.d.ts +88 -0
- package/dist/plugins/parental/parental-plugin.d.ts.map +1 -0
- package/dist/plugins/parental/parental-plugin.js +666 -0
- package/dist/plugins/parental/parental-plugin.js.map +1 -0
- package/dist/plugins/parental/stores/index.d.ts +7 -0
- package/dist/plugins/parental/stores/index.d.ts.map +1 -0
- package/dist/plugins/parental/stores/index.js +7 -0
- package/dist/plugins/parental/stores/index.js.map +1 -0
- package/dist/plugins/parental/stores/postgres-store.d.ts +10 -0
- package/dist/plugins/parental/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/parental/stores/postgres-store.js +209 -0
- package/dist/plugins/parental/stores/postgres-store.js.map +1 -0
- package/dist/plugins/parental/types.d.ts +154 -0
- package/dist/plugins/parental/types.d.ts.map +1 -0
- package/dist/plugins/parental/types.js +10 -0
- package/dist/plugins/parental/types.js.map +1 -0
- package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts +11 -0
- package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts.map +1 -0
- package/dist/plugins/profiles/__tests__/profiles-plugin.test.js +243 -0
- package/dist/plugins/profiles/__tests__/profiles-plugin.test.js.map +1 -0
- package/dist/plugins/profiles/index.d.ts +12 -0
- package/dist/plugins/profiles/index.d.ts.map +1 -0
- package/dist/plugins/profiles/index.js +13 -0
- package/dist/plugins/profiles/index.js.map +1 -0
- package/dist/plugins/profiles/profiles-plugin.d.ts +71 -0
- package/dist/plugins/profiles/profiles-plugin.d.ts.map +1 -0
- package/dist/plugins/profiles/profiles-plugin.js +481 -0
- package/dist/plugins/profiles/profiles-plugin.js.map +1 -0
- package/dist/plugins/profiles/stores/index.d.ts +9 -0
- package/dist/plugins/profiles/stores/index.d.ts.map +1 -0
- package/dist/plugins/profiles/stores/index.js +9 -0
- package/dist/plugins/profiles/stores/index.js.map +1 -0
- package/dist/plugins/profiles/stores/postgres-store.d.ts +18 -0
- package/dist/plugins/profiles/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/profiles/stores/postgres-store.js +310 -0
- package/dist/plugins/profiles/stores/postgres-store.js.map +1 -0
- package/dist/plugins/profiles/types.d.ts +289 -0
- package/dist/plugins/profiles/types.d.ts.map +1 -0
- package/dist/plugins/profiles/types.js +10 -0
- package/dist/plugins/profiles/types.js.map +1 -0
- package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts +11 -0
- package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts.map +1 -0
- package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js +305 -0
- package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js.map +1 -0
- package/dist/plugins/subscriptions/index.d.ts +12 -0
- package/dist/plugins/subscriptions/index.d.ts.map +1 -0
- package/dist/plugins/subscriptions/index.js +13 -0
- package/dist/plugins/subscriptions/index.js.map +1 -0
- package/dist/plugins/subscriptions/stores/index.d.ts +9 -0
- package/dist/plugins/subscriptions/stores/index.d.ts.map +1 -0
- package/dist/plugins/subscriptions/stores/index.js +9 -0
- package/dist/plugins/subscriptions/stores/index.js.map +1 -0
- package/dist/plugins/subscriptions/stores/postgres-store.d.ts +14 -0
- package/dist/plugins/subscriptions/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/subscriptions/stores/postgres-store.js +359 -0
- package/dist/plugins/subscriptions/stores/postgres-store.js.map +1 -0
- package/dist/plugins/subscriptions/subscriptions-plugin.d.ts +82 -0
- package/dist/plugins/subscriptions/subscriptions-plugin.d.ts.map +1 -0
- package/dist/plugins/subscriptions/subscriptions-plugin.js +449 -0
- package/dist/plugins/subscriptions/subscriptions-plugin.js.map +1 -0
- package/dist/plugins/subscriptions/types.d.ts +308 -0
- package/dist/plugins/subscriptions/types.d.ts.map +1 -0
- package/dist/plugins/subscriptions/types.js +10 -0
- package/dist/plugins/subscriptions/types.js.map +1 -0
- package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts +11 -0
- package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts.map +1 -0
- package/dist/plugins/usage/__tests__/usage-plugin.test.js +218 -0
- package/dist/plugins/usage/__tests__/usage-plugin.test.js.map +1 -0
- package/dist/plugins/usage/index.d.ts +12 -0
- package/dist/plugins/usage/index.d.ts.map +1 -0
- package/dist/plugins/usage/index.js +13 -0
- package/dist/plugins/usage/index.js.map +1 -0
- package/dist/plugins/usage/stores/index.d.ts +9 -0
- package/dist/plugins/usage/stores/index.d.ts.map +1 -0
- package/dist/plugins/usage/stores/index.js +9 -0
- package/dist/plugins/usage/stores/index.js.map +1 -0
- package/dist/plugins/usage/stores/postgres-store.d.ts +14 -0
- package/dist/plugins/usage/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/usage/stores/postgres-store.js +146 -0
- package/dist/plugins/usage/stores/postgres-store.js.map +1 -0
- package/dist/plugins/usage/types.d.ts +195 -0
- package/dist/plugins/usage/types.d.ts.map +1 -0
- package/dist/plugins/usage/types.js +10 -0
- package/dist/plugins/usage/types.js.map +1 -0
- package/dist/plugins/usage/usage-plugin.d.ts +51 -0
- package/dist/plugins/usage/usage-plugin.d.ts.map +1 -0
- package/dist/plugins/usage/usage-plugin.js +412 -0
- package/dist/plugins/usage/usage-plugin.js.map +1 -0
- package/dist/plugins/users/__tests__/postgres-store.test.d.ts +10 -0
- package/dist/plugins/users/__tests__/postgres-store.test.d.ts.map +1 -0
- package/dist/plugins/users/__tests__/postgres-store.test.js +229 -0
- package/dist/plugins/users/__tests__/postgres-store.test.js.map +1 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js +3 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -1
- package/dist/plugins/users/index.d.ts +2 -2
- package/dist/plugins/users/index.d.ts.map +1 -1
- package/dist/plugins/users/index.js +1 -1
- package/dist/plugins/users/index.js.map +1 -1
- package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -1
- package/dist/plugins/users/stores/postgres-store.js +76 -0
- package/dist/plugins/users/stores/postgres-store.js.map +1 -1
- package/dist/plugins/users/types.d.ts +74 -6
- package/dist/plugins/users/types.d.ts.map +1 -1
- package/dist/plugins/users/users-plugin.d.ts +15 -1
- package/dist/plugins/users/users-plugin.d.ts.map +1 -1
- package/dist/plugins/users/users-plugin.js +29 -0
- package/dist/plugins/users/users-plugin.js.map +1 -1
- package/dist-ui/assets/index-CynOqPkb.js +469 -0
- package/dist-ui/assets/index-CynOqPkb.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +46 -0
- package/dist-ui-lib/components/StatCard.d.ts +16 -0
- package/dist-ui-lib/dashboard/widgets/NotificationsStatsWidget.d.ts +12 -0
- package/dist-ui-lib/dashboard/widgets/index.d.ts +1 -0
- package/dist-ui-lib/index.js +1822 -1611
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/pages/NotificationsPage.d.ts +9 -0
- package/dist-ui-lib/utils/formatters.d.ts +19 -0
- package/package.json +3 -2
- package/src/index.ts +178 -0
- package/src/plugins/bans/bans-plugin.ts +15 -3
- package/src/plugins/devices/__tests__/devices-plugin.test.ts +551 -0
- package/src/plugins/devices/__tests__/token-utils.test.ts +264 -0
- package/src/plugins/devices/adapters/compute-adapter.ts +139 -0
- package/src/plugins/devices/adapters/index.ts +13 -0
- package/src/plugins/devices/adapters/mobile-adapter.ts +179 -0
- package/src/plugins/devices/devices-plugin.ts +538 -0
- package/src/plugins/devices/index.ts +69 -0
- package/src/plugins/devices/stores/index.ts +9 -0
- package/src/plugins/devices/stores/postgres-store.ts +304 -0
- package/src/plugins/devices/token-utils.ts +213 -0
- package/src/plugins/devices/types.ts +351 -0
- package/src/plugins/index.ts +218 -0
- package/src/plugins/notifications/__tests__/notifications-manager.test.ts +637 -0
- package/src/plugins/notifications/index.ts +91 -0
- package/src/plugins/notifications/notifications-manager.ts +773 -0
- package/src/plugins/notifications/notifications-plugin.ts +398 -0
- package/src/plugins/notifications/types.ts +207 -0
- package/src/plugins/parental/__tests__/parental-plugin.test.ts +465 -0
- package/src/plugins/parental/adapters/index.ts +8 -0
- package/src/plugins/parental/adapters/kids-adapter.ts +206 -0
- package/src/plugins/parental/index.ts +55 -0
- package/src/plugins/parental/parental-plugin.ts +759 -0
- package/src/plugins/parental/stores/index.ts +7 -0
- package/src/plugins/parental/stores/postgres-store.ts +304 -0
- package/src/plugins/parental/types.ts +180 -0
- package/src/plugins/profiles/__tests__/profiles-plugin.test.ts +321 -0
- package/src/plugins/profiles/index.ts +49 -0
- package/src/plugins/profiles/profiles-plugin.ts +546 -0
- package/src/plugins/profiles/stores/index.ts +9 -0
- package/src/plugins/profiles/stores/postgres-store.ts +439 -0
- package/src/plugins/profiles/types.ts +338 -0
- package/src/plugins/subscriptions/__tests__/subscriptions-plugin.test.ts +404 -0
- package/src/plugins/subscriptions/index.ts +51 -0
- package/src/plugins/subscriptions/stores/index.ts +9 -0
- package/src/plugins/subscriptions/stores/postgres-store.ts +482 -0
- package/src/plugins/subscriptions/subscriptions-plugin.ts +530 -0
- package/src/plugins/subscriptions/types.ts +355 -0
- package/src/plugins/usage/__tests__/usage-plugin.test.ts +288 -0
- package/src/plugins/usage/index.ts +39 -0
- package/src/plugins/usage/stores/index.ts +9 -0
- package/src/plugins/usage/stores/postgres-store.ts +213 -0
- package/src/plugins/usage/types.ts +222 -0
- package/src/plugins/usage/usage-plugin.ts +484 -0
- package/src/plugins/users/__tests__/postgres-store.test.ts +326 -0
- package/src/plugins/users/__tests__/users-plugin.test.ts +3 -0
- package/src/plugins/users/index.ts +6 -0
- package/src/plugins/users/stores/postgres-store.ts +104 -0
- package/src/plugins/users/types.ts +82 -6
- package/src/plugins/users/users-plugin.ts +37 -0
- package/ui/src/App.tsx +5 -1
- package/ui/src/api/controlPanelApi.ts +103 -6
- package/ui/src/components/StatCard.tsx +58 -0
- package/ui/src/dashboard/builtInWidgets.tsx +3 -1
- package/ui/src/dashboard/widgets/NotificationsStatsWidget.tsx +167 -0
- package/ui/src/dashboard/widgets/index.ts +1 -0
- package/ui/src/pages/NotificationsPage.tsx +417 -0
- package/ui/src/utils/formatters.ts +33 -0
- package/dist-ui/assets/index-D7DoZ9rL.js +0 -478
- package/dist-ui/assets/index-D7DoZ9rL.js.map +0 -1
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Subscriptions Store
|
|
3
|
+
*
|
|
4
|
+
* Subscriptions storage implementation using PostgreSQL.
|
|
5
|
+
* Manages tiers, entitlements, and user subscriptions.
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
SubscriptionsStore,
|
|
12
|
+
SubscriptionTier,
|
|
13
|
+
SubscriptionEntitlement,
|
|
14
|
+
UserSubscription,
|
|
15
|
+
UserSubscriptionWithTier,
|
|
16
|
+
CreateTierInput,
|
|
17
|
+
UpdateTierInput,
|
|
18
|
+
CreateEntitlementInput,
|
|
19
|
+
CreateUserSubscriptionInput,
|
|
20
|
+
UpdateUserSubscriptionInput,
|
|
21
|
+
PostgresSubscriptionsStoreConfig,
|
|
22
|
+
} from '../types.js';
|
|
23
|
+
|
|
24
|
+
// Pool interface (from pg package)
|
|
25
|
+
interface PgPool {
|
|
26
|
+
query(text: string, values?: unknown[]): Promise<{ rows: unknown[]; rowCount: number | null }>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a PostgreSQL subscriptions store
|
|
31
|
+
*/
|
|
32
|
+
export function postgresSubscriptionsStore(config: PostgresSubscriptionsStoreConfig): SubscriptionsStore {
|
|
33
|
+
const {
|
|
34
|
+
pool: poolOrFn,
|
|
35
|
+
tiersTable = 'subscription_tiers',
|
|
36
|
+
entitlementsTable = 'subscription_entitlements',
|
|
37
|
+
userSubscriptionsTable = 'user_subscriptions',
|
|
38
|
+
schema = 'public',
|
|
39
|
+
autoCreateTables = true,
|
|
40
|
+
} = config;
|
|
41
|
+
|
|
42
|
+
// Helper to get pool (supports lazy initialization via function)
|
|
43
|
+
const getPool = (): PgPool => {
|
|
44
|
+
const pool = typeof poolOrFn === 'function' ? poolOrFn() : poolOrFn;
|
|
45
|
+
return pool as PgPool;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const tiersTableFull = `"${schema}"."${tiersTable}"`;
|
|
49
|
+
const entitlementsTableFull = `"${schema}"."${entitlementsTable}"`;
|
|
50
|
+
const userSubsTableFull = `"${schema}"."${userSubscriptionsTable}"`;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
name: 'postgres',
|
|
54
|
+
|
|
55
|
+
async initialize(): Promise<void> {
|
|
56
|
+
if (!autoCreateTables) return;
|
|
57
|
+
|
|
58
|
+
// Create subscription_tiers table
|
|
59
|
+
await getPool().query(`
|
|
60
|
+
CREATE TABLE IF NOT EXISTS ${tiersTableFull} (
|
|
61
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
62
|
+
slug VARCHAR(50) UNIQUE NOT NULL,
|
|
63
|
+
name VARCHAR(100) NOT NULL,
|
|
64
|
+
description TEXT,
|
|
65
|
+
price_monthly_cents INTEGER,
|
|
66
|
+
price_yearly_cents INTEGER,
|
|
67
|
+
stripe_price_id_monthly VARCHAR(100),
|
|
68
|
+
stripe_price_id_yearly VARCHAR(100),
|
|
69
|
+
is_active BOOLEAN DEFAULT true,
|
|
70
|
+
sort_order INTEGER DEFAULT 0,
|
|
71
|
+
metadata JSONB DEFAULT '{}',
|
|
72
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
73
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_${tiersTable}_slug ON ${tiersTableFull}(slug);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_${tiersTable}_active ON ${tiersTableFull}(is_active);
|
|
78
|
+
`);
|
|
79
|
+
|
|
80
|
+
// Create subscription_entitlements table
|
|
81
|
+
await getPool().query(`
|
|
82
|
+
CREATE TABLE IF NOT EXISTS ${entitlementsTableFull} (
|
|
83
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
84
|
+
tier_id UUID NOT NULL REFERENCES ${tiersTableFull}(id) ON DELETE CASCADE,
|
|
85
|
+
feature_code VARCHAR(100) NOT NULL,
|
|
86
|
+
limit_value INTEGER,
|
|
87
|
+
metadata JSONB DEFAULT '{}',
|
|
88
|
+
UNIQUE(tier_id, feature_code)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_${entitlementsTable}_tier ON ${entitlementsTableFull}(tier_id);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_${entitlementsTable}_feature ON ${entitlementsTableFull}(feature_code);
|
|
93
|
+
`);
|
|
94
|
+
|
|
95
|
+
// Create user_subscriptions table
|
|
96
|
+
await getPool().query(`
|
|
97
|
+
CREATE TABLE IF NOT EXISTS ${userSubsTableFull} (
|
|
98
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
99
|
+
user_id UUID NOT NULL,
|
|
100
|
+
tier_id UUID NOT NULL REFERENCES ${tiersTableFull}(id),
|
|
101
|
+
stripe_customer_id VARCHAR(100),
|
|
102
|
+
stripe_subscription_id VARCHAR(100),
|
|
103
|
+
status VARCHAR(20) DEFAULT 'active'
|
|
104
|
+
CHECK (status IN ('active', 'canceled', 'past_due', 'trialing', 'inactive')),
|
|
105
|
+
current_period_start TIMESTAMPTZ,
|
|
106
|
+
current_period_end TIMESTAMPTZ,
|
|
107
|
+
cancel_at_period_end BOOLEAN DEFAULT false,
|
|
108
|
+
metadata JSONB DEFAULT '{}',
|
|
109
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
110
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_${userSubscriptionsTable}_user ON ${userSubsTableFull}(user_id);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_${userSubscriptionsTable}_stripe ON ${userSubsTableFull}(stripe_customer_id);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_${userSubscriptionsTable}_stripe_sub ON ${userSubsTableFull}(stripe_subscription_id);
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_${userSubscriptionsTable}_status ON ${userSubsTableFull}(status);
|
|
117
|
+
`);
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
121
|
+
// Tiers
|
|
122
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
123
|
+
|
|
124
|
+
async createTier(input: CreateTierInput): Promise<SubscriptionTier> {
|
|
125
|
+
const result = await getPool().query(
|
|
126
|
+
`INSERT INTO ${tiersTableFull}
|
|
127
|
+
(slug, name, description, price_monthly_cents, price_yearly_cents,
|
|
128
|
+
stripe_price_id_monthly, stripe_price_id_yearly, is_active, sort_order, metadata)
|
|
129
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
130
|
+
RETURNING *`,
|
|
131
|
+
[
|
|
132
|
+
input.slug,
|
|
133
|
+
input.name,
|
|
134
|
+
input.description || null,
|
|
135
|
+
input.price_monthly_cents || null,
|
|
136
|
+
input.price_yearly_cents || null,
|
|
137
|
+
input.stripe_price_id_monthly || null,
|
|
138
|
+
input.stripe_price_id_yearly || null,
|
|
139
|
+
input.is_active !== false,
|
|
140
|
+
input.sort_order || 0,
|
|
141
|
+
JSON.stringify(input.metadata || {}),
|
|
142
|
+
]
|
|
143
|
+
);
|
|
144
|
+
return result.rows[0] as SubscriptionTier;
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
async getTierById(id: string): Promise<SubscriptionTier | null> {
|
|
148
|
+
const result = await getPool().query(
|
|
149
|
+
`SELECT * FROM ${tiersTableFull} WHERE id = $1`,
|
|
150
|
+
[id]
|
|
151
|
+
);
|
|
152
|
+
return (result.rows[0] as SubscriptionTier) || null;
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
async getTierBySlug(slug: string): Promise<SubscriptionTier | null> {
|
|
156
|
+
const result = await getPool().query(
|
|
157
|
+
`SELECT * FROM ${tiersTableFull} WHERE slug = $1`,
|
|
158
|
+
[slug]
|
|
159
|
+
);
|
|
160
|
+
return (result.rows[0] as SubscriptionTier) || null;
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
async listTiers(activeOnly = true): Promise<SubscriptionTier[]> {
|
|
164
|
+
let query = `SELECT * FROM ${tiersTableFull}`;
|
|
165
|
+
if (activeOnly) {
|
|
166
|
+
query += ` WHERE is_active = true`;
|
|
167
|
+
}
|
|
168
|
+
query += ` ORDER BY sort_order ASC, name ASC`;
|
|
169
|
+
|
|
170
|
+
const result = await getPool().query(query);
|
|
171
|
+
return result.rows as SubscriptionTier[];
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
async updateTier(id: string, input: UpdateTierInput): Promise<SubscriptionTier | null> {
|
|
175
|
+
const updates: string[] = [];
|
|
176
|
+
const values: unknown[] = [];
|
|
177
|
+
let paramIndex = 1;
|
|
178
|
+
|
|
179
|
+
if (input.name !== undefined) {
|
|
180
|
+
updates.push(`name = $${paramIndex++}`);
|
|
181
|
+
values.push(input.name);
|
|
182
|
+
}
|
|
183
|
+
if (input.description !== undefined) {
|
|
184
|
+
updates.push(`description = $${paramIndex++}`);
|
|
185
|
+
values.push(input.description);
|
|
186
|
+
}
|
|
187
|
+
if (input.price_monthly_cents !== undefined) {
|
|
188
|
+
updates.push(`price_monthly_cents = $${paramIndex++}`);
|
|
189
|
+
values.push(input.price_monthly_cents);
|
|
190
|
+
}
|
|
191
|
+
if (input.price_yearly_cents !== undefined) {
|
|
192
|
+
updates.push(`price_yearly_cents = $${paramIndex++}`);
|
|
193
|
+
values.push(input.price_yearly_cents);
|
|
194
|
+
}
|
|
195
|
+
if (input.stripe_price_id_monthly !== undefined) {
|
|
196
|
+
updates.push(`stripe_price_id_monthly = $${paramIndex++}`);
|
|
197
|
+
values.push(input.stripe_price_id_monthly);
|
|
198
|
+
}
|
|
199
|
+
if (input.stripe_price_id_yearly !== undefined) {
|
|
200
|
+
updates.push(`stripe_price_id_yearly = $${paramIndex++}`);
|
|
201
|
+
values.push(input.stripe_price_id_yearly);
|
|
202
|
+
}
|
|
203
|
+
if (input.is_active !== undefined) {
|
|
204
|
+
updates.push(`is_active = $${paramIndex++}`);
|
|
205
|
+
values.push(input.is_active);
|
|
206
|
+
}
|
|
207
|
+
if (input.sort_order !== undefined) {
|
|
208
|
+
updates.push(`sort_order = $${paramIndex++}`);
|
|
209
|
+
values.push(input.sort_order);
|
|
210
|
+
}
|
|
211
|
+
if (input.metadata !== undefined) {
|
|
212
|
+
updates.push(`metadata = $${paramIndex++}`);
|
|
213
|
+
values.push(JSON.stringify(input.metadata));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (updates.length === 0) {
|
|
217
|
+
return this.getTierById(id);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
updates.push(`updated_at = NOW()`);
|
|
221
|
+
values.push(id);
|
|
222
|
+
|
|
223
|
+
const result = await getPool().query(
|
|
224
|
+
`UPDATE ${tiersTableFull} SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
|
|
225
|
+
values
|
|
226
|
+
);
|
|
227
|
+
return (result.rows[0] as SubscriptionTier) || null;
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
async deleteTier(id: string): Promise<boolean> {
|
|
231
|
+
// Soft delete by setting is_active = false
|
|
232
|
+
const result = await getPool().query(
|
|
233
|
+
`UPDATE ${tiersTableFull} SET is_active = false, updated_at = NOW() WHERE id = $1`,
|
|
234
|
+
[id]
|
|
235
|
+
);
|
|
236
|
+
return (result.rowCount ?? 0) > 0;
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
240
|
+
// Entitlements
|
|
241
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
242
|
+
|
|
243
|
+
async createEntitlement(input: CreateEntitlementInput): Promise<SubscriptionEntitlement> {
|
|
244
|
+
const result = await getPool().query(
|
|
245
|
+
`INSERT INTO ${entitlementsTableFull} (tier_id, feature_code, limit_value, metadata)
|
|
246
|
+
VALUES ($1, $2, $3, $4)
|
|
247
|
+
ON CONFLICT (tier_id, feature_code) DO UPDATE SET
|
|
248
|
+
limit_value = EXCLUDED.limit_value,
|
|
249
|
+
metadata = EXCLUDED.metadata
|
|
250
|
+
RETURNING *`,
|
|
251
|
+
[
|
|
252
|
+
input.tier_id,
|
|
253
|
+
input.feature_code,
|
|
254
|
+
input.limit_value ?? null,
|
|
255
|
+
JSON.stringify(input.metadata || {}),
|
|
256
|
+
]
|
|
257
|
+
);
|
|
258
|
+
return result.rows[0] as SubscriptionEntitlement;
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
async getEntitlementsByTier(tierId: string): Promise<SubscriptionEntitlement[]> {
|
|
262
|
+
const result = await getPool().query(
|
|
263
|
+
`SELECT * FROM ${entitlementsTableFull} WHERE tier_id = $1 ORDER BY feature_code`,
|
|
264
|
+
[tierId]
|
|
265
|
+
);
|
|
266
|
+
return result.rows as SubscriptionEntitlement[];
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
async updateEntitlement(id: string, limitValue: number | null): Promise<SubscriptionEntitlement | null> {
|
|
270
|
+
const result = await getPool().query(
|
|
271
|
+
`UPDATE ${entitlementsTableFull} SET limit_value = $1 WHERE id = $2 RETURNING *`,
|
|
272
|
+
[limitValue, id]
|
|
273
|
+
);
|
|
274
|
+
return (result.rows[0] as SubscriptionEntitlement) || null;
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
async deleteEntitlement(id: string): Promise<boolean> {
|
|
278
|
+
const result = await getPool().query(
|
|
279
|
+
`DELETE FROM ${entitlementsTableFull} WHERE id = $1`,
|
|
280
|
+
[id]
|
|
281
|
+
);
|
|
282
|
+
return (result.rowCount ?? 0) > 0;
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
async setTierEntitlements(
|
|
286
|
+
tierId: string,
|
|
287
|
+
entitlements: Array<{ feature_code: string; limit_value?: number }>
|
|
288
|
+
): Promise<void> {
|
|
289
|
+
// Delete existing entitlements
|
|
290
|
+
await getPool().query(
|
|
291
|
+
`DELETE FROM ${entitlementsTableFull} WHERE tier_id = $1`,
|
|
292
|
+
[tierId]
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Insert new entitlements
|
|
296
|
+
for (const ent of entitlements) {
|
|
297
|
+
await this.createEntitlement({
|
|
298
|
+
tier_id: tierId,
|
|
299
|
+
feature_code: ent.feature_code,
|
|
300
|
+
limit_value: ent.limit_value,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
306
|
+
// User Subscriptions
|
|
307
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
308
|
+
|
|
309
|
+
async createUserSubscription(input: CreateUserSubscriptionInput): Promise<UserSubscription> {
|
|
310
|
+
const result = await getPool().query(
|
|
311
|
+
`INSERT INTO ${userSubsTableFull}
|
|
312
|
+
(user_id, tier_id, stripe_customer_id, stripe_subscription_id, status,
|
|
313
|
+
current_period_start, current_period_end, metadata)
|
|
314
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
315
|
+
RETURNING *`,
|
|
316
|
+
[
|
|
317
|
+
input.user_id,
|
|
318
|
+
input.tier_id,
|
|
319
|
+
input.stripe_customer_id || null,
|
|
320
|
+
input.stripe_subscription_id || null,
|
|
321
|
+
input.status || 'active',
|
|
322
|
+
input.current_period_start || null,
|
|
323
|
+
input.current_period_end || null,
|
|
324
|
+
JSON.stringify(input.metadata || {}),
|
|
325
|
+
]
|
|
326
|
+
);
|
|
327
|
+
return result.rows[0] as UserSubscription;
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
async getUserSubscriptionById(id: string): Promise<UserSubscription | null> {
|
|
331
|
+
const result = await getPool().query(
|
|
332
|
+
`SELECT * FROM ${userSubsTableFull} WHERE id = $1`,
|
|
333
|
+
[id]
|
|
334
|
+
);
|
|
335
|
+
return (result.rows[0] as UserSubscription) || null;
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
async getActiveSubscription(userId: string): Promise<UserSubscriptionWithTier | null> {
|
|
339
|
+
const result = await getPool().query(
|
|
340
|
+
`SELECT us.*, t.slug as tier_slug, t.name as tier_name, t.description as tier_description,
|
|
341
|
+
t.price_monthly_cents, t.price_yearly_cents, t.metadata as tier_metadata
|
|
342
|
+
FROM ${userSubsTableFull} us
|
|
343
|
+
JOIN ${tiersTableFull} t ON us.tier_id = t.id
|
|
344
|
+
WHERE us.user_id = $1 AND us.status = 'active'
|
|
345
|
+
ORDER BY us.created_at DESC
|
|
346
|
+
LIMIT 1`,
|
|
347
|
+
[userId]
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
if (result.rows.length === 0) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const row = result.rows[0] as Record<string, unknown>;
|
|
355
|
+
return {
|
|
356
|
+
id: row.id as string,
|
|
357
|
+
user_id: row.user_id as string,
|
|
358
|
+
tier_id: row.tier_id as string,
|
|
359
|
+
stripe_customer_id: row.stripe_customer_id as string | undefined,
|
|
360
|
+
stripe_subscription_id: row.stripe_subscription_id as string | undefined,
|
|
361
|
+
status: row.status as UserSubscription['status'],
|
|
362
|
+
current_period_start: row.current_period_start as Date | undefined,
|
|
363
|
+
current_period_end: row.current_period_end as Date | undefined,
|
|
364
|
+
cancel_at_period_end: row.cancel_at_period_end as boolean,
|
|
365
|
+
metadata: row.metadata as Record<string, unknown>,
|
|
366
|
+
created_at: row.created_at as Date,
|
|
367
|
+
updated_at: row.updated_at as Date,
|
|
368
|
+
tier: {
|
|
369
|
+
id: row.tier_id as string,
|
|
370
|
+
slug: row.tier_slug as string,
|
|
371
|
+
name: row.tier_name as string,
|
|
372
|
+
description: row.tier_description as string | undefined,
|
|
373
|
+
price_monthly_cents: row.price_monthly_cents as number | undefined,
|
|
374
|
+
price_yearly_cents: row.price_yearly_cents as number | undefined,
|
|
375
|
+
is_active: true,
|
|
376
|
+
sort_order: 0,
|
|
377
|
+
metadata: row.tier_metadata as Record<string, unknown>,
|
|
378
|
+
created_at: row.created_at as Date,
|
|
379
|
+
updated_at: row.updated_at as Date,
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
async getByStripeSubscriptionId(stripeSubId: string): Promise<UserSubscription | null> {
|
|
385
|
+
const result = await getPool().query(
|
|
386
|
+
`SELECT * FROM ${userSubsTableFull} WHERE stripe_subscription_id = $1`,
|
|
387
|
+
[stripeSubId]
|
|
388
|
+
);
|
|
389
|
+
return (result.rows[0] as UserSubscription) || null;
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
async updateUserSubscription(id: string, input: UpdateUserSubscriptionInput): Promise<UserSubscription | null> {
|
|
393
|
+
const updates: string[] = [];
|
|
394
|
+
const values: unknown[] = [];
|
|
395
|
+
let paramIndex = 1;
|
|
396
|
+
|
|
397
|
+
if (input.tier_id !== undefined) {
|
|
398
|
+
updates.push(`tier_id = $${paramIndex++}`);
|
|
399
|
+
values.push(input.tier_id);
|
|
400
|
+
}
|
|
401
|
+
if (input.stripe_customer_id !== undefined) {
|
|
402
|
+
updates.push(`stripe_customer_id = $${paramIndex++}`);
|
|
403
|
+
values.push(input.stripe_customer_id);
|
|
404
|
+
}
|
|
405
|
+
if (input.stripe_subscription_id !== undefined) {
|
|
406
|
+
updates.push(`stripe_subscription_id = $${paramIndex++}`);
|
|
407
|
+
values.push(input.stripe_subscription_id);
|
|
408
|
+
}
|
|
409
|
+
if (input.status !== undefined) {
|
|
410
|
+
updates.push(`status = $${paramIndex++}`);
|
|
411
|
+
values.push(input.status);
|
|
412
|
+
}
|
|
413
|
+
if (input.current_period_start !== undefined) {
|
|
414
|
+
updates.push(`current_period_start = $${paramIndex++}`);
|
|
415
|
+
values.push(input.current_period_start);
|
|
416
|
+
}
|
|
417
|
+
if (input.current_period_end !== undefined) {
|
|
418
|
+
updates.push(`current_period_end = $${paramIndex++}`);
|
|
419
|
+
values.push(input.current_period_end);
|
|
420
|
+
}
|
|
421
|
+
if (input.cancel_at_period_end !== undefined) {
|
|
422
|
+
updates.push(`cancel_at_period_end = $${paramIndex++}`);
|
|
423
|
+
values.push(input.cancel_at_period_end);
|
|
424
|
+
}
|
|
425
|
+
if (input.metadata !== undefined) {
|
|
426
|
+
updates.push(`metadata = $${paramIndex++}`);
|
|
427
|
+
values.push(JSON.stringify(input.metadata));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (updates.length === 0) {
|
|
431
|
+
return this.getUserSubscriptionById(id);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
updates.push(`updated_at = NOW()`);
|
|
435
|
+
values.push(id);
|
|
436
|
+
|
|
437
|
+
const result = await getPool().query(
|
|
438
|
+
`UPDATE ${userSubsTableFull} SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
|
|
439
|
+
values
|
|
440
|
+
);
|
|
441
|
+
return (result.rows[0] as UserSubscription) || null;
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
async cancelSubscription(id: string): Promise<boolean> {
|
|
445
|
+
const result = await getPool().query(
|
|
446
|
+
`UPDATE ${userSubsTableFull}
|
|
447
|
+
SET cancel_at_period_end = true, updated_at = NOW()
|
|
448
|
+
WHERE id = $1`,
|
|
449
|
+
[id]
|
|
450
|
+
);
|
|
451
|
+
return (result.rowCount ?? 0) > 0;
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
async getFeatureLimit(userId: string, featureCode: string): Promise<number | null> {
|
|
455
|
+
const result = await getPool().query(
|
|
456
|
+
`SELECT se.limit_value
|
|
457
|
+
FROM ${userSubsTableFull} us
|
|
458
|
+
JOIN ${entitlementsTableFull} se ON us.tier_id = se.tier_id
|
|
459
|
+
WHERE us.user_id = $1 AND us.status = 'active' AND se.feature_code = $2
|
|
460
|
+
ORDER BY us.created_at DESC
|
|
461
|
+
LIMIT 1`,
|
|
462
|
+
[userId, featureCode]
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
if (result.rows.length === 0) {
|
|
466
|
+
return null; // No subscription or feature not found
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return (result.rows[0] as { limit_value: number | null }).limit_value;
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
async hasFeature(userId: string, featureCode: string): Promise<boolean> {
|
|
473
|
+
const limit = await this.getFeatureLimit(userId, featureCode);
|
|
474
|
+
// Has feature if limit is not null and not 0 (0 means disabled)
|
|
475
|
+
return limit !== null && limit !== 0;
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
async shutdown(): Promise<void> {
|
|
479
|
+
// Pool is managed externally, nothing to do here
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
}
|