提示词场景库提示词场景库提示词场景库
提示词分类发现
提示词场景库·2026
关于/免责隐私条款
全部标签

Subscription提示词

2 个提示词
订阅支出审计器
技能

从简单 CSV 审计流媒体/App/健身房等经常性订阅的真实月费。

---
name: subscription-spending-auditor
description: 从简单 CSV 审计流媒体/App/健身房等经常性订阅的真实月费。
---

# 订阅支出审计器

你是「订阅支出审计器」技能助手。上方 description 为中文任务说明。下方保留原技能完整正文(含脚本与代码);执行时遵循中文说明与原文技术细节。

# Subscription Spending Auditor

You help people see what their subscriptions really cost and decide what to keep. You turn bank statement lines, app store receipts, or a rough list into one table, convert every charge to the same currency and to monthly and yearly amounts, and point out the easy wins: services nobody uses, three video services where one would do, trials about to turn into paid plans, quiet price increases, and annual plans that are cheaper if the service is kept anyway. The user decides; you make the trade-offs visible.

## Files in this skill

- `scripts/sub_audit.py` - reads a subscriptions CSV and prints totals, your share of shared plans, category totals, upcoming charges, and flags (TRIAL, RENEWS_YEARLY, UNUSED, OVERLAP, PRICE_UP, ANNUAL_SAVES) (Python 3 standard library only)
- `references/cost-normalization.md` - how cycles, currencies, shared plans, trials and price changes are converted, with the formulas
- `references/cancellation-playbook.md` - how to cancel, pause, downgrade or negotiate, and how to avoid surprise renewals
- `templates/subscription-review.md` - a one-page review with decisions and reminders
- `examples/subscriptions.csv` - 12 sample subscriptions in EUR and USD with weekly, monthly, quarterly and yearly cycles, a shared family plan, a trial, unused services and price increases

## Workflow

### 1. Build the list
Ask the user to scan the last 12 months of bank and card statements, app store and Google Play subscriptions, PayPal automatic payments, and email receipts (search "renewal", "subscription", "receipt", "trial"). Put each item in the CSV shape of `examples/subscriptions.csv`. Required columns: name, amount, cycle. Helpful: currency, next_renewal, category, last_used (an honest guess is fine), users (people sharing the plan), trial_end and paid_after_trial, previous_amount, annual_price.

### 2. Run the auditor
```
python3 scripts/sub_audit.py examples/subscriptions.csv --currency EUR --rate USD=0.92 --today 2026-10-20
python3 scripts/sub_audit.py subs.csv --currency USD --rate EUR=1.08 --rate GBP=1.27 --days 45 --unused-days 60
python3 scripts/sub_audit.py subs.csv --json > audit.json
python3 scripts/sub_audit.py --self-test
```
Rates are "1 unit of that currency = X report currency"; use the rate on the user's card statement. Exit code 2 means bad input (unknown cycle, missing rate, bad date, negative amount).

### 3. Walk through the action list
Order of attention: TRIAL (deadline soonest), RENEWS_YEARLY (a big charge is coming), UNUSED, OVERLAP, PRICE_UP, ANNUAL_SAVES. For each item, ask one question: "Would you sign up for this today at this price?" Offer cancel, pause, downgrade, share, or switch to annual (only for services they are sure to keep 12 months).

### 4. Act safely
Use `references/cancellation-playbook.md`. Cancel through the official account page or app store, screenshot the confirmation, and set calendar reminders 3 days before trial ends and yearly renewals.

### 5. Summarize
Fill `templates/subscription-review.md`: before and after monthly and yearly totals, decisions, and reminder dates.

## Example result
For `examples/subscriptions.csv` with USD=0.92 on 2026-10-20: 136.65 EUR/month (1,639.80 EUR/year), 112.66 EUR/month as the user's share, 699.49 EUR/year in four unused services, a DesignPro trial converting on 2026-10-25 to 22.99 EUR/month, a 99.99 EUR CloudVault renewal on 2026-11-05, three overlapping video services, three price increases worth 108 EUR/year, and 237.87 EUR of charges in the next 30 days.

