Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .llm/plans/completed/membership-rename/00-EXECUTION-ORDER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Execution order — membership rename

Phases are **sequential**. Do not start the next phase until the previous phase is complete and verified.

## Phase 1 — Schema (blocking)

- [01-schema-migrations.md](01-schema-migrations.md)

## Phase 2 — ORM entity and relations

- [02-orm-entity-and-relations.md](02-orm-entity-and-relations.md)

Depends on Phase 1 (table name in `@Entity`).

## Phase 3 — ORM services

- [03-orm-services.md](03-orm-services.md)

Depends on Phase 2 (`UserMembership` entity and `User.membership`).

## Phase 4 — API and management-api

- [04-api-and-management-usages.md](04-api-and-management-usages.md)

Depends on Phase 3 (`upsertMembership`, relation name).

## Phase 5 — Seed and E2E tooling

- [05-seed-and-e2e-tooling.md](05-seed-and-e2e-tooling.md)

Depends on Phase 1 (`user_membership` table exists in schema).

## After all phases

Operator verification (cumulative):

```bash
npm run build:packages
npm run lint
npm run test:unit
npm run test:e2e:api
make e2e_test_web_report_spec SPEC=e2e/auth-stale-cookies-redirect-login.spec.ts
```

For existing local Docker DB (table rename without full re-init), re-run `make local_db_init` or apply the one-off in [05-seed-and-e2e-tooling.md](05-seed-and-e2e-tooling.md).
41 changes: 41 additions & 0 deletions .llm/plans/completed/membership-rename/00-SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Membership rename plan set

Hard-break rename of `user_trust_settings` → `user_membership` and `UserTrustSettings` → `UserMembership` across schema, ORM, APIs, seeds, and E2E tooling. No fallbacks or legacy names.

## Recorded decisions

| Decision | Choice |
|----------|--------|
| Table / entity | `user_membership` / `UserMembership` (single 1:1 table, inline `membership_tier`) |
| Podverse mirror | Naming domain only (membership); **not** the two-table split (`account_membership` + `account_membership_status`) |
| Scope | Table, entity, relation, and code usages only |
| Out of scope | `packages/helpers/src/trust/` (entitlement capability keys + membership constants); management-web `trustTier*` i18n keys |
| Migration style | Edit original `CREATE TABLE` in `0003`; fold `0004` ALTER columns into `0003`; rename migration files; regenerate `0003a` baseline |

## Plan files

| File | Topic |
|------|--------|
| [01-schema-migrations.md](01-schema-migrations.md) | Linear SQL, kustomization, baseline regen |
| [02-orm-entity-and-relations.md](02-orm-entity-and-relations.md) | Entity, User relation, exports |
| [03-orm-services.md](03-orm-services.md) | UserService, renewal orchestrator, period extension |
| [04-api-and-management-usages.md](04-api-and-management-usages.md) | requireAuth, billing, management users |
| [05-seed-and-e2e-tooling.md](05-seed-and-e2e-tooling.md) | Local dev seed + E2E seed script |

## Dependency map

```mermaid
flowchart LR
schema[01_schema] --> ormEntity[02_orm_entity]
ormEntity --> ormServices[03_orm_services]
ormServices --> api[04_api_mgmt]
schema --> seed[05_seed]
```

## Integrates

- Local dev login fix: `localdev` gets `user_membership` row (premium, long expiry) in [0008_seed_local_user.sql](../../../infra/k8s/base/db/source/bootstrap/0008_seed_local_user.sql).

## Status

Implemented on branch; operator must re-init local DB or run one-off insert for existing Docker volumes.
45 changes: 45 additions & 0 deletions .llm/plans/completed/membership-rename/01-schema-migrations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 01 — Schema migrations

## Scope

Replace `user_trust_settings` with `user_membership` in the linear migration chain. Fold billing-renewal columns from `0004` into the `0003` `CREATE TABLE`. No ALTER-rename migration.

## Steps

1. **Replace** `infra/k8s/base/ops/source/database/linear-migrations/app/0003_user_trust_and_entitlement_overrides.sql` with `0003_user_membership.sql`:
- `CREATE TABLE user_membership` with all membership + renewal columns
- Constraints `chk_user_membership_*`
- Trigger `set_updated_at_user_membership`
- Backfill `INSERT INTO user_membership ... SELECT ... FROM "user"`
- Indexes `idx_user_membership_next_renewal_attempt_at`, `idx_user_membership_renewal_retry_backoff_until`

2. **Replace** `0004_billing_catalog_and_trust_renewal.sql` with `0004_billing_catalog_and_membership_renewal.sql`:
- Keep billing catalog tables and seeds
- Remove `ALTER TABLE user_trust_settings` and trust-named indexes

3. **Update** [infra/k8s/base/ops/kustomization.yaml](infra/k8s/base/ops/kustomization.yaml) ConfigMap file list for the two new filenames.

4. **Regenerate baseline** (operator or agent with Nix):

