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.
This commit is contained in:
barsa 2025-12-15 10:32:07 +09:00
parent b193361a72
commit 9764ccfbad
11 changed files with 66 additions and 43 deletions

1
.gitignore vendored
View File

@ -156,6 +156,7 @@ prisma/migrations/dev.db*
# Large archive files # Large archive files
*.tar *.tar
*.tar.gz *.tar.gz
*.tar.gz.sha256
*.zip *.zip
# API Documentation (contains sensitive API details) # API Documentation (contains sensitive API details)

View File

@ -1,6 +1,8 @@
"use client"; "use client";
import { useCallback } from "react"; import { useCallback } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/lib/api";
import { accountService } from "@/features/account/services/account.service"; import { accountService } from "@/features/account/services/account.service";
import { import {
addressFormSchema, addressFormSchema,
@ -10,10 +12,17 @@ import {
import { useZodForm } from "@/hooks/useZodForm"; import { useZodForm } from "@/hooks/useZodForm";
export function useAddressEdit(initial: AddressFormData) { export function useAddressEdit(initial: AddressFormData) {
const handleSave = useCallback(async (formData: AddressFormData) => { const queryClient = useQueryClient();
const requestData = addressFormToRequest(formData);
await accountService.updateAddress(requestData); 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({ return useZodForm({
schema: addressFormSchema, schema: addressFormSchema,

View File

@ -1,6 +1,8 @@
"use client"; "use client";
import { useEffect, useState, useCallback } from "react"; 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 { useAuthStore } from "@/features/auth/services/auth.store";
import { accountService } from "@/features/account/services/account.service"; import { accountService } from "@/features/account/services/account.service";
import { logger } from "@/lib/logger"; import { logger } from "@/lib/logger";
@ -10,6 +12,7 @@ import type { ProfileEditFormData, Address } from "@customer-portal/domain/custo
export function useProfileData() { export function useProfileData() {
const { user } = useAuthStore(); const { user } = useAuthStore();
const queryClient = useQueryClient();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isSavingProfile, setIsSavingProfile] = useState(false); const [isSavingProfile, setIsSavingProfile] = useState(false);
@ -112,6 +115,8 @@ export function useProfileData() {
phoneNumber: next.phoneNumber, phoneNumber: next.phoneNumber,
phoneCountryCode: next.phoneCountryCode, phoneCountryCode: next.phoneCountryCode,
}); });
// Address changes can affect server-personalized catalog results (eligibility).
await queryClient.invalidateQueries({ queryKey: queryKeys.catalog.all() });
setBillingInfo({ address: next }); setBillingInfo({ address: next });
setAddress(next); setAddress(next);
return true; return true;

View File

@ -6,11 +6,13 @@ import { Button } from "@/components/atoms/button";
import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { accountService } from "@/features/account/services/account.service"; import { accountService } from "@/features/account/services/account.service";
import { log } from "@/lib/logger"; import { log } from "@/lib/logger";
import { StatusPill } from "@/components/atoms/status-pill"; import { StatusPill } from "@/components/atoms/status-pill";
import { MapPinIcon, PencilIcon, CheckIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { MapPinIcon, PencilIcon, CheckIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { COUNTRY_OPTIONS, getCountryName } from "@/lib/constants/countries"; import { COUNTRY_OPTIONS, getCountryName } from "@/lib/constants/countries";
import { queryKeys } from "@/lib/api";
// Use canonical Address type from domain // Use canonical Address type from domain
import type { Address } from "@customer-portal/domain/customer"; import type { Address } from "@customer-portal/domain/customer";
@ -37,6 +39,7 @@ export function AddressConfirmation({
orderType, orderType,
embedded = false, embedded = false,
}: AddressConfirmationProps) { }: AddressConfirmationProps) {
const queryClient = useQueryClient();
const [billingInfo, setBillingInfo] = useState<BillingInfo | null>(null); const [billingInfo, setBillingInfo] = useState<BillingInfo | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
@ -152,6 +155,9 @@ export function AddressConfirmation({
// Persist to server (WHMCS via BFF) // Persist to server (WHMCS via BFF)
const updatedAddress = await accountService.updateAddress(sanitizedAddress); 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 // Rebuild BillingInfo from updated address
const updatedInfo: BillingInfo = { const updatedInfo: BillingInfo = {
company: null, company: null,

View File

@ -140,6 +140,7 @@ export const queryKeys = {
summary: () => ["dashboard", "summary"] as const, summary: () => ["dashboard", "summary"] as const,
}, },
catalog: { catalog: {
all: () => ["catalog"] as const,
products: () => ["catalog", "products"] as const, products: () => ["catalog", "products"] as const,
internet: { internet: {
combined: () => ["catalog", "internet", "combined"] as const, combined: () => ["catalog", "internet", "combined"] as const,

View File

@ -284,8 +284,7 @@ export function createClient(options: CreateClientOptions = {}): ApiClient {
opts: RequestOptions = {} opts: RequestOptions = {}
): Promise<ApiResponse<T>> => { ): Promise<ApiResponse<T>> => {
const resolvedPath = applyPathParams(path, opts.params?.path); const resolvedPath = applyPathParams(path, opts.params?.path);
const normalizedPath = normalizeApiPath(resolvedPath); const url = new URL(resolvedPath, baseUrl);
const url = new URL(normalizedPath, baseUrl);
const queryString = buildQueryString(opts.params?.query); const queryString = buildQueryString(opts.params?.query);
if (queryString) { if (queryString) {

View File

@ -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`. - Mapping is stored: `portalUserId ↔ whmcsClientId ↔ sfAccountId`.
2. Add payment method (required before checkout) 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. - Portal checks `GET /billing/payment-methods/summary` to confirm presence before enabling checkout.
3. Browse catalog and configure 3. Browse catalog and configure
@ -138,7 +138,7 @@ Implementation notes:
- `GET /billing/payment-methods/summary` (new) - `GET /billing/payment-methods/summary` (new)
- Returns `{ hasPaymentMethod: boolean }` using WHMCS `GetPayMethods` for mapped client. - 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. - Used to open WHMCS payment methods and invoice/pay pages.
### 2.5 Catalog (Salesforce Product2 as Source of Truth) ### 2.5 Catalog (Salesforce Product2 as Source of Truth)

View File

@ -30,7 +30,7 @@ This roadmap references `PORTAL-ORDERING-PROVISIONING.md` (complete flows and ar
5. Portal UI: Address & payment method 5. Portal UI: Address & payment method
- Address step after signup; `PATCH /api/me/address` to update address fields. - 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 ## Phase 3 Catalog

View File

@ -63,23 +63,23 @@ apps/portal/src/
### ✅ `lib/` - Truly Generic, Reusable Across Features ### ✅ `lib/` - Truly Generic, Reusable Across Features
| File | Purpose | Used By | | File | Purpose | Used By |
|------|---------|---------| | ----------------------------- | ------------------------------------ | -------------- |
| `lib/api/client.ts` | API client instance | All features | | `lib/api/client.ts` | API client instance | All features |
| `lib/api/query-keys.ts` | React Query keys factory | All features | | `lib/api/query-keys.ts` | React Query keys factory | All features |
| `lib/api/helpers.ts` | `getDataOrThrow`, `getDataOrDefault` | All features | | `lib/api/helpers.ts` | `getDataOrThrow`, `getDataOrDefault` | All features |
| `lib/utils/cn.ts` | Tailwind className merger | All components | | `lib/utils/cn.ts` | Tailwind className merger | All components |
| `lib/utils/error-handling.ts` | Generic error parsing | All features | | `lib/utils/error-handling.ts` | Generic error parsing | All features |
| `lib/providers.tsx` | Root providers (QueryClient, Theme) | App root | | `lib/providers.tsx` | Root providers (QueryClient, Theme) | App root |
### ✅ `features/*/hooks/` - Feature-Specific Hooks ### ✅ `features/*/hooks/` - Feature-Specific Hooks
| File | Purpose | Used By | | File | Purpose | Used By |
|------|---------|---------| | -------------------------------------------------- | --------------------------- | ----------------------- |
| `features/billing/hooks/useBilling.ts` | Invoice queries & mutations | Billing pages only | | `features/billing/hooks/useBilling.ts` | Invoice queries & mutations | Billing pages only |
| `features/subscriptions/hooks/useSubscriptions.ts` | Subscription queries | Subscription pages only | | `features/subscriptions/hooks/useSubscriptions.ts` | Subscription queries | Subscription pages only |
| `features/orders/hooks/useOrders.ts` | Order queries | Order pages only | | `features/orders/hooks/useOrders.ts` | Order queries | Order pages only |
| `features/auth/hooks/useAuth.ts` | Auth state & actions | Auth-related components | | `features/auth/hooks/useAuth.ts` | Auth state & actions | Auth-related components |
--- ---
@ -94,6 +94,7 @@ lib/
``` ```
**Why this is bad:** **Why this is bad:**
- Hard to find (is it in `lib` or `features`?) - Hard to find (is it in `lib` or `features`?)
- Breaks feature encapsulation - Breaks feature encapsulation
- Harder to delete features - Harder to delete features
@ -134,10 +135,7 @@ export function getDataOrThrow<T>(
return response.data; return response.data;
} }
export function getDataOrDefault<T>( export function getDataOrDefault<T>(response: { data?: T; error?: unknown }, defaultValue: T): T {
response: { data?: T; error?: unknown },
defaultValue: T
): T {
return response.data ?? defaultValue; return response.data ?? defaultValue;
} }
@ -263,12 +261,12 @@ export function usePaymentMethods() {
export function useCreateInvoiceSsoLink() { export function useCreateInvoiceSsoLink() {
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({
invoiceId, invoiceId,
target target,
}: { }: {
invoiceId: number; invoiceId: number;
target?: "view" | "download" | "pay" target?: "view" | "download" | "pay";
}) => { }) => {
const response = await apiClient.POST<InvoiceSsoLink>("/api/invoices/{id}/sso-link", { const response = await apiClient.POST<InvoiceSsoLink>("/api/invoices/{id}/sso-link", {
params: { params: {
@ -284,7 +282,7 @@ export function useCreateInvoiceSsoLink() {
export function useCreatePaymentMethodsSsoLink() { export function useCreatePaymentMethodsSsoLink() {
return useMutation({ return useMutation({
mutationFn: async () => { mutationFn: async () => {
const response = await apiClient.POST<InvoiceSsoLink>("/auth/sso-link", { const response = await apiClient.POST<InvoiceSsoLink>("/api/auth/sso-link", {
body: { destination: "index.php?rp=/account/paymentmethods" }, body: { destination: "index.php?rp=/account/paymentmethods" },
}); });
return getDataOrThrow(response, "Failed to create payment methods SSO link"); return getDataOrThrow(response, "Failed to create payment methods SSO link");
@ -342,7 +340,7 @@ import { useInvoices } from "@/features/billing/hooks/useBilling";
function InvoicesPage() { function InvoicesPage() {
const { data: invoices, isLoading } = useInvoices({ status: "Unpaid" }); const { data: invoices, isLoading } = useInvoices({ status: "Unpaid" });
// ... // ...
} }
``` ```
@ -354,11 +352,11 @@ function InvoicesPage() {
import { apiClient, queryKeys, getDataOrThrow } from "@/lib/api"; import { apiClient, queryKeys, getDataOrThrow } from "@/lib/api";
// ✅ Import domain types // ✅ Import domain types
import { import {
type Invoice, type Invoice,
type InvoiceList, type InvoiceList,
invoiceSchema, invoiceSchema,
type InvoiceQueryParams type InvoiceQueryParams,
} from "@customer-portal/domain/billing"; } from "@customer-portal/domain/billing";
export function useInvoices(params?: InvoiceQueryParams) { export function useInvoices(params?: InvoiceQueryParams) {
@ -373,10 +371,10 @@ export function useInvoices(params?: InvoiceQueryParams) {
import { apiClient, queryKeys, getDataOrThrow } from "@/lib/api"; import { apiClient, queryKeys, getDataOrThrow } from "@/lib/api";
// ✅ Domain types for subscriptions // ✅ Domain types for subscriptions
import { import {
type Subscription, type Subscription,
subscriptionSchema, subscriptionSchema,
type SubscriptionQueryParams type SubscriptionQueryParams,
} from "@customer-portal/domain/subscriptions"; } from "@customer-portal/domain/subscriptions";
export function useSubscriptions(params?: SubscriptionQueryParams) { export function useSubscriptions(params?: SubscriptionQueryParams) {
@ -389,11 +387,13 @@ export function useSubscriptions(params?: SubscriptionQueryParams) {
## 📋 Benefits of This Structure ## 📋 Benefits of This Structure
### 1. **Clear Separation of Concerns** ### 1. **Clear Separation of Concerns**
- `api/` - HTTP client & infrastructure - `api/` - HTTP client & infrastructure
- `hooks/` - React Query abstractions - `hooks/` - React Query abstractions
- `utils/` - Helper functions - `utils/` - Helper functions
### 2. **Clean Imports** ### 2. **Clean Imports**
```typescript ```typescript
// ❌ Before: Messy // ❌ Before: Messy
import { apiClient, queryKeys, getDataOrDefault, getDataOrThrow } from "@/lib/api"; import { apiClient, queryKeys, getDataOrDefault, getDataOrThrow } from "@/lib/api";
@ -413,12 +413,15 @@ import {
``` ```
### 3. **Easy to Find Things** ### 3. **Easy to Find Things**
- Need a query hook? → `lib/hooks/queries/` - Need a query hook? → `lib/hooks/queries/`
- Need API utilities? → `lib/api/` - Need API utilities? → `lib/api/`
- Need to update a domain type? → `packages/domain/billing/` - Need to update a domain type? → `packages/domain/billing/`
### 4. **Testable** ### 4. **Testable**
Each piece can be tested independently: Each piece can be tested independently:
- API helpers are pure functions - API helpers are pure functions
- Hooks can be tested with React Testing Library - Hooks can be tested with React Testing Library
- Domain logic is already in domain package - 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. Let me check if this old validation path exists and needs cleanup.

View File

@ -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

View File

@ -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