feat(license): add Lite SKU; remove user-facing free trial
Two coupled changes:
1. Lite tier
- New Tier.LITE in src/license/schema.py.
- FEATURES_BY_TIER[Tier.LITE] = {Deduplicator, Text Cleaner,
Format Standardizer}. The three universally-useful tools that
cover the most common bookkeeping / RevOps / Klaviyo prep
workflows. Other six tools require Core.
- i18n: license.tier_lite, license.feature_locked_title,
license.feature_locked_body, license.upgrade_link,
license.status_locked (en + es).
- Per-tool feature gate at every GUI tool page
(require_feature_or_render_upgrade) and every tool CLI
(guard(feature=...)). A locked tool renders an upgrade
prompt + Manage-license button (GUI) or exits with code 2
(CLI).
- Home grid: tool cards the user's tier doesn't unlock get a
red 🔒 Locked badge in place of green Ready.
2. Trial removed
- Activation form's "Start 1-year trial" button removed.
- license_cli's `trial` subcommand removed.
- activation.trial_button / activation.trial_help i18n keys
dropped (pack parity test stays green).
- Tier.TRIAL stays in the enum (back-compat with any field-
tested trial licenses); LicenseManager._mint stays internal
for tests and the seller's key generator.
- Decision logged in DECISIONS §9b: a 1-year all-features
trial undercuts paid Lite; paid-only keeps tier economics
clean.
Tests (+29 net): +17 Lite-tier unit/guard tests + 13 Lite-tier
GUI tests + 1 trial-absent assertion - 2 trial CLI tests - 1
trial GUI button test. Total: 1995 → 2024.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,31 +59,18 @@ class TestLicenseCliStatus:
|
||||
assert data["days_remaining"] >= 0
|
||||
|
||||
|
||||
class TestLicenseCliTrial:
|
||||
def test_trial_issues_one_year_license(self, unactivated_license_manager):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(license_app, [
|
||||
"trial", "--name", "Trial User", "--email", "trial@example.com",
|
||||
])
|
||||
assert result.exit_code == 0
|
||||
assert "Trial issued" in result.stdout
|
||||
# And the manager now sees it as active.
|
||||
from src.license import LicenseManager
|
||||
mgr = LicenseManager()
|
||||
assert mgr.is_valid()
|
||||
lic = mgr.load()
|
||||
assert lic.tier.value == "trial"
|
||||
class TestNoTrialSubcommand:
|
||||
"""The ``trial`` subcommand was removed in v1.6 (no free trial —
|
||||
paid licenses only). Pin its absence so a future re-add has to be
|
||||
intentional."""
|
||||
|
||||
def test_trial_rejects_bad_email(self, unactivated_license_manager):
|
||||
def test_trial_subcommand_not_registered(self):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(license_app, [
|
||||
"trial", "--name", "T", "--email", "not-an-email",
|
||||
])
|
||||
assert result.exit_code == 2
|
||||
# ``typer.echo(..., err=True)`` lands in ``result.output`` when
|
||||
# ``mix_stderr`` is the default True; ``result.stdout`` only has
|
||||
# the bare stdout.
|
||||
assert "valid email" in result.output.lower()
|
||||
result = runner.invoke(license_app, ["trial", "--help"])
|
||||
assert result.exit_code != 0
|
||||
# Typer surfaces unknown commands with "No such command" or
|
||||
# similar — exact wording varies by version, so we just confirm
|
||||
# it's a usage error, not a successful execution.
|
||||
|
||||
|
||||
class TestLicenseCliActivate:
|
||||
|
||||
Reference in New Issue
Block a user