```bash
bash scripts/database/generate-linear-baseline.sh
```

Commit `infra/k8s/base/db/source/bootstrap/0003a_app_linear_baseline.sql.gz`.

## Key files

- `infra/k8s/base/ops/source/database/linear-migrations/app/0003_user_membership.sql`
- `infra/k8s/base/ops/source/database/linear-migrations/app/0004_billing_catalog_and_membership_renewal.sql`
- `infra/k8s/base/ops/kustomization.yaml`
- `infra/k8s/base/db/source/bootstrap/0003a_app_linear_baseline.sql.gz`

## Verification

```bash
bash scripts/database/validate-linear-migrations.sh
bash scripts/database/verify-linear-baseline.sh
gunzip -c infra/k8s/base/db/source/bootstrap/0003a_app_linear_baseline.sql.gz | rg user_membership
```

Expect `user_membership` in baseline; no `user_trust_settings`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 02 — ORM entity and relations

## Scope

Rename TypeORM entity and `User` relation from trust settings to membership.

## Steps

1. **Delete** `packages/orm/src/entities/UserTrustSettings.ts`.

2. **Add** `packages/orm/src/entities/UserMembership.ts`:
- `@Entity('user_membership')`
- Class `UserMembership`
- `@OneToOne('User', (u) => u.membership)`

3. **Update** [packages/orm/src/entities/User.ts](packages/orm/src/entities/User.ts):
- Import `UserMembership`
- Relation property `membership!: UserMembership`

4. **Update** [packages/orm/src/types/UserWithRelations.ts](packages/orm/src/types/UserWithRelations.ts):
- `membership: UserMembership | null`

5. **Update** [packages/orm/src/data-source.ts](packages/orm/src/data-source.ts) entity list.

6. **Update** [packages/orm/src/index.ts](packages/orm/src/index.ts):
- Export `UserMembership` (not `UserTrustSettings`)

## Key files

- `packages/orm/src/entities/UserMembership.ts`
- `packages/orm/src/entities/User.ts`
- `packages/orm/src/types/UserWithRelations.ts`
- `packages/orm/src/data-source.ts`
- `packages/orm/src/index.ts`

## Verification

```bash
npm run build -w @metaboost/orm
rg "UserTrustSettings|trustSettings" packages/orm/src
```

Expect no matches.
36 changes: 36 additions & 0 deletions .llm/plans/completed/membership-rename/03-orm-services.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 03 — ORM services

## Scope

Update services that read/write membership rows and raw SQL against the table.

## Steps

1. **UserService** ([packages/orm/src/services/UserService.ts](packages/orm/src/services/UserService.ts)):
- `USER_RELATIONS`: `'membership'`
- Repository `UserMembership`
- Rename `upsertTrustSettings` → `upsertMembership`

2. **BillingRenewalOrchestratorService** ([packages/orm/src/services/billingRenewalOrchestrator.ts](packages/orm/src/services/billingRenewalOrchestrator.ts)):
- `FROM user_membership` in raw SQL
- `getRepository(UserMembership)`

3. **MembershipPeriodExtensionService** ([packages/orm/src/services/membershipPeriodExtension.ts](packages/orm/src/services/membershipPeriodExtension.ts)):
- `getRepository(UserMembership)`
- Local variable `membership` (not `trust`)
- Error messages reference "membership"

## Key files

- `packages/orm/src/services/UserService.ts`
- `packages/orm/src/services/billingRenewalOrchestrator.ts`
- `packages/orm/src/services/membershipPeriodExtension.ts`

## Verification

```bash
npm run build -w @metaboost/orm
rg "user_trust_settings|UserTrustSettings|trustSettings" packages/orm/src
```

Expect no matches.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 04 — API and management-api usages

## Scope

Update auth middleware, billing read model, management user CRUD, and integration tests.

## Steps

1. **requireAuth** ([apps/api/src/middleware/requireAuth.ts](apps/api/src/middleware/requireAuth.ts)):
- `Reflect.get(value, 'membership')` for `membershipExpiresAt`

2. **billingController** ([apps/api/src/controllers/billingController.ts](apps/api/src/controllers/billingController.ts)):
- `user.membership`

3. **billingReadModelSerialization** ([apps/api/src/lib/billingReadModelSerialization.ts](apps/api/src/lib/billingReadModelSerialization.ts)):
- `params.user.membership`
- Rename `premiumBillingCadenceFromTrust` → `premiumBillingCadenceFromMembership`

4. **management usersController** ([apps/management-api/src/controllers/usersController.ts](apps/management-api/src/controllers/usersController.ts)):
- `user.membership` in `userToJson`
- `leftJoinAndSelect('user.membership', 'membership')`
- `UserService.upsertMembership`

5. **Integration test** ([apps/api/src/test/billing-renewal-orchestrator.test.ts](apps/api/src/test/billing-renewal-orchestrator.test.ts)):
- SQL `user_membership`
- `refreshed?.membership`

