diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/mod.rs index 4345044952..b48441ed7e 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/mod.rs @@ -24,4 +24,4 @@ pub mod support; mod seedance_2p0; -mod seedance_2p5_tests; +mod seedance_2p5; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/seedance_2p5/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/seedance_2p5/mod.rs new file mode 100644 index 0000000000..4a9b349637 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/seedance_2p5/mod.rs @@ -0,0 +1,3 @@ +//! Seedance 2.5 family pricing tests. + +mod seedance_2p5_tests; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/seedance_2p5/seedance_2p5_tests.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/seedance_2p5/seedance_2p5_tests.rs new file mode 100644 index 0000000000..3602ca12ad --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/seedance_2p5/seedance_2p5_tests.rs @@ -0,0 +1,967 @@ +//! Seedance 2.5 family pricing tests: 2.5 and 2.5 Ultra through the real +//! generate endpoint against the test database, asserting exact wallet +//! debits — including the reference-video input-seconds dimension. +//! +//! Rate cards (ceil-rounded once): +//! - Seedance 2.5: 480p 11.76954733 ¢/s, 720p 26.70781893 ¢/s; +//! with video refs 480p 7.24279835 ¢/s, 720p 15.84362140 ¢/s over +//! (output + input) seconds +//! - Seedance 2.5 Ultra: 480p 13.90946502 ¢/s, 720p 31.56378601 ¢/s; +//! with video refs 480p 8.55967078 ¢/s, 720p 18.72427984 ¢/s +//! +//! Only 480p and 720p are offered (1080p/4K downgrade to 720p), durations +//! run 4-30s, and every request generates a single video regardless of the +//! requested batch size. + +use enums::common::generation::common_resolution::CommonResolution; +use enums::common::generation::common_video_model::CommonVideoModel; + +use crate::http_server::endpoints::omni_gen::generate::video::tests::support::{ + assert_reference_video_charge_then_refund, assert_successful_generation_charges, + assert_variant_charges_premium, Batch, CreditsDelta, ExpectedCredits, Seconds, TestHarness, +}; + +// ── Seedance 2.5 ── +mod seedance_2p5 { + use super::*; + + /// Every duration 4-30s at every resolution, single video. + /// Seedance 2.5 offers 480p and 720p; 1080p and 4K downgrade to 720p (and price accordingly). + #[tokio::test] + #[cfg_attr(feature = "skip_database_tests", ignore)] + async fn charges_every_duration_at_every_resolution() { + let harness = TestHarness::create().await; + + let cases: &[(Option, Seconds, Batch, ExpectedCredits)] = &[ + (Some(CommonResolution::FourEightyP), Seconds(4), Batch(1), ExpectedCredits(48)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(59)), + (Some(CommonResolution::FourEightyP), Seconds(6), Batch(1), ExpectedCredits(71)), + (Some(CommonResolution::FourEightyP), Seconds(7), Batch(1), ExpectedCredits(83)), + (Some(CommonResolution::FourEightyP), Seconds(8), Batch(1), ExpectedCredits(95)), + (Some(CommonResolution::FourEightyP), Seconds(9), Batch(1), ExpectedCredits(106)), + (Some(CommonResolution::FourEightyP), Seconds(10), Batch(1), ExpectedCredits(118)), + (Some(CommonResolution::FourEightyP), Seconds(11), Batch(1), ExpectedCredits(130)), + (Some(CommonResolution::FourEightyP), Seconds(12), Batch(1), ExpectedCredits(142)), + (Some(CommonResolution::FourEightyP), Seconds(13), Batch(1), ExpectedCredits(154)), + (Some(CommonResolution::FourEightyP), Seconds(14), Batch(1), ExpectedCredits(165)), + (Some(CommonResolution::FourEightyP), Seconds(15), Batch(1), ExpectedCredits(177)), + (Some(CommonResolution::FourEightyP), Seconds(16), Batch(1), ExpectedCredits(189)), + (Some(CommonResolution::FourEightyP), Seconds(17), Batch(1), ExpectedCredits(201)), + (Some(CommonResolution::FourEightyP), Seconds(18), Batch(1), ExpectedCredits(212)), + (Some(CommonResolution::FourEightyP), Seconds(19), Batch(1), ExpectedCredits(224)), + (Some(CommonResolution::FourEightyP), Seconds(20), Batch(1), ExpectedCredits(236)), + (Some(CommonResolution::FourEightyP), Seconds(21), Batch(1), ExpectedCredits(248)), + (Some(CommonResolution::FourEightyP), Seconds(22), Batch(1), ExpectedCredits(259)), + (Some(CommonResolution::FourEightyP), Seconds(23), Batch(1), ExpectedCredits(271)), + (Some(CommonResolution::FourEightyP), Seconds(24), Batch(1), ExpectedCredits(283)), + (Some(CommonResolution::FourEightyP), Seconds(25), Batch(1), ExpectedCredits(295)), + (Some(CommonResolution::FourEightyP), Seconds(26), Batch(1), ExpectedCredits(307)), + (Some(CommonResolution::FourEightyP), Seconds(27), Batch(1), ExpectedCredits(318)), + (Some(CommonResolution::FourEightyP), Seconds(28), Batch(1), ExpectedCredits(330)), + (Some(CommonResolution::FourEightyP), Seconds(29), Batch(1), ExpectedCredits(342)), + (Some(CommonResolution::FourEightyP), Seconds(30), Batch(1), ExpectedCredits(354)), + (Some(CommonResolution::SevenTwentyP), Seconds(4), Batch(1), ExpectedCredits(107)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(134)), + (Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(1), ExpectedCredits(161)), + (Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(1), ExpectedCredits(187)), + (Some(CommonResolution::SevenTwentyP), Seconds(8), Batch(1), ExpectedCredits(214)), + (Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(1), ExpectedCredits(241)), + (Some(CommonResolution::SevenTwentyP), Seconds(10), Batch(1), ExpectedCredits(268)), + (Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(1), ExpectedCredits(294)), + (Some(CommonResolution::SevenTwentyP), Seconds(12), Batch(1), ExpectedCredits(321)), + (Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(1), ExpectedCredits(348)), + (Some(CommonResolution::SevenTwentyP), Seconds(14), Batch(1), ExpectedCredits(374)), + (Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(1), ExpectedCredits(401)), + (Some(CommonResolution::SevenTwentyP), Seconds(16), Batch(1), ExpectedCredits(428)), + (Some(CommonResolution::SevenTwentyP), Seconds(17), Batch(1), ExpectedCredits(455)), + (Some(CommonResolution::SevenTwentyP), Seconds(18), Batch(1), ExpectedCredits(481)), + (Some(CommonResolution::SevenTwentyP), Seconds(19), Batch(1), ExpectedCredits(508)), + (Some(CommonResolution::SevenTwentyP), Seconds(20), Batch(1), ExpectedCredits(535)), + (Some(CommonResolution::SevenTwentyP), Seconds(21), Batch(1), ExpectedCredits(561)), + (Some(CommonResolution::SevenTwentyP), Seconds(22), Batch(1), ExpectedCredits(588)), + (Some(CommonResolution::SevenTwentyP), Seconds(23), Batch(1), ExpectedCredits(615)), + (Some(CommonResolution::SevenTwentyP), Seconds(24), Batch(1), ExpectedCredits(641)), + (Some(CommonResolution::SevenTwentyP), Seconds(25), Batch(1), ExpectedCredits(668)), + (Some(CommonResolution::SevenTwentyP), Seconds(26), Batch(1), ExpectedCredits(695)), + (Some(CommonResolution::SevenTwentyP), Seconds(27), Batch(1), ExpectedCredits(722)), + (Some(CommonResolution::SevenTwentyP), Seconds(28), Batch(1), ExpectedCredits(748)), + (Some(CommonResolution::SevenTwentyP), Seconds(29), Batch(1), ExpectedCredits(775)), + (Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(802)), + (Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(107)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(134)), + (Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(161)), + (Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(187)), + (Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(214)), + (Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(241)), + (Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(268)), + (Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(294)), + (Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(321)), + (Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(348)), + (Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(374)), + (Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(401)), + (Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(428)), + (Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(455)), + (Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(481)), + (Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(508)), + (Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(535)), + (Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(561)), + (Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(588)), + (Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(615)), + (Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(641)), + (Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(668)), + (Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(695)), + (Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(722)), + (Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(748)), + (Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(775)), + (Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(802)), + (Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(107)), + (Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(134)), + (Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(161)), + (Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(187)), + (Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(214)), + (Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(241)), + (Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(268)), + (Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(294)), + (Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(321)), + (Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(348)), + (Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(374)), + (Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(401)), + (Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(428)), + (Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(455)), + (Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(481)), + (Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(508)), + (Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(535)), + (Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(561)), + (Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(588)), + (Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(615)), + (Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(641)), + (Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(668)), + (Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(695)), + (Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(722)), + (Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(748)), + (Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(775)), + (Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(802)), + ]; + + for (resolution, seconds, batch, expected) in cases { + assert_successful_generation_charges( + &harness, CommonVideoModel::Seedance2p5, *resolution, *seconds, *batch, *expected, + ).await; + } + } + + /// Every batch size 1-10 at every resolution (5s). + /// Seedance 2.5 generates a single video per request: every requested batch size 1-10 bills exactly one video. + /// Seedance 2.5 offers 480p and 720p; 1080p and 4K downgrade to 720p (and price accordingly). + #[tokio::test] + #[cfg_attr(feature = "skip_database_tests", ignore)] + async fn charges_every_batch_size_at_every_resolution() { + let harness = TestHarness::create().await; + + let cases: &[(Option, Seconds, Batch, ExpectedCredits)] = &[ + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(59)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(59)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(59)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(59)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(59)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(59)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(59)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(59)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(59)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(59)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(134)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(134)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(134)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(134)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(134)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(134)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(134)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(134)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(134)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(134)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(134)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(134)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(134)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(134)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(134)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(134)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(134)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(134)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(134)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(134)), + (Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(134)), + (Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(134)), + (Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(134)), + (Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(134)), + (Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(134)), + (Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(134)), + (Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(134)), + (Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(134)), + (Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(134)), + (Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(134)), + ]; + + for (resolution, seconds, batch, expected) in cases { + assert_successful_generation_charges( + &harness, CommonVideoModel::Seedance2p5, *resolution, *seconds, *batch, *expected, + ).await; + } + } + + /// Spot checks across duration x batch x resolution (batch always bills as one video). + #[tokio::test] + #[cfg_attr(feature = "skip_database_tests", ignore)] + async fn charges_spot_checked_combinations() { + let harness = TestHarness::create().await; + + let cases: &[(Option, Seconds, Batch, ExpectedCredits)] = &[ + (Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(71)), + (Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(83)), + (Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(106)), + (Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(130)), + (Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(154)), + (Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(177)), + (Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(161)), + (Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(187)), + (Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(241)), + (Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(294)), + (Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(348)), + (Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(401)), + (Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(161)), + (Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(187)), + (Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(241)), + (Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(294)), + (Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(348)), + (Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(401)), + (Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(161)), + (Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(187)), + (Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(241)), + (Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(294)), + (Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(348)), + (Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(401)), + ]; + + for (resolution, seconds, batch, expected) in cases { + assert_successful_generation_charges( + &harness, CommonVideoModel::Seedance2p5, *resolution, *seconds, *batch, *expected, + ).await; + } + } + + /// With video references the per-second rate drops but billed seconds = output duration + probed input seconds. The fixture media is unreachable, so the probe fails open to the worst-case 30 input seconds, the upload then fails, and the exact charge is asserted on the refunded ledger entry. + /// Every duration 4-30s at every resolution. + #[tokio::test] + #[cfg_attr(feature = "skip_database_tests", ignore)] + async fn video_references_charge_every_duration_at_every_resolution() { + let harness = TestHarness::create().await; + + let cases: &[(Option, Seconds, Batch, ExpectedCredits)] = &[ + (Some(CommonResolution::FourEightyP), Seconds(4), Batch(1), ExpectedCredits(247)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(254)), + (Some(CommonResolution::FourEightyP), Seconds(6), Batch(1), ExpectedCredits(261)), + (Some(CommonResolution::FourEightyP), Seconds(7), Batch(1), ExpectedCredits(268)), + (Some(CommonResolution::FourEightyP), Seconds(8), Batch(1), ExpectedCredits(276)), + (Some(CommonResolution::FourEightyP), Seconds(9), Batch(1), ExpectedCredits(283)), + (Some(CommonResolution::FourEightyP), Seconds(10), Batch(1), ExpectedCredits(290)), + (Some(CommonResolution::FourEightyP), Seconds(11), Batch(1), ExpectedCredits(297)), + (Some(CommonResolution::FourEightyP), Seconds(12), Batch(1), ExpectedCredits(305)), + (Some(CommonResolution::FourEightyP), Seconds(13), Batch(1), ExpectedCredits(312)), + (Some(CommonResolution::FourEightyP), Seconds(14), Batch(1), ExpectedCredits(319)), + (Some(CommonResolution::FourEightyP), Seconds(15), Batch(1), ExpectedCredits(326)), + (Some(CommonResolution::FourEightyP), Seconds(16), Batch(1), ExpectedCredits(334)), + (Some(CommonResolution::FourEightyP), Seconds(17), Batch(1), ExpectedCredits(341)), + (Some(CommonResolution::FourEightyP), Seconds(18), Batch(1), ExpectedCredits(348)), + (Some(CommonResolution::FourEightyP), Seconds(19), Batch(1), ExpectedCredits(355)), + (Some(CommonResolution::FourEightyP), Seconds(20), Batch(1), ExpectedCredits(363)), + (Some(CommonResolution::FourEightyP), Seconds(21), Batch(1), ExpectedCredits(370)), + (Some(CommonResolution::FourEightyP), Seconds(22), Batch(1), ExpectedCredits(377)), + (Some(CommonResolution::FourEightyP), Seconds(23), Batch(1), ExpectedCredits(384)), + (Some(CommonResolution::FourEightyP), Seconds(24), Batch(1), ExpectedCredits(392)), + (Some(CommonResolution::FourEightyP), Seconds(25), Batch(1), ExpectedCredits(399)), + (Some(CommonResolution::FourEightyP), Seconds(26), Batch(1), ExpectedCredits(406)), + (Some(CommonResolution::FourEightyP), Seconds(27), Batch(1), ExpectedCredits(413)), + (Some(CommonResolution::FourEightyP), Seconds(28), Batch(1), ExpectedCredits(421)), + (Some(CommonResolution::FourEightyP), Seconds(29), Batch(1), ExpectedCredits(428)), + (Some(CommonResolution::FourEightyP), Seconds(30), Batch(1), ExpectedCredits(435)), + (Some(CommonResolution::SevenTwentyP), Seconds(4), Batch(1), ExpectedCredits(539)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(555)), + (Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(1), ExpectedCredits(571)), + (Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(1), ExpectedCredits(587)), + (Some(CommonResolution::SevenTwentyP), Seconds(8), Batch(1), ExpectedCredits(603)), + (Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(1), ExpectedCredits(618)), + (Some(CommonResolution::SevenTwentyP), Seconds(10), Batch(1), ExpectedCredits(634)), + (Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(1), ExpectedCredits(650)), + (Some(CommonResolution::SevenTwentyP), Seconds(12), Batch(1), ExpectedCredits(666)), + (Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(1), ExpectedCredits(682)), + (Some(CommonResolution::SevenTwentyP), Seconds(14), Batch(1), ExpectedCredits(698)), + (Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(1), ExpectedCredits(713)), + (Some(CommonResolution::SevenTwentyP), Seconds(16), Batch(1), ExpectedCredits(729)), + (Some(CommonResolution::SevenTwentyP), Seconds(17), Batch(1), ExpectedCredits(745)), + (Some(CommonResolution::SevenTwentyP), Seconds(18), Batch(1), ExpectedCredits(761)), + (Some(CommonResolution::SevenTwentyP), Seconds(19), Batch(1), ExpectedCredits(777)), + (Some(CommonResolution::SevenTwentyP), Seconds(20), Batch(1), ExpectedCredits(793)), + (Some(CommonResolution::SevenTwentyP), Seconds(21), Batch(1), ExpectedCredits(809)), + (Some(CommonResolution::SevenTwentyP), Seconds(22), Batch(1), ExpectedCredits(824)), + (Some(CommonResolution::SevenTwentyP), Seconds(23), Batch(1), ExpectedCredits(840)), + (Some(CommonResolution::SevenTwentyP), Seconds(24), Batch(1), ExpectedCredits(856)), + (Some(CommonResolution::SevenTwentyP), Seconds(25), Batch(1), ExpectedCredits(872)), + (Some(CommonResolution::SevenTwentyP), Seconds(26), Batch(1), ExpectedCredits(888)), + (Some(CommonResolution::SevenTwentyP), Seconds(27), Batch(1), ExpectedCredits(904)), + (Some(CommonResolution::SevenTwentyP), Seconds(28), Batch(1), ExpectedCredits(919)), + (Some(CommonResolution::SevenTwentyP), Seconds(29), Batch(1), ExpectedCredits(935)), + (Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(951)), + (Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(539)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(555)), + (Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(571)), + (Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(587)), + (Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(603)), + (Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(618)), + (Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(634)), + (Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(650)), + (Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(666)), + (Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(682)), + (Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(698)), + (Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(713)), + (Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(729)), + (Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(745)), + (Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(761)), + (Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(777)), + (Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(793)), + (Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(809)), + (Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(824)), + (Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(840)), + (Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(856)), + (Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(872)), + (Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(888)), + (Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(904)), + (Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(919)), + (Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(935)), + (Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(951)), + (Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(539)), + (Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(555)), + (Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(571)), + (Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(587)), + (Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(603)), + (Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(618)), + (Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(634)), + (Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(650)), + (Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(666)), + (Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(682)), + (Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(698)), + (Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(713)), + (Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(729)), + (Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(745)), + (Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(761)), + (Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(777)), + (Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(793)), + (Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(809)), + (Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(824)), + (Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(840)), + (Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(856)), + (Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(872)), + (Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(888)), + (Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(904)), + (Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(919)), + (Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(935)), + (Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(951)), + ]; + + for (resolution, seconds, batch, expected) in cases { + assert_reference_video_charge_then_refund( + &harness, CommonVideoModel::Seedance2p5, *resolution, *seconds, *batch, *expected, + ).await; + } + } + + /// With video references the per-second rate drops but billed seconds = output duration + probed input seconds. The fixture media is unreachable, so the probe fails open to the worst-case 30 input seconds, the upload then fails, and the exact charge is asserted on the refunded ledger entry. + /// Every batch size 1-10 at every resolution (5s). + /// Seedance 2.5 generates a single video per request: every requested batch size 1-10 bills exactly one video. + #[tokio::test] + #[cfg_attr(feature = "skip_database_tests", ignore)] + async fn video_references_charge_every_batch_size_at_every_resolution() { + let harness = TestHarness::create().await; + + let cases: &[(Option, Seconds, Batch, ExpectedCredits)] = &[ + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(254)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(254)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(254)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(254)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(254)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(254)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(254)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(254)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(254)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(254)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(555)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(555)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(555)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(555)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(555)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(555)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(555)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(555)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(555)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(555)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(555)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(555)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(555)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(555)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(555)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(555)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(555)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(555)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(555)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(555)), + (Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(555)), + (Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(555)), + (Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(555)), + (Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(555)), + (Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(555)), + (Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(555)), + (Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(555)), + (Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(555)), + (Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(555)), + (Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(555)), + ]; + + for (resolution, seconds, batch, expected) in cases { + assert_reference_video_charge_then_refund( + &harness, CommonVideoModel::Seedance2p5, *resolution, *seconds, *batch, *expected, + ).await; + } + } + + /// With video references the per-second rate drops but billed seconds = output duration + probed input seconds. The fixture media is unreachable, so the probe fails open to the worst-case 30 input seconds, the upload then fails, and the exact charge is asserted on the refunded ledger entry. + /// Spot checks across duration x batch x resolution. + #[tokio::test] + #[cfg_attr(feature = "skip_database_tests", ignore)] + async fn video_references_charge_spot_checked_combinations() { + let harness = TestHarness::create().await; + + let cases: &[(Option, Seconds, Batch, ExpectedCredits)] = &[ + (Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(261)), + (Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(268)), + (Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(283)), + (Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(297)), + (Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(312)), + (Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(326)), + (Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(571)), + (Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(587)), + (Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(618)), + (Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(650)), + (Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(682)), + (Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(713)), + (Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(571)), + (Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(587)), + (Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(618)), + (Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(650)), + (Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(682)), + (Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(713)), + (Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(571)), + (Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(587)), + (Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(618)), + (Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(650)), + (Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(682)), + (Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(713)), + ]; + + for (resolution, seconds, batch, expected) in cases { + assert_reference_video_charge_then_refund( + &harness, CommonVideoModel::Seedance2p5, *resolution, *seconds, *batch, *expected, + ).await; + } + } +} + +// ── Seedance 2.5 Ultra ── +// Fulfilled by Seedance 2.5 (same execution request), priced as itself and +// routed to a different provider account. If the pipeline ever collapses +// the model before billing, every row here fails with the regular 2.5 +// numbers. +mod seedance_2p5_u { + use super::*; + + /// Every duration 4-30s at every resolution, single video. + /// Seedance 2.5 offers 480p and 720p; 1080p and 4K downgrade to 720p (and price accordingly). + #[tokio::test] + #[cfg_attr(feature = "skip_database_tests", ignore)] + async fn charges_every_duration_at_every_resolution() { + let harness = TestHarness::create().await; + + let cases: &[(Option, Seconds, Batch, ExpectedCredits)] = &[ + (Some(CommonResolution::FourEightyP), Seconds(4), Batch(1), ExpectedCredits(56)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(70)), + (Some(CommonResolution::FourEightyP), Seconds(6), Batch(1), ExpectedCredits(84)), + (Some(CommonResolution::FourEightyP), Seconds(7), Batch(1), ExpectedCredits(98)), + (Some(CommonResolution::FourEightyP), Seconds(8), Batch(1), ExpectedCredits(112)), + (Some(CommonResolution::FourEightyP), Seconds(9), Batch(1), ExpectedCredits(126)), + (Some(CommonResolution::FourEightyP), Seconds(10), Batch(1), ExpectedCredits(140)), + (Some(CommonResolution::FourEightyP), Seconds(11), Batch(1), ExpectedCredits(154)), + (Some(CommonResolution::FourEightyP), Seconds(12), Batch(1), ExpectedCredits(167)), + (Some(CommonResolution::FourEightyP), Seconds(13), Batch(1), ExpectedCredits(181)), + (Some(CommonResolution::FourEightyP), Seconds(14), Batch(1), ExpectedCredits(195)), + (Some(CommonResolution::FourEightyP), Seconds(15), Batch(1), ExpectedCredits(209)), + (Some(CommonResolution::FourEightyP), Seconds(16), Batch(1), ExpectedCredits(223)), + (Some(CommonResolution::FourEightyP), Seconds(17), Batch(1), ExpectedCredits(237)), + (Some(CommonResolution::FourEightyP), Seconds(18), Batch(1), ExpectedCredits(251)), + (Some(CommonResolution::FourEightyP), Seconds(19), Batch(1), ExpectedCredits(265)), + (Some(CommonResolution::FourEightyP), Seconds(20), Batch(1), ExpectedCredits(279)), + (Some(CommonResolution::FourEightyP), Seconds(21), Batch(1), ExpectedCredits(293)), + (Some(CommonResolution::FourEightyP), Seconds(22), Batch(1), ExpectedCredits(307)), + (Some(CommonResolution::FourEightyP), Seconds(23), Batch(1), ExpectedCredits(320)), + (Some(CommonResolution::FourEightyP), Seconds(24), Batch(1), ExpectedCredits(334)), + (Some(CommonResolution::FourEightyP), Seconds(25), Batch(1), ExpectedCredits(348)), + (Some(CommonResolution::FourEightyP), Seconds(26), Batch(1), ExpectedCredits(362)), + (Some(CommonResolution::FourEightyP), Seconds(27), Batch(1), ExpectedCredits(376)), + (Some(CommonResolution::FourEightyP), Seconds(28), Batch(1), ExpectedCredits(390)), + (Some(CommonResolution::FourEightyP), Seconds(29), Batch(1), ExpectedCredits(404)), + (Some(CommonResolution::FourEightyP), Seconds(30), Batch(1), ExpectedCredits(418)), + (Some(CommonResolution::SevenTwentyP), Seconds(4), Batch(1), ExpectedCredits(127)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(158)), + (Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(1), ExpectedCredits(190)), + (Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(1), ExpectedCredits(221)), + (Some(CommonResolution::SevenTwentyP), Seconds(8), Batch(1), ExpectedCredits(253)), + (Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(1), ExpectedCredits(285)), + (Some(CommonResolution::SevenTwentyP), Seconds(10), Batch(1), ExpectedCredits(316)), + (Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(1), ExpectedCredits(348)), + (Some(CommonResolution::SevenTwentyP), Seconds(12), Batch(1), ExpectedCredits(379)), + (Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(1), ExpectedCredits(411)), + (Some(CommonResolution::SevenTwentyP), Seconds(14), Batch(1), ExpectedCredits(442)), + (Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(1), ExpectedCredits(474)), + (Some(CommonResolution::SevenTwentyP), Seconds(16), Batch(1), ExpectedCredits(506)), + (Some(CommonResolution::SevenTwentyP), Seconds(17), Batch(1), ExpectedCredits(537)), + (Some(CommonResolution::SevenTwentyP), Seconds(18), Batch(1), ExpectedCredits(569)), + (Some(CommonResolution::SevenTwentyP), Seconds(19), Batch(1), ExpectedCredits(600)), + (Some(CommonResolution::SevenTwentyP), Seconds(20), Batch(1), ExpectedCredits(632)), + (Some(CommonResolution::SevenTwentyP), Seconds(21), Batch(1), ExpectedCredits(663)), + (Some(CommonResolution::SevenTwentyP), Seconds(22), Batch(1), ExpectedCredits(695)), + (Some(CommonResolution::SevenTwentyP), Seconds(23), Batch(1), ExpectedCredits(726)), + (Some(CommonResolution::SevenTwentyP), Seconds(24), Batch(1), ExpectedCredits(758)), + (Some(CommonResolution::SevenTwentyP), Seconds(25), Batch(1), ExpectedCredits(790)), + (Some(CommonResolution::SevenTwentyP), Seconds(26), Batch(1), ExpectedCredits(821)), + (Some(CommonResolution::SevenTwentyP), Seconds(27), Batch(1), ExpectedCredits(853)), + (Some(CommonResolution::SevenTwentyP), Seconds(28), Batch(1), ExpectedCredits(884)), + (Some(CommonResolution::SevenTwentyP), Seconds(29), Batch(1), ExpectedCredits(916)), + (Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(947)), + (Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(127)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(158)), + (Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(190)), + (Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(221)), + (Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(253)), + (Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(285)), + (Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(316)), + (Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(348)), + (Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(379)), + (Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(411)), + (Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(442)), + (Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(474)), + (Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(506)), + (Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(537)), + (Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(569)), + (Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(600)), + (Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(632)), + (Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(663)), + (Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(695)), + (Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(726)), + (Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(758)), + (Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(790)), + (Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(821)), + (Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(853)), + (Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(884)), + (Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(916)), + (Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(947)), + (Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(127)), + (Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(158)), + (Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(190)), + (Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(221)), + (Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(253)), + (Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(285)), + (Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(316)), + (Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(348)), + (Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(379)), + (Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(411)), + (Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(442)), + (Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(474)), + (Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(506)), + (Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(537)), + (Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(569)), + (Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(600)), + (Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(632)), + (Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(663)), + (Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(695)), + (Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(726)), + (Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(758)), + (Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(790)), + (Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(821)), + (Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(853)), + (Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(884)), + (Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(916)), + (Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(947)), + ]; + + for (resolution, seconds, batch, expected) in cases { + assert_successful_generation_charges( + &harness, CommonVideoModel::Seedance2p5Ultra, *resolution, *seconds, *batch, *expected, + ).await; + } + } + + /// Every batch size 1-10 at every resolution (5s). + /// Seedance 2.5 generates a single video per request: every requested batch size 1-10 bills exactly one video. + /// Seedance 2.5 offers 480p and 720p; 1080p and 4K downgrade to 720p (and price accordingly). + #[tokio::test] + #[cfg_attr(feature = "skip_database_tests", ignore)] + async fn charges_every_batch_size_at_every_resolution() { + let harness = TestHarness::create().await; + + let cases: &[(Option, Seconds, Batch, ExpectedCredits)] = &[ + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(70)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(70)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(70)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(70)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(70)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(70)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(70)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(70)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(70)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(70)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(158)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(158)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(158)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(158)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(158)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(158)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(158)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(158)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(158)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(158)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(158)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(158)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(158)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(158)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(158)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(158)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(158)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(158)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(158)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(158)), + (Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(158)), + (Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(158)), + (Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(158)), + (Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(158)), + (Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(158)), + (Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(158)), + (Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(158)), + (Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(158)), + (Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(158)), + (Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(158)), + ]; + + for (resolution, seconds, batch, expected) in cases { + assert_successful_generation_charges( + &harness, CommonVideoModel::Seedance2p5Ultra, *resolution, *seconds, *batch, *expected, + ).await; + } + } + + /// Spot checks across duration x batch x resolution (batch always bills as one video). + #[tokio::test] + #[cfg_attr(feature = "skip_database_tests", ignore)] + async fn charges_spot_checked_combinations() { + let harness = TestHarness::create().await; + + let cases: &[(Option, Seconds, Batch, ExpectedCredits)] = &[ + (Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(84)), + (Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(98)), + (Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(126)), + (Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(154)), + (Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(181)), + (Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(209)), + (Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(190)), + (Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(221)), + (Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(285)), + (Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(348)), + (Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(411)), + (Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(474)), + (Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(190)), + (Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(221)), + (Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(285)), + (Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(348)), + (Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(411)), + (Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(474)), + (Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(190)), + (Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(221)), + (Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(285)), + (Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(348)), + (Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(411)), + (Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(474)), + ]; + + for (resolution, seconds, batch, expected) in cases { + assert_successful_generation_charges( + &harness, CommonVideoModel::Seedance2p5Ultra, *resolution, *seconds, *batch, *expected, + ).await; + } + } + + /// With video references the per-second rate drops but billed seconds = output duration + probed input seconds. The fixture media is unreachable, so the probe fails open to the worst-case 30 input seconds, the upload then fails, and the exact charge is asserted on the refunded ledger entry. + /// Every duration 4-30s at every resolution. + #[tokio::test] + #[cfg_attr(feature = "skip_database_tests", ignore)] + async fn video_references_charge_every_duration_at_every_resolution() { + let harness = TestHarness::create().await; + + let cases: &[(Option, Seconds, Batch, ExpectedCredits)] = &[ + (Some(CommonResolution::FourEightyP), Seconds(4), Batch(1), ExpectedCredits(292)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(300)), + (Some(CommonResolution::FourEightyP), Seconds(6), Batch(1), ExpectedCredits(309)), + (Some(CommonResolution::FourEightyP), Seconds(7), Batch(1), ExpectedCredits(317)), + (Some(CommonResolution::FourEightyP), Seconds(8), Batch(1), ExpectedCredits(326)), + (Some(CommonResolution::FourEightyP), Seconds(9), Batch(1), ExpectedCredits(334)), + (Some(CommonResolution::FourEightyP), Seconds(10), Batch(1), ExpectedCredits(343)), + (Some(CommonResolution::FourEightyP), Seconds(11), Batch(1), ExpectedCredits(351)), + (Some(CommonResolution::FourEightyP), Seconds(12), Batch(1), ExpectedCredits(360)), + (Some(CommonResolution::FourEightyP), Seconds(13), Batch(1), ExpectedCredits(369)), + (Some(CommonResolution::FourEightyP), Seconds(14), Batch(1), ExpectedCredits(377)), + (Some(CommonResolution::FourEightyP), Seconds(15), Batch(1), ExpectedCredits(386)), + (Some(CommonResolution::FourEightyP), Seconds(16), Batch(1), ExpectedCredits(394)), + (Some(CommonResolution::FourEightyP), Seconds(17), Batch(1), ExpectedCredits(403)), + (Some(CommonResolution::FourEightyP), Seconds(18), Batch(1), ExpectedCredits(411)), + (Some(CommonResolution::FourEightyP), Seconds(19), Batch(1), ExpectedCredits(420)), + (Some(CommonResolution::FourEightyP), Seconds(20), Batch(1), ExpectedCredits(428)), + (Some(CommonResolution::FourEightyP), Seconds(21), Batch(1), ExpectedCredits(437)), + (Some(CommonResolution::FourEightyP), Seconds(22), Batch(1), ExpectedCredits(446)), + (Some(CommonResolution::FourEightyP), Seconds(23), Batch(1), ExpectedCredits(454)), + (Some(CommonResolution::FourEightyP), Seconds(24), Batch(1), ExpectedCredits(463)), + (Some(CommonResolution::FourEightyP), Seconds(25), Batch(1), ExpectedCredits(471)), + (Some(CommonResolution::FourEightyP), Seconds(26), Batch(1), ExpectedCredits(480)), + (Some(CommonResolution::FourEightyP), Seconds(27), Batch(1), ExpectedCredits(488)), + (Some(CommonResolution::FourEightyP), Seconds(28), Batch(1), ExpectedCredits(497)), + (Some(CommonResolution::FourEightyP), Seconds(29), Batch(1), ExpectedCredits(506)), + (Some(CommonResolution::FourEightyP), Seconds(30), Batch(1), ExpectedCredits(514)), + (Some(CommonResolution::SevenTwentyP), Seconds(4), Batch(1), ExpectedCredits(637)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(656)), + (Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(1), ExpectedCredits(675)), + (Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(1), ExpectedCredits(693)), + (Some(CommonResolution::SevenTwentyP), Seconds(8), Batch(1), ExpectedCredits(712)), + (Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(1), ExpectedCredits(731)), + (Some(CommonResolution::SevenTwentyP), Seconds(10), Batch(1), ExpectedCredits(749)), + (Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(1), ExpectedCredits(768)), + (Some(CommonResolution::SevenTwentyP), Seconds(12), Batch(1), ExpectedCredits(787)), + (Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(1), ExpectedCredits(806)), + (Some(CommonResolution::SevenTwentyP), Seconds(14), Batch(1), ExpectedCredits(824)), + (Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(1), ExpectedCredits(843)), + (Some(CommonResolution::SevenTwentyP), Seconds(16), Batch(1), ExpectedCredits(862)), + (Some(CommonResolution::SevenTwentyP), Seconds(17), Batch(1), ExpectedCredits(881)), + (Some(CommonResolution::SevenTwentyP), Seconds(18), Batch(1), ExpectedCredits(899)), + (Some(CommonResolution::SevenTwentyP), Seconds(19), Batch(1), ExpectedCredits(918)), + (Some(CommonResolution::SevenTwentyP), Seconds(20), Batch(1), ExpectedCredits(937)), + (Some(CommonResolution::SevenTwentyP), Seconds(21), Batch(1), ExpectedCredits(955)), + (Some(CommonResolution::SevenTwentyP), Seconds(22), Batch(1), ExpectedCredits(974)), + (Some(CommonResolution::SevenTwentyP), Seconds(23), Batch(1), ExpectedCredits(993)), + (Some(CommonResolution::SevenTwentyP), Seconds(24), Batch(1), ExpectedCredits(1012)), + (Some(CommonResolution::SevenTwentyP), Seconds(25), Batch(1), ExpectedCredits(1030)), + (Some(CommonResolution::SevenTwentyP), Seconds(26), Batch(1), ExpectedCredits(1049)), + (Some(CommonResolution::SevenTwentyP), Seconds(27), Batch(1), ExpectedCredits(1068)), + (Some(CommonResolution::SevenTwentyP), Seconds(28), Batch(1), ExpectedCredits(1087)), + (Some(CommonResolution::SevenTwentyP), Seconds(29), Batch(1), ExpectedCredits(1105)), + (Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(1124)), + (Some(CommonResolution::TenEightyP), Seconds(4), Batch(1), ExpectedCredits(637)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(656)), + (Some(CommonResolution::TenEightyP), Seconds(6), Batch(1), ExpectedCredits(675)), + (Some(CommonResolution::TenEightyP), Seconds(7), Batch(1), ExpectedCredits(693)), + (Some(CommonResolution::TenEightyP), Seconds(8), Batch(1), ExpectedCredits(712)), + (Some(CommonResolution::TenEightyP), Seconds(9), Batch(1), ExpectedCredits(731)), + (Some(CommonResolution::TenEightyP), Seconds(10), Batch(1), ExpectedCredits(749)), + (Some(CommonResolution::TenEightyP), Seconds(11), Batch(1), ExpectedCredits(768)), + (Some(CommonResolution::TenEightyP), Seconds(12), Batch(1), ExpectedCredits(787)), + (Some(CommonResolution::TenEightyP), Seconds(13), Batch(1), ExpectedCredits(806)), + (Some(CommonResolution::TenEightyP), Seconds(14), Batch(1), ExpectedCredits(824)), + (Some(CommonResolution::TenEightyP), Seconds(15), Batch(1), ExpectedCredits(843)), + (Some(CommonResolution::TenEightyP), Seconds(16), Batch(1), ExpectedCredits(862)), + (Some(CommonResolution::TenEightyP), Seconds(17), Batch(1), ExpectedCredits(881)), + (Some(CommonResolution::TenEightyP), Seconds(18), Batch(1), ExpectedCredits(899)), + (Some(CommonResolution::TenEightyP), Seconds(19), Batch(1), ExpectedCredits(918)), + (Some(CommonResolution::TenEightyP), Seconds(20), Batch(1), ExpectedCredits(937)), + (Some(CommonResolution::TenEightyP), Seconds(21), Batch(1), ExpectedCredits(955)), + (Some(CommonResolution::TenEightyP), Seconds(22), Batch(1), ExpectedCredits(974)), + (Some(CommonResolution::TenEightyP), Seconds(23), Batch(1), ExpectedCredits(993)), + (Some(CommonResolution::TenEightyP), Seconds(24), Batch(1), ExpectedCredits(1012)), + (Some(CommonResolution::TenEightyP), Seconds(25), Batch(1), ExpectedCredits(1030)), + (Some(CommonResolution::TenEightyP), Seconds(26), Batch(1), ExpectedCredits(1049)), + (Some(CommonResolution::TenEightyP), Seconds(27), Batch(1), ExpectedCredits(1068)), + (Some(CommonResolution::TenEightyP), Seconds(28), Batch(1), ExpectedCredits(1087)), + (Some(CommonResolution::TenEightyP), Seconds(29), Batch(1), ExpectedCredits(1105)), + (Some(CommonResolution::TenEightyP), Seconds(30), Batch(1), ExpectedCredits(1124)), + (Some(CommonResolution::FourK), Seconds(4), Batch(1), ExpectedCredits(637)), + (Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(656)), + (Some(CommonResolution::FourK), Seconds(6), Batch(1), ExpectedCredits(675)), + (Some(CommonResolution::FourK), Seconds(7), Batch(1), ExpectedCredits(693)), + (Some(CommonResolution::FourK), Seconds(8), Batch(1), ExpectedCredits(712)), + (Some(CommonResolution::FourK), Seconds(9), Batch(1), ExpectedCredits(731)), + (Some(CommonResolution::FourK), Seconds(10), Batch(1), ExpectedCredits(749)), + (Some(CommonResolution::FourK), Seconds(11), Batch(1), ExpectedCredits(768)), + (Some(CommonResolution::FourK), Seconds(12), Batch(1), ExpectedCredits(787)), + (Some(CommonResolution::FourK), Seconds(13), Batch(1), ExpectedCredits(806)), + (Some(CommonResolution::FourK), Seconds(14), Batch(1), ExpectedCredits(824)), + (Some(CommonResolution::FourK), Seconds(15), Batch(1), ExpectedCredits(843)), + (Some(CommonResolution::FourK), Seconds(16), Batch(1), ExpectedCredits(862)), + (Some(CommonResolution::FourK), Seconds(17), Batch(1), ExpectedCredits(881)), + (Some(CommonResolution::FourK), Seconds(18), Batch(1), ExpectedCredits(899)), + (Some(CommonResolution::FourK), Seconds(19), Batch(1), ExpectedCredits(918)), + (Some(CommonResolution::FourK), Seconds(20), Batch(1), ExpectedCredits(937)), + (Some(CommonResolution::FourK), Seconds(21), Batch(1), ExpectedCredits(955)), + (Some(CommonResolution::FourK), Seconds(22), Batch(1), ExpectedCredits(974)), + (Some(CommonResolution::FourK), Seconds(23), Batch(1), ExpectedCredits(993)), + (Some(CommonResolution::FourK), Seconds(24), Batch(1), ExpectedCredits(1012)), + (Some(CommonResolution::FourK), Seconds(25), Batch(1), ExpectedCredits(1030)), + (Some(CommonResolution::FourK), Seconds(26), Batch(1), ExpectedCredits(1049)), + (Some(CommonResolution::FourK), Seconds(27), Batch(1), ExpectedCredits(1068)), + (Some(CommonResolution::FourK), Seconds(28), Batch(1), ExpectedCredits(1087)), + (Some(CommonResolution::FourK), Seconds(29), Batch(1), ExpectedCredits(1105)), + (Some(CommonResolution::FourK), Seconds(30), Batch(1), ExpectedCredits(1124)), + ]; + + for (resolution, seconds, batch, expected) in cases { + assert_reference_video_charge_then_refund( + &harness, CommonVideoModel::Seedance2p5Ultra, *resolution, *seconds, *batch, *expected, + ).await; + } + } + + /// With video references the per-second rate drops but billed seconds = output duration + probed input seconds. The fixture media is unreachable, so the probe fails open to the worst-case 30 input seconds, the upload then fails, and the exact charge is asserted on the refunded ledger entry. + /// Every batch size 1-10 at every resolution (5s). + /// Seedance 2.5 generates a single video per request: every requested batch size 1-10 bills exactly one video. + #[tokio::test] + #[cfg_attr(feature = "skip_database_tests", ignore)] + async fn video_references_charge_every_batch_size_at_every_resolution() { + let harness = TestHarness::create().await; + + let cases: &[(Option, Seconds, Batch, ExpectedCredits)] = &[ + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(300)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(2), ExpectedCredits(300)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(3), ExpectedCredits(300)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(4), ExpectedCredits(300)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(5), ExpectedCredits(300)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(6), ExpectedCredits(300)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(7), ExpectedCredits(300)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(8), ExpectedCredits(300)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(9), ExpectedCredits(300)), + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(10), ExpectedCredits(300)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(656)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(2), ExpectedCredits(656)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(3), ExpectedCredits(656)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(4), ExpectedCredits(656)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(5), ExpectedCredits(656)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(6), ExpectedCredits(656)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(7), ExpectedCredits(656)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(8), ExpectedCredits(656)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(9), ExpectedCredits(656)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(10), ExpectedCredits(656)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(656)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(2), ExpectedCredits(656)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(3), ExpectedCredits(656)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(4), ExpectedCredits(656)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(5), ExpectedCredits(656)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(6), ExpectedCredits(656)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(7), ExpectedCredits(656)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(8), ExpectedCredits(656)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(9), ExpectedCredits(656)), + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(10), ExpectedCredits(656)), + (Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(656)), + (Some(CommonResolution::FourK), Seconds(5), Batch(2), ExpectedCredits(656)), + (Some(CommonResolution::FourK), Seconds(5), Batch(3), ExpectedCredits(656)), + (Some(CommonResolution::FourK), Seconds(5), Batch(4), ExpectedCredits(656)), + (Some(CommonResolution::FourK), Seconds(5), Batch(5), ExpectedCredits(656)), + (Some(CommonResolution::FourK), Seconds(5), Batch(6), ExpectedCredits(656)), + (Some(CommonResolution::FourK), Seconds(5), Batch(7), ExpectedCredits(656)), + (Some(CommonResolution::FourK), Seconds(5), Batch(8), ExpectedCredits(656)), + (Some(CommonResolution::FourK), Seconds(5), Batch(9), ExpectedCredits(656)), + (Some(CommonResolution::FourK), Seconds(5), Batch(10), ExpectedCredits(656)), + ]; + + for (resolution, seconds, batch, expected) in cases { + assert_reference_video_charge_then_refund( + &harness, CommonVideoModel::Seedance2p5Ultra, *resolution, *seconds, *batch, *expected, + ).await; + } + } + + /// With video references the per-second rate drops but billed seconds = output duration + probed input seconds. The fixture media is unreachable, so the probe fails open to the worst-case 30 input seconds, the upload then fails, and the exact charge is asserted on the refunded ledger entry. + /// Spot checks across duration x batch x resolution. + #[tokio::test] + #[cfg_attr(feature = "skip_database_tests", ignore)] + async fn video_references_charge_spot_checked_combinations() { + let harness = TestHarness::create().await; + + let cases: &[(Option, Seconds, Batch, ExpectedCredits)] = &[ + (Some(CommonResolution::FourEightyP), Seconds(6), Batch(2), ExpectedCredits(309)), + (Some(CommonResolution::FourEightyP), Seconds(7), Batch(3), ExpectedCredits(317)), + (Some(CommonResolution::FourEightyP), Seconds(9), Batch(4), ExpectedCredits(334)), + (Some(CommonResolution::FourEightyP), Seconds(11), Batch(5), ExpectedCredits(351)), + (Some(CommonResolution::FourEightyP), Seconds(13), Batch(8), ExpectedCredits(369)), + (Some(CommonResolution::FourEightyP), Seconds(15), Batch(10), ExpectedCredits(386)), + (Some(CommonResolution::SevenTwentyP), Seconds(6), Batch(2), ExpectedCredits(675)), + (Some(CommonResolution::SevenTwentyP), Seconds(7), Batch(3), ExpectedCredits(693)), + (Some(CommonResolution::SevenTwentyP), Seconds(9), Batch(4), ExpectedCredits(731)), + (Some(CommonResolution::SevenTwentyP), Seconds(11), Batch(5), ExpectedCredits(768)), + (Some(CommonResolution::SevenTwentyP), Seconds(13), Batch(8), ExpectedCredits(806)), + (Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(10), ExpectedCredits(843)), + (Some(CommonResolution::TenEightyP), Seconds(6), Batch(2), ExpectedCredits(675)), + (Some(CommonResolution::TenEightyP), Seconds(7), Batch(3), ExpectedCredits(693)), + (Some(CommonResolution::TenEightyP), Seconds(9), Batch(4), ExpectedCredits(731)), + (Some(CommonResolution::TenEightyP), Seconds(11), Batch(5), ExpectedCredits(768)), + (Some(CommonResolution::TenEightyP), Seconds(13), Batch(8), ExpectedCredits(806)), + (Some(CommonResolution::TenEightyP), Seconds(15), Batch(10), ExpectedCredits(843)), + (Some(CommonResolution::FourK), Seconds(6), Batch(2), ExpectedCredits(675)), + (Some(CommonResolution::FourK), Seconds(7), Batch(3), ExpectedCredits(693)), + (Some(CommonResolution::FourK), Seconds(9), Batch(4), ExpectedCredits(731)), + (Some(CommonResolution::FourK), Seconds(11), Batch(5), ExpectedCredits(768)), + (Some(CommonResolution::FourK), Seconds(13), Batch(8), ExpectedCredits(806)), + (Some(CommonResolution::FourK), Seconds(15), Batch(10), ExpectedCredits(843)), + ]; + + for (resolution, seconds, batch, expected) in cases { + assert_reference_video_charge_then_refund( + &harness, CommonVideoModel::Seedance2p5Ultra, *resolution, *seconds, *batch, *expected, + ).await; + } + } +} + +// ── Revenue premium: Ultra over the base model ── +// Both prices and the delta are encoded so a change to EITHER rate card +// shows up here, not just a flipped ordering. +mod premium { + use super::*; + + /// Seedance 2.5 Ultra is fulfilled by Seedance 2.5 but has its own (higher) + /// price. Same shape as the 2.0 collapse-bug pins: if the pipeline ever + /// collapses Ultra before billing, these fail with the regular 2.5 numbers. + #[tokio::test] + #[cfg_attr(feature = "skip_database_tests", ignore)] + async fn seedance_2p5_ultra_charges_a_premium_over_the_base_model() { + let harness = TestHarness::create().await; + + let cases: &[(Option, Seconds, Batch, ExpectedCredits, ExpectedCredits, CreditsDelta)] = &[ + (Some(CommonResolution::FourEightyP), Seconds(5), Batch(1), ExpectedCredits(59), ExpectedCredits(70), CreditsDelta(11)), + (Some(CommonResolution::FourEightyP), Seconds(30), Batch(1), ExpectedCredits(354), ExpectedCredits(418), CreditsDelta(64)), + (Some(CommonResolution::SevenTwentyP), Seconds(5), Batch(1), ExpectedCredits(134), ExpectedCredits(158), CreditsDelta(24)), + (Some(CommonResolution::SevenTwentyP), Seconds(10), Batch(1), ExpectedCredits(268), ExpectedCredits(316), CreditsDelta(48)), + (Some(CommonResolution::SevenTwentyP), Seconds(15), Batch(1), ExpectedCredits(401), ExpectedCredits(474), CreditsDelta(73)), + (Some(CommonResolution::SevenTwentyP), Seconds(30), Batch(1), ExpectedCredits(802), ExpectedCredits(947), CreditsDelta(145)), + // 1080p and 4K downgrade to 720p, so the premium matches 720p. + (Some(CommonResolution::TenEightyP), Seconds(5), Batch(1), ExpectedCredits(134), ExpectedCredits(158), CreditsDelta(24)), + (Some(CommonResolution::FourK), Seconds(5), Batch(1), ExpectedCredits(134), ExpectedCredits(158), CreditsDelta(24)), + ]; + + for (resolution, seconds, batch, base, variant, delta) in cases { + assert_variant_charges_premium( + &harness, + CommonVideoModel::Seedance2p5, + CommonVideoModel::Seedance2p5Ultra, + *resolution, *seconds, *batch, *base, *variant, *delta, + ).await; + } + } +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/seedance_2p5_tests.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/seedance_2p5_tests.rs deleted file mode 100644 index 9933df732f..0000000000 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/tests/seedance_2p5_tests.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! Seedance 2.5 family pricing tests: 2.5 and 2.5 Ultra through the real -//! generate endpoint against the test database, asserting exact wallet -//! debits — including the reference-video input-seconds dimension. -//! -//! Reference-video note: the fixture media files point at unreachable CDN -//! URLs, so the duration probe fails open and bills the worst-case 30 input -//! seconds (pinned here), and the provider upload then fails, triggering a -//! refund. The ledger entry keeps its original debit amount with -//! `is_refunded = true`, which lets these tests assert the exact charge for -//! reference-video requests without any real media. - -use enums::common::generation::common_resolution::CommonResolution; -use enums::common::generation::common_video_model::CommonVideoModel; - -use super::support::{ - assert_reference_video_charge_then_refund, assert_successful_generation_charges, Batch, - ExpectedCredits, Seconds, TestHarness, -}; - -// ── Text-to-video pricing (successful generation via the stub provider) ── - -#[tokio::test] -#[cfg_attr(feature = "skip_database_tests", ignore)] -async fn seedance_2p5_charges_by_resolution_and_duration() { - let harness = TestHarness::create().await; - - // 2.5 rates: 480p 11.76954733 ¢/s, 720p 26.70781893 ¢/s, ceil-rounded. - // Only 480p/720p exist; 1080p downgrades to 720p pricing. No batching. - let cases: &[(Option, Seconds, ExpectedCredits)] = &[ - (Some(CommonResolution::FourEightyP), Seconds(4), ExpectedCredits(48)), - (Some(CommonResolution::FourEightyP), Seconds(5), ExpectedCredits(59)), - (Some(CommonResolution::FourEightyP), Seconds(30), ExpectedCredits(354)), - (Some(CommonResolution::SevenTwentyP), Seconds(4), ExpectedCredits(107)), - (Some(CommonResolution::SevenTwentyP), Seconds(5), ExpectedCredits(134)), - (Some(CommonResolution::SevenTwentyP), Seconds(10), ExpectedCredits(268)), - (Some(CommonResolution::SevenTwentyP), Seconds(30), ExpectedCredits(802)), - // Default resolution is 720p. - (None, Seconds(5), ExpectedCredits(134)), - ]; - - for (resolution, seconds, expected) in cases { - assert_successful_generation_charges( - &harness, - CommonVideoModel::Seedance2p5, - *resolution, - *seconds, - Batch(1), - *expected, - ) - .await; - } -} - -/// Seedance 2.5 Ultra is fulfilled by Seedance 2.5 but has its own (higher) -/// price. Same shape as the 2.0 collapse-bug pins: if the pipeline ever -/// collapses Ultra before billing, these fail with the regular 2.5 numbers. -#[tokio::test] -#[cfg_attr(feature = "skip_database_tests", ignore)] -async fn seedance_2p5_ultra_charges_its_own_higher_rates() { - let harness = TestHarness::create().await; - - // Ultra rates: 480p 13.90946502 ¢/s, 720p 31.56378601 ¢/s, ceil-rounded. - let cases: &[(Option, Seconds, ExpectedCredits)] = &[ - (Some(CommonResolution::FourEightyP), Seconds(5), ExpectedCredits(70)), // regular would be 59 - (Some(CommonResolution::FourEightyP), Seconds(30), ExpectedCredits(418)), // regular would be 354 - (Some(CommonResolution::SevenTwentyP), Seconds(5), ExpectedCredits(158)), // regular would be 134 - (Some(CommonResolution::SevenTwentyP), Seconds(30), ExpectedCredits(947)), // regular would be 802 - (None, Seconds(5), ExpectedCredits(158)), - ]; - - for (resolution, seconds, expected) in cases { - assert_successful_generation_charges( - &harness, - CommonVideoModel::Seedance2p5Ultra, - *resolution, - *seconds, - Batch(1), - *expected, - ) - .await; - } -} - -// ── Reference-video input-seconds billing ── -// -// With video references the per-second rate drops but billed seconds = -// output duration + probed input seconds. Unreachable fixture media probes -// fail open to the 30-second worst case: -// 2.5: 480p with refs = 7.24279835 ¢/s → 30s out + 30s in = 60 × rate = 435 -// 2.5: 720p with refs = 15.84362140 ¢/s → 60 × rate = 951 -// Ultra: 480p with refs = 8.55967078 ¢/s → 60 × rate = 514 -// Ultra: 720p with refs = 18.72427984 ¢/s → 60 × rate = 1124 - -#[tokio::test] -#[cfg_attr(feature = "skip_database_tests", ignore)] -async fn seedance_2p5_bills_reference_video_input_seconds_at_worst_case_when_unprobeable() { - let harness = TestHarness::create().await; - - let cases: &[(CommonVideoModel, Option, Seconds, ExpectedCredits)] = &[ - (CommonVideoModel::Seedance2p5, Some(CommonResolution::FourEightyP), Seconds(30), ExpectedCredits(435)), - (CommonVideoModel::Seedance2p5, Some(CommonResolution::SevenTwentyP), Seconds(30), ExpectedCredits(951)), - (CommonVideoModel::Seedance2p5Ultra, Some(CommonResolution::FourEightyP), Seconds(30), ExpectedCredits(514)), - (CommonVideoModel::Seedance2p5Ultra, Some(CommonResolution::SevenTwentyP), Seconds(30), ExpectedCredits(1124)), - ]; - - for (model, resolution, seconds, expected) in cases { - assert_reference_video_charge_then_refund( - &harness, - *model, - *resolution, - *seconds, - Batch(1), - *expected, - ) - .await; - } -}