Public/Get-GarminBadges.ps1

<#
.SYNOPSIS
Fetches the earned Garmin Connect badges and summarizes them by year, month or name.
 
.DESCRIPTION
`Get-GarminBadges` calls the Garmin Connect API endpoint `/badge-service/badge/earned`
directly from PowerShell. Authentication uses the DI OAuth2 token
store written by `Get-GarminToken` (`garmin_tokens.json`). An access
token that expires within 15 minutes is refreshed automatically and saved back.
 
Before returning, the function writes a console summary: total number of badges,
total points (badge points multiplied by the number of times each badge was earned),
the current Garmin level (1-10), the next level threshold, and the points still missing.
 
Without `-GroupBy`, the raw badge objects are returned. With `-GroupBy Year` or
`-GroupBy Month`, the badges are aggregated by the badge earned date and
summary objects are returned instead. The first summary row always has
`Year = 'Total'`; the following rows are sorted chronologically and contain a
running `TotalPoints` value.
 
Summary object properties:
- Year: 'Total' or the four-digit year.
- Month: Two-digit month (only with `-GroupBy Month`; empty for the 'Total' row).
- BadgeCount: Number of badges earned in the period.
- Points: Points earned in the period.
- TotalPoints: Cumulative points up to and including the period.
 
Because the result consists of objects, it can be filtered and formatted with
standard cmdlets such as `Where-Object`, `Sort-Object`, and `Format-Table`.
Note that `Year` and `Month` are strings.
 
.PARAMETER TokenStore
Optional path to the Garmin Connect token store directory or `garmin_tokens.json` file.
Defaults to `$env:GARMINTOKENS`, then `~\.garminconnect`.
 
.PARAMETER EnableLogging
Optional switch to enable module logging behavior (if supported by module logging helpers).
 
.PARAMETER GroupBy
Optional summary interval. Valid values: `Year`, `Month`, `Name`.
- `Year`: One row per year plus a 'Total' row, with badge count, points, and cumulative points.
- `Month`: One row per year and month plus a 'Total' row, with badge count, points, and cumulative points.
- `Name`: One row per badge (Name, EarnedCount, BadgePoints, Points, LastEarned), sorted by points.
 
If omitted, the individual badges are returned.
 
.OUTPUTS
System.Object[]
Returns the earned badges, or summary objects when `-GroupBy` is specified.
 
.NOTES
- Requires helper functions in module scope:
  `Get-FunctionName`, `Write-Log`, `Invoke-Output`, `Get-RunTime`, `Invoke-GarminConnectApi`.
- Requires a token file created by `Get-GarminToken`.
- Badges without an earned date are excluded from the yearly and monthly rows,
  but are counted in the 'Total' row.
 
.EXAMPLE
Get-GarminBadges
 
Returns the earned Garmin Connect badges and shows the level summary in the console.
 
.EXAMPLE
Get-GarminBadges -TokenStore "C:\Temp\.garminconnect"
 
Fetches earned badges using a custom token store.
 
.EXAMPLE
Get-GarminBadges -GroupBy Year | Format-Table
 
Shows badge count, points, and cumulative points per year, including a 'Total' row.
 
.EXAMPLE
Get-GarminBadges -GroupBy Month | Where-Object { $_.Year -eq 2025 } | Format-Table
 
Shows the monthly badge count, points, and cumulative points for 2025 only.
 
.EXAMPLE
Get-GarminBadges -GroupBy Name | Select-Object -First 10 | Format-Table
 
Shows the ten badges that earned the most points.
#>