## Key files

- `apps/api/src/middleware/requireAuth.ts`
- `apps/api/src/controllers/billingController.ts`
- `apps/api/src/lib/billingReadModelSerialization.ts`
- `apps/management-api/src/controllers/usersController.ts`
- `apps/api/src/test/billing-renewal-orchestrator.test.ts`

## Verification

```bash
npm run build -w apps/api
npm run build -w apps/management-api
npm run test:e2e:api
```
52 changes: 52 additions & 0 deletions .llm/plans/completed/membership-rename/05-seed-and-e2e-tooling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 05 — Seed and E2E tooling

## Scope

Local dev bootstrap user and Playwright E2E seed use `user_membership`. Integrates deferred login fix for `localdev@example.com`.

## Steps

1. **Local Docker bootstrap** [infra/k8s/base/db/source/bootstrap/0008_seed_local_user.sql](infra/k8s/base/db/source/bootstrap/0008_seed_local_user.sql):
- After `user_bio`, insert premium membership with long expiry:

```sql
WITH u AS (SELECT user_id AS id FROM user_credentials WHERE email = 'localdev@example.com')
INSERT INTO user_membership (user_id, membership_tier, membership_expires_at, auto_renew)
SELECT id, 'premium', NOW() + INTERVAL '100 years', false FROM u
ON CONFLICT (user_id) DO NOTHING;
```

- Keep `user_terms_acceptance` insert for terms gate.

2. **E2E seed** [tools/web/seed-e2e.mjs](tools/web/seed-e2e.mjs):
- `INSERT INTO user_membership` (replace `user_trust_settings`)

## Operator — existing local DB (before re-init)

After code deploy, if the volume still has `user_trust_settings`, **re-init** is the clean path:

```bash
make local_db_init
```

Or one-off on fresh schema (`user_membership` table):

```bash
docker exec -i metaboost_local_postgres psql -U metaboost_app_owner -d metaboost_app <<'SQL'
WITH u AS (SELECT user_id AS id FROM user_credentials WHERE email = 'localdev@example.com')
INSERT INTO user_membership (user_id, membership_tier, membership_expires_at, auto_renew)
SELECT id, 'premium', NOW() + INTERVAL '100 years', false FROM u
ON CONFLICT (user_id) DO NOTHING;
SQL
```

Clear cookies for `http://localhost:4002` and log in with `localdev@example.com` / `Test!1Aa`.

## Verification

```bash
make e2e_seed
make e2e_test_web_report_spec SPEC=e2e/auth-stale-cookies-redirect-login.spec.ts
```

Open `.artifacts/e2e-reports/latest/web/index.html`.
53 changes: 53 additions & 0 deletions .llm/plans/completed/membership-rename/COPY-PASTA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# COPY-PASTA — membership rename

Execute prompts in order. Mark `[x]` when complete. Move completed numbered files to `.llm/plans/completed/membership-rename/` per plan-execution-completion-tracking.

---

## Phase 1 — Schema

- [x] **Prompt 1** — Implement [01-schema-migrations.md](01-schema-migrations.md): replace `0003`/`0004` migration files, update kustomization, regenerate `0003a` baseline.

---

## Phase 2 — ORM entity

- [x] **Prompt 2** — Implement [02-orm-entity-and-relations.md](02-orm-entity-and-relations.md): `UserMembership` entity, `User.membership`, exports.

---

## Phase 3 — ORM services

- [x] **Prompt 3** — Implement [03-orm-services.md](03-orm-services.md): `UserService.upsertMembership`, renewal orchestrator, period extension.

---

## Phase 4 — API and management-api

- [x] **Prompt 4** — Implement [04-api-and-management-usages.md](04-api-and-management-usages.md): requireAuth, billing, management users, integration tests.

---

## Phase 5 — Seed and E2E

- [x] **Prompt 5** — Implement [05-seed-and-e2e-tooling.md](05-seed-and-e2e-tooling.md): `0008` local user membership seed, `tools/web/seed-e2e.mjs`.

---

## Cumulative verification (operator)

```bash
npm run build:packages
npm run lint
npm run test:unit
npm run test:e2e:api
make e2e_test_web_report_spec SPEC=e2e/auth-stale-cookies-redirect-login.spec.ts
```

For local dev login after rename:

```bash
make local_db_init
```

Or apply the one-off in [05-seed-and-e2e-tooling.md](05-seed-and-e2e-tooling.md), then restart `npm run dev:all:watch`.
6 changes: 3 additions & 3 deletions apps/api/src/controllers/billingController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ export async function membershipSummary(req: Request, res: Response): Promise<vo
res.status(401).json({ message: 'Authentication required' });
return;
}
const trust = user.trustSettings;
if (trust === undefined) {
res.status(404).json({ message: 'Membership settings not found' });
const membership = user.membership;
if (membership === undefined || membership === null) {
res.status(404).json({ message: 'Membership not found' });
return;
}

Expand Down
Loading
Loading