Skip to content

Hosting: state the reservation grace window where it matters - #1439

Merged
feruzm merged 3 commits into
developfrom
feature/hosting-reservation-grace
Aug 12, 2026
Merged

Hosting: state the reservation grace window where it matters#1439
feruzm merged 3 commits into
developfrom
feature/hosting-reservation-grace

Conversation

@feruzm

@feruzm feruzm commented Aug 11, 2026

Copy link
Copy Markdown
Member

Closes #1415. Stacked on #1438 (base retargets to develop when that merges).

Investigating this issue showed the expiry lifecycle already exists end to end: the payment listener's sweep reclaims unpaid (inactive) reservations after ABANDONED_TENANT_GRACE_DAYS (fail-safe 7), the name frees after the re-registration quarantine, a reclaim overwrites the abandoned draft config and the ?resume= deep link is the return door, with the checkout grace-clock refresh protecting actively-paid reservations. What was missing is the messaging half: nothing user-facing said the reservation and its saved look are held for a limited window.

  • One shared reservationGraceDays() resolver in the subscription service now feeds both the sweep's configuration and a new reservation.graceDays field on GET /v1/payments/methods, so the number quoted to users cannot drift from the number the sweep enforces.
  • The signup payment step states it for fresh reservations: "This name and your saved look are held for N days. Complete payment to keep them; unpaid reservations are then released." Renewals are not reservations and stay silent.
  • The manage panel's awaiting-payment entries state the window next to the existing resume link.
  • Both surfaces stay silent when the API omits the field, so an older service degrades cleanly.

Tests: API methods endpoint pins the fail-safe 7 and a configured value; web specs pin the notice on the payment step for a fresh reservation and its absence without the field. Hosting API 417 tests, apps/web 2622, typechecks green.

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 11, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Hosting: surface reservation grace window in API and UI messaging

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Expose reservation grace window from hosting API via a single shared resolver.
• Show grace-day messaging in signup payment step and manage awaiting-payment entries.
• Add API and web tests to lock fallback/configured grace-day behavior.
Diagram

graph TD
  env{{"ABANDONED_TENANT_GRACE_DAYS"}} --> sub["reservationGraceDays()"] --> sweep["Payment sweep config"]
  sub --> methods["GET /v1/payments/methods"] --> client["web: paymentMethods()"]
  client --> signup(["Signup payment step"])
  client --> manage(["Manage: awaiting payment"])

  subgraph Legend
    direction LR
    _ext{{"External config"}} ~~~ _mod["Shared resolver"] ~~~ _ui(["UI surface"])
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Include graceDays on the tenants/list API
  • ➕ Avoids an extra /payments/methods request from the manage panel
  • ➕ Keeps manage view data in a single fetch
  • ➖ Expands a higher-traffic response shape
  • ➖ Still needs careful versioning/optional handling for older clients
2. Expose a dedicated hosting config endpoint
  • ➕ Clean separation between payment methods and policy/config metadata
  • ➕ Can grow to include other UI-needed policy values
  • ➖ Adds another endpoint and client integration for a single value today
  • ➖ Still requires ensuring the sweep and endpoint share the same resolver

Recommendation: Current approach is solid: one shared resolver prevents drift and the new API field is optional for safe rollout. If the manage panel’s conditional /methods fetch becomes a performance concern later, consider folding graceDays into the tenants payload to eliminate the extra call.

Files changed (9) +113 / -3

Enhancement (5) +44 / -1
payments.tsExpose reservation.graceDays on GET /v1/payments/methods +7/-0

Expose reservation.graceDays on GET /v1/payments/methods

• Adds a reservation.graceDays field to the payment methods response. The value is sourced from reservationGraceDays() to prevent drift from sweep behavior.

apps/self-hosted/hosting/api/src/routes/payments.ts

subscription.tsIntroduce reservationGraceDays() resolver with fail-safe default +10/-0

Introduce reservationGraceDays() resolver with fail-safe default

• Adds reservationGraceDays() which parses ABANDONED_TENANT_GRACE_DAYS with a default of 7. Documents the safety rationale to avoid sweeping all inactive reservations on misconfig.

apps/self-hosted/hosting/api/src/services/subscription.ts

hosting-api.tsExtend payment methods typing with optional reservation.graceDays +2/-0

