forked from coracle/caravel
Round prorations to the nearest hour
This commit is contained in:
@@ -574,13 +574,17 @@ impl BillingPeriod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fraction of this period still unused at `at`, in `[0.0, 1.0]`, for
|
/// Fraction of this period still unused at `at`, in `[0.0, 1.0]`, for
|
||||||
/// prorating a mid-period charge or credit.
|
/// prorating a mid-period charge or credit. The remaining time is rounded to
|
||||||
|
/// the nearest hour so proration tracks whole hours rather than exact seconds.
|
||||||
fn fraction_remaining(&self, at: i64) -> f64 {
|
fn fraction_remaining(&self, at: i64) -> f64 {
|
||||||
|
const HOUR: i64 = 60 * 60;
|
||||||
|
|
||||||
let len = (self.end - self.start) as f64;
|
let len = (self.end - self.start) as f64;
|
||||||
if len <= 0.0 {
|
if len <= 0.0 {
|
||||||
return 1.0;
|
return 1.0;
|
||||||
}
|
}
|
||||||
(((self.end - at) as f64) / len).clamp(0.0, 1.0)
|
let remaining = ((self.end - at) + HOUR / 2) / HOUR * HOUR;
|
||||||
|
(remaining as f64 / len).clamp(0.0, 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prorate a minor-unit `amount` by the fraction of this period remaining
|
/// Prorate a minor-unit `amount` by the fraction of this period remaining
|
||||||
|
|||||||
@@ -14,35 +14,49 @@ export default function RelayNew() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [pendingInvoice, setPendingInvoice] = createSignal<Invoice | undefined>()
|
const [pendingInvoice, setPendingInvoice] = createSignal<Invoice | undefined>()
|
||||||
const [paymentSetupOpen, setPaymentSetupOpen] = createSignal(false)
|
const [paymentSetupOpen, setPaymentSetupOpen] = createSignal(false)
|
||||||
|
const [creatingPaid, setCreatingPaid] = createSignal(false)
|
||||||
let createdRelayId = ""
|
let createdRelayId = ""
|
||||||
|
|
||||||
// While this flow's inline modals are open, suppress the shared banner's
|
// Suppress the shared banner's overlapping pay/setup prompts (it still surfaces
|
||||||
// overlapping pay/setup prompts (it still surfaces churn / method errors).
|
// churn / method errors) for the whole paid-creation flow: `creatingPaid` covers
|
||||||
createEffect(() => setBillingFlowActive(Boolean(pendingInvoice()) || paymentSetupOpen()))
|
// the async gap between create and a modal opening — without it the invoice that
|
||||||
|
// refetchBilling surfaces flashes the banner before the modal appears — and
|
||||||
|
// pendingInvoice/paymentSetupOpen cover the time the modals are open.
|
||||||
|
createEffect(() => setBillingFlowActive(creatingPaid() || Boolean(pendingInvoice()) || paymentSetupOpen()))
|
||||||
onCleanup(() => setBillingFlowActive(false))
|
onCleanup(() => setBillingFlowActive(false))
|
||||||
|
|
||||||
async function handleSubmit(values: RelayFormValues) {
|
async function handleSubmit(values: RelayFormValues) {
|
||||||
const relay = await createRelayForActiveTenant(values)
|
// Paid plans materialize an open invoice on create; mark the flow active
|
||||||
createdRelayId = relay.id
|
// before refetchBilling surfaces it so the banner stays suppressed across the
|
||||||
void refetchBilling()
|
// whole async gap, not just once a modal opens. The finally releases it on
|
||||||
|
// every exit (error, free plan, or after a modal has taken over suppression).
|
||||||
|
const paid = values.plan_id !== "free"
|
||||||
|
if (paid) setCreatingPaid(true)
|
||||||
|
try {
|
||||||
|
const relay = await createRelayForActiveTenant(values)
|
||||||
|
createdRelayId = relay.id
|
||||||
|
void refetchBilling()
|
||||||
|
|
||||||
if (values.plan_id !== "free") {
|
if (paid) {
|
||||||
const needsSetup = await tenantNeedsPaymentSetup()
|
const needsSetup = await tenantNeedsPaymentSetup()
|
||||||
if (needsSetup) {
|
if (needsSetup) {
|
||||||
// Materialize the invoice for this change (no collection, no DM) so we
|
// Materialize the invoice for this change (no collection, no DM) so we
|
||||||
// can prompt the tenant to pay it directly. listTenantInvoices reconciles
|
// can prompt the tenant to pay it directly. listTenantInvoices reconciles
|
||||||
// first, so a just-created invoice is visible here.
|
// first, so a just-created invoice is visible here.
|
||||||
const invoice = await getLatestOpenInvoice()
|
const invoice = await getLatestOpenInvoice()
|
||||||
if (invoice) {
|
if (invoice) {
|
||||||
setPendingInvoice(invoice)
|
setPendingInvoice(invoice)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setPaymentSetupOpen(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setPaymentSetupOpen(true)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
navigate(`/relays/${relay.id}`)
|
navigate(`/relays/${relay.id}`)
|
||||||
|
} finally {
|
||||||
|
setCreatingPaid(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInvoiceClose() {
|
function handleInvoiceClose() {
|
||||||
|
|||||||
Reference in New Issue
Block a user