Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions backend/migrations/create_price_history.sql
Original file line number Diff line number Diff line change
@@ -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();
24 changes: 24 additions & 0 deletions backend/src/routes/subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 40 additions & 1 deletion backend/src/services/subscription-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
Expand Down Expand Up @@ -442,6 +459,28 @@ export class SubscriptionService {
throw error;
}
}

/**
* Get price history for a subscription
*/
async getPriceHistory(
userId: string,
subscriptionId: string
): Promise<any[]> {
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();
9 changes: 9 additions & 0 deletions client/components/pages/subscriptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,15 @@ function SubscriptionCard({
Unused {unusedInfo.daysSinceLastUse}d
</span>
)}
{sub.latest_price_change && (
<span className={`text-xs px-2 py-0.5 rounded-full font-semibold flex items-center gap-1 ${
sub.latest_price_change.new_price > 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
</span>
)}
</div>
<div className="flex items-center gap-2">
<p className={`text-xs ${darkMode ? "text-gray-400" : "text-gray-500"}`}>{sub.category}</p>
Expand Down
Loading