Proration Preview
Interactive component that shows billing adjustments when users change subscription plans, featuring cost breakdowns, credits, and prorated charges with immediate or next-cycle options.
Default
Review the charges and credits for your plan change
Pro
$29.99/monthly
Enterprise
$99.99/monthly
Billing Breakdown
Your plan will change immediately. You'll be charged $35.00.
"use client";
import { ProrationPreview } from "@/components/billingsdk/proration-preview";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
const currentPlan = {
plan: {
id: "pro",
title: "Pro",
description: "Best for small teams",
monthlyPrice: "29.99",
yearlyPrice: "299.99",
currency: "$",
buttonText: "Current Plan",
features: [
{ name: "Advanced features", icon: "check" },
{ name: "Priority support", icon: "check" },
],
},
type: "monthly" as const,
price: "29.99",
nextBillingDate: "2024-01-15",
paymentMethod: "•••• 4242",
status: "active" as const,
};
const yearlyCurrentPlan = {
plan: {
id: "pro-yearly",
title: "Pro",
description: "Best for small teams",
monthlyPrice: "29.99",
yearlyPrice: "299.99",
currency: "$",
buttonText: "Current Plan",
features: [
{ name: "Advanced features", icon: "check" },
{ name: "Priority support", icon: "check" },
],
},
type: "yearly" as const,
price: "299.99",
nextBillingDate: "2024-12-15",
paymentMethod: "•••• 4242",
status: "active" as const,
};
const basicPlan = {
id: "basic",
title: "Basic",
description: "Perfect for getting started",
monthlyPrice: "9.99",
yearlyPrice: "99.99",
currency: "$",
buttonText: "Downgrade",
features: [
{ name: "Basic features", icon: "check" },
{ name: "Email support", icon: "check" },
],
};
const enterprisePlan = {
id: "enterprise",
title: "Enterprise",
description: "For large organizations",
monthlyPrice: "99.99",
yearlyPrice: "999.99",
currency: "$",
buttonText: "Upgrade",
features: [
{ name: "All features", icon: "check" },
{ name: "Priority support", icon: "check" },
{ name: "Custom integrations", icon: "check" },
],
};
const customPlan = {
id: "custom",
title: "Custom",
description: "Tailored for your needs",
monthlyPrice: "Custom",
yearlyPrice: "Custom",
currency: "$",
buttonText: "Contact Sales",
features: [
{ name: "Custom features", icon: "check" },
{ name: "Dedicated support", icon: "check" },
],
};
export function ProrationPreviewDemo() {
return (
<div className="space-y-8">
<Tabs defaultValue="upgrade" className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="upgrade">Upgrade</TabsTrigger>
<TabsTrigger value="downgrade">Downgrade</TabsTrigger>
<TabsTrigger value="cycle-change">Cycle Change</TabsTrigger>
<TabsTrigger value="next-cycle">Next Cycle</TabsTrigger>
</TabsList>
<TabsContent value="upgrade" className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Monthly to Enterprise Upgrade</CardTitle>
<CardDescription>
Upgrading from Pro monthly to Enterprise monthly with 15 days remaining
</CardDescription>
</CardHeader>
<CardContent>
<ProrationPreview
currentPlan={currentPlan}
newPlan={enterprisePlan}
billingCycle="monthly"
daysRemaining={15}
effectiveDate="immediately"
theme="minimal"
onConfirm={() => console.log("Upgrade confirmed")}
onCancel={() => console.log("Upgrade cancelled")}
/>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="downgrade" className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Enterprise to Basic Downgrade</CardTitle>
<CardDescription>
Downgrading from Enterprise to Basic with account credit
</CardDescription>
</CardHeader>
<CardContent>
<ProrationPreview
currentPlan={{
plan: enterprisePlan,
type: "monthly",
price: "99.99",
nextBillingDate: "2024-01-15",
paymentMethod: "•••• 4242",
status: "active",
}}
newPlan={basicPlan}
billingCycle="monthly"
daysRemaining={20}
effectiveDate="immediately"
theme="minimal"
onConfirm={() => console.log("Downgrade confirmed")}
onCancel={() => console.log("Downgrade cancelled")}
/>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="cycle-change" className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Monthly to Yearly Switch</CardTitle>
<CardDescription>
Switching from monthly to yearly billing for the same plan
</CardDescription>
</CardHeader>
<CardContent>
<ProrationPreview
currentPlan={currentPlan}
newPlan={currentPlan.plan}
billingCycle="yearly"
daysRemaining={10}
effectiveDate="immediately"
theme="minimal"
onConfirm={() => console.log("Cycle change confirmed")}
onCancel={() => console.log("Cycle change cancelled")}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Yearly to Monthly Switch</CardTitle>
<CardDescription>
Switching from yearly to monthly billing
</CardDescription>
</CardHeader>
<CardContent>
<ProrationPreview
currentPlan={yearlyCurrentPlan}
newPlan={yearlyCurrentPlan.plan}
billingCycle="monthly"
daysRemaining={120}
effectiveDate="immediately"
theme="classic"
onConfirm={() => console.log("Cycle change confirmed")}
onCancel={() => console.log("Cycle change cancelled")}
/>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="next-cycle" className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Next Cycle Change</CardTitle>
<CardDescription>
Plan change effective at the next billing cycle with no immediate charge
</CardDescription>
</CardHeader>
<CardContent>
<ProrationPreview
currentPlan={currentPlan}
newPlan={enterprisePlan}
billingCycle="monthly"
daysRemaining={25}
effectiveDate="next billing cycle"
theme="minimal"
onConfirm={() => console.log("Next cycle change confirmed")}
onCancel={() => console.log("Next cycle change cancelled")}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Custom Pricing Scenario</CardTitle>
<CardDescription>
Handling custom/enterprise pricing that doesn't follow standard rates
</CardDescription>
</CardHeader>
<CardContent>
<ProrationPreview
currentPlan={currentPlan}
newPlan={customPlan}
billingCycle="monthly"
daysRemaining={12}
effectiveDate="next billing cycle"
theme="classic"
onConfirm={() => console.log("Custom plan confirmed")}
onCancel={() => console.log("Custom plan cancelled")}
/>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}
Installation
yarn dlx @billingsdk/cli add proration-preview
Step 1: Install the following dependencies:
npm install motion lucide-react class-variance-authority
Step 2: Copy and paste the following code into your project.
"use client";
import { motion } from "motion/react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { ArrowRight, Calendar, CreditCard, Calculator, Clock } from "lucide-react";
import { type Plan, type CurrentPlan } from "@/lib/billingsdk-config";
import { cn } from "@/lib/utils";
import { cva, type VariantProps } from "class-variance-authority";
const prorationPreviewVariants = cva("w-full max-w-3xl mx-auto", {
variants: {
theme: {
minimal: "",
classic: "relative overflow-hidden rounded-xl border border-border/50 bg-gradient-to-br from-card/50 to-muted/30 backdrop-blur-sm",
},
size: {
small: "text-sm",
medium: "text-base",
large: "text-lg",
},
},
defaultVariants: {
theme: "minimal",
size: "medium",
},
});
const cardVariants = cva("transition-all duration-300", {
variants: {
theme: {
minimal: "border border-border bg-card",
classic: "border border-border/30 bg-gradient-to-br from-card/80 to-muted/20 backdrop-blur-sm shadow-md",
},
},
defaultVariants: {
theme: "minimal",
},
});
/**
* Props for `ProrationPreview`.
*
* Combines visual variants from `prorationPreviewVariants` with
* the billing context needed to compute proration math.
*
* - `currentPlan` and `newPlan` supply pricing and labels
* - `billingCycle` controls the target cycle for the new plan
* - `daysRemaining` and `effectiveDate` influence credit/charge math
* - `onConfirm`/`onCancel` wire user actions
*/
export interface ProrationPreviewProps extends VariantProps<typeof prorationPreviewVariants> {
className?: string;
currentPlan: CurrentPlan;
newPlan: Plan;
billingCycle: 'monthly' | 'yearly';
daysRemaining?: number;
effectiveDate?: string;
onConfirm?: () => void;
onCancel?: () => void;
confirmText?: string;
cancelText?: string;
}
/**
* ProrationPreview
*
* Renders a detailed, accessible preview of billing changes when a user switches
* subscription plans. It calculates and displays credits for unused time,
* prorated charges for the new plan, and the resulting net amount, with clear
* visual hierarchy and responsive styles.
*
* Key UI sections:
* - From/To plan summary with upgrade/downgrade badges
* - Billing breakdown (credit, prorated charge, net amount)
* - Timeline note describing when changes take effect
* - Primary/secondary actions to confirm or cancel
*
* Props accept both monthly and yearly cycles and support custom pricing
* scenarios (e.g., enterprise). Visual appearance can be adjusted via
* `theme` and `size` variants.
*
* @param props.className Optional container className override.
* @param props.currentPlan Current subscription context including plan and cycle.
* @param props.newPlan Target plan the user is moving to.
* @param props.billingCycle Billing cycle for the new plan (monthly/yearly).
* @param props.daysRemaining Remaining days in the current cycle (defaults to 15).
* @param props.effectiveDate When the change takes effect (e.g., "immediately" or "next billing cycle").
* @param props.onConfirm Callback invoked when user confirms the change.
* @param props.onCancel Callback invoked when user cancels the change.
* @param props.confirmText Custom label for the confirm action.
* @param props.cancelText Custom label for the cancel action.
* @param props.theme Visual theme variant (minimal | classic). Defaults to minimal.
* @param props.size Component size (small | medium | large). Defaults to medium.
*/
export function ProrationPreview({
className,
currentPlan,
newPlan,
billingCycle,
daysRemaining = 15,
effectiveDate = "immediately",
onConfirm,
onCancel,
confirmText = "Confirm Change",
cancelText = "Cancel",
theme = "minimal",
size = "medium",
}: ProrationPreviewProps) {
// Prices & proration (robust) - Fixed CodeRabbit issues
const currentCycleDays = currentPlan.type === 'yearly' ? 365 : 30;
const newCycleDays = billingCycle === 'yearly' ? 365 : 30;
const isNumericValue = (v?: string) => {
if (v == null) return false;
const s = String(v).replace(/[^\d.\-]/g, "");
const n = Number.parseFloat(s);
return Number.isFinite(n);
};
const toNumber = (v?: string) => {
if (v == null) return undefined;
const s = String(v).replace(/[^\d.\-]/g, "");
const n = Number.parseFloat(s);
return Number.isFinite(n) ? n : undefined;
};
const currentRaw =
currentPlan.type === 'monthly' ? currentPlan.plan.monthlyPrice :
currentPlan.type === 'yearly' ? currentPlan.plan.yearlyPrice :
currentPlan.price;
const newRaw = billingCycle === 'monthly' ? newPlan.monthlyPrice : newPlan.yearlyPrice;
const currentPrice = toNumber(currentRaw);
const newPrice = toNumber(newRaw);
const isCustomCurrent = !isNumericValue(currentRaw);
const isCustomNew = !isNumericValue(newRaw);
const chargeCurrency = newPlan.currency ?? currentPlan.plan.currency ?? "$";
const creditCurrency = currentPlan.plan.currency ?? newPlan.currency ?? "$";
const clampedUnusedDays = Math.max(0, Math.min(daysRemaining, currentCycleDays));
const isNextCycle = typeof effectiveDate === 'string' && effectiveDate.toLowerCase().includes('next');
const prorationDays = isNextCycle ? 0 : clampedUnusedDays;
const canCompute = !isNextCycle && !isCustomCurrent && !isCustomNew && currentPrice !== undefined && newPrice !== undefined;
const creditAmount = canCompute ? (currentPrice! / currentCycleDays) * clampedUnusedDays : 0;
const proratedCharge = canCompute ? (newPrice! / newCycleDays) * prorationDays : 0;
const netAmount = proratedCharge - creditAmount;
const normalizedCurrentMonthly = currentPlan.type === 'yearly' && currentPrice !== undefined ? currentPrice / 12 : (currentPrice ?? 0);
const normalizedNewMonthly = billingCycle === 'yearly' && newPrice !== undefined ? newPrice / 12 : (newPrice ?? 0);
const hasComparablePrices = !isCustomCurrent && !isCustomNew && currentPrice !== undefined && newPrice !== undefined;
const isUpgrade = hasComparablePrices && normalizedNewMonthly > normalizedCurrentMonthly;
const isDowngrade = hasComparablePrices && normalizedNewMonthly < normalizedCurrentMonthly;
return (
<div className={cn(prorationPreviewVariants({ theme, size }), className)}>
{theme === "classic" && (
<>
<div className="absolute inset-0 bg-grid-pattern opacity-5" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-primary/5 rounded-full blur-3xl" />
</>
)}
<Card className={cn(cardVariants({ theme }))}>
<CardHeader className="pb-3 sm:pb-4 px-4 sm:px-6">
<CardTitle className="flex items-center gap-2 text-base sm:text-lg">
<div className="p-2 rounded-lg bg-primary/10 ring-1 ring-primary/20">
<Calculator className="h-4 w-4 sm:h-5 sm:w-5 text-primary" />
</div>
Plan Change Preview
</CardTitle>
<p className="text-sm text-muted-foreground">
Review the charges and credits for your plan change
</p>
</CardHeader>
<CardContent className="space-y-4 sm:space-y-6 px-4 sm:px-6">
{/* From/To Plans */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 sm:gap-4 items-center">
{/* Current Plan */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}
className={cn(
"p-3 sm:p-4 rounded-lg border",
theme === "classic"
? "bg-gradient-to-br from-muted/50 to-background/50 border-border/50"
: "bg-muted/50 border-border"
)}
>
<div className="flex items-center gap-2 mb-2">
<Badge variant="outline" className="text-[10px] sm:text-xs">Current</Badge>
{isDowngrade && (
<Badge variant="secondary" className="text-[10px] sm:text-xs">
Downgrading
</Badge>
)}
</div>
<h3 className="font-semibold text-base sm:text-lg">{currentPlan.plan.title}</h3>
<p className="text-xs sm:text-sm text-muted-foreground mb-3">
{isCustomCurrent ? 'Custom' : `${creditCurrency}${currentPrice}/${currentPlan.type}`}
</p>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3 sm:h-4 sm:w-4" />
{daysRemaining} days remaining
</div>
</motion.div>
{/* Arrow */}
<div className="flex justify-center">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.3, delay: 0.2 }}
className={cn(
"p-2 rounded-full",
theme === "classic"
? "bg-gradient-to-r from-primary to-primary/80 text-primary-foreground shadow-lg"
: "bg-primary/10 text-primary"
)}
>
<ArrowRight className="h-4 w-4 sm:h-5 sm:w-5" />
</motion.div>
</div>
{/* New Plan */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, delay: 0.1 }}
className={cn(
"p-3 sm:p-4 rounded-lg border",
theme === "classic"
? "bg-gradient-to-br from-primary/5 to-primary/10 border-primary/30"
: "bg-primary/5 border-primary/30"
)}
>
<div className="flex items-center gap-2 mb-2">
<Badge variant="default" className="text-[10px] sm:text-xs">New Plan</Badge>
{isUpgrade && (
<Badge variant="secondary" className="text-[10px] sm:text-xs">
Upgrading
</Badge>
)}
</div>
<h3 className="font-semibold text-base sm:text-lg">{newPlan.title}</h3>
<p className="text-xs sm:text-sm text-muted-foreground mb-3">
{isCustomNew ? 'Custom' : `${chargeCurrency}${newPrice}/${billingCycle}`}
</p>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Calendar className="h-3 w-3 sm:h-4 sm:w-4" />
Effective {effectiveDate}
</div>
</motion.div>
</div>
<Separator className={cn(
theme === "classic" && "bg-gradient-to-r from-transparent via-border to-transparent"
)} />
{/* Calculation Breakdown */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.4 }}
className={cn(
"p-3 sm:p-4 rounded-lg border",
theme === "classic"
? "bg-gradient-to-br from-muted/30 to-background/30 border-border/50"
: "bg-muted/30 border-border"
)}
>
<h4 className="font-medium mb-3 sm:mb-4 flex items-center gap-2">
<CreditCard className="h-4 w-4 sm:h-5 sm:w-5" />
Billing Breakdown
</h4>
<div className="space-y-3">
<div className="flex justify-between items-center text-xs sm:text-sm">
<span className="text-muted-foreground">Credit for unused time</span>
<span className="text-green-600 font-medium">
{canCompute ? `-${creditCurrency}${Math.abs(creditAmount).toFixed(2)}` : "—"}
</span>
</div>
<div className="flex justify-between items-center text-xs sm:text-sm">
<span className="text-muted-foreground">
Prorated charge ({prorationDays} days)
</span>
<span className="font-medium">
{canCompute ? `+${chargeCurrency}${proratedCharge.toFixed(2)}` : "—"}
</span>
</div>
<Separator className="my-2" />
<div className="flex justify-between items-center font-semibold">
<span>{canCompute ? (netAmount >= 0 ? "Amount to charge" : "Credit to account") : "Amount due will be calculated at checkout"}</span>
<span className={cn(
"text-base sm:text-lg",
canCompute ? (netAmount >= 0 ? "text-foreground" : "text-green-600") : "text-muted-foreground"
)}>
{canCompute ? `${netAmount >= 0 ? "+" : ""}${chargeCurrency}${netAmount.toFixed(2)}` : "—"}
</span>
</div>
</div>
</motion.div>
{/* Timeline */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3, delay: 0.6 }}
className="text-center p-3 sm:p-4 bg-muted/20 rounded-lg border border-border/50"
>
<p className="text-xs sm:text-sm text-muted-foreground">
Your plan will change {effectiveDate}.
{isNextCycle ? ' No immediate charge.' : (hasComparablePrices
? (netAmount >= 0
? ` You'll be charged ${chargeCurrency}${Math.abs(netAmount).toFixed(2)}.`
: ` You'll receive a ${chargeCurrency}${Math.abs(netAmount).toFixed(2)} credit.`)
: ' Amount will be finalized at checkout.')}
</p>
</motion.div>
{/* Action Buttons */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: 0.8 }}
className="flex flex-col sm:flex-row gap-3 pt-3 sm:pt-4"
>
<Button
onClick={onConfirm}
className={cn(
"flex-1",
theme === "classic" && "bg-gradient-to-r from-primary to-primary/90 hover:shadow-md active:scale-95 transition-all duration-200"
)}
size="lg"
>
{confirmText}
<ArrowRight className="ml-2 h-4 w-4 sm:h-5 sm:w-5" />
</Button>
<Button
variant="outline"
onClick={onCancel}
className="flex-1"
size="lg"
>
{cancelText}
</Button>
</motion.div>
</CardContent>
</Card>
</div>
);
}
Step 3: Update the import paths to match your project setup.
Usage
import { ProrationPreview } from "@/components/billingsdk/proration-preview"
<ProrationPreview
currentPlan={{
plan: {
id: "pro",
title: "Pro",
description: "Best for small teams",
monthlyPrice: "29.99",
yearlyPrice: "299.99",
currency: "$",
buttonText: "Current Plan",
features: [
{ name: "Advanced features", icon: "check" },
{ name: "Priority support", icon: "check" },
],
},
type: "monthly",
price: "29.99",
nextBillingDate: "2024-01-15",
paymentMethod: "•••• 4242",
status: "active",
}}
newPlan={{
id: "enterprise",
title: "Enterprise",
description: "For large organizations",
monthlyPrice: "99.99",
yearlyPrice: "999.99",
currency: "$",
buttonText: "Upgrade",
features: [
{ name: "All features", icon: "check" },
{ name: "Priority support", icon: "check" },
{ name: "Custom integrations", icon: "check" },
],
}}
billingCycle="monthly"
daysRemaining={15}
effectiveDate="immediately"
onConfirm={() => console.log("Plan change confirmed")}
onCancel={() => console.log("Plan change cancelled")}
/>
Examples
Upgrade Scenario
Shows cost breakdown when upgrading from Pro to Enterprise with remaining billing cycle.
Downgrade Scenario
Displays credit calculations when downgrading plans with unused time refunds.
Billing Cycle Changes
Handles switching between monthly and yearly billing for the same plan.
Next Cycle Changes
Shows plan changes that take effect at the next billing cycle with no immediate charges.
API Reference
ProrationPreview
Prop | Type | Default | Description |
---|---|---|---|
currentPlan | CurrentPlan | - | The user's current subscription plan details |
newPlan | Plan | - | The target plan they want to switch to |
billingCycle | 'monthly' | 'yearly' | - | The billing cycle for the new plan |
daysRemaining | number | 15 | Days remaining in current billing cycle |
effectiveDate | string | "immediately" | When the plan change takes effect |
onConfirm | () => void | - | Callback when user confirms the change |
onCancel | () => void | - | Callback when user cancels the change |
confirmText | string | "Confirm Change" | Text for the confirm button |
cancelText | string | "Cancel" | Text for the cancel button |
theme | 'minimal' | 'classic' | 'minimal' | Visual theme variant |
size | 'small' | 'medium' | 'large' | 'medium' | Component size |
className | string | - | Additional CSS classes |
CurrentPlan
interface CurrentPlan {
plan: Plan;
type: 'monthly' | 'yearly' | 'custom';
price?: string;
nextBillingDate: string;
paymentMethod: string;
status: 'active' | 'inactive' | 'past_due' | 'cancelled';
}
Plan
interface Plan {
id: string;
title: string;
description: string;
monthlyPrice: string;
yearlyPrice: string;
currency?: string;
buttonText: string;
features: {
name: string;
icon: string;
iconColor?: string;
}[];
}
Features
- Accurate Proration Calculations - Handles complex billing scenarios with precise math
- Credit & Charge Breakdown - Shows detailed billing adjustments and credits
- Immediate vs Next Cycle - Supports both immediate and next-cycle plan changes
- Theme Variants - Minimal and classic visual themes
- Responsive Design - Works seamlessly across all device sizes
- Accessibility Ready - Full keyboard navigation and screen reader support
- Animation Effects - Smooth transitions using Motion
- Custom Pricing Support - Handles non-standard pricing like "Custom" or "Enterprise"
- Robust Error Handling - Graceful handling of invalid pricing data
Use Cases
- Plan Upgrades - Show costs when upgrading to higher-tier plans
- Plan Downgrades - Display credits when downgrading plans
- Billing Cycle Changes - Handle monthly/yearly billing switches
- Enterprise Sales - Support custom pricing and next-cycle changes
- Billing Transparency - Provide clear breakdown of charges and credits
Billing Logic
The component implements sophisticated proration logic:
- Credits - Calculated based on unused time in current plan (remaining days, not elapsed)
- Prorated Charges - New plan charges based on remaining cycle days
- Net Amount - Final charge or credit after calculations
- Cycle Handling - Different logic for immediate vs next-cycle changes
- Price Validation - Robust parsing of pricing strings with fallbacks
- Currency Consistency - Uses current plan currency for credits, new plan currency for charges