## Rules

- Never ask for card numbers, bank logins, or passwords; amounts and dates are enough.
- Do not cancel anything or contact providers on the user's behalf unless they ask; draft the message instead.
- Keep it judgment-free: some subscriptions are worth it. Present costs and let the user choose.
- Trials count as 0 in current totals; always show what they cost after conversion.
FILE:references/cost-normalization.md
# Cost Normalization

## Charges per month by cycle
| Cycle | Charges per month | Yearly cost of 10.00 |
|---|---|---|
| weekly | 4.348125 (365.25 / 12 / 7) | 521.78 |
| biweekly | 2.1740625 | 260.89 |
| monthly | 1 | 120.00 |
| quarterly | 1/3 | 40.00 |
| semiannual | 1/6 | 20.00 |
| yearly | 1/12 | 10.00 |
| every N days | 30.436875 / N | |

monthly cost = charge x rate x charges per month; yearly cost = monthly x 12. Rounding to cents happens only at the end, so totals do not drift.

## Currencies
- Use one report currency and one rate per foreign currency, e.g. `--rate USD=0.92` (1 USD = 0.92 EUR).
- Best source: the converted amount on the card statement divided by the original amount. It includes the card's FX margin.
- Mention foreign transaction fees separately if the card charges them (often 1-3%).

## Shared plans
- `users` = number of people sharing. Your share = cost / users.
- Show both the full cost (what the card pays) and your share (what you would pay after others pay you back). If others do not actually pay, use users = 1.

## Trials
- A trial has amount 0, `trial_end`, and `paid_after_trial`. Charges on or after `trial_end` use the paid amount in the upcoming-charges list.
- Totals show today's cost (0) so the user sees what changes if they do nothing: the TRIAL flag gives the converted monthly and yearly price.

## Price changes
- `previous_amount` = the price before the latest increase. Yearly impact = (new - old) x rate x charges per month x 12.
- Providers often announce increases by email 30 days ahead; search for "price change" and "new price".

## Annual vs monthly
- Annual saves money only if the service would be kept for 12 months anyway. Saving = monthly cost x 12 - annual price.
- Annual plans are usually not refundable after a short window; switch only for services used every week.

## Renewal dates
- `next_renewal` in the past is rolled forward by the cycle until it is today or later.
- Month-end dates clamp: a plan renewing on the 31st renews on Feb 28 (or 29) and then on the 28th or 31st depending on the provider; check the receipt.
FILE:references/cancellation-playbook.md
# Cancellation and Downgrade Playbook

## Where to cancel
| Billed through | Where to cancel |
|---|---|
| Apple App Store | Settings > your name > Subscriptions |
| Google Play | Play Store > profile > Payments & subscriptions > Subscriptions |
| PayPal | Settings > Payments > Manage automatic payments (stops payments, also cancel with the provider) |
| Directly with the service | Account or billing page on the website; some require a chat or phone call |
| Mobile carrier or TV bundle | Carrier account or support line |

Deleting an app does not cancel its subscription.

## Steps
1. Check the renewal date and whether cancelling ends access now or at the end of the period (usually the end).
2. Export anything you want to keep (photos, playlists, notes, workout history).
3. Cancel through the official channel above, not through links in unexpected emails.
4. Save proof: confirmation email or screenshot with date.
5. Watch the next statement; dispute a charge after cancellation with the provider first, then the card issuer.

## Alternatives to cancelling
- **Pause** (gyms, meal kits, some streaming services) for travel or busy months.
- **Downgrade** to an ad-supported, single-screen, or smaller storage tier.
- **Rotate** streaming: keep one service at a time and switch monthly for the shows you want.
- **Share** a family plan with your household, within the provider's terms.
- **Ask** for a retention offer: many services offer a discount when you start cancelling. Decide your walk-away price first.

## Trials
- Set a reminder 2-3 days before `trial_end`; many trials can be cancelled right after signing up while keeping the trial period.
- Use a virtual card with a limit or a single-merchant card for trials if the bank offers it.

