surcharge amount

This commit is contained in:
Brandon Thomas committed 2026-06-11 23:27:19 -04:00
1 parent 923609ea34
commit 5a022d010d
3 files changed
+141 -70

No files matched your search

@@ -3,34 +3,37 @@ use crate::cost::kinovi_generation_cost::KinoviGenerationCost;
/// The cost of a Kinovi Seedance generation, with the video-reference
/// surcharge broken out from the base price.
///
/// `total_cost` covers `base_credits_cost + video_reference_surcharge_cost`.
/// `total_cost` covers base + surcharge. NB: the total's USD conversions are
/// computed from the SUMMED credits (rounded once), so they may differ by a
/// cent from adding the parts' rounded USD values together.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct KinoviSeedanceGenerationCost {
/// The full cost (base + any surcharge), in credits and USD cents.
pub total_cost: KinoviGenerationCost,
/// The base generation cost in Kinovi credits (resolution rate × output
/// duration × batch count), excluding any surcharges.
pub base_credits_cost: u64,
/// The base generation cost (resolution rate × output duration × batch
/// count), excluding any surcharges.
pub base_cost: KinoviGenerationCost,
/// The video-reference surcharge in Kinovi credits, when one or more
/// reference videos are attached. `None` when no reference videos are
/// attached.
pub video_reference_surcharge_cost: Option<u64>,
/// The video-reference surcharge, when one or more reference videos are
/// attached. `None` when no reference videos are attached.
pub video_reference_surcharge_cost: Option<KinoviGenerationCost>,
}
impl KinoviSeedanceGenerationCost {
/// Build a cost from base credits plus an optional video-reference
/// surcharge. The total (and its USD conversion) is derived.
/// surcharge (both in Kinovi credits). The per-part and total USD
/// conversions are derived.
pub fn from_base_and_surcharge(
base_credits_cost: u64,
video_reference_surcharge_cost: Option<u64>,
base_credits: u64,
maybe_video_reference_surcharge_credits: Option<u64>,
) -> Self {
let total_credits = base_credits_cost + video_reference_surcharge_cost.unwrap_or(0);
let total_credits = base_credits + maybe_video_reference_surcharge_credits.unwrap_or(0);
Self {
total_cost: KinoviGenerationCost::from_kinovi_credits(total_credits),
base_credits_cost,
video_reference_surcharge_cost,
base_cost: KinoviGenerationCost::from_kinovi_credits(base_credits),
video_reference_surcharge_cost: maybe_video_reference_surcharge_credits
.map(KinoviGenerationCost::from_kinovi_credits),
}
}
}
@@ -44,32 +47,64 @@ mod tests {
#[test]
fn without_surcharge() {
let cost = KinoviSeedanceGenerationCost::from_base_and_surcharge(200, None);
assert_eq!(cost.base_credits_cost, 200);
assert_eq!(cost.video_reference_surcharge_cost, None);
assert_eq!(cost.total_cost.kinovi_credits, 200);
// 20000/193 = 103.63.
assert_eq!(cost.total_cost.usd_cents_rounded_up, 104);
assert_eq!(cost.total_cost.usd_cents_rounded_down, 103);
assert!((cost.total_cost.usd_cents_fractional - (20000.0 / 193.0)).abs() < FLOAT_TOLERANCE);
assert!(cost.video_reference_surcharge_cost.is_none());
// Base: 200 credits; 20000/193 = 103.63.
assert_eq!(cost.base_cost.kinovi_credits, 200);
assert_eq!(cost.base_cost.usd_cents_rounded_up, 104);
assert_eq!(cost.base_cost.usd_cents_rounded_down, 103);
assert!((cost.base_cost.usd_cents_fractional - (20000.0 / 193.0)).abs() < FLOAT_TOLERANCE);
// Total equals base when there's no surcharge.
assert_eq!(cost.total_cost, cost.base_cost);
}
#[test]
fn with_surcharge() {
let cost = KinoviSeedanceGenerationCost::from_base_and_surcharge(200, Some(40));
assert_eq!(cost.base_credits_cost, 200);
assert_eq!(cost.video_reference_surcharge_cost, Some(40));
// Base: 200 credits; 20000/193 = 103.63.
assert_eq!(cost.base_cost.kinovi_credits, 200);
assert_eq!(cost.base_cost.usd_cents_rounded_up, 104);
assert_eq!(cost.base_cost.usd_cents_rounded_down, 103);
assert!((cost.base_cost.usd_cents_fractional - (20000.0 / 193.0)).abs() < FLOAT_TOLERANCE);
// Surcharge: 40 credits; 4000/193 = 20.73.
let surcharge = cost.video_reference_surcharge_cost.expect("should have surcharge");
assert_eq!(surcharge.kinovi_credits, 40);
assert_eq!(surcharge.usd_cents_rounded_up, 21);
assert_eq!(surcharge.usd_cents_rounded_down, 20);
assert!((surcharge.usd_cents_fractional - (4000.0 / 193.0)).abs() < FLOAT_TOLERANCE);
// Total: 240 credits; 24000/193 = 124.35.
assert_eq!(cost.total_cost.kinovi_credits, 240);
// 24000/193 = 124.35.
assert_eq!(cost.total_cost.usd_cents_rounded_up, 125);
assert_eq!(cost.total_cost.usd_cents_rounded_down, 124);
assert!((cost.total_cost.usd_cents_fractional - (24000.0 / 193.0)).abs() < FLOAT_TOLERANCE);
}
#[test]
fn total_is_base_plus_surcharge() {
fn total_credits_are_base_plus_surcharge() {
let cost = KinoviSeedanceGenerationCost::from_base_and_surcharge(450, Some(90));
assert_eq!(
cost.total_cost.kinovi_credits,
cost.base_credits_cost + cost.video_reference_surcharge_cost.unwrap());
cost.base_cost.kinovi_credits + cost.video_reference_surcharge_cost.unwrap().kinovi_credits);
}
/// The total's USD is rounded ONCE on the summed credits, so it can be a
/// cent under the sum of the parts' rounded-up values.
#[test]
fn total_usd_is_rounded_once_not_per_part() {
let cost = KinoviSeedanceGenerationCost::from_base_and_surcharge(200, Some(40));
let surcharge = cost.video_reference_surcharge_cost.unwrap();
// Parts round up to 104 + 21 = 125 — which happens to match here…
assert_eq!(cost.base_cost.usd_cents_rounded_up + surcharge.usd_cents_rounded_up, 125);
assert_eq!(cost.total_cost.usd_cents_rounded_up, 125);
// …but the fractional parts always sum exactly to the fractional total.
let parts = cost.base_cost.usd_cents_fractional + surcharge.usd_cents_fractional;
assert!((parts - cost.total_cost.usd_cents_fractional).abs() < FLOAT_TOLERANCE);
}
}
@@ -115,16 +115,16 @@ impl GenerateSeedance2p0Request {
};
let duration = u64::from(self.duration_seconds);
let base_credits_cost = duration * credits_per_second * batch_multiplier;
let video_reference_surcharge_cost = if self.has_video_reference() {
let base_credits = duration * credits_per_second * batch_multiplier;
let maybe_video_reference_surcharge_credits = if self.has_video_reference() {
Some(duration * video_reference_surcharge_per_second * batch_multiplier)
} else {
None
};
KinoviSeedanceGenerationCost::from_base_and_surcharge(
base_credits_cost,
video_reference_surcharge_cost,
base_credits,
maybe_video_reference_surcharge_credits,
)
}
@@ -433,8 +433,8 @@ mod tests {
assert_eq!(costs.total_cost.usd_cents_rounded_up, 39);
assert_eq!(costs.total_cost.usd_cents_rounded_down, 38);
assert!((costs.total_cost.usd_cents_fractional - (7500.0 / 193.0)).abs() < 1e-9);
assert_eq!(costs.base_credits_cost, 75);
assert_eq!(costs.video_reference_surcharge_cost, None);
assert_eq!(costs.base_cost.kinovi_credits, 75);
assert!(costs.video_reference_surcharge_cost.is_none());
}
#[test]
@@ -445,8 +445,8 @@ mod tests {
assert_eq!(costs.total_cost.usd_cents_rounded_up, 104);
assert_eq!(costs.total_cost.usd_cents_rounded_down, 103);
assert!((costs.total_cost.usd_cents_fractional - (20000.0 / 193.0)).abs() < 1e-9);
assert_eq!(costs.base_credits_cost, 200);
assert_eq!(costs.video_reference_surcharge_cost, None);
assert_eq!(costs.base_cost.kinovi_credits, 200);
assert!(costs.video_reference_surcharge_cost.is_none());
}
#[test]
@@ -457,8 +457,8 @@ mod tests {
assert_eq!(costs.total_cost.usd_cents_rounded_up, 234);
assert_eq!(costs.total_cost.usd_cents_rounded_down, 233);
assert!((costs.total_cost.usd_cents_fractional - (45000.0 / 193.0)).abs() < 1e-9);
assert_eq!(costs.base_credits_cost, 450);
assert_eq!(costs.video_reference_surcharge_cost, None);
assert_eq!(costs.base_cost.kinovi_credits, 450);
assert!(costs.video_reference_surcharge_cost.is_none());
}
#[test]
@@ -469,8 +469,8 @@ mod tests {
assert_eq!(costs.total_cost.usd_cents_rounded_up, 311);
assert_eq!(costs.total_cost.usd_cents_rounded_down, 310);
assert!((costs.total_cost.usd_cents_fractional - (60000.0 / 193.0)).abs() < 1e-9);
assert_eq!(costs.base_credits_cost, 600);
assert_eq!(costs.video_reference_surcharge_cost, None);
assert_eq!(costs.base_cost.kinovi_credits, 600);
assert!(costs.video_reference_surcharge_cost.is_none());
}
#[test]
@@ -481,8 +481,8 @@ mod tests {
assert_eq!(costs.total_cost.usd_cents_rounded_up, 700);
assert_eq!(costs.total_cost.usd_cents_rounded_down, 699);
assert!((costs.total_cost.usd_cents_fractional - (135000.0 / 193.0)).abs() < 1e-9);
assert_eq!(costs.base_credits_cost, 1350);
assert_eq!(costs.video_reference_surcharge_cost, None);
assert_eq!(costs.base_cost.kinovi_credits, 1350);
assert!(costs.video_reference_surcharge_cost.is_none());
}
#[test]
@@ -493,8 +493,8 @@ mod tests {
assert_eq!(costs.total_cost.usd_cents_rounded_up, 415);
assert_eq!(costs.total_cost.usd_cents_rounded_down, 414);
assert!((costs.total_cost.usd_cents_fractional - (80000.0 / 193.0)).abs() < 1e-9);
assert_eq!(costs.base_credits_cost, 800);
assert_eq!(costs.video_reference_surcharge_cost, None);
assert_eq!(costs.base_cost.kinovi_credits, 800);
assert!(costs.video_reference_surcharge_cost.is_none());
}
/// The deprecated shims return the corresponding struct fields.
@@ -547,8 +547,8 @@ mod tests {
for (make, duration, base, surcharge) in cases {
let costs = with_video_ref(make(*duration)).calculate_costs();
assert_eq!(costs.base_credits_cost, *base, "base for {duration}s");
assert_eq!(costs.video_reference_surcharge_cost, Some(*surcharge), "surcharge for {duration}s");
assert_eq!(costs.base_cost.kinovi_credits, *base, "base for {duration}s");
assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(*surcharge), "surcharge for {duration}s");
assert_eq!(costs.total_cost.kinovi_credits, base + surcharge, "total for {duration}s");
}
}
@@ -557,21 +557,39 @@ mod tests {
fn surcharge_includes_usd_cents() {
// 720p 5s + video ref = 200 + 40 = 240 credits; 24000/193 = 124.35 → 125¢
let costs = with_video_ref(r720(5)).calculate_costs();
assert_eq!(costs.base_credits_cost, 200);
assert_eq!(costs.video_reference_surcharge_cost, Some(40));
assert_eq!(costs.base_cost.kinovi_credits, 200);
assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(40));
assert_eq!(costs.total_cost.kinovi_credits, 240);
assert_eq!(costs.total_cost.usd_cents_rounded_up, 125);
assert_eq!(costs.total_cost.usd_cents_rounded_down, 124);
assert!((costs.total_cost.usd_cents_fractional - (24000.0 / 193.0)).abs() < 1e-9);
}
/// The base and surcharge parts each carry their own USD conversions.
#[test]
fn parts_have_their_own_usd_conversions() {
let costs = with_video_ref(r720(5)).calculate_costs();
// Base: 200 credits; 20000/193 = 103.63.
assert_eq!(costs.base_cost.usd_cents_rounded_up, 104);
assert_eq!(costs.base_cost.usd_cents_rounded_down, 103);
assert!((costs.base_cost.usd_cents_fractional - (20000.0 / 193.0)).abs() < 1e-9);
// Surcharge: 40 credits; 4000/193 = 20.73.
let surcharge = costs.video_reference_surcharge_cost.expect("should have surcharge");
assert_eq!(surcharge.kinovi_credits, 40);
assert_eq!(surcharge.usd_cents_rounded_up, 21);
assert_eq!(surcharge.usd_cents_rounded_down, 20);
assert!((surcharge.usd_cents_fractional - (4000.0 / 193.0)).abs() < 1e-9);
}
#[test]
fn empty_video_reference_list_has_no_surcharge() {
let mut request = r720(5);
request.reference_video_urls = Some(vec![]);
let costs = request.calculate_costs();
assert_eq!(costs.total_cost.kinovi_credits, 200);
assert_eq!(costs.video_reference_surcharge_cost, None);
assert!(costs.video_reference_surcharge_cost.is_none());
}
/// Surcharge is flat per generation regardless of how many reference
@@ -592,8 +610,8 @@ mod tests {
let request = with_video_ref(build_request(5, None, Some(KinoviSeedance2p0BatchCount::Two)));
// (200 base + 40 surcharge) × 2 = 480 credits
let costs = request.calculate_costs();
assert_eq!(costs.base_credits_cost, 400);
assert_eq!(costs.video_reference_surcharge_cost, Some(80));
assert_eq!(costs.base_cost.kinovi_credits, 400);
assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(80));
assert_eq!(costs.total_cost.kinovi_credits, 480);
}
}
@@ -110,16 +110,16 @@ impl GenerateSeedance2p0FastRequest {
};
let duration = u64::from(self.duration_seconds);
let base_credits_cost = duration * credits_per_second * batch_multiplier;
let video_reference_surcharge_cost = if self.has_video_reference() {
let base_credits = duration * credits_per_second * batch_multiplier;
let maybe_video_reference_surcharge_credits = if self.has_video_reference() {
Some(duration * video_reference_surcharge_per_second * batch_multiplier)
} else {
None
};
KinoviSeedanceGenerationCost::from_base_and_surcharge(
base_credits_cost,
video_reference_surcharge_cost,
base_credits,
maybe_video_reference_surcharge_credits,
)
}
@@ -389,8 +389,8 @@ mod tests {
assert_eq!(costs.total_cost.usd_cents_rounded_up, 37);
assert_eq!(costs.total_cost.usd_cents_rounded_down, 36);
assert!((costs.total_cost.usd_cents_fractional - (7000.0 / 193.0)).abs() < 1e-9);
assert_eq!(costs.base_credits_cost, 70);
assert_eq!(costs.video_reference_surcharge_cost, None);
assert_eq!(costs.base_cost.kinovi_credits, 70);
assert!(costs.video_reference_surcharge_cost.is_none());
}
#[test]
@@ -401,8 +401,8 @@ mod tests {
assert_eq!(costs.total_cost.usd_cents_rounded_up, 73);
assert_eq!(costs.total_cost.usd_cents_rounded_down, 72);
assert!((costs.total_cost.usd_cents_fractional - (14000.0 / 193.0)).abs() < 1e-9);
assert_eq!(costs.base_credits_cost, 140);
assert_eq!(costs.video_reference_surcharge_cost, None);
assert_eq!(costs.base_cost.kinovi_credits, 140);
assert!(costs.video_reference_surcharge_cost.is_none());
}
#[test]
@@ -413,8 +413,8 @@ mod tests {
assert_eq!(costs.total_cost.usd_cents_rounded_up, 218);
assert_eq!(costs.total_cost.usd_cents_rounded_down, 217);
assert!((costs.total_cost.usd_cents_fractional - (42000.0 / 193.0)).abs() < 1e-9);
assert_eq!(costs.base_credits_cost, 420);
assert_eq!(costs.video_reference_surcharge_cost, None);
assert_eq!(costs.base_cost.kinovi_credits, 420);
assert!(costs.video_reference_surcharge_cost.is_none());
}
#[test]
@@ -425,8 +425,8 @@ mod tests {
assert_eq!(costs.total_cost.usd_cents_rounded_up, 109);
assert_eq!(costs.total_cost.usd_cents_rounded_down, 108);
assert!((costs.total_cost.usd_cents_fractional - (21000.0 / 193.0)).abs() < 1e-9);
assert_eq!(costs.base_credits_cost, 210);
assert_eq!(costs.video_reference_surcharge_cost, None);
assert_eq!(costs.base_cost.kinovi_credits, 210);
assert!(costs.video_reference_surcharge_cost.is_none());
}
#[test]
@@ -437,8 +437,8 @@ mod tests {
assert_eq!(costs.total_cost.usd_cents_rounded_up, 146);
assert_eq!(costs.total_cost.usd_cents_rounded_down, 145);
assert!((costs.total_cost.usd_cents_fractional - (28000.0 / 193.0)).abs() < 1e-9);
assert_eq!(costs.base_credits_cost, 280);
assert_eq!(costs.video_reference_surcharge_cost, None);
assert_eq!(costs.base_cost.kinovi_credits, 280);
assert!(costs.video_reference_surcharge_cost.is_none());
}
/// The deprecated shims return the corresponding struct fields.
@@ -487,8 +487,8 @@ mod tests {
for (make, duration, base, surcharge) in cases {
let costs = with_video_ref(make(*duration)).calculate_costs();
assert_eq!(costs.base_credits_cost, *base, "base for {duration}s");
assert_eq!(costs.video_reference_surcharge_cost, Some(*surcharge), "surcharge for {duration}s");
assert_eq!(costs.base_cost.kinovi_credits, *base, "base for {duration}s");
assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(*surcharge), "surcharge for {duration}s");
assert_eq!(costs.total_cost.kinovi_credits, base + surcharge, "total for {duration}s");
}
}
@@ -497,21 +497,39 @@ mod tests {
fn surcharge_includes_usd_cents() {
// 720p 5s + video ref = 140 + 30 = 170 credits; 17000/193 = 88.08 → 89¢
let costs = with_video_ref(r720(5)).calculate_costs();
assert_eq!(costs.base_credits_cost, 140);
assert_eq!(costs.video_reference_surcharge_cost, Some(30));
assert_eq!(costs.base_cost.kinovi_credits, 140);
assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(30));
assert_eq!(costs.total_cost.kinovi_credits, 170);
assert_eq!(costs.total_cost.usd_cents_rounded_up, 89);
assert_eq!(costs.total_cost.usd_cents_rounded_down, 88);
assert!((costs.total_cost.usd_cents_fractional - (17000.0 / 193.0)).abs() < 1e-9);
}
/// The base and surcharge parts each carry their own USD conversions.
#[test]
fn parts_have_their_own_usd_conversions() {
let costs = with_video_ref(r720(5)).calculate_costs();
// Base: 140 credits; 14000/193 = 72.54.
assert_eq!(costs.base_cost.usd_cents_rounded_up, 73);
assert_eq!(costs.base_cost.usd_cents_rounded_down, 72);
assert!((costs.base_cost.usd_cents_fractional - (14000.0 / 193.0)).abs() < 1e-9);
// Surcharge: 30 credits; 3000/193 = 15.54.
let surcharge = costs.video_reference_surcharge_cost.expect("should have surcharge");
assert_eq!(surcharge.kinovi_credits, 30);
assert_eq!(surcharge.usd_cents_rounded_up, 16);
assert_eq!(surcharge.usd_cents_rounded_down, 15);
assert!((surcharge.usd_cents_fractional - (3000.0 / 193.0)).abs() < 1e-9);
}
#[test]
fn empty_video_reference_list_has_no_surcharge() {
let mut request = r720(5);
request.reference_video_urls = Some(vec![]);
let costs = request.calculate_costs();
assert_eq!(costs.total_cost.kinovi_credits, 140);
assert_eq!(costs.video_reference_surcharge_cost, None);
assert!(costs.video_reference_surcharge_cost.is_none());
}
/// Surcharge is flat per generation regardless of how many reference
@@ -532,8 +550,8 @@ mod tests {
let request = with_video_ref(build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Two)));
// (140 base + 30 surcharge) × 2 = 340 credits
let costs = request.calculate_costs();
assert_eq!(costs.base_credits_cost, 280);
assert_eq!(costs.video_reference_surcharge_cost, Some(60));
assert_eq!(costs.base_cost.kinovi_credits, 280);
assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(60));
assert_eq!(costs.total_cost.kinovi_credits, 340);
}
}