134 lines · 5.7 KB · Swift
RawDownload
import Foundation public struct PerplexityUsageSnapshot: Sendable {    public let recurringTotal: Double    public let recurringUsed: Double    public let promoTotal: Double    public let promoUsed: Double    public let purchasedTotal: Double    public let purchasedUsed: Double    public let balanceCents: Double    public let totalUsageCents: Double    public let renewalDate: Date    public let promoExpiration: Date?    public let updatedAt: Date     public init(response: PerplexityCreditsResponse, now: Date) {        let recurring = response.creditGrants.filter { $0.type == "recurring" }        let promotional = response.creditGrants.filter {            $0.type == "promotional" && ($0.expiresAtTs ?? .infinity) > now.timeIntervalSince1970        }        let purchased = response.creditGrants.filter { $0.type == "purchased" }         // All timestamps from the Perplexity API are Unix seconds (verified Feb 2026).        let recurringSum = max(0, recurring.reduce(0.0) { $0 + $1.amountCents })        let promoSum = max(0, promotional.reduce(0.0) { $0 + $1.amountCents })        // Purchased credits may appear in the top-level field, in the credit_grants        // array (type == "purchased"), or both. Take whichever is larger to avoid        // double-counting while still catching either source.        let purchasedFromGrants = max(0, purchased.reduce(0.0) { $0 + $1.amountCents })        let purchasedFromField = max(0, response.currentPeriodPurchasedCents)        let purchasedSum = max(purchasedFromGrants, purchasedFromField)         // Waterfall attribution: recurring → purchased → promotional        var remaining = response.totalUsageCents        let usedFromRecurring = min(remaining, recurringSum); remaining -= usedFromRecurring        let usedFromPurchased = min(remaining, purchasedSum); remaining -= usedFromPurchased        let usedFromPromo = min(remaining, promoSum)         self.recurringTotal = recurringSum        self.recurringUsed = usedFromRecurring        self.promoTotal = promoSum        self.promoUsed = usedFromPromo        self.purchasedTotal = purchasedSum        self.purchasedUsed = usedFromPurchased        self.balanceCents = response.balanceCents        self.totalUsageCents = response.totalUsageCents        self.renewalDate = Date(timeIntervalSince1970: response.renewalDateTs)        self.promoExpiration = promotional            .compactMap { $0.expiresAtTs.map { Date(timeIntervalSince1970: $0) } }            .min()        self.updatedAt = now    }     /// Infer plan name from recurring credit allotment.    /// Free = 0, Pro = small pool (~500–1000), Max = 10,000+.    public var planName: String? {        if self.recurringTotal <= 0 { return nil }        if self.recurringTotal < 5000 { return "Pro" }        return "Max"    }     private static let promoExpiryFormatter: DateFormatter = {        let fmt = DateFormatter()        fmt.dateFormat = "MMM d"        return fmt    }()} extension PerplexityUsageSnapshot {    public func toUsageSnapshot() -> UsageSnapshot {        // Primary: recurring (monthly) credits        let hasFallbackCredits = self.promoTotal > 0 || self.purchasedTotal > 0        let primaryWindow: RateWindow? = {            if self.recurringTotal > 0 {                let primaryPercent = min(100, max(0, self.recurringUsed / self.recurringTotal * 100))                return RateWindow(                    usedPercent: primaryPercent,                    windowMinutes: nil,                    resetsAt: self.renewalDate,                    resetDescription: "\(Int(self.recurringUsed.rounded()))/\(Int(self.recurringTotal)) credits")            }            if hasFallbackCredits {                // When recurring is absent but bonus/purchased credits remain, omit the fake 0/0 primary lane                // so automatic menu-bar rendering can fall through to the usable pool.                return nil            }            return RateWindow(                usedPercent: 100,                windowMinutes: nil,                resetsAt: self.renewalDate,                resetDescription: "0/0 credits")        }()         // Secondary: promotional bonus credits — always shown.        // usedPercent=100 when promoTotal==0 so the bar renders empty rather than full.        let promoPercent = self.promoTotal > 0            ? min(100, max(0, self.promoUsed / self.promoTotal * 100))            : 100.0        var promoDesc = "\(Int(promoUsed.rounded()))/\(Int(self.promoTotal)) bonus"        if let expiry = promoExpiration {            promoDesc += " \u{00b7} exp. \(Self.promoExpiryFormatter.string(from: expiry))"        }        let secondary = RateWindow(            usedPercent: promoPercent,            windowMinutes: nil,            resetsAt: nil,            resetDescription: promoDesc)         // Tertiary: on-demand purchased credits — always shown.        // usedPercent=100 when purchasedTotal==0 so the bar renders empty rather than full.        let purchasedPercent = self.purchasedTotal > 0            ? min(100, max(0, self.purchasedUsed / self.purchasedTotal * 100))            : 100.0        let tertiary = RateWindow(            usedPercent: purchasedPercent,            windowMinutes: nil,            resetsAt: nil,            resetDescription: "\(Int(purchasedUsed.rounded()))/\(Int(self.purchasedTotal)) credits")         let identity = ProviderIdentitySnapshot(            providerID: .perplexity,            accountEmail: nil,            accountOrganization: nil,            loginMethod: planName)         return UsageSnapshot(            primary: primaryWindow,            secondary: secondary,            tertiary: tertiary,            providerCost: nil,            updatedAt: self.updatedAt,            identity: identity)    }}