## Message template (support chat or email)
> Hello, please cancel my <service> subscription on the account <email> effective at the end of the current billing period, and confirm by email that no further charges will be made. Thank you.

## Habits that keep costs down
- Review subscriptions every quarter with this skill.
- Keep one calendar with all yearly renewal dates.
- Before subscribing, ask: will I use it at least weekly?
FILE:templates/subscription-review.md
# Subscription Review - <name> - <date>

**Report currency:** <EUR>  **Rates:** <1 USD = 0.92 EUR (card statement)>
**Before:** <monthly> /month = <yearly> /year (your share <monthly> /month)
**After decisions:** <monthly> /month = <yearly> /year  **Saved per year:** <amount>

## Decisions
| Subscription | Cost per year | Flag | Decision | Done |
|---|---|---|---|---|
| | | | keep / cancel / pause / downgrade / switch to annual | [ ] |

## Deadlines and reminders
| Date | What | Reminder set |
|---|---|---|
| | trial ends | [ ] |
| | yearly renewal | [ ] |

## Next 30 days of charges
- <date> <name> <amount>

## Notes
- Shared plans: <who pays what back>
- Price increases noticed: <service old -> new>
- Next review: <date, about 3 months from now>
FILE:examples/subscriptions.csv
name,amount,currency,cycle,next_renewal,category,last_used,users,trial_end,previous_amount,annual_price,paid_after_trial
StreamFlix Premium,17.99,EUR,monthly,2026-10-27,Video streaming,2026-10-18,2,,15.99,,
CineBox,8.99,EUR,monthly,2026-11-03,Video streaming,2026-08-02,1,,,89.90,
AnimeNow,6.49,USD,monthly,2026-10-22,Video streaming,2026-07-15,1,,,,
TuneWave Family,19.99,EUR,monthly,2026-11-10,Music,2026-10-19,4,,16.99,,
CloudVault 2 TB,99.99,EUR,yearly,2026-11-05,Cloud storage,2026-10-19,1,,,,
PhotoCloud 200 GB,2.99,EUR,monthly,2026-11-01,Cloud storage,2026-10-10,1,,,29.99,
FitPulse Gym,39.00,EUR,monthly,2026-11-01,Fitness,2026-09-04,1,,35.00,,
Yoga Daily App,59.99,USD,yearly,2027-03-14,Fitness,2026-10-12,1,,,,
NewsLine Digital,4.99,EUR,weekly,2026-10-23,News,2026-10-19,1,,,,
PasswordSafe,36.00,USD,yearly,2027-01-20,Software,2026-10-19,1,,,,
DesignPro Trial,0.00,USD,monthly,2026-10-25,Design,2026-10-02,1,2026-10-25,,,24.99
LangLeap Plus,12.99,EUR,quarterly,2026-12-01,Learning,2026-06-30,1,,,,
FILE:scripts/sub_audit.py
#!/usr/bin/env python3
"""Subscription spending auditor (Python 3 standard library only).

Input CSV columns (header names are case-insensitive; only name, amount, cycle are required):
  name, amount, currency, cycle, next_renewal, category, last_used, users, trial_end,
  previous_amount, annual_price, paid_after_trial
cycle: weekly, biweekly, monthly, quarterly, semiannual, yearly/annual, or "every N days|weeks|months|years"
       (also short forms like 2w, 3m, 1y).
users: how many people share the plan; your share = amount / users (default 1).
trial_end + paid_after_trial: a free trial (amount 0) that converts to paid_after_trial on trial_end.

Reports, all converted to one currency with explicit rates (--rate USD=0.92):
  monthly and yearly cost per item and in total, by category, your share of shared plans,
  renewals in the next --days days (past dates are rolled forward by the cycle),
  flags: UNUSED (last_used older than --unused-days), OVERLAP (several items in one category),
  TRIAL (trial ends within --days; amount after trial if given), PRICE_UP (amount > previous_amount),
  ANNUAL_SAVES (annual_price cheaper than 12 x monthly), RENEWS_YEARLY (a yearly charge within --days).
Usage:
  python3 sub_audit.py subscriptions.csv --currency EUR --rate USD=0.92 --today 2026-10-20
  python3 sub_audit.py subscriptions.csv --json
  python3 sub_audit.py --self-test
Exit code: 0 OK, 2 bad input.
"""
import argparse
import calendar
import csv
import datetime as dt
import json
import re
import sys
from decimal import Decimal, ROUND_HALF_UP

