# Abonnia price & attribute catalogue (static JSON, one file per country)

File: site-catalog/v1/<cc>.json  (cc = dk | se | no | de | gb)

{
  "version": 1,
  "country": "DK",
  "currency": "DKK",
  "updatedAt": "2026-10-17",
  "services": {
    "<serviceKey>": {                       // key must match src/domain/abonnia/catalog-data.ts keys (netflix, spotify, viaplay, yousee, …)
      "name": "Netflix",
      "tiers": [
        { "key": "standard_ads", "name": "Standard med reklamer", "monthly": 79, "yearly": null, "asOf": "2026-10-17", "source": "https://…", "note": null }
      ],
      "attributes": { ... category-specific, see below ... },
      "pause": true,                        // provider offers pausing the subscription
      "noticeDays": 0,                      // provider's stated notice period (days); 0 = cancel any time, ends at period end
      "bindingMonths": 0,                   // typical minimum term
      "cancelUrl": "https://…",             // official manage/cancel page (omit if unsure)
      "cancelHow": ["web","app"],           // web | app | phone | written
      "priceChanges": [ { "date": "2025-04-01", "tier": "standard", "from": 109, "to": 129, "source": "https://…" } ]   // known increases, newest last, optional
    }
  },
  "bundles": [
    { "key": "apple_one_individual", "name": "Apple One Individuel", "monthly": 199, "includes": ["icloud", "applemusic", "appletv", "applearcade"], "source": "https://…", "asOf": "2026-10-17", "note": "iCloud+ 50 GB" }
  ]
}

Attribute keys (use only these, booleans/numbers; omit unknown):
- streaming: ads (bool, cheapest tier has ads), adFreeTier (bool), uhd (bool), screens (number, max simultaneous on top tier), offline (bool), profiles (number), sportsLeagues (string[] e.g. ["Superliga","Champions League","F1","Premier League"]), kids (bool), localContent (bool: strong Nordic/German local catalogue), liveTv (bool)
- music: lossless, family (bool), familyPrice (number monthly), duo (bool), duoPrice (number), student (bool), studentPrice, podcasts, audiobooks (bool), audiobookHours (number/month), offline
- cloud: storageGb (number for base paid tier), familySharing (bool)
- gaming: cloudGaming, onlinePlay, gameLibrary (bool), tiersCount
- fitness: open247, nationwide, classes, pool, sauna, childcare, studentDiscount (bool), bindingMonths, noticeDays, joiningFee (number), cheapestMonthly (number)
- telecom: dataGb (number for the listed tier), euRoamingGb, fiveG, familyDiscount, kidsPlan (bool), bindingMonths
- utilities (electricity): variablePrice, fixedPrice, green (bool), monthlyFee (number), bindingMonths
- news: digitalOnly, includesEpaper, student (bool)

## v1.1 additions (2026-10-18) — used by the Compare tab

Per service (all optional, additive):
- "content": string[]      // what you get, as short local-language labels the user can filter on. Streaming: genres/content
                           //   ("Reality", "Dansk drama", "Dokumentar", "Børn", "Anime", "Live-tv", "Film premierer", "HBO-serier",
                           //   "Nyheder"). Fitness: facilities not covered by attributes ("Yoga", "Crossfit", "Svømmehal", "Padel").
                           //   Telecom: ("Fri tale", "EU-data", "5G"). Keep 3–10 per service. Sports go in attributes.sportsLeagues.
- "campaign": Campaign     // service-wide offer running NOW (omit if none). See Campaign.
- "summary": string        // one line in local language, ≤ 90 chars: who it's for ("Størst katalog, bedst til film og serier").

Per tier (optional): "campaign": Campaign, "popular": true (the provider's "most chosen"/recommended tier),
"addon": true (an add-on that requires a base plan — e.g. a sports pack — never counted as the "from" price).
Per bundle (optional): "campaign": Campaign, "content": string[].

Campaign = {
  "name": "3 mdr. til 1 kr.",            // as the provider words it, short
  "price": 1,                            // promo monthly price (number) or null if it's not a price (e.g. "gratis Chromecast")
  "months": 3,                           // promo length in months, or null
  "until": "2026-11-30",                 // last day to sign up (ISO) or null if open-ended
  "newOnly": true,                       // new customers only
  "asOf": "2026-10-18",                  // when we verified it
  "source": "https://…",
  "note": "Derefter 109 kr./md."         // optional
}

Rules: a campaign with "until" in the past must be removed by the weekly job. Prices in "tiers" are ALWAYS the regular
price; the campaign is shown next to it. "asOf" on every tier/campaign/bundle is what the app shows as "pris pr. <date>".

## v1.2 (2026-10-18) — tier-level content for pricing a specific wish
- tiers[].sportsTier: true   // this tier carries the service's sports rights. If ANY tier of a service is marked, unmarked tiers have no sport.
- tiers[].content: string[]  // for a sportsTier: the subset of attributes.sportsLeagues this tier actually includes (omit = all).
                             // The Compare tab prices a wish ("Champions League") with the cheapest tier whose content covers it;
                             // if only add-on tiers cover it, price = cheapest base tier + add-on.
- bundles[].pick: number     // "choose N of the included services" bundles (telco streaming packs). Omit when the bundle includes all of them.
