From 9764ccfbad72e9e6b7eb396ac628e3aa4de54197 Mon Sep 17 00:00:00 2001 From: barsa Date: Mon, 15 Dec 2025 10:32:07 +0900 Subject: [PATCH] Update .gitignore to include SHA256 checksum files and refresh address handling in hooks - Added '*.tar.gz.sha256' to .gitignore to exclude SHA256 checksum files from version control. - Updated SHA256 checksums for the latest portal backend and frontend tar.gz files to reflect new builds. - Enhanced address handling in `useAddressEdit`, `useProfileData`, and `AddressConfirmation` components to invalidate catalog queries upon address updates, ensuring accurate server-personalized results. - Introduced new query key for catalog queries in the API to streamline cache management. --- .gitignore | 1 + .../features/account/hooks/useAddressEdit.ts | 17 +++-- .../features/account/hooks/useProfileData.ts | 5 ++ .../components/base/AddressConfirmation.tsx | 6 ++ apps/portal/src/lib/api/index.ts | 1 + apps/portal/src/lib/api/runtime/client.ts | 3 +- docs/orders/PORTAL-ORDERING-PROVISIONING.md | 4 +- docs/portal/PORTAL-ROADMAP.md | 2 +- docs/portal/RECOMMENDED-LIB-STRUCTURE.md | 66 ++++++++++--------- portal-backend.latest.tar.gz.sha256 | 2 +- portal-frontend.latest.tar.gz.sha256 | 2 +- 11 files changed, 66 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index 2ac8b3b3..9a474469 100644 --- a/.gitignore +++ b/.gitignore @@ -156,6 +156,7 @@ prisma/migrations/dev.db* # Large archive files *.tar *.tar.gz +*.tar.gz.sha256 *.zip # API Documentation (contains sensitive API details) diff --git a/apps/portal/src/features/account/hooks/useAddressEdit.ts b/apps/portal/src/features/account/hooks/useAddressEdit.ts index b33cb069..34261a02 100644 --- a/apps/portal/src/features/account/hooks/useAddressEdit.ts +++ b/apps/portal/src/features/account/hooks/useAddressEdit.ts @@ -1,6 +1,8 @@ "use client"; import { useCallback } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { queryKeys } from "@/lib/api"; import { accountService } from "@/features/account/services/account.service"; import { addressFormSchema, @@ -10,10 +12,17 @@ import { import { useZodForm } from "@/hooks/useZodForm"; export function useAddressEdit(initial: AddressFormData) { - const handleSave = useCallback(async (formData: AddressFormData) => { - const requestData = addressFormToRequest(formData); - await accountService.updateAddress(requestData); - }, []); + const queryClient = useQueryClient(); + + const handleSave = useCallback( + async (formData: AddressFormData) => { + const requestData = addressFormToRequest(formData); + await accountService.updateAddress(requestData); + // Address changes can affect server-personalized catalog results (eligibility). + await queryClient.invalidateQueries({ queryKey: queryKeys.catalog.all() }); + }, + [queryClient] + ); return useZodForm({ schema: addressFormSchema, diff --git a/apps/portal/src/features/account/hooks/useProfileData.ts b/apps/portal/src/features/account/hooks/useProfileData.ts index c2f729cb..735c7778 100644 --- a/apps/portal/src/features/account/hooks/useProfileData.ts +++ b/apps/portal/src/features/account/hooks/useProfileData.ts @@ -1,6 +1,8 @@ "use client"; import { useEffect, useState, useCallback } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { queryKeys } from "@/lib/api"; import { useAuthStore } from "@/features/auth/services/auth.store"; import { accountService } from "@/features/account/services/account.service"; import { logger } from "@/lib/logger"; @@ -10,6 +12,7 @@ import type { ProfileEditFormData, Address } from "@customer-portal/domain/custo export function useProfileData() { const { user } = useAuthStore(); + const queryClient = useQueryClient(); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isSavingProfile, setIsSavingProfile] = useState(false); @@ -112,6 +115,8 @@ export function useProfileData() { phoneNumber: next.phoneNumber, phoneCountryCode: next.phoneCountryCode, }); + // Address changes can affect server-personalized catalog results (eligibility). + await queryClient.invalidateQueries({ queryKey: queryKeys.catalog.all() }); setBillingInfo({ address: next }); setAddress(next); return true; diff --git a/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx b/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx index 2b4e0e80..67b2721e 100644 --- a/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx +++ b/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx @@ -6,11 +6,13 @@ import { Button } from "@/components/atoms/button"; import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { useState, useEffect, useCallback } from "react"; +import { useQueryClient } from "@tanstack/react-query"; import { accountService } from "@/features/account/services/account.service"; import { log } from "@/lib/logger"; import { StatusPill } from "@/components/atoms/status-pill"; import { MapPinIcon, PencilIcon, CheckIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { COUNTRY_OPTIONS, getCountryName } from "@/lib/constants/countries"; +import { queryKeys } from "@/lib/api"; // Use canonical Address type from domain import type { Address } from "@customer-portal/domain/customer"; @@ -37,6 +39,7 @@ export function AddressConfirmation({ orderType, embedded = false, }: AddressConfirmationProps) { + const queryClient = useQueryClient(); const [billingInfo, setBillingInfo] = useState(null); const [loading, setLoading] = useState(true); const [editing, setEditing] = useState(false); @@ -152,6 +155,9 @@ export function AddressConfirmation({ // Persist to server (WHMCS via BFF) const updatedAddress = await accountService.updateAddress(sanitizedAddress); + // Address changes can affect server-personalized catalog results (eligibility). + await queryClient.invalidateQueries({ queryKey: queryKeys.catalog.all() }); + // Rebuild BillingInfo from updated address const updatedInfo: BillingInfo = { company: null, diff --git a/apps/portal/src/lib/api/index.ts b/apps/portal/src/lib/api/index.ts index 6c3bb677..f6148197 100644 --- a/apps/portal/src/lib/api/index.ts +++ b/apps/portal/src/lib/api/index.ts @@ -140,6 +140,7 @@ export const queryKeys = { summary: () => ["dashboard", "summary"] as const, }, catalog: { + all: () => ["catalog"] as const, products: () => ["catalog", "products"] as const, internet: { combined: () => ["catalog", "internet", "combined"] as const, diff --git a/apps/portal/src/lib/api/runtime/client.ts b/apps/portal/src/lib/api/runtime/client.ts index d71f33ae..aefda618 100644 --- a/apps/portal/src/lib/api/runtime/client.ts +++ b/apps/portal/src/lib/api/runtime/client.ts @@ -284,8 +284,7 @@ export function createClient(options: CreateClientOptions = {}): ApiClient { opts: RequestOptions = {} ): Promise> => { const resolvedPath = applyPathParams(path, opts.params?.path); - const normalizedPath = normalizeApiPath(resolvedPath); - const url = new URL(normalizedPath, baseUrl); + const url = new URL(resolvedPath, baseUrl); const queryString = buildQueryString(opts.params?.query); if (queryString) { diff --git a/docs/orders/PORTAL-ORDERING-PROVISIONING.md b/docs/orders/PORTAL-ORDERING-PROVISIONING.md index 1b9a011c..3ec5882a 100644 --- a/docs/orders/PORTAL-ORDERING-PROVISIONING.md +++ b/docs/orders/PORTAL-ORDERING-PROVISIONING.md @@ -42,7 +42,7 @@ We require a Customer Number (SF Number) at signup and gate checkout on the pres - Mapping is stored: `portalUserId ↔ whmcsClientId ↔ sfAccountId`. 2. Add payment method (required before checkout) - - Portal shows an “Add payment method” CTA that opens WHMCS payment methods via SSO (`POST /auth/sso-link` → `index.php?rp=/account/paymentmethods`). + - Portal shows an “Add payment method” CTA that opens WHMCS payment methods via SSO (`POST /api/auth/sso-link` → `index.php?rp=/account/paymentmethods`). - Portal checks `GET /billing/payment-methods/summary` to confirm presence before enabling checkout. 3. Browse catalog and configure @@ -138,7 +138,7 @@ Implementation notes: - `GET /billing/payment-methods/summary` (new) - Returns `{ hasPaymentMethod: boolean }` using WHMCS `GetPayMethods` for mapped client. -- `POST /auth/sso-link` (exists) +- `POST /api/auth/sso-link` (exists) - Used to open WHMCS payment methods and invoice/pay pages. ### 2.5 Catalog (Salesforce Product2 as Source of Truth) diff --git a/docs/portal/PORTAL-ROADMAP.md b/docs/portal/PORTAL-ROADMAP.md index 9c4e9977..3da47f14 100644 --- a/docs/portal/PORTAL-ROADMAP.md +++ b/docs/portal/PORTAL-ROADMAP.md @@ -30,7 +30,7 @@ This roadmap references `PORTAL-ORDERING-PROVISIONING.md` (complete flows and ar 5. Portal UI: Address & payment method - Address step after signup; `PATCH /api/me/address` to update address fields. - - Payment methods page/button: `POST /auth/sso-link` to WHMCS payment methods; show banner on dashboard until `GET /billing/payment-methods/summary` is true. + - Payment methods page/button: `POST /api/auth/sso-link` to WHMCS payment methods; show banner on dashboard until `GET /billing/payment-methods/summary` is true. ## Phase 3 – Catalog diff --git a/docs/portal/RECOMMENDED-LIB-STRUCTURE.md b/docs/portal/RECOMMENDED-LIB-STRUCTURE.md index 4bafc7f4..9b762b4e 100644 --- a/docs/portal/RECOMMENDED-LIB-STRUCTURE.md +++ b/docs/portal/RECOMMENDED-LIB-STRUCTURE.md @@ -63,23 +63,23 @@ apps/portal/src/ ### ✅ `lib/` - Truly Generic, Reusable Across Features -| File | Purpose | Used By | -|------|---------|---------| -| `lib/api/client.ts` | API client instance | All features | -| `lib/api/query-keys.ts` | React Query keys factory | All features | -| `lib/api/helpers.ts` | `getDataOrThrow`, `getDataOrDefault` | All features | -| `lib/utils/cn.ts` | Tailwind className merger | All components | -| `lib/utils/error-handling.ts` | Generic error parsing | All features | -| `lib/providers.tsx` | Root providers (QueryClient, Theme) | App root | +| File | Purpose | Used By | +| ----------------------------- | ------------------------------------ | -------------- | +| `lib/api/client.ts` | API client instance | All features | +| `lib/api/query-keys.ts` | React Query keys factory | All features | +| `lib/api/helpers.ts` | `getDataOrThrow`, `getDataOrDefault` | All features | +| `lib/utils/cn.ts` | Tailwind className merger | All components | +| `lib/utils/error-handling.ts` | Generic error parsing | All features | +| `lib/providers.tsx` | Root providers (QueryClient, Theme) | App root | ### ✅ `features/*/hooks/` - Feature-Specific Hooks -| File | Purpose | Used By | -|------|---------|---------| -| `features/billing/hooks/useBilling.ts` | Invoice queries & mutations | Billing pages only | -| `features/subscriptions/hooks/useSubscriptions.ts` | Subscription queries | Subscription pages only | -| `features/orders/hooks/useOrders.ts` | Order queries | Order pages only | -| `features/auth/hooks/useAuth.ts` | Auth state & actions | Auth-related components | +| File | Purpose | Used By | +| -------------------------------------------------- | --------------------------- | ----------------------- | +| `features/billing/hooks/useBilling.ts` | Invoice queries & mutations | Billing pages only | +| `features/subscriptions/hooks/useSubscriptions.ts` | Subscription queries | Subscription pages only | +| `features/orders/hooks/useOrders.ts` | Order queries | Order pages only | +| `features/auth/hooks/useAuth.ts` | Auth state & actions | Auth-related components | --- @@ -94,6 +94,7 @@ lib/ ``` **Why this is bad:** + - Hard to find (is it in `lib` or `features`?) - Breaks feature encapsulation - Harder to delete features @@ -134,10 +135,7 @@ export function getDataOrThrow( return response.data; } -export function getDataOrDefault( - response: { data?: T; error?: unknown }, - defaultValue: T -): T { +export function getDataOrDefault(response: { data?: T; error?: unknown }, defaultValue: T): T { return response.data ?? defaultValue; } @@ -263,12 +261,12 @@ export function usePaymentMethods() { export function useCreateInvoiceSsoLink() { return useMutation({ - mutationFn: async ({ - invoiceId, - target - }: { - invoiceId: number; - target?: "view" | "download" | "pay" + mutationFn: async ({ + invoiceId, + target, + }: { + invoiceId: number; + target?: "view" | "download" | "pay"; }) => { const response = await apiClient.POST("/api/invoices/{id}/sso-link", { params: { @@ -284,7 +282,7 @@ export function useCreateInvoiceSsoLink() { export function useCreatePaymentMethodsSsoLink() { return useMutation({ mutationFn: async () => { - const response = await apiClient.POST("/auth/sso-link", { + const response = await apiClient.POST("/api/auth/sso-link", { body: { destination: "index.php?rp=/account/paymentmethods" }, }); return getDataOrThrow(response, "Failed to create payment methods SSO link"); @@ -342,7 +340,7 @@ import { useInvoices } from "@/features/billing/hooks/useBilling"; function InvoicesPage() { const { data: invoices, isLoading } = useInvoices({ status: "Unpaid" }); - + // ... } ``` @@ -354,11 +352,11 @@ function InvoicesPage() { import { apiClient, queryKeys, getDataOrThrow } from "@/lib/api"; // ✅ Import domain types -import { +import { type Invoice, type InvoiceList, invoiceSchema, - type InvoiceQueryParams + type InvoiceQueryParams, } from "@customer-portal/domain/billing"; export function useInvoices(params?: InvoiceQueryParams) { @@ -373,10 +371,10 @@ export function useInvoices(params?: InvoiceQueryParams) { import { apiClient, queryKeys, getDataOrThrow } from "@/lib/api"; // ✅ Domain types for subscriptions -import { - type Subscription, +import { + type Subscription, subscriptionSchema, - type SubscriptionQueryParams + type SubscriptionQueryParams, } from "@customer-portal/domain/subscriptions"; export function useSubscriptions(params?: SubscriptionQueryParams) { @@ -389,11 +387,13 @@ export function useSubscriptions(params?: SubscriptionQueryParams) { ## 📋 Benefits of This Structure ### 1. **Clear Separation of Concerns** + - `api/` - HTTP client & infrastructure - `hooks/` - React Query abstractions - `utils/` - Helper functions ### 2. **Clean Imports** + ```typescript // ❌ Before: Messy import { apiClient, queryKeys, getDataOrDefault, getDataOrThrow } from "@/lib/api"; @@ -413,12 +413,15 @@ import { ``` ### 3. **Easy to Find Things** + - Need a query hook? → `lib/hooks/queries/` - Need API utilities? → `lib/api/` - Need to update a domain type? → `packages/domain/billing/` ### 4. **Testable** + Each piece can be tested independently: + - API helpers are pure functions - Hooks can be tested with React Testing Library - Domain logic is already in domain package @@ -440,4 +443,3 @@ import { invoiceSchema } from "@customer-portal/domain/billing"; ``` Let me check if this old validation path exists and needs cleanup. - diff --git a/portal-backend.latest.tar.gz.sha256 b/portal-backend.latest.tar.gz.sha256 index e92d6615..cd90fb45 100644 --- a/portal-backend.latest.tar.gz.sha256 +++ b/portal-backend.latest.tar.gz.sha256 @@ -1 +1 @@ -b9e6a7c804df143f276ec06e4411004e08475923b35e8c29fb20495b1a637e61 /home/barsa/projects/customer_portal/customer-portal/portal-backend.latest.tar.gz +32dd63df821868464fa1df1f9de8966b40b38da7cc969a37aa0aee1ef9c83215 /home/barsa/projects/customer_portal/customer-portal/portal-backend.latest.tar.gz diff --git a/portal-frontend.latest.tar.gz.sha256 b/portal-frontend.latest.tar.gz.sha256 index 0dde62ab..e2140948 100644 --- a/portal-frontend.latest.tar.gz.sha256 +++ b/portal-frontend.latest.tar.gz.sha256 @@ -1 +1 @@ -d342327a541914cf92d768189597fb2323e1faf55d2eadfb56edc8cf5cec7a75 /home/barsa/projects/customer_portal/customer-portal/portal-frontend.latest.tar.gz +c4ee8a17de6dfad930a2d8d983b2cc5055e2e0baa1625da59f33398325ddaa36 /home/barsa/projects/customer_portal/customer-portal/portal-frontend.latest.tar.gz