feat: implement subscription checkout flow with plan tracking - #25
feat: implement subscription checkout flow with plan tracking#25HyperRanger wants to merge 2 commits into
Conversation
- Add subscription checkout page with vendor validation and plan selection - Create subscription payment request endpoint (POST /subscriptions/payment-request) - Add subscription utilities and types for API communication - Track vendor subscription plan in database (free/pro) - Display subscription status in vendor profile - Update pricing component with 1% fee for Pro plan - Add PriceTier component to home page for non-authenticated users - Register subscriptions router in main FastAPI app
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR implements a complete vendor subscription upgrade system. The backend adds a new payment request API endpoint that validates vendors, creates database payment and transaction records, and returns checkout configuration. The frontend provides a checkout flow for vendors to upgrade to pro plans, includes session-aware pricing display on the home page, and adds a new product sales analytics dashboard for vendors to track performance metrics. ChangesSubscription Feature
Sequence Diagram(s)sequenceDiagram
participant Vendor as Vendor Client
participant PricingUI as Pricing Page
participant CheckoutUI as Checkout Page
participant ClientAction as createSubscriptionPaymentRequest
participant BackendAPI as POST /subscriptions/payment-request
participant VendorService as Vendor Service
participant Database as Database
participant PaymentCallback as Payment Callback Route
Vendor->>PricingUI: View Pro tier (not logged in)
PricingUI->>Vendor: Show "Upgrade to Pro" button
Vendor->>CheckoutUI: Click upgrade link to /subscription/checkout?plan=pro
CheckoutUI->>CheckoutUI: Validate session exists, user is vendor
Vendor->>CheckoutUI: Review plan details and click "Proceed to Payment"
CheckoutUI->>ClientAction: Call createSubscriptionPaymentRequest
ClientAction->>BackendAPI: POST vendor_id, plan, amount_kobo, currency
BackendAPI->>VendorService: Verify vendor exists
BackendAPI->>Database: Ensure subscription columns exist
BackendAPI->>Database: INSERT payment_requests row
BackendAPI->>Database: INSERT transactions row (pending)
BackendAPI->>Database: COMMIT transaction
BackendAPI->>ClientAction: Return payment_request_id and checkout config
ClientAction->>CheckoutUI: Resolve with response data
CheckoutUI->>PaymentCallback: Navigate to /payments/callback/{payment_request_id}
PaymentCallback->>Vendor: Initiate external payment processing
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/app/services/vendor_service.py (1)
156-165:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winInclude
subscription_planin the signup response too.
login_account()now returnssubscription_plan, butcreate_account()still drops it when shaping the vendor payload. Newly created vendor sessions will miss the new field until the user logs in again, which breaks the updated cross-layer contract for surfaced subscription state.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/app/services/vendor_service.py` around lines 156 - 165, The signup response shaping in vendor_service.py omits subscription_plan causing new accounts to lack that field; update the dict returned by the account-creation shaping (the function that currently returns keys like "user_id", "vendor_id", "role", "full_name", etc.) to include "subscription_plan": vendor.get("subscription_plan") so the create_account()/vendor payload matches login_account()'s contract and newly created sessions include the subscription state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/app/api/v1/routes_subscriptions.py`:
- Around line 103-130: The current flow calls conn.commit() before calling
build_checkout_config, so if build_checkout_config(...) fails the DB commit
remains and the endpoint returns 500 causing duplicate attempts; move the
conn.commit() to after you successfully construct checkout_config and the full
response payload (i.e., call build_checkout_config(...) first, then
conn.commit(), then return the dict). Ensure the except block still rolls back
on any error and reference the functions/variables payment_request_id,
build_checkout_config, checkout_config, conn.commit(), and the returned payload
to locate and reorder the operations.
- Around line 16-20: The request model CreateSubscriptionPaymentRequestBody
currently accepts amount_kobo and currency which lets clients tamper pricing;
remove amount_kobo and currency from that Pydantic model and change the
subscription payment endpoint handler that consumes it to validate plan against
an allowlist (e.g., {"free","pro"}), then compute the canonical amount_kobo and
currency server-side (lookup from your pricing table/constant) and use those
computed values when creating the checkout/persisting the payment (update any
calls in the handler that previously read amount_kobo/currency to use the
server-computed values); also apply the same change to the other handlers
referenced in the diff (the other subscription checkout/create flows around the
same file) and add/adjust tests to assert server-side pricing is enforced.
- Around line 23-38: The handler create_subscription_payment_request currently
trusts body.vendor_id and calls get_vendor_by_id; instead derive the vendor
identity from the authenticated session/token (e.g., use the existing auth
dependency like get_current_user or get_current_vendor_from_token) and use that
vendor for all operations; ignore or remove body.vendor_id, validate that
get_vendor_by_id(current_vendor.id) exists, and if the caller supplied a
different vendor_id reject with 403 (or simply ignore the supplied id and do not
allow creating payment requests for other vendors). Update the route signature
to accept the auth dependency and replace uses of body.vendor_id with the
authenticated vendor's id.
In `@backend/app/services/vendor_service.py`:
- Around line 39-41: The helper _ensure_vendor_subscription_columns currently
runs ALTER TABLE DDL during request handling; remove calls to
_ensure_vendor_subscription_columns from login/signup/subscription paths and
delete or disable that function in vendor_service.py, and instead add a
dedicated schema migration that performs the two ALTER TABLE ADD COLUMN IF NOT
EXISTS statements (setting subscription_plan TEXT DEFAULT 'free' and
subscription_started_at TEXT) with appropriate transactional/migration tooling
for your DB; after deploying the migration, ensure any code that previously
depended on _ensure_vendor_subscription_columns no longer invokes it (or falls
back safely) so runtime requests no longer execute DDL.
In `@web/app/page.tsx`:
- Around line 26-27: The PriceTier component is rendered twice for
unauthenticated users and also shown to authenticated users because of the
unconditional second render; remove the unconditional <PriceTier /> and ensure
PriceTier is only rendered inside the conditional that checks session (i.e.,
keep the {!session && <PriceTier />} usage, delete the standalone PriceTier
render) so PriceTier appears only for logged-out users.
- Around line 14-18: The subscription passed to useSyncExternalStore is a no-op
so session (from getCachedSession) never updates on login/logout; export and
implement a real subscription in web/lib/session.ts (e.g., subscribeToSession or
onSessionChange) that listens for auth/cookie/session updates and returns an
unsubscribe function, then change the useSyncExternalStore call in
web/app/page.tsx to pass that subscribe function as the first argument (keep
getCachedSession as the snapshot) so session re-renders when the session
changes.
In `@web/app/subscription/checkout/page.tsx`:
- Around line 144-147: Update the Pro plan fee text in the checkout page JSX:
locate the list item rendering the fee (the <li className="flex items-start
gap-2"> containing the span with "2% transaction fee (vs 3% for free)") in
web/app/subscription/checkout/page.tsx and change the copy to "1% transaction
fee (vs 3% for free)" so the displayed plan terms match the PR that sets Pro to
1%.
- Around line 27-38: The page currently uses planParam (raw search param) for
API requests even though plan is validated/fallback via SUBSCRIPTION_PLANS;
compute a validated plan key (e.g., validatedPlanKey = planParam in
SUBSCRIPTION_PLANS ? planParam : "pro") and use that validatedPlanKey for the
checkout POST and any other places that currently reference planParam (including
the occurrences around lines 95-99). Update uses of planParam to reference
validatedPlanKey so the API receives the normalized/validated plan key
consistent with the displayed plan (symbols to locate: planParam, plan,
SUBSCRIPTION_PLANS and the checkout POST call).
In `@web/app/vendors/products/page.tsx`:
- Around line 46-50: The route currently only redirects when session === null,
allowing authenticated non-vendor users to render the vendor page and block on
fetch; update the guards so only users with session.role === "vendor" proceed.
In the useEffect that currently checks session (the useEffect in page.tsx),
change the logic to redirect any non-vendor (session === null OR session.role
!== "vendor") to "/vendors/signup" (or the appropriate non-vendor route), and
update any data-fetching guards (the vendor products fetch/loader and any code
that uses vendor_id) to only run when session?.role === "vendor" and vendor_id
is present (e.g., session.user.vendor_id or the vendor_id prop), preventing
rendering of vendor components and fetch calls for buyers.
- Around line 142-144: The displayed completion percentage uses
product.completion_rate as a 0..1 ratio; multiply it by 100 before formatting
and appending '%' so 0.8 becomes 80.0%. Update the JSX that references
product.completion_rate (in the paragraph alongside product.paid_count) to use
(product.completion_rate * 100).toFixed(1) + '%' (or a small null-safe
expression like product.completion_rate != null ? (product.completion_rate *
100).toFixed(1) + '%' : '—') to render the correct percentage.
In `@web/components/home/price-tier.tsx`:
- Line 31: The Pro tier fee string in the PriceTier component is "1%" (symbol:
fee in web/components/home/price-tier.tsx) but the subscription checkout
included-features list (symbol: the transaction fee text in page.tsx) shows
"2%"; update one of these to match the other so messaging is consistent—either
change fee: "1%" to "2%" in the PriceTier component or update the checkout
page's transaction fee text to "1%", and ensure both the PriceTier component
(fee) and the checkout included-features text use the exact same percentage
string.
---
Outside diff comments:
In `@backend/app/services/vendor_service.py`:
- Around line 156-165: The signup response shaping in vendor_service.py omits
subscription_plan causing new accounts to lack that field; update the dict
returned by the account-creation shaping (the function that currently returns
keys like "user_id", "vendor_id", "role", "full_name", etc.) to include
"subscription_plan": vendor.get("subscription_plan") so the
create_account()/vendor payload matches login_account()'s contract and newly
created sessions include the subscription state.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1773249f-7cb8-49b5-b277-211c64ed506c
📒 Files selected for processing (9)
backend/app/api/v1/routes_subscriptions.pybackend/app/main.pybackend/app/services/vendor_service.pyweb/app/page.tsxweb/app/subscription/checkout/page.tsxweb/app/vendors/products/page.tsxweb/app/vendors/profile/page.tsxweb/components/home/price-tier.tsxweb/lib/actions/subscriptions.ts
| class CreateSubscriptionPaymentRequestBody(BaseModel): | ||
| vendor_id: str | ||
| plan: str # "free" or "pro" | ||
| amount_kobo: int | ||
| currency: str = "NGN" |
There was a problem hiding this comment.
Derive plan pricing server-side instead of trusting amount_kobo from the request.
A caller can send plan="pro" with any amount_kobo and those values are persisted and used for checkout as-is. That makes the upgrade price client-tamperable. Keep only the selected plan in the request, validate it against an allowlist, and compute the amount/currency on the server.
Also applies to: 67-80, 95-100
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/app/api/v1/routes_subscriptions.py` around lines 16 - 20, The request
model CreateSubscriptionPaymentRequestBody currently accepts amount_kobo and
currency which lets clients tamper pricing; remove amount_kobo and currency from
that Pydantic model and change the subscription payment endpoint handler that
consumes it to validate plan against an allowlist (e.g., {"free","pro"}), then
compute the canonical amount_kobo and currency server-side (lookup from your
pricing table/constant) and use those computed values when creating the
checkout/persisting the payment (update any calls in the handler that previously
read amount_kobo/currency to use the server-computed values); also apply the
same change to the other handlers referenced in the diff (the other subscription
checkout/create flows around the same file) and add/adjust tests to assert
server-side pricing is enforced.
| @router.post("/subscriptions/payment-request", status_code=201) | ||
| def create_subscription_payment_request(body: CreateSubscriptionPaymentRequestBody): | ||
| """ | ||
| Create a subscription payment request for upgrading to Pro plan. | ||
| Similar to payment requests but specifically for subscriptions. | ||
| """ | ||
| # Verify vendor exists | ||
| vendor = get_vendor_by_id(body.vendor_id) | ||
| if not vendor: | ||
| raise HTTPException( | ||
| status_code=404, | ||
| detail={ | ||
| "code": "VENDOR_NOT_FOUND", | ||
| "message": "Vendor not found.", | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Enforce vendor ownership on the backend, not just existence.
This route only checks that body.vendor_id exists. Any caller who knows another vendor's ID can create subscription payment rows for that account. The handler should derive the vendor from the authenticated session/token and reject mismatches instead of accepting vendor_id from the body.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/app/api/v1/routes_subscriptions.py` around lines 23 - 38, The handler
create_subscription_payment_request currently trusts body.vendor_id and calls
get_vendor_by_id; instead derive the vendor identity from the authenticated
session/token (e.g., use the existing auth dependency like get_current_user or
get_current_vendor_from_token) and use that vendor for all operations; ignore or
remove body.vendor_id, validate that get_vendor_by_id(current_vendor.id) exists,
and if the caller supplied a different vendor_id reject with 403 (or simply
ignore the supplied id and do not allow creating payment requests for other
vendors). Update the route signature to accept the auth dependency and replace
uses of body.vendor_id with the authenticated vendor's id.
| conn.commit() | ||
|
|
||
| # Build checkout config | ||
| checkout_config = build_checkout_config( | ||
| settings.kora_public_key, | ||
| get_kora_notification_url(), | ||
| kora_reference, | ||
| body.amount_kobo, | ||
| body.currency, | ||
| vendor.get("business_name") if hasattr(vendor, 'get') else vendor["business_name"], | ||
| vendor.get("email") if hasattr(vendor, 'get') else vendor.get("email"), | ||
| ) | ||
|
|
||
| return { | ||
| "payment_request_id": payment_request_id, | ||
| "kora_reference": kora_reference, | ||
| "status": "created", | ||
| "checkout_config": checkout_config | ||
| } | ||
|
|
||
| except Exception as e: | ||
| conn.rollback() | ||
| raise HTTPException( | ||
| status_code=500, | ||
| detail={ | ||
| "code": "SUBSCRIPTION_CREATION_FAILED", | ||
| "message": "Could not create subscription payment request.", | ||
| }, |
There was a problem hiding this comment.
Build the checkout payload before conn.commit().
If build_checkout_config(...) raises after Line 103, the endpoint returns 500 even though the payment_requests and transactions rows are already committed. A retry then creates duplicate pending subscription attempts. Commit only after the response payload is ready.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/app/api/v1/routes_subscriptions.py` around lines 103 - 130, The
current flow calls conn.commit() before calling build_checkout_config, so if
build_checkout_config(...) fails the DB commit remains and the endpoint returns
500 causing duplicate attempts; move the conn.commit() to after you successfully
construct checkout_config and the full response payload (i.e., call
build_checkout_config(...) first, then conn.commit(), then return the dict).
Ensure the except block still rolls back on any error and reference the
functions/variables payment_request_id, build_checkout_config, checkout_config,
conn.commit(), and the returned payload to locate and reorder the operations.
| def _ensure_vendor_subscription_columns(cursor) -> None: | ||
| cursor.execute("ALTER TABLE vendors ADD COLUMN IF NOT EXISTS subscription_plan TEXT DEFAULT 'free'", ()) | ||
| cursor.execute("ALTER TABLE vendors ADD COLUMN IF NOT EXISTS subscription_started_at TEXT", ()) |
There was a problem hiding this comment.
Move this schema change out of request handling.
ALTER TABLE ... ADD COLUMN IF NOT EXISTS is now part of login/signup/subscription flows through this helper. In Postgres that DDL takes a strong table lock and also requires schema-change privileges, so auth/checkout traffic can block or fail at runtime. This belongs in a migration, not an application request path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/app/services/vendor_service.py` around lines 39 - 41, The helper
_ensure_vendor_subscription_columns currently runs ALTER TABLE DDL during
request handling; remove calls to _ensure_vendor_subscription_columns from
login/signup/subscription paths and delete or disable that function in
vendor_service.py, and instead add a dedicated schema migration that performs
the two ALTER TABLE ADD COLUMN IF NOT EXISTS statements (setting
subscription_plan TEXT DEFAULT 'free' and subscription_started_at TEXT) with
appropriate transactional/migration tooling for your DB; after deploying the
migration, ensure any code that previously depended on
_ensure_vendor_subscription_columns no longer invokes it (or falls back safely)
so runtime requests no longer execute DDL.
| const session = useSyncExternalStore( | ||
| () => () => {}, | ||
| () => getCachedSession(), | ||
| () => null, | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether session store exposes a real subscribe API for useSyncExternalStore.
# Expected: a subscribe/listener function in web/lib/session.ts (or equivalent),
# otherwise this hook wiring is non-reactive by design.
fd -i 'session.ts' web | xargs -I{} sh -c 'echo "---- {} ----"; sed -n "1,260p" "{}" | rg -n "subscribe|listener|useSyncExternalStore|getCachedSession|getSession" -n -C 2'Repository: fisayo-dev/proofpay-ai
Length of output: 758
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## web/app/page.tsx: useSyncExternalStore wiring"
if [ -f web/app/page.tsx ]; then
sed -n '1,120p' web/app/page.tsx | nl -ba | sed -n '1,120p'
echo "----"
rg -n "useSyncExternalStore" web/app/page.tsx || true
rg -n "useSyncExternalStore\\(" web/app/page.tsx -n -C 3 || true
fi
echo "## web/lib/session.ts: full search for subscribe/listener APIs"
if [ -f web/lib/session.ts ]; then
echo "wc -l web/lib/session.ts"
wc -l web/lib/session.ts
echo "---- matching lines ----"
rg -n "subscribe|listener" web/lib/session.ts || true
echo "---- matching useSyncExternalStore related names ----"
rg -n "useSyncExternalStore|getCachedSession|getSession|SESSION_KEY" web/lib/session.ts -n || true
echo "---- full file small preview around caching ----"
sed -n '1,220p' web/lib/session.ts | nl -ba | sed -n '1,220p'
echo "---- tail preview ----"
tail -n 80 web/lib/session.ts | nl -ba
fi
echo "## find any session subscribe API in web/lib"
fd -i "session.ts" web | xargs -I{} sh -c 'echo "---- {} ----"; rg -n "subscribe|listener|getCachedSession|getSession|setSession|clearSession" "{}" || true'Repository: fisayo-dev/proofpay-ai
Length of output: 176
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## web/app/page.tsx: show around useSyncExternalStore call"
if [ -f web/app/page.tsx ]; then
sed -n '1,120p' web/app/page.tsx | awk '{printf "%4d:%s\n", NR, $0}'
echo "---- matches ----"
rg -n "useSyncExternalStore" web/app/page.tsx || true
echo "---- useSyncExternalStore(...) context ----"
rg -n "useSyncExternalStore\\(" web/app/page.tsx -C 3 || true
fi
echo "## web/lib/session.ts: full search for subscribe/listener APIs"
if [ -f web/lib/session.ts ]; then
echo "line_count=$(wc -l < web/lib/session.ts)"
echo "---- matching subscribe/listener in file ----"
rg -n "subscribe|listener" web/lib/session.ts || true
echo "---- matching useSyncExternalStore/getCachedSession/getSession/SESSION_KEY/setSession/clearSession ----"
rg -n "useSyncExternalStore|getCachedSession|getSession|SESSION_KEY|setSession|clearSession" web/lib/session.ts || true
echo "---- show the relevant cache section ----"
rg -n "getCachedSession" web/lib/session.ts -n -C 20 || true
fi
echo "## repo-wide: any session subscribe/listener in other session files"
fd -i "session.ts" web | xargs -I{} sh -c 'echo "---- {} ----"; rg -n "subscribe|listener" "{}" || true'Repository: fisayo-dev/proofpay-ai
Length of output: 3441
Fix useSyncExternalStore subscription so session re-renders on login/logout (web/app/page.tsx)
web/app/page.tsx wires useSyncExternalStore with a no-op subscribe (() => () => {}), while web/lib/session.ts only reads/caches cookies (getCachedSession) and exposes no subscribe/listener API—so session can stay stale until an unrelated re-render.
Current wiring
const session = useSyncExternalStore(
() => () => {},
() => getCachedSession(),
() => null,
);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/app/page.tsx` around lines 14 - 18, The subscription passed to
useSyncExternalStore is a no-op so session (from getCachedSession) never updates
on login/logout; export and implement a real subscription in web/lib/session.ts
(e.g., subscribeToSession or onSessionChange) that listens for
auth/cookie/session updates and returns an unsubscribe function, then change the
useSyncExternalStore call in web/app/page.tsx to pass that subscribe function as
the first argument (keep getCachedSession as the snapshot) so session re-renders
when the session changes.
| const planParam = (searchParams.get("plan") || "pro") as PlanKey; | ||
|
|
||
| const session = useSyncExternalStore( | ||
| () => () => {}, | ||
| () => getCachedSession(), | ||
| () => null, | ||
| ); | ||
|
|
||
| const [isLoading, setIsLoading] = useState(false); | ||
| const [error, setError] = useState<string | null>(null); | ||
|
|
||
| const plan = SUBSCRIPTION_PLANS[planParam] || SUBSCRIPTION_PLANS.pro; |
There was a problem hiding this comment.
Use the normalized plan key for the API call.
plan falls back to SUBSCRIPTION_PLANS.pro for display, but the request still sends raw planParam. For /subscription/checkout?plan=enterprise, the page renders Pro pricing and then posts plan: "enterprise". Send the validated key instead of the unchecked search param.
Also applies to: 95-99
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/app/subscription/checkout/page.tsx` around lines 27 - 38, The page
currently uses planParam (raw search param) for API requests even though plan is
validated/fallback via SUBSCRIPTION_PLANS; compute a validated plan key (e.g.,
validatedPlanKey = planParam in SUBSCRIPTION_PLANS ? planParam : "pro") and use
that validatedPlanKey for the checkout POST and any other places that currently
reference planParam (including the occurrences around lines 95-99). Update uses
of planParam to reference validatedPlanKey so the API receives the
normalized/validated plan key consistent with the displayed plan (symbols to
locate: planParam, plan, SUBSCRIPTION_PLANS and the checkout POST call).
| <li className="flex items-start gap-2"> | ||
| <span className="text-primary font-bold">✓</span> | ||
| <span>2% transaction fee (vs 3% for free)</span> | ||
| </li> |
There was a problem hiding this comment.
Update the Pro fee copy to match this PR.
This still says “2% transaction fee,” but the PR objective changes Pro to 1%. The checkout page will advertise the wrong plan terms unless this copy is updated too.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/app/subscription/checkout/page.tsx` around lines 144 - 147, Update the
Pro plan fee text in the checkout page JSX: locate the list item rendering the
fee (the <li className="flex items-start gap-2"> containing the span with "2%
transaction fee (vs 3% for free)") in web/app/subscription/checkout/page.tsx and
change the copy to "1% transaction fee (vs 3% for free)" so the displayed plan
terms match the PR that sets Pro to 1%.
| useEffect(() => { | ||
| if (session === null) { | ||
| router.push("/vendors/signup"); | ||
| } | ||
| }, [session, router]); |
There was a problem hiding this comment.
Gate this route to vendors before rendering.
Right now only session === null is redirected. An authenticated buyer session bypasses that check, never fetches because vendor_id is missing, and stays on this vendor-only page showing a perpetual “Loading your products...” state plus vendor CTAs. Add an explicit session.role === "vendor" guard before rendering/fetching.
Suggested fix
useEffect(() => {
- if (session === null) {
- router.push("/vendors/signup");
+ if (session === null || session.role !== "vendor") {
+ router.replace("/vendors/signup");
}
}, [session, router]);
useEffect(() => {
- if (!session?.vendor_id) return;
+ if (session?.role !== "vendor" || !session.vendor_id) {
+ setLoading(false);
+ setProducts([]);
+ return;
+ }
let ignore = false;
setLoading(true);Also applies to: 52-78, 78-100
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/app/vendors/products/page.tsx` around lines 46 - 50, The route currently
only redirects when session === null, allowing authenticated non-vendor users to
render the vendor page and block on fetch; update the guards so only users with
session.role === "vendor" proceed. In the useEffect that currently checks
session (the useEffect in page.tsx), change the logic to redirect any non-vendor
(session === null OR session.role !== "vendor") to "/vendors/signup" (or the
appropriate non-vendor route), and update any data-fetching guards (the vendor
products fetch/loader and any code that uses vendor_id) to only run when
session?.role === "vendor" and vendor_id is present (e.g.,
session.user.vendor_id or the vendor_id prop), preventing rendering of vendor
components and fetch calls for buyers.
| <p className="text-xs text-muted-foreground mt-1"> | ||
| {product.paid_count} completed sales · {product.completion_rate.toFixed(1)}% completion rate | ||
| </p> |
There was a problem hiding this comment.
Multiply completion_rate before appending %.
The backend contract returns completion_rate as a ratio in the 0..1 range, so this currently renders 0.8% for an 80% product instead of 80.0%.
Suggested fix
- {product.paid_count} completed sales · {product.completion_rate.toFixed(1)}% completion rate
+ {product.paid_count} completed sales · {(product.completion_rate * 100).toFixed(1)}% completion rate📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <p className="text-xs text-muted-foreground mt-1"> | |
| {product.paid_count} completed sales · {product.completion_rate.toFixed(1)}% completion rate | |
| </p> | |
| <p className="text-xs text-muted-foreground mt-1"> | |
| {product.paid_count} completed sales · {(product.completion_rate * 100).toFixed(1)}% completion rate | |
| </p> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/app/vendors/products/page.tsx` around lines 142 - 144, The displayed
completion percentage uses product.completion_rate as a 0..1 ratio; multiply it
by 100 before formatting and appending '%' so 0.8 becomes 80.0%. Update the JSX
that references product.completion_rate (in the paragraph alongside
product.paid_count) to use (product.completion_rate * 100).toFixed(1) + '%' (or
a small null-safe expression like product.completion_rate != null ?
(product.completion_rate * 100).toFixed(1) + '%' : '—') to render the correct
percentage.
| name: "Pro", | ||
| price: 1500, | ||
| fee: "2%", | ||
| fee: "1%", |
There was a problem hiding this comment.
Fee messaging is now inconsistent across upgrade screens.
Line 31 shows Pro as 1%, but web/app/subscription/checkout/page.tsx still states 2% transaction fee in the included-features list. Keep these values aligned to avoid pricing confusion.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/components/home/price-tier.tsx` at line 31, The Pro tier fee string in
the PriceTier component is "1%" (symbol: fee in
web/components/home/price-tier.tsx) but the subscription checkout
included-features list (symbol: the transaction fee text in page.tsx) shows
"2%"; update one of these to match the other so messaging is consistent—either
change fee: "1%" to "2%" in the PriceTier component or update the checkout
page's transaction fee text to "1%", and ensure both the PriceTier component
(fee) and the checkout included-features text use the exact same percentage
string.
Summary by CodeRabbit