Extend payment methods typing with optional reservation.graceDays

• Updates the HostingPaymentMethods interface to include an optional reservation.graceDays field. Enables UI to degrade cleanly when older API versions omit it.

apps/web/src/features/hosting-signup/hosting-api.ts

hosting-manage.tsxShow grace-window note for awaiting-payment reservations +16/-1

Show grace-window note for awaiting-payment reservations

• Conditionally fetches payment methods only when inactive tenants exist. Displays a translated grace-window note next to the existing resume-payment link when graceDays is provided.

apps/web/src/features/hosting-signup/hosting-manage.tsx

hosting-signup.tsxDisplay reservation hold notice on signup payment step +9/-0

Display reservation hold notice on signup payment step

• Adds a user-facing notice stating how long an unpaid reservation (and saved look) is held. The message is shown only for fresh reservations (not renewals) and only when graceDays is present.

apps/web/src/features/hosting-signup/hosting-signup.tsx

Refactor (1) +2 / -2
payment-listener.tsUse shared reservationGraceDays() for abandoned reservation sweep +2/-2

Use shared reservationGraceDays() for abandoned reservation sweep

• Replaces direct env parsing for ABANDONED_GRACE_DAYS with the shared reservationGraceDays() resolver. This keeps sweep enforcement aligned with the value exposed to clients.

apps/self-hosted/hosting/api/src/payment-listener.ts

Tests (2) +65 / -0
card-availability.test.tsAdd tests for reservation graceDays on payment methods endpoint +19/-0

Add tests for reservation graceDays on payment methods endpoint

• Introduces coverage asserting the methods endpoint reports the fail-safe default of 7 days. Adds a second test verifying a configured ABANDONED_TENANT_GRACE_DAYS value is reflected.

apps/self-hosted/hosting/api/src/routes/card-availability.test.ts

hosting-signup.spec.tsxAdd UI specs for grace notice presence/absence +46/-0

Add UI specs for grace notice presence/absence

• Adds a spec verifying the grace-window notice appears on the payment step for fresh reservations when graceDays is returned. Adds a second spec ensuring the UI stays silent when the API omits the field.

apps/web/src/specs/features/hosting-signup/hosting-signup.spec.tsx

Documentation (1) +2 / -0
en-US.jsonAdd English strings for reservation grace-window messaging +2/-0

Add English strings for reservation grace-window messaging

• Introduces new translation keys for the signup payment notice and the manage-panel awaiting-payment note. Both interpolate the grace-day count.

apps/web/src/features/i18n/locales/en-US.json

@qodo-code-review

qodo-code-review Bot commented Aug 11, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Env var not restored ✓ Resolved 🐞 Bug ☼ Reliability
Description
The new /v1/payments/methods reservation-window tests mutate process.env.ABANDONED_TENANT_GRACE_DAYS
but don’t restore its pre-test value (and cleanup isn’t guaranteed if the test throws), which can
leak state into later tests and cause flakiness.
Code

apps/self-hosted/hosting/api/src/routes/card-availability.test.ts[R55-58]

+    delete process.env.ABANDONED_TENANT_GRACE_DAYS;
+    process.env.HOSTING_INTERNAL_SECRET = STRONG;
+    const res = await paymentRoutes.request('http://localhost/methods');
+    const body = (await res.json()) as { reservation: { graceDays: number } };
Relevance

●●● Strong

State leakage via process.env in tests causes flakiness; team has accepted similar “restore previous
value in cleanup” fixes.

PR-#690

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The file’s afterEach restores only HOSTING_INTERNAL_SECRET, but the newly added tests modify
ABANDONED_TENANT_GRACE_DAYS and do not restore the original value, so later tests can observe a
changed environment.

