feat(pricing): subscription-level amount→cost pricing#207
Conversation
…on_cost_by_amount) Adds PricingEngine::get_subscription_cost_by_amount, the subscription- level equivalent of get_cost_by_amount (which only prices VM-backed subscriptions). It turns an arbitrary paid amount into time, priced from the subscription's line-item amounts and billing interval (interval_amount/interval_type), converting the per-interval list price to the payment method currency and scaling the interval time by input/interval_cost — with the same tax determination and processing-fee handling as the VM path. Wires the on-chain payment watcher to use it for subscriptions without a VM, replacing the previous quote-scaling fallback (which re-used the payment's stale quote). regenerate() now prices VM and non-VM deposits uniformly through the engine; the pro_rate/pro_rate_f64/current_rate helpers and their test are removed. Tests: unit test for get_subscription_cost_by_amount (half-interval payment -> half the time) and the on-chain no-VM test now asserts line-item pricing. Closes #181
|
Addressed review: the on-chain received amount is always the gross deposit, but Added (the exact inverse of the |
…ut, not on top) The amount-based pricing (get_cost_by_amount for LNURL/on-chain VM top-ups, and get_subscription_cost_by_amount) treated its input as the NET price and added tax + processing fee on top. But the amount a customer actually pays is the GROSS total — the sats a wallet sends for an LNURL top-up, or an on-chain deposit — so tax and any fee must be taken OUT of it to get the net that buys time. Both methods now back the net out of the gross via a new PricingEngine::net_from_gross: net = (gross*(1 - fee_rate) - fee_base) / (1 + tax) and return amount/tax/processing_fee that sum to exactly the paid amount (fee = remainder). The fee-config read is refactored into a shared processing_fee_params helper used by both calculate_processing_fee and net_from_gross. The on-chain watcher now passes the gross deposit straight through (no pre-stripping, no clamping). Effects: LNURL top-ups for taxed customers no longer over-credit time / over-invoice; on-chain deposits remove the processing fee too, not just tax. No change for untaxed, no-fee payments. Tests: net_from_gross (lightning + Revolut fee round-trip) and a subscription gross-input test asserting net+tax==gross with 23% VAT.
01f3413 to
3cb8947
Compare
|
Good catch — this applies to Lightning/LNURL too. If a wallet pays 1000 sats, that 1000 is the gross the customer paid, so tax (and any fee) must come out of it to get the net that buys time. Fixed properly now: Effects:
Added a test asserting |
The e2e suite only exercised LNURL error paths (invalid id / missing
VM). Add a real top-up step to the lifecycle test: hit the
unauthenticated LNURL callback for a live VM, assert the { pr, routes }
response (and no hodl_invoice field), pay the returned bolt11 via the
regtest lnd-payer, and poll until the VM expiry advances — exercising
the amount->time pricing path end-to-end. Falls back gracefully when
lnd-payer is unavailable.
|
Re: e2e coverage — it previously only hit the LNURL error paths (invalid id / missing VM). Added a real top-up step to the lifecycle test: it hits the unauthenticated LNURL callback for a live VM, asserts the |
…ed customer Add a taxed (IE, 23% VAT) case to the get_cost_by_amount test: paying a gross of 1_230_000 msat yields net 1_000_000 + tax 230_000 (components sum to the gross), and time_value is scaled by the net — strictly less than the gross-based time. Directly covers the LNURL/VM top-up pricing path with a non-zero tax rate.
|
Re: asserting the time value for a given tax rate —
So the "time credited from net, not gross" behaviour is deterministically pinned in unit tests; e2e stays a settlement smoke test since the taxed path needs a taxed customer + company VAT config that would perturb the shared lifecycle test. |
Closes #181.
Summary
Adds
PricingEngine::get_subscription_cost_by_amount— the subscription-level equivalent ofget_cost_by_amount(which only prices VM-backed subscriptions). It turns an arbitrary paid amount into time for any subscription (IP range, DNS, ASN, …), priced from the subscription’s line-item amounts and billing interval (interval_amount/interval_type):get_amount_and_rate).input / interval_cost.inputis the net amount; tax added on top).On-chain watcher
OnChainPaymentHandler::regeneratenow prices all deposits uniformly through the engine — VMs viaget_cost_by_amount, everything else viaget_subscription_cost_by_amount. This replaces the previous fallback that scaled the payment’s original (stale) quote by the value received. The now-deadpro_rate/pro_rate_f64/current_ratehelpers (and their test) are removed.Tests
get_subscription_cost_by_amount(paying half a month’s price buys ~half the interval’s time; zero tax with no VAT config).test_no_vm_prices_from_line_items) now asserts pricing comes from the subscription line items, matching the engine.No API surface change.
Closes #181