NAMED = {"weekly": (1, "w"), "biweekly": (2, "w"), "fortnightly": (2, "w"), "monthly": (1, "m"),
         "bimonthly": (2, "m"), "quarterly": (3, "m"), "semiannual": (6, "m"), "semiannually": (6, "m"),
         "half-yearly": (6, "m"), "yearly": (12, "m"), "annual": (12, "m"), "annually": (12, "m")}
UNIT = {"d": "d", "day": "d", "days": "d", "w": "w", "week": "w", "weeks": "w", "m": "m", "month": "m",
        "months": "m", "y": "y", "year": "y", "years": "y"}
PER_MONTH = {"d": Decimal("30.436875"), "w": Decimal("4.348125"), "m": Decimal(1), "y": Decimal(1) / 12}
D2 = Decimal("0.01")


class AuditError(ValueError):
    pass


def parse_cycle(s):
    s = (s or "").strip().lower()
    if s in NAMED:
        n, u = NAMED[s]
        return n, u
    m = re.fullmatch(r"(?:every\s+)?(\d+)\s*([a-z]+)", s)
    if m and m.group(2) in UNIT:
        n, u = int(m.group(1)), UNIT[m.group(2)]
        if n <= 0:
            raise AuditError(f"cycle must be positive: {s!r}")
        if u == "y":
            return n * 12, "m"
        return n, u
    raise AuditError(f"unknown cycle {s!r} (use weekly, monthly, quarterly, yearly or 'every 2 months')")


def monthly_factor(n, u):
    """How many charges per month for a cycle of n units."""
    return PER_MONTH[u] / n


def add_cycle(d, n, u):
    if u == "d":
        return d + dt.timedelta(days=n)
    if u == "w":
        return d + dt.timedelta(weeks=n)
    m = d.month - 1 + n
    y, m = d.year + m // 12, m % 12 + 1
    return dt.date(y, m, min(d.day, calendar.monthrange(y, m)[1]))


def parse_date(s, field, name):
    s = (s or "").strip()
    if not s:
        return None
    try:
        return dt.date.fromisoformat(s)
    except ValueError:
        raise AuditError(f"{name}: bad {field} date {s!r} (use YYYY-MM-DD)")


def money(s, field, name, required=False):
    s = (s or "").strip().replace(",", "") if (s or "").count(",") and "." in (s or "") else (s or "").strip().replace(",", ".")
    if not s:
        if required:
            raise AuditError(f"{name}: {field} is required")
        return None
    try:
        v = Decimal(s)
    except Exception:
        raise AuditError(f"{name}: bad {field} {s!r}")
    if v < 0:
        raise AuditError(f"{name}: {field} cannot be negative")
    return v


def q(v):
    return v.quantize(D2, rounding=ROUND_HALF_UP)


def load(lines):
    rd = csv.DictReader(lines)
    if not rd.fieldnames:
        raise AuditError("empty CSV")
    rows = []
    for r in rd:
        r = {(k or "").strip().lower(): (v or "").strip() for k, v in r.items()}
        if not any(r.values()):
            continue
        if not r.get("name"):
            raise AuditError("every row needs a name")
        rows.append(r)
    if not rows:
        raise AuditError("no subscriptions found")
    return rows


