diff --git a/backend/migrations/create_price_history.sql b/backend/migrations/create_price_history.sql new file mode 100644 index 0000000..db9c789 --- /dev/null +++ b/backend/migrations/create_price_history.sql @@ -0,0 +1,36 @@ +-- Create subscription_price_history table +CREATE TABLE IF NOT EXISTS subscription_price_history ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + subscription_id UUID NOT NULL REFERENCES subscriptions(id) ON DELETE CASCADE, + old_price DECIMAL(10, 2) NOT NULL, + new_price DECIMAL(10, 2) NOT NULL, + changed_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL, + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE +); + +-- Enable RLS +ALTER TABLE subscription_price_history ENABLE ROW LEVEL SECURITY; + +-- RLS Policies +CREATE POLICY "Users can view their own price history" + ON subscription_price_history FOR SELECT + USING (auth.uid() = user_id); + +-- Function to handle price changes +CREATE OR REPLACE FUNCTION handle_subscription_price_change() +RETURNS TRIGGER AS $$ +BEGIN + IF (OLD.price IS DISTINCT FROM NEW.price) THEN + INSERT INTO subscription_price_history (subscription_id, old_price, new_price, user_id) + VALUES (NEW.id, OLD.price, NEW.price, NEW.user_id); + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Trigger for price changes +DROP TRIGGER IF EXISTS on_subscription_price_change ON subscriptions; +CREATE TRIGGER on_subscription_price_change + AFTER UPDATE ON subscriptions + FOR EACH ROW + EXECUTE FUNCTION handle_subscription_price_change(); diff --git a/backend/src/routes/subscriptions.ts b/backend/src/routes/subscriptions.ts index c749492..0060ccb 100644 --- a/backend/src/routes/subscriptions.ts +++ b/backend/src/routes/subscriptions.ts @@ -109,6 +109,30 @@ router.get("/:id", validateSubscriptionOwnership, async (req: AuthenticatedReque } }); +/** + * GET /api/subscriptions/:id/price-history + * Get price history for a subscription + */ +router.get("/:id/price-history", validateSubscriptionOwnership, async (req: AuthenticatedRequest, res: Response) => { + try { + const history = await subscriptionService.getPriceHistory( + req.user!.id, + Array.isArray(req.params.id) ? req.params.id[0] : req.params.id + ); + + res.json({ + success: true, + data: history, + }); + } catch (error) { + logger.error("Get price history error:", error); + res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : "Failed to get price history", + }); + } +}); + /** * POST /api/subscriptions * Create new subscription with idempotency support diff --git a/backend/src/services/subscription-service.ts b/backend/src/services/subscription-service.ts index b00ff33..0b21987 100644 --- a/backend/src/services/subscription-service.ts +++ b/backend/src/services/subscription-service.ts @@ -327,8 +327,25 @@ export class SubscriptionService { throw new Error(`Failed to fetch subscriptions: ${error.message}`); } + // Fetch latest price change for each subscription + const enhancedSubscriptions = await Promise.all( + (subscriptions || []).map(async (sub) => { + const { data: priceHistory } = await supabase + .from("subscription_price_history") + .select("*") + .eq("subscription_id", sub.id) + .order("changed_at", { ascending: false }) + .limit(1); + + return { + ...sub, + latest_price_change: priceHistory && priceHistory.length > 0 ? priceHistory[0] : null, + }; + }) + ); + return { - subscriptions: subscriptions || [], + subscriptions: enhancedSubscriptions, total: count || 0, }; } @@ -442,6 +459,28 @@ export class SubscriptionService { throw error; } } + + /** + * Get price history for a subscription + */ + async getPriceHistory( + userId: string, + subscriptionId: string + ): Promise { + const { data, error } = await supabase + .from("subscription_price_history") + .select("*") + .eq("subscription_id", subscriptionId) + .eq("user_id", userId) + .order("changed_at", { ascending: false }); + + if (error) { + logger.error("Failed to fetch price history:", error); + throw new Error(`Failed to fetch price history: ${error.message}`); + } + + return data || []; + } } export const subscriptionService = new SubscriptionService(); diff --git a/client/components/pages/subscriptions.tsx b/client/components/pages/subscriptions.tsx index 3c39968..68a65b7 100644 --- a/client/components/pages/subscriptions.tsx +++ b/client/components/pages/subscriptions.tsx @@ -390,6 +390,15 @@ function SubscriptionCard({ Unused {unusedInfo.daysSinceLastUse}d )} + {sub.latest_price_change && ( + sub.latest_price_change.old_price + ? "bg-red-100 text-red-700" + : "bg-green-100 text-green-700" + }`}> + {sub.latest_price_change.new_price > sub.latest_price_change.old_price ? "↑" : "↓"} Price Changed + + )}

{sub.category}