apps/self-hosted/hosting/api/src/routes/card-availability.test.ts[13-18]
apps/self-hosted/hosting/api/src/routes/card-availability.test.ts[53-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`card-availability.test.ts` now adds tests that `delete`/`set` `process.env.ABANDONED_TENANT_GRACE_DAYS` but does not save and restore the original value. This can leak environment state to other tests in the same process. Additionally, in the “configured window” test, cleanup occurs only on the success path (if an assertion or request throws before the trailing `delete`, the env var remains modified).

### Issue Context
This file already uses an `afterEach` to restore `HOSTING_INTERNAL_SECRET`; the same pattern should be applied to `ABANDONED_TENANT_GRACE_DAYS`.

### Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/card-availability.test.ts[13-18]
- apps/self-hosted/hosting/api/src/routes/card-availability.test.ts[53-69]

### Suggested fix
- Capture `const originalAbandoned = process.env.ABANDONED_TENANT_GRACE_DAYS;` near the existing `original` secret capture.
- In `afterEach`, restore it similarly:
 - if `originalAbandoned === undefined`, `delete process.env.ABANDONED_TENANT_GRACE_DAYS`
 - else `process.env.ABANDONED_TENANT_GRACE_DAYS = originalAbandoned`
- Optionally, remove the per-test trailing `delete ...` once `afterEach` handles restoration, or wrap per-test env mutations in `try/finally` for extra safety.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Hardcoded useQuery key literal 📘 Rule violation ⚙ Maintainability
Description
The new useQuery call uses a hardcoded array literal as its queryKey instead of the shared
QueryKeys constants, which risks cache key drift and inconsistent invalidation. This violates the
requirement to centralize React Query keys via QueryKeys from @ecency/sdk.
Code

apps/web/src/features/hosting-signup/hosting-manage.tsx[35]

+    queryKey: ["hosting", "payment-methods"],
Relevance

● Weak

Close precedent: team previously rejected replacing hardcoded queryKey arrays with centralized
QueryKeys/constants (same file/pattern).

PR-#1153
PR-#706

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2667922 requires React Query keys to come from QueryKeys (from @ecency/sdk)
rather than hardcoded literals. The added useQuery block uses the inline literal `queryKey:
["hosting", "payment-methods"]`, directly violating that requirement.

Rule 2667922: Use QueryKeys constants for react-query keys instead of hardcoded literals
apps/web/src/features/hosting-signup/hosting-manage.tsx[35-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new React Query call uses an inline array literal for `queryKey` (`["hosting", "payment-methods"]`) instead of using centralized `QueryKeys` from `@ecency/sdk`.

## Issue Context
Compliance requires all React Query keys to be obtained from `QueryKeys` to prevent key drift and ensure consistent cache invalidation.

## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-manage.tsx[35-35]
- packages/sdk/src/modules/core/query-keys.ts[28-40]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context
✅ Compliance rules (platform): 75 rules
✅ Skills: 6 invoked
  add-feature
  add-query
  add-sdk-mutation
  add-test
  code-review
  debug
Review mode: ⚖️ Balanced: This is a behavioral change spanning the reservation-expiry service, payment API contract, and multiple UI surfaces; it has meaningful integration and compatibility risk, but not enough independent logic density to justify extended review.

Grey Divider

Tip of the day
💡 Did you know, you can enable the Remediation agent and Qodo fixes findings in a dedicated fix PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread apps/self-hosted/hosting/api/src/routes/card-availability.test.ts
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📎 Requirement gaps (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Env var not restored 🐞 Bug ☼ Reliability ⭐ New
Description
The new Vitest cases mutate process.env.ABANDONED_TENANT_GRACE_DAYS but the suite cleanup only
restores HOSTING_INTERNAL_SECRET, so any pre-existing ABANDONED_TENANT_GRACE_DAYS value from the
runner is lost after the suite and a mid-test failure can leak '14' into later tests.
Code

apps/self-hosted/hosting/api/src/routes/card-availability.test.ts[R55-57]

+    delete process.env.ABANDONED_TENANT_GRACE_DAYS;
+    process.env.HOSTING_INTERNAL_SECRET = STRONG;
+    const res = await paymentRoutes.request('http://localhost/methods');
Evidence
The suite’s afterEach only resets HOSTING_INTERNAL_SECRET, but the newly added tests delete/set
ABANDONED_TENANT_GRACE_DAYS and do not restore any pre-test value, so the environment is left
modified after tests (and can leak on failure).

apps/self-hosted/hosting/api/src/routes/card-availability.test.ts[13-18]
apps/self-hosted/hosting/api/src/routes/card-availability.test.ts[53-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`apps/self-hosted/hosting/api/src/routes/card-availability.test.ts` adds tests that delete/set `process.env.ABANDONED_TENANT_GRACE_DAYS`, but the suite’s `afterEach` only restores `HOSTING_INTERNAL_SECRET`. This breaks test isolation: an env-provided value is unconditionally cleared on success, and a failing test can leave `'14'` behind for subsequent tests.

## Issue Context
This file already snapshots/restores `HOSTING_INTERNAL_SECRET` via an `original` constant. Apply the same pattern to `ABANDONED_TENANT_GRACE_DAYS` (either snapshot once at module init, or snapshot per-test in `beforeEach`) and restore it in `afterEach`.

## Fix Focus Areas
- apps/self-hosted/hosting/api/src/routes/card-availability.test.ts[12-69]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Hardcoded useQuery key literal 📘 Rule violation ⚙ Maintainability
Description
The new useQuery call uses a hardcoded array literal as its queryKey instead of the shared
QueryKeys constants, which risks cache key drift and inconsistent invalidation. This violates the
requirement to centralize React Query keys via QueryKeys from @ecency/sdk.
Code

apps/web/src/features/hosting-signup/hosting-manage.tsx[35]

+    queryKey: ["hosting", "payment-methods"],
Relevance

● Weak

Close precedent: team previously rejected replacing hardcoded queryKey arrays with centralized
QueryKeys/constants (same file/pattern).

PR-#1153
PR-#706

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2667922 requires React Query keys to come from QueryKeys (from @ecency/sdk)
rather than hardcoded literals. The added useQuery block uses the inline literal `queryKey:
["hosting", "payment-methods"]`, directly violating that requirement.

Rule 2667922: Use QueryKeys constants for react-query keys instead of hardcoded literals
apps/web/src/features/hosting-signup/hosting-manage.tsx[35-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new React Query call uses an inline array literal for `queryKey` (`["hosting", "payment-methods"]`) instead of using centralized `QueryKeys` from `@ecency/sdk`.
## Issue Context
Compliance requires all React Query keys to be obtained from `QueryKeys` to prevent key drift and ensure consistent cache invalidation.
## Fix Focus Areas
- apps/web/src/features/hosting-signup/hosting-manage.tsx[35-35]
- packages/sdk/src/modules/core/query-keys.ts[28-40]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context
Review mode: ⚖️ Balanced: This is a behavior-changing cross-layer feature spanning the reservation sweep configuration, API contract, and two UI surfaces; it has real lifecycle and compatibility risk, but the logic is cohesive rather than dense enough to require redundant review passes.

Grey Divider

Tip of the day
💡 Did you know, you can group findings by type and pick your Finding display, from Minimal to Full

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

feruzm added 2 commits August 12, 2026 06:01
The expiry lifecycle already existed end to end (the sweep reclaims
unpaid reservations after ABANDONED_TENANT_GRACE_DAYS, the name frees
after quarantine and a reclaim overwrites the draft config), but
nothing user-facing said so: an owner had no way to know their
customized reservation is held for a limited window.

The window resolves through one shared reservationGraceDays() in the
subscription service, used by the sweep's config and now surfaced in
GET /v1/payments/methods, so the number quoted to users cannot drift
from the number the sweep enforces. The signup payment step states it
for fresh reservations (renewals are not reservations and stay silent),
and the manage panel's awaiting-payment entry states it next to the
resume link. Silent when an older service omits the field.

Part of #1415
@feruzm
feruzm force-pushed the feature/hosting-reservation-grace branch from fd0f2ec to ec4829e Compare August 12, 2026 06:02
Review finding: the notice keyed on a null renewal baseline, which is
also the state for expired and suspended tenants, so an owner renewing
an expired blog was told their name would be released after the window,
which is false: the sweep only reclaims inactive rows with no payments.
An explicit fresh-reservation flag is now set by each entry into the
payment step (fresh create and resume true, the 409 renewal path
false), pinned by a renewal spec. The window strings switch to count
based plurals so a one-day window does not read as 1 days.
Base automatically changed from feature/hosting-signup-customize to develop August 12, 2026 06:09
@feruzm
feruzm merged commit b088641 into develop Aug 12, 2026
4 checks passed
@feruzm
feruzm deleted the feature/hosting-reservation-grace branch August 12, 2026 06:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hosting: expire unpaid reservations and discard their draft config

1 participant