def audit(rows, currency="EUR", rates=None, today=None, days=30, unused_days=45):
    rates = {k.upper(): Decimal(str(v)) for k, v in (rates or {}).items()}
    rates[currency.upper()] = Decimal(1)
    today = today or dt.date.today()
    horizon = today + dt.timedelta(days=days)
    items, flags, renewals, raw = [], [], [], {}
    for r in rows:
        name = r["name"]
        cur = (r.get("currency") or currency).upper()
        if cur not in rates:
            raise AuditError(f"{name}: no rate for {cur}; pass --rate {cur}=<value in {currency}>")
        rate = rates[cur]
        n, u = parse_cycle(r.get("cycle"))
        amount = money(r.get("amount"), "amount", name, required=True)
        after_trial = money(r.get("paid_after_trial"), "paid_after_trial", name)
        trial_end = parse_date(r.get("trial_end"), "trial_end", name)
        users = int(r.get("users") or 1)
        if users < 1:
            raise AuditError(f"{name}: users must be at least 1")
        charge = amount * rate
        f = monthly_factor(n, u)
        monthly = charge * f
        item = {"name": name, "category": r.get("category") or "Other", "currency": cur, "amount": float(amount),
                "cycle": r.get("cycle"), "monthly": q(monthly), "yearly": q(monthly * 12), "users": users,
                "your_monthly": q(monthly / users), "your_yearly": q(monthly * 12 / users)}
        raw[name] = (monthly, monthly / users)
        items.append(item)
        nxt = parse_date(r.get("next_renewal"), "next_renewal", name)
        if nxt:
            while nxt < today:
                nxt = add_cycle(nxt, n, u)
            item["next_renewal"] = nxt.isoformat()
            d = nxt
            while d <= horizon:
                due = charge
                if trial_end and d >= trial_end and after_trial is not None:
                    due = after_trial * rate
                renewals.append({"date": d.isoformat(), "name": name, "amount": q(due)})
                d = add_cycle(d, n, u)
            if (n, u) in ((12, "m"),) or (u == "m" and n >= 12):
                if nxt <= horizon:
                    flags.append({"name": name, "flag": "RENEWS_YEARLY",
                                  "detail": f"{q(charge)} {currency} charge on {nxt.isoformat()}; decide before then"})
        last = parse_date(r.get("last_used"), "last_used", name)
        if last and (today - last).days > unused_days:
            flags.append({"name": name, "flag": "UNUSED",
                          "detail": f"last used {last.isoformat()} ({(today - last).days} days ago); costs {q(monthly * 12)} {currency}/year"})
        if trial_end and today <= trial_end <= horizon:
            extra = (f"; then {q(after_trial * rate)} {currency} per charge ({q(after_trial * rate * f * 12)} {currency}/year)"
                     if after_trial is not None else "")
            flags.append({"name": name, "flag": "TRIAL",
                          "detail": f"trial ends {trial_end.isoformat()} ({(trial_end - today).days} days){extra}; cancel or keep on purpose"})
        prev = money(r.get("previous_amount"), "previous_amount", name)
        if prev and amount > prev:
            pct = (amount - prev) / prev * 100
            flags.append({"name": name, "flag": "PRICE_UP",
                          "detail": f"{prev} -> {amount} {cur} (+{q(pct)}%), +{q((amount - prev) * rate * f * 12)} {currency}/year"})
        annual = money(r.get("annual_price"), "annual_price", name)
        if annual is not None and not (u == "m" and n == 12):
            save = monthly * 12 - annual * rate
            if save > 0:
                flags.append({"name": name, "flag": "ANNUAL_SAVES",
                              "detail": f"annual plan {q(annual * rate)} {currency} vs {q(monthly * 12)} {currency} paid {r.get('cycle')}: saves {q(save)} {currency}/year if you keep it 12 months"})
    cats = {}
    for it in items:
        c = cats.setdefault(it["category"], {"count": 0, "monthly": Decimal(0), "names": []})
        c["count"] += 1
        c["monthly"] += raw[it["name"]][0]
        c["names"].append(it["name"])
    for cname, c in sorted(cats.items()):
        if c["count"] > 1:
            flags.append({"name": ", ".join(c["names"]), "flag": "OVERLAP",
                          "detail": f"{c['count']} subscriptions in {cname} ({q(c['monthly'])} {currency}/month); keep the one you use most"})
    order = {"TRIAL": 0, "RENEWS_YEARLY": 1, "UNUSED": 2, "OVERLAP": 3, "PRICE_UP": 4, "ANNUAL_SAVES": 5}
    flags.sort(key=lambda x: (order[x["flag"]], x["name"]))
    renewals.sort(key=lambda x: (x["date"], x["name"]))
    unused_names = {f["name"] for f in flags if f["flag"] == "UNUSED"}
    total_m = sum((raw[it["name"]][0] for it in items), Decimal(0))
    your_m = sum((raw[it["name"]][1] for it in items), Decimal(0))
    return {
        "currency": currency, "today": today.isoformat(), "days": days, "rates": {k: str(v) for k, v in rates.items()},
        "count": len(items), "monthly_total": q(total_m), "yearly_total": q(total_m * 12),
        "your_monthly_total": q(your_m), "your_yearly_total": q(your_m * 12),
        "unused_yearly": q(sum((raw[it["name"]][0] * 12 for it in items if it["name"] in unused_names), Decimal(0))),
        "next_days_total": q(sum((x["amount"] for x in renewals), Decimal(0))),
        "by_category": {k: {"count": v["count"], "monthly": q(v["monthly"])} for k, v in sorted(cats.items(), key=lambda kv: -kv[1]["monthly"])},
        "items": sorted(items, key=lambda it: -it["monthly"]), "renewals": renewals, "flags": flags,
    }


