diff --git a/docs/superpowers/plans/2026-04-21-backup-days-param.md b/docs/superpowers/plans/2026-04-21-backup-days-param.md new file mode 100644 index 0000000..bffe958 --- /dev/null +++ b/docs/superpowers/plans/2026-04-21-backup-days-param.md @@ -0,0 +1,282 @@ +# Backup --days Parameter Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace `--start`/`--end` date range params on the `backup` command with a single `--days N` param that backs up all orders older than N days. + +**Architecture:** The `backup_orders` function currently takes `start`/`end` datetime pair and filters `[start, end)`. We change it to accept a single `cutoff` datetime and filter `OrderTime < cutoff`. The CLI computes `cutoff = today_00_utc - timedelta(days=N)`. + +**Tech Stack:** Python, Click, pytest + +--- + +### Task 1: Update `backup.py` — change function signature and filter logic + +**Files:** +- Modify: `src/order_bak/backup.py` + +- [ ] **Step 1: Write the failing test** + +In `tests/test_backup.py`, add a new test that uses the `cutoff` signature: + +```python +def test_backup_filters_by_cutoff(tmp_path): + scan_dir = tmp_path / "scan" + backup_dir = tmp_path / "backups" + scan_dir.mkdir() + backup_dir.mkdir() + + entities = [ + _make_entity("u1", "o1", datetime(2025, 1, 1, 10, tzinfo=timezone.utc), 1), + _make_entity("u2", "o2", datetime(2025, 1, 2, 8, tzinfo=timezone.utc), 1), + _make_entity("u3", "o3", datetime(2025, 1, 3, 8, tzinfo=timezone.utc), 1), # at/after cutoff + ] + scan_csv = scan_dir / "scan-2025-01-03.csv" + write_csv(scan_csv, entities) + + cutoff = datetime(2025, 1, 3, tzinfo=timezone.utc) + result = backup_orders( + scan_path=scan_csv, + backup_dir=backup_dir, + cutoff=cutoff, + ) + + assert result == 2 + assert (backup_dir / "2025-01-01.csv").exists() + assert (backup_dir / "2025-01-02.csv").exists() + assert not (backup_dir / "2025-01-03.csv").exists() +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `uv run pytest tests/test_backup.py::test_backup_filters_by_cutoff -v` +Expected: FAIL — `backup_orders()` got unexpected keyword `cutoff` + +- [ ] **Step 3: Update `backup_orders` signature and filter** + +In `src/order_bak/backup.py`, replace the function: + +```python +def backup_orders( + scan_path: Path, + backup_dir: Path, + cutoff: datetime, +) -> int: + backup_dir.mkdir(parents=True, exist_ok=True) + + by_date: dict[str, list[dict]] = defaultdict(list) + + with open(scan_path, newline="") as f: + reader = csv.DictReader(f) + for raw_row in reader: + entity = parse_row(raw_row) + if entity["state"] != 1: + continue + order_time = entity["OrderTime"] + if order_time is None: + continue + if order_time >= cutoff: + continue + date_key = order_time.strftime("%Y-%m-%d") + by_date[date_key].append(entity) + + total = 0 + for date_key, rows in sorted(by_date.items()): + csv_path = backup_dir / f"{date_key}.csv" + with open(csv_path, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=CSV_COLUMNS) + writer.writeheader() + writer.writerows(entity_to_row(r) for r in rows) + total += len(rows) + print(f" {date_key}: {len(rows)} orders") + + print(f"Backup complete: {total} orders across {len(by_date)} days") + return total +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `uv run pytest tests/test_backup.py::test_backup_filters_by_cutoff -v` +Expected: PASS + +- [ ] **Step 5: Update the old test to use `cutoff`** + +In `tests/test_backup.py`, replace `test_backup_filters_by_state_and_date`: + +```python +def test_backup_filters_by_state_and_date(tmp_path): + scan_dir = tmp_path / "scan" + backup_dir = tmp_path / "backups" + scan_dir.mkdir() + backup_dir.mkdir() + + entities = [ + _make_entity("u1", "o1", datetime(2025, 1, 1, 10, tzinfo=timezone.utc), 1), + _make_entity("u2", "o2", datetime(2025, 1, 1, 12, tzinfo=timezone.utc), 0), # Payed, skip + _make_entity("u3", "o3", datetime(2025, 1, 2, 8, tzinfo=timezone.utc), 1), + _make_entity("u4", "o4", datetime(2025, 1, 3, 8, tzinfo=timezone.utc), 1), # at cutoff, skip + ] + scan_csv = scan_dir / "scan-2025-01-03.csv" + write_csv(scan_csv, entities) + + result = backup_orders( + scan_path=scan_csv, + backup_dir=backup_dir, + cutoff=datetime(2025, 1, 3, tzinfo=timezone.utc), + ) + + assert result == 2 + jan1 = backup_dir / "2025-01-01.csv" + jan2 = backup_dir / "2025-01-02.csv" + assert jan1.exists() + assert jan2.exists() + assert not (backup_dir / "2025-01-03.csv").exists() + + from order_bak.csv_utils import read_csv + jan1_rows = list(read_csv(jan1)) + assert len(jan1_rows) == 1 + assert jan1_rows[0]["RowKey"] == "o1" +``` + +- [ ] **Step 6: Run all backup tests** + +Run: `uv run pytest tests/test_backup.py -v` +Expected: All PASS + +- [ ] **Step 7: Commit** + +```bash +git add src/order_bak/backup.py tests/test_backup.py +git commit -m "refactor: change backup_orders to use cutoff param instead of start/end" +``` + +--- + +### Task 2: Update `cli.py` — replace `--start`/`--end` with `--days` + +**Files:** +- Modify: `src/order_bak/cli.py` + +- [ ] **Step 1: Update the backup command in `cli.py`** + +Replace the `backup` command function in `src/order_bak/cli.py`: + +```python +@main.command() +@click.option("--days", required=True, type=int, help="Backup orders older than N days (min 1)") +@click.option("--scan", "scan_path", default=None, type=click.Path(), help="Path to scan CSV (default: latest in scan_dir)") +@click.pass_context +def backup(ctx, days, scan_path): + if days < 1: + click.echo("Error: --days must be at least 1") + raise SystemExit(1) + + cfg = load_config(ctx.obj["config_path"]) + from order_bak.backup import backup_orders + + if scan_path: + scan_file = Path(scan_path) + else: + scan_files = sorted(cfg.scan_dir.glob("scan-*.csv")) + if not scan_files: + click.echo("No scan files found in scan_dir") + raise SystemExit(1) + scan_file = scan_files[-1] + + from datetime import timedelta + today_utc = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + cutoff = today_utc - timedelta(days=days) + + click.echo(f"Backing up orders before {cutoff.strftime('%Y-%m-%d')} UTC") + backup_orders( + scan_path=scan_file, + backup_dir=Path("./backups"), + cutoff=cutoff, + ) +``` + +Add `from datetime import datetime, timedelta, timezone` to the top-level imports (replace the existing `from datetime import timezone`). + +- [ ] **Step 2: Run all tests** + +Run: `uv run pytest tests/ -v` +Expected: All PASS (backup tests already updated in Task 1) + +- [ ] **Step 3: Commit** + +```bash +git add src/order_bak/cli.py +git commit -m "feat: replace --start/--end with --days param on backup command" +``` + +--- + +### Task 3: Add edge case tests + +**Files:** +- Modify: `tests/test_backup.py` + +- [ ] **Step 1: Add edge case tests** + +Append to `tests/test_backup.py`: + +```python +def test_backup_cutoff_at_midnight(tmp_path): + """Order exactly at cutoff midnight should be excluded.""" + scan_dir = tmp_path / "scan" + backup_dir = tmp_path / "backups" + scan_dir.mkdir() + backup_dir.mkdir() + + entities = [ + _make_entity("u1", "o1", datetime(2025, 1, 3, 0, 0, 0, tzinfo=timezone.utc), 1), # exactly at cutoff + _make_entity("u2", "o2", datetime(2025, 1, 2, 23, 59, 59, tzinfo=timezone.utc), 1), # just before + ] + scan_csv = scan_dir / "scan.csv" + write_csv(scan_csv, entities) + + result = backup_orders( + scan_path=scan_csv, + backup_dir=backup_dir, + cutoff=datetime(2025, 1, 3, tzinfo=timezone.utc), + ) + + assert result == 1 + assert (backup_dir / "2025-01-02.csv").exists() + assert not (backup_dir / "2025-01-03.csv").exists() + + +def test_backup_empty_result(tmp_path): + """No orders before cutoff returns 0 and no files.""" + scan_dir = tmp_path / "scan" + backup_dir = tmp_path / "backups" + scan_dir.mkdir() + backup_dir.mkdir() + + entities = [ + _make_entity("u1", "o1", datetime(2025, 1, 5, 10, tzinfo=timezone.utc), 1), + ] + scan_csv = scan_dir / "scan.csv" + write_csv(scan_csv, entities) + + result = backup_orders( + scan_path=scan_csv, + backup_dir=backup_dir, + cutoff=datetime(2025, 1, 1, tzinfo=timezone.utc), + ) + + assert result == 0 + assert list(backup_dir.glob("*.csv")) == [] +``` + +- [ ] **Step 2: Run all tests** + +Run: `uv run pytest tests/test_backup.py -v` +Expected: All 4 tests PASS + +- [ ] **Step 3: Commit** + +```bash +git add tests/test_backup.py +git commit -m "test: add edge cases for backup cutoff filtering" +```