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
|
||||
*.tar
|
||||
*.tar.gz
|
||||
*.tar.gz.sha256
|
||||
*.zip
|
||||
|
||||
# API Documentation (contains sensitive API details)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<string | null>(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;
|
||||
|
||||
@ -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<BillingInfo | null>(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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -284,8 +284,7 @@ export function createClient(options: CreateClientOptions = {}): ApiClient {
|
||||
opts: RequestOptions = {}
|
||||
): Promise<ApiResponse<T>> => {
|
||||
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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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<T>(
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export function getDataOrDefault<T>(
|
||||
response: { data?: T; error?: unknown },
|
||||
defaultValue: T
|
||||
): T {
|
||||
export function getDataOrDefault<T>(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<InvoiceSsoLink>("/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<InvoiceSsoLink>("/auth/sso-link", {
|
||||
const response = await apiClient.POST<InvoiceSsoLink>("/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.
|
||||
|
||||
|
||||
@ -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