def to_markdown(r):
    c = r["currency"]
    out = ["# Subscription audit", "",
           f"**As of:** {r['today']}  **Subscriptions:** {r['count']}  **Currency:** {c}  **Rates:** "
           + ", ".join(f"1 {k} = {v} {c}" for k, v in r["rates"].items() if k != c.upper()),
           f"**Total:** {r['monthly_total']} {c}/month = {r['yearly_total']} {c}/year  "
           f"**Your share:** {r['your_monthly_total']} {c}/month = {r['your_yearly_total']} {c}/year",
           f"**Unused (yearly cost):** {r['unused_yearly']} {c}  **Charges in next {r['days']} days:** {r['next_days_total']} {c}",
           "", "## Action list", ""]
    out += [f"- **{f['flag']}** {f['name']}: {f['detail']}" for f in r["flags"]] or ["- Nothing to act on."]
    out += ["", "## All subscriptions (most expensive first)", "",
            "| Name | Category | Charge | Cycle | Per month | Per year | Users | Your year | Next renewal |",
            "|---|---|---|---|---|---|---|---|---|"]
    for it in r["items"]:
        out.append(f"| {it['name']} | {it['category']} | {it['amount']:.2f} {it['currency']} | {it['cycle']} | {it['monthly']} | "
                   f"{it['yearly']} | {it['users']} | {it['your_yearly']} | {it.get('next_renewal', '')} |")
    out += ["", "## By category", "", "| Category | Count | Per month |", "|---|---|---|"]
    out += [f"| {k} | {v['count']} | {v['monthly']} |" for k, v in r["by_category"].items()]
    out += ["", f"## Charges in the next {r['days']} days", ""]
    out += [f"- {x['date']} {x['name']}: {x['amount']} {c}" for x in r["renewals"]] or ["- none"]
    return "\n".join(out)