Function Get-GarminBadges {

    [CmdletBinding()]
    param(
        [string]$TokenStore,
        [switch]$EnableLogging,
        [ValidateSet('Year', 'Month', 'Name')]
        [string]$GroupBy
    )

    $CurrentFunction = Get-FunctionName
    Write-Log -Message "### Start Function $CurrentFunction ###"
    $StartRunTime = (Get-Date).ToString($Script:DateFormatLog)
    #################### main code | out- host #####################

    # Minimum points required for Garmin levels 1-10 (index 0 = level 1)
    $LevelThresholds = @(0, 20, 60, 140, 300, 620, 1260, 2540, 5100, 10220)

    function Get-BadgePoints($BadgeList) {
        $sum = (@($BadgeList) | ForEach-Object {
                [double]$_.badgePoints * [double]$_.badgeEarnedNumber
            } | Measure-Object -Sum).Sum
        if ($null -eq $sum) { 0 } else { $sum }
    }

    function Get-BadgeEarnedDate($Badge) {
        [datetime]::Parse([string]$Badge.badgeEarnedDate, [Globalization.CultureInfo]::InvariantCulture)
    }

    Invoke-Output -Type Header -Message "Fetching earned Garmin Connect badges..."

    $allBadges = @(Invoke-GarminConnectApi -Path '/badge-service/badge/earned' -TokenStore $TokenStore)
    Write-Log -Message " >> $($allBadges.Count) badges received"

    # Console summary: badges, points and level
    $totalPoints = Get-BadgePoints $allBadges
    $CurrentLevel = @($LevelThresholds | Where-Object { $totalPoints -ge $_ }).Count

    Write-Host ""
    Invoke-Output -Type Bullet -Message "Total Badges: " -TextMaker $allBadges.Count -NoExtraLines
    if ($CurrentLevel -lt 10) {
        $NextLevel = $CurrentLevel + 1
        $NextPoints = $LevelThresholds[$NextLevel - 1]
        $Missing = $NextPoints - $totalPoints

        Invoke-Output -Type Bullet -Message "Total Points: " -TextMaker $totalPoints -NoExtraLines
        Invoke-Output -Type Bullet -Message "Current Level: " -TextMaker $CurrentLevel -NoExtraLines
        Invoke-Output -Type Bullet -Message "Next Level: " -TextMaker "$NextLevel ($NextPoints Points)"
        Invoke-Output -Type Bullet -Message "Points Missing:" -TextMaker $Missing
    }
    else {
        Invoke-Output -Type Bullet -Message "Total Points: " -TextMaker $totalPoints -NoExtraLines
        Invoke-Output -Type TextMaker -Message "Level 10 reached." -TextMaker "(highest)"
    }

    $result = $allBadges

    if ($GroupBy -in 'Year', 'Month') {
        $byMonth = $GroupBy -eq 'Month'

        $totalRow = [ordered]@{ Year = 'Total' }
        if ($byMonth) { $totalRow.Month = '' }
        $totalRow.BadgeCount = $allBadges.Count
        $totalRow.Points = $totalPoints
        $totalRow.TotalPoints = $totalPoints
        $badgeSummary = @([pscustomobject]$totalRow)

        # Group keys 'yyyy' or 'yyyy-MM' sort chronologically as plain strings
        $groups = $allBadges |
            Where-Object { $_.badgeEarnedDate } |
            Group-Object -Property { (Get-BadgeEarnedDate $_).ToString($(if ($byMonth) { 'yyyy-MM' } else { 'yyyy' })) } |
            Sort-Object -Property Name

        $runningTotalPoints = 0
        foreach ($group in $groups) {
            $points = Get-BadgePoints $group.Group
            $runningTotalPoints += $points

            $row = [ordered]@{ Year = $group.Name.Substring(0, 4) }
            if ($byMonth) { $row.Month = $group.Name.Substring(5, 2) }
            $row.BadgeCount = $group.Count
            $row.Points = $points
            $row.TotalPoints = $runningTotalPoints
            $badgeSummary += [pscustomobject]$row
        }

        $result = $badgeSummary
    }

    if ($GroupBy -eq 'Name') {
        $result = $allBadges | ForEach-Object {
            [pscustomobject]@{
                Name        = $_.badgeName
                EarnedCount = [int]$_.badgeEarnedNumber
                BadgePoints = [double]$_.badgePoints
                Points      = [double]$_.badgePoints * [double]$_.badgeEarnedNumber
                LastEarned  = if ($_.badgeEarnedDate) { Get-BadgeEarnedDate $_ } else { $null }
            }
        } | Sort-Object -Property @{ Expression = 'Points'; Descending = $true }, Name
    }

    Invoke-Output -Type Success -Message "Garmin Connect badges fetched successfully."
    ######################## main code ############################
    $runtime = Get-RunTime -StartRunTime $StartRunTime
    Write-Log -Message " Run Time: $runtime [h] ###"
    Write-Log -Message "### End Function $CurrentFunction ###"

    return $result
}