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:
parent
b193361a72
commit
9764ccfbad
1
.gitignore
vendored
1
.gitignore
vendored
@ -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)
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user