def self_test():
    cases = []

    def ok(name, cond):
        cases.append((name, bool(cond)))

    T = dt.date(2026, 10, 20)
    hdr = "name,amount,currency,cycle,next_renewal,category,last_used,users,trial_end,previous_amount,annual_price,paid_after_trial"

    def run(*lines, **kw):
        return audit(load([hdr, *lines]), today=T, **kw)

    ok("named cycles", parse_cycle("Quarterly") == (3, "m") and parse_cycle("annual") == (12, "m"))
    ok("every N units", parse_cycle("every 2 weeks") == (2, "w") and parse_cycle("1y") == (12, "m"))
    ok("month end clamps", add_cycle(dt.date(2026, 1, 31), 1, "m") == dt.date(2026, 2, 28))
    ok("leap year clamp", add_cycle(dt.date(2027, 2, 28), 12, "m") == dt.date(2028, 2, 28) and add_cycle(dt.date(2028, 1, 31), 1, "m") == dt.date(2028, 2, 29))
    r = run("A,10,EUR,monthly,,X,,,,,,")
    ok("monthly 10 = 120/year", r["yearly_total"] == Decimal("120.00"))
    r = run("A,120,EUR,yearly,,X,,,,,,")
    ok("yearly 120 = 10/month", r["monthly_total"] == Decimal("10.00"))
    r = run("A,5,EUR,weekly,,X,,,,,,")
    ok("weekly 5 = 260.89/year", r["yearly_total"] == Decimal("260.89") and r["items"][0]["monthly"] == Decimal("21.74"))
    r = run("A,1,EUR,monthly,,X,,3,,,,", "B,1,EUR,monthly,,X,,3,,,,", "C,1,EUR,monthly,,X,,3,,,,")
    ok("totals use unrounded shares", r["your_monthly_total"] == Decimal("1.00"))
    r = run("A,10,USD,monthly,,X,,,,,,", rates={"USD": "0.9"})
    ok("currency rate applied", r["monthly_total"] == Decimal("9.00"))
    try:
        run("A,10,GBP,monthly,,X,,,,,,")
        ok("missing rate rejected", False)
    except AuditError:
        ok("missing rate rejected", True)
    r = run("Fam,20,EUR,monthly,,Music,,4,,,,")
    ok("shared plan your share", r["your_monthly_total"] == Decimal("5.00") and r["monthly_total"] == Decimal("20.00"))
    r = run("Old,10,EUR,monthly,,X,2026-07-01,,,,,")
    ok("unused flagged", any(f["flag"] == "UNUSED" for f in r["flags"]) and r["unused_yearly"] == Decimal("120.00"))
    r = run("Fresh,10,EUR,monthly,,X,2026-10-01,,,,,")
    ok("recent use not flagged", not r["flags"])
    r = run("A,10,EUR,monthly,,Video,,,,,,", "B,8,EUR,monthly,,Video,,,,,,")
    ok("overlap flagged", [f["flag"] for f in r["flags"]] == ["OVERLAP"])
    r = run("T,0,EUR,monthly,2026-10-25,Software,,,2026-10-25,,,24.99")
    ok("trial flagged with paid price", r["flags"][0]["flag"] == "TRIAL" and "24.99" in r["flags"][0]["detail"])
    ok("trial charge counted in renewals", r["renewals"][0]["amount"] == Decimal("24.99"))
    r = run("P,12,EUR,monthly,,X,,,,10,,")
    ok("price increase flagged", r["flags"][0]["flag"] == "PRICE_UP" and "+20.00%" in r["flags"][0]["detail"] and "+24.00" in r["flags"][0]["detail"])
    r = run("M,10,EUR,monthly,,X,,,,,99,")
    ok("annual saving flagged", r["flags"][0]["flag"] == "ANNUAL_SAVES" and "saves 21.00" in r["flags"][0]["detail"])
    r = run("M,10,EUR,monthly,,X,,,,,130,")
    ok("no saving when annual is pricier", not r["flags"])
    r = run("Y,50,EUR,yearly,2026-11-01,X,,,,,,")
    ok("yearly renewal soon flagged", r["flags"][0]["flag"] == "RENEWS_YEARLY")
    r = run("W,1,EUR,weekly,2026-10-21,X,,,,,,")
    ok("weekly renewals listed in window", len(r["renewals"]) == 5 and r["next_days_total"] == Decimal("5.00"))
    r = run("Past,10,EUR,monthly,2026-08-15,X,,,,,,")
    ok("rolled forward to the future", r["items"][0]["next_renewal"] == "2026-11-15")
    for line, label in [("A,-3,EUR,monthly,,X,,,,,,", "negative amount rejected"),
                        ("A,3,EUR,sometimes,,X,,,,,,", "unknown cycle rejected"),
                        ("A,3,EUR,monthly,20-10-2026,X,,,,,,", "bad date rejected"),
                        ("A,3,EUR,monthly,,X,,0,,,,", "zero users rejected")]:
        try:
            run(line)
            ok(label, False)
        except AuditError:
            ok(label, True)
    ok("markdown has action list", "## Action list" in to_markdown(run("A,10,EUR,monthly,,X,,,,,,")))
    passed = sum(c for _, c in cases)
    for name, c in cases:
        print(("PASS " if c else "FAIL ") + name)
    print(f"{passed}/{len(cases)} passed")
    return 0 if passed == len(cases) else 1


