feat(admin): verifyOffice + real /admin/settings + public-search gating#16
Open
abdout wants to merge 14 commits into
Open
feat(admin): verifyOffice + real /admin/settings + public-search gating#16abdout wants to merge 14 commits into
abdout wants to merge 14 commits into
Conversation
…d-start The pg adapter holds long-lived TCP connections that Neon drops when its serverless compute scales to zero, causing the next query to fail with "Server has closed the connection". The neon serverless adapter speaks HTTPS+WS and wakes the compute on demand. Detect Neon URLs and switch automatically when DATABASE_URL_ADAPTER is unset. Pairs with a backfill of the missing Location.createdAt/updatedAt columns applied via Neon MCP — schema.prisma listed them but production never received the ALTER TABLE, so every Listing.findMany() failed once the nested location include resolved. Refs #4
…apes, restructure meta
- Replace pathname.startsWith("/ar") heuristic with the existing
reportIssue.* dictionary slice via useDictionary(). A single locale
source keeps the widget consistent with every other localized surface.
- Use literal Arabic glyphs in fallback handling instead of \u escape
sequences (carry-over from the previous edit; Arabic is now sourced
from the dictionary anyway).
- Pull browser/viewport/direction into a nested `meta` object on the
server action's input shape so the body fields are always emitted in
the same canonical order — needed by the kun report-agent parser.
- Self-bootstrap the `report` GitHub label on 422 with color #d93f0b so
the action works in any repo without manual seeding.
Refs #5
…link Mounts <ReportIssue variant="icon"/> as a fixed-position floating button in the locale-root layout so every one of the 117 locale-prefixed routes exposes the report widget. Previously only two footer components carried it, leaving auth, hosting, dashboard, transport, and admin surfaces with no path to file a bug. Adds the explicit Next 16 viewport export (width/initialScale/themeColor) that mobile Safari needs to render at the correct DPR and to honour prefers-color-scheme for the address bar. Moves the bilingual skip-link literal at layout.tsx:74 to common.skipToContent in both dictionaries — the only remaining inline ternary in the locale-root layout. Refs #5
…consolidated proxy - Flip `i18n.defaultLocale` from `en` to `ar` per the global rule. The proxy already cookie-pins NEXT_LOCALE, so visitors with a prior English preference are not redirected; only fresh, headerless visits land on /ar/*. - Make `useDictionary()` lenient — return the bundled English JSON as a static fallback when no DictionaryProvider is mounted instead of throwing, so test fixtures, isolated demos, and the root error page render strings. - Drop the duplicated `src/components/internationalization/middleware.ts`. Its `localizationMiddleware` export was never imported (proxy.ts is the live entry point in Next 16) and the parallel logic was a footgun. - Update `[lang]/layout.tsx` so the default-lang and default-config fallbacks reference `i18n.defaultLocale` instead of a hardcoded "en", and the hreflang `x-default` mirrors the same. Metadata API auto-emits the alternate hreflang links. - Add `src/lib/i18n/date-locale.ts` (`dateLocaleFor` + `intlLocaleFor`) and `src/components/ui/direction-aware-icon.tsx` so future code has one place to look for "date-fns locale per app locale" and "icon flip on RTL". Refs #7
…n, transport metadata, and skipToContent Adds the keys the upcoming refactor needs so per-page generateMetadata, filter panels, paginators, and the skip-link can pull from the dictionary instead of inline `lang === 'ar' ? ... : ...` ternaries: - `common.skipToContent` (replaces the bilingual literal at layout.tsx:74) - `pageMetadata.<page>` objects with `title` + `description` + (optional) `subtitle` for help / host / hosting / hostingListings / transportHost / transportOffices / transportCheckout / landing / login / join / reset / managersProperties / tenantsFavorites / tenantsResidences - `rental.property.filters` — `title`, `clearAll`, `any`, `priceRange` - `rental.property.pagination` — `previous`, `next`, `pageOf` - `transport.search.metadataTitle` + `metadataDescription` + `pagination` - `transport.routes.hoursFormat` + `currency` - `transport.metadataTitle` + `metadataDescription` Both en.json and ar.json updated in parity. Refs #7
…String calls
Every visible string and date now flows through the dictionary or through
the locale-aware formatter helpers. Concrete swaps:
- generateMetadata across 17 pages now pulls from `dictionary.pageMetadata.*`
instead of `lang === 'ar' ? "..." : "..."` ternaries.
- `transport/page.tsx`, `transport/search/page.tsx`, `listings/page.tsx`,
`transport/booking/checkout/content.tsx`, etc. drop their per-key
ternary fallbacks now that the dictionary has the keys upstream.
- Date display sites — bookings, transport-host, dashboard, tenants,
managers, admin tables, ApplicationCard — call `formatDate(date, lang)`
from `src/lib/i18n/formatters.ts` instead of `new Date(x).toLocaleDateString()`.
Admin tables receive `lang` as a prop from their server-page caller.
- Locale narrowing in client components (`useParams<{lang}>().lang === 'ar' ? 'ar' : 'en'`)
is replaced by `useLocale()` which returns the typed `Locale`.
- Data-shape ternaries (category.labelAr/label, tw.rangeAr/En, DEMO_DATA.ar/en)
are restructured to be locale-keyed objects so call sites do
`obj[lang]` instead of branching.
- The decorative `data-day` attribute in `ui/calendar.tsx` switches to a
stable ISO date so the value is locale-independent.
Closes the i18n anti-pattern audit's `inline lang ternaries` and
`raw toLocaleDateString calls` budgets at zero.
Refs #7
…t + lefthook gate - `ui/carousel.tsx`, `ui/calendar.tsx`, `listings/pagination.tsx`, `[lang]/hosting/calendar/page.tsx`, and the manager-applications back-button each pick up `rtl:rotate-180` so their chevrons and arrows point the right way under RTL. Decorative arrows whose meaning is rotation-independent (e.g. the ticket-page absolute-positioned arrow) are intentionally left as-is. - `HeartButton` and the `<ReportIssue variant="icon" />` trigger now wrap their svg/lucide icon in a 44x44 hit area (`h-11 w-11 inline-flex`), matching iOS HIG and WCAG 2.5.5 (Target Size, AA). - New `scripts/i18n-anti-pattern-check.sh` greps for inline lang ternaries, raw toLocaleDateString, raw toLocaleString, and likely-hardcoded English JSX. Each metric has an env-overridable budget; the budget can only shrink. Wired as `pnpm i18n:check` and added to lefthook pre-commit. - `tests/e2e/seo.spec.ts` extended with three new assertions: html lang+dir parity per locale (en/ltr, ar/rtl) and presence of the report-issue mount on every locale-prefixed surface. Refs #7
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final pieces of Phase C1: - Mobile listing detail now passes the latest 8 reviews + average rating + total count into <MobileReviews/>. The component drops its sample-data fallback and renders an empty state when there are no reviews. Review date is locale-aware via formatDate. - /host/content.tsx no-op `handleCreateFromExisting` TODO is replaced with a route to the host overview (where the existing listings are pickable). Clone-from-existing is filed as a follow-up. - New `docs/booking-vs-application.md` documents the dual-track rental flow (short-term Booking vs. long-term Application -> Lease) and the rule that they must not be mixed in a single UI flow. Refs #9
…ency log Schema additions for Phase C2: - TransportOffice gains `bankName`, `bankAccount`, `bankHolder`, `momoNumber`, `momoProvider` — per-office Sudan payment instructions so the bank-transfer card on the transport checkout reads from the operator's own row instead of the hardcoded `1234567890` that was sending every guest's money to one global mkan account. - New `WebhookEvent` model logs every Stripe (and future provider) webhook by `eventId @unique`. Inserts before processing so a duplicate Stripe delivery hits the unique constraint, the route returns 200, and side effects don't run twice. Migration `20260428103000_payments_complete` is idempotent (`ADD COLUMN IF NOT EXISTS`, `CREATE TABLE IF NOT EXISTS`) and was applied directly to prod via Neon MCP. Refs #14
…egration `src/lib/refund.ts` is a pure function (no Date, no DB, no fetch) that takes the listing's `cancellationPolicy`, the total paid, optional cleaning fee, and the hours-before-check-in, and returns a Stripe-ready refund breakdown (`refundAmount`, `refundAmountMinor`, human-readable `reason`, `isFullRefund`). Policy semantics mirror Airbnb so guests don't have to learn a new vocabulary: - Flexible: full up to 24h before, cleaning fee kept within 24h. - Moderate: full 5+ days before; 50% of nightly + cleaning between 5d–24h; none within 24h. - Firm: full 30+ days before; none within 30 days. - Strict: full 7+ days before; none within 7 days. - NonRefundable: never. `tests/lib/refund.test.ts` covers all five policies + null fallback + post-check-in lockout + Stripe minor-units rounding (14 cases, all pass). `cancelBooking` now computes the breakdown server-side and returns it in the action response so the client cancellation dialog can confirm the refund the guest will receive. The Stripe refund itself is still fired by admin via processRefund (the Booking model doesn't carry a payment_intent yet — tracked as a follow-up). Refs #14
…n checkout
- Stripe webhook handler inserts into `WebhookEvent` by `eventId @unique`
before running side effects. A duplicate delivery hits the unique
constraint, the handler short-circuits with `{ ok: true }`, and the
route returns 200 — no double-charged Payment rows, no double-paid
TransportPayment rows. Other DB errors still surface so Stripe retries.
- The transport-booking checkout's bank-transfer card now reads
`office.bankName`/`bankAccount`/`bankHolder` off the booking's nested
`trip.route.office` (relation already included in `getBooking`), and
shows a translated "operator hasn't published bank details yet"
fallback when the office row is empty. The hardcoded
`Bank of Khartoum / Mkan Transport Services / 1234567890` global
account is gone.
- `paymentSucceeded` and `paymentFailed` toast messages move from inline
`locale === 'ar' ? ... : ...` ternaries into the file-local
`paymentMethodTranslations` table so the i18n audit stays clean.
Refs #14
…earch-gating - New `verifyOffice(officeId)` and `unverifyOffice(officeId, reason?)` admin actions in `lib/actions/admin-actions.ts`. Verification flips `TransportOffice.isVerified` and audits the change. - `searchTrips` (and the routes it builds via `buildTripWhere`) now requires `route.office.isVerified=true` AND `office.isActive=true`. Unverified operators stay invisible to public guests even if their `isActive` flag was flipped on. Admin can still browse them via /admin/transport. - New singleton `PlatformSetting` model with platformFeePct, defaultCancellationPolicy, supportedCurrencies, payoutScheduleDays, emailFrom, supportEmail. The `getPlatformSettings` action lazily creates id=1 on first read so the rest of the app can rely on it always existing. - Migration `20260428113000_admin_settings` is applied to prod via Neon MCP. Refs #14
…sport/[id] - Replace /admin/settings "Coming soon" Card with a real tabbed form bound to `getPlatformSettings` / `updatePlatformSettings`. Six sections: platform fee %, default cancellation policy, supported currencies, payout schedule, outbound email From, support email. Saves via server action with toast. - New <VerifyOfficeButton/> client island on /admin/transport/[id] wraps verifyOffice / unverifyOffice in useTransition. Disabled state during the call; toast on success/error. Each click revalidates the public /transport/search and /transport/offices paths so the gate flips immediately. - Calendar's data-day attribute stays on locale-independent ISO date so the i18n audit doesn't regress (this kept reverting between sessions). Refs #14
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase C3 of the production-readiness push. Built on top of #6 + #8 + #10 + #15.
admin-actions.ts. Audited via the existing audit log.TransportOffice.isVerifiedis the gating signal for public search.searchTrips(and the route filter it builds viabuildTripWhere) now requiresroute.office.isVerified=trueANDroute.office.isActive=true. Unverified operators stay invisible to guests even if theirisActiveflag is flipped on. Admin can still browse them via /admin/transport./admin/transport/[id]: client island wraps the actions inuseTransition, toast on success/error, revalidates the public surfaces so the gate flips immediately./admin/settings: replaces the "Coming soon" Card with a six-section form (platform fee %, default cancellation policy, supported currencies, payout schedule, outbound email From, support email). Backed by a new singletonPlatformSettingmodel (id=1 always);getPlatformSettingslazily creates the row on first read so callers can rely on it always existing.Migrations:
20260428113000_admin_settings(PlatformSetting table) applied to prod via Neon MCP.Verification
pnpm typecheckclean.pnpm i18n:checkPASS.isVerifiedflips to true. Open /transport/search with that office's route → the trip is now visible. Click "Unverify" → trip disappears from search.Out of scope (followups)
/admin/homesfiltered-by-verified queue: Listings don't have a separateverifiedfield — they useisPublishedwhich is host-controlled. The existingforceUnpublishListingadmin action already covers the moderation use case. Document this in EPICS.getPlatformSettingsfrom booking/payout flows (so the platform fee actually applies): Phase D.Listing.cancellationPolicydefault override on listing creation: Phase D.Refs #7, #14