Three fixes to the Stripe subscription flow:
1. Change payment_behavior from 'default_incomplete' to
'error_if_incomplete' so Stripe charges the card immediately instead
of leaving the subscription in incomplete status waiting for frontend
payment confirmation that never happens.
2. Read currentPeriodStart/End from subscription items instead of the
top-level subscription object. Stripe moved these fields to
items.data[0] in API version 2025-03-31.basil, causing epoch-zero
dates (Dec 31, 1969).
3. Map Stripe 'incomplete' status to 'active' in mapStripeStatus() so
it doesn't fall through to the default 'canceled' mapping.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stripe requires payment methods to be attached to a customer before they
can be set as default_payment_method on a subscription. The
createSubscription() method was skipping this step, causing 500 errors
on checkout with: "The customer does not have a payment method with the
ID pm_xxx".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stripe Price IDs were hardcoded and duplicated across 4 compose files.
Log levels were hardcoded per-overlay instead of using generate-log-config.sh.
This refactors all environment-specific variables into a single .env file
that CI/CD generates from Gitea repo variables + generate-log-config.sh.
- Add .env.example template with documented variables
- Replace hardcoded values with ${VAR:-default} substitution in base compose
- Simplify prod overlay from 90 to 32 lines (remove redundant env blocks)
- Add YAML anchors to blue-green overlay (eliminate blue/green duplication)
- Remove redundant OCR env block from staging overlay
- Change generate-log-config.sh to output to stdout (pipe into .env)
- Update staging/production CI/CD to generate .env with Stripe + log vars
- Remove dangerous pk_live_ default from VITE_STRIPE_PUBLISHABLE_KEY
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Vehicles service and subscriptions code still queried user_profiles by
auth0_sub after the UUID migration, causing 500 errors on GET /api/vehicles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
16 controllers still used request.user.sub (Auth0 ID) instead of
request.userContext.userId (UUID) after the user_id column migration,
causing 500 errors on all authenticated endpoints including dashboard.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users with both auth0_sub and UUID rows in user_preferences get the same
user_profile_id after backfill, causing unique constraint violation on
rename. Keep the newest row per user_profile_id.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
user_preferences had rows where user_id already contained user_profiles.id
(UUID) instead of auth0_sub. Added second backfill pass matching UUID-format
values directly, and cleanup for 2 orphaned rows with no matching profile.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend test fixtures:
- Replace auth0|xxx format with UUID in all test userId values
- Update admin tests for new id/userProfileId schema
- Add missing deletionRequestedAt/deletionScheduledFor to auth test mocks
- Fix admin integration test supertest usage (app.server)
Frontend:
- AdminUser type: auth0Sub -> id + userProfileId
- admin.api.ts: all user management methods use userId (UUID) params
- useUsers/useAdmins hooks: auth0Sub -> userId/id in mutations
- AdminUsersPage + AdminUsersMobileScreen: user.auth0Sub -> user.id
- Remove encodeURIComponent (UUIDs don't need encoding)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- audit-log: JOIN on user_profiles.id instead of auth0_sub
- backup: use userContext.userId instead of auth0Sub
- ocr: use request.userContext.userId instead of request.user.sub
- user-profile controller: use getById() with UUID instead of getOrCreateProfile()
- user-profile service: accept UUID userId for all admin-focused methods
- user-profile repository: fix admin JOIN aliases from auth0_sub to id
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Migrate admin controller, routes, validation, and users controller
from auth0Sub identifiers to UUID. Admin CRUD now uses admin UUID id,
user management routes use user_profiles UUID. Clean up debug logging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Updated user-profile.repository.ts to use UUID instead of auth0_sub:
- Added getById(id) method for UUID-based lookups
- Changed all methods (except getByAuth0Sub, getOrCreate) to accept userId (UUID) instead of auth0Sub
- Updated SQL WHERE clauses from auth0_sub to id for UUID-based queries
- Fixed cross-table joins in listAllUsers and getUserWithAdminStatus to use user_profile_id
- Updated hardDeleteUser to use UUID for all DELETE statements
- Updated auth.plugin.ts to call updateEmail and updateEmailVerified with userId (UUID)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Auth plugin now uses profile.id (UUID) as userContext.userId instead
of raw JWT sub. Admin guard queries admin_users by user_profile_id.
Auth0 Management API calls continue using auth0Sub from JWT.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Multi-phase SQL migration converting all user_id columns from
VARCHAR(255) auth0_sub to UUID referencing user_profiles.id.
Restructures admin_users with UUID PK and user_profile_id FK.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete resolveStripeCustomerId() and replace with ensureStripeCustomer()
that includes orphaned Stripe customer cleanup on DB failure. Make
syncTierToUserProfile() blocking (errors propagate). Add null guards to
cancel/reactivate for admin-set subscriptions. Fix getInvoices() null
check. Clean controller comment. Add deleteCustomer() to StripeClient.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove admin_override_ placeholder from createForAdminOverride(), use NULL.
Update mapSubscriptionRow() with ?? null. Make stripeCustomerId optional
in create() method.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Make stripe_customer_id NULLABLE via migration, clean up admin_override_*
values to NULL, and update Subscription/SubscriptionResponse/UpdateSubscriptionData
types in both backend and frontend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The /guide SPA route conflicts with the static /guide/ screenshot directory.
Nginx's try_files $uri/ matches the directory and issues a 301 redirect to
/guide/ with trailing slash, bypassing SPA routing. Removing $uri/ ensures
all non-file paths fall through to index.html for client-side routing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create CLAUDE.md for GuidePage directory with architecture docs
- Create CLAUDE.md index for pages/ directory
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Guide link to public nav bar (desktop + mobile) in HomePage
- Add Guide link to authenticated sidebar in Layout.tsx
- Add Guide link to HamburgerDrawer with window.location guard
- Add GuidePage integration tests (6 test scenarios)
- Remove old PDF user guide at public/docs/v2026-01-03.pdf
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All 10 guide sections converted from USER-GUIDE.md to styled React
components using GuideTable and GuideScreenshot shared components.
Sections 1-5: Getting Started, Dashboard, Vehicles, Fuel Logs, Maintenance.
Sections 6-10: Gas Stations, Documents, Settings, Subscription Tiers, Mobile Experience.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create GuidePage with responsive layout (sticky TOC sidebar desktop, collapsible accordion mobile)
- Add GuideTableOfContents with scroll-based active section tracking
- Create GuideScreenshot and GuideTable shared components
- Add guideTypes.ts with section metadata for all 10 sections
- Add lazy-loaded /guide route in App.tsx with public access
- Placeholder section components for all 10 guide sections
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename "Schedule Maintenance" to "Add Maintenance", match contained
button style to "Add Fuel Log", and open inline MaintenanceRecordForm
dialog on click. Applied to both desktop and mobile views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: React fires child effects before parent effects. App's URL
sync effect called history.replaceState() on /callback, stripping the
?code= and &state= query params before Auth0Provider's useEffect could
read them via hasAuthParams(). The SDK fell through to checkSession()
instead of handleRedirectCallback(), silently failing with no error.
Guard both URL sync effects to skip on /callback, /signup, /verify-email.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>