def default(o):
    if isinstance(o, Decimal):
        return float(o)
    raise TypeError(type(o))


def main(argv=None):
    ap = argparse.ArgumentParser(description="Audit recurring subscriptions: true yearly cost, unused, overlapping, trials and renewals.")
    ap.add_argument("file", nargs="?", help="subscriptions CSV ('-' for stdin)")
    ap.add_argument("--currency", default="EUR", help="report currency (default EUR)")
    ap.add_argument("--rate", action="append", default=[], metavar="CUR=VALUE",
                    help="1 CUR = VALUE report-currency units, e.g. USD=0.92 (repeatable)")
    ap.add_argument("--today", help="evaluation date YYYY-MM-DD (default: today)")
    ap.add_argument("--days", type=int, default=30, help="look-ahead for renewals and trials (default 30)")
    ap.add_argument("--unused-days", type=int, default=45, help="flag items not used for this many days (default 45)")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args(argv)
    if a.self_test:
        return self_test()
    if not a.file:
        ap.error("subscriptions CSV required (or --self-test)")
    try:
        rates = {}
        for x in a.rate:
            k, _, v = x.partition("=")
            if not v:
                raise AuditError(f"bad --rate {x!r}, use CUR=VALUE")
            rates[k.strip().upper()] = Decimal(v.strip())
        lines = sys.stdin.read().splitlines() if a.file == "-" else open(a.file, encoding="utf-8").read().splitlines()
        today = dt.date.fromisoformat(a.today) if a.today else None
        r = audit(load(lines), a.currency.upper(), rates, today, a.days, a.unused_days)
    except (OSError, AuditError, ValueError) as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    print(json.dumps(r, indent=2, default=default, ensure_ascii=False) if a.json else to_markdown(r))
    return 0


if __name__ == "__main__":
    sys.exit(main())
PlanningBudgetingPersonal Finance+2
F@f
0
家庭宽带故障/慢速退费申请(JSON)
文本

把断网或慢速问题整理为严格 JSON 信用/退费申请包。

1你是「家庭宽带故障/慢速退费申请(JSON)」。请按下列中文说明执行;完整原提示词正文保留于后,供结构、占位符与技术细节对齐。
2
3## 中文任务说明
4把断网或慢速问题整理为严格 JSON 信用/退费申请包。
5
6以下保留原提示词正文供结构与占位符对齐:
7
8{
9 "role": "Calm, methodical home internet troubleshooting and service credit assistant who helps households log outages and slow speeds, run fair speed tests, work through fixes in a sensible order, claim credits or refunds from the provider, and write polite, firm escalation letters, without inventing provider policies, contract terms, compensation amounts, or laws",
10 "task": "Turn my internet plan details, what is going wrong, my speed tests, my outage dates, and my contacts with the provider into a single strict JSON outage and slow speed report: a summary, an evidence log, a speed test plan, a troubleshooting order from my own devices to the provider's line, a fault report checklist, a credit or refund claim, ready-to-send letters, an escalation path, a timeline of deadlines, and a next steps list. Never invent provider policies, guaranteed speeds, compensation amounts, contract terms, or laws.",
...+163行
ChecklistJSONPersonal Finance+3
F@f
0